diff options
Diffstat (limited to 'toolkit')
1279 files changed, 45129 insertions, 14647 deletions
diff --git a/toolkit/actors/AutoCompleteChild.sys.mjs b/toolkit/actors/AutoCompleteChild.sys.mjs index 4cd9b9f3de..3694e497d5 100644 --- a/toolkit/actors/AutoCompleteChild.sys.mjs +++ b/toolkit/actors/AutoCompleteChild.sys.mjs @@ -33,7 +33,7 @@ export class AutoCompleteChild extends JSWindowActorChild { receiveMessage(message) { switch (message.name) { - case "FormAutoComplete:HandleEnter": { + case "AutoComplete:HandleEnter": { this.selectedIndex = message.data.selectedIndex; let controller = Cc[ @@ -43,22 +43,22 @@ export class AutoCompleteChild extends JSWindowActorChild { break; } - case "FormAutoComplete:PopupClosed": { + case "AutoComplete:PopupClosed": { this._popupOpen = false; this.notifyListeners(message.name, message.data); break; } - case "FormAutoComplete:PopupOpened": { + case "AutoComplete:PopupOpened": { this._popupOpen = true; this.notifyListeners(message.name, message.data); break; } - case "FormAutoComplete:Focus": { + case "AutoComplete:Focus": { // XXX See bug 1582722 // Before bug 1573836, the messages here didn't match - // ("FormAutoComplete:Focus" versus "FormAutoComplete:RequestFocus") + // ("AutoComplete:Focus" versus "AutoComplete:RequestFocus") // so this was never called. However this._input is actually a // nsIAutoCompleteInput, which doesn't have a focus() method, so it // wouldn't have worked anyway. So for now, I have just disabled this. @@ -87,7 +87,7 @@ export class AutoCompleteChild extends JSWindowActorChild { } set selectedIndex(index) { - this.sendAsyncMessage("FormAutoComplete:SetSelectedIndex", { index }); + this.sendAsyncMessage("AutoComplete:SetSelectedIndex", { index }); } get selectedIndex() { @@ -98,7 +98,7 @@ export class AutoCompleteChild extends JSWindowActorChild { // selectedIndex is trivial to catch (e.g. moving the mouse over the // list). let selectedIndexResult = Services.cpmm.sendSyncMessage( - "FormAutoComplete:GetSelectedIndex", + "AutoComplete:GetSelectedIndex", { browsingContext: this.browsingContext, } @@ -131,7 +131,7 @@ export class AutoCompleteChild extends JSWindowActorChild { ); let inputElementIdentifier = lazy.ContentDOMReference.get(element); - this.sendAsyncMessage("FormAutoComplete:MaybeOpenPopup", { + this.sendAsyncMessage("AutoComplete:MaybeOpenPopup", { results, rect, dir, @@ -148,18 +148,18 @@ export class AutoCompleteChild extends JSWindowActorChild { // up in a state where the content thinks that a popup // is open when it isn't (or soon won't be). this._popupOpen = false; - this.sendAsyncMessage("FormAutoComplete:ClosePopup", {}); + this.sendAsyncMessage("AutoComplete:ClosePopup", {}); } invalidate() { if (this._popupOpen) { let results = this.getResultsFromController(this._input); - this.sendAsyncMessage("FormAutoComplete:Invalidate", { results }); + this.sendAsyncMessage("AutoComplete:Invalidate", { results }); } } selectBy(reverse, page) { - Services.cpmm.sendSyncMessage("FormAutoComplete:SelectBy", { + Services.cpmm.sendSyncMessage("AutoComplete:SelectBy", { browsingContext: this.browsingContext, reverse, page, diff --git a/toolkit/actors/AutoCompleteParent.sys.mjs b/toolkit/actors/AutoCompleteParent.sys.mjs index 611677a84f..70cbcea44d 100644 --- a/toolkit/actors/AutoCompleteParent.sys.mjs +++ b/toolkit/actors/AutoCompleteParent.sys.mjs @@ -41,21 +41,18 @@ function compareContext(message) { // The browsingContext within the message data is either the one that has // the active autocomplete popup or the top-level of the one that has // the active autocomplete popup. -Services.ppmm.addMessageListener( - "FormAutoComplete:GetSelectedIndex", - message => { - if (compareContext(message)) { - let actor = currentActor; - if (actor && actor.openedPopup) { - return actor.openedPopup.selectedIndex; - } +Services.ppmm.addMessageListener("AutoComplete:GetSelectedIndex", message => { + if (compareContext(message)) { + let actor = currentActor; + if (actor && actor.openedPopup) { + return actor.openedPopup.selectedIndex; } - - return -1; } -); -Services.ppmm.addMessageListener("FormAutoComplete:SelectBy", message => { + return -1; +}); + +Services.ppmm.addMessageListener("AutoComplete:SelectBy", message => { if (compareContext(message)) { let actor = currentActor; if (actor && actor.openedPopup) { @@ -174,7 +171,7 @@ export class AutoCompleteParent extends JSWindowActorParent { handleEvent(evt) { switch (evt.type) { case "popupshowing": { - this.sendAsyncMessage("FormAutoComplete:PopupOpened", {}); + this.sendAsyncMessage("AutoComplete:PopupOpened", {}); break; } @@ -188,7 +185,7 @@ export class AutoCompleteParent extends JSWindowActorParent { selectedIndex != -1 ? AutoCompleteResultView.getStyleAt(selectedIndex) : ""; - this.sendAsyncMessage("FormAutoComplete:PopupClosed", { + this.sendAsyncMessage("AutoComplete:PopupClosed", { selectedRowComment, selectedRowStyle, }); @@ -250,7 +247,7 @@ export class AutoCompleteParent extends JSWindowActorParent { // the scrollbar in login or form autofill popups. if ( resultStyles.size && - (resultStyles.has("autofill-profile") || resultStyles.has("loginsFooter")) + (resultStyles.has("autofill") || resultStyles.has("loginsFooter")) ) { this.openedPopup._normalMaxRows = this.openedPopup.maxRows; this.openedPopup.mInput.maxRows = 10; @@ -397,7 +394,7 @@ export class AutoCompleteParent extends JSWindowActorParent { } switch (message.name) { - case "FormAutoComplete:SetSelectedIndex": { + case "AutoComplete:SetSelectedIndex": { let { index } = message.data; if (this.openedPopup) { this.openedPopup.selectedIndex = index; @@ -405,7 +402,7 @@ export class AutoCompleteParent extends JSWindowActorParent { break; } - case "FormAutoComplete:MaybeOpenPopup": { + case "AutoComplete:MaybeOpenPopup": { let { results, rect, dir, inputElementIdentifier, formOrigin } = message.data; if (lazy.DELEGATE_AUTOCOMPLETE) { @@ -422,13 +419,13 @@ export class AutoCompleteParent extends JSWindowActorParent { break; } - case "FormAutoComplete:Invalidate": { + case "AutoComplete:Invalidate": { let { results } = message.data; this.invalidate(results); break; } - case "FormAutoComplete:ClosePopup": { + case "AutoComplete:ClosePopup": { if (lazy.DELEGATE_AUTOCOMPLETE) { lazy.GeckoViewAutocomplete.delegateDismiss(); break; @@ -489,7 +486,7 @@ export class AutoCompleteParent extends JSWindowActorParent { */ handleEnter(aIsPopupSelection) { if (this.openedPopup) { - this.sendAsyncMessage("FormAutoComplete:HandleEnter", { + this.sendAsyncMessage("AutoComplete:HandleEnter", { selectedIndex: this.openedPopup.selectedIndex, isPopupSelection: aIsPopupSelection, }); @@ -507,7 +504,7 @@ export class AutoCompleteParent extends JSWindowActorParent { // disabled. /* if (this.openedPopup) { - this.sendAsyncMessage("FormAutoComplete:Focus"); + this.sendAsyncMessage("AutoComplete:Focus"); } */ } diff --git a/toolkit/actors/AutoplayChild.sys.mjs b/toolkit/actors/AutoplayChild.sys.mjs index 87fa966cb1..42bff87ea1 100644 --- a/toolkit/actors/AutoplayChild.sys.mjs +++ b/toolkit/actors/AutoplayChild.sys.mjs @@ -4,7 +4,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ export class AutoplayChild extends JSWindowActorChild { - handleEvent(event) { + handleEvent() { this.sendAsyncMessage("GloballyAutoplayBlocked", {}); } } diff --git a/toolkit/actors/AutoplayParent.sys.mjs b/toolkit/actors/AutoplayParent.sys.mjs index 3e9f807b3a..7b33c49c7f 100644 --- a/toolkit/actors/AutoplayParent.sys.mjs +++ b/toolkit/actors/AutoplayParent.sys.mjs @@ -4,7 +4,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ export class AutoplayParent extends JSWindowActorParent { - receiveMessage(aMessage) { + receiveMessage() { let topBrowsingContext = this.manager.browsingContext.top; let browser = topBrowsingContext.embedderElement; let document = browser.ownerDocument; diff --git a/toolkit/actors/ControllersParent.sys.mjs b/toolkit/actors/ControllersParent.sys.mjs index 05ea166112..8461d76ba2 100644 --- a/toolkit/actors/ControllersParent.sys.mjs +++ b/toolkit/actors/ControllersParent.sys.mjs @@ -47,7 +47,7 @@ export class ControllersParent extends JSWindowActorParent { this.sendAsyncMessage("ControllerCommands:Do", aCommand); } - getCommandStateWithParams(aCommand, aCommandParams) { + getCommandStateWithParams() { throw Components.Exception("Not implemented", Cr.NS_ERROR_NOT_IMPLEMENTED); } diff --git a/toolkit/actors/NetErrorParent.sys.mjs b/toolkit/actors/NetErrorParent.sys.mjs index ac0dbf49a0..071529976a 100644 --- a/toolkit/actors/NetErrorParent.sys.mjs +++ b/toolkit/actors/NetErrorParent.sys.mjs @@ -33,7 +33,7 @@ class CaptivePortalObserver { Services.obs.removeObserver(this, "captive-portal-login-success"); } - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { switch (aTopic) { case "captive-portal-login-abort": case "captive-portal-login-success": @@ -172,7 +172,7 @@ export class NetErrorParent extends JSWindowActorParent { request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; - request.addEventListener("error", event => { + request.addEventListener("error", () => { // Make sure the user is still on the cert error page. if (!browser.documentURI.spec.startsWith("about:certerror")) { return; diff --git a/toolkit/actors/PictureInPictureChild.sys.mjs b/toolkit/actors/PictureInPictureChild.sys.mjs index 2a415df32d..bffcb55e6d 100644 --- a/toolkit/actors/PictureInPictureChild.sys.mjs +++ b/toolkit/actors/PictureInPictureChild.sys.mjs @@ -947,11 +947,8 @@ export class PictureInPictureToggleChild extends JSWindowActorChild { * tear out or in. If we happened to be tracking videos before the tear * occurred, we re-add the mouse event listeners so that they're attached to * the right WindowRoot. - * - * @param {Event} event The pageshow event fired when completing a tab tear - * out or in. */ - onPageShow(event) { + onPageShow() { let state = this.docState; if (state.isTrackingVideos) { this.addMouseButtonListeners(); @@ -963,11 +960,8 @@ export class PictureInPictureToggleChild extends JSWindowActorChild { * tear out or in. If we happened to be tracking videos before the tear * occurred, we remove the mouse event listeners. We'll re-add them when the * pageshow event fires. - * - * @param {Event} event The pagehide event fired when starting a tab tear - * out or in. */ - onPageHide(event) { + onPageHide() { let state = this.docState; if (state.isTrackingVideos) { this.removeMouseButtonListeners(); @@ -1049,7 +1043,7 @@ export class PictureInPictureToggleChild extends JSWindowActorChild { } } - startPictureInPicture(event, video, toggle) { + startPictureInPicture(event, video) { Services.telemetry.keyedScalarAdd( "pictureinpicture.opened_method", "toggle", @@ -2445,7 +2439,7 @@ export class PictureInPictureChild extends JSWindowActorChild { } } - onCueChange(e) { + onCueChange() { if (!lazy.DISPLAY_TEXT_TRACKS_PREF) { this.updateWebVTTTextTracksDisplay(null); } else { @@ -3110,10 +3104,10 @@ class PictureInPictureChildVideoWrapper { * a cue change is triggered {@see updatePiPTextTracks()}. * @param {HTMLVideoElement} video * The originating video source element - * @param {Function} callback + * @param {Function} _callback * The callback function to be executed when cue changes are detected */ - setCaptionContainerObserver(video, callback) { + setCaptionContainerObserver(video, _callback) { return this.#callWrapperMethod({ name: "setCaptionContainerObserver", args: [ diff --git a/toolkit/actors/PrintingChild.sys.mjs b/toolkit/actors/PrintingChild.sys.mjs index 4fca3ab403..182784cfe7 100644 --- a/toolkit/actors/PrintingChild.sys.mjs +++ b/toolkit/actors/PrintingChild.sys.mjs @@ -94,7 +94,7 @@ export class PrintingChild extends JSWindowActorChild { // will wait for MozAfterPaint event to be fired. let actor = thisWindow.windowGlobalChild.getActor("Printing"); let webProgressListener = { - onStateChange(webProgress, req, flags, status) { + onStateChange(webProgress, req, flags) { if (flags & Ci.nsIWebProgressListener.STATE_STOP) { webProgress.removeProgressListener(webProgressListener); let domUtils = contentWindow.windowUtils; diff --git a/toolkit/actors/SelectChild.sys.mjs b/toolkit/actors/SelectChild.sys.mjs index 1bc80001ae..9901f72648 100644 --- a/toolkit/actors/SelectChild.sys.mjs +++ b/toolkit/actors/SelectChild.sys.mjs @@ -70,7 +70,7 @@ SelectContentHelper.prototype = { mozSystemGroup: true, }); let MutationObserver = this.element.ownerGlobal.MutationObserver; - this.mut = new MutationObserver(mutations => { + this.mut = new MutationObserver(() => { // Something changed the <select> while it was open, so // we'll poke a DeferredTask to update the parent sometime // in the very near future. diff --git a/toolkit/actors/SelectParent.sys.mjs b/toolkit/actors/SelectParent.sys.mjs index 5382b35ab3..fa8ce002f0 100644 --- a/toolkit/actors/SelectParent.sys.mjs +++ b/toolkit/actors/SelectParent.sys.mjs @@ -670,8 +670,9 @@ export var SelectParentHelper = { if (!currentItem.hiddenByContent) { // Get label and tooltip (title) from option and change to // lower case for comparison - let itemLabel = currentItem.getAttribute("label").toLowerCase(); - let itemTooltip = currentItem.getAttribute("title").toLowerCase(); + let itemLabel = currentItem.getAttribute("label")?.toLowerCase() || ""; + let itemTooltip = + currentItem.getAttribute("title")?.toLowerCase() || ""; // If search input is empty, all options should be shown if (!input) { diff --git a/toolkit/actors/UserCharacteristicsChild.sys.mjs b/toolkit/actors/UserCharacteristicsChild.sys.mjs new file mode 100644 index 0000000000..da70c428e6 --- /dev/null +++ b/toolkit/actors/UserCharacteristicsChild.sys.mjs @@ -0,0 +1,28 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +const lazy = {}; + +ChromeUtils.defineLazyGetter(lazy, "console", () => { + return console.createInstance({ + prefix: "UserCharacteristicsPage", + maxLogLevelPref: "toolkit.telemetry.user_characteristics_ping.logLevel", + }); +}); + +export class UserCharacteristicsChild extends JSWindowActorChild { + handleEvent(event) { + lazy.console.debug("Got ", event.type); + switch (event.type) { + case "UserCharacteristicsDataDone": + lazy.console.debug("creating IdleDispatch"); + ChromeUtils.idleDispatch(() => { + lazy.console.debug("sending PageReady"); + this.sendAsyncMessage("UserCharacteristics::PageReady", event.detail); + }); + break; + } + } +} diff --git a/toolkit/actors/UserCharacteristicsParent.sys.mjs b/toolkit/actors/UserCharacteristicsParent.sys.mjs new file mode 100644 index 0000000000..87405ebf4d --- /dev/null +++ b/toolkit/actors/UserCharacteristicsParent.sys.mjs @@ -0,0 +1,34 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; +ChromeUtils.defineLazyGetter(lazy, "console", () => { + return console.createInstance({ + prefix: "UserCharacteristicsPage", + maxLogLevelPref: "toolkit.telemetry.user_characteristics_ping.logLevel", + }); +}); + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "UserCharacteristicsPageService", + "@mozilla.org/user-characteristics-page;1", + "nsIUserCharacteristicsPageService" +); + +export class UserCharacteristicsParent extends JSWindowActorParent { + receiveMessage(aMessage) { + lazy.console.debug("Got ", aMessage.name); + if (aMessage.name == "UserCharacteristics::PageReady") { + lazy.console.debug("Got pageReady"); + lazy.UserCharacteristicsPageService.pageLoaded( + this.browsingContext, + aMessage.data + ); + } + } +} diff --git a/toolkit/actors/ViewSourcePageChild.sys.mjs b/toolkit/actors/ViewSourcePageChild.sys.mjs index d5c2ba46cd..ff8a046327 100644 --- a/toolkit/actors/ViewSourcePageChild.sys.mjs +++ b/toolkit/actors/ViewSourcePageChild.sys.mjs @@ -125,7 +125,7 @@ export class ViewSourcePageChild extends JSWindowActorChild { * @param event * The pageshow event being handled. */ - onPageShow(event) { + onPageShow() { // If we need to draw the selection, wait until an actual view source page // has loaded, instead of about:blank. if ( diff --git a/toolkit/actors/moz.build b/toolkit/actors/moz.build index 8e2d8a5efc..d0fda6c42f 100644 --- a/toolkit/actors/moz.build +++ b/toolkit/actors/moz.build @@ -75,6 +75,8 @@ FINAL_TARGET_FILES.actors += [ "UAWidgetsChild.sys.mjs", "UnselectedTabHoverChild.sys.mjs", "UnselectedTabHoverParent.sys.mjs", + "UserCharacteristicsChild.sys.mjs", + "UserCharacteristicsParent.sys.mjs", "ViewSourceChild.sys.mjs", "ViewSourcePageChild.sys.mjs", "ViewSourcePageParent.sys.mjs", diff --git a/toolkit/components/aboutconfig/content/aboutconfig.js b/toolkit/components/aboutconfig/content/aboutconfig.js index 5e67b764b5..ef4238975b 100644 --- a/toolkit/components/aboutconfig/content/aboutconfig.js +++ b/toolkit/components/aboutconfig/content/aboutconfig.js @@ -509,7 +509,7 @@ function loadPrefs() { }); }); - showAll.addEventListener("click", event => { + showAll.addEventListener("click", () => { search.focus(); search.value = ""; gFilterPrefsTask.disarm(); diff --git a/toolkit/components/aboutconfig/test/browser/browser.toml b/toolkit/components/aboutconfig/test/browser/browser.toml index a0cce686b8..373d51cf06 100644 --- a/toolkit/components/aboutconfig/test/browser/browser.toml +++ b/toolkit/components/aboutconfig/test/browser/browser.toml @@ -13,7 +13,6 @@ support-files = ["head.js"] ["browser_clipboard.js"] ["browser_edit.js"] -fail-if = ["a11y_checks"] # Bugs 1854447 and 1882380 span may not be focusable skip-if = ["os == 'linux' && ccov"] # Bug 1613515, the test consistently times out on Linux coverage builds. ["browser_locked.js"] diff --git a/toolkit/components/aboutmemory/content/aboutMemory.js b/toolkit/components/aboutmemory/content/aboutMemory.js index 049818263f..6b6f2e637c 100644 --- a/toolkit/components/aboutmemory/content/aboutMemory.js +++ b/toolkit/components/aboutmemory/content/aboutMemory.js @@ -608,7 +608,7 @@ function dumpGCLogAndCCLog(aVerbose) { ); let section = appendElement(gMain, "div", "section"); - function displayInfo(aGCLog, aCCLog, aIsParent) { + function displayInfo(aGCLog, aCCLog) { appendElementWithText(section, "div", "", "Saved GC log to " + aGCLog.path); let ccLogType = aVerbose ? "verbose" : "concise"; @@ -824,7 +824,7 @@ function loadMemoryReportsFromFile(aFilename, aTitleNote, aFn) { "uncompressed", { data: [], - onStartRequest(aR, aC) {}, + onStartRequest() {}, onDataAvailable(aR, aStream, aO, aCount) { let bi = new nsBinaryStream(aStream); this.data.push(bi.readBytes(aCount)); diff --git a/toolkit/components/aboutmemory/tests/test_aboutmemory.xhtml b/toolkit/components/aboutmemory/tests/test_aboutmemory.xhtml index 4617a019ad..a1a712dd0a 100644 --- a/toolkit/components/aboutmemory/tests/test_aboutmemory.xhtml +++ b/toolkit/components/aboutmemory/tests/test_aboutmemory.xhtml @@ -40,7 +40,7 @@ const PERCENTAGE = Ci.nsIMemoryReporter.UNITS_PERCENTAGE; let fakeReporters = [ - { collectReports(aCbObj, aClosure, aAnonymize) { + { collectReports(aCbObj, aClosure) { function f(aP, aK, aU, aA) { aCbObj.callback("Main Process", aP, aK, aU, aA, "Desc.", aClosure); } @@ -77,7 +77,7 @@ f("compartments/system/foo", OTHER, COUNT, 1); } }, - { collectReports(aCbObj, aClosure, aAnonymize) { + { collectReports(aCbObj, aClosure) { function f(aP, aK, aU, aA) { aCbObj.callback("Main Process", aP, aK, aU, aA, "Desc.", aClosure); } @@ -92,7 +92,7 @@ f("explicit/f/g/h/j", HEAP, BYTES, 10 * MB); } }, - { collectReports(aCbObj, aClosure, aAnonymize) { + { collectReports(aCbObj, aClosure) { function f(aP, aK, aU, aA) { aCbObj.callback("Main Process", aP, aK, aU, aA, "Desc.", aClosure); } @@ -103,7 +103,7 @@ f("compartments/user/https:\\\\very-long-url.com\\very-long\\oh-so-long\\really-quite-long.html?a=2&b=3&c=4&d=5&e=abcdefghijklmnopqrstuvwxyz&f=123456789123456789123456789", OTHER, COUNT, 1); } }, - { collectReports(aCbObj, aClosure, aAnonymize) { + { collectReports(aCbObj, aClosure) { function f(aP) { aCbObj.callback("Main Process", aP, OTHER, COUNT, 1, "Desc.", aClosure); } @@ -121,7 +121,7 @@ // the largest). Processes without a |resident| memory reporter are saved // for the end. let fakeReporters2 = [ - { collectReports(aCbObj, aClosure, aAnonymize) { + { collectReports(aCbObj, aClosure) { function f(aP1, aP2, aK, aU, aA) { aCbObj.callback(aP1, aP2, aK, aU, aA, "Desc.", aClosure); } diff --git a/toolkit/components/aboutmemory/tests/test_aboutmemory2.xhtml b/toolkit/components/aboutmemory/tests/test_aboutmemory2.xhtml index 28c8f7fa4f..f5c3885959 100644 --- a/toolkit/components/aboutmemory/tests/test_aboutmemory2.xhtml +++ b/toolkit/components/aboutmemory/tests/test_aboutmemory2.xhtml @@ -36,7 +36,7 @@ let jk2Path = "explicit/j/k2"; let fakeReporters = [ - { collectReports(aCbObj, aClosure, aAnonymize) { + { collectReports(aCbObj, aClosure) { function f(aP, aK, aA) { aCbObj.callback("Main Process ABC", aP, aK, BYTES, aA, "Desc.", aClosure); } diff --git a/toolkit/components/aboutmemory/tests/test_aboutmemory3.xhtml b/toolkit/components/aboutmemory/tests/test_aboutmemory3.xhtml index 1391af38b7..7292cbf252 100644 --- a/toolkit/components/aboutmemory/tests/test_aboutmemory3.xhtml +++ b/toolkit/components/aboutmemory/tests/test_aboutmemory3.xhtml @@ -31,7 +31,7 @@ const BYTES = Ci.nsIMemoryReporter.UNITS_BYTES; let fakeReporters = [ - { collectReports(aCbObj, aClosure, aAnonymize) { + { collectReports(aCbObj, aClosure) { function f(aP, aK, aA, aD) { aCbObj.callback("", aP, aK, BYTES, aA, aD, aClosure); } diff --git a/toolkit/components/aboutmemory/tests/test_aboutmemory4.xhtml b/toolkit/components/aboutmemory/tests/test_aboutmemory4.xhtml index 9d9db694f2..1fcdb86406 100644 --- a/toolkit/components/aboutmemory/tests/test_aboutmemory4.xhtml +++ b/toolkit/components/aboutmemory/tests/test_aboutmemory4.xhtml @@ -35,7 +35,7 @@ frame.height = 300; frame.src = "about:memory?file=" + makePathname(aFilename); document.documentElement.appendChild(frame); - frame.addEventListener("load", function onFrameLoad(e) { + frame.addEventListener("load", function onFrameLoad() { frame.focus(); // Initialize the clipboard contents. diff --git a/toolkit/components/aboutmemory/tests/test_aboutmemory7.xhtml b/toolkit/components/aboutmemory/tests/test_aboutmemory7.xhtml index 28872cd516..a31eb6e6b4 100644 --- a/toolkit/components/aboutmemory/tests/test_aboutmemory7.xhtml +++ b/toolkit/components/aboutmemory/tests/test_aboutmemory7.xhtml @@ -30,7 +30,7 @@ const BYTES = Ci.nsIMemoryReporter.UNITS_BYTES; let fakeReporters = [ - { collectReports(aCbObj, aClosure, aAnonymize) { + { collectReports(aCbObj, aClosure) { function f(aP, aK, aA) { aCbObj.callback("Main Process", aP, aK, BYTES, aA, "Desc.", aClosure); } diff --git a/toolkit/components/aboutmemory/tests/test_memoryReporters.xhtml b/toolkit/components/aboutmemory/tests/test_memoryReporters.xhtml index c97981258e..bc715b6778 100644 --- a/toolkit/components/aboutmemory/tests/test_memoryReporters.xhtml +++ b/toolkit/components/aboutmemory/tests/test_memoryReporters.xhtml @@ -77,8 +77,7 @@ let mySandbox = Cu.Sandbox(document.nodePrincipal, { sandboxName: "this-is-a-sandbox-name" }); - function handleReportNormal(aProcess, aPath, aKind, aUnits, aAmount, - aDescription) + function handleReportNormal(aProcess, aPath, aKind, aUnits, aAmount) { if (aProcess.startsWith(`Utility `)) { // The Utility process that runs the ORB JavaScript validator starts on first @@ -128,8 +127,7 @@ } } - function handleReportAnonymized(aProcess, aPath, aKind, aUnits, aAmount, - aDescription) + function handleReportAnonymized(aProcess, aPath) { // Path might include an xmlns using http, which is safe to ignore. let reducedPath = aPath.replace(XUL_NS, ""); @@ -331,7 +329,7 @@ } }, // nsIMemoryReporter - collectReports(callback, data, anonymize) { + collectReports(callback, data) { for (let path of Object.keys(this.tests)) { try { let test = this.tests[path]; @@ -350,7 +348,7 @@ } }, // nsIHandleReportCallback - callback(process, path, kind, units, amount, data) { + callback(process, path, kind, units, amount) { if (path in this.tests) { this.seen++; let test = this.tests[path]; diff --git a/toolkit/components/aboutmemory/tests/test_memoryReporters2.xhtml b/toolkit/components/aboutmemory/tests/test_memoryReporters2.xhtml index b7251c4e62..8dc7567109 100644 --- a/toolkit/components/aboutmemory/tests/test_memoryReporters2.xhtml +++ b/toolkit/components/aboutmemory/tests/test_memoryReporters2.xhtml @@ -55,7 +55,7 @@ { let residents = {}; - let handleReport = function(aProcess, aPath, aKind, aUnits, aAmount, aDesc) { + let handleReport = function(aProcess, aPath, aKind, aUnits, aAmount) { if (aProcess.startsWith(`Utility `)) { // The Utility process that runs the ORB JavaScript validator starts on first // idle in the parent process. This makes it notoriously hard to know _if_ it diff --git a/toolkit/components/aboutmemory/tests/xpcshell/test_gpuprocess.js b/toolkit/components/aboutmemory/tests/xpcshell/test_gpuprocess.js index 4a77752b17..764f5a1302 100644 --- a/toolkit/components/aboutmemory/tests/xpcshell/test_gpuprocess.js +++ b/toolkit/components/aboutmemory/tests/xpcshell/test_gpuprocess.js @@ -17,14 +17,7 @@ function run_test() { }; let foundGPUProcess = false; - let onHandleReport = function ( - aProcess, - aPath, - aKind, - aUnits, - aAmount, - aDescription - ) { + let onHandleReport = function (aProcess) { if (/GPU \(pid \d+\)/.test(aProcess)) { foundGPUProcess = true; } diff --git a/toolkit/components/aboutprocesses/content/aboutProcesses.js b/toolkit/components/aboutprocesses/content/aboutProcesses.js index 4ad00b2840..3d80512407 100644 --- a/toolkit/components/aboutprocesses/content/aboutProcesses.js +++ b/toolkit/components/aboutprocesses/content/aboutProcesses.js @@ -787,7 +787,7 @@ var View = { return isOpen; }, - displayDOMWindowRow(data, parent) { + displayDOMWindowRow(data) { const cellCount = 2; let rowId = "w:" + data.outerWindowId; let row = this._getOrCreateRow(rowId, cellCount); @@ -1124,7 +1124,7 @@ var Control = { // Visibility change: // - stop updating while the user isn't looking; // - resume updating when the user returns. - window.addEventListener("visibilitychange", event => { + window.addEventListener("visibilitychange", () => { if (!document.hidden) { this._updateDisplay(true); } diff --git a/toolkit/components/aboutthirdparty/nsIAboutThirdParty.idl b/toolkit/components/aboutthirdparty/nsIAboutThirdParty.idl index ef5590465a..68e41843d0 100644 --- a/toolkit/components/aboutthirdparty/nsIAboutThirdParty.idl +++ b/toolkit/components/aboutthirdparty/nsIAboutThirdParty.idl @@ -39,12 +39,12 @@ interface nsIAboutThirdParty : nsISupports /** * Returns true if DynamicBlocklist is available. */ - readonly attribute bool isDynamicBlocklistAvailable; + readonly attribute boolean isDynamicBlocklistAvailable; /** * Returns true if DynamicBlocklist is available but disabled. */ - readonly attribute bool isDynamicBlocklistDisabled; + readonly attribute boolean isDynamicBlocklistDisabled; /** * Add or remove an entry from the dynamic blocklist and save diff --git a/toolkit/components/aboutthirdparty/tests/browser/browser_aboutthirdparty.js b/toolkit/components/aboutthirdparty/tests/browser/browser_aboutthirdparty.js index 0a53823a35..865e7b949d 100644 --- a/toolkit/components/aboutthirdparty/tests/browser/browser_aboutthirdparty.js +++ b/toolkit/components/aboutthirdparty/tests/browser/browser_aboutthirdparty.js @@ -135,7 +135,7 @@ add_task(async () => { "after system info was collected." ); - await BrowserTestUtils.withNewTab("about:third-party", async browser => { + await BrowserTestUtils.withNewTab("about:third-party", async () => { if (!content.fetchDataDone) { const mainDiv = content.document.getElementById("main"); await BrowserTestUtils.waitForMutationCondition( diff --git a/toolkit/components/aboutwebauthn/content/aboutWebauthn.js b/toolkit/components/aboutwebauthn/content/aboutWebauthn.js index 9142ed198a..6232a30936 100644 --- a/toolkit/components/aboutwebauthn/content/aboutWebauthn.js +++ b/toolkit/components/aboutwebauthn/content/aboutWebauthn.js @@ -546,7 +546,7 @@ function sidebar_set_disabled(disabled) { }); } -function check_pin_repeat_is_correct(button) { +function check_pin_repeat_is_correct() { let pin = document.getElementById("new-pin"); let pin_repeat = document.getElementById("new-pin-repeat"); let has_current_pin = !document.getElementById("current-pin-div").hidden; @@ -853,7 +853,7 @@ try { Ci.nsIWebAuthnService ); document.addEventListener("DOMContentLoaded", onLoad); - window.addEventListener("beforeunload", event => { + window.addEventListener("beforeunload", () => { AboutWebauthnManagerJS.uninit(); if (AboutWebauthnService) { AboutWebauthnService.cancel(0); diff --git a/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_credentials.js b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_credentials.js index ff45ea2962..e2b01d67ef 100644 --- a/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_credentials.js +++ b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_credentials.js @@ -24,7 +24,7 @@ registerCleanupFunction(async function () { function send_credential_list(credlist) { let num_of_creds = 0; credlist.forEach(domain => { - domain.credentials.forEach(c => { + domain.credentials.forEach(() => { num_of_creds += 1; }); }); diff --git a/toolkit/components/aboutwindowsmessages/tests/browser/browser_aboutwindowsmessages.js b/toolkit/components/aboutwindowsmessages/tests/browser/browser_aboutwindowsmessages.js index 7c667891d6..18fbff351a 100644 --- a/toolkit/components/aboutwindowsmessages/tests/browser/browser_aboutwindowsmessages.js +++ b/toolkit/components/aboutwindowsmessages/tests/browser/browser_aboutwindowsmessages.js @@ -29,7 +29,7 @@ add_task(async () => { }); await BrowserTestUtils.withNewTab( { gBrowser: originalBrowser, url: "about:windows-messages" }, - async browser => { + async () => { let messagesList = content.document.getElementById("windows-div"); // This is tricky because the test framework has its own windows Assert.greaterOrEqual( diff --git a/toolkit/components/alerts/nsIAlertsService.idl b/toolkit/components/alerts/nsIAlertsService.idl index 2e75205ef5..412875a37b 100644 --- a/toolkit/components/alerts/nsIAlertsService.idl +++ b/toolkit/components/alerts/nsIAlertsService.idl @@ -312,14 +312,14 @@ interface nsIAlertsDoNotDisturb : nsISupports * code to show an alert. e.g. on OS X, the system will take care not * disrupting a user if we simply create a notification like usual. */ - attribute bool manualDoNotDisturb; + attribute boolean manualDoNotDisturb; /** * Toggles a mode for the service to suppress all notifications from * being dispatched when sharing the screen via the getMediaDisplay * API. */ - attribute bool suppressForScreenSharing; + attribute boolean suppressForScreenSharing; }; [scriptable, uuid(fc6d7f0a-0cf6-4268-8c71-ab640842b9b1)] diff --git a/toolkit/components/alerts/test/test_alerts_requireinteraction.html b/toolkit/components/alerts/test/test_alerts_requireinteraction.html index ca8fa1c9e3..dc189920ad 100644 --- a/toolkit/components/alerts/test/test_alerts_requireinteraction.html +++ b/toolkit/components/alerts/test/test_alerts_requireinteraction.html @@ -23,7 +23,7 @@ const chromeScript = SpecialPowers.loadChromeScript(_ => { sendAsyncMessage("waitForXULAlert", false); }, 2000); - var windowObserver = function(win, aTopic, aData) { + var windowObserver = function(win, aTopic) { if (aTopic != "domwindowopened") { return; } @@ -95,7 +95,7 @@ add_task(async function test_require_interaction() { var actualSequence = []; function createAlertListener(name, showCallback, finishCallback) { - return (subject, topic, data) => { + return (subject, topic) => { if (topic == "alertshow") { actualSequence.push(name + " show"); if (showCallback) { diff --git a/toolkit/components/alerts/test/test_invalid_utf16.html b/toolkit/components/alerts/test/test_invalid_utf16.html index a4f862238c..5d7fa9c079 100644 --- a/toolkit/components/alerts/test/test_invalid_utf16.html +++ b/toolkit/components/alerts/test/test_invalid_utf16.html @@ -18,7 +18,7 @@ let promise = new Promise((res, rej) => {resolve = res; reject = rej}); let success = false; - function observe(aSubject, aTopic, aData) { + function observe(aSubject, aTopic) { if (aTopic == "alertshow") { success = true; notifier.closeAlert(alertName); diff --git a/toolkit/components/alerts/test/test_multiple_alerts.html b/toolkit/components/alerts/test/test_multiple_alerts.html index 95843b2e89..830dfdc1bb 100644 --- a/toolkit/components/alerts/test/test_multiple_alerts.html +++ b/toolkit/components/alerts/test/test_multiple_alerts.html @@ -26,7 +26,7 @@ const chromeScript = SpecialPowers.loadChromeScript(_ => { sendAsyncMessage("waitedForPosition", null); }, 2000); - var windowObserver = function(win, aTopic, aData) { + var windowObserver = function(win, aTopic) { if (aTopic != "domwindowopened") { return; } diff --git a/toolkit/components/alerts/test/test_principal.html b/toolkit/components/alerts/test/test_principal.html index 5464d92977..7b0795d3f8 100644 --- a/toolkit/components/alerts/test/test_principal.html +++ b/toolkit/components/alerts/test/test_principal.html @@ -38,7 +38,7 @@ const chromeScript = SpecialPowers.loadChromeScript(_ => { function notify(alertName, principal) { return new Promise((resolve, reject) => { var source; - async function observe(subject, topic, data) { + async function observe(subject, topic) { if (topic == "alertclickcallback") { reject(new Error("Alerts should not be clicked during test")); } else if (topic == "alertshow") { diff --git a/toolkit/components/antitracking/AntiTrackingUtils.cpp b/toolkit/components/antitracking/AntiTrackingUtils.cpp index 2c530dc3da..d9624237de 100644 --- a/toolkit/components/antitracking/AntiTrackingUtils.cpp +++ b/toolkit/components/antitracking/AntiTrackingUtils.cpp @@ -514,6 +514,10 @@ AntiTrackingUtils::GetStoragePermissionStateInParent(nsIChannel* aChannel) { return nsILoadInfo::NoStoragePermission; } + if (targetPrincipal->IsSystemPrincipal()) { + return nsILoadInfo::HasStoragePermission; + } + nsCOMPtr<nsIURI> trackingURI; rv = aChannel->GetURI(getter_AddRefs(trackingURI)); if (NS_WARN_IF(NS_FAILED(rv))) { @@ -821,7 +825,8 @@ void AntiTrackingUtils::ComputeIsThirdPartyToTopWindow(nsIChannel* aChannel) { // whether the page is third-party, so we use channel result principal // instead. By doing this, an the resource inherits the principal from // its parent is considered not a third-party. - if (NS_IsAboutBlank(uri) || NS_IsAboutSrcdoc(uri)) { + if (NS_IsAboutBlank(uri) || NS_IsAboutSrcdoc(uri) || + uri->SchemeIs("blob")) { nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); if (NS_WARN_IF(!ssm)) { return; @@ -847,10 +852,36 @@ void AntiTrackingUtils::ComputeIsThirdPartyToTopWindow(nsIChannel* aChannel) { bool AntiTrackingUtils::IsThirdPartyChannel(nsIChannel* aChannel) { MOZ_ASSERT(aChannel); - // We only care whether the channel is 3rd-party with respect to - // the top-level. - nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); - return loadInfo->GetIsThirdPartyContextToTopWindow(); + // We have to handle blob URLs here because they always fail + // IsThirdPartyChannel because of how blob URLs are constructed. We just + // recompare to their ancestor chain from the loadInfo, bailing if any is + // third party. + nsAutoCString scheme; + nsCOMPtr<nsIURI> channelURI; + nsresult rv = aChannel->GetURI(getter_AddRefs(channelURI)); + if (NS_SUCCEEDED(rv) && channelURI->SchemeIs("blob")) { + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + for (const nsCOMPtr<nsIPrincipal>& principal : + loadInfo->AncestorPrincipals()) { + bool thirdParty = true; + rv = loadInfo->PrincipalToInherit()->IsThirdPartyPrincipal(principal, + &thirdParty); + if (NS_SUCCEEDED(rv) && thirdParty) { + return true; + } + } + return false; + } + + nsCOMPtr<mozIThirdPartyUtil> tpuService = + mozilla::components::ThirdPartyUtil::Service(); + if (!tpuService) { + return true; + } + bool thirdParty = true; + rv = tpuService->IsThirdPartyChannel(aChannel, nullptr, &thirdParty); + NS_ENSURE_SUCCESS(rv, true); + return thirdParty; } /* static */ @@ -903,19 +934,29 @@ bool AntiTrackingUtils::IsThirdPartyWindow(nsPIDOMWindowInner* aWindow, /* static */ bool AntiTrackingUtils::IsThirdPartyDocument(Document* aDocument) { MOZ_ASSERT(aDocument); - if (!aDocument->GetChannel()) { + nsCOMPtr<mozIThirdPartyUtil> tpuService = + mozilla::components::ThirdPartyUtil::Service(); + if (!tpuService) { + return true; + } + bool thirdParty = true; + if (!aDocument->GetChannel() || + aDocument->GetDocumentURI()->SchemeIs("blob")) { // If we can't get the channel from the document, i.e. initial about:blank // page, we use the browsingContext of the document to check if it's in the // third-party context. If the browsing context is still not available, we // will treat the window as third-party. + // We also rely on IsThirdPartyContext for blob documents because the + // IsThirdPartyChannel check relies on getting the BaseDomain, + // which correctly fails for blobs URIs. RefPtr<BrowsingContext> bc = aDocument->GetBrowsingContext(); return bc ? IsThirdPartyContext(bc) : true; } - // We only care whether the channel is 3rd-party with respect to - // the top-level. - nsCOMPtr<nsILoadInfo> loadInfo = aDocument->GetChannel()->LoadInfo(); - return loadInfo->GetIsThirdPartyContextToTopWindow(); + nsresult rv = tpuService->IsThirdPartyChannel(aDocument->GetChannel(), + nullptr, &thirdParty); + NS_ENSURE_SUCCESS(rv, true); + return thirdParty; } /* static */ @@ -923,41 +964,47 @@ bool AntiTrackingUtils::IsThirdPartyContext(BrowsingContext* aBrowsingContext) { MOZ_ASSERT(aBrowsingContext); MOZ_ASSERT(aBrowsingContext->IsInProcess()); - if (aBrowsingContext->IsTopContent()) { - return false; - } - - // If the top browsing context is not in the same process, it's cross-origin. - if (!aBrowsingContext->Top()->IsInProcess()) { - return true; - } - + // iframes with SANDBOX_ORIGIN are always third-party contexts + // because they are a unique origin nsIDocShell* docShell = aBrowsingContext->GetDocShell(); if (!docShell) { return true; } Document* doc = docShell->GetExtantDocument(); - if (!doc) { + if (!doc || doc->GetSandboxFlags() & SANDBOXED_ORIGIN) { return true; } nsIPrincipal* principal = doc->NodePrincipal(); - nsIDocShell* topDocShell = aBrowsingContext->Top()->GetDocShell(); - if (!topDocShell) { - return true; - } - Document* topDoc = topDocShell->GetDocument(); - if (!topDoc) { - return true; - } - nsIPrincipal* topPrincipal = topDoc->NodePrincipal(); + BrowsingContext* traversingParent = aBrowsingContext->GetParent(); + while (traversingParent) { + // If the parent browsing context is not in the same process, it's + // cross-origin. + if (!traversingParent->IsInProcess()) { + return true; + } - auto* topBasePrin = BasePrincipal::Cast(topPrincipal); - bool isThirdParty = true; + nsIDocShell* parentDocShell = traversingParent->GetDocShell(); + if (!parentDocShell) { + return true; + } + Document* parentDoc = parentDocShell->GetDocument(); + if (!parentDoc || parentDoc->GetSandboxFlags() & SANDBOXED_ORIGIN) { + return true; + } + nsIPrincipal* parentPrincipal = parentDoc->NodePrincipal(); + + auto* parentBasePrin = BasePrincipal::Cast(parentPrincipal); + bool isThirdParty = true; - topBasePrin->IsThirdPartyPrincipal(principal, &isThirdParty); + parentBasePrin->IsThirdPartyPrincipal(principal, &isThirdParty); + if (isThirdParty) { + return true; + } - return isThirdParty; + traversingParent = traversingParent->GetParent(); + } + return false; } /* static */ @@ -1005,6 +1052,18 @@ void AntiTrackingUtils::UpdateAntiTrackingInfoForChannel(nsIChannel* aChannel) { ->MarkOverriddenFingerprintingSettingsAsSet(); #endif + ExtContentPolicyType contentType = loadInfo->GetExternalContentPolicyType(); + if (contentType == ExtContentPolicy::TYPE_DOCUMENT || + contentType == ExtContentPolicy::TYPE_SUBDOCUMENT) { + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + Unused << loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)); + // For subdocuments, the channel's partition key is that of the parent + // document. This document may have a different partition key, particularly + // one without the same-site bit. + net::CookieJarSettings::Cast(cookieJarSettings) + ->UpdatePartitionKeyForDocumentLoadedByChannel(aChannel); + } + // We only update the IsOnContentBlockingAllowList flag and the partition key // for the top-level http channel. // @@ -1015,17 +1074,15 @@ void AntiTrackingUtils::UpdateAntiTrackingInfoForChannel(nsIChannel* aChannel) { // The partition key is computed based on the site, so it's no point to set it // for channels other than http channels. nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel); - if (!httpChannel || loadInfo->GetExternalContentPolicyType() != - ExtContentPolicy::TYPE_DOCUMENT) { + if (!httpChannel || contentType != ExtContentPolicy::TYPE_DOCUMENT) { return; } - nsCOMPtr<nsICookieJarSettings> cookieJarSettings; - Unused << loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)); - // Update the IsOnContentBlockingAllowList flag in the CookieJarSettings // if this is a top level loading. For sub-document loading, this flag // would inherit from the parent. + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + Unused << loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)); net::CookieJarSettings::Cast(cookieJarSettings) ->UpdateIsOnContentBlockingAllowList(aChannel); @@ -1033,7 +1090,7 @@ void AntiTrackingUtils::UpdateAntiTrackingInfoForChannel(nsIChannel* aChannel) { // propagated to non-top level loads via CookieJarSetting. nsCOMPtr<nsIURI> uri; Unused << aChannel->GetURI(getter_AddRefs(uri)); - net::CookieJarSettings::Cast(cookieJarSettings)->SetPartitionKey(uri); + net::CookieJarSettings::Cast(cookieJarSettings)->SetPartitionKey(uri, false); // Generate the fingerprinting randomization key for top-level loads. The key // will automatically be propagated to sub loads. diff --git a/toolkit/components/antitracking/StorageAccess.h b/toolkit/components/antitracking/StorageAccess.h index 4dbd5355c0..bca26057f3 100644 --- a/toolkit/components/antitracking/StorageAccess.h +++ b/toolkit/components/antitracking/StorageAccess.h @@ -40,8 +40,6 @@ enum class StorageAccess { // Allow access to the storage, but only if it is secure to do so in a // private browsing context. ePrivateBrowsing = 1, - // Allow access to the storage, but only persist it for the current session - eSessionScoped = 2, // Allow access to the storage eAllow = 3, // Keep this at the end. Used for serialization, but not a valid value. diff --git a/toolkit/components/antitracking/StorageAccessAPIHelper.cpp b/toolkit/components/antitracking/StorageAccessAPIHelper.cpp index 5baa2c2557..b4fcf2f6e9 100644 --- a/toolkit/components/antitracking/StorageAccessAPIHelper.cpp +++ b/toolkit/components/antitracking/StorageAccessAPIHelper.cpp @@ -475,7 +475,6 @@ StorageAccessAPIHelper::CompleteAllowAccessForOnParentProcess( [aParentContext, aTopLevelWindowId, trackingOrigin, trackingPrincipal, aCookieBehavior, aReason](int aAllowMode) -> RefPtr<StorageAccessPermissionGrantPromise> { - MOZ_ASSERT(!aParentContext->IsInProcess()); // We don't have the window, send an IPC to the content process that // owns the parent window. But there is a special case, for window.open, // we'll return to the content process we need to inform when this @@ -1060,12 +1059,7 @@ StorageAccessAPIHelper::CheckSameSiteCallingContextDecidesStorageAccessAPI( } } - nsIChannel* chan = aDocument->GetChannel(); - if (!chan) { - return Some(false); - } - nsCOMPtr<nsILoadInfo> loadInfo = chan->LoadInfo(); - if (loadInfo->GetIsThirdPartyContextToTopWindow()) { + if (AntiTrackingUtils::IsThirdPartyDocument(aDocument)) { return Some(false); } diff --git a/toolkit/components/antitracking/StoragePrincipalHelper.cpp b/toolkit/components/antitracking/StoragePrincipalHelper.cpp index 10be1112ca..79bafead2c 100644 --- a/toolkit/components/antitracking/StoragePrincipalHelper.cpp +++ b/toolkit/components/antitracking/StoragePrincipalHelper.cpp @@ -86,8 +86,10 @@ bool ChooseOriginAttributes(nsIChannel* aChannel, OriginAttributes& aAttrs, if (NS_WARN_IF(NS_FAILED(rv))) { return false; } - - aAttrs.SetPartitionKey(principalURI); + bool foreignByAncestorContext = + AntiTrackingUtils::IsThirdPartyChannel(aChannel) && + !loadInfo->GetIsThirdPartyContextToTopWindow(); + aAttrs.SetPartitionKey(principalURI, foreignByAncestorContext); return true; } @@ -313,7 +315,7 @@ nsresult StoragePrincipalHelper::GetPrincipal(nsIChannel* aChannel, // We only support foreign partitioned principal when dFPI is enabled. if (cjs->GetCookieBehavior() == nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN && - loadInfo->GetIsThirdPartyContextToTopWindow()) { + AntiTrackingUtils::IsThirdPartyChannel(aChannel)) { outPrincipal = partitionedPrincipal; } break; @@ -435,7 +437,7 @@ bool StoragePrincipalHelper::ShouldUsePartitionPrincipalForServiceWorker( return false; } - return aWorkerPrivate->IsThirdPartyContextToTopWindow(); + return aWorkerPrivate->IsThirdPartyContext(); } // static @@ -479,7 +481,7 @@ bool StoragePrincipalHelper::GetOriginAttributes( // Otherwise, we will use the regular principal. if (cjs->GetCookieBehavior() == nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN && - loadInfo->GetIsThirdPartyContextToTopWindow()) { + AntiTrackingUtils::IsThirdPartyChannel(aChannel)) { ChooseOriginAttributes(aChannel, aAttributes, true); } break; @@ -560,7 +562,7 @@ void StoragePrincipalHelper::UpdateOriginAttributesForNetworkState( return; } - aAttributes.SetPartitionKey(aFirstPartyURI); + aAttributes.SetPartitionKey(aFirstPartyURI, false); } enum SupportedScheme { HTTP, HTTPS }; @@ -664,8 +666,9 @@ bool StoragePrincipalHelper::PartitionKeyHasBaseDomain( nsString scheme; nsString pkBaseDomain; int32_t port; - bool success = OriginAttributes::ParsePartitionKey(aPartitionKey, scheme, - pkBaseDomain, port); + bool foreign; + bool success = OriginAttributes::ParsePartitionKey( + aPartitionKey, scheme, pkBaseDomain, port, foreign); if (!success) { return false; @@ -674,4 +677,26 @@ bool StoragePrincipalHelper::PartitionKeyHasBaseDomain( return aBaseDomain.Equals(pkBaseDomain); } +// static +void StoragePrincipalHelper::UpdatePartitionKeyWithForeignAncestorBit( + nsAString& aKey, bool aForeignByAncestorContext) { + bool site = 0 == aKey.Find(u"("); + if (!site) { + return; + } + if (aForeignByAncestorContext) { + int32_t index = aKey.Find(u",f)"); + if (index == -1) { + uint32_t cutStart = aKey.Length() - 1; + aKey.ReplaceLiteral(cutStart, 1, u",f)"); + } + } else { + int32_t index = aKey.Find(u",f)"); + if (index != -1) { + uint32_t cutLength = aKey.Length() - index; + aKey.ReplaceLiteral(index, cutLength, u")"); + } + } +} + } // namespace mozilla diff --git a/toolkit/components/antitracking/StoragePrincipalHelper.h b/toolkit/components/antitracking/StoragePrincipalHelper.h index f813417eb6..0fe362d8c1 100644 --- a/toolkit/components/antitracking/StoragePrincipalHelper.h +++ b/toolkit/components/antitracking/StoragePrincipalHelper.h @@ -351,6 +351,14 @@ class StoragePrincipalHelper final { static bool PartitionKeyHasBaseDomain(const nsAString& aPartitionKey, const nsAString& aBaseDomain); + + // Partition keys can have the same-site bit added or removed from them. + // "(https,foo.com)", false -> "(https,foo.com)" + // "(https,foo.com,f)", false -> "(https,foo.com)" + // "(https,foo.com,f)", true -> "(https,foo.com,f)" + // "(https,foo.com)", true -> "(https,foo.com,f)" + static void UpdatePartitionKeyWithForeignAncestorBit( + nsAString& aKey, bool aForeignByAncestorContext); }; } // namespace mozilla diff --git a/toolkit/components/antitracking/StripOnShareLists/LGPL/LICENSE b/toolkit/components/antitracking/StripOnShareLists/LGPL/LICENSE new file mode 100644 index 0000000000..6600f1c98d --- /dev/null +++ b/toolkit/components/antitracking/StripOnShareLists/LGPL/LICENSE @@ -0,0 +1,165 @@ +GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/toolkit/components/antitracking/StripOnShareLists/LGPL/StripOnShareLGPL.json b/toolkit/components/antitracking/StripOnShareLists/LGPL/StripOnShareLGPL.json new file mode 100644 index 0000000000..b34fbfff6b --- /dev/null +++ b/toolkit/components/antitracking/StripOnShareLists/LGPL/StripOnShareLGPL.json @@ -0,0 +1,556 @@ +{ + "global": { + "queryParams": [ + "at_campaign", + "at_send_date", + "at_campaign_type", + "at_recipient_list", + "at_creation", + "at_recipient_id", + "at_emailtype", + "at_ptr_name", + "at_link", + "at_link_id", + "at_medium", + "at_link_type", + "at_link_origin", + "utm_email", + "utm_email_id", + "utm_campaign_id", + "utm_newsletter_id" + ], + "topLevelSites": ["*"] + }, + "twitter": { + "queryParams": ["refsrc", "cxt", "s"], + "topLevelSites": ["www.twitter.com", "twitter.com", "x.com"] + }, + "amazon": { + "queryParams": [ + "pd_rd_i", + "pf_rd_i", + "pf_rd_m", + "pf_rd_s", + "pf_rd_t", + "pf_rd_w", + "adgrpid", + "ascsubtag", + "creative", + "creativeASIN", + "dchild", + "field-lbr_brands_browse-bin", + "hvadid", + "hvbmt", + "hvdev", + "hvlocphy", + "hvnetw", + "hvrand", + "hvtargid", + "hydadcr", + "initialIssue", + "ingress", + "linkCode", + "linkId", + "plattr", + "qid", + "rdc", + "refRID", + "th", + "ts_id", + "visitId", + "vtr", + "spIA", + "_encoding", + "qualifier" + ], + "topLevelSites": [ + "www.amazon.com", + "www.amazon.de", + "www.amazon.nl", + "www.amazon.fr", + "www.amazon.co.jp", + "www.amazon.in", + "www.amazon.es", + "www.amazon.ac", + "www.amazon.cn", + "www.amazon.eg", + "www.amazon.in", + "www.amazon.co.uk", + "www.amazon.it", + "www.amazon.pl", + "www.amazon.sg", + "www.amazon.ca" + ] + }, + "youtube": { + "queryParams": ["si", "feature", "kw"], + "topLevelSites": ["www.youtube.com", "www.youtu.be"] + }, + "carousell": { + "queryParams": [ + "referrer_search_query", + "referrer_search_query_source", + "referrer_request_id", + "referrer_sort_by", + "referrer_source", + "referrer_page_type", + "referrer_category_id", + "t-source", + "tap_index", + "t-referrer_browse_type", + "t-id" + ], + "topLevelSites": [ + "ca.carousell.com", + "www.carousell.sg", + "www.carousell.com.hk", + "us.carousell.com", + "au.carousell.com" + ] + }, + "etsy": { + "queryParams": ["click_sum", "ref", "click_key", "organic_search_click"], + "topLevelSites": ["www.etsy.com"] + }, + "wikipedia": { + "queryParams": ["wprov"], + "topLevelSites": ["www.wikipedia.org"] + }, + "wallstreetjournal": { + "queryParams": ["reflink"], + "topLevelSites": ["www.wsj.com"] + }, + "theathletic": { + "queryParams": ["source"], + "topLevelSites": ["theathletic.com"] + }, + "forbes": { + "queryParams": ["sh"], + "topLevelSites": ["www.forbes.com"] + }, + "bloomberg": { + "queryParams": ["leadSource", "sref", "srnd"], + "topLevelSites": ["www.bloomberg.com"] + }, + "tiktok": { + "queryParams": [ + "share_app_id", + "share_author_id", + "tt_from", + "embed_source", + "referer_url", + "refer", + "is_from_webapp", + "sender_device", + "sec_uid", + "web_id", + "enter_method", + "t", + "q" + ], + "topLevelSites": ["www.tiktok.com"] + }, + "bbc": { + "queryParams": [ + "facebook_page", + "at_bbc_team", + "ocid", + "ns_mchannel", + "ns_source", + "ns_campaign", + "ns_linkname", + "ns_fee", + "xtor" + ], + "topLevelSites": ["www.bbc.com"] + }, + "cnn": { + "queryParams": ["ref"], + "topLevelSites": ["www.cnn.co.jp", "www.cnn.com"] + }, + "imdb": { + "queryParams": [ + "pf_rd_p", + "pf_rd_i", + "pf_rd_m", + "pf_rd_s", + "pf_rd_t", + "pf_rd_r" + ], + "topLevelSites": ["www.imdb.com"] + }, + "mirror": { + "queryParams": ["int_source"], + "topLevelSites": ["www.mirror.co.uk"] + }, + "wise": { + "queryParams": ["partnerizecampaignID", "clickref", "adref", "partnerID"], + "topLevelSites": ["wise.com"] + }, + "facebook": { + "queryParams": ["tracking", "extid", "mibextid"], + "topLevelSites": ["www.facebook.com"] + }, + "lazada": { + "queryParams": [ + "mkttid", + "laz_trackid", + "spm", + "clickTrackInfo", + "trafficFrom", + "scm", + "acm", + "ad_src", + "did", + "mp", + "cid", + "pos" + ], + "topLevelSites": [ + "www.lazada.com.ph", + "www.lazada.vn", + "www.lazada.sg", + "www.lazada.com.my" + ] + }, + "msn": { + "queryParams": ["cvid", "pc", "ei"], + "topLevelSites": ["www.msn.com"] + }, + "aliexpress": { + "queryParams": [ + "sk", + "dp", + "spm", + "scm", + "pvid", + "scm_id", + "scm-url", + "utparam", + "aff_fsk", + "aff_fcid", + "terminal_id", + "aff_trace_key", + "pdp_npi", + "aff_short_key", + "aff_platform", + "algo_pvid", + "curPageLogUid", + "mall_affr", + "algo_expid", + "gps-id" + ], + "topLevelSites": ["www.tiktok.com"] + }, + "reddit": { + "queryParams": [ + "ref", + "ref_source", + "ref_campaign", + "correlation_id", + "share_id" + ], + "topLevelSites": ["www.reddit.com"] + }, + "wired": { + "queryParams": ["mbid"], + "topLevelSites": ["www.wired.co.uk", "www.wired.com"] + }, + "yandex": { + "queryParams": [ + "utm-term", + "did", + "from", + "msid", + "stid", + "mlid", + "persistent_id", + "source-serpid", + "suggest_reqid", + "clid" + ], + "topLevelSites": ["yandex.com"] + }, + "ebay": { + "queryParams": [ + "_trkparms", + "mkcid", + "_trksid", + "mkevt", + "amdata", + "ssuid", + "mkrid", + "campid", + "sssrc", + "ssspo", + "_from", + "hash" + ], + "topLevelSites": ["www.ebay.com"] + }, + "flipkart": { + "queryParams": [ + "_refId", + "store", + "ssid", + "affid", + "cid", + "iid", + "pwsvid", + "pageUID", + "lid" + ], + "topLevelSites": ["www.flipkart.com"] + }, + "google": { + "queryParams": [ + "sca_esv", + "ved", + "pcampaignid", + "rlz", + "ei", + "sxsrf", + "sourceid", + "aqs", + "cad", + "usg", + "dpr", + "dcr", + "sei", + "cd", + "vet", + "esrc", + "site", + "gs_l", + "gs_lp", + "sclient", + "oe", + "visit_id", + "biw", + "bih", + "gs_lcp", + "gs_lcrp" + ], + "topLevelSites": [ + "www.google.com", + "www.google.ad", + "www.google.ae", + "www.google.com.af", + "www.google.com.ag", + "www.google.al", + "www.google.am", + "www.google.co.ao", + "www.google.com.ar", + "www.google.as", + "www.google.at", + "www.google.com.au", + "www.google.az", + "www.google.ba", + "www.google.com.bd", + "www.google.be", + "www.google.bf", + "www.google.bg", + "www.google.com.bh", + "www.google.bi", + "www.google.bj", + "www.google.com.bn", + "www.google.com.bo", + "www.google.com.br", + "www.google.bs", + "www.google.bt", + "www.google.co.bw", + "www.google.by", + "www.google.com.bz", + "www.google.ca", + "www.google.cd", + "www.google.cf", + "www.google.cg", + "www.google.ch", + "www.google.ci", + "www.google.co.ck", + "www.google.cl", + "www.google.cm", + "www.google.cn", + "www.google.com.co", + "www.google.co.cr", + "www.google.com.cu", + "www.google.cv", + "www.google.com.cy", + "www.google.cz", + "www.google.de", + "www.google.dj", + "www.google.dk", + "www.google.dm", + "www.google.com.do", + "www.google.dz", + "www.google.com.ec", + "www.google.ee", + "www.google.com.eg", + "www.google.es", + "www.google.com.et", + "www.google.fi", + "www.google.com.fj", + "www.google.fm", + "www.google.fr", + "www.google.ga", + "www.google.ge", + "www.google.gg", + "www.google.com.gh", + "www.google.com.gi", + "www.google.gl", + "www.google.gm", + "www.google.gr", + "www.google.com.gt", + "www.google.gy", + "www.google.com.hk", + "www.google.hn", + "www.google.hr", + "www.google.ht", + "www.google.hu", + "www.google.co.id", + "www.google.ie", + "www.google.co.il", + "www.google.im", + "www.google.co.in", + "www.google.iq", + "www.google.is", + "www.google.it", + "www.google.je", + "www.google.com.jm", + "www.google.jo", + "www.google.co.jp", + "www.google.co.ke", + "www.google.com.kh", + "www.google.ki", + "www.google.kg", + "www.google.co.kr", + "www.google.com.kw", + "www.google.kz", + "www.google.la", + "www.google.com.lb", + "www.google.li", + "www.google.lk", + "www.google.co.ls", + "www.google.lt", + "www.google.lu", + "www.google.lv", + "www.google.com.ly", + "www.google.co.ma", + "www.google.md", + "www.google.me", + "www.google.mg", + "www.google.mk", + "www.google.ml", + "www.google.com.mm", + "www.google.mn", + "www.google.com.mt", + "www.google.mu", + "www.google.mv", + "www.google.mw", + "www.google.com.mx", + "www.google.com.my", + "www.google.co.mz", + "www.google.com.na", + "www.google.com.ng", + "www.google.com.ni", + "www.google.ne", + "www.google.nl", + "www.google.no", + "www.google.com.np", + "www.google.nr", + "www.google.nu", + "www.google.co.nz", + "www.google.com.om", + "www.google.com.pa", + "www.google.com.pe", + "www.google.com.pg", + "www.google.com.ph", + "www.google.com.pk", + "www.google.pl", + "www.google.pn", + "www.google.com.pr", + "www.google.ps", + "www.google.pt", + "www.google.com.py", + "www.google.com.qa", + "www.google.ro", + "www.google.ru", + "www.google.rw", + "www.google.com.sa", + "www.google.com.sb", + "www.google.sc", + "www.google.se", + "www.google.com.sg", + "www.google.sh", + "www.google.si", + "www.google.sk", + "www.google.com.sl", + "www.google.sn", + "www.google.so", + "www.google.sm", + "www.google.sr", + "www.google.st", + "www.google.com.sv", + "www.google.td", + "www.google.tg", + "www.google.co.th", + "www.google.com.tj", + "www.google.tl", + "www.google.tm", + "www.google.tn", + "www.google.to", + "www.google.com.tr", + "www.google.tt", + "www.google.com.tw", + "www.google.co.tz", + "www.google.com.ua", + "www.google.co.ug", + "www.google.co.uk", + "www.google.com.uy", + "www.google.co.uz", + "www.google.com.vc", + "www.google.co.ve", + "www.google.co.vi", + "www.google.com.vn", + "www.google.vu", + "www.google.ws", + "www.google.rs", + "www.google.co.za", + "www.google.co.zm", + "www.google.co.zw", + "www.google.cat" + ] + }, + "bing": { + "queryParams": ["qp", "cvid", "qs", "form", "sk", "sc", "sp"], + "topLevelSites": ["www.bing.com"] + }, + "twitch": { + "queryParams": ["tt_content", "tt_medium"], + "topLevelSites": ["www.twitch.tv"] + }, + "cnet": { + "queryParams": ["ftag"], + "topLevelSites": ["www.cnet.com"] + }, + "nytimes": { + "queryParams": ["smid"], + "topLevelSites": ["www.nytimes.com"] + }, + "github": { + "queryParams": ["email_token", "email_source"], + "topLevelSites": ["github.com"] + }, + "linkedin": { + "queryParams": ["refId", "trk", "trackingId"], + "topLevelSites": ["ca.linkedin.com"] + }, + "newyorker": { + "queryParams": ["esrc", "bxid", "cndid", "source", "mbid"], + "topLevelSites": ["www.newyorker.com"] + }, + "bestbuy": { + "queryParams": ["acampID", "irclickid", "irgwc", "loc", "mpid", "intl"], + "topLevelSites": ["www.bestbuy.com"] + } +} diff --git a/toolkit/components/antitracking/data/StripOnShare.json b/toolkit/components/antitracking/StripOnShareLists/MPL2/StripOnShare.json index 394723fa00..00cc973af6 100644 --- a/toolkit/components/antitracking/data/StripOnShare.json +++ b/toolkit/components/antitracking/StripOnShareLists/MPL2/StripOnShare.json @@ -55,7 +55,7 @@ }, "twitter": { "queryParams": ["ref_src", "ref_url"], - "topLevelSites": ["www.twitter.com", "twitter.com"] + "topLevelSites": ["www.twitter.com", "twitter.com", "x.com"] }, "instagram": { "queryParams": ["igshid", "ig_rid"], diff --git a/toolkit/components/antitracking/URLDecorationStripper.cpp b/toolkit/components/antitracking/URLDecorationStripper.cpp index 38af391945..fc94fe8ed5 100644 --- a/toolkit/components/antitracking/URLDecorationStripper.cpp +++ b/toolkit/components/antitracking/URLDecorationStripper.cpp @@ -22,8 +22,8 @@ namespace mozilla { nsresult URLDecorationStripper::StripTrackingIdentifiers(nsIURI* aURI, nsACString& aOutSpec) { - nsAutoString tokenList; - nsresult rv = Preferences::GetString(kPrefName, tokenList); + nsAutoCString tokenList; + nsresult rv = Preferences::GetCString(kPrefName, tokenList); ToLowerCase(tokenList); nsAutoCString path; @@ -34,12 +34,12 @@ nsresult URLDecorationStripper::StripTrackingIdentifiers(nsIURI* aURI, int32_t queryBegins = path.FindChar('?'); // Only positive values are valid since the path must begin with a '/'. if (queryBegins > 0) { - for (const nsAString& token : tokenList.Split(' ')) { + for (const nsACString& token : tokenList.Split(' ')) { if (token.IsEmpty()) { continue; } - nsAutoString value; + nsAutoCString value; if (URLParams::Extract(Substring(path, queryBegins + 1), token, value) && !value.IsVoid()) { // Tracking identifier found in the URL! diff --git a/toolkit/components/antitracking/URLQueryStringStripper.cpp b/toolkit/components/antitracking/URLQueryStringStripper.cpp index 2e154b9103..5eb513472b 100644 --- a/toolkit/components/antitracking/URLQueryStringStripper.cpp +++ b/toolkit/components/antitracking/URLQueryStringStripper.cpp @@ -17,6 +17,7 @@ #include "nsIURIMutator.h" #include "nsUnicharUtils.h" #include "nsURLHelper.h" +#include "nsNetUtil.h" #include "mozilla/dom/StripOnShareRuleBinding.h" namespace { @@ -84,64 +85,16 @@ URLQueryStringStripper::StripForCopyOrShare(nsIURI* aURI, NS_ENSURE_ARG_POINTER(strippedURI); int aStripCount = 0; - nsAutoCString query; - nsresult rv = aURI->GetQuery(query); - NS_ENSURE_SUCCESS(rv, rv); - // We don't need to do anything if there is no query string. - if (query.IsEmpty()) { - Telemetry::Accumulate(Telemetry::STRIP_ON_SHARE_PARAMS_REMOVED, 0); - return NS_OK; - } - nsAutoCString host; - rv = aURI->GetHost(host); + nsresult rv = + StripForCopyOrShareInternal(aURI, strippedURI, aStripCount, false); NS_ENSURE_SUCCESS(rv, rv); - URLParams params; - - URLParams::Parse(query, true, [&](nsString&& name, nsString&& value) { - nsAutoString lowerCaseName; - ToLowerCase(name, lowerCaseName); - // Look through the global rules. - dom::StripRule globalRule; - bool keyExists = mStripOnShareMap.Get("*"_ns, &globalRule); - // There should always be a global rule. - MOZ_ASSERT(keyExists); - for (const auto& param : globalRule.mQueryParams) { - if (param == lowerCaseName) { - aStripCount++; - return true; - } - } - - // Check for site specific rules. - dom::StripRule siteSpecificRule; - keyExists = mStripOnShareMap.Get(host, &siteSpecificRule); - if (keyExists) { - for (const auto& param : siteSpecificRule.mQueryParams) { - if (param == lowerCaseName) { - aStripCount++; - return true; - } - } - } - - params.Append(name, value); - return true; - }); - Telemetry::Accumulate(Telemetry::STRIP_ON_SHARE_PARAMS_REMOVED, aStripCount); if (!aStripCount) { return NS_OK; } - nsAutoString newQuery; - params.Serialize(newQuery, false); - - Unused << NS_MutateURI(aURI) - .SetQuery(NS_ConvertUTF16toUTF8(newQuery)) - .Finalize(strippedURI); - // To calculate difference in length of the URL // after stripping occurs for Telemetry nsAutoCString specOriginalURI; @@ -308,9 +261,8 @@ nsresult URLQueryStringStripper::StripQueryString(nsIURI* aURI, URLParams params; - URLParams::Parse(query, false, [&](nsString&& name, nsString&& value) { - nsAutoString lowerCaseName; - + URLParams::Parse(query, false, [&](nsCString&& name, nsCString&& value) { + nsAutoCString lowerCaseName; ToLowerCase(name, lowerCaseName); if (mList.Contains(lowerCaseName)) { @@ -320,7 +272,7 @@ nsresult URLQueryStringStripper::StripQueryString(nsIURI* aURI, // this will only count query params listed in the Histogram definition. // Calls for any other query params will be discarded. nsAutoCString telemetryLabel("param_"); - AppendUTF16toUTF8(lowerCaseName, telemetryLabel); + telemetryLabel.Append(lowerCaseName); Telemetry::AccumulateCategorical( Telemetry::QUERY_STRIPPING_COUNT_BY_PARAM, telemetryLabel); @@ -336,13 +288,10 @@ nsresult URLQueryStringStripper::StripQueryString(nsIURI* aURI, return NS_OK; } - nsAutoString newQuery; + nsAutoCString newQuery; params.Serialize(newQuery, false); - Unused << NS_MutateURI(uri) - .SetQuery(NS_ConvertUTF16toUTF8(newQuery)) - .Finalize(aOutput); - + Unused << NS_MutateURI(uri).SetQuery(newQuery).Finalize(aOutput); return NS_OK; } @@ -362,10 +311,10 @@ bool URLQueryStringStripper::CheckAllowList(nsIURI* aURI) { return mAllowList.Contains(baseDomain); } -void URLQueryStringStripper::PopulateStripList(const nsAString& aList) { +void URLQueryStringStripper::PopulateStripList(const nsACString& aList) { mList.Clear(); - for (const nsAString& item : aList.Split(' ')) { + for (const nsACString& item : aList.Split(' ')) { mList.Insert(item); } } @@ -380,7 +329,7 @@ void URLQueryStringStripper::PopulateAllowList(const nsACString& aList) { NS_IMETHODIMP URLQueryStringStripper::OnQueryStrippingListUpdate( - const nsAString& aStripList, const nsACString& aAllowList) { + const nsACString& aStripList, const nsACString& aAllowList) { PopulateStripList(aStripList); PopulateAllowList(aAllowList); return NS_OK; @@ -396,8 +345,7 @@ URLQueryStringStripper::OnStripOnShareUpdate(const nsTArray<nsString>& aArgs, continue; } for (const auto& topLevelSite : rule.mTopLevelSites) { - mStripOnShareMap.InsertOrUpdate(NS_ConvertUTF16toUTF8(topLevelSite), - rule); + mStripOnShareMap.InsertOrUpdate(topLevelSite, rule); } } return NS_OK; @@ -407,10 +355,9 @@ NS_IMETHODIMP URLQueryStringStripper::TestGetStripList(nsACString& aStripList) { aStripList.Truncate(); - StringJoinAppend(aStripList, " "_ns, mList, - [](auto& aResult, const auto& aValue) { - aResult.Append(NS_ConvertUTF16toUTF8(aValue)); - }); + StringJoinAppend( + aStripList, " "_ns, mList, + [](auto& aResult, const auto& aValue) { aResult.Append(aValue); }); return NS_OK; } @@ -426,4 +373,110 @@ URLQueryStringStripper::Observe(nsISupports*, const char* aTopic, return NS_OK; } +nsresult URLQueryStringStripper::StripForCopyOrShareInternal( + nsIURI* aURI, nsIURI** strippedURI, int& aStripCount, + bool aStripNestedURIs) { + nsAutoCString query; + nsresult rv = aURI->GetQuery(query); + NS_ENSURE_SUCCESS(rv, rv); + + // We don't need to do anything if there is no query string. + if (query.IsEmpty()) { + Telemetry::Accumulate(Telemetry::STRIP_ON_SHARE_PARAMS_REMOVED, 0); + return NS_OK; + } + + nsAutoCString host; + rv = aURI->GetHost(host); + NS_ENSURE_SUCCESS(rv, rv); + + URLParams params; + + URLParams::Parse(query, false, [&](nsCString&& name, nsCString&& value) { + nsAutoCString lowerCaseName; + ToLowerCase(name, lowerCaseName); + + // Look through the global rules. + dom::StripRule globalRule; + bool keyExists = mStripOnShareMap.Get("*"_ns, &globalRule); + // There should always be a global rule. + MOZ_ASSERT(keyExists); + + // Look through the global rules. + for (const auto& param : globalRule.mQueryParams) { + if (param == lowerCaseName) { + aStripCount++; + return true; + } + } + + // Check for site specific rules. + dom::StripRule siteSpecificRule; + keyExists = mStripOnShareMap.Get(host, &siteSpecificRule); + if (keyExists) { + for (const auto& param : siteSpecificRule.mQueryParams) { + if (param == lowerCaseName) { + aStripCount++; + return true; + } + } + } + + // Only if it is top layer of the recursion then it + // checks if the value of the query parameter is a valid URI + // if not then it gets added back to the query, if it is then + // it gets passed back into this method but with the recursive + // stripping flag set to true + if (!aStripNestedURIs) { + nsAutoCString decodeValue; + URLParams::DecodeString(value, decodeValue); + + nsCOMPtr<nsIURI> nestedURI; + rv = NS_NewURI(getter_AddRefs(nestedURI), decodeValue); + + if (NS_WARN_IF(NS_FAILED(rv))) { + params.Append(name, value); + return true; + } + + nsCOMPtr<nsIURI> strippedNestedURI; + rv = StripForCopyOrShareInternal( + nestedURI, getter_AddRefs(strippedNestedURI), aStripCount, true); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + if (!strippedNestedURI) { + params.Append(name, value); + return true; + } + + nsAutoCString nestedURIString; + rv = strippedNestedURI->GetSpec(nestedURIString); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + // Encodes URI + nsAutoCString encodedURI; + URLParams::SerializeString(nestedURIString, encodedURI); + + params.Append(name, encodedURI); + return true; + } + + params.Append(name, value); + return true; + }); + + // Returns null for strippedURI if no query params have been stripped. + if (!aStripCount) { + return NS_OK; + } + + nsAutoCString newQuery; + params.Serialize(newQuery, false); + return NS_MutateURI(aURI).SetQuery(newQuery).Finalize(strippedURI); +} + } // namespace mozilla diff --git a/toolkit/components/antitracking/URLQueryStringStripper.h b/toolkit/components/antitracking/URLQueryStringStripper.h index 89466cfd12..3e8e8203a9 100644 --- a/toolkit/components/antitracking/URLQueryStringStripper.h +++ b/toolkit/components/antitracking/URLQueryStringStripper.h @@ -46,10 +46,16 @@ class URLQueryStringStripper final : public nsIObserver, bool CheckAllowList(nsIURI* aURI); - void PopulateStripList(const nsAString& aList); + void PopulateStripList(const nsACString& aList); void PopulateAllowList(const nsACString& aList); - nsTHashSet<nsString> mList; + // Recursive helper function that helps strip URIs of tracking parameters + // and enables the stripping of tracking paramerters that are in a URI which + // is nested in a query parameter + nsresult StripForCopyOrShareInternal(nsIURI* aURI, nsIURI** strippedURI, + int& aStripCount, bool aStripNestedURIs); + + nsTHashSet<nsCString> mList; nsTHashSet<nsCString> mAllowList; nsCOMPtr<nsIURLQueryStrippingListService> mListService; nsTHashMap<nsCString, dom::StripRule> mStripOnShareMap; diff --git a/toolkit/components/antitracking/URLQueryStrippingListService.sys.mjs b/toolkit/components/antitracking/URLQueryStrippingListService.sys.mjs index baaa9f3824..d9c1995821 100644 --- a/toolkit/components/antitracking/URLQueryStrippingListService.sys.mjs +++ b/toolkit/components/antitracking/URLQueryStrippingListService.sys.mjs @@ -31,10 +31,9 @@ XPCOMUtils.defineLazyPreferenceGetter( PREF_STRIP_IS_TEST ); -// Lazy getter for the strip-on-share strip list. -ChromeUtils.defineLazyGetter(lazy, "StripOnShareList", async () => { +async function fetchAndParseList(fileName) { let response = await fetch( - "chrome://global/content/antitracking/StripOnShare.json" + "chrome://global/content/antitracking/" + fileName ); if (!response.ok) { lazy.logger.error( @@ -44,7 +43,48 @@ ChromeUtils.defineLazyGetter(lazy, "StripOnShareList", async () => { "Error fetching strip-on-share strip list" + response.status ); } - return response.json(); + return await response.json(); +} + +// Lazy getter for the strip-on-share strip list. +ChromeUtils.defineLazyGetter(lazy, "StripOnShareList", async () => { + let [stripOnShareList, stripOnShareLGPLParams] = await Promise.all([ + fetchAndParseList("StripOnShare.json"), + fetchAndParseList("StripOnShareLGPL.json"), + ]); + + if (!stripOnShareList || !stripOnShareLGPLParams) { + lazy.logger.error("Error strip-on-share strip list were not loaded"); + throw new Error("Error fetching strip-on-share strip list were not loaded"); + } + + // Combines the mozilla licensed strip on share param + // list and the LGPL licensed strip on share param list + for (const key in stripOnShareLGPLParams) { + if (Object.hasOwn(stripOnShareList, key)) { + stripOnShareList[key].queryParams.push( + ...stripOnShareLGPLParams[key].queryParams + ); + + stripOnShareList[key].topLevelSites.push( + ...stripOnShareLGPLParams[key].topLevelSites + ); + + // Removes duplicates topLevelSitres + stripOnShareList[key].topLevelSites = [ + ...new Set(stripOnShareList[key].topLevelSites), + ]; + + // Removes duplicates queryParams + stripOnShareList[key].queryParams = [ + ...new Set(stripOnShareList[key].queryParams), + ]; + } else { + stripOnShareList[key] = stripOnShareLGPLParams[key]; + } + } + + return stripOnShareList; }); export class URLQueryStrippingListService { @@ -142,11 +182,11 @@ export class URLQueryStrippingListService { } // Get the list from pref. - this._onPrefUpdate( + await this._onPrefUpdate( PREF_STRIP_LIST_NAME, Services.prefs.getStringPref(PREF_STRIP_LIST_NAME, "") ); - this._onPrefUpdate( + await this._onPrefUpdate( PREF_ALLOW_LIST_NAME, Services.prefs.getStringPref(PREF_ALLOW_LIST_NAME, "") ); @@ -219,7 +259,7 @@ export class URLQueryStrippingListService { this._notifyObservers(); } - _onPrefUpdate(pref, value) { + async _onPrefUpdate(pref, value) { switch (pref) { case PREF_STRIP_LIST_NAME: this.prefStripList = new Set(value ? value.split(" ") : []); @@ -235,7 +275,7 @@ export class URLQueryStrippingListService { } this._notifyObservers(); - this._notifyStripOnShareObservers(); + await this._notifyStripOnShareObservers(); } _getListFromSharedData() { diff --git a/toolkit/components/antitracking/jar.mn b/toolkit/components/antitracking/jar.mn index e45b37c5b6..2f8c4d7ee4 100644 --- a/toolkit/components/antitracking/jar.mn +++ b/toolkit/components/antitracking/jar.mn @@ -9,4 +9,7 @@ toolkit.jar: # domain1: {queryParams: [param1, param2, ..], topLevelSites: [www.site.de, www.site.com,...]}, domain2: {...} # This list will be consumed from the nsIQueryStrippingListService and # later be dispatched to the nsIURLQueryStringStripper in a further processed form. - content/global/antitracking/StripOnShare.json (data/StripOnShare.json) + content/global/antitracking/StripOnShare.json (StripOnShareLists/MPL2/StripOnShare.json) + + # Separate StripOnShare list for parameters that are covered in the LGPL License + content/global/antitracking/StripOnShareLGPL.json (StripOnShareLists/LGPL/StripOnShareLGPL.json) diff --git a/toolkit/components/antitracking/nsIURLQueryStringStripper.idl b/toolkit/components/antitracking/nsIURLQueryStringStripper.idl index 372cc0f94a..b3e939094b 100644 --- a/toolkit/components/antitracking/nsIURLQueryStringStripper.idl +++ b/toolkit/components/antitracking/nsIURLQueryStringStripper.idl @@ -23,7 +23,7 @@ interface nsIURLQueryStringStripper : nsISupports { // Strip the query parameters that are in the strip list. Return the amount of // query parameters that have been stripped. Returns 0 if no query parameters // have been stripped or the feature is disabled. - uint32_t strip(in nsIURI aURI, in bool aIsPBM, out nsIURI aOutput); + uint32_t strip(in nsIURI aURI, in boolean aIsPBM, out nsIURI aOutput); // Strip the query parameters that are in the stripForCopy/Share strip list. // Returns ether the stripped URI or null if no query parameters have been stripped diff --git a/toolkit/components/antitracking/nsIURLQueryStrippingListService.idl b/toolkit/components/antitracking/nsIURLQueryStrippingListService.idl index f746126c13..1af2c4727f 100644 --- a/toolkit/components/antitracking/nsIURLQueryStrippingListService.idl +++ b/toolkit/components/antitracking/nsIURLQueryStrippingListService.idl @@ -22,7 +22,7 @@ interface nsIURLQueryStrippingListObserver : nsISupports * A comma-separated list of hosts (eTLD+1) that are exempt from query * stripping. */ - void onQueryStrippingListUpdate(in AString aStripList, in ACString aAllowList); + void onQueryStrippingListUpdate(in ACString aStripList, in ACString aAllowList); /** * Called by nsIQueryStrippingListService when the list of query stripping diff --git a/toolkit/components/antitracking/test/browser/browser.toml b/toolkit/components/antitracking/test/browser/browser.toml index 9d0874ed3a..edb429b49e 100644 --- a/toolkit/components/antitracking/test/browser/browser.toml +++ b/toolkit/components/antitracking/test/browser/browser.toml @@ -1,5 +1,9 @@ [DEFAULT] -skip-if = ["os == 'linux' && (asan || tsan)"] # bug 1662229 - task exception +skip-if = [ + "os == 'linux' && os_version == '18.04' && asan", # bug 1662229 - task exception + "os == 'linux' && os_version == '18.04' && tsan", # bug 1662229 - task exception + "debug", # bug 1884982 - takes 20+ minutes to run on debug +] prefs = [ "dom.storage_access.prompt.testing=true", # Disable the Storage Access API prompts for all of the tests in this directory "dom.storage_access.prompt.testing.allow=true", @@ -108,6 +112,8 @@ skip-if = ["os == 'mac' && !debug"] # Bug 1503778, 1577362 ["browser_onModifyRequestNotificationForTrackingResources.js"] +["browser_partitionedABA.js"] + ["browser_partitionedClearSiteDataHeader.js"] support-files = ["clearSiteData.sjs"] @@ -194,7 +200,7 @@ support-files = [ "!/browser/components/originattributes/test/browser/file_thirdPartyChild.request.html", "!/browser/components/originattributes/test/browser/file_thirdPartyChild.script.js", "!/browser/components/originattributes/test/browser/file_thirdPartyChild.sharedworker.js", - "!/browser/components/originattributes/test/browser/file_thirdPartyChild.video.ogv", + "!/browser/components/originattributes/test/browser/file_thirdPartyChild.video.webm", "!/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.fetch.html", "!/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.js", "!/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.request.html", @@ -210,7 +216,7 @@ support-files = [ "file_saveAsImage.sjs", "file_saveAsVideo.sjs", "file_saveAsPageInfo.html", - "file_video.ogv", + "file_video.webm", ] ["browser_staticPartition_tls_session.js"] diff --git a/toolkit/components/antitracking/test/browser/browser_doublyNestedTracker.js b/toolkit/components/antitracking/test/browser/browser_doublyNestedTracker.js index 4f38b29a7d..c3b6342ce1 100644 --- a/toolkit/components/antitracking/test/browser/browser_doublyNestedTracker.js +++ b/toolkit/components/antitracking/test/browser/browser_doublyNestedTracker.js @@ -36,7 +36,7 @@ add_task(async function () { async function runChecks() { is(document.cookie, "", "No cookies for me"); document.cookie = "name=value"; - is(document.cookie, "name=value", "I have the cookies!"); + is(document.cookie, "", "I don't have the cookies!"); } await new Promise(resolve => { diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedABA.js b/toolkit/components/antitracking/test/browser/browser_partitionedABA.js new file mode 100644 index 0000000000..b6acc82d48 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_partitionedABA.js @@ -0,0 +1,86 @@ +/* vim: set ts=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/. */ + +/* + * A test to verify that ABA iframes partition at least localStorage and document.cookie + */ + +"use strict"; + +add_setup(async function () { + await setCookieBehaviorPref( + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + false + ); +}); + +add_task(async function runTest() { + info("Creating the tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = tab.linkedBrowser; + await BrowserTestUtils.browserLoaded(browser); + + info("Creating the third-party iframe"); + let ifrBC = await SpecialPowers.spawn( + browser, + [TEST_TOP_PAGE_7], + async page => { + let ifr = content.document.createElement("iframe"); + + let loading = ContentTaskUtils.waitForEvent(ifr, "load"); + content.document.body.appendChild(ifr); + ifr.src = page; + await loading; + + return ifr.browsingContext; + } + ); + + info("Creating the ABA iframe"); + let ifrABABC = await SpecialPowers.spawn( + ifrBC, + [TEST_TOP_PAGE], + async page => { + let ifr = content.document.createElement("iframe"); + + let loading = ContentTaskUtils.waitForEvent(ifr, "load"); + content.document.body.appendChild(ifr); + ifr.src = page; + await loading; + + return ifr.browsingContext; + } + ); + + info("Write cookie to the ABA third-party iframe"); + await SpecialPowers.spawn(ifrABABC, [], async _ => { + content.document.cookie = "foo; SameSite=None; Secure; Partitioned"; + }); + + let cookie = await SpecialPowers.spawn(browser, [], async () => { + return content.document.cookie; + }); + is(cookie, "", "Cookie is not in the top level"); + + info("Write localstorage to the ABA third-party iframe"); + await SpecialPowers.spawn(ifrABABC, [], async _ => { + content.localStorage.setItem("foo", "bar"); + }); + + let storage = await SpecialPowers.spawn(browser, [], async () => { + return content.localStorage.getItem("foo"); + }); + is(storage, null, "LocalStorage update is not in the top level"); + + info("Clean up"); + BrowserTestUtils.removeTab(tab); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, () => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_staticPartition_cache.js b/toolkit/components/antitracking/test/browser/browser_staticPartition_cache.js index 9afe3180fb..9383e5a6f9 100644 --- a/toolkit/components/antitracking/test/browser/browser_staticPartition_cache.js +++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_cache.js @@ -105,7 +105,7 @@ add_task(async function () { // The CSS cache needs to be cleared in-process. content.windowUtils.clearSharedStyleSheetCache(); - let videoURL = arg.urlPrefix + "file_thirdPartyChild.video.ogv"; + let videoURL = arg.urlPrefix + "file_thirdPartyChild.video.webm"; let audioURL = arg.urlPrefix + "file_thirdPartyChild.audio.ogg"; let URLSuffix = "?r=" + arg.randomSuffix; @@ -176,7 +176,7 @@ add_task(async function () { "xhr.html", "worker.xhr.html", "audio.ogg", - "video.ogv", + "video.webm", "fetch.html", "worker.fetch.html", "request.html", diff --git a/toolkit/components/antitracking/test/browser/browser_staticPartition_saveAs.js b/toolkit/components/antitracking/test/browser/browser_staticPartition_saveAs.js index b9984829eb..9da751cadb 100644 --- a/toolkit/components/antitracking/test/browser/browser_staticPartition_saveAs.js +++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_saveAs.js @@ -401,7 +401,7 @@ add_task(async function testPageInfoMediaSaveAs() { ); info("Open the media panel of the pageinfo."); - let pageInfo = BrowserPageInfo( + let pageInfo = BrowserCommands.pageInfo( gBrowser.selectedBrowser.currentURI.spec, "mediaTab" ); @@ -478,7 +478,7 @@ add_task(async function testPageInfoMediaMultipleSelectedSaveAs() { ); info("Open the media panel of the pageinfo."); - let pageInfo = BrowserPageInfo( + let pageInfo = BrowserCommands.pageInfo( gBrowser.selectedBrowser.currentURI.spec, "mediaTab" ); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessThirdPartyChecks_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_storageAccessThirdPartyChecks_alwaysPartition.js index f31c7893d0..8ea97dac68 100644 --- a/toolkit/components/antitracking/test/browser/browser_storageAccessThirdPartyChecks_alwaysPartition.js +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessThirdPartyChecks_alwaysPartition.js @@ -53,6 +53,7 @@ AntiTracking._createTask({ "https://tracking.example.org", "https://tracking.example.org", "https://tracking.example.org", + "https://another-tracking.example.net", "https://itisatracker.org", ], }); diff --git a/toolkit/components/antitracking/test/browser/file_saveAsPageInfo.html b/toolkit/components/antitracking/test/browser/file_saveAsPageInfo.html index aa3de2a555..c8c409d130 100644 --- a/toolkit/components/antitracking/test/browser/file_saveAsPageInfo.html +++ b/toolkit/components/antitracking/test/browser/file_saveAsPageInfo.html @@ -1,6 +1,6 @@ <html> <body> <img src="http://example.net/browser/toolkit/components/antitracking/test/browser/raptor.jpg" id="image1"> - <video src="http://example.net/browser/toolkit/components/antitracking/test/browser/file_video.ogv" id="video1"> </video> + <video src="http://example.net/browser/toolkit/components/antitracking/test/browser/file_video.webm" id="video1"> </video> </body> </html> diff --git a/toolkit/components/antitracking/test/browser/file_video.ogv b/toolkit/components/antitracking/test/browser/file_video.ogv Binary files differdeleted file mode 100644 index 68dee3cf2b..0000000000 --- a/toolkit/components/antitracking/test/browser/file_video.ogv +++ /dev/null diff --git a/toolkit/components/antitracking/test/browser/file_video.webm b/toolkit/components/antitracking/test/browser/file_video.webm Binary files differnew file mode 100644 index 0000000000..2cee5a9e51 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/file_video.webm diff --git a/toolkit/components/antitracking/test/gtest/TestStoragePrincipalHelper.cpp b/toolkit/components/antitracking/test/gtest/TestStoragePrincipalHelper.cpp index 44959e4dc8..666757c382 100644 --- a/toolkit/components/antitracking/test/gtest/TestStoragePrincipalHelper.cpp +++ b/toolkit/components/antitracking/test/gtest/TestStoragePrincipalHelper.cpp @@ -45,7 +45,7 @@ nsresult CreateMockChannel(nsIPrincipal* aPrincipal, bool isThirdParty, NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr<nsILoadInfo> mockLoadInfo = mockChannel->LoadInfo(); - rv = mockLoadInfo->SetIsThirdPartyContextToTopWindow(isThirdParty); + rv = mockLoadInfo->SetIsInThirdPartyContext(isThirdParty); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr<nsICookieJarSettings> cjs; diff --git a/toolkit/components/antitracking/test/xpcshell/test_validate_strip_on_share_list.js b/toolkit/components/antitracking/test/xpcshell/test_validate_strip_on_share_list.js index 48a0bd4682..f8be5fccad 100644 --- a/toolkit/components/antitracking/test/xpcshell/test_validate_strip_on_share_list.js +++ b/toolkit/components/antitracking/test/xpcshell/test_validate_strip_on_share_list.js @@ -7,67 +7,19 @@ const { JsonSchema } = ChromeUtils.importESModule( "resource://gre/modules/JsonSchema.sys.mjs" ); -let stripOnShareList; +let stripOnShareMergedList, stripOnShareLGPLParams, stripOnShareList; -// Fetching strip on share list -add_setup(async function () { - /* globals fetch */ - let response = await fetch( - "chrome://global/content/antitracking/StripOnShare.json" - ); +async function fetchAndParseList(fileName) { + let response = await fetch("chrome://global/content/" + fileName); if (!response.ok) { throw new Error( "Error fetching strip-on-share strip list" + response.status ); } - stripOnShareList = await response.json(); -}); - -// Check if the Strip on Share list contains any duplicate params -add_task(async function test_check_duplicates() { - let stripOnShareParams = stripOnShareList; - - const allQueryParams = []; - - for (const domain in stripOnShareParams) { - for (let param in stripOnShareParams[domain].queryParams) { - allQueryParams.push(stripOnShareParams[domain].queryParams[param]); - } - } - - let setOfParams = new Set(allQueryParams); - - if (setOfParams.size != allQueryParams.length) { - let setToCheckDupes = new Set(); - let dupeList = new Set(); - for (const domain in stripOnShareParams) { - for (let param in stripOnShareParams[domain].queryParams) { - let tempParam = stripOnShareParams[domain].queryParams[param]; - - if (setToCheckDupes.has(tempParam)) { - dupeList.add(tempParam); - } else { - setToCheckDupes.add(tempParam); - } - } - } - - Assert.equal( - setOfParams.size, - allQueryParams.length, - "There are duplicates rules. The duplicate rules are " + [...dupeList] - ); - } + return await response.json(); +} - Assert.equal( - setOfParams.size, - allQueryParams.length, - "There are no duplicates rules." - ); -}); - -// Validate the format of Strip on Share list with Schema -add_task(async function test_check_schema() { +async function validateSchema(paramList, nameOfList) { let schema = { $schema: "http://json-schema.org/draft-07/schema#", type: "object", @@ -88,13 +40,86 @@ add_task(async function test_check_schema() { required: ["global"], }; - let stripOnShareParams = stripOnShareList; let validator = new JsonSchema.Validator(schema); - let { valid, errors } = validator.validate(stripOnShareParams); + let { valid, errors } = validator.validate(paramList); if (!valid) { - info("validation errors: " + JSON.stringify(errors, null, 2)); + info( + nameOfList + + " JSON contains these validation errors: " + + JSON.stringify(errors, null, 2) + ); + } + + Assert.ok(valid, nameOfList + " JSON is valid"); +} + +// Fetching strip on share list +add_setup(async function () { + /* globals fetch */ + + [stripOnShareList, stripOnShareLGPLParams] = await Promise.all([ + fetchAndParseList("antitracking/StripOnShare.json"), + fetchAndParseList("antitracking/StripOnShareLGPL.json"), + ]); + + stripOnShareMergedList = stripOnShareList; + + // Combines the mozilla licensed strip on share param + // list and the LGPL licensed strip on share param list + for (const key in stripOnShareLGPLParams) { + if (Object.hasOwn(stripOnShareMergedList, key)) { + stripOnShareMergedList[key].queryParams.push( + ...stripOnShareLGPLParams[key].queryParams + ); + } else { + stripOnShareMergedList[key] = stripOnShareLGPLParams[key]; + } + } +}); + +// Check if the Strip on Share list contains any duplicate params +add_task(async function test_check_duplicates() { + for (const domain in stripOnShareMergedList) { + // Creates a set of query parameters for a given domain to check + // for duplicates + let setOfParams = new Set(stripOnShareMergedList[domain].queryParams); + + // If there are duplicates which is known because the sizes of the set + // and array don't match, then we check which parameters are duplciates + if (setOfParams.size != stripOnShareMergedList[domain].queryParams.length) { + let setToCheckDupes = new Set(); + let dupeList = new Set(); + for (let param in stripOnShareMergedList[domain].queryParams) { + let tempParam = stripOnShareMergedList[domain].queryParams[param]; + + if (setToCheckDupes.has(tempParam)) { + dupeList.add(tempParam); + } else { + setToCheckDupes.add(tempParam); + } + } + + Assert.equal( + setOfParams.size, + stripOnShareMergedList[domain].queryParams.length, + "There are duplicates rules in " + + domain + + ". The duplicate rules are " + + [...dupeList] + ); + } + + Assert.equal( + setOfParams.size, + stripOnShareMergedList[domain].queryParams.length, + "There are no duplicates rules." + ); } +}); - Assert.ok(valid, "Strip on share JSON is valid"); +// Validate the format of Strip on Share list with Schema +add_task(async function test_check_schema() { + validateSchema(stripOnShareLGPLParams, "Strip On Share LGPL"); + validateSchema(stripOnShareList, "Strip On Share"); }); diff --git a/toolkit/components/autocomplete/nsAutoCompleteController.cpp b/toolkit/components/autocomplete/nsAutoCompleteController.cpp index 7c3035271b..126199e3cf 100644 --- a/toolkit/components/autocomplete/nsAutoCompleteController.cpp +++ b/toolkit/components/autocomplete/nsAutoCompleteController.cpp @@ -49,13 +49,11 @@ nsAutoCompleteController::nsAutoCompleteController() mPopupClosedByCompositionStart(false), mProhibitAutoFill(false), mUserClearedAutoFill(false), - mClearingAutoFillSearchesAgain(false), mCompositionState(eCompositionState_None), mSearchStatus(nsAutoCompleteController::STATUS_NONE), mMatchCount(0), mSearchesOngoing(0), mSearchesFailed(0), - mImmediateSearchesCount(0), mCompletedSelectionIndex(-1) {} nsAutoCompleteController::~nsAutoCompleteController() { SetInput(nullptr); } @@ -131,9 +129,6 @@ nsAutoCompleteController::SetInput(nsIAutoCompleteInput* aInput) { input->GetTextValue(value); SetSearchStringInternal(value); - // Since the controller can be used as a service it's important to reset this. - mClearingAutoFillSearchesAgain = false; - return NS_OK; } @@ -243,20 +238,16 @@ nsAutoCompleteController::HandleText(bool* _retval) { newValue.Length() < mPlaceholderCompletionString.Length() && Substring(mPlaceholderCompletionString, 0, newValue.Length()) .Equals(newValue); - bool searchAgainOnAutoFillClear = - mUserClearedAutoFill && mClearingAutoFillSearchesAgain; - if (!handlingCompositionCommit && !searchAgainOnAutoFillClear && - newValue.Length() > 0 && repeatingPreviousSearch) { + if (!handlingCompositionCommit && newValue.Length() > 0 && + repeatingPreviousSearch) { return NS_OK; } - if (userRemovedText || searchAgainOnAutoFillClear) { - if (userRemovedText) { - // We need to throw away previous results so we don't try to search - // through them again. - ClearResults(); - } + if (userRemovedText) { + // We need to throw away previous results so we don't try to search + // through them again. + ClearResults(); mProhibitAutoFill = true; mPlaceholderCompletionString.Truncate(); } else { @@ -848,16 +839,7 @@ MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP nsAutoCompleteController::Notify(nsITimer* timer) { mTimer = nullptr; - - if (mImmediateSearchesCount == 0) { - // If there were no immediate searches, BeforeSearches has not yet been - // called, so do it now. - nsresult rv = BeforeSearches(); - if (NS_FAILED(rv)) return rv; - } - StartSearch(nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_DELAYED); - AfterSearches(); - return NS_OK; + return DoSearches(); } //////////////////////////////////////////////////////////////////////// @@ -928,7 +910,7 @@ nsresult nsAutoCompleteController::BeforeSearches() { return NS_OK; } -nsresult nsAutoCompleteController::StartSearch(uint16_t aSearchType) { +nsresult nsAutoCompleteController::StartSearch() { NS_ENSURE_STATE(mInput); nsCOMPtr<nsIAutoCompleteInput> input = mInput; @@ -940,14 +922,6 @@ nsresult nsAutoCompleteController::StartSearch(uint16_t aSearchType) { for (uint32_t i = 0; i < searchesCopy.Length(); ++i) { nsCOMPtr<nsIAutoCompleteSearch> search = searchesCopy[i]; - // Filter on search type. Not all the searches implement this interface, - // in such a case just consider them delayed. - uint16_t searchType = nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_DELAYED; - nsCOMPtr<nsIAutoCompleteSearchDescriptor> searchDesc = - do_QueryInterface(search); - if (searchDesc) searchDesc->GetSearchType(&searchType); - if (searchType != aSearchType) continue; - nsIAutoCompleteResult* result = mResultCache.SafeObjectAt(i); if (result) { @@ -963,13 +937,6 @@ nsresult nsAutoCompleteController::StartSearch(uint16_t aSearchType) { nsresult rv = input->GetSearchParam(searchParam); if (NS_FAILED(rv)) return rv; - // FormFill expects the searchParam to only contain the input element id, - // other consumers may have other expectations, so this modifies it only - // for new consumers handling autoFill by themselves. - if (mProhibitAutoFill && mClearingAutoFillSearchesAgain) { - searchParam.AppendLiteral(" prohibit-autofill"); - } - uint32_t userContextId; rv = input->GetUserContextId(&userContextId); if (NS_SUCCEEDED(rv) && @@ -1078,7 +1045,6 @@ nsresult nsAutoCompleteController::StartSearches() { input->GetSearchCount(&searchCount); mResults.SetCapacity(searchCount); mSearches.SetCapacity(searchCount); - mImmediateSearchesCount = 0; const char* searchCID = kAutoCompleteSearchCID; @@ -1095,24 +1061,6 @@ nsresult nsAutoCompleteController::StartSearches() { nsCOMPtr<nsIAutoCompleteSearch> search = do_GetService(cid.get()); if (search) { mSearches.AppendObject(search); - - // Count immediate searches. - nsCOMPtr<nsIAutoCompleteSearchDescriptor> searchDesc = - do_QueryInterface(search); - if (searchDesc) { - uint16_t searchType = - nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_DELAYED; - if (NS_SUCCEEDED(searchDesc->GetSearchType(&searchType)) && - searchType == - nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_IMMEDIATE) { - mImmediateSearchesCount++; - } - - if (!mClearingAutoFillSearchesAgain) { - searchDesc->GetClearingAutoFillSearchesAgain( - &mClearingAutoFillSearchesAgain); - } - } } } } @@ -1125,36 +1073,28 @@ nsresult nsAutoCompleteController::StartSearches() { uint32_t timeout; input->GetTimeout(&timeout); - uint32_t immediateSearchesCount = mImmediateSearchesCount; if (timeout == 0) { - // All the searches should be executed immediately. - immediateSearchesCount = mSearches.Length(); + // If the timeout is 0, we still have to execute the delayed searches, + // otherwise this will be a no-op. + return DoSearches(); } - if (immediateSearchesCount > 0) { - nsresult rv = BeforeSearches(); - if (NS_FAILED(rv)) return rv; - StartSearch(nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_IMMEDIATE); - - if (mSearches.Length() == immediateSearchesCount) { - // Either all searches are immediate, or the timeout is 0. In the - // latter case we still have to execute the delayed searches, otherwise - // this will be a no-op. - StartSearch(nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_DELAYED); - - // All the searches have been started, just finish. - AfterSearches(); - return NS_OK; - } - } - - MOZ_ASSERT(timeout > 0, "Trying to delay searches with a 0 timeout!"); - // Now start the delayed searches. return NS_NewTimerWithCallback(getter_AddRefs(mTimer), this, timeout, nsITimer::TYPE_ONE_SHOT); } +nsresult nsAutoCompleteController::DoSearches() { + nsresult rv = BeforeSearches(); + if (NS_FAILED(rv)) return rv; + + StartSearch(); + + // All the searches have been started, just finish. + AfterSearches(); + return NS_OK; +} + nsresult nsAutoCompleteController::ClearSearchTimer() { if (mTimer) { mTimer->Cancel(); diff --git a/toolkit/components/autocomplete/nsAutoCompleteController.h b/toolkit/components/autocomplete/nsAutoCompleteController.h index 7e6d8a3b06..fdaf245f56 100644 --- a/toolkit/components/autocomplete/nsAutoCompleteController.h +++ b/toolkit/components/autocomplete/nsAutoCompleteController.h @@ -54,8 +54,9 @@ class nsAutoCompleteController final : public nsIAutoCompleteController, MOZ_CAN_RUN_SCRIPT nsresult OpenPopup(); MOZ_CAN_RUN_SCRIPT nsresult ClosePopup(); - nsresult StartSearch(uint16_t aSearchType); + nsresult StartSearch(); + MOZ_CAN_RUN_SCRIPT nsresult DoSearches(); nsresult BeforeSearches(); MOZ_CAN_RUN_SCRIPT nsresult StartSearches(); MOZ_CAN_RUN_SCRIPT void AfterSearches(); @@ -182,17 +183,13 @@ class nsAutoCompleteController final : public nsIAutoCompleteController, bool mDefaultIndexCompleted; bool mPopupClosedByCompositionStart; - // Whether autofill is allowed for the next search. May be retrieved by the - // search through the "prohibit-autofill" searchParam. + // Whether autofill is allowed for the next search. bool mProhibitAutoFill; // Indicates whether the user cleared the autofilled part, returning to the // originally entered search string. bool mUserClearedAutoFill; - // Indicates whether clearing the autofilled string should issue a new search. - bool mClearingAutoFillSearchesAgain; - enum CompositionState { eCompositionState_None, eCompositionState_Composing, @@ -203,7 +200,6 @@ class nsAutoCompleteController final : public nsIAutoCompleteController, uint32_t mMatchCount; uint32_t mSearchesOngoing; uint32_t mSearchesFailed; - uint32_t mImmediateSearchesCount; // The index of the match on the popup that was selected using the keyboard, // if the completeselectedindex attribute is set. // This is used to distinguish that selection (which would have been put in diff --git a/toolkit/components/autocomplete/nsIAutoCompleteResult.idl b/toolkit/components/autocomplete/nsIAutoCompleteResult.idl index 8e8b05ea46..7f85b49b82 100644 --- a/toolkit/components/autocomplete/nsIAutoCompleteResult.idl +++ b/toolkit/components/autocomplete/nsIAutoCompleteResult.idl @@ -83,7 +83,7 @@ interface nsIAutoCompleteResult : nsISupports /** * True if the value at the given index is removable. */ - bool isRemovableAt(in long rowIndex); + boolean isRemovableAt(in long rowIndex); /** * Remove the value at the given index from the autocomplete results. diff --git a/toolkit/components/autocomplete/nsIAutoCompleteSearch.idl b/toolkit/components/autocomplete/nsIAutoCompleteSearch.idl index 2545340bf4..ca41be4a36 100644 --- a/toolkit/components/autocomplete/nsIAutoCompleteSearch.idl +++ b/toolkit/components/autocomplete/nsIAutoCompleteSearch.idl @@ -43,26 +43,3 @@ interface nsIAutoCompleteObserver : nsISupports [can_run_script] void onSearchResult(in nsIAutoCompleteSearch search, in nsIAutoCompleteResult result); }; - -[scriptable, uuid(4c3e7462-fbfb-4310-8f4b-239238392b75)] -interface nsIAutoCompleteSearchDescriptor : nsISupports -{ - // The search is started after the timeout specified by the corresponding - // nsIAutoCompleteInput implementation. - const unsigned short SEARCH_TYPE_DELAYED = 0; - // The search is started synchronously, before any delayed searches. - const unsigned short SEARCH_TYPE_IMMEDIATE = 1; - - /** - * Identifies the search behavior. - * Should be one of the SEARCH_TYPE_* constants above. - * Defaults to SEARCH_TYPE_DELAYED. - */ - readonly attribute unsigned short searchType; - - /* - * Whether a new search should be triggered when the user deletes the - * autofilled part. - */ - readonly attribute boolean clearingAutoFillSearchesAgain; -}; diff --git a/toolkit/components/autocomplete/tests/unit/head_autocomplete.js b/toolkit/components/autocomplete/tests/unit/head_autocomplete.js index 6aa0382d82..b11d02a855 100644 --- a/toolkit/components/autocomplete/tests/unit/head_autocomplete.js +++ b/toolkit/components/autocomplete/tests/unit/head_autocomplete.js @@ -101,7 +101,7 @@ AutoCompleteResultBase.prototype = { return this._styles[aIndex]; }, - getImageAt(aIndex) { + getImageAt() { return ""; }, @@ -109,11 +109,11 @@ AutoCompleteResultBase.prototype = { return this._finalCompleteValues[aIndex] || this._values[aIndex]; }, - isRemovableAt(aRowIndex) { + isRemovableAt() { return true; }, - removeValueAt(aRowIndex) {}, + removeValueAt() {}, // nsISupports implementation QueryInterface: ChromeUtils.generateQI(["nsIAutoCompleteResult"]), @@ -161,7 +161,7 @@ function AutocompletePopupBase(input) { AutocompletePopupBase.prototype = { selectedIndex: 0, invalidate() {}, - selectBy(reverse, page) { + selectBy(reverse) { let numRows = this.input.controller.matchCount; if (numRows > 0) { let delta = reverse ? -1 : 1; diff --git a/toolkit/components/autocomplete/tests/unit/test_378079.js b/toolkit/components/autocomplete/tests/unit/test_378079.js index 06819df40b..756783d17f 100644 --- a/toolkit/components/autocomplete/tests/unit/test_378079.js +++ b/toolkit/components/autocomplete/tests/unit/test_378079.js @@ -45,7 +45,7 @@ AutoCompleteInput.prototype = { popupOpen: false, popup: { - setSelectedIndex(aIndex) {}, + setSelectedIndex() {}, invalidate() {}, // nsISupports implementation @@ -100,7 +100,7 @@ AutoCompleteResult.prototype = { return this._styles[aIndex]; }, - getImageAt(aIndex) { + getImageAt() { return ""; }, @@ -108,11 +108,11 @@ AutoCompleteResult.prototype = { return this.getValueAt(aIndex); }, - isRemovableAt(aRowIndex) { + isRemovableAt() { return true; }, - removeValueAt(aRowIndex) {}, + removeValueAt() {}, // nsISupports implementation QueryInterface: ChromeUtils.generateQI(["nsIAutoCompleteResult"]), diff --git a/toolkit/components/autocomplete/tests/unit/test_393191.js b/toolkit/components/autocomplete/tests/unit/test_393191.js index 9a0481718d..c39a7586e9 100644 --- a/toolkit/components/autocomplete/tests/unit/test_393191.js +++ b/toolkit/components/autocomplete/tests/unit/test_393191.js @@ -44,7 +44,7 @@ AutoCompleteInput.prototype = { popupOpen: false, popup: { - setSelectedIndex(aIndex) {}, + setSelectedIndex() {}, invalidate() {}, // nsISupports implementation @@ -99,7 +99,7 @@ AutoCompleteResult.prototype = { return this._styles[aIndex]; }, - getImageAt(aIndex) { + getImageAt() { return ""; }, @@ -107,11 +107,11 @@ AutoCompleteResult.prototype = { return this.getValueAt(aIndex); }, - isRemovableAt(aRowIndex) { + isRemovableAt() { return true; }, - removeValueAt(aRowIndex) {}, + removeValueAt() {}, // nsISupports implementation QueryInterface: ChromeUtils.generateQI(["nsIAutoCompleteResult"]), diff --git a/toolkit/components/autocomplete/tests/unit/test_440866.js b/toolkit/components/autocomplete/tests/unit/test_440866.js index ef538cd931..fdc7850b28 100644 --- a/toolkit/components/autocomplete/tests/unit/test_440866.js +++ b/toolkit/components/autocomplete/tests/unit/test_440866.js @@ -43,7 +43,7 @@ AutoCompleteInput.prototype = { popupOpen: false, popup: { - setSelectedIndex(aIndex) {}, + setSelectedIndex() {}, invalidate() {}, // nsISupports implementation @@ -98,7 +98,7 @@ AutoCompleteResult.prototype = { return this._styles[aIndex]; }, - getImageAt(aIndex) { + getImageAt() { return ""; }, @@ -106,11 +106,11 @@ AutoCompleteResult.prototype = { return this.getValueAt(aIndex); }, - isRemovableAt(aRowIndex) { + isRemovableAt() { return true; }, - removeValueAt(aRowIndex) {}, + removeValueAt() {}, // nsISupports implementation QueryInterface: ChromeUtils.generateQI(["nsIAutoCompleteResult"]), diff --git a/toolkit/components/autocomplete/tests/unit/test_autocomplete_multiple.js b/toolkit/components/autocomplete/tests/unit/test_autocomplete_multiple.js index 68885dc4ab..d94da5ae59 100644 --- a/toolkit/components/autocomplete/tests/unit/test_autocomplete_multiple.js +++ b/toolkit/components/autocomplete/tests/unit/test_autocomplete_multiple.js @@ -40,7 +40,7 @@ AutoCompleteInput.prototype = { popupOpen: false, popup: { - setSelectedIndex(aIndex) {}, + setSelectedIndex() {}, invalidate() {}, // nsISupports implementation @@ -92,7 +92,7 @@ AutoCompleteResult.prototype = { return this._styles[aIndex]; }, - getImageAt(aIndex) { + getImageAt() { return ""; }, @@ -100,11 +100,11 @@ AutoCompleteResult.prototype = { return this.getValueAt(aIndex); }, - isRemovableAt(aRowIndex) { + isRemovableAt() { return true; }, - removeValueAt(aRowIndex) {}, + removeValueAt() {}, // nsISupports implementation QueryInterface: ChromeUtils.generateQI(["nsIAutoCompleteResult"]), diff --git a/toolkit/components/autocomplete/tests/unit/test_autocomplete_userContextId.js b/toolkit/components/autocomplete/tests/unit/test_autocomplete_userContextId.js index 4fbec408dc..6862e63ede 100644 --- a/toolkit/components/autocomplete/tests/unit/test_autocomplete_userContextId.js +++ b/toolkit/components/autocomplete/tests/unit/test_autocomplete_userContextId.js @@ -21,12 +21,7 @@ function doSearch(aString, aUserContextId) { return new Promise(resolve => { let search = new AutoCompleteSearch("test"); - search.startSearch = function ( - aSearchString, - aSearchParam, - aPreviousResult, - aListener - ) { + search.startSearch = function (aSearchString, aSearchParam) { unregisterAutoCompleteSearch(search); resolve(aSearchParam); }; diff --git a/toolkit/components/autocomplete/tests/unit/test_immediate_search.js b/toolkit/components/autocomplete/tests/unit/test_immediate_search.js deleted file mode 100644 index a4524ca5c9..0000000000 --- a/toolkit/components/autocomplete/tests/unit/test_immediate_search.js +++ /dev/null @@ -1,174 +0,0 @@ -/* 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/. */ - -function AutoCompleteImmediateSearch(aName, aResult) { - this.name = aName; - this._result = aResult; -} -AutoCompleteImmediateSearch.prototype = Object.create( - AutoCompleteSearchBase.prototype -); -AutoCompleteImmediateSearch.prototype.searchType = - Ci.nsIAutoCompleteSearchDescriptor.SEARCH_TYPE_IMMEDIATE; -AutoCompleteImmediateSearch.prototype.QueryInterface = ChromeUtils.generateQI([ - "nsIFactory", - "nsIAutoCompleteSearch", - "nsIAutoCompleteSearchDescriptor", -]); - -function AutoCompleteDelayedSearch(aName, aResult) { - this.name = aName; - this._result = aResult; -} -AutoCompleteDelayedSearch.prototype = Object.create( - AutoCompleteSearchBase.prototype -); - -function AutoCompleteResult(aValues, aDefaultIndex) { - this._values = aValues; - this.defaultIndex = aDefaultIndex; -} -AutoCompleteResult.prototype = Object.create(AutoCompleteResultBase.prototype); - -/** - * An immediate search should be executed synchronously. - */ -add_test(function test_immediate_search() { - let inputStr = "moz"; - - let immediateSearch = new AutoCompleteImmediateSearch( - "immediate", - new AutoCompleteResult(["moz-immediate"], 0) - ); - registerAutoCompleteSearch(immediateSearch); - let delayedSearch = new AutoCompleteDelayedSearch( - "delayed", - new AutoCompleteResult(["moz-delayed"], 0) - ); - registerAutoCompleteSearch(delayedSearch); - - let controller = Cc["@mozilla.org/autocomplete/controller;1"].getService( - Ci.nsIAutoCompleteController - ); - - let input = new AutoCompleteInputBase([ - delayedSearch.name, - immediateSearch.name, - ]); - input.completeDefaultIndex = true; - input.textValue = inputStr; - - // Caret must be at the end. Autofill doesn't happen unless you're typing - // characters at the end. - let strLen = inputStr.length; - input.selectTextRange(strLen, strLen); - - controller.input = input; - controller.startSearch(inputStr); - - // Immediately check the result, the immediate search should have finished. - Assert.equal(input.textValue, "moz-immediate"); - - // Wait for both queries to finish. - input.onSearchComplete = function () { - // Sanity check. - Assert.equal(input.textValue, "moz-immediate"); - - unregisterAutoCompleteSearch(immediateSearch); - unregisterAutoCompleteSearch(delayedSearch); - run_next_test(); - }; -}); - -/** - * An immediate search should be executed before any delayed search. - */ -add_test(function test_immediate_search_notimeout() { - let inputStr = "moz"; - - let immediateSearch = new AutoCompleteImmediateSearch( - "immediate", - new AutoCompleteResult(["moz-immediate"], 0) - ); - registerAutoCompleteSearch(immediateSearch); - - let delayedSearch = new AutoCompleteDelayedSearch( - "delayed", - new AutoCompleteResult(["moz-delayed"], 0) - ); - registerAutoCompleteSearch(delayedSearch); - - let controller = Cc["@mozilla.org/autocomplete/controller;1"].getService( - Ci.nsIAutoCompleteController - ); - - let input = new AutoCompleteInputBase([ - delayedSearch.name, - immediateSearch.name, - ]); - input.completeDefaultIndex = true; - input.textValue = inputStr; - input.timeout = 0; - - // Caret must be at the end. Autofill doesn't happen unless you're typing - // characters at the end. - let strLen = inputStr.length; - input.selectTextRange(strLen, strLen); - - controller.input = input; - let complete = false; - input.onSearchComplete = function () { - complete = true; - }; - controller.startSearch(inputStr); - Assert.ok(complete); - - // Immediately check the result, the immediate search should have finished. - Assert.equal(input.textValue, "moz-immediate"); - - unregisterAutoCompleteSearch(immediateSearch); - unregisterAutoCompleteSearch(delayedSearch); - run_next_test(); -}); - -/** - * A delayed search should be executed synchronously with a zero timeout. - */ -add_test(function test_delayed_search_notimeout() { - let inputStr = "moz"; - - let delayedSearch = new AutoCompleteDelayedSearch( - "delayed", - new AutoCompleteResult(["moz-delayed"], 0) - ); - registerAutoCompleteSearch(delayedSearch); - - let controller = Cc["@mozilla.org/autocomplete/controller;1"].getService( - Ci.nsIAutoCompleteController - ); - - let input = new AutoCompleteInputBase([delayedSearch.name]); - input.completeDefaultIndex = true; - input.textValue = inputStr; - input.timeout = 0; - - // Caret must be at the end. Autofill doesn't happen unless you're typing - // characters at the end. - let strLen = inputStr.length; - input.selectTextRange(strLen, strLen); - - controller.input = input; - let complete = false; - input.onSearchComplete = function () { - complete = true; - }; - controller.startSearch(inputStr); - Assert.ok(complete); - - // Immediately check the result, the delayed search should have finished. - Assert.equal(input.textValue, "moz-delayed"); - - unregisterAutoCompleteSearch(delayedSearch); - run_next_test(); -}); diff --git a/toolkit/components/autocomplete/tests/unit/test_previousResult.js b/toolkit/components/autocomplete/tests/unit/test_previousResult.js index 85db3f08c5..e015ff481e 100644 --- a/toolkit/components/autocomplete/tests/unit/test_previousResult.js +++ b/toolkit/components/autocomplete/tests/unit/test_previousResult.js @@ -45,7 +45,7 @@ AutoCompleteInput.prototype = { popupOpen: false, popup: { - setSelectedIndex(aIndex) {}, + setSelectedIndex() {}, invalidate() {}, // nsISupports implementation @@ -100,7 +100,7 @@ AutoCompleteResult.prototype = { return this._styles[aIndex]; }, - getImageAt(aIndex) { + getImageAt() { return ""; }, @@ -108,11 +108,11 @@ AutoCompleteResult.prototype = { return this.getValueAt(aIndex); }, - isRemovableAt(aRowIndex) { + isRemovableAt() { return true; }, - removeValueAt(aRowIndex) {}, + removeValueAt() {}, // nsISupports implementation QueryInterface: ChromeUtils.generateQI(["nsIAutoCompleteResult"]), diff --git a/toolkit/components/autocomplete/tests/unit/test_search_zerotimeout.js b/toolkit/components/autocomplete/tests/unit/test_search_zerotimeout.js new file mode 100644 index 0000000000..a5a0f9aa0f --- /dev/null +++ b/toolkit/components/autocomplete/tests/unit/test_search_zerotimeout.js @@ -0,0 +1,58 @@ +/* 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/. */ + +function AutoCompleteDelayedSearch(aName, aResult) { + this.name = aName; + this._result = aResult; +} +AutoCompleteDelayedSearch.prototype = Object.create( + AutoCompleteSearchBase.prototype +); + +function AutoCompleteResult(aValues, aDefaultIndex) { + this._values = aValues; + this.defaultIndex = aDefaultIndex; +} +AutoCompleteResult.prototype = Object.create(AutoCompleteResultBase.prototype); + +/** + * A delayed search should be executed synchronously with a zero timeout. + */ +add_test(function test_delayed_search_notimeout() { + let inputStr = "moz"; + + let delayedSearch = new AutoCompleteDelayedSearch( + "delayed", + new AutoCompleteResult(["moz-delayed"], 0) + ); + registerAutoCompleteSearch(delayedSearch); + + let controller = Cc["@mozilla.org/autocomplete/controller;1"].getService( + Ci.nsIAutoCompleteController + ); + + let input = new AutoCompleteInputBase([delayedSearch.name]); + input.completeDefaultIndex = true; + input.textValue = inputStr; + input.timeout = 0; + + // Caret must be at the end. Autofill doesn't happen unless you're typing + // characters at the end. + let strLen = inputStr.length; + input.selectTextRange(strLen, strLen); + + controller.input = input; + let complete = false; + input.onSearchComplete = function () { + complete = true; + }; + controller.startSearch(inputStr); + Assert.ok(complete); + + // Immediately check the result, the delayed search should have finished. + Assert.equal(input.textValue, "moz-delayed"); + + unregisterAutoCompleteSearch(delayedSearch); + run_next_test(); +}); diff --git a/toolkit/components/autocomplete/tests/unit/test_stopSearch.js b/toolkit/components/autocomplete/tests/unit/test_stopSearch.js index aecf572df0..01bd67d63b 100644 --- a/toolkit/components/autocomplete/tests/unit/test_stopSearch.js +++ b/toolkit/components/autocomplete/tests/unit/test_stopSearch.js @@ -60,7 +60,7 @@ function AutoCompleteSearch(aName) { AutoCompleteSearch.prototype = { constructor: AutoCompleteSearch, stopSearchInvoked: true, - startSearch(aSearchString, aSearchParam, aPreviousResult, aListener) { + startSearch() { info("Check stop search has been called"); Assert.ok(this.stopSearchInvoked); this.stopSearchInvoked = false; diff --git a/toolkit/components/autocomplete/tests/unit/xpcshell.toml b/toolkit/components/autocomplete/tests/unit/xpcshell.toml index 00d238cdf7..0c2e12495e 100644 --- a/toolkit/components/autocomplete/tests/unit/xpcshell.toml +++ b/toolkit/components/autocomplete/tests/unit/xpcshell.toml @@ -33,12 +33,12 @@ head = "head_autocomplete.js" ["test_finalDefaultCompleteValue.js"] -["test_immediate_search.js"] - ["test_insertMatchAt.js"] ["test_previousResult.js"] ["test_removeMatchAt.js"] +["test_search_zerotimeout.js"] + ["test_stopSearch.js"] diff --git a/toolkit/components/backgroundhangmonitor/BHRTelemetryService.sys.mjs b/toolkit/components/backgroundhangmonitor/BHRTelemetryService.sys.mjs index 98c1274b5c..c675526559 100644 --- a/toolkit/components/backgroundhangmonitor/BHRTelemetryService.sys.mjs +++ b/toolkit/components/backgroundhangmonitor/BHRTelemetryService.sys.mjs @@ -142,7 +142,7 @@ BHRTelemetryService.prototype = Object.freeze({ this.submit(); }, - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { switch (aTopic) { case "profile-after-change": this.resetPayload(); diff --git a/toolkit/components/backgroundhangmonitor/nsIHangDetails.idl b/toolkit/components/backgroundhangmonitor/nsIHangDetails.idl index f9c6ba2de0..df5fcebea8 100644 --- a/toolkit/components/backgroundhangmonitor/nsIHangDetails.idl +++ b/toolkit/components/backgroundhangmonitor/nsIHangDetails.idl @@ -24,7 +24,7 @@ interface nsIHangDetails : nsISupports * The hang was persisted to disk as a permahang, so we can clear the * permahang file once we submit this. */ - readonly attribute bool wasPersisted; + readonly attribute boolean wasPersisted; /** * The detected duration of the hang in milliseconds. diff --git a/toolkit/components/backgroundtasks/BackgroundTask_message.sys.mjs b/toolkit/components/backgroundtasks/BackgroundTask_message.sys.mjs index 6fc1d789b9..aafe4a6dcf 100644 --- a/toolkit/components/backgroundtasks/BackgroundTask_message.sys.mjs +++ b/toolkit/components/backgroundtasks/BackgroundTask_message.sys.mjs @@ -82,10 +82,7 @@ outputInfo = (sentinel, info) => { dump(`${sentinel}${JSON.stringify(info)}${sentinel}\n`); }; -function monkeyPatchRemoteSettingsClient({ - last_modified = new Date().getTime(), - data = [], -}) { +function monkeyPatchRemoteSettingsClient({ data = [] }) { lazy.RemoteSettingsClient.prototype.get = async (options = {}) => { outputInfo({ "RemoteSettingsClient.get": { options, response: { data } } }); return data; diff --git a/toolkit/components/backgroundtasks/BackgroundTasksManager.sys.mjs b/toolkit/components/backgroundtasks/BackgroundTasksManager.sys.mjs index dcf17625f7..97b2f4d93d 100644 --- a/toolkit/components/backgroundtasks/BackgroundTasksManager.sys.mjs +++ b/toolkit/components/backgroundtasks/BackgroundTasksManager.sys.mjs @@ -172,7 +172,7 @@ export class BackgroundTasksManager { const waitFlag = commandLine.findFlag("wait-for-jsdebugger", CASE_INSENSITIVE) != -1; if (waitFlag) { - function onDevtoolsThreadReady(subject, topic, data) { + function onDevtoolsThreadReady(subject, topic) { lazy.log.info( `${Services.appinfo.processID}: Setting breakpoints for background task named '${name}'` + ` (with ${commandLine.length} arguments)` diff --git a/toolkit/components/backgroundtasks/BackgroundTasksUtils.sys.mjs b/toolkit/components/backgroundtasks/BackgroundTasksUtils.sys.mjs index 9521af5a56..2ee2f085ae 100644 --- a/toolkit/components/backgroundtasks/BackgroundTasksUtils.sys.mjs +++ b/toolkit/components/backgroundtasks/BackgroundTasksUtils.sys.mjs @@ -193,7 +193,7 @@ export var BackgroundTasksUtils = { lazy.log.info(`readPreferences: profile is locked`); let prefs = {}; - let addPref = (kind, name, value, sticky, locked) => { + let addPref = (kind, name, value) => { if (predicate && !predicate(name)) { return; } diff --git a/toolkit/components/backgroundtasks/tests/BackgroundTask_crash.sys.mjs b/toolkit/components/backgroundtasks/tests/BackgroundTask_crash.sys.mjs index 10764bc1f7..7afc94ed47 100644 --- a/toolkit/components/backgroundtasks/tests/BackgroundTask_crash.sys.mjs +++ b/toolkit/components/backgroundtasks/tests/BackgroundTask_crash.sys.mjs @@ -4,7 +4,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ export async function runBackgroundTask(commandLine) { - // This task depends on `CrashTestUtils.jsm` and requires the + // This task depends on `CrashTestUtils.sys.mjs` and requires the // sibling `testcrasher` library to be in the current working // directory. Fail right away if we can't find the module or the // native library. diff --git a/toolkit/components/backgroundtasks/tests/BackgroundTask_jsdebugger.sys.mjs b/toolkit/components/backgroundtasks/tests/BackgroundTask_jsdebugger.sys.mjs index 45cf00a449..9479a404ff 100644 --- a/toolkit/components/backgroundtasks/tests/BackgroundTask_jsdebugger.sys.mjs +++ b/toolkit/components/backgroundtasks/tests/BackgroundTask_jsdebugger.sys.mjs @@ -11,7 +11,7 @@ * code to exit code 0. */ -export function runBackgroundTask(commandLine) { +export function runBackgroundTask() { // In the future, will be modifed by the JS debugger (to 0, success). var exposedExitCode = 0; diff --git a/toolkit/components/backgroundtasks/tests/BackgroundTask_shouldprocessupdates.sys.mjs b/toolkit/components/backgroundtasks/tests/BackgroundTask_shouldprocessupdates.sys.mjs index 4030f42e5d..0b47970dee 100644 --- a/toolkit/components/backgroundtasks/tests/BackgroundTask_shouldprocessupdates.sys.mjs +++ b/toolkit/components/backgroundtasks/tests/BackgroundTask_shouldprocessupdates.sys.mjs @@ -3,7 +3,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -export async function runBackgroundTask(commandLine) { +export async function runBackgroundTask() { const get = Services.env.get("MOZ_TEST_PROCESS_UPDATES"); let exitCode = 81; if (get == "ShouldNotProcessUpdates(): OtherInstanceRunning") { diff --git a/toolkit/components/backgroundtasks/tests/BackgroundTask_targeting.sys.mjs b/toolkit/components/backgroundtasks/tests/BackgroundTask_targeting.sys.mjs index fd47d18470..b7a9d08896 100644 --- a/toolkit/components/backgroundtasks/tests/BackgroundTask_targeting.sys.mjs +++ b/toolkit/components/backgroundtasks/tests/BackgroundTask_targeting.sys.mjs @@ -20,7 +20,7 @@ const EXCLUDED_NAMES = [ * Return 0 (success) if all targeting getters succeed, 11 (failure) * otherwise. */ -export async function runBackgroundTask(commandLine) { +export async function runBackgroundTask() { let exitCode = EXIT_CODE.SUCCESS; // Can't use `ASRouterTargeting.getEnvironmentSnapshot`, since that diff --git a/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_experiments.js b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_experiments.js index 9d834bfd5b..cb4f3bb957 100644 --- a/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_experiments.js +++ b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_experiments.js @@ -136,8 +136,8 @@ async function doMessage({ extraArgs = [], extraEnv = {} } = {}) { // i.e., persisted. Verify that messages are shown until we hit the lifetime // frequency caps. // -// It's awkward to inspect the `ASRouter.jsm` internal state directly in this -// manner, but this is the pattern for testing such things at the time of +// It's awkward to inspect the `ASRouter.sys.mjs` internal state directly in +// this manner, but this is the pattern for testing such things at the time of // writing. add_task(async function test_backgroundtask_caps() { let experimentFile = do_get_file("experiment.json"); diff --git a/toolkit/components/bitsdownload/bits_client/bits/src/wide.rs b/toolkit/components/bitsdownload/bits_client/bits/src/wide.rs index c108f8d629..b4f3157801 100644 --- a/toolkit/components/bitsdownload/bits_client/bits/src/wide.rs +++ b/toolkit/components/bitsdownload/bits_client/bits/src/wide.rs @@ -6,9 +6,8 @@ // Minimal null-terminated wide string support from wio. -use std::ffi::{OsStr, OsString}; -use std::os::windows::ffi::{OsStrExt, OsStringExt}; -use std::slice; +use std::ffi::OsStr; +use std::os::windows::ffi::OsStrExt; pub trait ToWideNull { fn to_wide_null(&self) -> Vec<u16>; @@ -19,20 +18,3 @@ impl<T: AsRef<OsStr>> ToWideNull for T { self.as_ref().encode_wide().chain(Some(0)).collect() } } - -pub trait FromWidePtrNull { - unsafe fn from_wide_ptr_null(wide: *const u16) -> Self; -} - -impl FromWidePtrNull for OsString { - unsafe fn from_wide_ptr_null(wide: *const u16) -> Self { - assert!(!wide.is_null()); - - for i in 0.. { - if *wide.offset(i) == 0 { - return Self::from_wide(&slice::from_raw_parts(wide, i as usize)); - } - } - unreachable!() - } -} diff --git a/toolkit/components/bitsdownload/src/bits_interface/action.rs b/toolkit/components/bitsdownload/src/bits_interface/action.rs index 1019e29827..c73d916a07 100644 --- a/toolkit/components/bitsdownload/src/bits_interface/action.rs +++ b/toolkit/components/bitsdownload/src/bits_interface/action.rs @@ -11,7 +11,6 @@ //! A value of type `ServiceAction` or `RequestAction` can easily be converted //! to an `Action` using the `into()` method. -use std::convert::From; use xpcom::interfaces::nsIBits; #[derive(Debug, PartialEq, Eq, Clone, Copy)] diff --git a/toolkit/components/bitsdownload/src/bits_interface/error.rs b/toolkit/components/bitsdownload/src/bits_interface/error.rs index c046ae5e70..bed6975f9d 100644 --- a/toolkit/components/bitsdownload/src/bits_interface/error.rs +++ b/toolkit/components/bitsdownload/src/bits_interface/error.rs @@ -13,7 +13,6 @@ use bits_client::{ use comedy::error::HResult as ComedyError; use nserror::{nsresult, NS_ERROR_FAILURE}; use nsstring::nsCString; -use std::convert::From; use xpcom::interfaces::nsIBits; #[derive(Debug, PartialEq, Clone, Copy)] diff --git a/toolkit/components/browser/nsIWebBrowserChromeFocus.idl b/toolkit/components/browser/nsIWebBrowserChromeFocus.idl index ee24155b51..077ce39201 100644 --- a/toolkit/components/browser/nsIWebBrowserChromeFocus.idl +++ b/toolkit/components/browser/nsIWebBrowserChromeFocus.idl @@ -21,12 +21,12 @@ interface nsIWebBrowserChromeFocus : nsISupports * focus the parent window. */ - void focusNextElement(in bool aForDocumentNavigation); + void focusNextElement(in boolean aForDocumentNavigation); /** * Set the focus at the previous focusable element in the chrome. */ - void focusPrevElement(in bool aForDocumentNavigation); + void focusPrevElement(in boolean aForDocumentNavigation); }; diff --git a/toolkit/components/browser/nsIWebBrowserPrint.idl b/toolkit/components/browser/nsIWebBrowserPrint.idl index 5a900b7b65..2b5d7669bc 100644 --- a/toolkit/components/browser/nsIWebBrowserPrint.idl +++ b/toolkit/components/browser/nsIWebBrowserPrint.idl @@ -30,7 +30,7 @@ native PrintPreviewResolver(std::function<void(const mozilla::dom::PrintPreviewR * nsIWebBrowserPrint corresponds to the main interface * for printing an embedded Gecko web browser window/document */ -[scriptable, uuid(c9a934ed-fff1-4971-bfba-6c25ad70e1e6)] +[scriptable, builtinclass, uuid(c9a934ed-fff1-4971-bfba-6c25ad70e1e6)] interface nsIWebBrowserPrint : nsISupports { /** @@ -85,6 +85,13 @@ interface nsIWebBrowserPrint : nsISupports */ readonly attribute long printPreviewCurrentPageNumber; + /* + * Whether the document to print needs to have its window closed after printing + * is done - see OPEN_PRINT_BROWSER and specifically handleStaticCloneCreatedForPrint(). + * This is set if the document is not a static clone. + */ + [infallible] attribute boolean closeWindowAfterPrint; + /** * Print the specified DOM window * diff --git a/toolkit/components/captivedetect/CaptiveDetect.sys.mjs b/toolkit/components/captivedetect/CaptiveDetect.sys.mjs index 9008fe8a08..5b191eccd8 100644 --- a/toolkit/components/captivedetect/CaptiveDetect.sys.mjs +++ b/toolkit/components/captivedetect/CaptiveDetect.sys.mjs @@ -56,7 +56,7 @@ function URLFetcher(url, timeout) { xhr.onerror = function () { self.onerror(); }; - xhr.onreadystatechange = function (oEvent) { + xhr.onreadystatechange = function () { if (xhr.readyState === 4) { if (self._isAborted) { return; @@ -181,10 +181,7 @@ function LoginObserver(captivePortalDetector) { observeActivity: function observeActivity( aHttpChannel, aActivityType, - aActivitySubtype, - aTimestamp, - aExtraSizeData, - aExtraStringData + aActivitySubtype ) { if ( aActivityType === @@ -541,5 +538,5 @@ if (DEBUG) { }; } else { // eslint-disable-next-line no-global-assign - debug = function (s) {}; + debug = function () {}; } diff --git a/toolkit/components/captivedetect/nsICaptivePortalDetector.idl b/toolkit/components/captivedetect/nsICaptivePortalDetector.idl index ba8fff416b..774c2dd284 100644 --- a/toolkit/components/captivedetect/nsICaptivePortalDetector.idl +++ b/toolkit/components/captivedetect/nsICaptivePortalDetector.idl @@ -16,7 +16,7 @@ interface nsICaptivePortalCallback : nsISupports /** * Invoke callbacks after captive portal detection finished. */ - void complete(in bool success); + void complete(in boolean success); }; [scriptable, uuid(2f827c5a-f551-477f-af09-71adbfbd854a)] diff --git a/toolkit/components/captivedetect/test/unit/test_abort.js b/toolkit/components/captivedetect/test/unit/test_abort.js index 0e0a944f9b..5cb649b4c0 100644 --- a/toolkit/components/captivedetect/test/unit/test_abort.js +++ b/toolkit/components/captivedetect/test/unit/test_abort.js @@ -21,7 +21,7 @@ function xhr_handler(metadata, response) { } function fakeUIResponse() { - Services.obs.addObserver(function observe(subject, topic, data) { + Services.obs.addObserver(function observe(subject, topic) { if (topic === "captive-portal-login") { do_throw("should not receive captive-portal-login event"); } @@ -37,7 +37,7 @@ function test_abort() { Assert.equal(++step, 1); gCaptivePortalDetector.finishPreparation(kInterfaceName); }, - complete: function complete(success) { + complete: function complete() { do_throw("should not execute |complete| callback"); }, }; diff --git a/toolkit/components/captivedetect/test/unit/test_abort_during_user_login.js b/toolkit/components/captivedetect/test/unit/test_abort_during_user_login.js index bd0011817d..a933caee55 100644 --- a/toolkit/components/captivedetect/test/unit/test_abort_during_user_login.js +++ b/toolkit/components/captivedetect/test/unit/test_abort_during_user_login.js @@ -51,7 +51,7 @@ function test_abort() { Assert.equal(++step, 1); gCaptivePortalDetector.finishPreparation(kInterfaceName); }, - complete: function complete(success) { + complete: function complete() { do_throw("should not execute |complete| callback"); }, }; diff --git a/toolkit/components/captivedetect/test/unit/test_abort_ongoing_request.js b/toolkit/components/captivedetect/test/unit/test_abort_ongoing_request.js index b209e02796..187e27bc5d 100644 --- a/toolkit/components/captivedetect/test/unit/test_abort_ongoing_request.js +++ b/toolkit/components/captivedetect/test/unit/test_abort_ongoing_request.js @@ -22,7 +22,7 @@ function xhr_handler(metadata, response) { } function fakeUIResponse() { - Services.obs.addObserver(function observe(subject, topic, data) { + Services.obs.addObserver(function observe(subject, topic) { if (topic === "captive-portal-login") { let xhr = new XMLHttpRequest(); xhr.open("GET", gServerURL + kCanonicalSitePath, true); @@ -42,7 +42,7 @@ function test_multiple_requests_abort() { Assert.equal(++step, 1); gCaptivePortalDetector.finishPreparation(kInterfaceName); }, - complete: function complete(success) { + complete: function complete() { do_throw("should not execute |complete| callback for " + kInterfaceName); }, }; diff --git a/toolkit/components/captivedetect/test/unit/test_abort_pending_request.js b/toolkit/components/captivedetect/test/unit/test_abort_pending_request.js index 16d621a06b..db6d37d904 100644 --- a/toolkit/components/captivedetect/test/unit/test_abort_pending_request.js +++ b/toolkit/components/captivedetect/test/unit/test_abort_pending_request.js @@ -22,7 +22,7 @@ function xhr_handler(metadata, response) { } function fakeUIResponse() { - Services.obs.addObserver(function observe(subject, topic, data) { + Services.obs.addObserver(function observe(subject, topic) { if (topic === "captive-portal-login") { let xhr = new XMLHttpRequest(); xhr.open("GET", gServerURL + kCanonicalSitePath, true); @@ -56,7 +56,7 @@ function test_abort() { "should not execute |prepare| callback for " + kOtherInterfaceName ); }, - complete: function complete(success) { + complete: function complete() { do_throw("should not execute |complete| callback for " + kInterfaceName); }, }; diff --git a/toolkit/components/captivedetect/test/unit/test_captive_portal_found.js b/toolkit/components/captivedetect/test/unit/test_captive_portal_found.js index 2a9acb8ab0..056bf072ee 100644 --- a/toolkit/components/captivedetect/test/unit/test_captive_portal_found.js +++ b/toolkit/components/captivedetect/test/unit/test_captive_portal_found.js @@ -21,7 +21,7 @@ function xhr_handler(metadata, response) { } function fakeUIResponse() { - Services.obs.addObserver(function observe(subject, topic, data) { + Services.obs.addObserver(function observe(subject, topic) { if (topic === "captive-portal-login") { let xhr = new XMLHttpRequest(); xhr.open("GET", gServerURL + kCanonicalSitePath, true); @@ -31,7 +31,7 @@ function fakeUIResponse() { } }, "captive-portal-login"); - Services.obs.addObserver(function observe(subject, topic, data) { + Services.obs.addObserver(function observe(subject, topic) { if (topic === "captive-portal-login-success") { Assert.equal(++step, 4); gServer.stop(do_test_finished); diff --git a/toolkit/components/captivedetect/test/unit/test_captive_portal_found_303.js b/toolkit/components/captivedetect/test/unit/test_captive_portal_found_303.js index 1e4ec1f97d..deb80bc67c 100644 --- a/toolkit/components/captivedetect/test/unit/test_captive_portal_found_303.js +++ b/toolkit/components/captivedetect/test/unit/test_captive_portal_found_303.js @@ -26,7 +26,7 @@ function xhr_handler(metadata, response) { } function fakeUIResponse() { - Services.obs.addObserver(function observe(subject, topic, data) { + Services.obs.addObserver(function observe(subject, topic) { if (topic === "captive-portal-login") { let xhr = new XMLHttpRequest(); xhr.open("GET", gServerURL + kCanonicalSitePath, true); @@ -36,7 +36,7 @@ function fakeUIResponse() { } }, "captive-portal-login"); - Services.obs.addObserver(function observe(subject, topic, data) { + Services.obs.addObserver(function observe(subject, topic) { if (topic === "captive-portal-login-success") { Assert.equal(++step, 4); gServer.stop(function () { diff --git a/toolkit/components/captivedetect/test/unit/test_captive_portal_not_found.js b/toolkit/components/captivedetect/test/unit/test_captive_portal_not_found.js index 25dd5f7c99..4f8c9615dc 100644 --- a/toolkit/components/captivedetect/test/unit/test_captive_portal_not_found.js +++ b/toolkit/components/captivedetect/test/unit/test_captive_portal_not_found.js @@ -19,7 +19,7 @@ function xhr_handler(metadata, response) { } function fakeUIResponse() { - Services.obs.addObserver(function observe(subject, topic, data) { + Services.obs.addObserver(function observe(subject, topic) { if (topic == "captive-portal-login") { do_throw("should not receive captive-portal-login event"); } diff --git a/toolkit/components/captivedetect/test/unit/test_captive_portal_not_found_404.js b/toolkit/components/captivedetect/test/unit/test_captive_portal_not_found_404.js index 3f9d212a1f..c6608f9548 100644 --- a/toolkit/components/captivedetect/test/unit/test_captive_portal_not_found_404.js +++ b/toolkit/components/captivedetect/test/unit/test_captive_portal_not_found_404.js @@ -15,7 +15,7 @@ function xhr_handler(metadata, response) { } function fakeUIResponse() { - Services.obs.addObserver(function observe(subject, topic, data) { + Services.obs.addObserver(function observe(subject, topic) { if (topic === "captive-portal-login") { do_throw("should not receive captive-portal-login event"); } diff --git a/toolkit/components/captivedetect/test/unit/test_multiple_requests.js b/toolkit/components/captivedetect/test/unit/test_multiple_requests.js index 7778a595b0..4b6d6511b2 100644 --- a/toolkit/components/captivedetect/test/unit/test_multiple_requests.js +++ b/toolkit/components/captivedetect/test/unit/test_multiple_requests.js @@ -23,7 +23,7 @@ function xhr_handler(metadata, response) { } function fakeUIResponse() { - Services.obs.addObserver(function observe(subject, topic, data) { + Services.obs.addObserver(function observe(subject, topic) { if (topic === "captive-portal-login") { let xhr = new XMLHttpRequest(); xhr.open("GET", gServerURL + kCanonicalSitePath, true); @@ -33,7 +33,7 @@ function fakeUIResponse() { } }, "captive-portal-login"); - Services.obs.addObserver(function observe(subject, topic, data) { + Services.obs.addObserver(function observe(subject, topic) { if (topic === "captive-portal-login-success") { loginSuccessCount++; if (loginSuccessCount > 1) { diff --git a/toolkit/components/certviewer/content/certviewer.mjs b/toolkit/components/certviewer/content/certviewer.mjs index 3e48a069f5..ba1c6c9144 100644 --- a/toolkit/components/certviewer/content/certviewer.mjs +++ b/toolkit/components/certviewer/content/certviewer.mjs @@ -10,7 +10,7 @@ import { pemToDER, } from "chrome://global/content/certviewer/certDecoder.mjs"; -document.addEventListener("DOMContentLoaded", async e => { +document.addEventListener("DOMContentLoaded", async () => { let url = new URL(document.URL); let certInfo = url.searchParams.getAll("cert"); if (certInfo.length === 0) { @@ -469,7 +469,7 @@ const buildChain = async chain => { let adjustedCerts = certs.map(cert => adjustCertInformation(cert)); return render(adjustedCerts, false); }) - .catch(err => { + .catch(() => { render(null, true); }); }; diff --git a/toolkit/components/certviewer/tests/browser/browser_checkNonRepeatedCertTabs.js b/toolkit/components/certviewer/tests/browser/browser_checkNonRepeatedCertTabs.js index 1e11cc4786..d0e2ad5644 100644 --- a/toolkit/components/certviewer/tests/browser/browser_checkNonRepeatedCertTabs.js +++ b/toolkit/components/certviewer/tests/browser/browser_checkNonRepeatedCertTabs.js @@ -56,7 +56,7 @@ add_task(async function testGoodCert() { info(`Loading ${url}`); await BrowserTestUtils.withNewTab({ gBrowser, url }, async function () { info("Opening pageinfo"); - let pageInfo = BrowserPageInfo(url, "securityTab", {}); + let pageInfo = BrowserCommands.pageInfo(url, "securityTab", {}); await BrowserTestUtils.waitForEvent(pageInfo, "load"); let securityTab = pageInfo.document.getElementById("securityTab"); diff --git a/toolkit/components/certviewer/tests/browser/browser_openTabAndSendCertInfo.js b/toolkit/components/certviewer/tests/browser/browser_openTabAndSendCertInfo.js index c6307f529a..24e9ed9767 100644 --- a/toolkit/components/certviewer/tests/browser/browser_openTabAndSendCertInfo.js +++ b/toolkit/components/certviewer/tests/browser/browser_openTabAndSendCertInfo.js @@ -63,7 +63,7 @@ function openCertDownloadDialog(cert) { cert, returnVals ); - return new Promise((resolve, reject) => { + return new Promise(resolve => { win.addEventListener( "load", function () { @@ -194,7 +194,7 @@ add_task(async function testGoodCert() { info(`Loading ${url}`); await BrowserTestUtils.withNewTab({ gBrowser, url }, async function () { info("Opening pageinfo"); - let pageInfo = BrowserPageInfo(url, "securityTab", {}); + let pageInfo = BrowserCommands.pageInfo(url, "securityTab", {}); await BrowserTestUtils.waitForEvent(pageInfo, "load"); let securityTab = pageInfo.document.getElementById("securityTab"); diff --git a/toolkit/components/certviewer/tests/browser/head.js b/toolkit/components/certviewer/tests/browser/head.js index 2a28444f9b..90e3b8168f 100644 --- a/toolkit/components/certviewer/tests/browser/head.js +++ b/toolkit/components/certviewer/tests/browser/head.js @@ -21,7 +21,7 @@ function is_element_visible(aElement, aMsg) { // Extracted from https://searchfox.org/mozilla-central/rev/40ef22080910c2e2c27d9e2120642376b1d8b8b2/browser/components/preferences/in-content/tests/head.js#41 function promiseLoadSubDialog(aURL) { - return new Promise((resolve, reject) => { + return new Promise(resolve => { content.gSubDialog._dialogStack.addEventListener( "dialogopen", function dialogopen(aEvent) { diff --git a/toolkit/components/cleardata/ClearDataService.sys.mjs b/toolkit/components/cleardata/ClearDataService.sys.mjs index ba34558f7e..5df5c03f58 100644 --- a/toolkit/components/cleardata/ClearDataService.sys.mjs +++ b/toolkit/components/cleardata/ClearDataService.sys.mjs @@ -193,7 +193,7 @@ const CookieCleaner = { }); }, - deleteByRange(aFrom, aTo) { + deleteByRange(aFrom) { return Services.cookies.removeAllSince(aFrom); }, @@ -584,7 +584,7 @@ const DownloadsCleaner = { }; const PasswordsCleaner = { - deleteByHost(aHost, aOriginAttributes) { + deleteByHost(aHost) { // Clearing by host also clears associated subdomains. return this._deleteInternal(aLogin => Services.eTLD.hasRootDomain(aLogin.hostname, aHost) @@ -630,7 +630,7 @@ const PasswordsCleaner = { }; const MediaDevicesCleaner = { - async deleteByRange(aFrom, aTo) { + async deleteByRange(aFrom) { let mediaMgr = Cc["@mozilla.org/mediaManagerService;1"].getService( Ci.nsIMediaManagerService ); @@ -799,7 +799,7 @@ const QuotaCleaner = { } }, - async deleteByHost(aHost, aOriginAttributes) { + async deleteByHost(aHost) { // XXX: The aOriginAttributes is expected to always be empty({}). Maybe have // a debug assertion here to ensure that? @@ -872,7 +872,7 @@ const QuotaCleaner = { _ => /* exceptionThrown = */ false, _ => /* exceptionThrown = */ true ) - .then(exceptionThrown => { + .then(() => { // QuotaManager: In the event of a failure, we call reject to propagate // the error upwards. return new Promise((aResolve, aReject) => { @@ -1002,7 +1002,7 @@ const PushNotificationsCleaner = { }); }, - deleteByHost(aHost, aOriginAttributes) { + deleteByHost(aHost) { // Will also clear entries for subdomains of aHost. Data is cleared across // all origin attributes. return this._deleteByRootDomain(aHost); @@ -1078,7 +1078,7 @@ const StorageAccessCleaner = { }); }, - async deleteByHost(aHost, aOriginAttributes) { + async deleteByHost(aHost) { // Clearing by host also clears associated subdomains. this._deleteInternal(({ principal }) => { let toBeRemoved = false; @@ -1095,7 +1095,7 @@ const StorageAccessCleaner = { ); }, - async deleteByRange(aFrom, aTo) { + async deleteByRange(aFrom) { Services.perms.removeByTypeSince("storageAccessAPI", aFrom / 1000); }, @@ -1105,7 +1105,7 @@ const StorageAccessCleaner = { }; const HistoryCleaner = { - deleteByHost(aHost, aOriginAttributes) { + deleteByHost(aHost) { if (!AppConstants.MOZ_PLACES) { return Promise.resolve(); } @@ -1142,7 +1142,7 @@ const HistoryCleaner = { }; const SessionHistoryCleaner = { - async deleteByHost(aHost, aOriginAttributes) { + async deleteByHost(aHost) { // Session storage and history also clear subdomains of aHost. Services.obs.notifyObservers(null, "browser:purge-sessionStorage", aHost); Services.obs.notifyObservers( @@ -1160,7 +1160,7 @@ const SessionHistoryCleaner = { return this.deleteByHost(aBaseDomain, {}); }, - async deleteByRange(aFrom, aTo) { + async deleteByRange(aFrom) { Services.obs.notifyObservers( null, "browser:purge-session-history", @@ -1275,7 +1275,7 @@ const PermissionsCleaner = { } }, - deleteByHost(aHost, aOriginAttributes) { + deleteByHost(aHost) { return this._deleteInternal({ host: aHost }); }, @@ -1287,7 +1287,7 @@ const PermissionsCleaner = { return this._deleteInternal({ baseDomain: aBaseDomain }); }, - async deleteByRange(aFrom, aTo) { + async deleteByRange(aFrom) { Services.perms.removeAllSince(aFrom / 1000); }, @@ -1301,7 +1301,7 @@ const PermissionsCleaner = { }; const PreferencesCleaner = { - deleteByHost(aHost, aOriginAttributes) { + deleteByHost(aHost) { // Also clears subdomains of aHost. return new Promise((aResolve, aReject) => { let cps2 = Cc["@mozilla.org/content-pref/service;1"].getService( @@ -1328,7 +1328,7 @@ const PreferencesCleaner = { return this.deleteByHost(aBaseDomain, {}); }, - async deleteByRange(aFrom, aTo) { + async deleteByRange(aFrom) { let cps2 = Cc["@mozilla.org/content-pref/service;1"].getService( Ci.nsIContentPrefService2 ); @@ -1477,7 +1477,7 @@ const EMECleaner = { }; const ReportsCleaner = { - deleteByHost(aHost, aOriginAttributes) { + deleteByHost(aHost) { // Also clears subdomains of aHost. return new Promise(aResolve => { Services.obs.notifyObservers(null, "reporting:purge-host", aHost); @@ -1520,7 +1520,7 @@ const ContentBlockingCleaner = { await this.deleteAll(); }, - deleteByRange(aFrom, aTo) { + deleteByRange(aFrom) { return lazy.TrackingDBService.clearSince(aFrom); }, }; @@ -1616,7 +1616,7 @@ const IdentityCredentialStorageCleaner = { } }, - async deleteByPrincipal(aPrincipal, aIsUserRequest) { + async deleteByPrincipal(aPrincipal) { if ( Services.prefs.getBoolPref( "dom.security.credentialmanagement.identity.enabled", @@ -1698,7 +1698,7 @@ const BounceTrackingProtectionStateCleaner = { await lazy.bounceTrackingProtection.clearAll(); }, - async deleteByPrincipal(aPrincipal, aIsUserRequest) { + async deleteByPrincipal(aPrincipal) { if (!lazy.isBounceTrackingProtectionEnabled) { return; } @@ -1709,7 +1709,7 @@ const BounceTrackingProtectionStateCleaner = { ); }, - async deleteByBaseDomain(aBaseDomain, aIsUserRequest) { + async deleteByBaseDomain(aBaseDomain) { if (!lazy.isBounceTrackingProtectionEnabled) { return; } @@ -1723,7 +1723,7 @@ const BounceTrackingProtectionStateCleaner = { await lazy.bounceTrackingProtection.clearByTimeRange(aFrom, aTo); }, - async deleteByHost(aHost, aOriginAttributes) { + async deleteByHost(aHost) { if (!lazy.isBounceTrackingProtectionEnabled) { return; } @@ -1741,6 +1741,59 @@ const BounceTrackingProtectionStateCleaner = { }, }; +const StoragePermissionsCleaner = { + async deleteByRange(aFrom) { + // We lack the ability to clear by range, but can clear from a certain time to now + // We have to divice aFrom by 1000 to convert the time from ms to microseconds + Services.perms.removeByTypeSince("storage-access", aFrom / 1000); + Services.perms.removeByTypeSince("persistent-storage", aFrom / 1000); + }, + + async deleteByPrincipal(aPrincipal) { + Services.perms.removeFromPrincipal(aPrincipal, "storage-access"); + Services.perms.removeFromPrincipal(aPrincipal, "persistent-storage"); + }, + + async deleteByHost(aHost) { + let permissions = this._getStoragePermissions(); + for (let perm of permissions) { + if (Services.eTLD.hasRootDomain(perm.principal.host, aHost)) { + Services.perms.removePermission(perm); + } + } + }, + + async deleteByBaseDomain(aBaseDomain) { + let permissions = this._getStoragePermissions(); + for (let perm of permissions) { + if (perm.principal.baseDomain == aBaseDomain) { + Services.perms.removePermission(perm); + } + } + }, + + async deleteByLocalFiles() { + let permissions = this._getStoragePermissions(); + for (let perm of permissions) { + if (perm.principal.schemeIs("file")) { + Services.perms.removePermission(perm); + } + } + }, + + async deleteAll() { + Services.perms.removeByType("storage-access"); + Services.perms.removeByType("persistent-storage"); + }, + + _getStoragePermissions() { + return Services.perms.getAllByTypes([ + "storage-access", + "persistent-storage", + ]); + }, +}; + // Here the map of Flags-Cleaners. const FLAGS_MAP = [ { @@ -1875,6 +1928,11 @@ const FLAGS_MAP = [ flag: Ci.nsIClearDataService.CLEAR_BOUNCE_TRACKING_PROTECTION_STATE, cleaners: [BounceTrackingProtectionStateCleaner], }, + + { + flag: Ci.nsIClearDataService.CLEAR_STORAGE_PERMISSIONS, + cleaners: [StoragePermissionsCleaner], + }, ]; export function ClearDataService() { diff --git a/toolkit/components/cleardata/PrincipalsCollector.sys.mjs b/toolkit/components/cleardata/PrincipalsCollector.sys.mjs index e4f5e827f6..a79da86757 100644 --- a/toolkit/components/cleardata/PrincipalsCollector.sys.mjs +++ b/toolkit/components/cleardata/PrincipalsCollector.sys.mjs @@ -66,8 +66,8 @@ export class PrincipalsCollector { /** * Fetches and collects all principals with cookies and/or site data (see module - * description). Originally for usage in Sanitizer.jsm to compute principals to be - * cleared on shutdown based on user settings. + * description). Originally for usage in Sanitizer.sys.mjs to compute principals + * to be cleared on shutdown based on user settings. * * This operation might take a while to complete on big profiles. * DO NOT call or await this in a way that makes it block user interaction, or you @@ -76,8 +76,8 @@ export class PrincipalsCollector { * This function will cache its result and return the same list on second call, * even if the actual number of principals with cookies and site data changed. * - * @param {Object} [optional] progress A Sanitizer.jsm progress object that will be - * updated to reflect the current step of fetching principals. + * @param {Object} [optional] progress A Sanitizer.sys.mjs progress object that + * will be updated to reflect the current step of fetching principals. * @returns {Array<nsIPrincipal>} the list of principals */ async getAllPrincipals(progress = {}) { diff --git a/toolkit/components/cleardata/SiteDataTestUtils.sys.mjs b/toolkit/components/cleardata/SiteDataTestUtils.sys.mjs index a777a19b7d..99bd1f72b0 100644 --- a/toolkit/components/cleardata/SiteDataTestUtils.sys.mjs +++ b/toolkit/components/cleardata/SiteDataTestUtils.sys.mjs @@ -244,10 +244,10 @@ export var SiteDataTestUtils = { return new Promise(resolve => { let data = true; let request = indexedDB.openForPrincipal(principal, "TestDatabase", 1); - request.onupgradeneeded = function (e) { + request.onupgradeneeded = function () { data = false; }; - request.onsuccess = function (e) { + request.onsuccess = function () { resolve(data); }; }); @@ -276,11 +276,11 @@ export var SiteDataTestUtils = { CacheListener.prototype = { QueryInterface: ChromeUtils.generateQI(["nsICacheEntryOpenCallback"]), - onCacheEntryCheck(entry) { + onCacheEntryCheck() { return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; }, - onCacheEntryAvailable(entry, isnew, status) { + onCacheEntryAvailable() { resolve(); }, }; diff --git a/toolkit/components/cleardata/nsIClearDataService.idl b/toolkit/components/cleardata/nsIClearDataService.idl index ea17059bbe..babbc8f05b 100644 --- a/toolkit/components/cleardata/nsIClearDataService.idl +++ b/toolkit/components/cleardata/nsIClearDataService.idl @@ -31,7 +31,7 @@ interface nsIClearDataService : nsISupports * @param aCallback this callback will be executed when the operation is * completed. */ - void deleteDataFromLocalFiles(in bool aIsUserRequest, + void deleteDataFromLocalFiles(in boolean aIsUserRequest, in uint32_t aFlags, in nsIClearDataCallback aCallback); @@ -50,7 +50,7 @@ interface nsIClearDataService : nsISupports * @deprecated Use deleteDataFromBaseDomain instead. */ void deleteDataFromHost(in AUTF8String aHost, - in bool aIsUserRequest, + in boolean aIsUserRequest, in uint32_t aFlags, in nsIClearDataCallback aCallback); @@ -76,7 +76,7 @@ interface nsIClearDataService : nsISupports * may fall back to clearing by principal or host. */ void deleteDataFromBaseDomain(in AUTF8String aDomainOrHost, - in bool aIsUserRequest, + in boolean aIsUserRequest, in uint32_t aFlags, in nsIClearDataCallback aCallback); @@ -93,7 +93,7 @@ interface nsIClearDataService : nsISupports * completed. */ void deleteDataFromPrincipal(in nsIPrincipal aPrincipal, - in bool aIsUserRequest, + in boolean aIsUserRequest, in uint32_t aFlags, in nsIClearDataCallback aCallback); @@ -111,7 +111,7 @@ interface nsIClearDataService : nsISupports * completed. */ void deleteDataInTimeRange(in PRTime aFrom, in PRTime aTo, - in bool aIsUserRequest, + in boolean aIsUserRequest, in uint32_t aFlags, in nsIClearDataCallback aCallback); @@ -308,6 +308,11 @@ interface nsIClearDataService : nsISupports const uint32_t CLEAR_BOUNCE_TRACKING_PROTECTION_STATE = 1 << 28; /** + * Clear permissions of type "persistent-storage" and "storage-access" + */ + const uint32_t CLEAR_STORAGE_PERMISSIONS = 1 << 29; + + /** * Use this value to delete all the data. */ const uint32_t CLEAR_ALL = 0xFFFFFFFF; @@ -340,6 +345,19 @@ interface nsIClearDataService : nsISupports CLEAR_CREDENTIAL_MANAGER_STATE | CLEAR_COOKIE_BANNER_EXCEPTION | CLEAR_COOKIE_BANNER_EXECUTED_RECORD | CLEAR_FINGERPRINTING_PROTECTION_STATE | CLEAR_BOUNCE_TRACKING_PROTECTION_STATE; + + /** + * Helper flag for clearing cookies and site data. + * This flag groups state that we consider site data + * from the user perspective. If you implement UI that + * offers site data clearing this is almost always what you want. + * If you need more granular control please use more specific + * flags like CLEAR_COOKIES and CLEAR_DOM_STORAGES. + */ + const uint32_t CLEAR_COOKIES_AND_SITE_DATA = + CLEAR_COOKIES | CLEAR_COOKIE_BANNER_EXECUTED_RECORD | CLEAR_DOM_STORAGES | CLEAR_HSTS | + CLEAR_EME | CLEAR_AUTH_TOKENS | CLEAR_AUTH_CACHE | CLEAR_FINGERPRINTING_PROTECTION_STATE | + CLEAR_BOUNCE_TRACKING_PROTECTION_STATE | CLEAR_STORAGE_PERMISSIONS; }; /** diff --git a/toolkit/components/cleardata/tests/browser/browser.toml b/toolkit/components/cleardata/tests/browser/browser.toml index 16ecffce02..d13f4f4f32 100644 --- a/toolkit/components/cleardata/tests/browser/browser.toml +++ b/toolkit/components/cleardata/tests/browser/browser.toml @@ -3,21 +3,18 @@ ["browser_auth_tokens.js"] ["browser_css_cache.js"] -https_first_disabled = true support-files = [ "file_css_cache.css", "file_css_cache.html" ] ["browser_image_cache.js"] -https_first_disabled = true support-files = [ "file_image_cache.html", "file_image_cache.jpg" ] ["browser_preflight_cache.js"] -https_first_disabled = true support-files = ["file_cors_preflight.sjs"] ["browser_quota.js"] diff --git a/toolkit/components/cleardata/tests/browser/browser_css_cache.js b/toolkit/components/cleardata/tests/browser/browser_css_cache.js index 47088e5011..61cee8b8d9 100644 --- a/toolkit/components/cleardata/tests/browser/browser_css_cache.js +++ b/toolkit/components/cleardata/tests/browser/browser_css_cache.js @@ -74,6 +74,7 @@ async function cleanupTestTabs() { } add_task(async function test_deleteByPrincipal() { + await SpecialPowers.setBoolPref("dom.security.https_first", false); await addTestTabs(); // Clear data for content principal of A diff --git a/toolkit/components/cleardata/tests/browser/browser_image_cache.js b/toolkit/components/cleardata/tests/browser/browser_image_cache.js index d7116d2502..947d4aba3e 100644 --- a/toolkit/components/cleardata/tests/browser/browser_image_cache.js +++ b/toolkit/components/cleardata/tests/browser/browser_image_cache.js @@ -114,6 +114,7 @@ add_setup(function () { }); add_task(async function test_deleteByPrincipal() { + await SpecialPowers.setBoolPref("dom.security.https_first", false); await addTestTabs(); // Clear data for content principal of A @@ -152,6 +153,7 @@ add_task(async function test_deleteByPrincipal() { }); add_task(async function test_deleteByBaseDomain() { + await SpecialPowers.setBoolPref("dom.security.https_first", false); await addTestTabs(); // Clear data for base domain of A. diff --git a/toolkit/components/cleardata/tests/browser/browser_preflight_cache.js b/toolkit/components/cleardata/tests/browser/browser_preflight_cache.js index d3eabb9e38..8973cc981c 100644 --- a/toolkit/components/cleardata/tests/browser/browser_preflight_cache.js +++ b/toolkit/components/cleardata/tests/browser/browser_preflight_cache.js @@ -9,8 +9,8 @@ const { SiteDataTestUtils } = ChromeUtils.importESModule( const uuidGenerator = Services.uuid; -const ORIGIN_A = "http://example.net"; -const ORIGIN_B = "http://example.org"; +const ORIGIN_A = "https://example.net"; +const ORIGIN_B = "https://example.org"; const PREFLIGHT_URL_PATH = "/browser/toolkit/components/cleardata/tests/browser/file_cors_preflight.sjs"; @@ -45,7 +45,7 @@ async function testDeleteAll( clearDataFlag, { deleteBy = "all", hasUserInput = false } = {} ) { - await BrowserTestUtils.withNewTab("http://example.com", async browser => { + await BrowserTestUtils.withNewTab("https://example.com", async browser => { let token = uuidGenerator.generateUUID().toString(); // Populate the preflight cache. @@ -132,7 +132,7 @@ add_task(async function test_deletePrivateBrowsingCache() { const tab = (browser.gBrowser.selectedTab = BrowserTestUtils.addTab( browser.gBrowser, - "http://example.com" + "https://example.com" )); await BrowserTestUtils.browserLoaded(tab.linkedBrowser); diff --git a/toolkit/components/cleardata/tests/marionette/test_moved_origin_directory_cleanup.py b/toolkit/components/cleardata/tests/marionette/test_moved_origin_directory_cleanup.py index 50f4c93f65..d30cf438e1 100644 --- a/toolkit/components/cleardata/tests/marionette/test_moved_origin_directory_cleanup.py +++ b/toolkit/components/cleardata/tests/marionette/test_moved_origin_directory_cleanup.py @@ -16,6 +16,7 @@ class MovedOriginDirectoryCleanupTestCase(MarionetteTestCase): "privacy.sanitize.sanitizeOnShutdown": True, "privacy.clearOnShutdown.offlineApps": True, "dom.quotaManager.backgroundTask.enabled": False, + "browser.sanitizer.loglevel": "All", } ) self.moved_origin_directory = ( @@ -30,6 +31,13 @@ class MovedOriginDirectoryCleanupTestCase(MarionetteTestCase): with self.marionette.using_context("chrome"): self.marionette.execute_script( """ + let promise = new Promise(resolve => { + function observer() { + Services.obs.removeObserver(observer, "cookie-saved-on-disk"); + resolve(); + } + Services.obs.addObserver(observer, "cookie-saved-on-disk"); + }); Services.cookies.add( "example.local", "path", @@ -43,6 +51,7 @@ class MovedOriginDirectoryCleanupTestCase(MarionetteTestCase): Ci.nsICookie.SAMESITE_NONE, Ci.nsICookie.SCHEME_UNSET ); + return promise; """ ) diff --git a/toolkit/components/cleardata/tests/unit/test_permissions.js b/toolkit/components/cleardata/tests/unit/test_permissions.js index 1f46ab5015..450476183a 100644 --- a/toolkit/components/cleardata/tests/unit/test_permissions.js +++ b/toolkit/components/cleardata/tests/unit/test_permissions.js @@ -88,7 +88,7 @@ add_task(async function test_principal_permissions() { await new Promise(aResolve => { Services.clearData.deleteData( Ci.nsIClearDataService.CLEAR_PERMISSIONS, - value => aResolve() + () => aResolve() ); }); }); @@ -465,7 +465,7 @@ add_task(async function test_3rdpartystorage_permissions() { await new Promise(aResolve => { Services.clearData.deleteData( Ci.nsIClearDataService.CLEAR_PERMISSIONS, - value => aResolve() + () => aResolve() ); }); }); diff --git a/toolkit/components/cleardata/tests/unit/test_storage_permission.js b/toolkit/components/cleardata/tests/unit/test_storage_permission.js index a44e9f2c6a..5ff3e6d15c 100644 --- a/toolkit/components/cleardata/tests/unit/test_storage_permission.js +++ b/toolkit/components/cleardata/tests/unit/test_storage_permission.js @@ -62,7 +62,7 @@ add_task(async function test_removing_storage_permission() { await new Promise(aResolve => { Services.clearData.deleteData( Ci.nsIClearDataService.CLEAR_PERMISSIONS, - value => aResolve() + () => aResolve() ); }); }); @@ -138,7 +138,7 @@ add_task(async function test_removing_storage_permission_from_principal() { await new Promise(aResolve => { Services.clearData.deleteData( Ci.nsIClearDataService.CLEAR_PERMISSIONS, - value => aResolve() + () => aResolve() ); }); }); @@ -240,7 +240,7 @@ add_task(async function test_removing_storage_permission_from_base_domainl() { await new Promise(aResolve => { Services.clearData.deleteData( Ci.nsIClearDataService.CLEAR_PERMISSIONS, - value => aResolve() + () => aResolve() ); }); }); @@ -392,7 +392,7 @@ add_task(async function test_deleteUserInteractionForClearingHistory() { await new Promise(aResolve => { Services.clearData.deleteData( Ci.nsIClearDataService.CLEAR_PERMISSIONS, - value => aResolve() + () => aResolve() ); }); }); diff --git a/toolkit/components/cleardata/tests/unit/test_storage_permission_clearing.js b/toolkit/components/cleardata/tests/unit/test_storage_permission_clearing.js new file mode 100644 index 0000000000..9285236958 --- /dev/null +++ b/toolkit/components/cleardata/tests/unit/test_storage_permission_clearing.js @@ -0,0 +1,271 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/*** + * Tests clearing storage access and persistent storage permissions + */ + +"use strict"; + +const baseDomain = "example.net"; +const baseDomain2 = "host.org"; +const uri = Services.io.newURI(`https://` + baseDomain); +const uri2 = Services.io.newURI(`https://` + baseDomain2); +const uri3 = Services.io.newURI(`https://www.` + baseDomain); +const principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} +); +const principal2 = Services.scriptSecurityManager.createContentPrincipal( + uri2, + {} +); +const principal3 = Services.scriptSecurityManager.createContentPrincipal( + uri3, + {} +); + +add_task(async function test_clearing_by_principal() { + Services.perms.addFromPrincipal( + principal, + "storage-access", + Services.perms.ALLOW_ACTION + ); + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + principal, + "storage-access" + ), + Services.perms.ALLOW_ACTION, + "There is a storage-access permission set for principal 1" + ); + + Services.perms.addFromPrincipal( + principal, + "persistent-storage", + Services.perms.ALLOW_ACTION + ); + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + principal, + "persistent-storage" + ), + Services.perms.ALLOW_ACTION, + "There is a persistent-storage permission set for principal 1" + ); + + // Add a principal that shouldn't get cleared + Services.perms.addFromPrincipal( + principal2, + "storage-access", + Services.perms.ALLOW_ACTION + ); + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + principal2, + "storage-access" + ), + Services.perms.ALLOW_ACTION, + "There is a storage-access permission set for principal 2" + ); + + // Add an unrelated permission which we don't expect to be cleared + Services.perms.addFromPrincipal( + principal, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + principal, + "desktop-notification" + ), + Services.perms.ALLOW_ACTION, + "There is a desktop-notification permission set for principal 1" + ); + + await new Promise(aResolve => { + Services.clearData.deleteDataFromPrincipal( + principal, + true, + Ci.nsIClearDataService.CLEAR_STORAGE_PERMISSIONS, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + principal, + "storage-access" + ), + Services.perms.UNKNOWN_ACTION, + "storage-access permission for principal 1 has been removed" + ); + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + principal, + "persistent-storage" + ), + Services.perms.UNKNOWN_ACTION, + "persistent-storage permission for principal 1 should be removed" + ); + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + principal2, + "storage-access" + ), + Services.perms.ALLOW_ACTION, + "storage-access permission for principal 2 should not be removed" + ); + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + principal, + "desktop-notification" + ), + Services.perms.ALLOW_ACTION, + "desktop-notification permission for principal should not be removed" + ); +}); + +add_task(async function test_clearing_by_baseDomain() { + Services.perms.addFromPrincipal( + principal, + "storage-access", + Services.perms.ALLOW_ACTION + ); + Services.perms.addFromPrincipal( + principal2, + "storage-access", + Services.perms.ALLOW_ACTION + ); + Services.perms.addFromPrincipal( + principal3, + "storage-access", + Services.perms.ALLOW_ACTION + ); + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + principal, + "storage-access" + ), + Services.perms.ALLOW_ACTION, + "There is a storage-access permission set for principal 1" + ); + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + principal2, + "storage-access" + ), + Services.perms.ALLOW_ACTION, + "There is a storage-access permission set for principal 2" + ); + + await new Promise(aResolve => { + Services.clearData.deleteDataFromBaseDomain( + baseDomain, + true, + Ci.nsIClearDataService.CLEAR_STORAGE_PERMISSIONS, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + principal, + "storage-access" + ), + Services.perms.UNKNOWN_ACTION, + "storage-access permission for principal 1 has been removed" + ); + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + principal2, + "storage-access" + ), + Services.perms.ALLOW_ACTION, + "storage-access permission for principal 2 should not be removed" + ); + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + principal3, + "storage-access" + ), + Services.perms.UNKNOWN_ACTION, + "storage-access permission for principal 3 should be removed" + ); +}); + +add_task(async function test_clearing_by_range() { + let currTime = Date.now(); + let modificationTimeTwoHoursAgo = new Date(currTime - 2 * 60 * 60 * 1000); + let modificationTimeThirtyMinsAgo = new Date(currTime - 30 * 60 * 1000); + + Services.perms.testAddFromPrincipalByTime( + principal, + "storage-access", + Services.perms.ALLOW_ACTION, + modificationTimeTwoHoursAgo + ); + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + principal, + "storage-access" + ), + Services.perms.ALLOW_ACTION, + "There is a storage-access permission set for principal 1" + ); + + Services.perms.testAddFromPrincipalByTime( + principal2, + "persistent-storage", + Services.perms.ALLOW_ACTION, + modificationTimeThirtyMinsAgo + ); + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + principal2, + "persistent-storage" + ), + Services.perms.ALLOW_ACTION, + "There is a persistent-storage permission set for principal 2" + ); + + let modificationTimeOneHourAgo = new Date(currTime - 60 * 60 * 1000); + // We need to pass in microseconds to the clear data service + // so we multiply the ranges by 1000 + await new Promise(aResolve => { + Services.clearData.deleteDataInTimeRange( + modificationTimeOneHourAgo * 1000, + Date.now() * 1000, + true, + Ci.nsIClearDataService.CLEAR_STORAGE_PERMISSIONS, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + principal, + "storage-access" + ), + Services.perms.ALLOW_ACTION, + "storage-access permission for principal 1 should not be removed" + ); + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + principal2, + "persistent-storage" + ), + Services.perms.UNKNOWN_ACTION, + "persistent-storage permission for principal 2 should be removed" + ); +}); diff --git a/toolkit/components/cleardata/tests/unit/xpcshell.toml b/toolkit/components/cleardata/tests/unit/xpcshell.toml index 2df07abcea..bf2813a401 100644 --- a/toolkit/components/cleardata/tests/unit/xpcshell.toml +++ b/toolkit/components/cleardata/tests/unit/xpcshell.toml @@ -38,3 +38,5 @@ skip-if = ["condprof"] # Bug 1769154 - expected fail w/condprof ["test_security_settings.js"] ["test_storage_permission.js"] + +["test_storage_permission_clearing.js"] diff --git a/toolkit/components/contentanalysis/ContentAnalysis.cpp b/toolkit/components/contentanalysis/ContentAnalysis.cpp index 1c71f5d986..2977072984 100644 --- a/toolkit/components/contentanalysis/ContentAnalysis.cpp +++ b/toolkit/components/contentanalysis/ContentAnalysis.cpp @@ -11,6 +11,7 @@ #include "base/process_util.h" #include "GMPUtils.h" // ToHexString #include "mozilla/Components.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/WindowGlobalParent.h" #include "mozilla/Logging.h" @@ -24,6 +25,9 @@ #include "nsIFile.h" #include "nsIGlobalObject.h" #include "nsIObserverService.h" +#include "nsIOutputStream.h" +#include "nsIPrintSettings.h" +#include "nsIStorageStream.h" #include "ScopedNSSTypes.h" #include "xpcpublic.h" @@ -35,6 +39,7 @@ # include <windows.h> # define SECURITY_WIN32 1 # include <security.h> +# include "mozilla/NativeNt.h" # include "mozilla/WinDllServices.h" #endif // XP_WIN @@ -114,6 +119,11 @@ nsIContentAnalysisAcknowledgement::FinalAction ConvertResult( } // anonymous namespace namespace mozilla::contentanalysis { +ContentAnalysisRequest::~ContentAnalysisRequest() { +#ifdef XP_WIN + CloseHandle(mPrintDataHandle); +#endif +} NS_IMETHODIMP ContentAnalysisRequest::GetAnalysisType(AnalysisType* aAnalysisType) { @@ -134,6 +144,34 @@ ContentAnalysisRequest::GetFilePath(nsAString& aFilePath) { } NS_IMETHODIMP +ContentAnalysisRequest::GetPrintDataHandle(uint64_t* aPrintDataHandle) { +#ifdef XP_WIN + uintptr_t printDataHandle = reinterpret_cast<uintptr_t>(mPrintDataHandle); + uint64_t printDataValue = static_cast<uint64_t>(printDataHandle); + *aPrintDataHandle = printDataValue; + return NS_OK; +#else + return NS_ERROR_NOT_IMPLEMENTED; +#endif +} + +NS_IMETHODIMP +ContentAnalysisRequest::GetPrinterName(nsAString& aPrinterName) { + aPrinterName = mPrinterName; + return NS_OK; +} + +NS_IMETHODIMP +ContentAnalysisRequest::GetPrintDataSize(uint64_t* aPrintDataSize) { +#ifdef XP_WIN + *aPrintDataSize = mPrintDataSize; + return NS_OK; +#else + return NS_ERROR_NOT_IMPLEMENTED; +#endif +} + +NS_IMETHODIMP ContentAnalysisRequest::GetUrl(nsIURI** aUrl) { NS_ENSURE_ARG_POINTER(aUrl); NS_IF_ADDREF(*aUrl = mUrl); @@ -234,6 +272,8 @@ ContentAnalysisRequest::ContentAnalysisRequest( mUrl(std::move(aUrl)), mSha256Digest(std::move(aSha256Digest)), mWindowGlobalParent(aWindowGlobalParent) { + MOZ_ASSERT(aAnalysisType != AnalysisType::ePrint, + "Print should use other ContentAnalysisRequest constructor!"); if (aStringIsFilePath) { mFilePath = std::move(aString); } else { @@ -251,6 +291,32 @@ ContentAnalysisRequest::ContentAnalysisRequest( mRequestToken = GenerateRequestToken(); } +ContentAnalysisRequest::ContentAnalysisRequest( + const nsTArray<uint8_t> aPrintData, nsCOMPtr<nsIURI> aUrl, + nsString aPrinterName, dom::WindowGlobalParent* aWindowGlobalParent) + : mAnalysisType(AnalysisType::ePrint), + mUrl(std::move(aUrl)), + mPrinterName(std::move(aPrinterName)), + mWindowGlobalParent(aWindowGlobalParent) { +#ifdef XP_WIN + LARGE_INTEGER dataContentLength; + dataContentLength.QuadPart = static_cast<LONGLONG>(aPrintData.Length()); + mPrintDataHandle = ::CreateFileMappingW( + INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, dataContentLength.HighPart, + dataContentLength.LowPart, nullptr); + if (mPrintDataHandle) { + mozilla::nt::AutoMappedView view(mPrintDataHandle, FILE_MAP_ALL_ACCESS); + memcpy(view.as<uint8_t>(), aPrintData.Elements(), aPrintData.Length()); + mPrintDataSize = aPrintData.Length(); + } +#else + MOZ_ASSERT_UNREACHABLE( + "Content Analysis is not supported on non-Windows platforms"); +#endif + mOperationTypeForDisplay = OperationType::eOperationPrint; + mRequestToken = GenerateRequestToken(); +} + nsresult ContentAnalysisRequest::GetFileDigest(const nsAString& aFilePath, nsCString& aDigestString) { MOZ_DIAGNOSTIC_ASSERT( @@ -366,22 +432,44 @@ static nsresult ConvertToProtobuf( requestData->set_digest(sha256Digest.get()); } - nsString filePath; - rv = aIn->GetFilePath(filePath); - NS_ENSURE_SUCCESS(rv, rv); - if (!filePath.IsEmpty()) { - std::string filePathStr = NS_ConvertUTF16toUTF8(filePath).get(); - aOut->set_file_path(filePathStr); - auto filename = filePathStr.substr(filePathStr.find_last_of("/\\") + 1); - if (!filename.empty()) { - requestData->set_filename(filename); + if (analysisType == nsIContentAnalysisRequest::AnalysisType::ePrint) { +#if XP_WIN + uint64_t printDataHandle; + MOZ_TRY(aIn->GetPrintDataHandle(&printDataHandle)); + if (!printDataHandle) { + return NS_ERROR_OUT_OF_MEMORY; } + aOut->mutable_print_data()->set_handle(printDataHandle); + + uint64_t printDataSize; + MOZ_TRY(aIn->GetPrintDataSize(&printDataSize)); + aOut->mutable_print_data()->set_size(printDataSize); + + nsString printerName; + MOZ_TRY(aIn->GetPrinterName(printerName)); + requestData->mutable_print_metadata()->set_printer_name( + NS_ConvertUTF16toUTF8(printerName).get()); +#else + return NS_ERROR_NOT_IMPLEMENTED; +#endif } else { - nsString textContent; - rv = aIn->GetTextContent(textContent); + nsString filePath; + rv = aIn->GetFilePath(filePath); NS_ENSURE_SUCCESS(rv, rv); - MOZ_ASSERT(!textContent.IsEmpty()); - aOut->set_text_content(NS_ConvertUTF16toUTF8(textContent).get()); + if (!filePath.IsEmpty()) { + std::string filePathStr = NS_ConvertUTF16toUTF8(filePath).get(); + aOut->set_file_path(filePathStr); + auto filename = filePathStr.substr(filePathStr.find_last_of("/\\") + 1); + if (!filename.empty()) { + requestData->set_filename(filename); + } + } else { + nsString textContent; + rv = aIn->GetTextContent(textContent); + NS_ENSURE_SUCCESS(rv, rv); + MOZ_ASSERT(!textContent.IsEmpty()); + aOut->set_text_content(NS_ConvertUTF16toUTF8(textContent).get()); + } } #ifdef XP_WIN @@ -412,8 +500,31 @@ static nsresult ConvertToProtobuf( return NS_OK; } +namespace { +// We don't want this overload to be called for string parameters, so +// use std::enable_if +template <typename T> +typename std::enable_if_t<!std::is_same<std::string, std::decay_t<T>>::value, + void> +LogWithMaxLength(std::stringstream& ss, T value, size_t maxLength) { + ss << value; +} + +// 0 indicates no max length +template <typename T> +typename std::enable_if_t<std::is_same<std::string, std::decay_t<T>>::value, + void> +LogWithMaxLength(std::stringstream& ss, T value, size_t maxLength) { + if (!maxLength || value.length() < maxLength) { + ss << value; + } else { + ss << value.substr(0, maxLength) << " (truncated)"; + } +} +} // namespace + static void LogRequest( - content_analysis::sdk::ContentAnalysisRequest* aPbRequest) { + const content_analysis::sdk::ContentAnalysisRequest* aPbRequest) { // We cannot use Protocol Buffer's DebugString() because we optimize for // lite runtime. if (!static_cast<LogModule*>(gContentAnalysisLog) @@ -425,12 +536,13 @@ static void LogRequest( ss << "ContentAnalysisRequest:" << "\n"; -#define ADD_FIELD(PBUF, NAME, FUNC) \ - ss << " " << (NAME) << ": "; \ - if ((PBUF)->has_##FUNC()) \ - ss << (PBUF)->FUNC() << "\n"; \ - else \ - ss << "<none>" \ +#define ADD_FIELD(PBUF, NAME, FUNC) \ + ss << " " << (NAME) << ": "; \ + if ((PBUF)->has_##FUNC()) { \ + LogWithMaxLength(ss, (PBUF)->FUNC(), 500); \ + ss << "\n"; \ + } else \ + ss << "<none>" \ << "\n"; #define ADD_EXISTS(PBUF, NAME, FUNC) \ @@ -612,6 +724,12 @@ ContentAnalysisResponse::GetAction(Action* aAction) { return NS_OK; } +NS_IMETHODIMP +ContentAnalysisResponse::GetCancelError(CancelError* aCancelError) { + *aCancelError = mCancelError; + return NS_OK; +} + static void LogAcknowledgement( content_analysis::sdk::ContentAnalysisAcknowledgement* aPbAck) { if (!static_cast<LogModule*>(gContentAnalysisLog) @@ -643,6 +761,10 @@ void ContentAnalysisResponse::SetOwner(RefPtr<ContentAnalysis> aOwner) { mOwner = std::move(aOwner); } +void ContentAnalysisResponse::SetCancelError(CancelError aCancelError) { + mCancelError = aCancelError; +} + void ContentAnalysisResponse::ResolveWarnAction(bool aAllowContent) { MOZ_ASSERT(mAction == Action::eWarn); mAction = aAllowContent ? Action::eAllow : Action::eBlock; @@ -684,15 +806,17 @@ NS_IMETHODIMP ContentAnalysisResult::GetShouldAllowContent( if (mValue.is<NoContentAnalysisResult>()) { NoContentAnalysisResult result = mValue.as<NoContentAnalysisResult>(); if (Preferences::GetBool(kDefaultAllowPref)) { - *aShouldAllowContent = result != NoContentAnalysisResult::CANCELED; + *aShouldAllowContent = + result != NoContentAnalysisResult::DENY_DUE_TO_CANCELED; } else { // Note that we allow content if we're unable to get it (for example, if // there's clipboard content that is not text or file) *aShouldAllowContent = - result == NoContentAnalysisResult::CONTENT_ANALYSIS_NOT_ACTIVE || - result == - NoContentAnalysisResult::CONTEXT_EXEMPT_FROM_CONTENT_ANALYSIS || - result == NoContentAnalysisResult::ERROR_COULD_NOT_GET_DATA; + result == NoContentAnalysisResult:: + ALLOW_DUE_TO_CONTENT_ANALYSIS_NOT_ACTIVE || + result == NoContentAnalysisResult:: + ALLOW_DUE_TO_CONTEXT_EXEMPT_FROM_CONTENT_ANALYSIS || + result == NoContentAnalysisResult::ALLOW_DUE_TO_COULD_NOT_GET_DATA; } } else { *aShouldAllowContent = @@ -735,8 +859,8 @@ ContentAnalysis::UrlFilterResult ContentAnalysis::FilterByUrlLists( nsIContentAnalysisRequest* aRequest) { EnsureParsedUrlFilters(); - nsIURI* nsiUrl = nullptr; - MOZ_ALWAYS_SUCCEEDS(aRequest->GetUrl(&nsiUrl)); + nsCOMPtr<nsIURI> nsiUrl; + MOZ_ALWAYS_SUCCEEDS(aRequest->GetUrl(getter_AddRefs(nsiUrl))); nsCString urlString; nsresult rv = nsiUrl->GetSpec(urlString); NS_ENSURE_SUCCESS(rv, UrlFilterResult::eDeny); @@ -825,6 +949,8 @@ NS_IMPL_ISUPPORTS(ContentAnalysisAcknowledgement, nsIContentAnalysisAcknowledgement); NS_IMPL_ISUPPORTS(ContentAnalysisCallback, nsIContentAnalysisCallback); NS_IMPL_ISUPPORTS(ContentAnalysisResult, nsIContentAnalysisResult); +NS_IMPL_ISUPPORTS(ContentAnalysisDiagnosticInfo, + nsIContentAnalysisDiagnosticInfo); NS_IMPL_ISUPPORTS(ContentAnalysis, nsIContentAnalysis, ContentAnalysis); ContentAnalysis::ContentAnalysis() @@ -942,6 +1068,7 @@ nsresult ContentAnalysis::CancelWithError(nsCString aRequestToken, // May be shutting down return; } + owner->SetLastResult(aResult); nsCOMPtr<nsIObserverService> obsServ = mozilla::services::GetObserverService(); bool allow = Preferences::GetBool(kDefaultAllowPref); @@ -951,6 +1078,20 @@ nsresult ContentAnalysis::CancelWithError(nsCString aRequestToken, : nsIContentAnalysisResponse::Action::eCanceled, aRequestToken); response->SetOwner(owner); + nsIContentAnalysisResponse::CancelError cancelError; + switch (aResult) { + case NS_ERROR_NOT_AVAILABLE: + cancelError = nsIContentAnalysisResponse::CancelError::eNoAgent; + break; + case NS_ERROR_INVALID_SIGNATURE: + cancelError = + nsIContentAnalysisResponse::CancelError::eInvalidAgentSignature; + break; + default: + cancelError = nsIContentAnalysisResponse::CancelError::eErrorOther; + break; + } + response->SetCancelError(cancelError); obsServ->NotifyObservers(response, "dlp-response", nullptr); nsMainThreadPtrHandle<nsIContentAnalysisCallback> callbackHolder; { @@ -1156,6 +1297,8 @@ void ContentAnalysis::IssueResponse(RefPtr<ContentAnalysisResponse>& response) { LOGE("Content analysis couldn't get request token from response!"); return; } + // Successfully made a request to the agent, so mark that we succeeded + mLastResult = NS_OK; Maybe<CallbackData> maybeCallbackData; { @@ -1187,7 +1330,9 @@ void ContentAnalysis::IssueResponse(RefPtr<ContentAnalysisResponse>& response) { LOGD("Content analysis resolving response promise for token %s", responseRequestToken.get()); - nsIContentAnalysisResponse::Action action = response->GetAction(); + nsIContentAnalysisResponse::Action action; + DebugOnly<nsresult> rv = response->GetAction(&action); + MOZ_ASSERT(NS_SUCCEEDED(rv)); nsCOMPtr<nsIObserverService> obsServ = mozilla::services::GetObserverService(); if (action == nsIContentAnalysisResponse::Action::eWarn) { @@ -1232,8 +1377,28 @@ NS_IMETHODIMP ContentAnalysis::AnalyzeContentRequestCallback( nsIContentAnalysisRequest* aRequest, bool aAutoAcknowledge, nsIContentAnalysisCallback* aCallback) { + MOZ_ASSERT(NS_IsMainThread()); NS_ENSURE_ARG(aRequest); NS_ENSURE_ARG(aCallback); + nsresult rv = AnalyzeContentRequestCallbackPrivate(aRequest, aAutoAcknowledge, + aCallback); + if (NS_FAILED(rv)) { + nsCString requestToken; + nsresult requestTokenRv = aRequest->GetRequestToken(requestToken); + NS_ENSURE_SUCCESS(requestTokenRv, requestTokenRv); + CancelWithError(requestToken, rv); + } + return rv; +} + +nsresult ContentAnalysis::AnalyzeContentRequestCallbackPrivate( + nsIContentAnalysisRequest* aRequest, bool aAutoAcknowledge, + nsIContentAnalysisCallback* aCallback) { + // Make sure we send the notification first, so if we later return + // an error the JS will handle it correctly. + nsCOMPtr<nsIObserverService> obsServ = + mozilla::services::GetObserverService(); + obsServ->NotifyObservers(aRequest, "dlp-request-made", nullptr); bool isActive; nsresult rv = GetIsActive(&isActive); @@ -1242,10 +1407,6 @@ ContentAnalysis::AnalyzeContentRequestCallback( return NS_ERROR_NOT_AVAILABLE; } - nsCOMPtr<nsIObserverService> obsServ = - mozilla::services::GetObserverService(); - obsServ->NotifyObservers(aRequest, "dlp-request-made", nullptr); - MOZ_ASSERT(NS_IsMainThread()); // since we're on the main thread, don't need to synchronize this int64_t requestCount = ++mRequestCount; @@ -1333,7 +1494,9 @@ ContentAnalysis::RespondToWarnDialog(const nsACString& aRequestToken, return; } entry->mResponse->ResolveWarnAction(aAllowContent); - auto action = entry->mResponse->GetAction(); + nsIContentAnalysisResponse::Action action; + DebugOnly<nsresult> rv = entry->mResponse->GetAction(&action); + MOZ_ASSERT(NS_SUCCEEDED(rv)); if (entry->mCallbackData.AutoAcknowledge()) { RefPtr<ContentAnalysisAcknowledgement> acknowledgement = new ContentAnalysisAcknowledgement( @@ -1358,6 +1521,181 @@ ContentAnalysis::RespondToWarnDialog(const nsACString& aRequestToken, return NS_OK; } +#if defined(XP_WIN) +RefPtr<ContentAnalysis::PrintAllowedPromise> +ContentAnalysis::PrintToPDFToDetermineIfPrintAllowed( + dom::CanonicalBrowsingContext* aBrowsingContext, + nsIPrintSettings* aPrintSettings) { + // Note that the IsChrome() check here excludes a few + // common about pages like about:config, about:preferences, + // and about:support, but other about: pages may still + // go through content analysis. + if (aBrowsingContext->IsChrome()) { + return PrintAllowedPromise::CreateAndResolve(PrintAllowedResult(true), + __func__); + } + nsCOMPtr<nsIPrintSettings> contentAnalysisPrintSettings; + if (NS_WARN_IF(NS_FAILED(aPrintSettings->Clone( + getter_AddRefs(contentAnalysisPrintSettings)))) || + NS_WARN_IF(!aBrowsingContext->GetCurrentWindowGlobal())) { + return PrintAllowedPromise::CreateAndReject( + PrintAllowedError(NS_ERROR_FAILURE), __func__); + } + contentAnalysisPrintSettings->SetOutputDestination( + nsIPrintSettings::OutputDestinationType::kOutputDestinationStream); + contentAnalysisPrintSettings->SetOutputFormat( + nsIPrintSettings::kOutputFormatPDF); + nsCOMPtr<nsIStorageStream> storageStream = + do_CreateInstance("@mozilla.org/storagestream;1"); + if (!storageStream) { + return PrintAllowedPromise::CreateAndReject( + PrintAllowedError(NS_ERROR_FAILURE), __func__); + } + // Use segment size of 512K + nsresult rv = storageStream->Init(0x80000, UINT32_MAX); + if (NS_WARN_IF(NS_FAILED(rv))) { + return PrintAllowedPromise::CreateAndReject(PrintAllowedError(rv), + __func__); + } + + nsCOMPtr<nsIOutputStream> outputStream; + storageStream->QueryInterface(NS_GET_IID(nsIOutputStream), + getter_AddRefs(outputStream)); + MOZ_ASSERT(outputStream); + + contentAnalysisPrintSettings->SetOutputStream(outputStream.get()); + RefPtr<dom::CanonicalBrowsingContext> browsingContext = aBrowsingContext; + auto promise = MakeRefPtr<PrintAllowedPromise::Private>(__func__); + nsCOMPtr<nsIPrintSettings> finalPrintSettings(aPrintSettings); + aBrowsingContext + ->PrintWithNoContentAnalysis(contentAnalysisPrintSettings, true, nullptr) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [browsingContext, contentAnalysisPrintSettings, finalPrintSettings, + promise]( + dom::MaybeDiscardedBrowsingContext cachedStaticBrowsingContext) + MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA mutable { + nsCOMPtr<nsIOutputStream> outputStream; + contentAnalysisPrintSettings->GetOutputStream( + getter_AddRefs(outputStream)); + nsCOMPtr<nsIStorageStream> storageStream = + do_QueryInterface(outputStream); + MOZ_ASSERT(storageStream); + nsTArray<uint8_t> printData; + uint32_t length = 0; + storageStream->GetLength(&length); + if (!printData.SetLength(length, fallible)) { + promise->Reject( + PrintAllowedError(NS_ERROR_OUT_OF_MEMORY, + cachedStaticBrowsingContext), + __func__); + return; + } + nsCOMPtr<nsIInputStream> inputStream; + nsresult rv = storageStream->NewInputStream( + 0, getter_AddRefs(inputStream)); + if (NS_FAILED(rv)) { + promise->Reject( + PrintAllowedError(rv, cachedStaticBrowsingContext), + __func__); + return; + } + uint32_t currentPosition = 0; + while (currentPosition < length) { + uint32_t elementsRead = 0; + // Make sure the reinterpret_cast<> below is safe + static_assert(std::is_trivially_assignable_v< + decltype(*printData.Elements()), char>); + rv = inputStream->Read( + reinterpret_cast<char*>(printData.Elements()) + + currentPosition, + length - currentPosition, &elementsRead); + if (NS_WARN_IF(NS_FAILED(rv) || !elementsRead)) { + promise->Reject( + PrintAllowedError(NS_FAILED(rv) ? rv : NS_ERROR_FAILURE, + cachedStaticBrowsingContext), + __func__); + return; + } + currentPosition += elementsRead; + } + + nsString printerName; + rv = contentAnalysisPrintSettings->GetPrinterName(printerName); + if (NS_WARN_IF(NS_FAILED(rv))) { + promise->Reject( + PrintAllowedError(rv, cachedStaticBrowsingContext), + __func__); + return; + } + + auto* windowParent = browsingContext->GetCurrentWindowGlobal(); + if (!windowParent) { + // The print window may have been closed by the user by now. + // Cancel the print. + promise->Reject( + PrintAllowedError(NS_ERROR_ABORT, + cachedStaticBrowsingContext), + __func__); + return; + } + nsCOMPtr<nsIURI> uri = windowParent->GetDocumentURI(); + nsCOMPtr<nsIContentAnalysisRequest> contentAnalysisRequest = + new contentanalysis::ContentAnalysisRequest( + std::move(printData), std::move(uri), + std::move(printerName), windowParent); + auto callback = + MakeRefPtr<contentanalysis::ContentAnalysisCallback>( + [browsingContext, cachedStaticBrowsingContext, promise, + finalPrintSettings = std::move(finalPrintSettings)]( + nsIContentAnalysisResponse* aResponse) + MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA mutable { + bool shouldAllow = false; + DebugOnly<nsresult> rv = + aResponse->GetShouldAllowContent( + &shouldAllow); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + promise->Resolve( + PrintAllowedResult( + shouldAllow, cachedStaticBrowsingContext), + __func__); + }, + [promise, + cachedStaticBrowsingContext](nsresult aError) { + promise->Reject( + PrintAllowedError(aError, + cachedStaticBrowsingContext), + __func__); + }); + nsCOMPtr<nsIContentAnalysis> contentAnalysis = + mozilla::components::nsIContentAnalysis::Service(); + if (NS_WARN_IF(!contentAnalysis)) { + promise->Reject( + PrintAllowedError(rv, cachedStaticBrowsingContext), + __func__); + } else { + bool isActive = false; + nsresult rv = contentAnalysis->GetIsActive(&isActive); + // Should not be called if content analysis is not active + MOZ_ASSERT(isActive); + Unused << NS_WARN_IF(NS_FAILED(rv)); + rv = contentAnalysis->AnalyzeContentRequestCallback( + contentAnalysisRequest, /* aAutoAcknowledge */ true, + callback); + if (NS_WARN_IF(NS_FAILED(rv))) { + promise->Reject( + PrintAllowedError(rv, cachedStaticBrowsingContext), + __func__); + } + } + }, + [promise](nsresult aError) { + promise->Reject(PrintAllowedError(aError), __func__); + }); + return promise; +} +#endif + NS_IMETHODIMP ContentAnalysisResponse::Acknowledge( nsIContentAnalysisAcknowledgement* aAcknowledgement) { @@ -1425,6 +1763,46 @@ nsresult ContentAnalysis::RunAcknowledgeTask( return rv; } +bool ContentAnalysis::LastRequestSucceeded() { + return mLastResult != NS_ERROR_NOT_AVAILABLE && + mLastResult != NS_ERROR_INVALID_SIGNATURE && + mLastResult != NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +ContentAnalysis::GetDiagnosticInfo(JSContext* aCx, + mozilla::dom::Promise** aPromise) { + RefPtr<mozilla::dom::Promise> promise; + nsresult rv = MakePromise(aCx, &promise); + NS_ENSURE_SUCCESS(rv, rv); + mCaClientPromise->Then( + GetCurrentSerialEventTarget(), __func__, + [promise](std::shared_ptr<content_analysis::sdk::Client> client) mutable { + if (!client) { + auto info = MakeRefPtr<ContentAnalysisDiagnosticInfo>( + false, EmptyString(), false, 0); + promise->MaybeResolve(info); + return; + } + RefPtr<ContentAnalysis> self = GetContentAnalysisFromService(); + std::string agentPath = client->GetAgentInfo().binary_path; + nsString agentWidePath = NS_ConvertUTF8toUTF16(agentPath); + auto info = MakeRefPtr<ContentAnalysisDiagnosticInfo>( + self->LastRequestSucceeded(), std::move(agentWidePath), false, + self ? self->mRequestCount : 0); + promise->MaybeResolve(info); + }, + [promise](nsresult rv) { + RefPtr<ContentAnalysis> self = GetContentAnalysisFromService(); + auto info = MakeRefPtr<ContentAnalysisDiagnosticInfo>( + false, EmptyString(), rv == NS_ERROR_INVALID_SIGNATURE, + self ? self->mRequestCount : 0); + promise->MaybeResolve(info); + }); + promise.forget(aPromise); + return NS_OK; +} + NS_IMETHODIMP ContentAnalysisCallback::ContentResult( nsIContentAnalysisResponse* aResponse) { if (mPromise.isSome()) { @@ -1448,6 +1826,28 @@ ContentAnalysisCallback::ContentAnalysisCallback(RefPtr<dom::Promise> aPromise) : mPromise(Some(new nsMainThreadPtrHolder<dom::Promise>( "content analysis promise", aPromise))) {} +NS_IMETHODIMP ContentAnalysisDiagnosticInfo::GetConnectedToAgent( + bool* aConnectedToAgent) { + *aConnectedToAgent = mConnectedToAgent; + return NS_OK; +} +NS_IMETHODIMP ContentAnalysisDiagnosticInfo::GetAgentPath( + nsAString& aAgentPath) { + aAgentPath = mAgentPath; + return NS_OK; +} +NS_IMETHODIMP ContentAnalysisDiagnosticInfo::GetFailedSignatureVerification( + bool* aFailedSignatureVerification) { + *aFailedSignatureVerification = mFailedSignatureVerification; + return NS_OK; +} + +NS_IMETHODIMP ContentAnalysisDiagnosticInfo::GetRequestCount( + int64_t* aRequestCount) { + *aRequestCount = mRequestCount; + return NS_OK; +} + #undef LOGD #undef LOGE } // namespace mozilla::contentanalysis diff --git a/toolkit/components/contentanalysis/ContentAnalysis.h b/toolkit/components/contentanalysis/ContentAnalysis.h index 17b6e3fc1b..f2545624fd 100644 --- a/toolkit/components/contentanalysis/ContentAnalysis.h +++ b/toolkit/components/contentanalysis/ContentAnalysis.h @@ -8,6 +8,8 @@ #include "mozilla/DataMutex.h" #include "mozilla/MozPromise.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/MaybeDiscarded.h" #include "mozilla/dom/Promise.h" #include "nsIContentAnalysis.h" #include "nsProxyRelease.h" @@ -18,10 +20,16 @@ #include <regex> #include <string> +#ifdef XP_WIN +# include <windows.h> +#endif // XP_WIN + class nsIPrincipal; +class nsIPrintSettings; class ContentAnalysisTest; namespace mozilla::dom { +class CanonicalBrowsingContext; class DataTransfer; class WindowGlobalParent; } // namespace mozilla::dom @@ -34,6 +42,27 @@ class ContentAnalysisResponse; namespace mozilla::contentanalysis { +class ContentAnalysisDiagnosticInfo final + : public nsIContentAnalysisDiagnosticInfo { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSICONTENTANALYSISDIAGNOSTICINFO + ContentAnalysisDiagnosticInfo(bool aConnectedToAgent, nsString aAgentPath, + bool aFailedSignatureVerification, + int64_t aRequestCount) + : mConnectedToAgent(aConnectedToAgent), + mAgentPath(std::move(aAgentPath)), + mFailedSignatureVerification(aFailedSignatureVerification), + mRequestCount(aRequestCount) {} + + private: + ~ContentAnalysisDiagnosticInfo() = default; + bool mConnectedToAgent; + nsString mAgentPath; + bool mFailedSignatureVerification; + int64_t mRequestCount; +}; + class ContentAnalysisRequest final : public nsIContentAnalysisRequest { public: NS_DECL_ISUPPORTS @@ -43,11 +72,15 @@ class ContentAnalysisRequest final : public nsIContentAnalysisRequest { bool aStringIsFilePath, nsCString aSha256Digest, nsCOMPtr<nsIURI> aUrl, OperationType aOperationType, dom::WindowGlobalParent* aWindowGlobalParent); + ContentAnalysisRequest(const nsTArray<uint8_t> aPrintData, + nsCOMPtr<nsIURI> aUrl, nsString aPrinterName, + dom::WindowGlobalParent* aWindowGlobalParent); static nsresult GetFileDigest(const nsAString& aFilePath, nsCString& aDigestString); private: - ~ContentAnalysisRequest() = default; + ~ContentAnalysisRequest(); + // Remove unneeded copy constructor/assignment ContentAnalysisRequest(const ContentAnalysisRequest&) = delete; ContentAnalysisRequest& operator=(ContentAnalysisRequest&) = delete; @@ -84,7 +117,16 @@ class ContentAnalysisRequest final : public nsIContentAnalysisRequest { // OPERATION_CUSTOMDISPLAYSTRING nsString mOperationDisplayString; + // The name of the printer being printed to + nsString mPrinterName; + RefPtr<dom::WindowGlobalParent> mWindowGlobalParent; +#ifdef XP_WIN + // The printed data to analyze, in PDF format + HANDLE mPrintDataHandle = 0; + // The size of the printed data in mPrintDataHandle + uint64_t mPrintDataSize = 0; +#endif friend class ::ContentAnalysisTest; }; @@ -105,6 +147,40 @@ class ContentAnalysis final : public nsIContentAnalysis { ContentAnalysis(); nsCString GetUserActionId(); + void SetLastResult(nsresult aLastResult) { mLastResult = aLastResult; } + + struct PrintAllowedResult final { + bool mAllowed; + dom::MaybeDiscarded<dom::BrowsingContext> + mCachedStaticDocumentBrowsingContext; + PrintAllowedResult(bool aAllowed, dom::MaybeDiscarded<dom::BrowsingContext> + aCachedStaticDocumentBrowsingContext) + : mAllowed(aAllowed), + mCachedStaticDocumentBrowsingContext( + aCachedStaticDocumentBrowsingContext) {} + explicit PrintAllowedResult(bool aAllowed) + : PrintAllowedResult(aAllowed, dom::MaybeDiscardedBrowsingContext()) {} + }; + struct PrintAllowedError final { + nsresult mError; + dom::MaybeDiscarded<dom::BrowsingContext> + mCachedStaticDocumentBrowsingContext; + PrintAllowedError(nsresult aError, dom::MaybeDiscarded<dom::BrowsingContext> + aCachedStaticDocumentBrowsingContext) + : mError(aError), + mCachedStaticDocumentBrowsingContext( + aCachedStaticDocumentBrowsingContext) {} + explicit PrintAllowedError(nsresult aError) + : PrintAllowedError(aError, dom::MaybeDiscardedBrowsingContext()) {} + }; + using PrintAllowedPromise = + MozPromise<PrintAllowedResult, PrintAllowedError, true>; +#if defined(XP_WIN) + MOZ_CAN_RUN_SCRIPT static RefPtr<PrintAllowedPromise> + PrintToPDFToDetermineIfPrintAllowed( + dom::CanonicalBrowsingContext* aBrowsingContext, + nsIPrintSettings* aPrintSettings); +#endif // defined(XP_WIN) private: ~ContentAnalysis(); @@ -114,6 +190,10 @@ class ContentAnalysis final : public nsIContentAnalysis { nsresult CreateContentAnalysisClient(nsCString&& aPipePathName, nsString&& aClientSignatureSetting, bool aIsPerUser); + nsresult AnalyzeContentRequestCallbackPrivate( + nsIContentAnalysisRequest* aRequest, bool aAutoAcknowledge, + nsIContentAnalysisCallback* aCallback); + nsresult RunAnalyzeRequestTask( const RefPtr<nsIContentAnalysisRequest>& aRequest, bool aAutoAcknowledge, int64_t aRequestCount, @@ -129,6 +209,7 @@ class ContentAnalysis final : public nsIContentAnalysis { content_analysis::sdk::ContentAnalysisRequest&& aRequest, const std::shared_ptr<content_analysis::sdk::Client>& aClient); void IssueResponse(RefPtr<ContentAnalysisResponse>& response); + bool LastRequestSucceeded(); // Did the URL filter completely handle the request or do we need to check // with the agent. @@ -147,6 +228,7 @@ class ContentAnalysis final : public nsIContentAnalysis { bool mClientCreationAttempted; bool mSetByEnterprise; + nsresult mLastResult = NS_OK; class CallbackData final { public: @@ -180,7 +262,7 @@ class ContentAnalysis final : public nsIContentAnalysis { std::vector<std::regex> mAllowUrlList; std::vector<std::regex> mDenyUrlList; - bool mParsedUrlLists; + bool mParsedUrlLists = false; friend class ContentAnalysisResponse; friend class ::ContentAnalysisTest; @@ -198,6 +280,7 @@ class ContentAnalysisResponse final : public nsIContentAnalysisResponse { void SetOwner(RefPtr<ContentAnalysis> aOwner); void DoNotAcknowledge() { mDoNotAcknowledge = true; } + void SetCancelError(CancelError aCancelError); private: ~ContentAnalysisResponse() = default; @@ -218,7 +301,11 @@ class ContentAnalysisResponse final : public nsIContentAnalysisResponse { // Identifier for the corresponding nsIContentAnalysisRequest nsCString mRequestToken; - // ContentAnalysis (or, more precisely, it's Client object) must outlive + // If mAction is eCanceled, this is the error explaining why the request was + // canceled, or eUserInitiated if the user canceled it. + CancelError mCancelError = CancelError::eUserInitiated; + + // ContentAnalysis (or, more precisely, its Client object) must outlive // the transaction. RefPtr<ContentAnalysis> mOwner; diff --git a/toolkit/components/contentanalysis/ContentAnalysisIPCTypes.h b/toolkit/components/contentanalysis/ContentAnalysisIPCTypes.h index a7cf812d0b..a554036257 100644 --- a/toolkit/components/contentanalysis/ContentAnalysisIPCTypes.h +++ b/toolkit/components/contentanalysis/ContentAnalysisIPCTypes.h @@ -17,12 +17,12 @@ namespace mozilla { namespace contentanalysis { enum class NoContentAnalysisResult : uint8_t { - CONTENT_ANALYSIS_NOT_ACTIVE, - CONTEXT_EXEMPT_FROM_CONTENT_ANALYSIS, - CANCELED, - ERROR_INVALID_JSON_RESPONSE, - ERROR_COULD_NOT_GET_DATA, - ERROR_OTHER, + ALLOW_DUE_TO_CONTENT_ANALYSIS_NOT_ACTIVE, + ALLOW_DUE_TO_CONTEXT_EXEMPT_FROM_CONTENT_ANALYSIS, + ALLOW_DUE_TO_COULD_NOT_GET_DATA, + DENY_DUE_TO_CANCELED, + DENY_DUE_TO_INVALID_JSON_RESPONSE, + DENY_DUE_TO_OTHER_ERROR, LAST_VALUE }; @@ -59,7 +59,8 @@ class ContentAnalysisResult : public nsIContentAnalysisResult { } } } - return FromNoResult(NoContentAnalysisResult::ERROR_INVALID_JSON_RESPONSE); + return FromNoResult( + NoContentAnalysisResult::DENY_DUE_TO_INVALID_JSON_RESPONSE); } static RefPtr<ContentAnalysisResult> FromJSONContentAnalysisResponse( @@ -76,16 +77,21 @@ class ContentAnalysisResult : public nsIContentAnalysisResult { } else if (shouldAllowValue.isFalse()) { return FromAction(nsIContentAnalysisResponse::Action::eBlock); } else { - return FromNoResult(NoContentAnalysisResult::ERROR_OTHER); + return FromNoResult(NoContentAnalysisResult::DENY_DUE_TO_OTHER_ERROR); } } } - return FromNoResult(NoContentAnalysisResult::ERROR_INVALID_JSON_RESPONSE); + return FromNoResult( + NoContentAnalysisResult::DENY_DUE_TO_INVALID_JSON_RESPONSE); } static RefPtr<ContentAnalysisResult> FromContentAnalysisResponse( nsIContentAnalysisResponse* aResponse) { - if (aResponse->GetShouldAllowContent()) { + bool shouldAllowContent = false; + DebugOnly<nsresult> rv = + aResponse->GetShouldAllowContent(&shouldAllowContent); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + if (shouldAllowContent) { return FromAction(nsIContentAnalysisResponse::Action::eAllow); } else { return FromAction(nsIContentAnalysisResponse::Action::eBlock); @@ -159,7 +165,8 @@ struct ParamTraits<mozilla::contentanalysis::ContentAnalysisResult*> { return true; } *aResult = mozilla::contentanalysis::ContentAnalysisResult::FromNoResult( - mozilla::contentanalysis::NoContentAnalysisResult::ERROR_OTHER); + mozilla::contentanalysis::NoContentAnalysisResult:: + DENY_DUE_TO_OTHER_ERROR); return ReadParam(aReader, &((*aResult)->mValue)); } }; diff --git a/toolkit/components/contentanalysis/components.conf b/toolkit/components/contentanalysis/components.conf index 82236cb1b9..1683ef99d7 100644 --- a/toolkit/components/contentanalysis/components.conf +++ b/toolkit/components/contentanalysis/components.conf @@ -11,5 +11,6 @@ Classes = [ 'contract_ids': ['@mozilla.org/contentanalysis;1'], 'type': 'mozilla::contentanalysis::ContentAnalysis', 'headers': ['/toolkit/components/contentanalysis/ContentAnalysis.h'], + 'overridable': True, }, ] diff --git a/toolkit/components/contentanalysis/content_analysis/sdk/analysis.pb.cc b/toolkit/components/contentanalysis/content_analysis/sdk/analysis.pb.cc index 6a2e1ccf46..b7a7434685 100644 --- a/toolkit/components/contentanalysis/content_analysis/sdk/analysis.pb.cc +++ b/toolkit/components/contentanalysis/content_analysis/sdk/analysis.pb.cc @@ -1,7 +1,7 @@ // Generated by the protocol buffer compiler. DO NOT EDIT! -// source: analysis.proto +// source: content_analysis/sdk/analysis.proto -#include "analysis.pb.h" +#include "content_analysis/sdk/analysis.pb.h" #include <algorithm> @@ -134,9 +134,10 @@ PROTOBUF_CONSTEXPR ContentAnalysisRequest::ContentAnalysisRequest( , /*decltype(_impl_.user_action_id_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} , /*decltype(_impl_.request_data_)*/nullptr , /*decltype(_impl_.client_metadata_)*/nullptr + , /*decltype(_impl_.analysis_connector_)*/0 + , /*decltype(_impl_.reason_)*/0 , /*decltype(_impl_.expires_at_)*/int64_t{0} , /*decltype(_impl_.user_action_requests_count_)*/int64_t{0} - , /*decltype(_impl_.analysis_connector_)*/0 , /*decltype(_impl_.content_data_)*/{} , /*decltype(_impl_._oneof_case_)*/{}} {} struct ContentAnalysisRequestDefaultTypeInternal { @@ -400,6 +401,94 @@ constexpr ClientDownloadRequest_ResourceType ClientDownloadRequest::ResourceType constexpr ClientDownloadRequest_ResourceType ClientDownloadRequest::ResourceType_MAX; constexpr int ClientDownloadRequest::ResourceType_ARRAYSIZE; #endif // (__cplusplus < 201703) && (!defined(_MSC_VER) || (_MSC_VER >= 1900 && _MSC_VER < 1912)) +bool ContentAnalysisRequest_Reason_IsValid(int value) { + switch (value) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + return true; + default: + return false; + } +} + +static ::PROTOBUF_NAMESPACE_ID::internal::ExplicitlyConstructed<std::string> ContentAnalysisRequest_Reason_strings[8] = {}; + +static const char ContentAnalysisRequest_Reason_names[] = + "CLIPBOARD_PASTE" + "DRAG_AND_DROP" + "FILE_PICKER_DIALOG" + "NORMAL_DOWNLOAD" + "PRINT_PREVIEW_PRINT" + "SAVE_AS_DOWNLOAD" + "SYSTEM_DIALOG_PRINT" + "UNKNOWN"; + +static const ::PROTOBUF_NAMESPACE_ID::internal::EnumEntry ContentAnalysisRequest_Reason_entries[] = { + { {ContentAnalysisRequest_Reason_names + 0, 15}, 1 }, + { {ContentAnalysisRequest_Reason_names + 15, 13}, 2 }, + { {ContentAnalysisRequest_Reason_names + 28, 18}, 3 }, + { {ContentAnalysisRequest_Reason_names + 46, 15}, 6 }, + { {ContentAnalysisRequest_Reason_names + 61, 19}, 4 }, + { {ContentAnalysisRequest_Reason_names + 80, 16}, 7 }, + { {ContentAnalysisRequest_Reason_names + 96, 19}, 5 }, + { {ContentAnalysisRequest_Reason_names + 115, 7}, 0 }, +}; + +static const int ContentAnalysisRequest_Reason_entries_by_number[] = { + 7, // 0 -> UNKNOWN + 0, // 1 -> CLIPBOARD_PASTE + 1, // 2 -> DRAG_AND_DROP + 2, // 3 -> FILE_PICKER_DIALOG + 4, // 4 -> PRINT_PREVIEW_PRINT + 6, // 5 -> SYSTEM_DIALOG_PRINT + 3, // 6 -> NORMAL_DOWNLOAD + 5, // 7 -> SAVE_AS_DOWNLOAD +}; + +const std::string& ContentAnalysisRequest_Reason_Name( + ContentAnalysisRequest_Reason value) { + static const bool dummy = + ::PROTOBUF_NAMESPACE_ID::internal::InitializeEnumStrings( + ContentAnalysisRequest_Reason_entries, + ContentAnalysisRequest_Reason_entries_by_number, + 8, ContentAnalysisRequest_Reason_strings); + (void) dummy; + int idx = ::PROTOBUF_NAMESPACE_ID::internal::LookUpEnumName( + ContentAnalysisRequest_Reason_entries, + ContentAnalysisRequest_Reason_entries_by_number, + 8, value); + return idx == -1 ? ::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString() : + ContentAnalysisRequest_Reason_strings[idx].get(); +} +bool ContentAnalysisRequest_Reason_Parse( + ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, ContentAnalysisRequest_Reason* value) { + int int_value; + bool success = ::PROTOBUF_NAMESPACE_ID::internal::LookUpEnumValue( + ContentAnalysisRequest_Reason_entries, 8, name, &int_value); + if (success) { + *value = static_cast<ContentAnalysisRequest_Reason>(int_value); + } + return success; +} +#if (__cplusplus < 201703) && (!defined(_MSC_VER) || (_MSC_VER >= 1900 && _MSC_VER < 1912)) +constexpr ContentAnalysisRequest_Reason ContentAnalysisRequest::UNKNOWN; +constexpr ContentAnalysisRequest_Reason ContentAnalysisRequest::CLIPBOARD_PASTE; +constexpr ContentAnalysisRequest_Reason ContentAnalysisRequest::DRAG_AND_DROP; +constexpr ContentAnalysisRequest_Reason ContentAnalysisRequest::FILE_PICKER_DIALOG; +constexpr ContentAnalysisRequest_Reason ContentAnalysisRequest::PRINT_PREVIEW_PRINT; +constexpr ContentAnalysisRequest_Reason ContentAnalysisRequest::SYSTEM_DIALOG_PRINT; +constexpr ContentAnalysisRequest_Reason ContentAnalysisRequest::NORMAL_DOWNLOAD; +constexpr ContentAnalysisRequest_Reason ContentAnalysisRequest::SAVE_AS_DOWNLOAD; +constexpr ContentAnalysisRequest_Reason ContentAnalysisRequest::Reason_MIN; +constexpr ContentAnalysisRequest_Reason ContentAnalysisRequest::Reason_MAX; +constexpr int ContentAnalysisRequest::Reason_ARRAYSIZE; +#endif // (__cplusplus < 201703) && (!defined(_MSC_VER) || (_MSC_VER >= 1900 && _MSC_VER < 1912)) bool ContentAnalysisResponse_Result_TriggeredRule_Action_IsValid(int value) { switch (value) { case 0: @@ -2650,7 +2739,7 @@ class ContentAnalysisRequest::_Internal { (*has_bits)[0] |= 1u; } static void set_has_analysis_connector(HasBits* has_bits) { - (*has_bits)[0] |= 64u; + (*has_bits)[0] |= 16u; } static const ::content_analysis::sdk::ContentMetaData& request_data(const ContentAnalysisRequest* msg); static void set_has_request_data(HasBits* has_bits) { @@ -2662,12 +2751,15 @@ class ContentAnalysisRequest::_Internal { } static const ::content_analysis::sdk::ContentAnalysisRequest_PrintData& print_data(const ContentAnalysisRequest* msg); static void set_has_expires_at(HasBits* has_bits) { - (*has_bits)[0] |= 16u; + (*has_bits)[0] |= 64u; } static void set_has_user_action_id(HasBits* has_bits) { (*has_bits)[0] |= 2u; } static void set_has_user_action_requests_count(HasBits* has_bits) { + (*has_bits)[0] |= 128u; + } + static void set_has_reason(HasBits* has_bits) { (*has_bits)[0] |= 32u; } }; @@ -2716,9 +2808,10 @@ ContentAnalysisRequest::ContentAnalysisRequest(const ContentAnalysisRequest& fro , decltype(_impl_.user_action_id_){} , decltype(_impl_.request_data_){nullptr} , decltype(_impl_.client_metadata_){nullptr} + , decltype(_impl_.analysis_connector_){} + , decltype(_impl_.reason_){} , decltype(_impl_.expires_at_){} , decltype(_impl_.user_action_requests_count_){} - , decltype(_impl_.analysis_connector_){} , decltype(_impl_.content_data_){} , /*decltype(_impl_._oneof_case_)*/{}}; @@ -2745,9 +2838,9 @@ ContentAnalysisRequest::ContentAnalysisRequest(const ContentAnalysisRequest& fro if (from._internal_has_client_metadata()) { _this->_impl_.client_metadata_ = new ::content_analysis::sdk::ClientMetadata(*from._impl_.client_metadata_); } - ::memcpy(&_impl_.expires_at_, &from._impl_.expires_at_, - static_cast<size_t>(reinterpret_cast<char*>(&_impl_.analysis_connector_) - - reinterpret_cast<char*>(&_impl_.expires_at_)) + sizeof(_impl_.analysis_connector_)); + ::memcpy(&_impl_.analysis_connector_, &from._impl_.analysis_connector_, + static_cast<size_t>(reinterpret_cast<char*>(&_impl_.user_action_requests_count_) - + reinterpret_cast<char*>(&_impl_.analysis_connector_)) + sizeof(_impl_.user_action_requests_count_)); clear_has_content_data(); switch (from.content_data_case()) { case kTextContent: { @@ -2782,9 +2875,10 @@ inline void ContentAnalysisRequest::SharedCtor( , decltype(_impl_.user_action_id_){} , decltype(_impl_.request_data_){nullptr} , decltype(_impl_.client_metadata_){nullptr} + , decltype(_impl_.analysis_connector_){0} + , decltype(_impl_.reason_){0} , decltype(_impl_.expires_at_){int64_t{0}} , decltype(_impl_.user_action_requests_count_){int64_t{0}} - , decltype(_impl_.analysis_connector_){0} , decltype(_impl_.content_data_){} , /*decltype(_impl_._oneof_case_)*/{} }; @@ -2873,10 +2967,10 @@ void ContentAnalysisRequest::Clear() { _impl_.client_metadata_->Clear(); } } - if (cached_has_bits & 0x00000070u) { - ::memset(&_impl_.expires_at_, 0, static_cast<size_t>( - reinterpret_cast<char*>(&_impl_.analysis_connector_) - - reinterpret_cast<char*>(&_impl_.expires_at_)) + sizeof(_impl_.analysis_connector_)); + if (cached_has_bits & 0x000000f0u) { + ::memset(&_impl_.analysis_connector_, 0, static_cast<size_t>( + reinterpret_cast<char*>(&_impl_.user_action_requests_count_) - + reinterpret_cast<char*>(&_impl_.analysis_connector_)) + sizeof(_impl_.user_action_requests_count_)); } clear_content_data(); _impl_._has_bits_.Clear(); @@ -2995,6 +3089,19 @@ const char* ContentAnalysisRequest::_InternalParse(const char* ptr, ::_pbi::Pars } else goto handle_unusual; continue; + // optional .content_analysis.sdk.ContentAnalysisRequest.Reason reason = 19; + case 19: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 152)) { + uint64_t val = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + if (PROTOBUF_PREDICT_TRUE(::content_analysis::sdk::ContentAnalysisRequest_Reason_IsValid(val))) { + _internal_set_reason(static_cast<::content_analysis::sdk::ContentAnalysisRequest_Reason>(val)); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::WriteVarint(19, val, mutable_unknown_fields()); + } + } else + goto handle_unusual; + continue; default: goto handle_unusual; } // switch @@ -3033,7 +3140,7 @@ uint8_t* ContentAnalysisRequest::_InternalSerialize( } // optional .content_analysis.sdk.AnalysisConnector analysis_connector = 9; - if (cached_has_bits & 0x00000040u) { + if (cached_has_bits & 0x00000010u) { target = stream->EnsureSpace(target); target = ::_pbi::WireFormatLite::WriteEnumToArray( 9, this->_internal_analysis_connector(), target); @@ -3073,7 +3180,7 @@ uint8_t* ContentAnalysisRequest::_InternalSerialize( default: ; } // optional int64 expires_at = 15; - if (cached_has_bits & 0x00000010u) { + if (cached_has_bits & 0x00000040u) { target = stream->EnsureSpace(target); target = ::_pbi::WireFormatLite::WriteInt64ToArray(15, this->_internal_expires_at(), target); } @@ -3085,7 +3192,7 @@ uint8_t* ContentAnalysisRequest::_InternalSerialize( } // optional int64 user_action_requests_count = 17; - if (cached_has_bits & 0x00000020u) { + if (cached_has_bits & 0x00000080u) { target = stream->EnsureSpace(target); target = ::_pbi::WireFormatLite::WriteInt64ToArray(17, this->_internal_user_action_requests_count(), target); } @@ -3097,6 +3204,13 @@ uint8_t* ContentAnalysisRequest::_InternalSerialize( _Internal::print_data(this).GetCachedSize(), target, stream); } + // optional .content_analysis.sdk.ContentAnalysisRequest.Reason reason = 19; + if (cached_has_bits & 0x00000020u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteEnumToArray( + 19, this->_internal_reason(), target); + } + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { target = stream->WriteRaw(_internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), static_cast<int>(_internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); @@ -3122,7 +3236,7 @@ size_t ContentAnalysisRequest::ByteSizeLong() const { } cached_has_bits = _impl_._has_bits_[0]; - if (cached_has_bits & 0x0000007fu) { + if (cached_has_bits & 0x000000ffu) { // optional string request_token = 5; if (cached_has_bits & 0x00000001u) { total_size += 1 + @@ -3151,22 +3265,28 @@ size_t ContentAnalysisRequest::ByteSizeLong() const { *_impl_.client_metadata_); } - // optional int64 expires_at = 15; + // optional .content_analysis.sdk.AnalysisConnector analysis_connector = 9; if (cached_has_bits & 0x00000010u) { - total_size += ::_pbi::WireFormatLite::Int64SizePlusOne(this->_internal_expires_at()); + total_size += 1 + + ::_pbi::WireFormatLite::EnumSize(this->_internal_analysis_connector()); } - // optional int64 user_action_requests_count = 17; + // optional .content_analysis.sdk.ContentAnalysisRequest.Reason reason = 19; if (cached_has_bits & 0x00000020u) { total_size += 2 + - ::_pbi::WireFormatLite::Int64Size( - this->_internal_user_action_requests_count()); + ::_pbi::WireFormatLite::EnumSize(this->_internal_reason()); } - // optional .content_analysis.sdk.AnalysisConnector analysis_connector = 9; + // optional int64 expires_at = 15; if (cached_has_bits & 0x00000040u) { - total_size += 1 + - ::_pbi::WireFormatLite::EnumSize(this->_internal_analysis_connector()); + total_size += ::_pbi::WireFormatLite::Int64SizePlusOne(this->_internal_expires_at()); + } + + // optional int64 user_action_requests_count = 17; + if (cached_has_bits & 0x00000080u) { + total_size += 2 + + ::_pbi::WireFormatLite::Int64Size( + this->_internal_user_action_requests_count()); } } @@ -3219,7 +3339,7 @@ void ContentAnalysisRequest::MergeFrom(const ContentAnalysisRequest& from) { _this->_impl_.tags_.MergeFrom(from._impl_.tags_); cached_has_bits = from._impl_._has_bits_[0]; - if (cached_has_bits & 0x0000007fu) { + if (cached_has_bits & 0x000000ffu) { if (cached_has_bits & 0x00000001u) { _this->_internal_set_request_token(from._internal_request_token()); } @@ -3235,13 +3355,16 @@ void ContentAnalysisRequest::MergeFrom(const ContentAnalysisRequest& from) { from._internal_client_metadata()); } if (cached_has_bits & 0x00000010u) { - _this->_impl_.expires_at_ = from._impl_.expires_at_; + _this->_impl_.analysis_connector_ = from._impl_.analysis_connector_; } if (cached_has_bits & 0x00000020u) { - _this->_impl_.user_action_requests_count_ = from._impl_.user_action_requests_count_; + _this->_impl_.reason_ = from._impl_.reason_; } if (cached_has_bits & 0x00000040u) { - _this->_impl_.analysis_connector_ = from._impl_.analysis_connector_; + _this->_impl_.expires_at_ = from._impl_.expires_at_; + } + if (cached_has_bits & 0x00000080u) { + _this->_impl_.user_action_requests_count_ = from._impl_.user_action_requests_count_; } _this->_impl_._has_bits_[0] |= cached_has_bits; } @@ -3296,8 +3419,8 @@ void ContentAnalysisRequest::InternalSwap(ContentAnalysisRequest* other) { &other->_impl_.user_action_id_, rhs_arena ); ::PROTOBUF_NAMESPACE_ID::internal::memswap< - PROTOBUF_FIELD_OFFSET(ContentAnalysisRequest, _impl_.analysis_connector_) - + sizeof(ContentAnalysisRequest::_impl_.analysis_connector_) + PROTOBUF_FIELD_OFFSET(ContentAnalysisRequest, _impl_.user_action_requests_count_) + + sizeof(ContentAnalysisRequest::_impl_.user_action_requests_count_) - PROTOBUF_FIELD_OFFSET(ContentAnalysisRequest, _impl_.request_data_)>( reinterpret_cast<char*>(&_impl_.request_data_), reinterpret_cast<char*>(&other->_impl_.request_data_)); diff --git a/toolkit/components/contentanalysis/content_analysis/sdk/analysis.pb.h b/toolkit/components/contentanalysis/content_analysis/sdk/analysis.pb.h index 186cc97649..875c632ebf 100644 --- a/toolkit/components/contentanalysis/content_analysis/sdk/analysis.pb.h +++ b/toolkit/components/contentanalysis/content_analysis/sdk/analysis.pb.h @@ -1,8 +1,8 @@ // Generated by the protocol buffer compiler. DO NOT EDIT! -// source: analysis.proto +// source: content_analysis/sdk/analysis.proto -#ifndef GOOGLE_PROTOBUF_INCLUDED_analysis_2eproto -#define GOOGLE_PROTOBUF_INCLUDED_analysis_2eproto +#ifndef GOOGLE_PROTOBUF_INCLUDED_content_5fanalysis_2fsdk_2fanalysis_2eproto +#define GOOGLE_PROTOBUF_INCLUDED_content_5fanalysis_2fsdk_2fanalysis_2eproto #include <limits> #include <string> @@ -13,7 +13,7 @@ #error incompatible with your Protocol Buffer headers. Please update #error your headers. #endif -#if 3021006 < PROTOBUF_MIN_PROTOC_VERSION +#if 3021012 < PROTOBUF_MIN_PROTOC_VERSION #error This file was generated by an older version of protoc which is #error incompatible with your Protocol Buffer headers. Please #error regenerate this file with a newer version of protoc. @@ -31,7 +31,7 @@ #include <google/protobuf/generated_enum_util.h> // @@protoc_insertion_point(includes) #include <google/protobuf/port_def.inc> -#define PROTOBUF_INTERNAL_EXPORT_analysis_2eproto +#define PROTOBUF_INTERNAL_EXPORT_content_5fanalysis_2fsdk_2fanalysis_2eproto PROTOBUF_NAMESPACE_OPEN namespace internal { class AnyMetadata; @@ -39,7 +39,7 @@ class AnyMetadata; PROTOBUF_NAMESPACE_CLOSE // Internal implementation detail -- do not use these members. -struct TableStruct_analysis_2eproto { +struct TableStruct_content_5fanalysis_2fsdk_2fanalysis_2eproto { static const uint32_t offsets[]; }; namespace content_analysis { @@ -154,6 +154,31 @@ inline const std::string& ClientDownloadRequest_ResourceType_Name(T enum_t_value } bool ClientDownloadRequest_ResourceType_Parse( ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, ClientDownloadRequest_ResourceType* value); +enum ContentAnalysisRequest_Reason : int { + ContentAnalysisRequest_Reason_UNKNOWN = 0, + ContentAnalysisRequest_Reason_CLIPBOARD_PASTE = 1, + ContentAnalysisRequest_Reason_DRAG_AND_DROP = 2, + ContentAnalysisRequest_Reason_FILE_PICKER_DIALOG = 3, + ContentAnalysisRequest_Reason_PRINT_PREVIEW_PRINT = 4, + ContentAnalysisRequest_Reason_SYSTEM_DIALOG_PRINT = 5, + ContentAnalysisRequest_Reason_NORMAL_DOWNLOAD = 6, + ContentAnalysisRequest_Reason_SAVE_AS_DOWNLOAD = 7 +}; +bool ContentAnalysisRequest_Reason_IsValid(int value); +constexpr ContentAnalysisRequest_Reason ContentAnalysisRequest_Reason_Reason_MIN = ContentAnalysisRequest_Reason_UNKNOWN; +constexpr ContentAnalysisRequest_Reason ContentAnalysisRequest_Reason_Reason_MAX = ContentAnalysisRequest_Reason_SAVE_AS_DOWNLOAD; +constexpr int ContentAnalysisRequest_Reason_Reason_ARRAYSIZE = ContentAnalysisRequest_Reason_Reason_MAX + 1; + +const std::string& ContentAnalysisRequest_Reason_Name(ContentAnalysisRequest_Reason value); +template<typename T> +inline const std::string& ContentAnalysisRequest_Reason_Name(T enum_t_value) { + static_assert(::std::is_same<T, ContentAnalysisRequest_Reason>::value || + ::std::is_integral<T>::value, + "Incorrect type passed to function ContentAnalysisRequest_Reason_Name."); + return ContentAnalysisRequest_Reason_Name(static_cast<ContentAnalysisRequest_Reason>(enum_t_value)); +} +bool ContentAnalysisRequest_Reason_Parse( + ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, ContentAnalysisRequest_Reason* value); enum ContentAnalysisResponse_Result_TriggeredRule_Action : int { ContentAnalysisResponse_Result_TriggeredRule_Action_ACTION_UNSPECIFIED = 0, ContentAnalysisResponse_Result_TriggeredRule_Action_REPORT_ONLY = 1, @@ -448,7 +473,7 @@ class ContentMetaData_PrintMetadata final : int printer_type_; }; union { Impl_ _impl_; }; - friend struct ::TableStruct_analysis_2eproto; + friend struct ::TableStruct_content_5fanalysis_2fsdk_2fanalysis_2eproto; }; // ------------------------------------------------------------------- @@ -717,7 +742,7 @@ class ContentMetaData final : ::content_analysis::sdk::ContentMetaData_PrintMetadata* print_metadata_; }; union { Impl_ _impl_; }; - friend struct ::TableStruct_analysis_2eproto; + friend struct ::TableStruct_content_5fanalysis_2fsdk_2fanalysis_2eproto; }; // ------------------------------------------------------------------- @@ -864,7 +889,7 @@ class ClientMetadata_Browser final : ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr machine_user_; }; union { Impl_ _impl_; }; - friend struct ::TableStruct_analysis_2eproto; + friend struct ::TableStruct_content_5fanalysis_2fsdk_2fanalysis_2eproto; }; // ------------------------------------------------------------------- @@ -1013,7 +1038,7 @@ class ClientMetadata final : ::content_analysis::sdk::ClientMetadata_Browser* browser_; }; union { Impl_ _impl_; }; - friend struct ::TableStruct_analysis_2eproto; + friend struct ::TableStruct_content_5fanalysis_2fsdk_2fanalysis_2eproto; }; // ------------------------------------------------------------------- @@ -1178,7 +1203,7 @@ class ClientDownloadRequest_Resource final : int type_; }; union { Impl_ _impl_; }; - friend struct ::TableStruct_analysis_2eproto; + friend struct ::TableStruct_content_5fanalysis_2fsdk_2fanalysis_2eproto; }; // ------------------------------------------------------------------- @@ -1360,7 +1385,7 @@ class ClientDownloadRequest final : mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; }; union { Impl_ _impl_; }; - friend struct ::TableStruct_analysis_2eproto; + friend struct ::TableStruct_content_5fanalysis_2fsdk_2fanalysis_2eproto; }; // ------------------------------------------------------------------- @@ -1517,7 +1542,7 @@ class ContentAnalysisRequest_PrintData final : int64_t size_; }; union { Impl_ _impl_; }; - friend struct ::TableStruct_analysis_2eproto; + friend struct ::TableStruct_content_5fanalysis_2fsdk_2fanalysis_2eproto; }; // ------------------------------------------------------------------- @@ -1637,6 +1662,44 @@ class ContentAnalysisRequest final : typedef ContentAnalysisRequest_PrintData PrintData; + typedef ContentAnalysisRequest_Reason Reason; + static constexpr Reason UNKNOWN = + ContentAnalysisRequest_Reason_UNKNOWN; + static constexpr Reason CLIPBOARD_PASTE = + ContentAnalysisRequest_Reason_CLIPBOARD_PASTE; + static constexpr Reason DRAG_AND_DROP = + ContentAnalysisRequest_Reason_DRAG_AND_DROP; + static constexpr Reason FILE_PICKER_DIALOG = + ContentAnalysisRequest_Reason_FILE_PICKER_DIALOG; + static constexpr Reason PRINT_PREVIEW_PRINT = + ContentAnalysisRequest_Reason_PRINT_PREVIEW_PRINT; + static constexpr Reason SYSTEM_DIALOG_PRINT = + ContentAnalysisRequest_Reason_SYSTEM_DIALOG_PRINT; + static constexpr Reason NORMAL_DOWNLOAD = + ContentAnalysisRequest_Reason_NORMAL_DOWNLOAD; + static constexpr Reason SAVE_AS_DOWNLOAD = + ContentAnalysisRequest_Reason_SAVE_AS_DOWNLOAD; + static inline bool Reason_IsValid(int value) { + return ContentAnalysisRequest_Reason_IsValid(value); + } + static constexpr Reason Reason_MIN = + ContentAnalysisRequest_Reason_Reason_MIN; + static constexpr Reason Reason_MAX = + ContentAnalysisRequest_Reason_Reason_MAX; + static constexpr int Reason_ARRAYSIZE = + ContentAnalysisRequest_Reason_Reason_ARRAYSIZE; + template<typename T> + static inline const std::string& Reason_Name(T enum_t_value) { + static_assert(::std::is_same<T, Reason>::value || + ::std::is_integral<T>::value, + "Incorrect type passed to function Reason_Name."); + return ContentAnalysisRequest_Reason_Name(enum_t_value); + } + static inline bool Reason_Parse(::PROTOBUF_NAMESPACE_ID::ConstStringParam name, + Reason* value) { + return ContentAnalysisRequest_Reason_Parse(name, value); + } + // accessors ------------------------------------------------------- enum : int { @@ -1645,9 +1708,10 @@ class ContentAnalysisRequest final : kUserActionIdFieldNumber = 16, kRequestDataFieldNumber = 10, kClientMetadataFieldNumber = 12, + kAnalysisConnectorFieldNumber = 9, + kReasonFieldNumber = 19, kExpiresAtFieldNumber = 15, kUserActionRequestsCountFieldNumber = 17, - kAnalysisConnectorFieldNumber = 9, kTextContentFieldNumber = 13, kFilePathFieldNumber = 14, kPrintDataFieldNumber = 18, @@ -1748,6 +1812,32 @@ class ContentAnalysisRequest final : ::content_analysis::sdk::ClientMetadata* client_metadata); ::content_analysis::sdk::ClientMetadata* unsafe_arena_release_client_metadata(); + // optional .content_analysis.sdk.AnalysisConnector analysis_connector = 9; + bool has_analysis_connector() const; + private: + bool _internal_has_analysis_connector() const; + public: + void clear_analysis_connector(); + ::content_analysis::sdk::AnalysisConnector analysis_connector() const; + void set_analysis_connector(::content_analysis::sdk::AnalysisConnector value); + private: + ::content_analysis::sdk::AnalysisConnector _internal_analysis_connector() const; + void _internal_set_analysis_connector(::content_analysis::sdk::AnalysisConnector value); + public: + + // optional .content_analysis.sdk.ContentAnalysisRequest.Reason reason = 19; + bool has_reason() const; + private: + bool _internal_has_reason() const; + public: + void clear_reason(); + ::content_analysis::sdk::ContentAnalysisRequest_Reason reason() const; + void set_reason(::content_analysis::sdk::ContentAnalysisRequest_Reason value); + private: + ::content_analysis::sdk::ContentAnalysisRequest_Reason _internal_reason() const; + void _internal_set_reason(::content_analysis::sdk::ContentAnalysisRequest_Reason value); + public: + // optional int64 expires_at = 15; bool has_expires_at() const; private: @@ -1774,19 +1864,6 @@ class ContentAnalysisRequest final : void _internal_set_user_action_requests_count(int64_t value); public: - // optional .content_analysis.sdk.AnalysisConnector analysis_connector = 9; - bool has_analysis_connector() const; - private: - bool _internal_has_analysis_connector() const; - public: - void clear_analysis_connector(); - ::content_analysis::sdk::AnalysisConnector analysis_connector() const; - void set_analysis_connector(::content_analysis::sdk::AnalysisConnector value); - private: - ::content_analysis::sdk::AnalysisConnector _internal_analysis_connector() const; - void _internal_set_analysis_connector(::content_analysis::sdk::AnalysisConnector value); - public: - // string text_content = 13; bool has_text_content() const; private: @@ -1864,9 +1941,10 @@ class ContentAnalysisRequest final : ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr user_action_id_; ::content_analysis::sdk::ContentMetaData* request_data_; ::content_analysis::sdk::ClientMetadata* client_metadata_; + int analysis_connector_; + int reason_; int64_t expires_at_; int64_t user_action_requests_count_; - int analysis_connector_; union ContentDataUnion { constexpr ContentDataUnion() : _constinit_{} {} ::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized _constinit_; @@ -1878,7 +1956,7 @@ class ContentAnalysisRequest final : }; union { Impl_ _impl_; }; - friend struct ::TableStruct_analysis_2eproto; + friend struct ::TableStruct_content_5fanalysis_2fsdk_2fanalysis_2eproto; }; // ------------------------------------------------------------------- @@ -2090,7 +2168,7 @@ class ContentAnalysisResponse_Result_TriggeredRule final : int action_; }; union { Impl_ _impl_; }; - friend struct ::TableStruct_analysis_2eproto; + friend struct ::TableStruct_content_5fanalysis_2fsdk_2fanalysis_2eproto; }; // ------------------------------------------------------------------- @@ -2302,7 +2380,7 @@ class ContentAnalysisResponse_Result final : int status_; }; union { Impl_ _impl_; }; - friend struct ::TableStruct_analysis_2eproto; + friend struct ::TableStruct_content_5fanalysis_2fsdk_2fanalysis_2eproto; }; // ------------------------------------------------------------------- @@ -2471,7 +2549,7 @@ class ContentAnalysisResponse final : ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr request_token_; }; union { Impl_ _impl_; }; - friend struct ::TableStruct_analysis_2eproto; + friend struct ::TableStruct_content_5fanalysis_2fsdk_2fanalysis_2eproto; }; // ------------------------------------------------------------------- @@ -2708,7 +2786,7 @@ class ContentAnalysisAcknowledgement final : int status_; }; union { Impl_ _impl_; }; - friend struct ::TableStruct_analysis_2eproto; + friend struct ::TableStruct_content_5fanalysis_2fsdk_2fanalysis_2eproto; }; // ------------------------------------------------------------------- @@ -2855,7 +2933,7 @@ class ContentAnalysisCancelRequests final : ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr user_action_id_; }; union { Impl_ _impl_; }; - friend struct ::TableStruct_analysis_2eproto; + friend struct ::TableStruct_content_5fanalysis_2fsdk_2fanalysis_2eproto; }; // ------------------------------------------------------------------- @@ -3042,7 +3120,7 @@ class ChromeToAgent final : ::content_analysis::sdk::ContentAnalysisCancelRequests* cancel_; }; union { Impl_ _impl_; }; - friend struct ::TableStruct_analysis_2eproto; + friend struct ::TableStruct_content_5fanalysis_2fsdk_2fanalysis_2eproto; }; // ------------------------------------------------------------------- @@ -3189,7 +3267,7 @@ class AgentToChrome final : ::content_analysis::sdk::ContentAnalysisResponse* response_; }; union { Impl_ _impl_; }; - friend struct ::TableStruct_analysis_2eproto; + friend struct ::TableStruct_content_5fanalysis_2fsdk_2fanalysis_2eproto; }; // =================================================================== @@ -4268,7 +4346,7 @@ inline void ContentAnalysisRequest::set_allocated_request_token(std::string* req // optional .content_analysis.sdk.AnalysisConnector analysis_connector = 9; inline bool ContentAnalysisRequest::_internal_has_analysis_connector() const { - bool value = (_impl_._has_bits_[0] & 0x00000040u) != 0; + bool value = (_impl_._has_bits_[0] & 0x00000010u) != 0; return value; } inline bool ContentAnalysisRequest::has_analysis_connector() const { @@ -4276,7 +4354,7 @@ inline bool ContentAnalysisRequest::has_analysis_connector() const { } inline void ContentAnalysisRequest::clear_analysis_connector() { _impl_.analysis_connector_ = 0; - _impl_._has_bits_[0] &= ~0x00000040u; + _impl_._has_bits_[0] &= ~0x00000010u; } inline ::content_analysis::sdk::AnalysisConnector ContentAnalysisRequest::_internal_analysis_connector() const { return static_cast< ::content_analysis::sdk::AnalysisConnector >(_impl_.analysis_connector_); @@ -4287,7 +4365,7 @@ inline ::content_analysis::sdk::AnalysisConnector ContentAnalysisRequest::analys } inline void ContentAnalysisRequest::_internal_set_analysis_connector(::content_analysis::sdk::AnalysisConnector value) { assert(::content_analysis::sdk::AnalysisConnector_IsValid(value)); - _impl_._has_bits_[0] |= 0x00000040u; + _impl_._has_bits_[0] |= 0x00000010u; _impl_.analysis_connector_ = value; } inline void ContentAnalysisRequest::set_analysis_connector(::content_analysis::sdk::AnalysisConnector value) { @@ -4780,7 +4858,7 @@ inline ::content_analysis::sdk::ContentAnalysisRequest_PrintData* ContentAnalysi // optional int64 expires_at = 15; inline bool ContentAnalysisRequest::_internal_has_expires_at() const { - bool value = (_impl_._has_bits_[0] & 0x00000010u) != 0; + bool value = (_impl_._has_bits_[0] & 0x00000040u) != 0; return value; } inline bool ContentAnalysisRequest::has_expires_at() const { @@ -4788,7 +4866,7 @@ inline bool ContentAnalysisRequest::has_expires_at() const { } inline void ContentAnalysisRequest::clear_expires_at() { _impl_.expires_at_ = int64_t{0}; - _impl_._has_bits_[0] &= ~0x00000010u; + _impl_._has_bits_[0] &= ~0x00000040u; } inline int64_t ContentAnalysisRequest::_internal_expires_at() const { return _impl_.expires_at_; @@ -4798,7 +4876,7 @@ inline int64_t ContentAnalysisRequest::expires_at() const { return _internal_expires_at(); } inline void ContentAnalysisRequest::_internal_set_expires_at(int64_t value) { - _impl_._has_bits_[0] |= 0x00000010u; + _impl_._has_bits_[0] |= 0x00000040u; _impl_.expires_at_ = value; } inline void ContentAnalysisRequest::set_expires_at(int64_t value) { @@ -4876,7 +4954,7 @@ inline void ContentAnalysisRequest::set_allocated_user_action_id(std::string* us // optional int64 user_action_requests_count = 17; inline bool ContentAnalysisRequest::_internal_has_user_action_requests_count() const { - bool value = (_impl_._has_bits_[0] & 0x00000020u) != 0; + bool value = (_impl_._has_bits_[0] & 0x00000080u) != 0; return value; } inline bool ContentAnalysisRequest::has_user_action_requests_count() const { @@ -4884,7 +4962,7 @@ inline bool ContentAnalysisRequest::has_user_action_requests_count() const { } inline void ContentAnalysisRequest::clear_user_action_requests_count() { _impl_.user_action_requests_count_ = int64_t{0}; - _impl_._has_bits_[0] &= ~0x00000020u; + _impl_._has_bits_[0] &= ~0x00000080u; } inline int64_t ContentAnalysisRequest::_internal_user_action_requests_count() const { return _impl_.user_action_requests_count_; @@ -4894,7 +4972,7 @@ inline int64_t ContentAnalysisRequest::user_action_requests_count() const { return _internal_user_action_requests_count(); } inline void ContentAnalysisRequest::_internal_set_user_action_requests_count(int64_t value) { - _impl_._has_bits_[0] |= 0x00000020u; + _impl_._has_bits_[0] |= 0x00000080u; _impl_.user_action_requests_count_ = value; } inline void ContentAnalysisRequest::set_user_action_requests_count(int64_t value) { @@ -4902,6 +4980,35 @@ inline void ContentAnalysisRequest::set_user_action_requests_count(int64_t value // @@protoc_insertion_point(field_set:content_analysis.sdk.ContentAnalysisRequest.user_action_requests_count) } +// optional .content_analysis.sdk.ContentAnalysisRequest.Reason reason = 19; +inline bool ContentAnalysisRequest::_internal_has_reason() const { + bool value = (_impl_._has_bits_[0] & 0x00000020u) != 0; + return value; +} +inline bool ContentAnalysisRequest::has_reason() const { + return _internal_has_reason(); +} +inline void ContentAnalysisRequest::clear_reason() { + _impl_.reason_ = 0; + _impl_._has_bits_[0] &= ~0x00000020u; +} +inline ::content_analysis::sdk::ContentAnalysisRequest_Reason ContentAnalysisRequest::_internal_reason() const { + return static_cast< ::content_analysis::sdk::ContentAnalysisRequest_Reason >(_impl_.reason_); +} +inline ::content_analysis::sdk::ContentAnalysisRequest_Reason ContentAnalysisRequest::reason() const { + // @@protoc_insertion_point(field_get:content_analysis.sdk.ContentAnalysisRequest.reason) + return _internal_reason(); +} +inline void ContentAnalysisRequest::_internal_set_reason(::content_analysis::sdk::ContentAnalysisRequest_Reason value) { + assert(::content_analysis::sdk::ContentAnalysisRequest_Reason_IsValid(value)); + _impl_._has_bits_[0] |= 0x00000020u; + _impl_.reason_ = value; +} +inline void ContentAnalysisRequest::set_reason(::content_analysis::sdk::ContentAnalysisRequest_Reason value) { + _internal_set_reason(value); + // @@protoc_insertion_point(field_set:content_analysis.sdk.ContentAnalysisRequest.reason) +} + inline bool ContentAnalysisRequest::has_content_data() const { return content_data_case() != CONTENT_DATA_NOT_SET; } @@ -5944,6 +6051,7 @@ PROTOBUF_NAMESPACE_OPEN template <> struct is_proto_enum< ::content_analysis::sdk::ContentMetaData_PrintMetadata_PrinterType> : ::std::true_type {}; template <> struct is_proto_enum< ::content_analysis::sdk::ClientDownloadRequest_ResourceType> : ::std::true_type {}; +template <> struct is_proto_enum< ::content_analysis::sdk::ContentAnalysisRequest_Reason> : ::std::true_type {}; template <> struct is_proto_enum< ::content_analysis::sdk::ContentAnalysisResponse_Result_TriggeredRule_Action> : ::std::true_type {}; template <> struct is_proto_enum< ::content_analysis::sdk::ContentAnalysisResponse_Result_Status> : ::std::true_type {}; template <> struct is_proto_enum< ::content_analysis::sdk::ContentAnalysisAcknowledgement_Status> : ::std::true_type {}; @@ -5955,4 +6063,4 @@ PROTOBUF_NAMESPACE_CLOSE // @@protoc_insertion_point(global_scope) #include <google/protobuf/port_undef.inc> -#endif // GOOGLE_PROTOBUF_INCLUDED_GOOGLE_PROTOBUF_INCLUDED_analysis_2eproto +#endif // GOOGLE_PROTOBUF_INCLUDED_GOOGLE_PROTOBUF_INCLUDED_content_5fanalysis_2fsdk_2fanalysis_2eproto diff --git a/toolkit/components/contentanalysis/nsIContentAnalysis.idl b/toolkit/components/contentanalysis/nsIContentAnalysis.idl index 21f98b88e6..63ac8d12fb 100644 --- a/toolkit/components/contentanalysis/nsIContentAnalysis.idl +++ b/toolkit/components/contentanalysis/nsIContentAnalysis.idl @@ -36,7 +36,7 @@ interface nsIContentAnalysisAcknowledgement : nsISupports readonly attribute nsIContentAnalysisAcknowledgement_FinalAction finalAction; }; -[scriptable, builtinclass, uuid(89088c61-15f6-4ace-a880-a1b5ea47ca66)] +[scriptable, uuid(89088c61-15f6-4ace-a880-a1b5ea47ca66)] interface nsIContentAnalysisResponse : nsISupports { // These values must stay synchronized with ContentAnalysisResponse @@ -52,8 +52,18 @@ interface nsIContentAnalysisResponse : nsISupports eCanceled = 1001, }; - [infallible] readonly attribute nsIContentAnalysisResponse_Action action; - [infallible] readonly attribute boolean shouldAllowContent; + cenum CancelError : 32 { + eUserInitiated = 0, + eNoAgent = 1, + eInvalidAgentSignature = 2, + eErrorOther = 3, + }; + + readonly attribute nsIContentAnalysisResponse_Action action; + readonly attribute boolean shouldAllowContent; + // If action is eCanceled, this is the error explaining why the request was canceled, + // or eUserInitiated if the user canceled it. + readonly attribute nsIContentAnalysisResponse_CancelError cancelError; // Identifier for the corresponding nsIContentAnalysisRequest readonly attribute ACString requestToken; @@ -121,6 +131,7 @@ interface nsIContentAnalysisRequest : nsISupports eCustomDisplayString = 0, eClipboard = 1, eDroppedText = 2, + eOperationPrint = 3, }; readonly attribute nsIContentAnalysisRequest_OperationType operationTypeForDisplay; readonly attribute AString operationDisplayString; @@ -131,6 +142,15 @@ interface nsIContentAnalysisRequest : nsISupports // Name of file to analyze. Only one of textContent or filePath is defined. readonly attribute AString filePath; + // HANDLE to the printed data in PDF format. + readonly attribute unsigned long long printDataHandle; + + // Size of the data stored in printDataHandle. + readonly attribute unsigned long long printDataSize; + + // Name of the printer being printed to. + readonly attribute AString printerName; + // The URL containing the file download/upload or to which web content is // being uploaded. readonly attribute nsIURI url; @@ -166,7 +186,16 @@ interface nsIContentAnalysisCallback : nsISupports void error(in nsresult aResult); }; -[scriptable, builtinclass, uuid(61497587-2bba-4a88-acd3-3fbb2cedf163)] +[scriptable, builtinclass, uuid(a430f6ef-a526-4055-8a82-7741ea757367)] +interface nsIContentAnalysisDiagnosticInfo : nsISupports +{ + [infallible] readonly attribute boolean connectedToAgent; + readonly attribute AString agentPath; + [infallible] readonly attribute boolean failedSignatureVerification; + [infallible] readonly attribute long long requestCount; +}; + +[scriptable, uuid(61497587-2bba-4a88-acd3-3fbb2cedf163)] interface nsIContentAnalysis : nsISupports { /** @@ -182,14 +211,14 @@ interface nsIContentAnalysis : nsISupports * into the parent process to determine whether content analysis is actually * active. */ - readonly attribute bool mightBeActive; + readonly attribute boolean mightBeActive; /** * True if content-analysis activation was determined by enterprise policy, * as opposed to enabled with the `allow-content-analysis` command-line * parameter. */ - attribute bool isSetByEnterprisePolicy; + attribute boolean isSetByEnterprisePolicy; /** * Consults content analysis server, if any, to request a permission @@ -210,7 +239,7 @@ interface nsIContentAnalysis : nsISupports * calling nsIContentAnalysisResponse::acknowledge() if the Promise is resolved. */ [implicit_jscontext] - Promise analyzeContentRequest(in nsIContentAnalysisRequest aCar, in bool aAutoAcknowledge); + Promise analyzeContentRequest(in nsIContentAnalysisRequest aCar, in boolean aAutoAcknowledge); /** * Same functionality as AnalyzeContentRequest(), but more convenient to call @@ -226,7 +255,7 @@ interface nsIContentAnalysis : nsISupports * @param callback * Callbacks to be called when the agent sends a response message (or when there is an error). */ - void analyzeContentRequestCallback(in nsIContentAnalysisRequest aCar, in bool aAutoAcknowledge, in nsIContentAnalysisCallback callback); + void analyzeContentRequestCallback(in nsIContentAnalysisRequest aCar, in boolean aAutoAcknowledge, in nsIContentAnalysisCallback callback); /** * Cancels the request that is in progress. This may not actually cancel the request @@ -242,7 +271,7 @@ interface nsIContentAnalysis : nsISupports * Indicates that the user has responded to a WARN dialog. aAllowContent represents * whether the user wants to allow the request to go through. */ - void respondToWarnDialog(in ACString aRequestToken, in bool aAllowContent); + void respondToWarnDialog(in ACString aRequestToken, in boolean aAllowContent); /** * Cancels all outstanding DLP requests. Used on shutdown. @@ -254,4 +283,11 @@ interface nsIContentAnalysis : nsISupports * given to Gecko on the command line. */ void testOnlySetCACmdLineArg(in boolean aVal); + + /** + * Gets diagnostic information about content analysis. Returns a + * nsIContentAnalysisDiagnosticInfo via the returned promise. + */ + [implicit_jscontext] + Promise getDiagnosticInfo(); }; diff --git a/toolkit/components/contentanalysis/tests/browser/browser.toml b/toolkit/components/contentanalysis/tests/browser/browser.toml index 0e21090299..bdbf350593 100644 --- a/toolkit/components/contentanalysis/tests/browser/browser.toml +++ b/toolkit/components/contentanalysis/tests/browser/browser.toml @@ -1,3 +1,20 @@ [DEFAULT] +run-if = ["os == 'win'"] +support-files = [ + "head.js", +] ["browser_content_analysis_policies.js"] + +["browser_print_changing_page_content_analysis.js"] +support-files = [ + "!/toolkit/components/printing/tests/head.js", + "changing_page_for_print.html", +] + +["browser_print_content_analysis.js"] +support-files = [ + "!/toolkit/components/printing/tests/head.js", + "!/toolkit/components/printing/tests/longerArticle.html", + "!/toolkit/components/printing/tests/simplifyArticleSample.html", +] diff --git a/toolkit/components/contentanalysis/tests/browser/browser_content_analysis_policies.js b/toolkit/components/contentanalysis/tests/browser/browser_content_analysis_policies.js index e2e001e9d1..b226c0a37a 100644 --- a/toolkit/components/contentanalysis/tests/browser/browser_content_analysis_policies.js +++ b/toolkit/components/contentanalysis/tests/browser/browser_content_analysis_policies.js @@ -11,15 +11,18 @@ "use strict"; -const { EnterprisePolicyTesting } = ChromeUtils.importESModule( - "resource://testing-common/EnterprisePolicyTesting.sys.mjs" -); +const { EnterprisePolicyTesting, PoliciesPrefTracker } = + ChromeUtils.importESModule( + "resource://testing-common/EnterprisePolicyTesting.sys.mjs" + ); const kEnabledPref = "enabled"; const kPipeNamePref = "pipe_path_name"; const kTimeoutPref = "agent_timeout"; const kAllowUrlPref = "allow_url_regex_list"; const kDenyUrlPref = "deny_url_regex_list"; +const kAgentNamePref = "agent_name"; +const kClientSignaturePref = "client_signature"; const kPerUserPref = "is_per_user"; const kShowBlockedPref = "show_blocked_result"; const kDefaultAllowPref = "default_allow"; @@ -29,6 +32,7 @@ const ca = Cc["@mozilla.org/contentanalysis;1"].getService( ); add_task(async function test_ca_active() { + PoliciesPrefTracker.start(); ok(!ca.isActive, "CA is inactive when pref and cmd line arg are missing"); // Set the pref without enterprise policy. CA should not be active. @@ -62,11 +66,15 @@ add_task(async function test_ca_active() { }, }); ok(ca.isActive, "CA is active when enabled by enterprise policy pref"); + PoliciesPrefTracker.stop(); }); add_task(async function test_ca_enterprise_config() { + PoliciesPrefTracker.start(); const string1 = "this is a string"; const string2 = "this is another string"; + const string3 = "an agent name"; + const string4 = "a client signature"; await EnterprisePolicyTesting.setupPolicyEngineWithJson({ policies: { @@ -75,6 +83,8 @@ add_task(async function test_ca_enterprise_config() { AgentTimeout: 99, AllowUrlRegexList: string1, DenyUrlRegexList: string2, + AgentName: string3, + ClientSignature: string4, IsPerUser: true, ShowBlockedResult: false, DefaultAllow: true, @@ -103,6 +113,18 @@ add_task(async function test_ca_enterprise_config() { "deny urls match" ); is( + Services.prefs.getStringPref("browser.contentanalysis." + kAgentNamePref), + string3, + "agent names match" + ); + is( + Services.prefs.getStringPref( + "browser.contentanalysis." + kClientSignaturePref + ), + string4, + "client signatures match" + ); + is( Services.prefs.getBoolPref("browser.contentanalysis." + kPerUserPref), true, "per user match" @@ -117,6 +139,7 @@ add_task(async function test_ca_enterprise_config() { true, "default allow match" ); + PoliciesPrefTracker.stop(); }); add_task(async function test_cleanup() { @@ -124,4 +147,9 @@ add_task(async function test_cleanup() { await EnterprisePolicyTesting.setupPolicyEngineWithJson({ policies: {}, }); + // These may have gotten set when ContentAnalysis was enabled through + // the policy and do not get cleared if there is no ContentAnalysis + // element - reset them manually here. + ca.isSetByEnterprisePolicy = false; + Services.prefs.setBoolPref("browser.contentanalysis." + kEnabledPref, false); }); diff --git a/toolkit/components/contentanalysis/tests/browser/browser_print_changing_page_content_analysis.js b/toolkit/components/contentanalysis/tests/browser/browser_print_changing_page_content_analysis.js new file mode 100644 index 0000000000..72a7dcbb91 --- /dev/null +++ b/toolkit/components/contentanalysis/tests/browser/browser_print_changing_page_content_analysis.js @@ -0,0 +1,339 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/printing/tests/head.js", + this +); + +const PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService( + Ci.nsIPrintSettingsService +); + +let mockCA = { + isActive: true, + mightBeActive: true, + errorValue: undefined, + + setupForTest(shouldAllowRequest) { + this.shouldAllowRequest = shouldAllowRequest; + this.errorValue = undefined; + this.calls = []; + }, + + setupForTestWithError(errorValue) { + this.errorValue = errorValue; + this.calls = []; + }, + + getAction() { + if (this.shouldAllowRequest === undefined) { + this.shouldAllowRequest = true; + } + return this.shouldAllowRequest + ? Ci.nsIContentAnalysisResponse.eAllow + : Ci.nsIContentAnalysisResponse.eBlock; + }, + + // nsIContentAnalysis methods + async analyzeContentRequest(request, _autoAcknowledge) { + info( + "Mock ContentAnalysis service: analyzeContentRequest, this.shouldAllowRequest=" + + this.shouldAllowRequest + + ", this.errorValue=" + + this.errorValue + ); + this.calls.push(request); + if (this.errorValue) { + throw this.errorValue; + } + // Use setTimeout to simulate an async activity + await new Promise(res => setTimeout(res, 0)); + return makeContentAnalysisResponse(this.getAction(), request.requestToken); + }, + + analyzeContentRequestCallback(request, autoAcknowledge, callback) { + info( + "Mock ContentAnalysis service: analyzeContentRequestCallback, this.shouldAllowRequest=" + + this.shouldAllowRequest + + ", this.errorValue=" + + this.errorValue + ); + this.calls.push(request); + if (this.errorValue) { + throw this.errorValue; + } + let response = makeContentAnalysisResponse( + this.getAction(), + request.requestToken + ); + // Use setTimeout to simulate an async activity + setTimeout(() => { + callback.contentResult(response); + }, 0); + }, +}; + +add_setup(async function test_setup() { + mockCA = mockContentAnalysisService(mockCA); +}); + +const TEST_PAGE_URL = PrintHelper.getTestPageUrlHTTPS( + "changing_page_for_print.html" +); + +function addUniqueSuffix(prefix) { + return `${prefix}-${Services.uuid + .generateUUID() + .toString() + .slice(1, -1)}.pdf`; +} + +async function printToDestination(aBrowser, aDestination) { + let tmpDir = Services.dirsvc.get("TmpD", Ci.nsIFile); + let fileName = addUniqueSuffix(`printDestinationTest-${aDestination}`); + let filePath = PathUtils.join(tmpDir.path, fileName); + + info(`Printing to ${filePath}`); + + let settings = PSSVC.createNewPrintSettings(); + settings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF; + settings.outputDestination = aDestination; + + settings.headerStrCenter = ""; + settings.headerStrLeft = ""; + settings.headerStrRight = ""; + settings.footerStrCenter = ""; + settings.footerStrLeft = ""; + settings.footerStrRight = ""; + + settings.unwriteableMarginTop = 1; /* Just to ensure settings are respected on both */ + let outStream = null; + if (aDestination == Ci.nsIPrintSettings.kOutputDestinationFile) { + settings.toFileName = PathUtils.join(tmpDir.path, fileName); + } else { + is(aDestination, Ci.nsIPrintSettings.kOutputDestinationStream); + outStream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( + Ci.nsIFileOutputStream + ); + let tmpFile = tmpDir.clone(); + tmpFile.append(fileName); + outStream.init(tmpFile, -1, 0o666, 0); + settings.outputStream = outStream; + } + + await aBrowser.browsingContext.print(settings); + + return filePath; +} + +function assertContentAnalysisRequest(request) { + is(request.url.spec, TEST_PAGE_URL, "request has correct URL"); + is( + request.analysisType, + Ci.nsIContentAnalysisRequest.ePrint, + "request has print analysisType" + ); + is( + request.operationTypeForDisplay, + Ci.nsIContentAnalysisRequest.eOperationPrint, + "request has print operationTypeForDisplay" + ); + is(request.textContent, "", "request textContent should be empty"); + is(request.filePath, "", "request filePath should be empty"); + isnot(request.printDataHandle, 0, "request printDataHandle should not be 0"); + isnot(request.printDataSize, 0, "request printDataSize should not be 0"); + ok(!!request.requestToken.length, "request requestToken should not be empty"); +} + +add_task( + async function testPrintToStreamWithContentAnalysisActiveAndAllowing() { + await PrintHelper.withTestPage( + async helper => { + mockCA.setupForTest(true); + + let filePath = await printToDestination( + helper.sourceBrowser, + Ci.nsIPrintSettings.kOutputDestinationFile + ); + is( + mockCA.calls.length, + 1, + "Correct number of calls to Content Analysis" + ); + assertContentAnalysisRequest(mockCA.calls[0]); + + // This effectively tests that the PDF content sent to Content Analysis + // and the content that is actually printed matches. This is necessary + // because a previous iteration of the Content Analysis code didn't use + // a static Document clone for this and so the content would differ. (since + // the .html file in question adds content to the page when print events + // happen) + await waitForFileToAlmostMatchSize( + filePath, + mockCA.calls[0].printDataSize + ); + + await IOUtils.remove(filePath); + }, + TEST_PAGE_URL, + true + ); + } +); + +add_task( + async function testPrintToStreamWithContentAnalysisActiveAndBlocking() { + await PrintHelper.withTestPage( + async helper => { + mockCA.setupForTest(false); + + try { + await printToDestination( + helper.sourceBrowser, + Ci.nsIPrintSettings.kOutputDestinationFile + ); + ok(false, "Content analysis should make this fail to print"); + } catch (e) { + ok( + /NS_ERROR_CONTENT_BLOCKED/.test(e.toString()), + "Got content blocked error" + ); + } + is( + mockCA.calls.length, + 1, + "Correct number of calls to Content Analysis" + ); + assertContentAnalysisRequest(mockCA.calls[0]); + }, + TEST_PAGE_URL, + true + ); + } +); + +add_task(async function testPrintToStreamWithContentAnalysisReturningError() { + await PrintHelper.withTestPage( + async helper => { + expectUncaughtException(); + mockCA.setupForTestWithError(Cr.NS_ERROR_NOT_AVAILABLE); + + try { + await printToDestination( + helper.sourceBrowser, + Ci.nsIPrintSettings.kOutputDestinationFile + ); + ok(false, "Content analysis should make this fail to print"); + } catch (e) { + ok( + /NS_ERROR_NOT_AVAILABLE/.test(e.toString()), + "Error in mock CA was propagated out" + ); + } + is(mockCA.calls.length, 1, "Correct number of calls to Content Analysis"); + assertContentAnalysisRequest(mockCA.calls[0]); + }, + TEST_PAGE_URL, + true + ); +}); + +add_task(async function testPrintThroughDialogWithContentAnalysisActive() { + await PrintHelper.withTestPage( + async helper => { + mockCA.setupForTest(true); + + let fileName = addUniqueSuffix(`printDialogTest`); + let file = helper.mockFilePicker(fileName); + info(`Printing to ${file.path}`); + await helper.startPrint(); + await helper.assertPrintToFile(file, () => { + EventUtils.sendKey("return", helper.win); + }); + + is(mockCA.calls.length, 1, "Correct number of calls to Content Analysis"); + assertContentAnalysisRequest(mockCA.calls[0]); + + await waitForFileToAlmostMatchSize( + file.path, + mockCA.calls[0].printDataSize + ); + }, + TEST_PAGE_URL, + true + ); +}); + +add_task( + async function testPrintThroughDialogWithContentAnalysisActiveAndBlocking() { + await PrintHelper.withTestPage( + async helper => { + mockCA.setupForTest(false); + + await helper.startPrint(); + let fileName = addUniqueSuffix(`printDialogTest`); + let file = helper.mockFilePicker(fileName); + info(`Printing to ${file.path}`); + try { + await helper.assertPrintToFile(file, () => { + EventUtils.sendKey("return", helper.win); + }); + } catch (e) { + ok( + /Wait for target file to get created/.test(e.toString()), + "Target file should not get created" + ); + } + ok(!file.exists(), "File should not exist"); + + is( + mockCA.calls.length, + 1, + "Correct number of calls to Content Analysis" + ); + assertContentAnalysisRequest(mockCA.calls[0]); + }, + TEST_PAGE_URL, + true + ); + } +); + +add_task( + async function testPrintThroughDialogWithContentAnalysisReturningError() { + await PrintHelper.withTestPage( + async helper => { + expectUncaughtException(); + mockCA.setupForTestWithError(Cr.NS_ERROR_NOT_AVAILABLE); + + await helper.startPrint(); + let fileName = addUniqueSuffix(`printDialogTest`); + let file = helper.mockFilePicker(fileName); + info(`Printing to ${file.path}`); + try { + await helper.assertPrintToFile(file, () => { + EventUtils.sendKey("return", helper.win); + }); + } catch (e) { + ok( + /Wait for target file to get created/.test(e.toString()), + "Target file should not get created" + ); + } + ok(!file.exists(), "File should not exist"); + + is( + mockCA.calls.length, + 1, + "Correct number of calls to Content Analysis" + ); + assertContentAnalysisRequest(mockCA.calls[0]); + }, + TEST_PAGE_URL, + true + ); + } +); diff --git a/toolkit/components/contentanalysis/tests/browser/browser_print_content_analysis.js b/toolkit/components/contentanalysis/tests/browser/browser_print_content_analysis.js new file mode 100644 index 0000000000..9b4c0ffa60 --- /dev/null +++ b/toolkit/components/contentanalysis/tests/browser/browser_print_content_analysis.js @@ -0,0 +1,390 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/printing/tests/head.js", + this +); + +const PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService( + Ci.nsIPrintSettingsService +); + +let mockCA = { + isActive: true, + mightBeActive: true, + errorValue: undefined, + + setupForTest(shouldAllowRequest) { + this.shouldAllowRequest = shouldAllowRequest; + this.errorValue = undefined; + this.calls = []; + }, + + setupForTestWithError(errorValue) { + this.errorValue = errorValue; + this.calls = []; + }, + + clearCalls() { + this.calls = []; + }, + + getAction() { + if (this.shouldAllowRequest === undefined) { + this.shouldAllowRequest = true; + } + return this.shouldAllowRequest + ? Ci.nsIContentAnalysisResponse.eAllow + : Ci.nsIContentAnalysisResponse.eBlock; + }, + + // nsIContentAnalysis methods + async analyzeContentRequest(request, _autoAcknowledge) { + info( + "Mock ContentAnalysis service: analyzeContentRequest, this.shouldAllowRequest=" + + this.shouldAllowRequest + + ", this.errorValue=" + + this.errorValue + ); + this.calls.push(request); + if (this.errorValue) { + throw this.errorValue; + } + // Use setTimeout to simulate an async activity + await new Promise(res => setTimeout(res, 0)); + return makeContentAnalysisResponse(this.getAction(), request.requestToken); + }, + + analyzeContentRequestCallback(request, autoAcknowledge, callback) { + info( + "Mock ContentAnalysis service: analyzeContentRequestCallback, this.shouldAllowRequest=" + + this.shouldAllowRequest + + ", this.errorValue=" + + this.errorValue + ); + this.calls.push(request); + if (this.errorValue) { + throw this.errorValue; + } + let response = makeContentAnalysisResponse( + this.getAction(), + request.requestToken + ); + // Use setTimeout to simulate an async activity + setTimeout(() => { + callback.contentResult(response); + }, 0); + }, +}; + +add_setup(async function test_setup() { + mockCA = mockContentAnalysisService(mockCA); +}); + +const TEST_PAGE_URL = + "https://example.com/browser/toolkit/components/printing/tests/simplifyArticleSample.html"; +const TEST_PAGE_URL_2 = + "https://example.com/browser/toolkit/components/printing/tests/longerArticle.html"; + +function addUniqueSuffix(prefix) { + return `${prefix}-${Services.uuid + .generateUUID() + .toString() + .slice(1, -1)}.pdf`; +} + +async function printToDestination(aBrowser, aDestination) { + let tmpDir = Services.dirsvc.get("TmpD", Ci.nsIFile); + let fileName = addUniqueSuffix(`printDestinationTest-${aDestination}`); + let filePath = PathUtils.join(tmpDir.path, fileName); + + info(`Printing to ${filePath}`); + + let settings = PSSVC.createNewPrintSettings(); + settings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF; + settings.outputDestination = aDestination; + + settings.headerStrCenter = ""; + settings.headerStrLeft = ""; + settings.headerStrRight = ""; + settings.footerStrCenter = ""; + settings.footerStrLeft = ""; + settings.footerStrRight = ""; + + settings.unwriteableMarginTop = 1; /* Just to ensure settings are respected on both */ + let outStream = null; + if (aDestination == Ci.nsIPrintSettings.kOutputDestinationFile) { + settings.toFileName = PathUtils.join(tmpDir.path, fileName); + } else { + is(aDestination, Ci.nsIPrintSettings.kOutputDestinationStream); + outStream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( + Ci.nsIFileOutputStream + ); + let tmpFile = tmpDir.clone(); + tmpFile.append(fileName); + outStream.init(tmpFile, -1, 0o666, 0); + settings.outputStream = outStream; + } + + await aBrowser.browsingContext.print(settings); + + return filePath; +} + +function assertContentAnalysisRequest(request, expectedUrl) { + is(request.url.spec, expectedUrl ?? TEST_PAGE_URL, "request has correct URL"); + is( + request.analysisType, + Ci.nsIContentAnalysisRequest.ePrint, + "request has print analysisType" + ); + is( + request.operationTypeForDisplay, + Ci.nsIContentAnalysisRequest.eOperationPrint, + "request has print operationTypeForDisplay" + ); + is(request.textContent, "", "request textContent should be empty"); + is(request.filePath, "", "request filePath should be empty"); + isnot(request.printDataHandle, 0, "request printDataHandle should not be 0"); + isnot(request.printDataSize, 0, "request printDataSize should not be 0"); + ok(!!request.requestToken.length, "request requestToken should not be empty"); +} + +// Printing to a stream is different than going through the print preview dialog because it +// doesn't make a static clone of the document before the print, which causes the +// Content Analysis code to go through a different code path. This is similar to what +// happens when various preferences are set to skip the print preview dialog, for example +// print.prefer_system_dialog. +add_task( + async function testPrintToStreamWithContentAnalysisActiveAndAllowing() { + await PrintHelper.withTestPage( + async helper => { + mockCA.setupForTest(true); + + let filePath = await printToDestination( + helper.sourceBrowser, + Ci.nsIPrintSettings.kOutputDestinationFile + ); + is( + mockCA.calls.length, + 1, + "Correct number of calls to Content Analysis" + ); + assertContentAnalysisRequest(mockCA.calls[0]); + + await waitForFileToAlmostMatchSize( + filePath, + mockCA.calls[0].printDataSize + ); + + await IOUtils.remove(filePath); + }, + TEST_PAGE_URL, + true + ); + } +); + +add_task( + async function testPrintToStreamAfterNavigationWithContentAnalysisActiveAndAllowing() { + await PrintHelper.withTestPage( + async helper => { + mockCA.setupForTest(true); + + let filePath = await printToDestination( + helper.sourceBrowser, + Ci.nsIPrintSettings.kOutputDestinationFile + ); + is( + mockCA.calls.length, + 1, + "Correct number of calls to Content Analysis" + ); + assertContentAnalysisRequest(mockCA.calls[0]); + mockCA.clearCalls(); + + await IOUtils.remove(filePath); + + BrowserTestUtils.startLoadingURIString( + helper.sourceBrowser, + TEST_PAGE_URL_2 + ); + await BrowserTestUtils.browserLoaded(helper.sourceBrowser); + + filePath = await printToDestination( + helper.sourceBrowser, + Ci.nsIPrintSettings.kOutputDestinationFile + ); + is( + mockCA.calls.length, + 1, + "Correct number of calls to Content Analysis" + ); + assertContentAnalysisRequest(mockCA.calls[0], TEST_PAGE_URL_2); + await waitForFileToAlmostMatchSize( + filePath, + mockCA.calls[0].printDataSize + ); + }, + TEST_PAGE_URL, + true + ); + } +); + +add_task( + async function testPrintToStreamWithContentAnalysisActiveAndBlocking() { + await PrintHelper.withTestPage( + async helper => { + mockCA.setupForTest(false); + + try { + await printToDestination( + helper.sourceBrowser, + Ci.nsIPrintSettings.kOutputDestinationFile + ); + ok(false, "Content analysis should make this fail to print"); + } catch (e) { + ok( + /NS_ERROR_CONTENT_BLOCKED/.test(e.toString()), + "Got content blocked error" + ); + } + is( + mockCA.calls.length, + 1, + "Correct number of calls to Content Analysis" + ); + assertContentAnalysisRequest(mockCA.calls[0]); + }, + TEST_PAGE_URL, + true + ); + } +); + +add_task(async function testPrintToStreamWithContentAnalysisReturningError() { + await PrintHelper.withTestPage( + async helper => { + expectUncaughtException(); + mockCA.setupForTestWithError(Cr.NS_ERROR_NOT_AVAILABLE); + + try { + await printToDestination( + helper.sourceBrowser, + Ci.nsIPrintSettings.kOutputDestinationFile + ); + ok(false, "Content analysis should make this fail to print"); + } catch (e) { + ok( + /NS_ERROR_NOT_AVAILABLE/.test(e.toString()), + "Error in mock CA was propagated out" + ); + } + is(mockCA.calls.length, 1, "Correct number of calls to Content Analysis"); + assertContentAnalysisRequest(mockCA.calls[0]); + }, + TEST_PAGE_URL, + true + ); +}); + +add_task(async function testPrintThroughDialogWithContentAnalysisActive() { + await PrintHelper.withTestPage( + async helper => { + mockCA.setupForTest(true); + + await helper.startPrint(); + let fileName = addUniqueSuffix(`printDialogTest`); + let file = helper.mockFilePicker(fileName); + info(`Printing to ${file.path}`); + await helper.assertPrintToFile(file, () => { + EventUtils.sendKey("return", helper.win); + }); + + is(mockCA.calls.length, 1, "Correct number of calls to Content Analysis"); + assertContentAnalysisRequest(mockCA.calls[0]); + + await waitForFileToAlmostMatchSize( + file.path, + mockCA.calls[0].printDataSize + ); + }, + TEST_PAGE_URL, + true + ); +}); + +add_task( + async function testPrintThroughDialogWithContentAnalysisActiveAndBlocking() { + await PrintHelper.withTestPage( + async helper => { + mockCA.setupForTest(false); + + await helper.startPrint(); + let fileName = addUniqueSuffix(`printDialogTest`); + let file = helper.mockFilePicker(fileName); + info(`Printing to ${file.path}`); + try { + await helper.assertPrintToFile(file, () => { + EventUtils.sendKey("return", helper.win); + }); + } catch (e) { + ok( + /Wait for target file to get created/.test(e.toString()), + "Target file should not get created" + ); + } + ok(!file.exists(), "File should not exist"); + + is( + mockCA.calls.length, + 1, + "Correct number of calls to Content Analysis" + ); + assertContentAnalysisRequest(mockCA.calls[0]); + }, + TEST_PAGE_URL, + true + ); + } +); + +add_task( + async function testPrintThroughDialogWithContentAnalysisReturningError() { + await PrintHelper.withTestPage( + async helper => { + expectUncaughtException(); + mockCA.setupForTestWithError(Cr.NS_ERROR_NOT_AVAILABLE); + + await helper.startPrint(); + let fileName = addUniqueSuffix(`printDialogTest`); + let file = helper.mockFilePicker(fileName); + info(`Printing to ${file.path}`); + try { + await helper.assertPrintToFile(file, () => { + EventUtils.sendKey("return", helper.win); + }); + } catch (e) { + ok( + /Wait for target file to get created/.test(e.toString()), + "Target file should not get created" + ); + } + ok(!file.exists(), "File should not exist"); + + is( + mockCA.calls.length, + 1, + "Correct number of calls to Content Analysis" + ); + assertContentAnalysisRequest(mockCA.calls[0]); + }, + TEST_PAGE_URL, + true + ); + } +); diff --git a/toolkit/components/contentanalysis/tests/browser/changing_page_for_print.html b/toolkit/components/contentanalysis/tests/browser/changing_page_for_print.html new file mode 100644 index 0000000000..de6f9001aa --- /dev/null +++ b/toolkit/components/contentanalysis/tests/browser/changing_page_for_print.html @@ -0,0 +1,12 @@ +<!doctype html> +<p>Some random text</p> +<button onclick="print()">Print the page</button> +<pre id="log"></pre> +<script> +let i = 0; +for (let t of ["beforeprint", "afterprint"]) { + addEventListener(t, () => { + document.getElementById("log").appendChild(document.createTextNode(`[${i++}] ${t}\n`)); + }); +} +</script> diff --git a/toolkit/components/contentanalysis/tests/browser/head.js b/toolkit/components/contentanalysis/tests/browser/head.js new file mode 100644 index 0000000000..e645caa2d7 --- /dev/null +++ b/toolkit/components/contentanalysis/tests/browser/head.js @@ -0,0 +1,114 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { MockRegistrar } = ChromeUtils.importESModule( + "resource://testing-common/MockRegistrar.sys.mjs" +); + +// Wraps the given object in an XPConnect wrapper and, if an interface +// is passed, queries the result to that interface. +function xpcWrap(obj, iface) { + let ifacePointer = Cc[ + "@mozilla.org/supports-interface-pointer;1" + ].createInstance(Ci.nsISupportsInterfacePointer); + + ifacePointer.data = obj; + if (iface) { + return ifacePointer.data.QueryInterface(iface); + } + return ifacePointer.data; +} + +/** + * Mock a (set of) service(s) as the object mockService. + * + * @param {[string]} serviceNames + * array of services names that mockService will be + * allowed to QI to. Must include the name of the + * service referenced by contractId. + * @param {string} contractId + * the component ID that will reference the mock object + * instead of the original service + * @param {object} interfaceObj + * interface object for the component + * @param {object} mockService + * object that satisfies the contract well + * enough to use as a mock of it + * @returns {object} The newly-mocked service + */ +function mockService(serviceNames, contractId, interfaceObj, mockService) { + // xpcWrap allows us to mock [implicit_jscontext] methods. + let newService = { + ...mockService, + QueryInterface: ChromeUtils.generateQI(serviceNames), + }; + let o = xpcWrap(newService, interfaceObj); + let cid = MockRegistrar.register(contractId, o); + registerCleanupFunction(() => { + MockRegistrar.unregister(cid); + }); + return newService; +} + +/** + * Mock the nsIContentAnalysis service with the object mockCAService. + * + * @param {object} mockCAService + * the service to mock for nsIContentAnalysis + * @returns {object} The newly-mocked service + */ +function mockContentAnalysisService(mockCAService) { + return mockService( + ["nsIContentAnalysis"], + "@mozilla.org/contentanalysis;1", + Ci.nsIContentAnalysis, + mockCAService + ); +} + +/** + * Make an nsIContentAnalysisResponse. + * + * @param {number} action The action to take, from the + * nsIContentAnalysisResponse.Action enum. + * @param {string} token The requestToken. + * @returns {object} An object that conforms to nsIContentAnalysisResponse. + */ +function makeContentAnalysisResponse(action, token) { + return { + action, + shouldAllowContent: action != Ci.nsIContentAnalysisResponse.eBlock, + requestToken: token, + acknowledge: _acknowledgement => {}, + }; +} + +async function waitForFileToAlmostMatchSize(filePath, expectedSize) { + // In Cocoa the CGContext adds a hash, plus there are other minor + // non-user-visible differences, so we need to be a bit more sloppy there. + // + // We see one byte difference in Windows and Linux on automation sometimes, + // though files are consistently the same locally, that needs + // investigation, but it's probably harmless. + // Note that this is copied from browser_print_stream.js. + const maxSizeDifference = AppConstants.platform == "macosx" ? 100 : 3; + + // Buffering shenanigans? Wait for sizes to match... There's no great + // IOUtils methods to force a flush without writing anything... + // Note that this means if this results in a timeout this is exactly + // the same as a test failure. + // This is taken from toolkit/components/printing/tests/browser_print_stream.js + await TestUtils.waitForCondition(async function () { + let fileStat = await IOUtils.stat(filePath); + + info("got size: " + fileStat.size + " expected: " + expectedSize); + Assert.greater( + fileStat.size, + 0, + "File should not be empty: " + fileStat.size + ); + return Math.abs(fileStat.size - expectedSize) <= maxSizeDifference; + }, "Sizes should (almost) match"); +} diff --git a/toolkit/components/contentprefs/ContentPrefService2.sys.mjs b/toolkit/components/contentprefs/ContentPrefService2.sys.mjs index 43fa0c4cb6..069229c9c5 100644 --- a/toolkit/components/contentprefs/ContentPrefService2.sys.mjs +++ b/toolkit/components/contentprefs/ContentPrefService2.sys.mjs @@ -39,7 +39,7 @@ export function ContentPrefService2() { } const cache = new ContentPrefStore(); -cache.set = function CPS_cache_set(group, name, val) { +cache.set = function CPS_cache_set() { Object.getPrototypeOf(this).set.apply(this, arguments); let groupCount = this._groups.size; if (groupCount >= CACHE_MAX_GROUP_ENTRIES) { @@ -211,7 +211,7 @@ ContentPrefService2.prototype = { cbHandleResult(callback, new ContentPref(grp, name, val)); } }, - onDone: (reason, ok, gotRow) => { + onDone: (reason, ok) => { if (ok) { for (let [pbGroup, pbName, pbVal] of pbPrefs) { cbHandleResult(callback, new ContentPref(pbGroup, pbName, pbVal)); @@ -1088,9 +1088,8 @@ ContentPrefService2.prototype = { * * @param subj This value depends on topic. * @param topic The backchannel "method" name. - * @param data This value depends on topic. */ - observe: function CPS2_observe(subj, topic, data) { + observe: function CPS2_observe(subj, topic) { switch (topic) { case "profile-before-change": this._destroy(); diff --git a/toolkit/components/contentprefs/ContentPrefServiceParent.sys.mjs b/toolkit/components/contentprefs/ContentPrefServiceParent.sys.mjs index 66b4a02741..13ef8e871c 100644 --- a/toolkit/components/contentprefs/ContentPrefServiceParent.sys.mjs +++ b/toolkit/components/contentprefs/ContentPrefServiceParent.sys.mjs @@ -92,7 +92,7 @@ export class ContentPrefsParent extends JSProcessActorParent { let actor = this; let args = data.args; - return new Promise(resolve => { + return new Promise(() => { let listener = { handleResult(pref) { actor.sendAsyncMessage("ContentPrefs:HandleResult", { diff --git a/toolkit/components/contentprefs/tests/browser/browser_remoteContentPrefs.js b/toolkit/components/contentprefs/tests/browser/browser_remoteContentPrefs.js index 00c845e488..0b9dd22f35 100644 --- a/toolkit/components/contentprefs/tests/browser/browser_remoteContentPrefs.js +++ b/toolkit/components/contentprefs/tests/browser/browser_remoteContentPrefs.js @@ -173,7 +173,7 @@ async function runTestsForFrame(browser, isPrivate) { onContentPrefSet(group, name, value, isPrivate) { resolve({ group, name, value, isPrivate }); }, - onContentPrefRemoved(group, name, isPrivate) { + onContentPrefRemoved() { reject("got unexpected notification"); }, }; @@ -202,7 +202,7 @@ async function runTestsForFrame(browser, isPrivate) { info("received handleResult"); results.push(pref); }, - handleCompletion(reason) { + handleCompletion() { resolve(); }, handleError(rv) { diff --git a/toolkit/components/contentprefs/tests/unit_cps2/head.js b/toolkit/components/contentprefs/tests/unit_cps2/head.js index c500d2bf01..bf5dcf2bc8 100644 --- a/toolkit/components/contentprefs/tests/unit_cps2/head.js +++ b/toolkit/components/contentprefs/tests/unit_cps2/head.js @@ -82,7 +82,7 @@ function setWithDate(group, name, val, timestamp, context) { }); } -async function getDate(group, name, context) { +async function getDate(group, name) { let conn = await sendMessage("db"); let [result] = await conn.execute( ` @@ -158,7 +158,7 @@ async function getGlobalOK(args, expectedVal) { await getOKEx("getGlobal", args, expectedPrefs); } -async function getOKEx(methodName, args, expectedPrefs, strict, context) { +async function getOKEx(methodName, args, expectedPrefs, strict) { let actualPrefs = []; await new Promise(resolve => { args.push( diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_migrationToSchema4.js b/toolkit/components/contentprefs/tests/unit_cps2/test_migrationToSchema4.js index 916fc55498..c5faf1a833 100644 --- a/toolkit/components/contentprefs/tests/unit_cps2/test_migrationToSchema4.js +++ b/toolkit/components/contentprefs/tests/unit_cps2/test_migrationToSchema4.js @@ -25,7 +25,7 @@ BEGIN TRANSACTION; CREATE INDEX prefs_idx ON prefs(groupID, settingID); COMMIT;`; -function prepareVersion3Schema(callback) { +function prepareVersion3Schema() { var dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile); dbFile.append("content-prefs.sqlite"); diff --git a/toolkit/components/contentrelevancy/ContentRelevancyManager.sys.mjs b/toolkit/components/contentrelevancy/ContentRelevancyManager.sys.mjs new file mode 100644 index 0000000000..ea3f2a78a2 --- /dev/null +++ b/toolkit/components/contentrelevancy/ContentRelevancyManager.sys.mjs @@ -0,0 +1,326 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + getFrecentRecentCombinedUrls: + "resource://gre/modules/contentrelevancy/private/InputUtils.sys.mjs", + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", + RelevancyStore: "resource://gre/modules/RustRelevancy.sys.mjs", +}); + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "timerManager", + "@mozilla.org/updates/timer-manager;1", + "nsIUpdateTimerManager" +); + +// Constants used by `nsIUpdateTimerManager` for a cross-session timer. +const TIMER_ID = "content-relevancy-timer"; +const PREF_TIMER_LAST_UPDATE = `app.update.lastUpdateTime.${TIMER_ID}`; +const PREF_TIMER_INTERVAL = "toolkit.contentRelevancy.timerInterval"; +// Set the timer interval to 1 day for validation. +const DEFAULT_TIMER_INTERVAL_SECONDS = 1 * 24 * 60 * 60; + +// Default maximum input URLs to fetch from Places. +const DEFAULT_MAX_URLS = 100; +// Default minimal input URLs for clasification. +const DEFAULT_MIN_URLS = 0; + +// File name of the relevancy database +const RELEVANCY_STORE_FILENAME = "content-relevancy.sqlite"; + +// Nimbus variables +const NIMBUS_VARIABLE_ENABLED = "enabled"; +const NIMBUS_VARIABLE_MAX_INPUT_URLS = "maxInputUrls"; +const NIMBUS_VARIABLE_MIN_INPUT_URLS = "minInputUrls"; +const NIMBUS_VARIABLE_TIMER_INTERVAL = "timerInterval"; + +ChromeUtils.defineLazyGetter(lazy, "log", () => { + return console.createInstance({ + prefix: "ContentRelevancyManager", + maxLogLevel: Services.prefs.getBoolPref( + "toolkit.contentRelevancy.log", + false + ) + ? "Debug" + : "Error", + }); +}); + +class RelevancyManager { + get initialized() { + return this.#initialized; + } + + /** + * Init the manager. An update timer is registered if the feature is enabled. + * The pref observer is always set so we can toggle the feature without restarting + * the browser. + * + * Note that this should be called once only. `#enable` and `#disable` can be + * used to toggle the feature once the manager is initialized. + */ + async init() { + if (this.initialized) { + return; + } + + lazy.log.info("Initializing the manager"); + + if (this.shouldEnable) { + await this.#enable(); + } + + this._nimbusUpdateCallback = this.#onNimbusUpdate.bind(this); + // This will handle both Nimbus updates and pref changes. + lazy.NimbusFeatures.contentRelevancy.onUpdate(this._nimbusUpdateCallback); + this.#initialized = true; + } + + uninit() { + if (!this.initialized) { + return; + } + + lazy.log.info("Uninitializing the manager"); + + lazy.NimbusFeatures.contentRelevancy.offUpdate(this._nimbusUpdateCallback); + this.#disable(); + + this.#initialized = false; + } + + /** + * Determine whether the feature should be enabled based on prefs and Nimbus. + */ + get shouldEnable() { + return ( + lazy.NimbusFeatures.contentRelevancy.getVariable( + NIMBUS_VARIABLE_ENABLED + ) ?? false + ); + } + + #startUpTimer() { + // Log the last timer tick for debugging. + const lastTick = Services.prefs.getIntPref(PREF_TIMER_LAST_UPDATE, 0); + if (lastTick) { + lazy.log.debug( + `Last timer tick: ${lastTick}s (${ + Math.round(Date.now() / 1000) - lastTick + })s ago` + ); + } else { + lazy.log.debug("Last timer tick: none"); + } + + const interval = + lazy.NimbusFeatures.contentRelevancy.getVariable( + NIMBUS_VARIABLE_TIMER_INTERVAL + ) ?? + Services.prefs.getIntPref( + PREF_TIMER_INTERVAL, + DEFAULT_TIMER_INTERVAL_SECONDS + ); + lazy.timerManager.registerTimer( + TIMER_ID, + this, + interval, + interval != 0 // Do not skip the first timer tick for a zero interval for testing + ); + } + + get #storePath() { + return PathUtils.join( + Services.dirsvc.get("ProfLD", Ci.nsIFile).path, + RELEVANCY_STORE_FILENAME + ); + } + + async #enable() { + if (!this.#_store) { + // Init the relevancy store. + const path = this.#storePath; + lazy.log.info(`Initializing RelevancyStore: ${path}`); + + try { + this.#_store = await lazy.RelevancyStore.init(path); + } catch (error) { + lazy.log.error(`Error initializing RelevancyStore: ${error}`); + return; + } + } + + this.#startUpTimer(); + } + + /** + * The reciprocal of `#enable()`, ensure this is safe to call when you add + * new disabling code here. It should be so even if `#enable()` hasn't been + * called. + */ + #disable() { + this.#_store = null; + lazy.timerManager.unregisterTimer(TIMER_ID); + } + + async #toggleFeature() { + if (this.shouldEnable) { + await this.#enable(); + } else { + this.#disable(); + } + } + + /** + * nsITimerCallback + */ + notify() { + lazy.log.info("Background job timer fired"); + this.#doClassification(); + } + + get isInProgress() { + return this.#isInProgress; + } + + /** + * Perform classification based on browsing history. + * + * It will fetch up to `DEFAULT_MAX_URLS` (or the corresponding Nimbus value) + * URLs from top frecent URLs and use most recent URLs as a fallback if the + * former is insufficient. The returned URLs might be fewer than requested. + * + * The classification will not be performed if the total number of input URLs + * is less than `DEFAULT_MIN_URLS` (or the corresponding Nimbus value). + */ + async #doClassification() { + if (this.isInProgress) { + lazy.log.info( + "Another classification is in progress, aborting interest classification" + ); + return; + } + + // Set a flag indicating this classification. Ensure it's cleared upon early + // exit points & success. + this.#isInProgress = true; + + try { + lazy.log.info("Fetching input data for interest classification"); + + const maxUrls = + lazy.NimbusFeatures.contentRelevancy.getVariable( + NIMBUS_VARIABLE_MAX_INPUT_URLS + ) ?? DEFAULT_MAX_URLS; + const minUrls = + lazy.NimbusFeatures.contentRelevancy.getVariable( + NIMBUS_VARIABLE_MIN_INPUT_URLS + ) ?? DEFAULT_MIN_URLS; + const urls = await lazy.getFrecentRecentCombinedUrls(maxUrls); + if (urls.length < minUrls) { + lazy.log.info("Aborting interest classification: insufficient input"); + return; + } + + lazy.log.info("Starting interest classification"); + await this.#doClassificationHelper(urls); + } catch (error) { + if (error instanceof StoreNotAvailableError) { + lazy.log.error("#store became null, aborting interest classification"); + } else { + lazy.log.error("Classification error: " + (error.reason ?? error)); + } + } finally { + this.#isInProgress = false; + } + + lazy.log.info("Finished interest classification"); + } + + /** + * Classification helper. Use the getter `this.#store` rather than `#_store` + * to access the store so that when it becomes null, a `StoreNotAvailableError` + * will be raised. Likewise, other store related errors should be propagated + * to the caller if you want to perform custom error handling in this helper. + * + * @param {Array} urls + * An array of URLs. + * @throws {StoreNotAvailableError} + * Thrown when the store became unavailable (i.e. set to null elsewhere). + * @throws {RelevancyAPIError} + * Thrown for other API errors on the store. + */ + async #doClassificationHelper(urls) { + // The following logs are unnecessary, only used to suppress the linting error. + // TODO(nanj): delete me once the following TODO is done. + if (!this.#store) { + lazy.log.error("#store became null, aborting interest classification"); + } + lazy.log.info("Classification input: " + urls); + + // TODO(nanj): uncomment the following once `ingest()` is implemented. + // await this.#store.ingest(urls); + } + + /** + * Exposed for testing. + */ + async _test_doClassification(urls) { + await this.#doClassificationHelper(urls); + } + + /** + * Internal getter for `#_store` used by for classification. It will throw + * a `StoreNotAvailableError` is the store is not ready. + */ + get #store() { + if (!this._isStoreReady) { + throw new StoreNotAvailableError("Store is not available"); + } + + return this.#_store; + } + + /** + * Whether or not the store is ready (i.e. not null). + */ + get _isStoreReady() { + return !!this.#_store; + } + + /** + * Nimbus update listener. + */ + #onNimbusUpdate(_event, _reason) { + this.#toggleFeature(); + } + + // The `RustRelevancy` store. + #_store; + + // Whether or not the module is initialized. + #initialized = false; + + // Whether or not there is an in-progress classification. Used to prevent + // duplicate classification tasks. + #isInProgress = false; +} + +/** + * Error raised when attempting to access a null store. + */ +class StoreNotAvailableError extends Error { + constructor(message, ...params) { + super(message, ...params); + this.name = "StoreNotAvailableError"; + } +} + +export var ContentRelevancyManager = new RelevancyManager(); diff --git a/toolkit/components/contentrelevancy/docs/index.md b/toolkit/components/contentrelevancy/docs/index.md new file mode 100644 index 0000000000..bd377d68dc --- /dev/null +++ b/toolkit/components/contentrelevancy/docs/index.md @@ -0,0 +1,3 @@ +# Content Relevancy + +This is the home for the project: Interest-based Content Relevance Ranking & Personalization for Firefox, a client-based privacy preserving approach to enhancing content experience of Firefox. diff --git a/toolkit/components/contentrelevancy/moz.build b/toolkit/components/contentrelevancy/moz.build new file mode 100644 index 0000000000..551af69924 --- /dev/null +++ b/toolkit/components/contentrelevancy/moz.build @@ -0,0 +1,27 @@ +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Application Services", "Relevancy") + +EXTRA_JS_MODULES += [ + "ContentRelevancyManager.sys.mjs", +] + +EXTRA_JS_MODULES["contentrelevancy/private"] += [ + "private/InputUtils.sys.mjs", +] + +XPCSHELL_TESTS_MANIFESTS += [ + "tests/xpcshell/xpcshell.toml", +] + +BROWSER_CHROME_MANIFESTS += [ + "tests/browser/browser.toml", +] + +SPHINX_TREES["/toolkit/components/contentrelevancy"] = "docs" + +with Files("docs/**"): + SCHEDULES.exclusive = ["docs"] diff --git a/toolkit/components/contentrelevancy/private/InputUtils.sys.mjs b/toolkit/components/contentrelevancy/private/InputUtils.sys.mjs new file mode 100644 index 0000000000..eccc5a5768 --- /dev/null +++ b/toolkit/components/contentrelevancy/private/InputUtils.sys.mjs @@ -0,0 +1,118 @@ +/* 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 lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", +}); + +/** + * Get the URLs with the top frecency scores. + * + * Note: + * - Blocked URLs are excluded + * - Allow multiple URLs from the same domain (www vs non-www urls) + * + * @param {number} maxUrls + * The maximum number of URLs to fetch. + * @param {number} frecencyThreshold + * The minimal frecency score of the URL. Will use the default set by + * the upstream module if unspecified. For reference, "100" means one + * visit in the past 3 days. see more details at: + * `/browser/components/urlbar/docs/ranking.rst` + * @returns {Array} + * An array of URLs. Note that the actual number could be less than `maxUrls`. + */ +export async function getTopFrecentUrls( + maxUrls, + frecencyThreshold /* for test */ +) { + const options = { + ignoreBlocked: true, + onePerDomain: false, + includeFavicon: false, + topsiteFrecency: frecencyThreshold, + numItems: maxUrls, + }; + const records = await lazy.NewTabUtils.activityStreamLinks.getTopSites( + options + ); + + return records.map(site => site.url); +} + +/** + * Get the URLs of the most recent browsing history. + * + * Note: + * - Blocked URLs are excluded + * - Recent bookmarks are excluded + * - Recent "Save-to-Pocket" URLs are excluded + * - It would only return URLs if the page meta data is present. We can relax + * this in the future. + * - Multiple URLs may be returned for the same domain + * + * @param {number} maxUrls + * The maximum number of URLs to fetch. + * @returns {Array} + * An array of URLs. Note that the actual number could be less than `maxUrls`. + */ +export async function getMostRecentUrls(maxUrls) { + const options = { + ignoreBlocked: true, + excludeBookmarks: true, + excludeHistory: false, + excludePocket: true, + withFavicons: false, + numItems: maxUrls, + }; + const records = await lazy.NewTabUtils.activityStreamLinks.getHighlights( + options + ); + + return records.map(site => site.url); +} + +/** + * Get the URLs as a combination of the top frecent and the most recent + * browsing history. + * + * It will fetch `maxUrls` URLs from top frecent URLs and use most recent URLs + * as a fallback if the former is insufficient. Duplicates will be removed + * As a result, the returned URLs might be fewer than requested. + * + * @param {number} maxUrls + * The maximum number of URLs to fetch. + * @returns {Array} + * An array of URLs. + */ +export async function getFrecentRecentCombinedUrls(maxUrls) { + let urls = await getTopFrecentUrls(maxUrls); + if (urls.length < maxUrls) { + const n = Math.round((maxUrls - urls.length) * 1.2); // Over-fetch for deduping + const recentUrls = await getMostRecentUrls(n); + urls = dedupUrls(urls, recentUrls).slice(0, maxUrls); + } + + return urls; +} + +/** + * A helper to deduplicate items from any number of grouped URLs. + * + * Note: + * - Currently, all the elements (URLs) of the input arrays are treated as keys. + * - It doesn't assume the uniqueness within the group, therefore, in-group + * duplicates will be deduped as well. + * + * @param {Array} groups + * Contains an arbitrary number of arrays of URLs. + * @returns {Array} + * An array of unique URLs from the input groups. + */ +function dedupUrls(...groups) { + const uniques = new Set(groups.flat()); + return [...uniques]; +} diff --git a/toolkit/components/contentrelevancy/tests/browser/browser.toml b/toolkit/components/contentrelevancy/tests/browser/browser.toml new file mode 100644 index 0000000000..ec1d3a3e66 --- /dev/null +++ b/toolkit/components/contentrelevancy/tests/browser/browser.toml @@ -0,0 +1,6 @@ +[DEFAULT] +prefs = [ + "toolkit.contentRelevancy.enabled=false", +] + +["browser_contentrelevancy_nimbus.js"] diff --git a/toolkit/components/contentrelevancy/tests/browser/browser_contentrelevancy_nimbus.js b/toolkit/components/contentrelevancy/tests/browser/browser_contentrelevancy_nimbus.js new file mode 100644 index 0000000000..47d54c2a87 --- /dev/null +++ b/toolkit/components/contentrelevancy/tests/browser/browser_contentrelevancy_nimbus.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { ExperimentAPI } = ChromeUtils.importESModule( + "resource://nimbus/ExperimentAPI.sys.mjs" +); +const { ExperimentFakes } = ChromeUtils.importESModule( + "resource://testing-common/NimbusTestUtils.sys.mjs" +); +const { ContentRelevancyManager } = ChromeUtils.importESModule( + "resource://gre/modules/ContentRelevancyManager.sys.mjs" +); +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +let gSandbox; + +add_setup(() => { + gSandbox = sinon.createSandbox(); + + registerCleanupFunction(() => { + gSandbox.restore(); + }); +}); + +/** + * Test Nimbus integration - enable. + */ +add_task(async function test_NimbusIntegration_enable() { + gSandbox.spy(ContentRelevancyManager, "notify"); + + await ExperimentAPI.ready(); + const doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({ + featureId: "contentRelevancy", + value: { + enabled: true, + minInputUrls: 1, + maxInputUrls: 3, + // Set the timer interval to 0 will trigger the timer right away. + timerInterval: 0, + }, + }); + + await TestUtils.waitForCondition( + () => ContentRelevancyManager.shouldEnable, + "Should enable it via Nimbus" + ); + + await TestUtils.waitForCondition( + () => ContentRelevancyManager.notify.called, + "The timer callback should be called" + ); + + await doExperimentCleanup(); + gSandbox.restore(); +}); + +/** + * Test Nimbus integration - disable. + */ +add_task(async function test_NimbusIntegration_disable() { + gSandbox.spy(ContentRelevancyManager, "notify"); + + await ExperimentAPI.ready(); + const doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({ + featureId: "contentRelevancy", + value: { + enabled: false, + minInputUrls: 1, + maxInputUrls: 3, + // Set the timer interval to 0 will trigger the timer right away. + timerInterval: 0, + }, + }); + + await TestUtils.waitForCondition( + () => !ContentRelevancyManager.shouldEnable, + "Should disable it via Nimbus" + ); + + await TestUtils.waitForCondition( + () => ContentRelevancyManager.notify.notCalled, + "The timer callback should not be called" + ); + + await doExperimentCleanup(); + gSandbox.restore(); +}); diff --git a/toolkit/components/contentrelevancy/tests/xpcshell/head.js b/toolkit/components/contentrelevancy/tests/xpcshell/head.js new file mode 100644 index 0000000000..e8c31f589c --- /dev/null +++ b/toolkit/components/contentrelevancy/tests/xpcshell/head.js @@ -0,0 +1,6 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This requires the profile directory for Places and the content relevancy +// component needs a profile directory for storage. +do_get_profile(); diff --git a/toolkit/components/contentrelevancy/tests/xpcshell/test_ContentRelevancyManager.js b/toolkit/components/contentrelevancy/tests/xpcshell/test_ContentRelevancyManager.js new file mode 100644 index 0000000000..633f9fc49b --- /dev/null +++ b/toolkit/components/contentrelevancy/tests/xpcshell/test_ContentRelevancyManager.js @@ -0,0 +1,149 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + ContentRelevancyManager: + "resource://gre/modules/ContentRelevancyManager.sys.mjs", + TestUtils: "resource://testing-common/TestUtils.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", + sinon: "resource://testing-common/Sinon.sys.mjs", +}); + +const PREF_CONTENT_RELEVANCY_ENABLED = "toolkit.contentRelevancy.enabled"; +const PREF_TIMER_INTERVAL = "toolkit.contentRelevancy.timerInterval"; + +// These consts are copied from the update timer manager test. See +// `initUpdateTimerManager()`. +const PREF_APP_UPDATE_TIMERMINIMUMDELAY = "app.update.timerMinimumDelay"; +const PREF_APP_UPDATE_TIMERFIRSTINTERVAL = "app.update.timerFirstInterval"; +const MAIN_TIMER_INTERVAL = 1000; // milliseconds +const CATEGORY_UPDATE_TIMER = "update-timer"; + +let gSandbox; + +add_setup(async () => { + gSandbox = sinon.createSandbox(); + initUpdateTimerManager(); + Services.prefs.setBoolPref(PREF_CONTENT_RELEVANCY_ENABLED, true); + await ContentRelevancyManager.init(); + + registerCleanupFunction(() => { + Services.prefs.clearUserPref(PREF_CONTENT_RELEVANCY_ENABLED); + gSandbox.restore(); + }); +}); + +add_task(async function test_init() { + Assert.ok(ContentRelevancyManager.initialized, "Init should succeed"); +}); + +add_task(async function test_uninit() { + ContentRelevancyManager.uninit(); + + Assert.ok(!ContentRelevancyManager.initialized, "Uninit should succeed"); +}); + +add_task(async function test_timer() { + // Set the timer interval to 0 will trigger the timer right away. + Services.prefs.setIntPref(PREF_TIMER_INTERVAL, 0); + gSandbox.spy(ContentRelevancyManager, "notify"); + + await ContentRelevancyManager.init(); + + await TestUtils.waitForCondition( + () => ContentRelevancyManager.notify.called, + "The timer callback should be called" + ); + + Services.prefs.clearUserPref(PREF_TIMER_INTERVAL); + gSandbox.restore(); +}); + +add_task(async function test_feature_toggling() { + Services.prefs.setBoolPref(PREF_CONTENT_RELEVANCY_ENABLED, false); + // Set the timer interval to 0 will trigger the timer right away. + Services.prefs.setIntPref(PREF_TIMER_INTERVAL, 0); + gSandbox.spy(ContentRelevancyManager, "notify"); + + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1100)); + Assert.ok( + ContentRelevancyManager.notify.notCalled, + "Timer should not be registered if disabled" + ); + + // Toggle the pref again should re-enable the feature. + Services.prefs.setBoolPref(PREF_CONTENT_RELEVANCY_ENABLED, true); + await TestUtils.waitForTick(); + + await TestUtils.waitForCondition( + () => ContentRelevancyManager.notify.called, + "The timer callback should be called" + ); + + Services.prefs.clearUserPref(PREF_CONTENT_RELEVANCY_ENABLED); + Services.prefs.clearUserPref(PREF_TIMER_INTERVAL); + gSandbox.restore(); +}); + +add_task(async function test_call_disable_twice() { + Services.prefs.setBoolPref(PREF_CONTENT_RELEVANCY_ENABLED, false); + await TestUtils.waitForTick(); + + Services.prefs.setBoolPref(PREF_CONTENT_RELEVANCY_ENABLED, false); + await TestUtils.waitForTick(); + + Assert.ok(true, "`#disable` should be safe to call multiple times"); + + Services.prefs.clearUserPref(PREF_CONTENT_RELEVANCY_ENABLED); +}); + +add_task(async function test_doClassification() { + Services.prefs.setBoolPref(PREF_CONTENT_RELEVANCY_ENABLED, true); + await TestUtils.waitForCondition(() => ContentRelevancyManager._isStoreReady); + await ContentRelevancyManager._test_doClassification([]); + + // Disable it to reset the store. + Services.prefs.setBoolPref(PREF_CONTENT_RELEVANCY_ENABLED, false); + await TestUtils.waitForTick(); + + await Assert.rejects( + ContentRelevancyManager._test_doClassification([]), + /Store is not available/, + "Should throw with an unset store" + ); + + Services.prefs.clearUserPref(PREF_CONTENT_RELEVANCY_ENABLED); +}); + +/** + * Sets up the update timer manager for testing: makes it fire more often, + * removes all existing timers, and initializes it for testing. The body of this + * function is copied from: + * https://searchfox.org/mozilla-central/source/toolkit/components/timermanager/tests/unit/consumerNotifications.js + */ +function initUpdateTimerManager() { + // Set the timer to fire every second + Services.prefs.setIntPref( + PREF_APP_UPDATE_TIMERMINIMUMDELAY, + MAIN_TIMER_INTERVAL / 1000 + ); + Services.prefs.setIntPref( + PREF_APP_UPDATE_TIMERFIRSTINTERVAL, + MAIN_TIMER_INTERVAL + ); + + // Remove existing update timers to prevent them from being notified + for (let { data: entry } of Services.catMan.enumerateCategory( + CATEGORY_UPDATE_TIMER + )) { + Services.catMan.deleteCategoryEntry(CATEGORY_UPDATE_TIMER, entry, false); + } + + Cc["@mozilla.org/updates/timer-manager;1"] + .getService(Ci.nsIUpdateTimerManager) + .QueryInterface(Ci.nsIObserver) + .observe(null, "utm-test-init", ""); +} diff --git a/toolkit/components/contentrelevancy/tests/xpcshell/test_InputUtils.js b/toolkit/components/contentrelevancy/tests/xpcshell/test_InputUtils.js new file mode 100644 index 0000000000..2bb2b8e62e --- /dev/null +++ b/toolkit/components/contentrelevancy/tests/xpcshell/test_InputUtils.js @@ -0,0 +1,98 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + getFrecentRecentCombinedUrls: + "resource://gre/modules/contentrelevancy/private/InputUtils.sys.mjs", + getMostRecentUrls: + "resource://gre/modules/contentrelevancy/private/InputUtils.sys.mjs", + getTopFrecentUrls: + "resource://gre/modules/contentrelevancy/private/InputUtils.sys.mjs", + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", + PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs", +}); + +const FRECENCY_SCORE_FOR_ONE_VISIT = 100; +const TEST_VISITS = [ + "http://test-1.com/", + "http://test-2.com/", + "http://test-3.com/", + "http://test-4.com/", +]; + +add_task(async function test_GetTopFrecentUrls() { + await PlacesUtils.history.clear(); + let urls = new Set(await getTopFrecentUrls(3, FRECENCY_SCORE_FOR_ONE_VISIT)); + + Assert.strictEqual(urls.size, 0, "Should have no top frecent links."); + + await PlacesTestUtils.addVisits(TEST_VISITS); + urls = new Set(await getTopFrecentUrls(3, FRECENCY_SCORE_FOR_ONE_VISIT)); + + Assert.strictEqual(urls.size, 3, "Should fetch the expected links"); + urls.forEach(url => { + Assert.ok(TEST_VISITS.includes(url), "Should be a link of the test visits"); + }); +}); + +add_task(async function test_GetMostRecentUrls() { + await PlacesUtils.history.clear(); + let urls = new Set(await getMostRecentUrls(3)); + + Assert.strictEqual(urls.size, 0, "Should have no recent links."); + + // Add visits and page meta data. + await PlacesTestUtils.addVisits(TEST_VISITS); + for (let url of TEST_VISITS) { + await PlacesUtils.history.update({ + description: "desc", + previewImageURL: "https://image/", + url, + }); + } + + urls = new Set(await getMostRecentUrls(3)); + + Assert.strictEqual(urls.size, 3, "Should fetch the expected links"); + urls.forEach(url => { + Assert.ok(TEST_VISITS.includes(url), "Should be a link of the test visits"); + }); +}); + +add_task(async function test_GetFrecentRecentCombinedUrls() { + await PlacesUtils.history.clear(); + let urls = new Set(await getFrecentRecentCombinedUrls(3)); + + Assert.strictEqual(urls.size, 0, "Should have no links."); + + // Add visits and page meta data. + await PlacesTestUtils.addVisits(TEST_VISITS); + for (let url of TEST_VISITS) { + await PlacesUtils.history.update({ + description: "desc", + previewImageURL: "https://image/", + url, + }); + } + + urls = new Set(await getFrecentRecentCombinedUrls(3)); + + Assert.strictEqual(urls.size, 3, "Should fetch the expected links"); + urls.forEach(url => { + Assert.ok(TEST_VISITS.includes(url), "Should be a link of the test visits"); + }); + + // Try getting twice as many URLs as the total in Places. + urls = new Set(await getFrecentRecentCombinedUrls(TEST_VISITS.length * 2)); + + Assert.strictEqual( + urls.size, + TEST_VISITS.length, + "Should not include duplicates" + ); + urls.forEach(url => { + Assert.ok(TEST_VISITS.includes(url), "Should be a link of the test visits"); + }); +}); diff --git a/toolkit/components/contentrelevancy/tests/xpcshell/xpcshell.toml b/toolkit/components/contentrelevancy/tests/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..70f6d45c2d --- /dev/null +++ b/toolkit/components/contentrelevancy/tests/xpcshell/xpcshell.toml @@ -0,0 +1,9 @@ +[DEFAULT] +head = "head.js" +firefox-appdir = "browser" + +["test_ContentRelevancyManager.js"] +skip-if = ["os == 'android'"] # bug 1886601 + +["test_InputUtils.js"] +skip-if = ["os == 'android'"] # bug 1886601 diff --git a/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs b/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs index 738f8743ee..32f3577c93 100644 --- a/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs +++ b/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs @@ -41,7 +41,7 @@ _TabRemovalObserver.prototype = { QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), - observe(subject, topic, data) { + observe(subject) { let remoteTab = subject.QueryInterface(Ci.nsIRemoteTab); if (this._remoteTabIds.has(remoteTab.tabId)) { this._remoteTabIds.delete(remoteTab.tabId); diff --git a/toolkit/components/cookiebanners/CookieBannerChild.sys.mjs b/toolkit/components/cookiebanners/CookieBannerChild.sys.mjs index 4d8b481504..b56303d95b 100644 --- a/toolkit/components/cookiebanners/CookieBannerChild.sys.mjs +++ b/toolkit/components/cookiebanners/CookieBannerChild.sys.mjs @@ -488,18 +488,18 @@ export class CookieBannerChild extends JSWindowActorChild { let querySelectorTimeUS = Math.round(querySelectorTimeMS * 1000); if (this.#isTopLevel) { - Glean.cookieBannersClick.querySelectorRunCountPerWindowTopLevel.accumulateSamples( - [querySelectorCount] + Glean.cookieBannersClick.querySelectorRunCountPerWindowTopLevel.accumulateSingleSample( + querySelectorCount ); - Glean.cookieBannersClick.querySelectorRunDurationPerWindowTopLevel.accumulateSamples( - [querySelectorTimeUS] + Glean.cookieBannersClick.querySelectorRunDurationPerWindowTopLevel.accumulateSingleSample( + querySelectorTimeUS ); } else { - Glean.cookieBannersClick.querySelectorRunCountPerWindowFrame.accumulateSamples( - [querySelectorCount] + Glean.cookieBannersClick.querySelectorRunCountPerWindowFrame.accumulateSingleSample( + querySelectorCount ); - Glean.cookieBannersClick.querySelectorRunDurationPerWindowFrame.accumulateSamples( - [querySelectorTimeUS] + Glean.cookieBannersClick.querySelectorRunDurationPerWindowFrame.accumulateSingleSample( + querySelectorTimeUS ); } diff --git a/toolkit/components/cookiebanners/components.conf b/toolkit/components/cookiebanners/components.conf index adf85f8d2d..744fb75510 100644 --- a/toolkit/components/cookiebanners/components.conf +++ b/toolkit/components/cookiebanners/components.conf @@ -36,4 +36,17 @@ Classes = [ 'constructor': 'CookieBannerListService', 'processes': ProcessSelector.MAIN_PROCESS_ONLY, }, + { + 'cid': '{56197e18-d144-45b5-9f77-84102f064462}', + 'interfaces': ['nsICookieBannerTelemetryService'], + 'headers': ['/toolkit/components/cookiebanners/nsCookieBannerTelemetryService.h'], + 'contract_ids': ['@mozilla.org/cookie-banner-telemetry-service;1'], + 'type': 'mozilla::nsCookieBannerTelemetryService', + 'singleton': True, + 'constructor': 'mozilla::nsCookieBannerTelemetryService::GetSingleton', + 'categories': { + 'profile-after-change': 'nsCookieBannerTelemetryService', + }, + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, ] diff --git a/toolkit/components/cookiebanners/cookieBanner.pb.cc b/toolkit/components/cookiebanners/cookieBanner.pb.cc new file mode 100644 index 0000000000..e415588ce0 --- /dev/null +++ b/toolkit/components/cookiebanners/cookieBanner.pb.cc @@ -0,0 +1,983 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: cookieBanner.proto + +#include "cookieBanner.pb.h" + +#include <algorithm> + +#include <google/protobuf/io/coded_stream.h> +#include <google/protobuf/extension_set.h> +#include <google/protobuf/wire_format_lite.h> +#include <google/protobuf/io/zero_copy_stream_impl_lite.h> +// @@protoc_insertion_point(includes) +#include <google/protobuf/port_def.inc> + +PROTOBUF_PRAGMA_INIT_SEG + +namespace _pb = ::PROTOBUF_NAMESPACE_ID; +namespace _pbi = _pb::internal; + +namespace mozilla { +namespace cookieBanner { +PROTOBUF_CONSTEXPR GoogleSOCSCookie_extraData::GoogleSOCSCookie_extraData( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.platform_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.region_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.unused1_)*/0u + , /*decltype(_impl_.unused2_)*/0u} {} +struct GoogleSOCSCookie_extraDataDefaultTypeInternal { + PROTOBUF_CONSTEXPR GoogleSOCSCookie_extraDataDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~GoogleSOCSCookie_extraDataDefaultTypeInternal() {} + union { + GoogleSOCSCookie_extraData _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 GoogleSOCSCookie_extraDataDefaultTypeInternal _GoogleSOCSCookie_extraData_default_instance_; +PROTOBUF_CONSTEXPR GoogleSOCSCookie_timeData::GoogleSOCSCookie_timeData( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.timestamp_)*/uint64_t{0u}} {} +struct GoogleSOCSCookie_timeDataDefaultTypeInternal { + PROTOBUF_CONSTEXPR GoogleSOCSCookie_timeDataDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~GoogleSOCSCookie_timeDataDefaultTypeInternal() {} + union { + GoogleSOCSCookie_timeData _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 GoogleSOCSCookie_timeDataDefaultTypeInternal _GoogleSOCSCookie_timeData_default_instance_; +PROTOBUF_CONSTEXPR GoogleSOCSCookie::GoogleSOCSCookie( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.data_)*/nullptr + , /*decltype(_impl_.time_)*/nullptr + , /*decltype(_impl_.gdpr_choice_)*/0u} {} +struct GoogleSOCSCookieDefaultTypeInternal { + PROTOBUF_CONSTEXPR GoogleSOCSCookieDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~GoogleSOCSCookieDefaultTypeInternal() {} + union { + GoogleSOCSCookie _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 GoogleSOCSCookieDefaultTypeInternal _GoogleSOCSCookie_default_instance_; +} // namespace cookieBanner +} // namespace mozilla +namespace mozilla { +namespace cookieBanner { + +// =================================================================== + +class GoogleSOCSCookie_extraData::_Internal { + public: + using HasBits = decltype(std::declval<GoogleSOCSCookie_extraData>()._impl_._has_bits_); + static void set_has_unused1(HasBits* has_bits) { + (*has_bits)[0] |= 4u; + } + static void set_has_platform(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static void set_has_region(HasBits* has_bits) { + (*has_bits)[0] |= 2u; + } + static void set_has_unused2(HasBits* has_bits) { + (*has_bits)[0] |= 8u; + } + static bool MissingRequiredFields(const HasBits& has_bits) { + return ((has_bits[0] & 0x0000000f) ^ 0x0000000f) != 0; + } +}; + +GoogleSOCSCookie_extraData::GoogleSOCSCookie_extraData(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:mozilla.cookieBanner.GoogleSOCSCookie.extraData) +} +GoogleSOCSCookie_extraData::GoogleSOCSCookie_extraData(const GoogleSOCSCookie_extraData& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + GoogleSOCSCookie_extraData* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.platform_){} + , decltype(_impl_.region_){} + , decltype(_impl_.unused1_){} + , decltype(_impl_.unused2_){}}; + + _internal_metadata_.MergeFrom<std::string>(from._internal_metadata_); + _impl_.platform_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.platform_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_platform()) { + _this->_impl_.platform_.Set(from._internal_platform(), + _this->GetArenaForAllocation()); + } + _impl_.region_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.region_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (from._internal_has_region()) { + _this->_impl_.region_.Set(from._internal_region(), + _this->GetArenaForAllocation()); + } + ::memcpy(&_impl_.unused1_, &from._impl_.unused1_, + static_cast<size_t>(reinterpret_cast<char*>(&_impl_.unused2_) - + reinterpret_cast<char*>(&_impl_.unused1_)) + sizeof(_impl_.unused2_)); + // @@protoc_insertion_point(copy_constructor:mozilla.cookieBanner.GoogleSOCSCookie.extraData) +} + +inline void GoogleSOCSCookie_extraData::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.platform_){} + , decltype(_impl_.region_){} + , decltype(_impl_.unused1_){0u} + , decltype(_impl_.unused2_){0u} + }; + _impl_.platform_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.platform_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.region_.InitDefault(); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + _impl_.region_.Set("", GetArenaForAllocation()); + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING +} + +GoogleSOCSCookie_extraData::~GoogleSOCSCookie_extraData() { + // @@protoc_insertion_point(destructor:mozilla.cookieBanner.GoogleSOCSCookie.extraData) + if (auto *arena = _internal_metadata_.DeleteReturnArena<std::string>()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void GoogleSOCSCookie_extraData::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + _impl_.platform_.Destroy(); + _impl_.region_.Destroy(); +} + +void GoogleSOCSCookie_extraData::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void GoogleSOCSCookie_extraData::Clear() { +// @@protoc_insertion_point(message_clear_start:mozilla.cookieBanner.GoogleSOCSCookie.extraData) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000003u) { + if (cached_has_bits & 0x00000001u) { + _impl_.platform_.ClearNonDefaultToEmpty(); + } + if (cached_has_bits & 0x00000002u) { + _impl_.region_.ClearNonDefaultToEmpty(); + } + } + if (cached_has_bits & 0x0000000cu) { + ::memset(&_impl_.unused1_, 0, static_cast<size_t>( + reinterpret_cast<char*>(&_impl_.unused2_) - + reinterpret_cast<char*>(&_impl_.unused1_)) + sizeof(_impl_.unused2_)); + } + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear<std::string>(); +} + +const char* GoogleSOCSCookie_extraData::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // required uint32 unused1 = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 8)) { + _Internal::set_has_unused1(&has_bits); + _impl_.unused1_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint32(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // required string platform = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 18)) { + auto str = _internal_mutable_platform(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // required string region = 3; + case 3: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 26)) { + auto str = _internal_mutable_region(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // required uint32 unused2 = 4; + case 4: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 32)) { + _Internal::set_has_unused2(&has_bits); + _impl_.unused2_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint32(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields<std::string>(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* GoogleSOCSCookie_extraData::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:mozilla.cookieBanner.GoogleSOCSCookie.extraData) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // required uint32 unused1 = 1; + if (cached_has_bits & 0x00000004u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt32ToArray(1, this->_internal_unused1(), target); + } + + // required string platform = 2; + if (cached_has_bits & 0x00000001u) { + target = stream->WriteStringMaybeAliased( + 2, this->_internal_platform(), target); + } + + // required string region = 3; + if (cached_has_bits & 0x00000002u) { + target = stream->WriteStringMaybeAliased( + 3, this->_internal_region(), target); + } + + // required uint32 unused2 = 4; + if (cached_has_bits & 0x00000008u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt32ToArray(4, this->_internal_unused2(), target); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast<int>(_internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:mozilla.cookieBanner.GoogleSOCSCookie.extraData) + return target; +} + +size_t GoogleSOCSCookie_extraData::RequiredFieldsByteSizeFallback() const { +// @@protoc_insertion_point(required_fields_byte_size_fallback_start:mozilla.cookieBanner.GoogleSOCSCookie.extraData) + size_t total_size = 0; + + if (_internal_has_platform()) { + // required string platform = 2; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_platform()); + } + + if (_internal_has_region()) { + // required string region = 3; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_region()); + } + + if (_internal_has_unused1()) { + // required uint32 unused1 = 1; + total_size += ::_pbi::WireFormatLite::UInt32SizePlusOne(this->_internal_unused1()); + } + + if (_internal_has_unused2()) { + // required uint32 unused2 = 4; + total_size += ::_pbi::WireFormatLite::UInt32SizePlusOne(this->_internal_unused2()); + } + + return total_size; +} +size_t GoogleSOCSCookie_extraData::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:mozilla.cookieBanner.GoogleSOCSCookie.extraData) + size_t total_size = 0; + + if (((_impl_._has_bits_[0] & 0x0000000f) ^ 0x0000000f) == 0) { // All required fields are present. + // required string platform = 2; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_platform()); + + // required string region = 3; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize( + this->_internal_region()); + + // required uint32 unused1 = 1; + total_size += ::_pbi::WireFormatLite::UInt32SizePlusOne(this->_internal_unused1()); + + // required uint32 unused2 = 4; + total_size += ::_pbi::WireFormatLite::UInt32SizePlusOne(this->_internal_unused2()); + + } else { + total_size += RequiredFieldsByteSizeFallback(); + } + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void GoogleSOCSCookie_extraData::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast<const GoogleSOCSCookie_extraData*>( + &from)); +} + +void GoogleSOCSCookie_extraData::MergeFrom(const GoogleSOCSCookie_extraData& from) { + GoogleSOCSCookie_extraData* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:mozilla.cookieBanner.GoogleSOCSCookie.extraData) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = from._impl_._has_bits_[0]; + if (cached_has_bits & 0x0000000fu) { + if (cached_has_bits & 0x00000001u) { + _this->_internal_set_platform(from._internal_platform()); + } + if (cached_has_bits & 0x00000002u) { + _this->_internal_set_region(from._internal_region()); + } + if (cached_has_bits & 0x00000004u) { + _this->_impl_.unused1_ = from._impl_.unused1_; + } + if (cached_has_bits & 0x00000008u) { + _this->_impl_.unused2_ = from._impl_.unused2_; + } + _this->_impl_._has_bits_[0] |= cached_has_bits; + } + _this->_internal_metadata_.MergeFrom<std::string>(from._internal_metadata_); +} + +void GoogleSOCSCookie_extraData::CopyFrom(const GoogleSOCSCookie_extraData& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:mozilla.cookieBanner.GoogleSOCSCookie.extraData) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool GoogleSOCSCookie_extraData::IsInitialized() const { + if (_Internal::MissingRequiredFields(_impl_._has_bits_)) return false; + return true; +} + +void GoogleSOCSCookie_extraData::InternalSwap(GoogleSOCSCookie_extraData* other) { + using std::swap; + auto* lhs_arena = GetArenaForAllocation(); + auto* rhs_arena = other->GetArenaForAllocation(); + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.platform_, lhs_arena, + &other->_impl_.platform_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( + &_impl_.region_, lhs_arena, + &other->_impl_.region_, rhs_arena + ); + ::PROTOBUF_NAMESPACE_ID::internal::memswap< + PROTOBUF_FIELD_OFFSET(GoogleSOCSCookie_extraData, _impl_.unused2_) + + sizeof(GoogleSOCSCookie_extraData::_impl_.unused2_) + - PROTOBUF_FIELD_OFFSET(GoogleSOCSCookie_extraData, _impl_.unused1_)>( + reinterpret_cast<char*>(&_impl_.unused1_), + reinterpret_cast<char*>(&other->_impl_.unused1_)); +} + +std::string GoogleSOCSCookie_extraData::GetTypeName() const { + return "mozilla.cookieBanner.GoogleSOCSCookie.extraData"; +} + + +// =================================================================== + +class GoogleSOCSCookie_timeData::_Internal { + public: + using HasBits = decltype(std::declval<GoogleSOCSCookie_timeData>()._impl_._has_bits_); + static void set_has_timestamp(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static bool MissingRequiredFields(const HasBits& has_bits) { + return ((has_bits[0] & 0x00000001) ^ 0x00000001) != 0; + } +}; + +GoogleSOCSCookie_timeData::GoogleSOCSCookie_timeData(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:mozilla.cookieBanner.GoogleSOCSCookie.timeData) +} +GoogleSOCSCookie_timeData::GoogleSOCSCookie_timeData(const GoogleSOCSCookie_timeData& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + GoogleSOCSCookie_timeData* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.timestamp_){}}; + + _internal_metadata_.MergeFrom<std::string>(from._internal_metadata_); + _this->_impl_.timestamp_ = from._impl_.timestamp_; + // @@protoc_insertion_point(copy_constructor:mozilla.cookieBanner.GoogleSOCSCookie.timeData) +} + +inline void GoogleSOCSCookie_timeData::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.timestamp_){uint64_t{0u}} + }; +} + +GoogleSOCSCookie_timeData::~GoogleSOCSCookie_timeData() { + // @@protoc_insertion_point(destructor:mozilla.cookieBanner.GoogleSOCSCookie.timeData) + if (auto *arena = _internal_metadata_.DeleteReturnArena<std::string>()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void GoogleSOCSCookie_timeData::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); +} + +void GoogleSOCSCookie_timeData::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void GoogleSOCSCookie_timeData::Clear() { +// @@protoc_insertion_point(message_clear_start:mozilla.cookieBanner.GoogleSOCSCookie.timeData) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + _impl_.timestamp_ = uint64_t{0u}; + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear<std::string>(); +} + +const char* GoogleSOCSCookie_timeData::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // required uint64 timeStamp = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 8)) { + _Internal::set_has_timestamp(&has_bits); + _impl_.timestamp_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields<std::string>(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* GoogleSOCSCookie_timeData::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:mozilla.cookieBanner.GoogleSOCSCookie.timeData) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // required uint64 timeStamp = 1; + if (cached_has_bits & 0x00000001u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt64ToArray(1, this->_internal_timestamp(), target); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast<int>(_internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:mozilla.cookieBanner.GoogleSOCSCookie.timeData) + return target; +} + +size_t GoogleSOCSCookie_timeData::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:mozilla.cookieBanner.GoogleSOCSCookie.timeData) + size_t total_size = 0; + + // required uint64 timeStamp = 1; + if (_internal_has_timestamp()) { + total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_timestamp()); + } + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void GoogleSOCSCookie_timeData::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast<const GoogleSOCSCookie_timeData*>( + &from)); +} + +void GoogleSOCSCookie_timeData::MergeFrom(const GoogleSOCSCookie_timeData& from) { + GoogleSOCSCookie_timeData* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:mozilla.cookieBanner.GoogleSOCSCookie.timeData) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + if (from._internal_has_timestamp()) { + _this->_internal_set_timestamp(from._internal_timestamp()); + } + _this->_internal_metadata_.MergeFrom<std::string>(from._internal_metadata_); +} + +void GoogleSOCSCookie_timeData::CopyFrom(const GoogleSOCSCookie_timeData& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:mozilla.cookieBanner.GoogleSOCSCookie.timeData) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool GoogleSOCSCookie_timeData::IsInitialized() const { + if (_Internal::MissingRequiredFields(_impl_._has_bits_)) return false; + return true; +} + +void GoogleSOCSCookie_timeData::InternalSwap(GoogleSOCSCookie_timeData* other) { + using std::swap; + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + swap(_impl_.timestamp_, other->_impl_.timestamp_); +} + +std::string GoogleSOCSCookie_timeData::GetTypeName() const { + return "mozilla.cookieBanner.GoogleSOCSCookie.timeData"; +} + + +// =================================================================== + +class GoogleSOCSCookie::_Internal { + public: + using HasBits = decltype(std::declval<GoogleSOCSCookie>()._impl_._has_bits_); + static void set_has_gdpr_choice(HasBits* has_bits) { + (*has_bits)[0] |= 4u; + } + static const ::mozilla::cookieBanner::GoogleSOCSCookie_extraData& data(const GoogleSOCSCookie* msg); + static void set_has_data(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static const ::mozilla::cookieBanner::GoogleSOCSCookie_timeData& time(const GoogleSOCSCookie* msg); + static void set_has_time(HasBits* has_bits) { + (*has_bits)[0] |= 2u; + } + static bool MissingRequiredFields(const HasBits& has_bits) { + return ((has_bits[0] & 0x00000007) ^ 0x00000007) != 0; + } +}; + +const ::mozilla::cookieBanner::GoogleSOCSCookie_extraData& +GoogleSOCSCookie::_Internal::data(const GoogleSOCSCookie* msg) { + return *msg->_impl_.data_; +} +const ::mozilla::cookieBanner::GoogleSOCSCookie_timeData& +GoogleSOCSCookie::_Internal::time(const GoogleSOCSCookie* msg) { + return *msg->_impl_.time_; +} +GoogleSOCSCookie::GoogleSOCSCookie(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:mozilla.cookieBanner.GoogleSOCSCookie) +} +GoogleSOCSCookie::GoogleSOCSCookie(const GoogleSOCSCookie& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + GoogleSOCSCookie* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.data_){nullptr} + , decltype(_impl_.time_){nullptr} + , decltype(_impl_.gdpr_choice_){}}; + + _internal_metadata_.MergeFrom<std::string>(from._internal_metadata_); + if (from._internal_has_data()) { + _this->_impl_.data_ = new ::mozilla::cookieBanner::GoogleSOCSCookie_extraData(*from._impl_.data_); + } + if (from._internal_has_time()) { + _this->_impl_.time_ = new ::mozilla::cookieBanner::GoogleSOCSCookie_timeData(*from._impl_.time_); + } + _this->_impl_.gdpr_choice_ = from._impl_.gdpr_choice_; + // @@protoc_insertion_point(copy_constructor:mozilla.cookieBanner.GoogleSOCSCookie) +} + +inline void GoogleSOCSCookie::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.data_){nullptr} + , decltype(_impl_.time_){nullptr} + , decltype(_impl_.gdpr_choice_){0u} + }; +} + +GoogleSOCSCookie::~GoogleSOCSCookie() { + // @@protoc_insertion_point(destructor:mozilla.cookieBanner.GoogleSOCSCookie) + if (auto *arena = _internal_metadata_.DeleteReturnArena<std::string>()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void GoogleSOCSCookie::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + if (this != internal_default_instance()) delete _impl_.data_; + if (this != internal_default_instance()) delete _impl_.time_; +} + +void GoogleSOCSCookie::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void GoogleSOCSCookie::Clear() { +// @@protoc_insertion_point(message_clear_start:mozilla.cookieBanner.GoogleSOCSCookie) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000003u) { + if (cached_has_bits & 0x00000001u) { + GOOGLE_DCHECK(_impl_.data_ != nullptr); + _impl_.data_->Clear(); + } + if (cached_has_bits & 0x00000002u) { + GOOGLE_DCHECK(_impl_.time_ != nullptr); + _impl_.time_->Clear(); + } + } + _impl_.gdpr_choice_ = 0u; + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear<std::string>(); +} + +const char* GoogleSOCSCookie::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // required uint32 gdpr_choice = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 8)) { + _Internal::set_has_gdpr_choice(&has_bits); + _impl_.gdpr_choice_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint32(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // required .mozilla.cookieBanner.GoogleSOCSCookie.extraData data = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 18)) { + ptr = ctx->ParseMessage(_internal_mutable_data(), ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // required .mozilla.cookieBanner.GoogleSOCSCookie.timeData time = 3; + case 3: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 26)) { + ptr = ctx->ParseMessage(_internal_mutable_time(), ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields<std::string>(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* GoogleSOCSCookie::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:mozilla.cookieBanner.GoogleSOCSCookie) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // required uint32 gdpr_choice = 1; + if (cached_has_bits & 0x00000004u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt32ToArray(1, this->_internal_gdpr_choice(), target); + } + + // required .mozilla.cookieBanner.GoogleSOCSCookie.extraData data = 2; + if (cached_has_bits & 0x00000001u) { + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(2, _Internal::data(this), + _Internal::data(this).GetCachedSize(), target, stream); + } + + // required .mozilla.cookieBanner.GoogleSOCSCookie.timeData time = 3; + if (cached_has_bits & 0x00000002u) { + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(3, _Internal::time(this), + _Internal::time(this).GetCachedSize(), target, stream); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast<int>(_internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:mozilla.cookieBanner.GoogleSOCSCookie) + return target; +} + +size_t GoogleSOCSCookie::RequiredFieldsByteSizeFallback() const { +// @@protoc_insertion_point(required_fields_byte_size_fallback_start:mozilla.cookieBanner.GoogleSOCSCookie) + size_t total_size = 0; + + if (_internal_has_data()) { + // required .mozilla.cookieBanner.GoogleSOCSCookie.extraData data = 2; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize( + *_impl_.data_); + } + + if (_internal_has_time()) { + // required .mozilla.cookieBanner.GoogleSOCSCookie.timeData time = 3; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize( + *_impl_.time_); + } + + if (_internal_has_gdpr_choice()) { + // required uint32 gdpr_choice = 1; + total_size += ::_pbi::WireFormatLite::UInt32SizePlusOne(this->_internal_gdpr_choice()); + } + + return total_size; +} +size_t GoogleSOCSCookie::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:mozilla.cookieBanner.GoogleSOCSCookie) + size_t total_size = 0; + + if (((_impl_._has_bits_[0] & 0x00000007) ^ 0x00000007) == 0) { // All required fields are present. + // required .mozilla.cookieBanner.GoogleSOCSCookie.extraData data = 2; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize( + *_impl_.data_); + + // required .mozilla.cookieBanner.GoogleSOCSCookie.timeData time = 3; + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize( + *_impl_.time_); + + // required uint32 gdpr_choice = 1; + total_size += ::_pbi::WireFormatLite::UInt32SizePlusOne(this->_internal_gdpr_choice()); + + } else { + total_size += RequiredFieldsByteSizeFallback(); + } + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void GoogleSOCSCookie::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast<const GoogleSOCSCookie*>( + &from)); +} + +void GoogleSOCSCookie::MergeFrom(const GoogleSOCSCookie& from) { + GoogleSOCSCookie* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:mozilla.cookieBanner.GoogleSOCSCookie) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = from._impl_._has_bits_[0]; + if (cached_has_bits & 0x00000007u) { + if (cached_has_bits & 0x00000001u) { + _this->_internal_mutable_data()->::mozilla::cookieBanner::GoogleSOCSCookie_extraData::MergeFrom( + from._internal_data()); + } + if (cached_has_bits & 0x00000002u) { + _this->_internal_mutable_time()->::mozilla::cookieBanner::GoogleSOCSCookie_timeData::MergeFrom( + from._internal_time()); + } + if (cached_has_bits & 0x00000004u) { + _this->_impl_.gdpr_choice_ = from._impl_.gdpr_choice_; + } + _this->_impl_._has_bits_[0] |= cached_has_bits; + } + _this->_internal_metadata_.MergeFrom<std::string>(from._internal_metadata_); +} + +void GoogleSOCSCookie::CopyFrom(const GoogleSOCSCookie& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:mozilla.cookieBanner.GoogleSOCSCookie) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool GoogleSOCSCookie::IsInitialized() const { + if (_Internal::MissingRequiredFields(_impl_._has_bits_)) return false; + if (_internal_has_data()) { + if (!_impl_.data_->IsInitialized()) return false; + } + if (_internal_has_time()) { + if (!_impl_.time_->IsInitialized()) return false; + } + return true; +} + +void GoogleSOCSCookie::InternalSwap(GoogleSOCSCookie* other) { + using std::swap; + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + ::PROTOBUF_NAMESPACE_ID::internal::memswap< + PROTOBUF_FIELD_OFFSET(GoogleSOCSCookie, _impl_.gdpr_choice_) + + sizeof(GoogleSOCSCookie::_impl_.gdpr_choice_) + - PROTOBUF_FIELD_OFFSET(GoogleSOCSCookie, _impl_.data_)>( + reinterpret_cast<char*>(&_impl_.data_), + reinterpret_cast<char*>(&other->_impl_.data_)); +} + +std::string GoogleSOCSCookie::GetTypeName() const { + return "mozilla.cookieBanner.GoogleSOCSCookie"; +} + + +// @@protoc_insertion_point(namespace_scope) +} // namespace cookieBanner +} // namespace mozilla +PROTOBUF_NAMESPACE_OPEN +template<> PROTOBUF_NOINLINE ::mozilla::cookieBanner::GoogleSOCSCookie_extraData* +Arena::CreateMaybeMessage< ::mozilla::cookieBanner::GoogleSOCSCookie_extraData >(Arena* arena) { + return Arena::CreateMessageInternal< ::mozilla::cookieBanner::GoogleSOCSCookie_extraData >(arena); +} +template<> PROTOBUF_NOINLINE ::mozilla::cookieBanner::GoogleSOCSCookie_timeData* +Arena::CreateMaybeMessage< ::mozilla::cookieBanner::GoogleSOCSCookie_timeData >(Arena* arena) { + return Arena::CreateMessageInternal< ::mozilla::cookieBanner::GoogleSOCSCookie_timeData >(arena); +} +template<> PROTOBUF_NOINLINE ::mozilla::cookieBanner::GoogleSOCSCookie* +Arena::CreateMaybeMessage< ::mozilla::cookieBanner::GoogleSOCSCookie >(Arena* arena) { + return Arena::CreateMessageInternal< ::mozilla::cookieBanner::GoogleSOCSCookie >(arena); +} +PROTOBUF_NAMESPACE_CLOSE + +// @@protoc_insertion_point(global_scope) +#include <google/protobuf/port_undef.inc> diff --git a/toolkit/components/cookiebanners/cookieBanner.pb.h b/toolkit/components/cookiebanners/cookieBanner.pb.h new file mode 100644 index 0000000000..f7bb138489 --- /dev/null +++ b/toolkit/components/cookiebanners/cookieBanner.pb.h @@ -0,0 +1,1058 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: cookieBanner.proto + +#ifndef GOOGLE_PROTOBUF_INCLUDED_cookieBanner_2eproto +#define GOOGLE_PROTOBUF_INCLUDED_cookieBanner_2eproto + +#include <limits> +#include <string> + +#include <google/protobuf/port_def.inc> +#if PROTOBUF_VERSION < 3021000 +#error This file was generated by a newer version of protoc which is +#error incompatible with your Protocol Buffer headers. Please update +#error your headers. +#endif +#if 3021012 < PROTOBUF_MIN_PROTOC_VERSION +#error This file was generated by an older version of protoc which is +#error incompatible with your Protocol Buffer headers. Please +#error regenerate this file with a newer version of protoc. +#endif + +#include <google/protobuf/port_undef.inc> +#include <google/protobuf/io/coded_stream.h> +#include <google/protobuf/arena.h> +#include <google/protobuf/arenastring.h> +#include <google/protobuf/generated_message_util.h> +#include <google/protobuf/metadata_lite.h> +#include <google/protobuf/message_lite.h> +#include <google/protobuf/repeated_field.h> // IWYU pragma: export +#include <google/protobuf/extension_set.h> // IWYU pragma: export +// @@protoc_insertion_point(includes) +#include <google/protobuf/port_def.inc> +#define PROTOBUF_INTERNAL_EXPORT_cookieBanner_2eproto +PROTOBUF_NAMESPACE_OPEN +namespace internal { +class AnyMetadata; +} // namespace internal +PROTOBUF_NAMESPACE_CLOSE + +// Internal implementation detail -- do not use these members. +struct TableStruct_cookieBanner_2eproto { + static const uint32_t offsets[]; +}; +namespace mozilla { +namespace cookieBanner { +class GoogleSOCSCookie; +struct GoogleSOCSCookieDefaultTypeInternal; +extern GoogleSOCSCookieDefaultTypeInternal _GoogleSOCSCookie_default_instance_; +class GoogleSOCSCookie_extraData; +struct GoogleSOCSCookie_extraDataDefaultTypeInternal; +extern GoogleSOCSCookie_extraDataDefaultTypeInternal _GoogleSOCSCookie_extraData_default_instance_; +class GoogleSOCSCookie_timeData; +struct GoogleSOCSCookie_timeDataDefaultTypeInternal; +extern GoogleSOCSCookie_timeDataDefaultTypeInternal _GoogleSOCSCookie_timeData_default_instance_; +} // namespace cookieBanner +} // namespace mozilla +PROTOBUF_NAMESPACE_OPEN +template<> ::mozilla::cookieBanner::GoogleSOCSCookie* Arena::CreateMaybeMessage<::mozilla::cookieBanner::GoogleSOCSCookie>(Arena*); +template<> ::mozilla::cookieBanner::GoogleSOCSCookie_extraData* Arena::CreateMaybeMessage<::mozilla::cookieBanner::GoogleSOCSCookie_extraData>(Arena*); +template<> ::mozilla::cookieBanner::GoogleSOCSCookie_timeData* Arena::CreateMaybeMessage<::mozilla::cookieBanner::GoogleSOCSCookie_timeData>(Arena*); +PROTOBUF_NAMESPACE_CLOSE +namespace mozilla { +namespace cookieBanner { + +// =================================================================== + +class GoogleSOCSCookie_extraData final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:mozilla.cookieBanner.GoogleSOCSCookie.extraData) */ { + public: + inline GoogleSOCSCookie_extraData() : GoogleSOCSCookie_extraData(nullptr) {} + ~GoogleSOCSCookie_extraData() override; + explicit PROTOBUF_CONSTEXPR GoogleSOCSCookie_extraData(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + GoogleSOCSCookie_extraData(const GoogleSOCSCookie_extraData& from); + GoogleSOCSCookie_extraData(GoogleSOCSCookie_extraData&& from) noexcept + : GoogleSOCSCookie_extraData() { + *this = ::std::move(from); + } + + inline GoogleSOCSCookie_extraData& operator=(const GoogleSOCSCookie_extraData& from) { + CopyFrom(from); + return *this; + } + inline GoogleSOCSCookie_extraData& operator=(GoogleSOCSCookie_extraData&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields<std::string>(); + } + + static const GoogleSOCSCookie_extraData& default_instance() { + return *internal_default_instance(); + } + static inline const GoogleSOCSCookie_extraData* internal_default_instance() { + return reinterpret_cast<const GoogleSOCSCookie_extraData*>( + &_GoogleSOCSCookie_extraData_default_instance_); + } + static constexpr int kIndexInFileMessages = + 0; + + friend void swap(GoogleSOCSCookie_extraData& a, GoogleSOCSCookie_extraData& b) { + a.Swap(&b); + } + inline void Swap(GoogleSOCSCookie_extraData* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(GoogleSOCSCookie_extraData* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + GoogleSOCSCookie_extraData* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage<GoogleSOCSCookie_extraData>(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const GoogleSOCSCookie_extraData& from); + void MergeFrom(const GoogleSOCSCookie_extraData& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(GoogleSOCSCookie_extraData* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "mozilla.cookieBanner.GoogleSOCSCookie.extraData"; + } + protected: + explicit GoogleSOCSCookie_extraData(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + // accessors ------------------------------------------------------- + + enum : int { + kPlatformFieldNumber = 2, + kRegionFieldNumber = 3, + kUnused1FieldNumber = 1, + kUnused2FieldNumber = 4, + }; + // required string platform = 2; + bool has_platform() const; + private: + bool _internal_has_platform() const; + public: + void clear_platform(); + const std::string& platform() const; + template <typename ArgT0 = const std::string&, typename... ArgT> + void set_platform(ArgT0&& arg0, ArgT... args); + std::string* mutable_platform(); + PROTOBUF_NODISCARD std::string* release_platform(); + void set_allocated_platform(std::string* platform); + private: + const std::string& _internal_platform() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_platform(const std::string& value); + std::string* _internal_mutable_platform(); + public: + + // required string region = 3; + bool has_region() const; + private: + bool _internal_has_region() const; + public: + void clear_region(); + const std::string& region() const; + template <typename ArgT0 = const std::string&, typename... ArgT> + void set_region(ArgT0&& arg0, ArgT... args); + std::string* mutable_region(); + PROTOBUF_NODISCARD std::string* release_region(); + void set_allocated_region(std::string* region); + private: + const std::string& _internal_region() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_region(const std::string& value); + std::string* _internal_mutable_region(); + public: + + // required uint32 unused1 = 1; + bool has_unused1() const; + private: + bool _internal_has_unused1() const; + public: + void clear_unused1(); + uint32_t unused1() const; + void set_unused1(uint32_t value); + private: + uint32_t _internal_unused1() const; + void _internal_set_unused1(uint32_t value); + public: + + // required uint32 unused2 = 4; + bool has_unused2() const; + private: + bool _internal_has_unused2() const; + public: + void clear_unused2(); + uint32_t unused2() const; + void set_unused2(uint32_t value); + private: + uint32_t _internal_unused2() const; + void _internal_set_unused2(uint32_t value); + public: + + // @@protoc_insertion_point(class_scope:mozilla.cookieBanner.GoogleSOCSCookie.extraData) + private: + class _Internal; + + // helper for ByteSizeLong() + size_t RequiredFieldsByteSizeFallback() const; + + template <typename T> friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr platform_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr region_; + uint32_t unused1_; + uint32_t unused2_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_cookieBanner_2eproto; +}; +// ------------------------------------------------------------------- + +class GoogleSOCSCookie_timeData final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:mozilla.cookieBanner.GoogleSOCSCookie.timeData) */ { + public: + inline GoogleSOCSCookie_timeData() : GoogleSOCSCookie_timeData(nullptr) {} + ~GoogleSOCSCookie_timeData() override; + explicit PROTOBUF_CONSTEXPR GoogleSOCSCookie_timeData(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + GoogleSOCSCookie_timeData(const GoogleSOCSCookie_timeData& from); + GoogleSOCSCookie_timeData(GoogleSOCSCookie_timeData&& from) noexcept + : GoogleSOCSCookie_timeData() { + *this = ::std::move(from); + } + + inline GoogleSOCSCookie_timeData& operator=(const GoogleSOCSCookie_timeData& from) { + CopyFrom(from); + return *this; + } + inline GoogleSOCSCookie_timeData& operator=(GoogleSOCSCookie_timeData&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields<std::string>(); + } + + static const GoogleSOCSCookie_timeData& default_instance() { + return *internal_default_instance(); + } + static inline const GoogleSOCSCookie_timeData* internal_default_instance() { + return reinterpret_cast<const GoogleSOCSCookie_timeData*>( + &_GoogleSOCSCookie_timeData_default_instance_); + } + static constexpr int kIndexInFileMessages = + 1; + + friend void swap(GoogleSOCSCookie_timeData& a, GoogleSOCSCookie_timeData& b) { + a.Swap(&b); + } + inline void Swap(GoogleSOCSCookie_timeData* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(GoogleSOCSCookie_timeData* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + GoogleSOCSCookie_timeData* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage<GoogleSOCSCookie_timeData>(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const GoogleSOCSCookie_timeData& from); + void MergeFrom(const GoogleSOCSCookie_timeData& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(GoogleSOCSCookie_timeData* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "mozilla.cookieBanner.GoogleSOCSCookie.timeData"; + } + protected: + explicit GoogleSOCSCookie_timeData(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + // accessors ------------------------------------------------------- + + enum : int { + kTimeStampFieldNumber = 1, + }; + // required uint64 timeStamp = 1; + bool has_timestamp() const; + private: + bool _internal_has_timestamp() const; + public: + void clear_timestamp(); + uint64_t timestamp() const; + void set_timestamp(uint64_t value); + private: + uint64_t _internal_timestamp() const; + void _internal_set_timestamp(uint64_t value); + public: + + // @@protoc_insertion_point(class_scope:mozilla.cookieBanner.GoogleSOCSCookie.timeData) + private: + class _Internal; + + template <typename T> friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + uint64_t timestamp_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_cookieBanner_2eproto; +}; +// ------------------------------------------------------------------- + +class GoogleSOCSCookie final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:mozilla.cookieBanner.GoogleSOCSCookie) */ { + public: + inline GoogleSOCSCookie() : GoogleSOCSCookie(nullptr) {} + ~GoogleSOCSCookie() override; + explicit PROTOBUF_CONSTEXPR GoogleSOCSCookie(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + GoogleSOCSCookie(const GoogleSOCSCookie& from); + GoogleSOCSCookie(GoogleSOCSCookie&& from) noexcept + : GoogleSOCSCookie() { + *this = ::std::move(from); + } + + inline GoogleSOCSCookie& operator=(const GoogleSOCSCookie& from) { + CopyFrom(from); + return *this; + } + inline GoogleSOCSCookie& operator=(GoogleSOCSCookie&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields<std::string>(); + } + + static const GoogleSOCSCookie& default_instance() { + return *internal_default_instance(); + } + static inline const GoogleSOCSCookie* internal_default_instance() { + return reinterpret_cast<const GoogleSOCSCookie*>( + &_GoogleSOCSCookie_default_instance_); + } + static constexpr int kIndexInFileMessages = + 2; + + friend void swap(GoogleSOCSCookie& a, GoogleSOCSCookie& b) { + a.Swap(&b); + } + inline void Swap(GoogleSOCSCookie* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(GoogleSOCSCookie* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + GoogleSOCSCookie* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage<GoogleSOCSCookie>(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const GoogleSOCSCookie& from); + void MergeFrom(const GoogleSOCSCookie& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(GoogleSOCSCookie* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "mozilla.cookieBanner.GoogleSOCSCookie"; + } + protected: + explicit GoogleSOCSCookie(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + typedef GoogleSOCSCookie_extraData extraData; + typedef GoogleSOCSCookie_timeData timeData; + + // accessors ------------------------------------------------------- + + enum : int { + kDataFieldNumber = 2, + kTimeFieldNumber = 3, + kGdprChoiceFieldNumber = 1, + }; + // required .mozilla.cookieBanner.GoogleSOCSCookie.extraData data = 2; + bool has_data() const; + private: + bool _internal_has_data() const; + public: + void clear_data(); + const ::mozilla::cookieBanner::GoogleSOCSCookie_extraData& data() const; + PROTOBUF_NODISCARD ::mozilla::cookieBanner::GoogleSOCSCookie_extraData* release_data(); + ::mozilla::cookieBanner::GoogleSOCSCookie_extraData* mutable_data(); + void set_allocated_data(::mozilla::cookieBanner::GoogleSOCSCookie_extraData* data); + private: + const ::mozilla::cookieBanner::GoogleSOCSCookie_extraData& _internal_data() const; + ::mozilla::cookieBanner::GoogleSOCSCookie_extraData* _internal_mutable_data(); + public: + void unsafe_arena_set_allocated_data( + ::mozilla::cookieBanner::GoogleSOCSCookie_extraData* data); + ::mozilla::cookieBanner::GoogleSOCSCookie_extraData* unsafe_arena_release_data(); + + // required .mozilla.cookieBanner.GoogleSOCSCookie.timeData time = 3; + bool has_time() const; + private: + bool _internal_has_time() const; + public: + void clear_time(); + const ::mozilla::cookieBanner::GoogleSOCSCookie_timeData& time() const; + PROTOBUF_NODISCARD ::mozilla::cookieBanner::GoogleSOCSCookie_timeData* release_time(); + ::mozilla::cookieBanner::GoogleSOCSCookie_timeData* mutable_time(); + void set_allocated_time(::mozilla::cookieBanner::GoogleSOCSCookie_timeData* time); + private: + const ::mozilla::cookieBanner::GoogleSOCSCookie_timeData& _internal_time() const; + ::mozilla::cookieBanner::GoogleSOCSCookie_timeData* _internal_mutable_time(); + public: + void unsafe_arena_set_allocated_time( + ::mozilla::cookieBanner::GoogleSOCSCookie_timeData* time); + ::mozilla::cookieBanner::GoogleSOCSCookie_timeData* unsafe_arena_release_time(); + + // required uint32 gdpr_choice = 1; + bool has_gdpr_choice() const; + private: + bool _internal_has_gdpr_choice() const; + public: + void clear_gdpr_choice(); + uint32_t gdpr_choice() const; + void set_gdpr_choice(uint32_t value); + private: + uint32_t _internal_gdpr_choice() const; + void _internal_set_gdpr_choice(uint32_t value); + public: + + // @@protoc_insertion_point(class_scope:mozilla.cookieBanner.GoogleSOCSCookie) + private: + class _Internal; + + // helper for ByteSizeLong() + size_t RequiredFieldsByteSizeFallback() const; + + template <typename T> friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + ::mozilla::cookieBanner::GoogleSOCSCookie_extraData* data_; + ::mozilla::cookieBanner::GoogleSOCSCookie_timeData* time_; + uint32_t gdpr_choice_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_cookieBanner_2eproto; +}; +// =================================================================== + + +// =================================================================== + +#ifdef __GNUC__ + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wstrict-aliasing" +#endif // __GNUC__ +// GoogleSOCSCookie_extraData + +// required uint32 unused1 = 1; +inline bool GoogleSOCSCookie_extraData::_internal_has_unused1() const { + bool value = (_impl_._has_bits_[0] & 0x00000004u) != 0; + return value; +} +inline bool GoogleSOCSCookie_extraData::has_unused1() const { + return _internal_has_unused1(); +} +inline void GoogleSOCSCookie_extraData::clear_unused1() { + _impl_.unused1_ = 0u; + _impl_._has_bits_[0] &= ~0x00000004u; +} +inline uint32_t GoogleSOCSCookie_extraData::_internal_unused1() const { + return _impl_.unused1_; +} +inline uint32_t GoogleSOCSCookie_extraData::unused1() const { + // @@protoc_insertion_point(field_get:mozilla.cookieBanner.GoogleSOCSCookie.extraData.unused1) + return _internal_unused1(); +} +inline void GoogleSOCSCookie_extraData::_internal_set_unused1(uint32_t value) { + _impl_._has_bits_[0] |= 0x00000004u; + _impl_.unused1_ = value; +} +inline void GoogleSOCSCookie_extraData::set_unused1(uint32_t value) { + _internal_set_unused1(value); + // @@protoc_insertion_point(field_set:mozilla.cookieBanner.GoogleSOCSCookie.extraData.unused1) +} + +// required string platform = 2; +inline bool GoogleSOCSCookie_extraData::_internal_has_platform() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + return value; +} +inline bool GoogleSOCSCookie_extraData::has_platform() const { + return _internal_has_platform(); +} +inline void GoogleSOCSCookie_extraData::clear_platform() { + _impl_.platform_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline const std::string& GoogleSOCSCookie_extraData::platform() const { + // @@protoc_insertion_point(field_get:mozilla.cookieBanner.GoogleSOCSCookie.extraData.platform) + return _internal_platform(); +} +template <typename ArgT0, typename... ArgT> +inline PROTOBUF_ALWAYS_INLINE +void GoogleSOCSCookie_extraData::set_platform(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.platform_.Set(static_cast<ArgT0 &&>(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:mozilla.cookieBanner.GoogleSOCSCookie.extraData.platform) +} +inline std::string* GoogleSOCSCookie_extraData::mutable_platform() { + std::string* _s = _internal_mutable_platform(); + // @@protoc_insertion_point(field_mutable:mozilla.cookieBanner.GoogleSOCSCookie.extraData.platform) + return _s; +} +inline const std::string& GoogleSOCSCookie_extraData::_internal_platform() const { + return _impl_.platform_.Get(); +} +inline void GoogleSOCSCookie_extraData::_internal_set_platform(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.platform_.Set(value, GetArenaForAllocation()); +} +inline std::string* GoogleSOCSCookie_extraData::_internal_mutable_platform() { + _impl_._has_bits_[0] |= 0x00000001u; + return _impl_.platform_.Mutable(GetArenaForAllocation()); +} +inline std::string* GoogleSOCSCookie_extraData::release_platform() { + // @@protoc_insertion_point(field_release:mozilla.cookieBanner.GoogleSOCSCookie.extraData.platform) + if (!_internal_has_platform()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000001u; + auto* p = _impl_.platform_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.platform_.IsDefault()) { + _impl_.platform_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void GoogleSOCSCookie_extraData::set_allocated_platform(std::string* platform) { + if (platform != nullptr) { + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + _impl_.platform_.SetAllocated(platform, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.platform_.IsDefault()) { + _impl_.platform_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:mozilla.cookieBanner.GoogleSOCSCookie.extraData.platform) +} + +// required string region = 3; +inline bool GoogleSOCSCookie_extraData::_internal_has_region() const { + bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0; + return value; +} +inline bool GoogleSOCSCookie_extraData::has_region() const { + return _internal_has_region(); +} +inline void GoogleSOCSCookie_extraData::clear_region() { + _impl_.region_.ClearToEmpty(); + _impl_._has_bits_[0] &= ~0x00000002u; +} +inline const std::string& GoogleSOCSCookie_extraData::region() const { + // @@protoc_insertion_point(field_get:mozilla.cookieBanner.GoogleSOCSCookie.extraData.region) + return _internal_region(); +} +template <typename ArgT0, typename... ArgT> +inline PROTOBUF_ALWAYS_INLINE +void GoogleSOCSCookie_extraData::set_region(ArgT0&& arg0, ArgT... args) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.region_.Set(static_cast<ArgT0 &&>(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:mozilla.cookieBanner.GoogleSOCSCookie.extraData.region) +} +inline std::string* GoogleSOCSCookie_extraData::mutable_region() { + std::string* _s = _internal_mutable_region(); + // @@protoc_insertion_point(field_mutable:mozilla.cookieBanner.GoogleSOCSCookie.extraData.region) + return _s; +} +inline const std::string& GoogleSOCSCookie_extraData::_internal_region() const { + return _impl_.region_.Get(); +} +inline void GoogleSOCSCookie_extraData::_internal_set_region(const std::string& value) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.region_.Set(value, GetArenaForAllocation()); +} +inline std::string* GoogleSOCSCookie_extraData::_internal_mutable_region() { + _impl_._has_bits_[0] |= 0x00000002u; + return _impl_.region_.Mutable(GetArenaForAllocation()); +} +inline std::string* GoogleSOCSCookie_extraData::release_region() { + // @@protoc_insertion_point(field_release:mozilla.cookieBanner.GoogleSOCSCookie.extraData.region) + if (!_internal_has_region()) { + return nullptr; + } + _impl_._has_bits_[0] &= ~0x00000002u; + auto* p = _impl_.region_.Release(); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.region_.IsDefault()) { + _impl_.region_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + return p; +} +inline void GoogleSOCSCookie_extraData::set_allocated_region(std::string* region) { + if (region != nullptr) { + _impl_._has_bits_[0] |= 0x00000002u; + } else { + _impl_._has_bits_[0] &= ~0x00000002u; + } + _impl_.region_.SetAllocated(region, GetArenaForAllocation()); +#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.region_.IsDefault()) { + _impl_.region_.Set("", GetArenaForAllocation()); + } +#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:mozilla.cookieBanner.GoogleSOCSCookie.extraData.region) +} + +// required uint32 unused2 = 4; +inline bool GoogleSOCSCookie_extraData::_internal_has_unused2() const { + bool value = (_impl_._has_bits_[0] & 0x00000008u) != 0; + return value; +} +inline bool GoogleSOCSCookie_extraData::has_unused2() const { + return _internal_has_unused2(); +} +inline void GoogleSOCSCookie_extraData::clear_unused2() { + _impl_.unused2_ = 0u; + _impl_._has_bits_[0] &= ~0x00000008u; +} +inline uint32_t GoogleSOCSCookie_extraData::_internal_unused2() const { + return _impl_.unused2_; +} +inline uint32_t GoogleSOCSCookie_extraData::unused2() const { + // @@protoc_insertion_point(field_get:mozilla.cookieBanner.GoogleSOCSCookie.extraData.unused2) + return _internal_unused2(); +} +inline void GoogleSOCSCookie_extraData::_internal_set_unused2(uint32_t value) { + _impl_._has_bits_[0] |= 0x00000008u; + _impl_.unused2_ = value; +} +inline void GoogleSOCSCookie_extraData::set_unused2(uint32_t value) { + _internal_set_unused2(value); + // @@protoc_insertion_point(field_set:mozilla.cookieBanner.GoogleSOCSCookie.extraData.unused2) +} + +// ------------------------------------------------------------------- + +// GoogleSOCSCookie_timeData + +// required uint64 timeStamp = 1; +inline bool GoogleSOCSCookie_timeData::_internal_has_timestamp() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + return value; +} +inline bool GoogleSOCSCookie_timeData::has_timestamp() const { + return _internal_has_timestamp(); +} +inline void GoogleSOCSCookie_timeData::clear_timestamp() { + _impl_.timestamp_ = uint64_t{0u}; + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline uint64_t GoogleSOCSCookie_timeData::_internal_timestamp() const { + return _impl_.timestamp_; +} +inline uint64_t GoogleSOCSCookie_timeData::timestamp() const { + // @@protoc_insertion_point(field_get:mozilla.cookieBanner.GoogleSOCSCookie.timeData.timeStamp) + return _internal_timestamp(); +} +inline void GoogleSOCSCookie_timeData::_internal_set_timestamp(uint64_t value) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.timestamp_ = value; +} +inline void GoogleSOCSCookie_timeData::set_timestamp(uint64_t value) { + _internal_set_timestamp(value); + // @@protoc_insertion_point(field_set:mozilla.cookieBanner.GoogleSOCSCookie.timeData.timeStamp) +} + +// ------------------------------------------------------------------- + +// GoogleSOCSCookie + +// required uint32 gdpr_choice = 1; +inline bool GoogleSOCSCookie::_internal_has_gdpr_choice() const { + bool value = (_impl_._has_bits_[0] & 0x00000004u) != 0; + return value; +} +inline bool GoogleSOCSCookie::has_gdpr_choice() const { + return _internal_has_gdpr_choice(); +} +inline void GoogleSOCSCookie::clear_gdpr_choice() { + _impl_.gdpr_choice_ = 0u; + _impl_._has_bits_[0] &= ~0x00000004u; +} +inline uint32_t GoogleSOCSCookie::_internal_gdpr_choice() const { + return _impl_.gdpr_choice_; +} +inline uint32_t GoogleSOCSCookie::gdpr_choice() const { + // @@protoc_insertion_point(field_get:mozilla.cookieBanner.GoogleSOCSCookie.gdpr_choice) + return _internal_gdpr_choice(); +} +inline void GoogleSOCSCookie::_internal_set_gdpr_choice(uint32_t value) { + _impl_._has_bits_[0] |= 0x00000004u; + _impl_.gdpr_choice_ = value; +} +inline void GoogleSOCSCookie::set_gdpr_choice(uint32_t value) { + _internal_set_gdpr_choice(value); + // @@protoc_insertion_point(field_set:mozilla.cookieBanner.GoogleSOCSCookie.gdpr_choice) +} + +// required .mozilla.cookieBanner.GoogleSOCSCookie.extraData data = 2; +inline bool GoogleSOCSCookie::_internal_has_data() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + PROTOBUF_ASSUME(!value || _impl_.data_ != nullptr); + return value; +} +inline bool GoogleSOCSCookie::has_data() const { + return _internal_has_data(); +} +inline void GoogleSOCSCookie::clear_data() { + if (_impl_.data_ != nullptr) _impl_.data_->Clear(); + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline const ::mozilla::cookieBanner::GoogleSOCSCookie_extraData& GoogleSOCSCookie::_internal_data() const { + const ::mozilla::cookieBanner::GoogleSOCSCookie_extraData* p = _impl_.data_; + return p != nullptr ? *p : reinterpret_cast<const ::mozilla::cookieBanner::GoogleSOCSCookie_extraData&>( + ::mozilla::cookieBanner::_GoogleSOCSCookie_extraData_default_instance_); +} +inline const ::mozilla::cookieBanner::GoogleSOCSCookie_extraData& GoogleSOCSCookie::data() const { + // @@protoc_insertion_point(field_get:mozilla.cookieBanner.GoogleSOCSCookie.data) + return _internal_data(); +} +inline void GoogleSOCSCookie::unsafe_arena_set_allocated_data( + ::mozilla::cookieBanner::GoogleSOCSCookie_extraData* data) { + if (GetArenaForAllocation() == nullptr) { + delete reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(_impl_.data_); + } + _impl_.data_ = data; + if (data) { + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + // @@protoc_insertion_point(field_unsafe_arena_set_allocated:mozilla.cookieBanner.GoogleSOCSCookie.data) +} +inline ::mozilla::cookieBanner::GoogleSOCSCookie_extraData* GoogleSOCSCookie::release_data() { + _impl_._has_bits_[0] &= ~0x00000001u; + ::mozilla::cookieBanner::GoogleSOCSCookie_extraData* temp = _impl_.data_; + _impl_.data_ = nullptr; +#ifdef PROTOBUF_FORCE_COPY_IN_RELEASE + auto* old = reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(temp); + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + if (GetArenaForAllocation() == nullptr) { delete old; } +#else // PROTOBUF_FORCE_COPY_IN_RELEASE + if (GetArenaForAllocation() != nullptr) { + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + } +#endif // !PROTOBUF_FORCE_COPY_IN_RELEASE + return temp; +} +inline ::mozilla::cookieBanner::GoogleSOCSCookie_extraData* GoogleSOCSCookie::unsafe_arena_release_data() { + // @@protoc_insertion_point(field_release:mozilla.cookieBanner.GoogleSOCSCookie.data) + _impl_._has_bits_[0] &= ~0x00000001u; + ::mozilla::cookieBanner::GoogleSOCSCookie_extraData* temp = _impl_.data_; + _impl_.data_ = nullptr; + return temp; +} +inline ::mozilla::cookieBanner::GoogleSOCSCookie_extraData* GoogleSOCSCookie::_internal_mutable_data() { + _impl_._has_bits_[0] |= 0x00000001u; + if (_impl_.data_ == nullptr) { + auto* p = CreateMaybeMessage<::mozilla::cookieBanner::GoogleSOCSCookie_extraData>(GetArenaForAllocation()); + _impl_.data_ = p; + } + return _impl_.data_; +} +inline ::mozilla::cookieBanner::GoogleSOCSCookie_extraData* GoogleSOCSCookie::mutable_data() { + ::mozilla::cookieBanner::GoogleSOCSCookie_extraData* _msg = _internal_mutable_data(); + // @@protoc_insertion_point(field_mutable:mozilla.cookieBanner.GoogleSOCSCookie.data) + return _msg; +} +inline void GoogleSOCSCookie::set_allocated_data(::mozilla::cookieBanner::GoogleSOCSCookie_extraData* data) { + ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation(); + if (message_arena == nullptr) { + delete _impl_.data_; + } + if (data) { + ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena = + ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(data); + if (message_arena != submessage_arena) { + data = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage( + message_arena, data, submessage_arena); + } + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + _impl_.data_ = data; + // @@protoc_insertion_point(field_set_allocated:mozilla.cookieBanner.GoogleSOCSCookie.data) +} + +// required .mozilla.cookieBanner.GoogleSOCSCookie.timeData time = 3; +inline bool GoogleSOCSCookie::_internal_has_time() const { + bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0; + PROTOBUF_ASSUME(!value || _impl_.time_ != nullptr); + return value; +} +inline bool GoogleSOCSCookie::has_time() const { + return _internal_has_time(); +} +inline void GoogleSOCSCookie::clear_time() { + if (_impl_.time_ != nullptr) _impl_.time_->Clear(); + _impl_._has_bits_[0] &= ~0x00000002u; +} +inline const ::mozilla::cookieBanner::GoogleSOCSCookie_timeData& GoogleSOCSCookie::_internal_time() const { + const ::mozilla::cookieBanner::GoogleSOCSCookie_timeData* p = _impl_.time_; + return p != nullptr ? *p : reinterpret_cast<const ::mozilla::cookieBanner::GoogleSOCSCookie_timeData&>( + ::mozilla::cookieBanner::_GoogleSOCSCookie_timeData_default_instance_); +} +inline const ::mozilla::cookieBanner::GoogleSOCSCookie_timeData& GoogleSOCSCookie::time() const { + // @@protoc_insertion_point(field_get:mozilla.cookieBanner.GoogleSOCSCookie.time) + return _internal_time(); +} +inline void GoogleSOCSCookie::unsafe_arena_set_allocated_time( + ::mozilla::cookieBanner::GoogleSOCSCookie_timeData* time) { + if (GetArenaForAllocation() == nullptr) { + delete reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(_impl_.time_); + } + _impl_.time_ = time; + if (time) { + _impl_._has_bits_[0] |= 0x00000002u; + } else { + _impl_._has_bits_[0] &= ~0x00000002u; + } + // @@protoc_insertion_point(field_unsafe_arena_set_allocated:mozilla.cookieBanner.GoogleSOCSCookie.time) +} +inline ::mozilla::cookieBanner::GoogleSOCSCookie_timeData* GoogleSOCSCookie::release_time() { + _impl_._has_bits_[0] &= ~0x00000002u; + ::mozilla::cookieBanner::GoogleSOCSCookie_timeData* temp = _impl_.time_; + _impl_.time_ = nullptr; +#ifdef PROTOBUF_FORCE_COPY_IN_RELEASE + auto* old = reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(temp); + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + if (GetArenaForAllocation() == nullptr) { delete old; } +#else // PROTOBUF_FORCE_COPY_IN_RELEASE + if (GetArenaForAllocation() != nullptr) { + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + } +#endif // !PROTOBUF_FORCE_COPY_IN_RELEASE + return temp; +} +inline ::mozilla::cookieBanner::GoogleSOCSCookie_timeData* GoogleSOCSCookie::unsafe_arena_release_time() { + // @@protoc_insertion_point(field_release:mozilla.cookieBanner.GoogleSOCSCookie.time) + _impl_._has_bits_[0] &= ~0x00000002u; + ::mozilla::cookieBanner::GoogleSOCSCookie_timeData* temp = _impl_.time_; + _impl_.time_ = nullptr; + return temp; +} +inline ::mozilla::cookieBanner::GoogleSOCSCookie_timeData* GoogleSOCSCookie::_internal_mutable_time() { + _impl_._has_bits_[0] |= 0x00000002u; + if (_impl_.time_ == nullptr) { + auto* p = CreateMaybeMessage<::mozilla::cookieBanner::GoogleSOCSCookie_timeData>(GetArenaForAllocation()); + _impl_.time_ = p; + } + return _impl_.time_; +} +inline ::mozilla::cookieBanner::GoogleSOCSCookie_timeData* GoogleSOCSCookie::mutable_time() { + ::mozilla::cookieBanner::GoogleSOCSCookie_timeData* _msg = _internal_mutable_time(); + // @@protoc_insertion_point(field_mutable:mozilla.cookieBanner.GoogleSOCSCookie.time) + return _msg; +} +inline void GoogleSOCSCookie::set_allocated_time(::mozilla::cookieBanner::GoogleSOCSCookie_timeData* time) { + ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation(); + if (message_arena == nullptr) { + delete _impl_.time_; + } + if (time) { + ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena = + ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(time); + if (message_arena != submessage_arena) { + time = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage( + message_arena, time, submessage_arena); + } + _impl_._has_bits_[0] |= 0x00000002u; + } else { + _impl_._has_bits_[0] &= ~0x00000002u; + } + _impl_.time_ = time; + // @@protoc_insertion_point(field_set_allocated:mozilla.cookieBanner.GoogleSOCSCookie.time) +} + +#ifdef __GNUC__ + #pragma GCC diagnostic pop +#endif // __GNUC__ +// ------------------------------------------------------------------- + +// ------------------------------------------------------------------- + + +// @@protoc_insertion_point(namespace_scope) + +} // namespace cookieBanner +} // namespace mozilla + +// @@protoc_insertion_point(global_scope) + +#include <google/protobuf/port_undef.inc> +#endif // GOOGLE_PROTOBUF_INCLUDED_GOOGLE_PROTOBUF_INCLUDED_cookieBanner_2eproto diff --git a/toolkit/components/cookiebanners/cookieBanner.proto b/toolkit/components/cookiebanners/cookieBanner.proto new file mode 100644 index 0000000000..ae20fed484 --- /dev/null +++ b/toolkit/components/cookiebanners/cookieBanner.proto @@ -0,0 +1,25 @@ +syntax = "proto2"; + +option optimize_for = LITE_RUNTIME; + +package mozilla.cookieBanner; + +// Represent the SOCS cookie used to store GDPR choice on the google search. +message GoogleSOCSCookie { + // The GDPR choice. 1 means reject All and 2 means Accept or Custom. + required uint32 gdpr_choice = 1; + message extraData { + required uint32 unused1 = 1; + // The value Represents where does the consent is set. We will use this + // value to differentiate Accept and Custom. + required string platform = 2; + required string region = 3; + required uint32 unused2 = 4; + } + required extraData data = 2; + + message timeData { + required uint64 timeStamp = 1; + } + required timeData time = 3; +} diff --git a/toolkit/components/cookiebanners/metrics.yaml b/toolkit/components/cookiebanners/metrics.yaml index 8dabab39f3..98f86fd4a7 100644 --- a/toolkit/components/cookiebanners/metrics.yaml +++ b/toolkit/components/cookiebanners/metrics.yaml @@ -210,6 +210,72 @@ cookie.banners: - pbz@mozilla.com - tihuang@mozilla.com expires: 128 + google_gdpr_choice_cookie: + type: labeled_string + description: > + Records the GDPR choice on Google Search based on the "SOCS" cookie of the + Google Search domains. The value could be "Accept", "Reject" or "Custom". + We use the label to record different choices on different Google domains. + We only collect this if the default search engine is Google. + bugs: + - https://bugzilla.mozilla.org/1874741 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1874741#c10 + data_sensitivity: + - stored_content + notification_emails: + - pbz@mozilla.com + - tihuang@mozilla.com + expires: never + google_gdpr_choice_cookie_event: + type: event + description: > + Recorded whenever a GDPR choice is made on a Google Search page. We assess + the "SOCS" cookie to know the GDPR choice. + bugs: + - https://bugzilla.mozilla.org/1874741 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1874741#c10 + data_sensitivity: + - stored_content + notification_emails: + - pbz@mozilla.com + - tihuang@mozilla.com + expires: never + extra_keys: + search_domain: + description: The Google search domain where the GDPR choice was made. + type: string + choice: + description: > + The GDPR choice. The value could be "Accept", "Reject" or "Custom". + type: string + region: + description: > + The region where the GDPR consent is made. This is based on the IP + location. + type: string + google_gdpr_choice_cookie_event_pbm: + type: event + description: > + Recorded whenever a GDPR choice is made on a Google Search page on private + browsing windows. We assess the "SOCS" cookie to know the GDPR choice. + bugs: + - https://bugzilla.mozilla.org/1874741 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1874741#c10 + data_sensitivity: + - stored_content + notification_emails: + - pbz@mozilla.com + - tihuang@mozilla.com + expires: never + extra_keys: + choice: + description: > + The GDPR choice. The value could be "Accept", "Reject" or "Custom". + type: string + cookie.banners.click: handle_duration: type: timing_distribution diff --git a/toolkit/components/cookiebanners/moz.build b/toolkit/components/cookiebanners/moz.build index 774c1dd96f..7dfbdb2c6b 100644 --- a/toolkit/components/cookiebanners/moz.build +++ b/toolkit/components/cookiebanners/moz.build @@ -14,11 +14,15 @@ XPIDL_SOURCES += [ "nsICookieBannerListService.idl", "nsICookieBannerRule.idl", "nsICookieBannerService.idl", + "nsICookieBannerTelemetryService.idl", "nsICookieRule.idl", ] XPIDL_MODULE = "toolkit_cookiebanners" +DEFINES["GOOGLE_PROTOBUF_NO_RTTI"] = True +DEFINES["GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER"] = True + EXTRA_JS_MODULES += [ "CookieBannerListService.sys.mjs", ] @@ -31,15 +35,18 @@ EXPORTS.mozilla += [ "nsClickRule.h", "nsCookieBannerRule.h", "nsCookieBannerService.h", + "nsCookieBannerTelemetryService.h", "nsCookieInjector.h", "nsCookieRule.h", ] UNIFIED_SOURCES += [ + "cookieBanner.pb.cc", "CookieBannerDomainPrefService.cpp", "nsClickRule.cpp", "nsCookieBannerRule.cpp", "nsCookieBannerService.cpp", + "nsCookieBannerTelemetryService.cpp", "nsCookieInjector.cpp", "nsCookieRule.cpp", ] diff --git a/toolkit/components/cookiebanners/nsCookieBannerTelemetryService.cpp b/toolkit/components/cookiebanners/nsCookieBannerTelemetryService.cpp new file mode 100644 index 0000000000..23906650f4 --- /dev/null +++ b/toolkit/components/cookiebanners/nsCookieBannerTelemetryService.cpp @@ -0,0 +1,392 @@ +/* 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 "nsCookieBannerTelemetryService.h" + +#include "cookieBanner.pb.h" + +#include "mozilla/Base64.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/glean/GleanMetrics.h" +#include "mozilla/OriginAttributes.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/Logging.h" +#include "Cookie.h" +#include "nsCRT.h" +#include "nsError.h" +#include "nsICookie.h" +#include "nsICookieManager.h" +#include "nsICookieNotification.h" +#include "nsTHashSet.h" +#include "nsIObserverService.h" +#include "nsIScriptSecurityManager.h" +#include "nsISearchService.h" +#include "nsServiceManagerUtils.h" +#include "nsStringFwd.h" +#include "nsTArray.h" + +namespace mozilla { + +NS_IMPL_ISUPPORTS(nsCookieBannerTelemetryService, + nsICookieBannerTelemetryService, nsIObserver) + +static LazyLogModule gCookieBannerTelemetryLog( + "nsCookieBannerTelemetryService"); + +static StaticRefPtr<nsCookieBannerTelemetryService> + sCookieBannerTelemetryServiceSingleton; + +// A hash set used to tell whether a base domain is a Google domain. +static StaticAutoPtr<nsTHashSet<nsCString>> sGoogleDomainsSet; + +namespace { + +// A helper function that decodes Google's SOCS cookie and returns the GDPR +// choice and the region. The choice and be either "Accept", "Reject", or +// "Custom". +nsresult DecodeSOCSGoogleCookie(const nsACString& aCookie, nsACString& aChoice, + nsACString& aRegion) { + aChoice.Truncate(); + aRegion.Truncate(); + + FallibleTArray<uint8_t> decoded; + nsresult rv = + Base64URLDecode(aCookie, Base64URLDecodePaddingPolicy::Ignore, decoded); + NS_ENSURE_SUCCESS(rv, rv); + + std::string buf(reinterpret_cast<const char*>(decoded.Elements()), + decoded.Length()); + cookieBanner::GoogleSOCSCookie socs; + + if (!socs.ParseFromString(buf)) { + return NS_ERROR_CANNOT_CONVERT_DATA; + } + + aRegion.Assign(socs.data().region().c_str(), socs.data().region().length()); + + // The first field represents the gdpr choice, 1 means "Reject" and 2 means + // either "Accept" or "Custom". We need to check a following field to decide. + if (socs.gdpr_choice() == 1) { + aChoice.AssignLiteral("Reject"); + return NS_OK; + } + + // The platform field represents where does this GDPR consent is selected. The + // value can be either "gws_*" or "boq_identityfrontenduiserver_*". If the + // field value starts with "gws_", it means the consent is from the Google + // search page. Otherwise, it's from the consent.google.com, which is used for + // the custom setting. + std::string prefix = "gws_"; + + if (socs.data().platform().compare(0, prefix.length(), prefix) == 0) { + aChoice.AssignLiteral("Accept"); + return NS_OK; + } + + aChoice.AssignLiteral("Custom"); + return NS_OK; +} + +} // anonymous namespace + +// static +already_AddRefed<nsCookieBannerTelemetryService> +nsCookieBannerTelemetryService::GetSingleton() { + MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug, ("GetSingleton.")); + + if (!sCookieBannerTelemetryServiceSingleton) { + sCookieBannerTelemetryServiceSingleton = + new nsCookieBannerTelemetryService(); + + RunOnShutdown([] { + DebugOnly<nsresult> rv = + sCookieBannerTelemetryServiceSingleton->Shutdown(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "nsCookieBannerTelemetryService::Shutdown failed."); + + sCookieBannerTelemetryServiceSingleton = nullptr; + }); + } + + return do_AddRef(sCookieBannerTelemetryServiceSingleton); +} + +nsresult nsCookieBannerTelemetryService::Init() { + MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug, ("Init.")); + if (mIsInitialized) { + return NS_OK; + } + + mIsInitialized = true; + + nsCOMPtr<nsIObserverService> obsSvc = mozilla::services::GetObserverService(); + NS_ENSURE_TRUE(obsSvc, NS_ERROR_FAILURE); + + nsresult rv = obsSvc->AddObserver(this, "browser-search-service", false); + NS_ENSURE_SUCCESS(rv, rv); + + rv = obsSvc->AddObserver(this, "idle-daily", false); + NS_ENSURE_SUCCESS(rv, rv); + + rv = obsSvc->AddObserver(this, "cookie-changed", false); + NS_ENSURE_SUCCESS(rv, rv); + + rv = obsSvc->AddObserver(this, "private-cookie-changed", false); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult nsCookieBannerTelemetryService::Shutdown() { + MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug, ("Shutdown.")); + + if (!mIsInitialized) { + return NS_OK; + } + + mIsInitialized = false; + + sGoogleDomainsSet = nullptr; + + nsCOMPtr<nsIObserverService> obsSvc = mozilla::services::GetObserverService(); + NS_ENSURE_TRUE(obsSvc, NS_ERROR_FAILURE); + + nsresult rv = obsSvc->RemoveObserver(this, "browser-search-service"); + NS_ENSURE_SUCCESS(rv, rv); + + rv = obsSvc->RemoveObserver(this, "idle-daily"); + NS_ENSURE_SUCCESS(rv, rv); + + rv = obsSvc->RemoveObserver(this, "cookie-changed"); + NS_ENSURE_SUCCESS(rv, rv); + + rv = obsSvc->RemoveObserver(this, "private-cookie-changed"); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP +nsCookieBannerTelemetryService::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) { + if (nsCRT::strcmp(aTopic, "profile-after-change") == 0) { + MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug, + ("Observe profile-after-change")); + + return Init(); + } + + if (nsCRT::strcmp(aTopic, "idle-daily") == 0) { + MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug, ("idle-daily")); + + return MaybeReportGoogleGDPRChoiceTelemetry(); + } + + if (nsCRT::strcmp(aTopic, "browser-search-service") == 0 && + nsDependentString(aData).EqualsLiteral("init-complete")) { + MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug, + ("Observe browser-search-service::init-complete.")); + mIsSearchServiceInitialized = true; + + return MaybeReportGoogleGDPRChoiceTelemetry(); + } + + if (nsCRT::strcmp(aTopic, "cookie-changed") == 0 || + nsCRT::strcmp(aTopic, "private-cookie-changed") == 0) { + MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug, ("Observe %s", aTopic)); + + nsCOMPtr<nsICookieNotification> notification = do_QueryInterface(aSubject); + NS_ENSURE_TRUE(notification, NS_ERROR_FAILURE); + + if (notification->GetAction() != nsICookieNotification::COOKIE_ADDED && + notification->GetAction() != nsICookieNotification::COOKIE_CHANGED) { + return NS_OK; + } + + nsCOMPtr<nsICookie> cookie; + nsresult rv = notification->GetCookie(getter_AddRefs(cookie)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString name; + rv = cookie->GetName(name); + NS_ENSURE_SUCCESS(rv, rv); + + // Bail out early if this is not a SOCS cookie. + if (!name.EqualsLiteral("SOCS")) { + return NS_OK; + } + + // A "SOCS" cookie is added. We record the GDPR choice as well as an event + // telemetry for recording a choice has been made at the moment. + return MaybeReportGoogleGDPRChoiceTelemetry(cookie, true); + } + + return NS_OK; +} + +nsresult nsCookieBannerTelemetryService::MaybeReportGoogleGDPRChoiceTelemetry( + nsICookie* aCookie, bool aReportEvent) { + MOZ_ASSERT(mIsInitialized); + nsresult rv; + + // Don't report the telemetry if the search service is not yet initialized. + if (!mIsSearchServiceInitialized) { + MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug, + ("Search Service is not yet initialized.")); + return NS_OK; + } + + // We only collect Google GDPR choice if Google is the default search engine. + nsCOMPtr<nsISearchService> searchService( + do_GetService("@mozilla.org/browser/search-service;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISearchEngine> engine; + rv = searchService->GetDefaultEngine(getter_AddRefs(engine)); + NS_ENSURE_SUCCESS(rv, rv); + + // Bail out early if no default search engine is available. This could happen + // if no search engine is shipped with the Gecko. + if (!engine) { + MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug, + ("No default search engine is available.")); + return NS_OK; + } + + nsAutoString id; + rv = engine->GetId(id); + NS_ENSURE_SUCCESS(rv, rv); + + // Bail out early if the default search engine is not Google. + if (!id.EqualsLiteral("google@search.mozilla.orgdefault")) { + return NS_OK; + } + + // Build up the Google domain set from the alternative Google search domains + if (!sGoogleDomainsSet) { + sGoogleDomainsSet = new nsTHashSet<nsCString>(); + + nsTArray<nsCString> googleDomains; + rv = searchService->GetAlternateDomains("www.google.com"_ns, googleDomains); + NS_ENSURE_SUCCESS(rv, rv); + + for (const auto& domain : googleDomains) { + // We need to trim down the preceding "www" to get the host because the + // alternate domains provided by search service always have leading "www". + // However, the "SOCS" cookie is always set for all subdomains, e.g. + // ".google.com". Therefore, we need to trim down "www" to match the host + // of the cookie. + NS_ENSURE_TRUE(domain.Length() > 3, NS_ERROR_FAILURE); + sGoogleDomainsSet->Insert(Substring(domain, 3, domain.Length() - 3)); + } + } + + nsTArray<RefPtr<nsICookie>> cookies; + if (aCookie) { + const auto& attrs = aCookie->AsCookie().OriginAttributesRef(); + + // We only report cookies for the default originAttributes or private + // browsing mode. + if (attrs.mPrivateBrowsingId != + nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID || + attrs == OriginAttributes()) { + cookies.AppendElement(RefPtr<nsICookie>(aCookie)); + } + } else { + // If no cookie is given, we will iterate all cookies under Google Search + // domains. + nsCOMPtr<nsICookieManager> cookieManager = + do_GetService("@mozilla.org/cookiemanager;1"); + if (!cookieManager) { + return NS_ERROR_FAILURE; + } + + for (const auto& domain : *sGoogleDomainsSet) { + // Getting Google Search cookies only for normal windows. We will exclude + // cookies of non-default originAttributes, like cookies from containers. + // + // Note that we need to trim the leading "." from the domain here. + nsTArray<RefPtr<nsICookie>> googleCookies; + rv = cookieManager->GetCookiesWithOriginAttributes( + u"{ \"privateBrowsingId\": 0, \"userContextId\": 0 }"_ns, + Substring(domain, 1, domain.Length() - 1), googleCookies); + NS_ENSURE_SUCCESS(rv, rv); + + cookies.AppendElements(googleCookies); + } + } + + for (const auto& cookie : cookies) { + nsAutoCString name; + rv = cookie->GetName(name); + NS_ENSURE_SUCCESS(rv, rv); + + if (!name.EqualsLiteral("SOCS")) { + continue; + } + + nsAutoCString host; + rv = cookie->GetHost(host); + NS_ENSURE_SUCCESS(rv, rv); + + if (!sGoogleDomainsSet->Contains(host)) { + continue; + } + + nsAutoCString value; + rv = cookie->GetValue(value); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString choice; + nsAutoCString region; + rv = DecodeSOCSGoogleCookie(value, choice, region); + NS_ENSURE_SUCCESS(rv, rv); + + bool isPrivateBrowsing = + cookie->AsCookie().OriginAttributesRef().mPrivateBrowsingId != + nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID; + + MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug, + ("Record the Google GDPR choice %s on the host %s in region %s for " + "the %s window", + choice.get(), host.get(), region.get(), + isPrivateBrowsing ? "private" : "normal")); + + // We rely on the dynamical labelled string which can send most 16 different + // labels per report. In average cases, 16 labels is sufficient because we + // expect a user might have only 1 or 2 "SOCS" cookies on different Google + // Search domains. + // + // Note that we only report event telemetry for private browsing windows + // because the private session is ephemeral. People can change GDPR + // choice across different private sessions, so it's hard to collect the + // state correctly. + if (!isPrivateBrowsing) { + glean::cookie_banners::google_gdpr_choice_cookie.Get(host).Set(choice); + } + + if (aReportEvent) { + if (isPrivateBrowsing) { + glean::cookie_banners::GoogleGdprChoiceCookieEventPbmExtra extra = { + .choice = Some(choice), + }; + glean::cookie_banners::google_gdpr_choice_cookie_event_pbm.Record( + Some(extra)); + } else { + glean::cookie_banners::GoogleGdprChoiceCookieEventExtra extra = { + .choice = Some(choice), + .region = Some(region), + .searchDomain = Some(host), + }; + glean::cookie_banners::google_gdpr_choice_cookie_event.Record( + Some(extra)); + } + } + } + + return NS_OK; +} + +} // namespace mozilla diff --git a/toolkit/components/cookiebanners/nsCookieBannerTelemetryService.h b/toolkit/components/cookiebanners/nsCookieBannerTelemetryService.h new file mode 100644 index 0000000000..1236343ad9 --- /dev/null +++ b/toolkit/components/cookiebanners/nsCookieBannerTelemetryService.h @@ -0,0 +1,47 @@ +/* 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/. */ + +#ifndef mozilla_nsCookieBannerTelemetryService_h__ +#define mozilla_nsCookieBannerTelemetryService_h__ + +#include "nsICookieBannerTelemetryService.h" + +#include "nsIObserver.h" + +class nsICookie; + +namespace mozilla { +class nsCookieBannerTelemetryService final + : public nsICookieBannerTelemetryService, + public nsIObserver { + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + NS_DECL_NSICOOKIEBANNERTELEMETRYSERVICE + + public: + static already_AddRefed<nsCookieBannerTelemetryService> GetSingleton(); + + private: + nsCookieBannerTelemetryService() = default; + ~nsCookieBannerTelemetryService() = default; + + bool mIsInitialized = false; + bool mIsSearchServiceInitialized = false; + + [[nodiscard]] nsresult Init(); + + [[nodiscard]] nsresult Shutdown(); + + // Record the telemetry regarding the GDPR choice on Google Search domains. + // We only record if the default search engine is Google. When passing no + // cookie, we will walk through all cookies under Google Search domains. + // Otherwise, we will report the GDPR choice according to the given cookie. + [[nodiscard]] nsresult MaybeReportGoogleGDPRChoiceTelemetry( + nsICookie* aCookie = nullptr, bool aReportEvent = false); +}; + +} // namespace mozilla + +#endif diff --git a/toolkit/components/cookiebanners/nsICookieBannerRule.idl b/toolkit/components/cookiebanners/nsICookieBannerRule.idl index 0ad557fe54..5ed456c90c 100644 --- a/toolkit/components/cookiebanners/nsICookieBannerRule.idl +++ b/toolkit/components/cookiebanners/nsICookieBannerRule.idl @@ -85,7 +85,7 @@ interface nsICookieBannerRule : nsISupports { * aOptIn - The CSS selector for selecting the opt-in banner button */ void addClickRule(in ACString aPresence, - [optional] in bool aSkipPresenceVisibilityCheck, + [optional] in boolean aSkipPresenceVisibilityCheck, [optional] in nsIClickRule_RunContext aRunContext, [optional] in ACString aHide, [optional] in ACString aOptOut, diff --git a/toolkit/components/cookiebanners/nsICookieBannerService.idl b/toolkit/components/cookiebanners/nsICookieBannerService.idl index b58e726310..4d8db6f94e 100644 --- a/toolkit/components/cookiebanners/nsICookieBannerService.idl +++ b/toolkit/components/cookiebanners/nsICookieBannerService.idl @@ -58,13 +58,13 @@ interface nsICookieBannerService : nsISupports { * this will return none, only reject rules or accept rules if there is no * reject rule available. */ - Array<nsICookieRule> getCookiesForURI(in nsIURI aURI, in bool aIsPrivateBrowsing); + Array<nsICookieRule> getCookiesForURI(in nsIURI aURI, in boolean aIsPrivateBrowsing); /** * Look up the click rules for a given domain. */ Array<nsIClickRule> getClickRulesForDomain(in ACString aDomain, - in bool aIsTopLevel); + in boolean aIsTopLevel); /** * Insert a cookie banner rule for a domain. If there was previously a rule @@ -131,28 +131,28 @@ interface nsICookieBannerService : nsISupports { * this session. */ boolean shouldStopBannerClickingForSite(in ACString aSite, - in bool aIsTopLevel, - in bool aIsPrivate); + in boolean aIsTopLevel, + in boolean aIsPrivate); /** * Mark that the cookie banner handling code was executed for the given site * for this session. */ void markSiteExecuted(in ACString aSite, - in bool aIsTopLevel, - in bool aIsPrivate); + in boolean aIsTopLevel, + in boolean aIsPrivate); /* * Remove the executed record for a given site under the private browsing * session or the normal session. */ - void removeExecutedRecordForSite(in ACString aSite, in bool aIsPrivate); + void removeExecutedRecordForSite(in ACString aSite, in boolean aIsPrivate); /** * Remove all the record of sites where cookie banner handling has been * executed under the private browsing session or normal session. */ - void removeAllExecutedRecords(in bool aIsPrivate); + void removeAllExecutedRecords(in boolean aIsPrivate); /** * Clears the in-memory set that we use to maintain the domains that we have diff --git a/toolkit/components/cookiebanners/nsICookieBannerTelemetryService.idl b/toolkit/components/cookiebanners/nsICookieBannerTelemetryService.idl new file mode 100644 index 0000000000..9c027bf88e --- /dev/null +++ b/toolkit/components/cookiebanners/nsICookieBannerTelemetryService.idl @@ -0,0 +1,13 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * 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 "nsISupports.idl" + +/** + * Service singleton which collect cookie banner telemetry. + */ +[scriptable, builtinclass, uuid(56197e18-d144-45b5-9f77-84102f064462)] +interface nsICookieBannerTelemetryService : nsISupports {}; diff --git a/toolkit/components/cookiebanners/test/browser/browser.toml b/toolkit/components/cookiebanners/test/browser/browser.toml index 69728540d9..a3f8a96489 100644 --- a/toolkit/components/cookiebanners/test/browser/browser.toml +++ b/toolkit/components/cookiebanners/test/browser/browser.toml @@ -36,6 +36,8 @@ support-files = [ ["browser_bannerClicking_visibilityOverride.js"] support-files = ["file_banner_invisible.html"] +["browser_cookiebanner_gdpr_telemetry.js"] + ["browser_cookiebanner_telemetry.js"] support-files = ["file_iframe_banner.html"] diff --git a/toolkit/components/cookiebanners/test/browser/browser_cookiebanner_gdpr_telemetry.js b/toolkit/components/cookiebanners/test/browser/browser_cookiebanner_gdpr_telemetry.js new file mode 100644 index 0000000000..2d6f3343b8 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_cookiebanner_gdpr_telemetry.js @@ -0,0 +1,189 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { SiteDataTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SiteDataTestUtils.sys.mjs" +); + +const TEST_CASES = [ + { + value: "CAISHAgBEhJnd3NfMjAyNDAzMjUtMF9SQzEaAmRlIAEaBgiAw42wBg", + expected: "Accept", + region: "de", + host: ".google.de", + }, + { + value: "CAESHAgBEhJnd3NfMjAyNDAzMjUtMF9SQzEaAmRlIAEaBgiAw42wBg", + expected: "Reject", + region: "de", + host: ".google.de", + }, + { + value: + "CAISNQgBEitib3FfaWRlbnRpdHlmcm9udGVuZHVpc2VydmVyXzIwMjQwMzI0LjA4X3AwGgJkZSADGgYIgMONsAY", + expected: "Custom", + region: "de", + host: ".google.de", + }, + { + value: "CAISHAgBEhJnd3NfMjAyNDAzMjUtMF9SQzEaAmVuIAEaBgiAw42wBg", + expected: "Accept", + region: "en", + host: ".google.com", + }, +]; + +add_setup(async function () { + registerCleanupFunction(async () => { + // Clear cookies that have been set during testing. + await SiteDataTestUtils.clear(); + }); +}); + +add_task(async function test_google_gdpr_telemetry() { + // Clear telemetry before starting telemetry test. + Services.fog.testResetFOG(); + + for (let test of TEST_CASES) { + // Add a Google SOCS cookie to trigger recording telemetry. + SiteDataTestUtils.addToCookies({ + name: "SOCS", + value: test.value, + host: test.host, + path: "/", + }); + + // Verify if the google GDPR choice cookie telemetry is recorded properly. + is( + Glean.cookieBanners.googleGdprChoiceCookie[test.host].testGetValue(), + test.expected, + "The Google GDPR telemetry is recorded properly." + ); + + // Verify the the event telemetry is recorded properly. + let events = Glean.cookieBanners.googleGdprChoiceCookieEvent.testGetValue(); + let event = events[events.length - 1]; + + is( + event.extra.search_domain, + test.host, + "The Google GDPR event telemetry records the search domain properly." + ); + + is( + event.extra.choice, + test.expected, + "The Google GDPR event telemetry records the choice properly." + ); + + is( + event.extra.region, + test.region, + "The Google GDPR event telemetry records the region properly." + ); + } + + is( + Glean.cookieBanners.googleGdprChoiceCookieEvent.testGetValue().length, + TEST_CASES.length, + "The number of events is expected." + ); +}); + +add_task(async function test_invalid_google_socs_cookies() { + // Clear telemetry before starting telemetry test. + Services.fog.testResetFOG(); + + // Add an invalid Google SOCS cookie which is not Base64 encoding. + SiteDataTestUtils.addToCookies({ + name: "SOCS", + value: "invalid", + host: ".google.com", + path: "/", + }); + + // Ensure no GDPR telemetry is recorded. + is( + Glean.cookieBanners.googleGdprChoiceCookie[".google.com"].testGetValue(), + null, + "No Google GDPR telemetry is recorded." + ); + + is( + Glean.cookieBanners.googleGdprChoiceCookieEvent.testGetValue(), + null, + "No event telemetry is recorded." + ); + + // Add an invalid Google SOCS cookie which is Base64 encoding but in wrong + // protobuf format. + SiteDataTestUtils.addToCookies({ + name: "SOCS", + value: "CAISAggBGgYIgMONsAY", + host: ".google.com", + path: "/", + }); + + // Ensure no GDPR telemetry is recorded. + is( + Glean.cookieBanners.googleGdprChoiceCookie[".google.com"].testGetValue(), + null, + "No Google GDPR telemetry is recorded." + ); + + is( + Glean.cookieBanners.googleGdprChoiceCookieEvent.testGetValue(), + null, + "No event telemetry is recorded." + ); +}); + +add_task(async function test_google_gdpr_telemetry_pbm() { + // Clear telemetry before starting telemetry test. + Services.fog.testResetFOG(); + + for (let test of TEST_CASES) { + // Add a private Google SOCS cookie to trigger recording telemetry. + SiteDataTestUtils.addToCookies({ + name: "SOCS", + value: test.value, + host: test.host, + path: "/", + originAttributes: { privateBrowsingId: 1 }, + }); + + // Ensure we don't record google GDPR choice cookie telemetry. + is( + Glean.cookieBanners.googleGdprChoiceCookie[test.host].testGetValue(), + null, + "No Google GDPR telemetry is recorded." + ); + + // Verify the the event telemetry for PBM is recorded properly. + let events = + Glean.cookieBanners.googleGdprChoiceCookieEventPbm.testGetValue(); + let event = events[events.length - 1]; + + is( + event.extra.choice, + test.expected, + "The Google GDPR event telemetry records the choice properly." + ); + + is(event.extra.search_domain, undefined, "No search domain is recorded."); + is(event.extra.region, undefined, "No region is recorded."); + } + + is( + Glean.cookieBanners.googleGdprChoiceCookieEventPbm.testGetValue().length, + TEST_CASES.length, + "The number of events is expected." + ); + + // We need to notify the "last-pb-context-exited" tp explicitly remove private + // cookies. Otherwise, the remaining private cookies could affect following + // tests. The SiteDataTestUtils.clear() doesn't clear for private cookies. + Services.obs.notifyObservers(null, "last-pb-context-exited"); +}); diff --git a/toolkit/components/cookiebanners/test/browser/browser_cookiebanner_telemetry.js b/toolkit/components/cookiebanners/test/browser/browser_cookiebanner_telemetry.js index 7366ae2ab6..8ac30770cb 100644 --- a/toolkit/components/cookiebanners/test/browser/browser_cookiebanner_telemetry.js +++ b/toolkit/components/cookiebanners/test/browser/browser_cookiebanner_telemetry.js @@ -86,7 +86,7 @@ async function reloadBrowser(browser, url) { let reloaded = BrowserTestUtils.browserLoaded(browser, false, url); // Reload as a user. - window.BrowserReload(); + window.BrowserCommands.reload(); await reloaded; } diff --git a/toolkit/components/corroborator/Corroborate.sys.mjs b/toolkit/components/corroborator/Corroborate.sys.mjs deleted file mode 100644 index 51b447726c..0000000000 --- a/toolkit/components/corroborator/Corroborate.sys.mjs +++ /dev/null @@ -1,53 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; - -const lazy = {}; - -XPCOMUtils.defineLazyServiceGetters(lazy, { - gCertDB: ["@mozilla.org/security/x509certdb;1", "nsIX509CertDB"], -}); - -/** - * Tools for verifying internal files in Mozilla products. - */ -export const Corroborate = { - async init() {}, - - /** - * Verify signed state of arbitrary JAR file. Currently only JAR files signed - * with Mozilla-internal keys are supported. - * - * @argument file - an nsIFile pointing to the JAR to verify. - * - * @returns {Promise} - resolves true if file exists and is valid, false otherwise. - * Never rejects. - */ - verifyJar(file) { - let root = Ci.nsIX509CertDB.AddonsPublicRoot; - let expectedOrganizationalUnit = "Mozilla Components"; - - return new Promise(resolve => { - lazy.gCertDB.openSignedAppFileAsync( - root, - file, - (rv, _zipReader, signatureInfos) => { - // aSignatureInfos is an array of nsIAppSignatureInfo. - // This implementation could be modified to iterate through the array to - // determine if one or all of the verified signatures used a satisfactory - // algorithm and signing certificate. - // For now, though, it maintains existing behavior by inspecting the - // first signing certificate encountered. - resolve( - Components.isSuccessCode(rv) && - signatureInfos.length && - signatureInfos[0].signerCert.organizationalUnit == - expectedOrganizationalUnit - ); - } - ); - }); - }, -}; diff --git a/toolkit/components/corroborator/test/xpcshell/data/privileged.xpi b/toolkit/components/corroborator/test/xpcshell/data/privileged.xpi Binary files differdeleted file mode 100644 index c22acaacd2..0000000000 --- a/toolkit/components/corroborator/test/xpcshell/data/privileged.xpi +++ /dev/null diff --git a/toolkit/components/corroborator/test/xpcshell/data/signed-amo.xpi b/toolkit/components/corroborator/test/xpcshell/data/signed-amo.xpi Binary files differdeleted file mode 100644 index e2ba7d6fd8..0000000000 --- a/toolkit/components/corroborator/test/xpcshell/data/signed-amo.xpi +++ /dev/null diff --git a/toolkit/components/corroborator/test/xpcshell/data/signed-components.xpi b/toolkit/components/corroborator/test/xpcshell/data/signed-components.xpi Binary files differdeleted file mode 100644 index f27ac1fb73..0000000000 --- a/toolkit/components/corroborator/test/xpcshell/data/signed-components.xpi +++ /dev/null diff --git a/toolkit/components/corroborator/test/xpcshell/data/signed-privileged.xpi b/toolkit/components/corroborator/test/xpcshell/data/signed-privileged.xpi Binary files differdeleted file mode 100644 index c22acaacd2..0000000000 --- a/toolkit/components/corroborator/test/xpcshell/data/signed-privileged.xpi +++ /dev/null diff --git a/toolkit/components/corroborator/test/xpcshell/data/unsigned.xpi b/toolkit/components/corroborator/test/xpcshell/data/unsigned.xpi Binary files differdeleted file mode 100644 index 9e10be5db3..0000000000 --- a/toolkit/components/corroborator/test/xpcshell/data/unsigned.xpi +++ /dev/null diff --git a/toolkit/components/corroborator/test/xpcshell/test_verify_jar.js b/toolkit/components/corroborator/test/xpcshell/test_verify_jar.js deleted file mode 100644 index c1c98f4485..0000000000 --- a/toolkit/components/corroborator/test/xpcshell/test_verify_jar.js +++ /dev/null @@ -1,31 +0,0 @@ -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -const { Corroborate } = ChromeUtils.importESModule( - "resource://gre/modules/Corroborate.sys.mjs" -); - -add_task(async function test_various_jars() { - let result = await Corroborate.verifyJar(do_get_file("data/unsigned.xpi")); - equal(result, false, "unsigned files do not verify"); - - result = await Corroborate.verifyJar(do_get_file("data/signed-amo.xpi")); - equal(result, false, "AMO signed files do not verify"); - - result = await Corroborate.verifyJar( - do_get_file("data/signed-privileged.xpi") - ); - equal(result, false, "Privileged signed files do not verify"); - - let missingFile = do_get_file("data"); - missingFile.append("missing.xpi"); - - result = await Corroborate.verifyJar(missingFile); - equal(result, false, "Missing (but expected) files do not verify"); - - result = await Corroborate.verifyJar( - do_get_file("data/signed-components.xpi") - ); - equal(result, true, "Components signed files do verify"); -}); diff --git a/toolkit/components/corroborator/test/xpcshell/xpcshell.toml b/toolkit/components/corroborator/test/xpcshell/xpcshell.toml deleted file mode 100644 index d30b8dfb17..0000000000 --- a/toolkit/components/corroborator/test/xpcshell/xpcshell.toml +++ /dev/null @@ -1,5 +0,0 @@ -[DEFAULT] -tags = "corroborator" -support-files = ["data/**"] - -["test_verify_jar.js"] diff --git a/toolkit/components/crashes/CrashService.sys.mjs b/toolkit/components/crashes/CrashService.sys.mjs index cedbca53b9..3634327b2e 100644 --- a/toolkit/components/crashes/CrashService.sys.mjs +++ b/toolkit/components/crashes/CrashService.sys.mjs @@ -66,7 +66,7 @@ function runMinidumpAnalyzer(minidumpPath, allThreads) { args.unshift("--full"); } - process.runAsync(args, args.length, (subject, topic, data) => { + process.runAsync(args, args.length, (subject, topic) => { switch (topic) { case "process-finished": gRunningProcesses.delete(process); @@ -211,7 +211,7 @@ CrashService.prototype = Object.freeze({ await blocker; }, - observe(subject, topic, data) { + observe(subject, topic) { switch (topic) { case "profile-after-change": // Side-effect is the singleton is instantiated. diff --git a/toolkit/components/crashes/tests/xpcshell/test_crash_manager.js b/toolkit/components/crashes/tests/xpcshell/test_crash_manager.js index 2f77ea5105..e776b8f87a 100644 --- a/toolkit/components/crashes/tests/xpcshell/test_crash_manager.js +++ b/toolkit/components/crashes/tests/xpcshell/test_crash_manager.js @@ -824,7 +824,7 @@ add_task(async function test_glean_crash_ping() { // Test with additional fields submitted = false; - GleanPings.crash.testBeforeNextSubmit(reason => { + GleanPings.crash.testBeforeNextSubmit(() => { submitted = true; const MINUTES = new Date(DUMMY_DATE_2); MINUTES.setSeconds(0); diff --git a/toolkit/components/crashes/tests/xpcshell/test_crash_service.js b/toolkit/components/crashes/tests/xpcshell/test_crash_service.js index 63ff3343d6..6648230785 100644 --- a/toolkit/components/crashes/tests/xpcshell/test_crash_service.js +++ b/toolkit/components/crashes/tests/xpcshell/test_crash_service.js @@ -170,7 +170,7 @@ add_task(async function test_addCrash_quitting() { await setup(firstCrashId); let minidumpAnalyzerKilledPromise = new Promise((resolve, reject) => { - Services.obs.addObserver((subject, topic, data) => { + Services.obs.addObserver((subject, topic) => { if (topic === "test-minidump-analyzer-killed") { resolve(); } diff --git a/toolkit/components/crashes/tests/xpcshell/test_crash_store.js b/toolkit/components/crashes/tests/xpcshell/test_crash_store.js index 515aec86a0..3690c13922 100644 --- a/toolkit/components/crashes/tests/xpcshell/test_crash_store.js +++ b/toolkit/components/crashes/tests/xpcshell/test_crash_store.js @@ -2,7 +2,7 @@ * http://creativecommons.org/publicdomain/zero/1.0/ */ /* - * This file tests the CrashStore type in CrashManager.jsm. + * This file tests the CrashStore type in CrashManager.sys.mjs. */ "use strict"; @@ -257,7 +257,7 @@ add_task(async function test_add_mixed_types() { allAdd && s.addCrash(ptName, CRASH_TYPE_CRASH, ptName + "crash", new Date()); }, - (_, ptName) => { + _ => { allAdd = allAdd && s.addCrash( diff --git a/toolkit/components/crashmonitor/CrashMonitor.sys.mjs b/toolkit/components/crashmonitor/CrashMonitor.sys.mjs index c4dfdcbd83..a23446f45c 100644 --- a/toolkit/components/crashmonitor/CrashMonitor.sys.mjs +++ b/toolkit/components/crashmonitor/CrashMonitor.sys.mjs @@ -189,7 +189,7 @@ export var CrashMonitor = { * * Update checkpoint file for every new notification received. */ - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { this.writeCheckpoint(aTopic); if ( diff --git a/toolkit/components/crashmonitor/nsCrashMonitor.sys.mjs b/toolkit/components/crashmonitor/nsCrashMonitor.sys.mjs index 0e91fc0859..008b0e7bc8 100644 --- a/toolkit/components/crashmonitor/nsCrashMonitor.sys.mjs +++ b/toolkit/components/crashmonitor/nsCrashMonitor.sys.mjs @@ -14,7 +14,7 @@ CrashMonitor.prototype = { QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { switch (aTopic) { case "profile-after-change": MonitorAPI.init(); diff --git a/toolkit/components/crashmonitor/test/unit/test_register.js b/toolkit/components/crashmonitor/test/unit/test_register.js index 1957af4ee1..22d3721df8 100644 --- a/toolkit/components/crashmonitor/test/unit/test_register.js +++ b/toolkit/components/crashmonitor/test/unit/test_register.js @@ -4,7 +4,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ /** - * Test that CrashMonitor.jsm is correctly loaded from XPCOM component + * Test that CrashMonitor.sys.mjs is correctly loaded from XPCOM component */ add_task(function test_register() { let cm = Cc["@mozilla.org/toolkit/crashmonitor;1"].createInstance( diff --git a/toolkit/components/credentialmanagement/tests/browser/browser.toml b/toolkit/components/credentialmanagement/tests/browser/browser.toml index a7202c9190..e14e36b938 100644 --- a/toolkit/components/credentialmanagement/tests/browser/browser.toml +++ b/toolkit/components/credentialmanagement/tests/browser/browser.toml @@ -10,9 +10,7 @@ scheme = "https" support-files = ["custom.svg"] ["browser_account_dialog.js"] -fail-if = ["a11y_checks"] # Bug 1854509 clicked identity-credential-list-item may not be focusable ["browser_policy_dialog.js"] ["browser_provider_dialog.js"] -fail-if = ["a11y_checks"] # Bug 1854509 clicked identity-credential-list-item may not be focusable diff --git a/toolkit/components/ctypes/tests/chrome/xpcshellTestHarnessAdaptor.js b/toolkit/components/ctypes/tests/chrome/xpcshellTestHarnessAdaptor.js index 9e90b22561..24f0cb2602 100644 --- a/toolkit/components/ctypes/tests/chrome/xpcshellTestHarnessAdaptor.js +++ b/toolkit/components/ctypes/tests/chrome/xpcshellTestHarnessAdaptor.js @@ -83,7 +83,7 @@ FileFaker.prototype = { }, }; -function do_get_file(path, allowNonexistent) { +function do_get_file(path) { if (!_WORKINGDIR_) { do_throw("No way to fake files if working directory is unknown!"); } diff --git a/toolkit/components/ctypes/tests/unit/test_errno.js b/toolkit/components/ctypes/tests/unit/test_errno.js index 24dba0597b..8905ec3a18 100644 --- a/toolkit/components/ctypes/tests/unit/test_errno.js +++ b/toolkit/components/ctypes/tests/unit/test_errno.js @@ -2,11 +2,10 @@ var ctypes = ctypes; function run_test() { - // Launch the test with regular loading of ctypes.jsm + // Launch the test with regular loading of ctypes.sys.mjs main_test(); - // Relaunch the test with exotic loading of ctypes.jsm - Cu.unload("resource://gre/modules/ctypes.jsm"); + // Relaunch the test with exotic loading of ctypes.sys.mjs let scope = ChromeUtils.importESModule( "resource://gre/modules/ctypes.sys.mjs" ); diff --git a/toolkit/components/ctypes/tests/unit/test_jsctypes.js b/toolkit/components/ctypes/tests/unit/test_jsctypes.js index 7227ec9925..66255d85b1 100644 --- a/toolkit/components/ctypes/tests/unit/test_jsctypes.js +++ b/toolkit/components/ctypes/tests/unit/test_jsctypes.js @@ -3470,7 +3470,7 @@ function run_string_tests(library) { Assert.equal(ptrValue(test_ansi_echo(null)), 0); } -function run_readstring_tests(library) { +function run_readstring_tests() { // ASCII decode test, "hello world" let ascii_string = ctypes.unsigned_char.array(12)(); ascii_string[0] = 0x68; diff --git a/toolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs b/toolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs index 15fa2193c6..00a5381133 100644 --- a/toolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs +++ b/toolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs @@ -296,7 +296,7 @@ EnterprisePoliciesManager.prototype = { }, // nsIObserver implementation - observe: function BG_observe(subject, topic, data) { + observe: function BG_observe(subject, topic) { switch (topic) { case "policies-startup": // Before the first set of policy callbacks runs, we must diff --git a/toolkit/components/enterprisepolicies/nsIEnterprisePolicies.idl b/toolkit/components/enterprisepolicies/nsIEnterprisePolicies.idl index 03c51af49f..24d4ae4dcf 100644 --- a/toolkit/components/enterprisepolicies/nsIEnterprisePolicies.idl +++ b/toolkit/components/enterprisepolicies/nsIEnterprisePolicies.idl @@ -15,7 +15,7 @@ interface nsIEnterprisePolicies : nsISupports readonly attribute short status; - bool isAllowed(in ACString feature); + boolean isAllowed(in ACString feature); /** * Get the active policies that have been successfully parsed. @@ -56,7 +56,7 @@ interface nsIEnterprisePolicies : nsISupports * * @returns A boolean - true of the addon may be installed. */ - bool mayInstallAddon(in jsval addon); + boolean mayInstallAddon(in jsval addon); /** * Uses install_sources to determine if an addon can be installed @@ -64,7 +64,7 @@ interface nsIEnterprisePolicies : nsISupports * * @returns A boolean - true of the addon may be installed. */ - bool allowedInstallSource(in nsIURI uri); + boolean allowedInstallSource(in nsIURI uri); /** * Uses ExemptDomainFileTypePairsFromFileTypeDownloadWarnings to determine * if a given file extension is exempted from executable behavior and @@ -72,5 +72,5 @@ interface nsIEnterprisePolicies : nsISupports * * @returns A boolean - true if the extension should be exempt. */ - bool isExemptExecutableExtension(in ACString url, in ACString extension); + boolean isExemptExecutableExtension(in ACString url, in ACString extension); }; diff --git a/toolkit/components/enterprisepolicies/tests/EnterprisePolicyTesting.sys.mjs b/toolkit/components/enterprisepolicies/tests/EnterprisePolicyTesting.sys.mjs index b877c6f738..dd0f5caf89 100644 --- a/toolkit/components/enterprisepolicies/tests/EnterprisePolicyTesting.sys.mjs +++ b/toolkit/components/enterprisepolicies/tests/EnterprisePolicyTesting.sys.mjs @@ -83,7 +83,7 @@ export var EnterprisePolicyTesting = { /** * This helper will track prefs that have been changed * by the policy engine through the setAndLockPref and - * setDefaultPref APIs (from Policies.jsm) and make sure + * setDefaultPref APIs (from Policies.sys.mjs) and make sure * that they are restored to their original values when * the test ends or another test case restarts the engine. */ diff --git a/toolkit/components/enterprisepolicies/tests/browser/browser_policies_enterprise_only.js b/toolkit/components/enterprisepolicies/tests/browser/browser_policies_enterprise_only.js index 3da3d250c6..c0fd63870a 100644 --- a/toolkit/components/enterprisepolicies/tests/browser/browser_policies_enterprise_only.js +++ b/toolkit/components/enterprisepolicies/tests/browser/browser_policies_enterprise_only.js @@ -14,13 +14,13 @@ add_task(async function test_enterprise_only_policies() { enterprisePolicyRan = false; Policies.NormalPolicy = { - onProfileAfterChange(manager, param) { + onProfileAfterChange() { normalPolicyRan = true; }, }; Policies.EnterpriseOnlyPolicy = { - onProfileAfterChange(manager, param) { + onProfileAfterChange() { enterprisePolicyRan = true; }, }; diff --git a/toolkit/components/extensions/.eslintrc.js b/toolkit/components/extensions/.eslintrc.js index 8cc2d2a16f..2009e44ec9 100644 --- a/toolkit/components/extensions/.eslintrc.js +++ b/toolkit/components/extensions/.eslintrc.js @@ -50,7 +50,7 @@ module.exports = { "no-unused-vars": [ "error", { - args: "none", + argsIgnorePattern: "^_", vars: "all", varsIgnorePattern: "^console$", }, @@ -223,7 +223,7 @@ module.exports = { "no-unused-vars": [ "error", { - args: "none", + argsIgnorePattern: "^_", vars: "local", }, ], diff --git a/toolkit/components/extensions/ConduitsChild.sys.mjs b/toolkit/components/extensions/ConduitsChild.sys.mjs index 598804f74a..d69a57cc19 100644 --- a/toolkit/components/extensions/ConduitsChild.sys.mjs +++ b/toolkit/components/extensions/ConduitsChild.sys.mjs @@ -12,6 +12,9 @@ * @property {ConduitID} [sender] * @property {boolean} query * @property {object} arg + * + * @typedef {import("ConduitsParent.sys.mjs").ConduitAddress} ConduitAddress + * @typedef {import("ConduitsParent.sys.mjs").ConduitID} ConduitID */ /** @@ -46,15 +49,15 @@ export class BaseConduit { } /** - * Internal, partially @abstract, uses the actor to send the message/query. + * Internal, uses the actor to send the message/query. * * @param {string} method * @param {boolean} query Flag indicating a response is expected. - * @param {JSWindowActor} actor + * @param {JSWindowActorChild|JSWindowActorParent} actor * @param {MessageData} data * @returns {Promise?} */ - _send(method, query, actor, data) { + _doSend(method, query, actor, data) { if (query) { return actor.sendQuery(method, data); } @@ -62,6 +65,16 @@ export class BaseConduit { } /** + * Internal @abstract, used by sendX stubs. + * + * @param {string} _name + * @param {boolean} _query + */ + _send(_name, _query, ..._args) { + throw new Error(`_send not implemented for ${this.constructor.name}`); + } + + /** * Internal, calls the specific recvX method based on the message. * * @param {string} name Message/method name. @@ -89,6 +102,7 @@ export class BaseConduit { * one specific ConduitsChild actor. */ export class PointConduit extends BaseConduit { + /** @type {ConduitGen} */ constructor(subject, address, actor) { super(subject, address); this.actor = actor; @@ -108,7 +122,7 @@ export class PointConduit extends BaseConduit { throw new Error(`send${method} on closed conduit ${this.id}`); } let sender = this.id; - return super._send(method, query, this.actor, { arg, query, sender }); + return super._doSend(method, query, this.actor, { arg, query, sender }); } /** @@ -156,9 +170,7 @@ export class ConduitsChild extends JSWindowActorChild { /** * Public entry point a child-side subject uses to open a conduit. * - * @param {object} subject - * @param {ConduitAddress} address - * @returns {PointConduit} + * @type {ConduitGen} */ openConduit(subject, address) { let conduit = new PointConduit(subject, address, this); @@ -211,6 +223,5 @@ export class ProcessConduitsChild extends JSProcessActorChild { openConduit = ConduitsChild.prototype.openConduit; receiveMessage = ConduitsChild.prototype.receiveMessage; - willDestroy = ConduitsChild.prototype.willDestroy; didDestroy = ConduitsChild.prototype.didDestroy; } diff --git a/toolkit/components/extensions/ConduitsParent.sys.mjs b/toolkit/components/extensions/ConduitsParent.sys.mjs index d90bc4afd7..afd81cafc2 100644 --- a/toolkit/components/extensions/ConduitsParent.sys.mjs +++ b/toolkit/components/extensions/ConduitsParent.sys.mjs @@ -29,6 +29,7 @@ * @property {string} [url] * @property {number} [frameId] * @property {string} [workerScriptURL] + * @property {number} [workerDescriptorId] * @property {string} [extensionId] * @property {string} [envType] * @property {string} [instanceId] @@ -299,7 +300,7 @@ export class BroadcastConduit extends BaseConduit { if (method === "RunListener" && arg.path.startsWith("webRequest.")) { return actor.batch(method, { target, arg, query, sender }); } - return super._send(method, query, actor, { target, arg, query, sender }); + return super._doSend(method, query, actor, { target, arg, query, sender }); } /** @@ -482,6 +483,5 @@ export class ConduitsParent extends JSWindowActorParent { */ export class ProcessConduitsParent extends JSProcessActorParent { receiveMessage = ConduitsParent.prototype.receiveMessage; - willDestroy = ConduitsParent.prototype.willDestroy; didDestroy = ConduitsParent.prototype.didDestroy; } diff --git a/toolkit/components/extensions/Extension.sys.mjs b/toolkit/components/extensions/Extension.sys.mjs index de6d4c8bfd..8ab3c30234 100644 --- a/toolkit/components/extensions/Extension.sys.mjs +++ b/toolkit/components/extensions/Extension.sys.mjs @@ -31,7 +31,9 @@ import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; import { ExtensionCommon } from "resource://gre/modules/ExtensionCommon.sys.mjs"; import { ExtensionParent } from "resource://gre/modules/ExtensionParent.sys.mjs"; import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs"; +import { Log } from "resource://gre/modules/Log.sys.mjs"; +/** @type {Lazy} */ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { @@ -54,7 +56,6 @@ ChromeUtils.defineESModuleGetters(lazy, { ExtensionTelemetry: "resource://gre/modules/ExtensionTelemetry.sys.mjs", LightweightThemeManager: "resource://gre/modules/LightweightThemeManager.sys.mjs", - Log: "resource://gre/modules/Log.sys.mjs", NetUtil: "resource://gre/modules/NetUtil.sys.mjs", SITEPERMS_ADDON_TYPE: "resource://gre/modules/addons/siteperms-addon-utils.sys.mjs", @@ -684,14 +685,13 @@ export var ExtensionProcessCrashObserver = { // `processCrashTimeframe` milliseconds. lastCrashTimestamps: [], + logger: Log.repository.getLogger("addons.process-crash-observer"), + init() { if (!this.initialized) { Services.obs.addObserver(this, "ipc:content-created"); Services.obs.addObserver(this, "process-type-set"); Services.obs.addObserver(this, "ipc:content-shutdown"); - this.logger = lazy.Log.repository.getLogger( - "addons.process-crash-observer" - ); if (this._isAndroid) { Services.obs.addObserver(this, "geckoview-initial-foreground"); Services.obs.addObserver(this, "application-foreground"); @@ -948,7 +948,7 @@ export class ExtensionData { get logger() { let id = this.id || "<unknown>"; - return lazy.Log.repository.getLogger(LOGGER_ID_BASE + id); + return Log.repository.getLogger(LOGGER_ID_BASE + id); } /** @@ -1120,6 +1120,24 @@ export class ExtensionData { return !(this.isPrivileged && this.hasPermission("mozillaAddons")); } + get optionsPageProperties() { + let page = this.manifest.options_ui?.page ?? this.manifest.options_page; + if (!page) { + return null; + } + return { + page, + open_in_tab: this.manifest.options_ui + ? this.manifest.options_ui.open_in_tab ?? false + : true, + // `options_ui.browser_style` is assigned the proper default value + // (true for MV2 and false for MV3 when not explicitly set), + // in `#parseBrowserStyleInManifest` (called when we are loading + // and parse manifest data from the `parseManifest` method). + browser_style: this.manifest.options_ui?.browser_style ?? false, + }; + } + /** * Given an array of host and permissions, generate a structured permissions object * that contains seperate host origins and permissions arrays. @@ -1658,6 +1676,8 @@ export class ExtensionData { ); } + // manifest.options_page opens the extension page in a new tab + // and so we will not need to special handling browser_style. if (manifest.options_ui) { if (manifest.options_ui.open_in_tab) { // browser_style:true has no effect when open_in_tab is true. @@ -2748,10 +2768,10 @@ class DictionaryBootstrapScope extends BootstrapScope { install() {} uninstall() {} - startup(data, reason) { + startup(data) { // eslint-disable-next-line no-use-before-define this.dictionary = new Dictionary(data); - return this.dictionary.startup(BootstrapScope.BOOTSTRAP_REASON_MAP[reason]); + return this.dictionary.startup(); } async shutdown(data, reason) { @@ -2765,10 +2785,10 @@ class LangpackBootstrapScope extends BootstrapScope { uninstall() {} async update() {} - startup(data, reason) { + startup(data) { // eslint-disable-next-line no-use-before-define this.langpack = new Langpack(data); - return this.langpack.startup(BootstrapScope.BOOTSTRAP_REASON_MAP[reason]); + return this.langpack.startup(); } async shutdown(data, reason) { @@ -2782,12 +2802,10 @@ class SitePermissionBootstrapScope extends BootstrapScope { install() {} uninstall() {} - startup(data, reason) { + startup(data) { // eslint-disable-next-line no-use-before-define this.sitepermission = new SitePermission(data); - return this.sitepermission.startup( - BootstrapScope.BOOTSTRAP_REASON_MAP[reason] - ); + return this.sitepermission.startup(); } async shutdown(data, reason) { @@ -2810,6 +2828,15 @@ export class Extension extends ExtensionData { /** @type {Map<string, Map<string, any>>} */ persistentListeners; + /** @type {import("ExtensionShortcuts.sys.mjs").ExtensionShortcuts} */ + shortcuts; + + /** @type {TabManagerBase} */ + tabManager; + + /** @type {(options?: { ignoreDevToolsAttached?: boolean, disableResetIdleForTest?: boolean }) => Promise} */ + terminateBackground; + constructor(addonData, startupReason, updateReason) { super(addonData.resourceURI, addonData.isPrivileged); @@ -3203,9 +3230,11 @@ export class Extension extends ExtensionData { }; } - // Extended serialized data which is only needed in the extensions process, - // and is never deserialized in web content processes. - // Keep in sync with BrowserExtensionContent in ExtensionChild.sys.mjs + /** + * Extended serialized data which is only needed in the extensions process, + * and is never deserialized in web content processes. + * Keep in sync with @see {ExtensionChild}. + */ serializeExtended() { return { backgroundScripts: this.backgroundScripts, @@ -3461,7 +3490,7 @@ export class Extension extends ExtensionData { ignoreQuarantine: this.ignoreQuarantine, temporarilyInstalled: this.temporarilyInstalled, allowedOrigins: new MatchPatternSet([]), - localizeCallback() {}, + localizeCallback: () => "", readyPromise, }); diff --git a/toolkit/components/extensions/ExtensionActions.sys.mjs b/toolkit/components/extensions/ExtensionActions.sys.mjs index 29a286442e..92ea9865e3 100644 --- a/toolkit/components/extensions/ExtensionActions.sys.mjs +++ b/toolkit/components/extensions/ExtensionActions.sys.mjs @@ -79,8 +79,8 @@ class PanelActionBase { /** * Set a global, window specific or tab specific property. * - * @param {XULElement|ChromeWindow|null} target - * A XULElement tab, a ChromeWindow, or null for the global data. + * @param {NativeTab|ChromeWindow|null} target + * A NativeTab tab, a ChromeWindow, or null for the global data. * @param {string} prop * String property to set. Should should be one of "icon", "title", "badgeText", * "popup", "badgeBackgroundColor", "badgeTextColor" or "enabled". @@ -104,8 +104,8 @@ class PanelActionBase { /** * Gets the data associated with a tab, window, or the global one. * - * @param {XULElement|ChromeWindow|null} target - * A XULElement tab, a ChromeWindow, or null for the global data. + * @param {NativeTab|ChromeWindow|null} target + * A NativeTab tab, a ChromeWindow, or null for the global data. * @returns {object} * The icon, title, badge, etc. associated with the target. */ @@ -119,8 +119,8 @@ class PanelActionBase { /** * Retrieve the value of a global, window specific or tab specific property. * - * @param {XULElement|ChromeWindow|null} target - * A XULElement tab, a ChromeWindow, or null for the global data. + * @param {NativeTab|ChromeWindow|null} target + * A NativeTab tab, a ChromeWindow, or null for the global data. * @param {string} prop * Name of property to retrieve. Should should be one of "icon", * "title", "badgeText", "popup", "badgeBackgroundColor" or "enabled". @@ -161,7 +161,7 @@ class PanelActionBase { * * @param {string} eventType * The type of the event, should be "location-change". - * @param {XULElement} tab + * @param {NativeTab} tab * The tab whose location changed, or which has become selected. * @param {boolean} [fromBrowse] * - `true` if navigation occurred in `tab`. @@ -178,7 +178,7 @@ class PanelActionBase { /** * Gets the popup url for a given tab. * - * @param {XULElement} tab + * @param {NativeTab} tab * The tab the popup refers to. * @param {boolean} strict * If errors should be thrown if a URL is not available. @@ -208,7 +208,7 @@ class PanelActionBase { * Will clear any existing activeTab permissions previously granted for any * other tab. * - * @param {XULElement} tab + * @param {NativeTab} tab * The tab that should be granted activeTab permission for. Set to * null to clear previously granted activeTab permission. */ @@ -229,7 +229,7 @@ class PanelActionBase { /** * Triggers this action and sends the appropriate event if needed. * - * @param {XULElement} tab + * @param {NativeTab} tab * The tab on which the action was fired. * @param {object} clickInfo * Extra data passed to the second parameter to the action API's @@ -322,7 +322,7 @@ class PanelActionBase { * If it only changes a parameter for a single window, `target` will be that window. * Otherwise `target` will be null. * - * @param {XULElement|ChromeWindow|null} _target + * @param {NativeTab|ChromeWindow} [_target] * Browser tab or browser chrome window, may be null. */ updateOnChange(_target) {} @@ -332,16 +332,22 @@ class PanelActionBase { * * @param {string} _tabId * Internal id of the tab to get. + * @returns {NativeTab} */ - getTab(_tabId) {} + getTab(_tabId) { + throw new Error("Not implemented."); + } /** * Get window object from windowId * * @param {string} _windowId * Internal id of the window to get. + * @returns {ChromeWindow} */ - getWindow(_windowId) {} + getWindow(_windowId) { + throw new Error("Not implemented."); + } /** * Gets the target object corresponding to the `details` parameter of the various @@ -352,19 +358,19 @@ class PanelActionBase { * @param {number} [_details.tabId] * @param {number} [_details.windowId] * @throws if both `tabId` and `windowId` are specified, or if they are invalid. - * @returns {XULElement|ChromeWindow|null} - * If a `tabId` was specified, the corresponding XULElement tab. + * @returns {NativeTab|ChromeWindow|null} + * If a `tabId` was specified, the corresponding NativeTab tab. * If a `windowId` was specified, the corresponding ChromeWindow. * Otherwise, `null`. */ getTargetFromDetails(_details) { - return null; + throw new Error("Not Implemented"); } /** * Triggers a click event. * - * @param {XULElement} _tab + * @param {NativeTab} _tab * The tab where this event should be fired. * @param {object} _clickInfo * Extra data passed to the second parameter to the action API's @@ -375,7 +381,7 @@ class PanelActionBase { /** * Checks whether this action is shown. * - * @param {XULElement} _tab + * @param {NativeTab} _tab * The tab to be checked * @returns {boolean} */ @@ -442,7 +448,7 @@ export class PageActionBase extends PanelActionBase { // Checks whether the tab action is shown when the specified tab becomes active. // Does pattern matching if necessary, and caches the result as a tab-specific value. - // @param {XULElement} tab + // @param {NativeTab} tab // The tab to be checked // @return boolean isShownForTab(tab) { @@ -571,6 +577,8 @@ export class BrowserActionBase extends PanelActionBase { /** * Determines the text badge color to be used in a tab, window, or globally. * + * @typedef {number[]} ColorArray from schemas/browser_action.json. + * * @param {object} values * The values associated with the tab or window, or global values. * @returns {ColorArray} diff --git a/toolkit/components/extensions/ExtensionChild.sys.mjs b/toolkit/components/extensions/ExtensionChild.sys.mjs index 70774db395..232d5cc659 100644 --- a/toolkit/components/extensions/ExtensionChild.sys.mjs +++ b/toolkit/components/extensions/ExtensionChild.sys.mjs @@ -15,6 +15,7 @@ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; +/** @type {Lazy} */ const lazy = {}; XPCOMUtils.defineLazyServiceGetter( @@ -144,7 +145,7 @@ const StrongPromise = { Services.obs.addObserver(StrongPromise, "extensions-onMessage-witness"); // Simple single-event emitter-like helper, exposes the EventManager api. -class SimpleEventAPI extends EventManager { +export class SimpleEventAPI extends EventManager { constructor(context, name) { let fires = new Set(); let register = fire => { @@ -162,7 +163,7 @@ class SimpleEventAPI extends EventManager { } // runtime.OnMessage event helper, handles custom async/sendResponse logic. -class MessageEvent extends SimpleEventAPI { +export class MessageEvent extends SimpleEventAPI { emit(holder, sender) { if (!this.fires.size || !this.context.active) { return { received: false }; @@ -229,7 +230,7 @@ function holdMessage(name, anonymizedName, data, native = null) { } // Implements the runtime.Port extension API object. -class Port { +export class Port { /** * @param {BaseContext} context The context that owns this port. * @param {number} portId Uniquely identifies this port's channel. @@ -310,7 +311,7 @@ class Port { * Each extension context gets its own Messenger object. It handles the * basics of sendMessage, onMessage, connect and onConnect. */ -class Messenger { +export class Messenger { constructor(context) { this.context = context; this.conduit = context.openConduit(this, { @@ -382,8 +383,11 @@ var ExtensionManager = { extensions: new Map(), }; -// Represents a browser extension in the content process. -class BrowserExtensionContent extends EventEmitter { +/** + * Represents an extension instance in the child process. + * Corresponds to the @see {Extension} instance in the parent. + */ +export class ExtensionChild extends EventEmitter { constructor(policy) { super(); @@ -580,7 +584,7 @@ class BrowserExtensionContent extends EventEmitter { /** * An object that runs an remote implementation of an API. */ -class ProxyAPIImplementation extends SchemaAPIInterface { +export class ProxyAPIImplementation extends SchemaAPIInterface { /** * @param {string} namespace The full path to the namespace that contains the * `name` member. This may contain dots, e.g. "storage.local". @@ -680,7 +684,7 @@ class ProxyAPIImplementation extends SchemaAPIInterface { } } -class ChildLocalAPIImplementation extends LocalAPIImplementation { +export class ChildLocalAPIImplementation extends LocalAPIImplementation { constructor(pathObj, namespace, name, childApiManager) { super(pathObj, name, childApiManager.context); this.childApiManagerId = childApiManager.id; @@ -730,7 +734,7 @@ class ChildLocalAPIImplementation extends LocalAPIImplementation { // JSProcessActor Conduits actors (see ConduitsChild.sys.mjs) to communicate // with the ParentAPIManager singleton in ExtensionParent.sys.mjs. // It handles asynchronous function calls as well as event listeners. -class ChildAPIManager { +export class ChildAPIManager { constructor(context, messageManager, localAPICan, contextData) { this.context = context; this.messageManager = messageManager; @@ -1012,14 +1016,3 @@ class ChildAPIManager { this.permissionsChangedCallbacks.add(callback); } } - -export var ExtensionChild = { - BrowserExtensionContent, - ChildAPIManager, - ChildLocalAPIImplementation, - MessageEvent, - Messenger, - Port, - ProxyAPIImplementation, - SimpleEventAPI, -}; diff --git a/toolkit/components/extensions/ExtensionCommon.sys.mjs b/toolkit/components/extensions/ExtensionCommon.sys.mjs index 512d1444a5..c06cf37a6a 100644 --- a/toolkit/components/extensions/ExtensionCommon.sys.mjs +++ b/toolkit/components/extensions/ExtensionCommon.sys.mjs @@ -14,6 +14,7 @@ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; +/** @type {Lazy} */ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { @@ -234,7 +235,7 @@ class NoCloneSpreadArgs { const LISTENERS = Symbol("listeners"); const ONCE_MAP = Symbol("onceMap"); -class EventEmitter { +export class EventEmitter { constructor() { this[LISTENERS] = new Map(); this[ONCE_MAP] = new WeakMap(); @@ -353,7 +354,7 @@ class EventEmitter { * that inherits from this class, the derived class is instantiated * once for each extension that uses the API. */ -class ExtensionAPI extends EventEmitter { +export class ExtensionAPI extends EventEmitter { constructor(extension) { super(); @@ -388,7 +389,7 @@ class ExtensionAPI extends EventEmitter { /** * @param {string} _id - * @param {Record<string, JSONValue>} _manifest + * @param {object} _manifest */ static onUpdate(_id, _manifest) {} } @@ -468,7 +469,7 @@ class ExtensionAPIPersistent extends ExtensionAPI { * * @abstract */ -class BaseContext { +export class BaseContext { /** @type {boolean} */ isTopContext; /** @type {string} */ @@ -553,10 +554,7 @@ class BaseContext { * Opens a conduit linked to this context, populating related address fields. * Only available in child contexts with an associated contentWindow. * - * @param {object} subject - * @param {ConduitAddress} address - * @returns {import("ConduitsChild.sys.mjs").PointConduit} - * @type {ConduitOpen} + * @type {ConduitGen} */ openConduit(subject, address) { let wgc = this.contentWindow.windowGlobalChild; @@ -614,7 +612,7 @@ class BaseContext { // All child contexts must implement logActivity. This is handled if the child // context subclasses ExtensionBaseContextChild. ProxyContextParent overrides // this with a noop for parent contexts. - logActivity() { + logActivity(_type, _name, _data) { throw new Error(`Not implemented for ${this.envType}`); } @@ -822,7 +820,7 @@ class BaseContext { * exception error. * * @param {Error|object} error - * @param {SavedFrame?} [caller] + * @param {nsIStackFrame?} [caller] * @returns {Error} */ normalizeError(error, caller) { @@ -864,7 +862,7 @@ class BaseContext { * * @param {object} error An object with a `message` property. May * optionally be an `Error` object belonging to the target scope. - * @param {SavedFrame?} caller + * @param {nsIStackFrame?} caller * The optional caller frame which triggered this callback, to be used * in error reporting. * @param {Function} callback The callback to call. @@ -885,7 +883,7 @@ class BaseContext { /** * Captures the most recent stack frame which belongs to the extension. * - * @returns {SavedFrame?} + * @returns {nsIStackFrame?} */ getCaller() { return ChromeUtils.getCallerLocation(this.principal); @@ -1037,7 +1035,7 @@ class BaseContext { * * @interface */ -class SchemaAPIInterface { +export class SchemaAPIInterface { /** * Calls this as a function that returns its return value. * @@ -1483,7 +1481,7 @@ class SchemaAPIManager extends EventEmitter { * "addon" - An addon process. * "content" - A content process. * "devtools" - A devtools process. - * @param {import("Schemas.sys.mjs").SchemaRoot} [schema] + * @param {import("Schemas.sys.mjs").SchemaInject} [schema] */ constructor(processType, schema) { super(); @@ -2023,10 +2021,14 @@ export function LocaleData(data) { this.locales = data.locales || new Map(); this.warnedMissingKeys = new Set(); - // Map(locale-name -> Map(message-key -> localized-string)) - // - // Contains a key for each loaded locale, each of which is a - // Map of message keys to their localized strings. + /** + * Map(locale-name -> Map(message-key -> localized-string)) + * + * Contains a key for each loaded locale, each of which is a + * Map of message keys to their localized strings. + * + * @type {Map<string, Map<string, string>>} + */ this.messages = data.messages || new Map(); if (data.builtinMessages) { diff --git a/toolkit/components/extensions/ExtensionContent.sys.mjs b/toolkit/components/extensions/ExtensionContent.sys.mjs index 015d1bc7c6..a2fce282ee 100644 --- a/toolkit/components/extensions/ExtensionContent.sys.mjs +++ b/toolkit/components/extensions/ExtensionContent.sys.mjs @@ -7,6 +7,7 @@ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; +/** @type {Lazy} */ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { @@ -39,8 +40,10 @@ const ScriptError = Components.Constructor( ); import { + ChildAPIManager, ExtensionChild, ExtensionActivityLogChild, + Messenger, } from "resource://gre/modules/ExtensionChild.sys.mjs"; import { ExtensionCommon } from "resource://gre/modules/ExtensionCommon.sys.mjs"; import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs"; @@ -63,8 +66,6 @@ const { runSafeSyncWithoutClone, } = ExtensionCommon; -const { BrowserExtensionContent, ChildAPIManager, Messenger } = ExtensionChild; - ChromeUtils.defineLazyGetter(lazy, "isContentScriptProcess", () => { return ( Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT || @@ -164,6 +165,7 @@ class ScriptCache extends CacheMap { super( SCRIPT_EXPIRY_TIMEOUT_MS, url => { + /** @type {Promise<PrecompiledScript> & { script?: PrecompiledScript }} */ let promise = ChromeUtils.compileScript(url, options); promise.then(script => { promise.script = script; @@ -282,49 +284,37 @@ class CSSCodeCache extends BaseCSSCache { } } -defineLazyGetter( - BrowserExtensionContent.prototype, - "staticScripts", - function () { - return new ScriptCache({ hasReturnValue: false }, this); - } -); +defineLazyGetter(ExtensionChild.prototype, "staticScripts", function () { + return new ScriptCache({ hasReturnValue: false }, this); +}); -defineLazyGetter( - BrowserExtensionContent.prototype, - "dynamicScripts", - function () { - return new ScriptCache({ hasReturnValue: true }, this); - } -); +defineLazyGetter(ExtensionChild.prototype, "dynamicScripts", function () { + return new ScriptCache({ hasReturnValue: true }, this); +}); -defineLazyGetter(BrowserExtensionContent.prototype, "userCSS", function () { +defineLazyGetter(ExtensionChild.prototype, "userCSS", function () { return new CSSCache(Ci.nsIStyleSheetService.USER_SHEET, this); }); -defineLazyGetter(BrowserExtensionContent.prototype, "authorCSS", function () { +defineLazyGetter(ExtensionChild.prototype, "authorCSS", function () { return new CSSCache(Ci.nsIStyleSheetService.AUTHOR_SHEET, this); }); // These two caches are similar to the above but specialized to cache the cssCode // using an hash computed from the cssCode string as the key (instead of the generated data // URI which can be pretty long for bigger injected cssCode). -defineLazyGetter(BrowserExtensionContent.prototype, "userCSSCode", function () { +defineLazyGetter(ExtensionChild.prototype, "userCSSCode", function () { return new CSSCodeCache(Ci.nsIStyleSheetService.USER_SHEET, this); }); -defineLazyGetter( - BrowserExtensionContent.prototype, - "authorCSSCode", - function () { - return new CSSCodeCache(Ci.nsIStyleSheetService.AUTHOR_SHEET, this); - } -); +defineLazyGetter(ExtensionChild.prototype, "authorCSSCode", function () { + return new CSSCodeCache(Ci.nsIStyleSheetService.AUTHOR_SHEET, this); +}); // Represents a content script. class Script { /** - * @param {BrowserExtensionContent} extension + * @param {ExtensionChild} extension * @param {WebExtensionContentScript|object} matcher * An object with a "matchesWindowGlobal" method and content script * execution details. This is usually a plain WebExtensionContentScript @@ -611,7 +601,7 @@ class Script { * @param {ContentScriptContextChild} context * The document to block the parsing on, if the scripts are not yet precompiled and cached. * - * @returns {Array<PreloadedScript> | Promise<Array<PreloadedScript>>} + * @returns {PrecompiledScript[] | Promise<PrecompiledScript[]>} * Returns an array of preloaded scripts if they are already available, or a promise which * resolves to the array of the preloaded scripts once they are precompiled and cached. */ @@ -667,7 +657,7 @@ class Script { // Represents a user script. class UserScript extends Script { /** - * @param {BrowserExtensionContent} extension + * @param {ExtensionChild} extension * @param {WebExtensionContentScript|object} matcher * An object with a "matchesWindowGlobal" method and content script * execution details. @@ -1014,7 +1004,7 @@ class ContentScriptContextChild extends BaseContext { // Responsible for creating ExtensionContexts and injecting content // scripts into them when new documents are created. DocumentManager = { - // Map[windowId -> Map[ExtensionChild -> ContentScriptContextChild]] + /** @type {Map<number, Map<ExtensionChild, ContentScriptContextChild>>} */ contexts: new Map(), initialized: false, @@ -1115,8 +1105,6 @@ DocumentManager = { }; export var ExtensionContent = { - BrowserExtensionContent, - contentScripts, shutdownExtension(extension) { diff --git a/toolkit/components/extensions/ExtensionDNR.sys.mjs b/toolkit/components/extensions/ExtensionDNR.sys.mjs index d18856a2b8..afc7d30751 100644 --- a/toolkit/components/extensions/ExtensionDNR.sys.mjs +++ b/toolkit/components/extensions/ExtensionDNR.sys.mjs @@ -71,6 +71,7 @@ const gRuleManagers = []; import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; +/** @type {Lazy} */ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { @@ -182,6 +183,8 @@ export class Rule { class Ruleset { /** + * @typedef {number} integer + * * @param {string} rulesetId - extension-defined ruleset ID. * @param {integer} rulesetPrecedence * @param {Rule[]} rules - extension-defined rules @@ -1316,7 +1319,7 @@ class RequestDetails { * @param {string} options.type - ResourceType (MozContentPolicyType). * @param {string} [options.method] - HTTP method * @param {integer} [options.tabId] - * @param {BrowsingContext} [options.browsingContext] - The BrowsingContext + * @param {CanonicalBrowsingContext} [options.browsingContext] - The CBC * associated with the request. Typically the bc for which the subresource * request is initiated, if any. For document requests, this is the parent * (i.e. the parent frame for sub_frame, null for main_frame). @@ -1975,7 +1978,10 @@ const NetworkIntegration = { /** * Applies the actions of the DNR rules. * - * @param {ChannelWrapper} channel + * @typedef {ChannelWrapper & { _dnrMatchedRules?: MatchedRule[] }} + * ChannelWrapperViaDNR + * + * @param {ChannelWrapperViaDNR} channel * @returns {boolean} Whether to ignore any responses from the webRequest API. */ onBeforeRequest(channel) { @@ -1993,7 +1999,7 @@ const NetworkIntegration = { this.applyRedirect(channel, finalMatch); return true; case "upgradeScheme": - this.applyUpgradeScheme(channel, finalMatch); + this.applyUpgradeScheme(channel); return true; } // If there are multiple rules, then it may be a combination of allow, @@ -2294,7 +2300,7 @@ function beforeWebRequestEvent(channel, kind) { /** * Applies matching DNR rules, some of which may potentially cancel the request. * - * @param {ChannelWrapper} channel + * @param {ChannelWrapperViaDNR} channel * @param {string} kind - The name of the webRequest event. * @returns {boolean} Whether to ignore any responses from the webRequest API. */ diff --git a/toolkit/components/extensions/ExtensionPageChild.sys.mjs b/toolkit/components/extensions/ExtensionPageChild.sys.mjs index 17c208572b..f0ac5ed229 100644 --- a/toolkit/components/extensions/ExtensionPageChild.sys.mjs +++ b/toolkit/components/extensions/ExtensionPageChild.sys.mjs @@ -22,8 +22,9 @@ const CATEGORY_EXTENSION_SCRIPTS_DEVTOOLS = "webextension-scripts-devtools"; import { ExtensionCommon } from "resource://gre/modules/ExtensionCommon.sys.mjs"; import { - ExtensionChild, + ChildAPIManager, ExtensionActivityLogChild, + Messenger, } from "resource://gre/modules/ExtensionChild.sys.mjs"; import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs"; @@ -32,8 +33,6 @@ const { getInnerWindowID, promiseEvent } = ExtensionUtils; const { BaseContext, CanOfAPIs, SchemaAPIManager, redefineGetter } = ExtensionCommon; -const { ChildAPIManager, Messenger } = ExtensionChild; - const initializeBackgroundPage = context => { // Override the `alert()` method inside background windows; // we alias it to console.log(). @@ -188,7 +187,7 @@ export class ExtensionBaseContextChild extends BaseContext { * This ExtensionBaseContextChild represents an addon execution environment * that is running in an addon or devtools child process. * - * @param {BrowserExtensionContent} extension This context's owner. + * @param {ExtensionChild} extension This context's owner. * @param {object} params * @param {string} params.envType One of "addon_child" or "devtools_child". * @param {nsIDOMWindow} params.contentWindow The window where the addon runs. @@ -301,7 +300,7 @@ class ExtensionPageContextChild extends ExtensionBaseContextChild { * This is the child side of the ExtensionPageContextParent class * defined in ExtensionParent.sys.mjs. * - * @param {BrowserExtensionContent} extension This context's owner. + * @param {ExtensionChild} extension This context's owner. * @param {object} params * @param {nsIDOMWindow} params.contentWindow The window where the addon runs. * @param {string} params.viewType One of "background", "popup", "sidebar" or "tab". @@ -340,7 +339,7 @@ export class DevToolsContextChild extends ExtensionBaseContextChild { * environment that has access to the devtools API namespace and to the same subset * of APIs available in a content script execution environment. * - * @param {BrowserExtensionContent} extension This context's owner. + * @param {ExtensionChild} extension This context's owner. * @param {object} params * @param {nsIDOMWindow} params.contentWindow The window where the addon runs. * @param {string} params.viewType One of "devtools_page" or "devtools_panel". @@ -424,7 +423,7 @@ export var ExtensionPageChild = { /** * Create a privileged context at initial-document-element-inserted. * - * @param {BrowserExtensionContent} extension + * @param {ExtensionChild} extension * The extension for which the context should be created. * @param {nsIDOMWindow} contentWindow The global of the page. */ diff --git a/toolkit/components/extensions/ExtensionParent.sys.mjs b/toolkit/components/extensions/ExtensionParent.sys.mjs index b4812a702a..f951433713 100644 --- a/toolkit/components/extensions/ExtensionParent.sys.mjs +++ b/toolkit/components/extensions/ExtensionParent.sys.mjs @@ -14,6 +14,7 @@ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; +/** @type {Lazy} */ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { @@ -245,8 +246,10 @@ let apiManager = new (class extends SchemaAPIManager { // to relevant child messengers. Also handles Native messaging and GeckoView. /** @typedef {typeof ProxyMessenger} NativeMessenger */ const ProxyMessenger = { - /** @type {Map<number, Partial<ParentPort>&Promise<ParentPort>>} */ + /** @type {Map<number, ParentPort>} */ ports: new Map(), + /** @type {Map<number, Promise>} */ + portPromises: new Map(), init() { this.conduit = new lazy.BroadcastConduit(ProxyMessenger, { @@ -300,7 +303,8 @@ const ProxyMessenger = { }; if (JSWindowActorParent.isInstance(source.actor)) { - let browser = source.actor.browsingContext.top.embedderElement; + let { currentWindowContext, top } = source.actor.browsingContext; + let browser = top.embedderElement; let data = browser && apiManager.global.tabTracker.getBrowserData(browser); if (data?.tabId > 0) { @@ -308,6 +312,13 @@ const ProxyMessenger = { // frameId is documented to only be set if sender.tab is set. sender.frameId = source.frameId; } + + let principal = currentWindowContext.documentPrincipal; + // We intend the serialization of null principals *and* file scheme to be + // "null". + sender.origin = new URL(principal.originNoSuffix).origin; + } else if (source.verified) { + sender.origin = `moz-extension://${extension.uuid}`; } return sender; @@ -363,17 +374,24 @@ const ProxyMessenger = { } // PortMessages that follow will need to wait for the port to be opened. - /** @type {callback} */ - let resolvePort; - this.ports.set(arg.portId, new Promise(res => (resolvePort = res))); + let { promise, resolve, reject } = Promise.withResolvers(); + this.portPromises.set(arg.portId, promise); - let kind = await this.normalizeArgs(arg, sender); - let all = await this.conduit.castPortConnect(kind, arg); - resolvePort(); + try { + let kind = await this.normalizeArgs(arg, sender); + let all = await this.conduit.castPortConnect(kind, arg); + resolve(); - // If there are no active onConnect listeners. - if (!all.some(x => x.value)) { - throw new ExtensionError(ERROR_NO_RECEIVERS); + // If there are no active onConnect listeners. + if (!all.some(x => x.value)) { + throw new ExtensionError(ERROR_NO_RECEIVERS); + } + } catch (err) { + // Throw _and_ reject with error, so everything awaiting this port fails. + reject(err); + throw err; + } finally { + this.portPromises.delete(arg.portId); } }, @@ -387,7 +405,7 @@ const ProxyMessenger = { // NOTE: the following await make sure we await for promised ports // (ports that were not yet open when added to the Map, // see recvPortConnect). - await this.ports.get(sender.portId); + await this.portPromises.get(sender.portId); this.sendPortMessage(sender.portId, holder, !sender.source); }, @@ -448,7 +466,7 @@ GlobalManager = { extensionMap: new Map(), initialized: false, - /** @type {WeakMap<Browser, object>} Extension Context init data. */ + /** @type {WeakMap<XULBrowserElement, object>} Extension Context init data. */ frameData: new WeakMap(), init(extension) { @@ -961,7 +979,6 @@ ParentAPIManager = { throw new Error(`Bad sender context envType: ${sender.envType}`); } - let isBackgroundWorker = false; if (JSWindowActorParent.isInstance(actor)) { const target = actor.browsingContext.top.embedderElement; let processMessageManager = @@ -979,6 +996,22 @@ ParentAPIManager = { "Attempt to create privileged extension parent from incorrect child process" ); } + + if (envType == "addon_parent") { + context = new ExtensionPageContextParent( + envType, + extension, + data, + actor.browsingContext + ); + } else if (envType == "devtools_parent") { + context = new DevToolsExtensionPageContextParent( + envType, + extension, + data, + actor.browsingContext + ); + } } else if (JSProcessActorParent.isInstance(actor)) { if (actor.manager.remoteType !== extension.remoteType) { throw new Error( @@ -996,7 +1029,7 @@ ParentAPIManager = { `Unexpected viewType ${data.viewType} on an extension process actor` ); } - isBackgroundWorker = true; + context = new BackgroundWorkerContextParent(envType, extension, data); } else { // Unreacheable: JSWindowActorParent and JSProcessActorParent are the // only actors. @@ -1004,24 +1037,6 @@ ParentAPIManager = { "Attempt to create privileged extension parent via incorrect actor" ); } - - if (isBackgroundWorker) { - context = new BackgroundWorkerContextParent(envType, extension, data); - } else if (envType == "addon_parent") { - context = new ExtensionPageContextParent( - envType, - extension, - data, - actor.browsingContext - ); - } else if (envType == "devtools_parent") { - context = new DevToolsExtensionPageContextParent( - envType, - extension, - data, - actor.browsingContext - ); - } } else if (envType == "content_parent") { // Note: actor is always a JSWindowActorParent, with a browsingContext. context = new ContentScriptContextParent( @@ -1340,11 +1355,9 @@ class HiddenXULWindow { // The windowless browser is a thin wrapper around a docShell that keeps // its related resources alive. It implements nsIWebNavigation and - // forwards its methods to the underlying docShell. That .docShell - // needs `QueryInterface(nsIWebNavigation)` to give us access to the - // webNav methods that are already available on the windowless browser. + // forwards its methods to the underlying docShell. let chromeShell = windowlessBrowser.docShell; - chromeShell.QueryInterface(Ci.nsIWebNavigation); + let webNav = chromeShell.QueryInterface(Ci.nsIWebNavigation); if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) { let attrs = chromeShell.getOriginAttributes(); @@ -1353,13 +1366,13 @@ class HiddenXULWindow { } windowlessBrowser.browsingContext.useGlobalHistory = false; - chromeShell.loadURI(DUMMY_PAGE_URI, { + webNav.loadURI(DUMMY_PAGE_URI, { triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), }); await promiseObserved( "chrome-document-global-created", - win => win.document == chromeShell.document + win => win.document == webNav.document ); await promiseDocumentLoaded(windowlessBrowser.document); if (this.unloaded) { @@ -1376,7 +1389,7 @@ class HiddenXULWindow { * An object that contains the xul attributes to set of the newly * created browser XUL element. * - * @returns {Promise<XULElement>} + * @returns {Promise<XULBrowserElement>} * A Promise which resolves to the newly created browser XUL element. */ async createBrowserElement(xulAttributes) { @@ -1458,16 +1471,17 @@ const SharedWindow = { * to inherits the shared boilerplate code needed to create a parent document for the hidden * extension pages (e.g. the background page, the devtools page) in the BackgroundPage and * DevToolsPage classes. - * - * @param {Extension} extension - * The Extension which owns the hidden extension page created (used to decide - * if the hidden extension page parent doc is going to be a windowlessBrowser or - * a visible XUL window). - * @param {string} viewType - * The viewType of the WebExtension page that is going to be loaded - * in the created browser element (e.g. "background" or "devtools_page"). */ class HiddenExtensionPage { + /** + * @param {Extension} extension + * The Extension which owns the hidden extension page created (used to decide + * if the hidden extension page parent doc is going to be a windowlessBrowser or + * a visible XUL window). + * @param {string} viewType + * The viewType of the WebExtension page that is going to be loaded + * in the created browser element (e.g. "background" or "devtools_page"). + */ constructor(extension, viewType) { if (!extension || !viewType) { throw new Error("extension and viewType parameters are mandatory"); @@ -1535,6 +1549,9 @@ class HiddenExtensionPage { } } +/** @typedef {import("resource://devtools/server/actors/descriptors/webextension.js") + .WebExtensionDescriptorActor} WebExtensionDescriptorActor */ + /** * This object provides utility functions needed by the devtools actors to * be able to connect and debug an extension (which can run in the main or in @@ -1545,9 +1562,9 @@ const DebugUtils = { // which are used to connect the webextension patent actor to the extension process. hiddenXULWindow: null, - // Map<extensionId, Promise<XULElement>> + /** @type {Map<string, Promise<XULBrowserElement> & { browser: XULBrowserElement }>} */ debugBrowserPromises: new Map(), - // DefaultWeakMap<Promise<browser XULElement>, Set<WebExtensionParentActor>> + /** @type {WeakMap<Promise<XULBrowserElement>, Set<WebExtensionDescriptorActor>>} */ debugActors: new DefaultWeakMap(() => new Set()), _extensionUpdatedWatcher: null, @@ -1696,10 +1713,10 @@ const DebugUtils = { * Retrieve a XUL browser element which has been configured to be able to connect * the devtools actor with the process where the extension is running. * - * @param {WebExtensionParentActor} webExtensionParentActor + * @param {WebExtensionDescriptorActor} webExtensionParentActor * The devtools actor that is retrieving the browser element. * - * @returns {Promise<XULElement>} + * @returns {Promise<XULBrowserElement>} * A promise which resolves to the configured browser XUL element. */ async getExtensionProcessBrowser(webExtensionParentActor) { @@ -1753,7 +1770,7 @@ const DebugUtils = { * it destroys the XUL browser element, and it also destroy the hidden XUL window * if it is not currently needed. * - * @param {WebExtensionParentActor} webExtensionParentActor + * @param {WebExtensionDescriptorActor} webExtensionParentActor * The devtools actor that has retrieved an addon debug browser element. */ async releaseExtensionProcessBrowser(webExtensionParentActor) { @@ -1783,7 +1800,7 @@ const DebugUtils = { * was received by the message manager. The promise is rejected if the message * manager was closed before a message was received. * - * @param {nsIMessageListenerManager} messageManager + * @param {MessageListenerManager} messageManager * The message manager on which to listen for messages. * @param {string} messageName * The message to listen for. diff --git a/toolkit/components/extensions/ExtensionPermissions.sys.mjs b/toolkit/components/extensions/ExtensionPermissions.sys.mjs index 1ee9afdc32..964503d8f6 100644 --- a/toolkit/components/extensions/ExtensionPermissions.sys.mjs +++ b/toolkit/components/extensions/ExtensionPermissions.sys.mjs @@ -8,11 +8,13 @@ import { computeSha1HashAsString } from "resource://gre/modules/addons/crypto-ut import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; +/** @type {Lazy} */ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { AddonManager: "resource://gre/modules/AddonManager.sys.mjs", AddonManagerPrivate: "resource://gre/modules/AddonManager.sys.mjs", + Extension: "resource://gre/modules/Extension.sys.mjs", ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs", FileUtils: "resource://gre/modules/FileUtils.sys.mjs", JSONFile: "resource://gre/modules/JSONFile.sys.mjs", @@ -347,6 +349,50 @@ export var ExtensionPermissions = { return this._getCached(extensionId); }, + /** + * Validate and normalize passed in `perms`, including a fixup to + * include all possible "all sites" permissions when appropriate. + * + * @throws if an origin or permission is not part of optional permissions. + * + * @typedef {object} Perms + * @property {string[]} origins + * @property {string[]} permissions + * + * @param {Perms} perms api permissions and origins to be added/removed. + * @param {Perms} optional permissions and origins from the manifest. + * @returns {Perms} normalized + */ + normalizeOptional(perms, optional) { + let allSites = false; + let patterns = new MatchPatternSet(optional.origins, { ignorePath: true }); + let normalized = Object.assign({}, perms); + + for (let o of perms.origins) { + if (!patterns.subsumes(new MatchPattern(o))) { + throw new Error(`${o} was not declared in the manifest`); + } + // If this is one of the "all sites" permissions + allSites ||= lazy.Extension.isAllSitesPermission(o); + } + + if (allSites) { + // Grant/revoke ALL "all sites" optional permissions from the manifest. + let origins = perms.origins.concat( + optional.origins.filter(o => lazy.Extension.isAllSitesPermission(o)) + ); + normalized.origins = Array.from(new Set(origins)); + } + + for (let p of perms.permissions) { + if (!optional.permissions.includes(p)) { + throw new Error(`${p} was not declared in optional_permissions`); + } + } + + return normalized; + }, + _fixupAllUrlsPerms(perms) { // Unfortunately, we treat <all_urls> as an API permission as well. // If it is added to either, ensure it is added to both. @@ -361,8 +407,10 @@ export var ExtensionPermissions = { * Add new permissions for the given extension. `permissions` is * in the format that is passed to browser.permissions.request(). * + * @typedef {import("ExtensionCommon.sys.mjs").EventEmitter} EventEmitter + * * @param {string} extensionId The extension id - * @param {object} perms Object with permissions and origins array. + * @param {Perms} perms Object with permissions and origins array. * @param {EventEmitter} [emitter] optional object implementing emitter interfaces */ async add(extensionId, perms, emitter) { @@ -401,7 +449,7 @@ export var ExtensionPermissions = { * in the format that is passed to browser.permissions.request(). * * @param {string} extensionId The extension id - * @param {object} perms Object with permissions and origins array. + * @param {Perms} perms Object with permissions and origins array. * @param {EventEmitter} [emitter] optional object implementing emitter interfaces */ async remove(extensionId, perms, emitter) { diff --git a/toolkit/components/extensions/ExtensionProcessScript.sys.mjs b/toolkit/components/extensions/ExtensionProcessScript.sys.mjs index 93746cb0ca..ef5694be9f 100644 --- a/toolkit/components/extensions/ExtensionProcessScript.sys.mjs +++ b/toolkit/components/extensions/ExtensionProcessScript.sys.mjs @@ -11,6 +11,7 @@ import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; +/** @type {Lazy} */ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { @@ -49,7 +50,7 @@ ChromeUtils.defineLazyGetter(lazy, "isContentScriptProcess", () => { }); var extensions = new DefaultWeakMap(policy => { - return new lazy.ExtensionChild.BrowserExtensionContent(policy); + return new lazy.ExtensionChild(policy); }); var pendingExtensions = new Map(); @@ -342,7 +343,7 @@ ExtensionManager = { perms.delete(perm); } } - policy.permissions = perms; + policy.permissions = Array.from(perms); } } diff --git a/toolkit/components/extensions/ExtensionShortcuts.sys.mjs b/toolkit/components/extensions/ExtensionShortcuts.sys.mjs index 7b30fa2cdf..17cff67eb9 100644 --- a/toolkit/components/extensions/ExtensionShortcuts.sys.mjs +++ b/toolkit/components/extensions/ExtensionShortcuts.sys.mjs @@ -6,6 +6,7 @@ import { ExtensionCommon } from "resource://gre/modules/ExtensionCommon.sys.mjs" import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs"; +/** @type {Lazy} */ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { @@ -462,7 +463,6 @@ export class ExtensionShortcuts { let win = event.target.ownerGlobal; action.triggerAction(win); } else { - this.extension.tabManager.addActiveTabPermission(); this.onCommand(name); } }); diff --git a/toolkit/components/extensions/ExtensionStorage.sys.mjs b/toolkit/components/extensions/ExtensionStorage.sys.mjs index b1b09d137f..5317fb2a91 100644 --- a/toolkit/components/extensions/ExtensionStorage.sys.mjs +++ b/toolkit/components/extensions/ExtensionStorage.sys.mjs @@ -8,6 +8,7 @@ import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs"; const { DefaultWeakMap, ExtensionError } = ExtensionUtils; +/** @type {Lazy} */ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { @@ -89,7 +90,7 @@ function serialize(name, anonymizedName, value) { } export var ExtensionStorage = { - // Map<extension-id, Promise<JSONFile>> + /** @type {Map<string, Promise<typeof lazy.JSONFile>>} */ jsonFilePromises: new Map(), listeners: new Map(), @@ -157,7 +158,7 @@ export var ExtensionStorage = { * * @param {any} value * The value to sanitize. - * @param {Context} context + * @param {BaseContext} context * The extension context in which to sanitize the value * @returns {value} * The sanitized value. diff --git a/toolkit/components/extensions/ExtensionStorageIDB.sys.mjs b/toolkit/components/extensions/ExtensionStorageIDB.sys.mjs index 604d29b4cf..5223d57466 100644 --- a/toolkit/components/extensions/ExtensionStorageIDB.sys.mjs +++ b/toolkit/components/extensions/ExtensionStorageIDB.sys.mjs @@ -5,6 +5,7 @@ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; import { IndexedDB } from "resource://gre/modules/IndexedDB.sys.mjs"; +/** @type {Lazy} */ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { @@ -801,6 +802,8 @@ export var ExtensionStorageIDB = { * from the internal IndexedDB operations have to be converted into an ExtensionError * to be accessible to the extension code). * + * @typedef {import("ExtensionUtils.sys.mjs").ExtensionError} ExtensionError + * * @param {object} params * @param {Error|ExtensionError|DOMException} params.error * The error object to normalize. diff --git a/toolkit/components/extensions/ExtensionStorageSync.sys.mjs b/toolkit/components/extensions/ExtensionStorageSync.sys.mjs index 3f82d91fac..06f0cc4310 100644 --- a/toolkit/components/extensions/ExtensionStorageSync.sys.mjs +++ b/toolkit/components/extensions/ExtensionStorageSync.sys.mjs @@ -10,6 +10,7 @@ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; const NS_ERROR_DOM_QUOTA_EXCEEDED_ERR = 0x80530016; +/** @type {Lazy} */ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { @@ -55,6 +56,7 @@ ExtensionStorageApiCallback.prototype = { }, handleError(code, message) { + /** @type {Error & { code?: number }} */ let e = new Error(message); e.code = code; Cu.reportError(e); diff --git a/toolkit/components/extensions/ExtensionStorageSyncKinto.sys.mjs b/toolkit/components/extensions/ExtensionStorageSyncKinto.sys.mjs index ace6e16c2c..62493e3b07 100644 --- a/toolkit/components/extensions/ExtensionStorageSyncKinto.sys.mjs +++ b/toolkit/components/extensions/ExtensionStorageSyncKinto.sys.mjs @@ -32,6 +32,7 @@ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs"; +/** @type {Lazy} */ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { @@ -120,11 +121,6 @@ function throwIfNoFxA(fxAccounts, action) { } } -// Global ExtensionStorageSyncKinto instance that extensions and Fx Sync use. -// On Android, because there's no FXAccounts instance, any syncing -// operations will fail. -export var extensionStorageSyncKinto = null; - /** * Utility function to enforce an order of fields when computing an HMAC. * @@ -558,6 +554,8 @@ class CryptoCollection { * "characters" are values, each within [0, 255]. You can produce * such a bytestring using e.g. CommonUtils.encodeUTF8. * + * @typedef {string} bytestring + * * The returned value is a base64url-encoded string of the hash. * * @param {bytestring} value The value to be hashed. @@ -696,7 +694,7 @@ let CollectionKeyEncryptionRemoteTransformer = class extends EncryptionRemoteTra * * @param {Extension} extension * The extension whose context just ended. - * @param {Context} context + * @param {BaseContext} context * The context that just ended. */ function cleanUpForContext(extension, context) { @@ -1199,7 +1197,7 @@ export class ExtensionStorageSyncKinto { * @param {Extension} extension * The extension for which we are seeking * a collection. - * @param {Context} context + * @param {BaseContext} context * The context of the extension, so that we can * stop syncing the collection when the extension ends. * @returns {Promise<Collection>} @@ -1370,7 +1368,14 @@ export class ExtensionStorageSyncKinto { } } -extensionStorageSyncKinto = new ExtensionStorageSyncKinto(_fxaService); +/** + * Global ExtensionStorageSyncKinto instance that extensions and Fx Sync use. + * On Android, because there's no FXAccounts instance, any syncing + * operations will fail. + */ +export const extensionStorageSyncKinto = new ExtensionStorageSyncKinto( + _fxaService +); // For test use only. export const KintoStorageTestUtils = { diff --git a/toolkit/components/extensions/ExtensionTelemetry.sys.mjs b/toolkit/components/extensions/ExtensionTelemetry.sys.mjs index 57f052372c..95b71ce007 100644 --- a/toolkit/components/extensions/ExtensionTelemetry.sys.mjs +++ b/toolkit/components/extensions/ExtensionTelemetry.sys.mjs @@ -68,7 +68,7 @@ export function getTrimmedString(str) { * If the resulting string is longer than 80 characters it is going to be * trimmed using the `getTrimmedString` helper function. * - * @param {Error | DOMException | Components.Exception} error + * @param {Error | DOMException | ReturnType<typeof Components.Exception>} error * The error object to convert into a string representation. * * @returns {string} @@ -146,7 +146,7 @@ class ExtensionTelemetryMetric { * @param {string} metric * The Glean timing_distribution metric to record (used to retrieve the Glean metric type from the * GLEAN_METRICS_TYPES map). - * @param {Extension | BrowserExtensionContent} extension + * @param {Extension | ExtensionChild} extension * The extension to record the telemetry for. * @param {any | undefined} [obj = extension] * An optional object the timing_distribution method call should be related to @@ -207,7 +207,7 @@ class ExtensionTelemetryMetric { * The stopwatch method to call ("start", "finish" or "cancel"). * @param {string} metric * The stopwatch metric to record (used to retrieve the base histogram id from the HISTOGRAMS_IDS object). - * @param {Extension | BrowserExtensionContent} extension + * @param {Extension | ExtensionChild} extension * The extension to record the telemetry for. * @param {any | undefined} [obj = extension] * An optional telemetry stopwatch object (which defaults to the extension parameter when missing). @@ -242,7 +242,7 @@ class ExtensionTelemetryMetric { * @param {string} metric * The metric to record (used to retrieve the base histogram id from the _histogram object). * @param {object} options - * @param {Extension | BrowserExtensionContent} options.extension + * @param {Extension | ExtensionChild} options.extension * The extension to record the telemetry for. * @param {string | undefined} [options.category] * An optional histogram category. @@ -290,7 +290,7 @@ class ExtensionTelemetryMetric { // NOTE: extensionsTiming may become a property of the GLEAN_METRICS_TYPES // map once we may introduce new histograms that are not part of the // extensionsTiming Glean metrics category. - Glean.extensionsTiming[metric].accumulateSamples([value]); + Glean.extensionsTiming[metric].accumulateSingleSample(value); break; } case "labeled_counter": { @@ -324,6 +324,8 @@ const metricsCache = new Map(); * ExtensionTelemetry.extensionStartup.stopwatchStart(extension); * ExtensionTelemetry.browserActionPreloadResult.histogramAdd({category: "Shown", extension}); */ +/** @type {Record<string, ExtensionTelemetryMetric>} */ +// @ts-ignore no easy way in TS to say Proxy is a different type from target. export var ExtensionTelemetry = new Proxy(metricsCache, { get(target, prop) { // NOTE: if we would be start adding glean probes that do not have a unified diff --git a/toolkit/components/extensions/ExtensionUtils.sys.mjs b/toolkit/components/extensions/ExtensionUtils.sys.mjs index 45f22aa530..84f7b25c01 100644 --- a/toolkit/components/extensions/ExtensionUtils.sys.mjs +++ b/toolkit/components/extensions/ExtensionUtils.sys.mjs @@ -43,7 +43,7 @@ function promiseTimeout(delay) { * An Error subclass for which complete error messages are always passed * to extensions, rather than being interpreted as an unknown error. */ -class ExtensionError extends DOMException { +export class ExtensionError extends DOMException { constructor(message) { super(message, "ExtensionError"); } @@ -67,7 +67,7 @@ function filterStack(error) { * only logged internally and raised to the worker script as * the generic unexpected error). */ -class WorkerExtensionError extends DOMException { +export class WorkerExtensionError extends DOMException { constructor(message) { super(message, "Error"); } @@ -122,9 +122,9 @@ function getInnerWindowID(window) { * A set with a limited number of slots, which flushes older entries as * newer ones are added. * - * @param {integer} limit + * @param {number} limit * The maximum size to trim the set to after it grows too large. - * @param {integer} [slop = limit * .25] + * @param {number} [slop = limit * .25] * The number of extra entries to allow in the set after it * reaches the size limit, before it is truncated to the limit. * @param {Iterable} [iterable] @@ -345,5 +345,4 @@ export var ExtensionUtils = { DefaultWeakMap, ExtensionError, LimitedSet, - WorkerExtensionError, }; diff --git a/toolkit/components/extensions/ExtensionWorkerChild.sys.mjs b/toolkit/components/extensions/ExtensionWorkerChild.sys.mjs index 44188d7ddb..d3fd477b10 100644 --- a/toolkit/components/extensions/ExtensionWorkerChild.sys.mjs +++ b/toolkit/components/extensions/ExtensionWorkerChild.sys.mjs @@ -10,8 +10,14 @@ */ import { - ExtensionChild, + ChildAPIManager, + ChildLocalAPIImplementation, ExtensionActivityLogChild, + MessageEvent, + Messenger, + Port, + ProxyAPIImplementation, + SimpleEventAPI, } from "resource://gre/modules/ExtensionChild.sys.mjs"; import { ExtensionCommon } from "resource://gre/modules/ExtensionCommon.sys.mjs"; @@ -19,20 +25,13 @@ import { ExtensionPageChild, getContextChildManagerGetter, } from "resource://gre/modules/ExtensionPageChild.sys.mjs"; -import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs"; +import { + ExtensionUtils, + WorkerExtensionError, +} from "resource://gre/modules/ExtensionUtils.sys.mjs"; const { BaseContext, redefineGetter } = ExtensionCommon; -const { - ChildAPIManager, - ChildLocalAPIImplementation, - MessageEvent, - Messenger, - Port, - ProxyAPIImplementation, - SimpleEventAPI, -} = ExtensionChild; - const { DefaultMap, getUniqueId } = ExtensionUtils; /** @@ -341,7 +340,7 @@ class WebIDLChildAPIManager extends ChildAPIManager { * The object that represents the API request received * (including arguments, an event listener wrapper etc) * - * @returns {mozIExtensionAPIRequestResult} + * @returns {Partial<mozIExtensionAPIRequestResult>} * Result for the API request, either a value to be returned * (which has to be a value that can be structure cloned * if the request was originated from the worker thread) or @@ -373,9 +372,8 @@ class WebIDLChildAPIManager extends ChildAPIManager { * into the expected mozIExtensionAPIRequestResult. * * @param {Error | WorkerExtensionError} error - * @returns {mozIExtensionAPIRequestResult} + * @returns {Partial<mozIExtensionAPIRequestResult>} */ - handleExtensionError(error) { // Propagate an extension error to the caller on the worker thread. if (error instanceof this.context.Error) { @@ -402,7 +400,6 @@ class WebIDLChildAPIManager extends ChildAPIManager { * @returns {any} * @throws {Error | WorkerExtensionError} */ - callAPIImplementation(request, impl) { const { requestType, normalizedArgs } = request; @@ -480,7 +477,7 @@ class WebIDLChildAPIManager extends ChildAPIManager { * Return an ExtensionAPI class instance given its namespace. * * @param {string} namespace - * @returns {ExtensionAPI} + * @returns {import("ExtensionCommon.sys.mjs").ExtensionAPI} */ getExtensionAPIInstance(namespace) { return this.apiCan.apis.get(namespace); @@ -569,7 +566,7 @@ class WorkerContextChild extends BaseContext { * This WorkerContextChild represents an addon execution environment * that is running on the worker thread in an extension child process. * - * @param {BrowserExtensionContent} extension This context's owner. + * @param {ExtensionChild} extension This context's owner. * @param {object} params * @param {mozIExtensionServiceWorkerInfo} params.serviceWorkerInfo */ @@ -607,7 +604,7 @@ class WorkerContextChild extends BaseContext { // ExtensionAPIRequestHandler as errors that should be propagated to // the worker thread and received by extension code that originated // the API request. - Error: ExtensionUtils.WorkerExtensionError, + Error: WorkerExtensionError, }; } @@ -616,6 +613,7 @@ class WorkerContextChild extends BaseContext { return { workerDescriptorId }; } + /** @type {ConduitGen} */ openConduit(subject, address) { let proc = ChromeUtils.domProcessChild; let conduit = proc.getActor("ProcessConduits").openConduit(subject, { @@ -658,7 +656,7 @@ class WorkerContextChild extends BaseContext { * Captures the most recent stack frame from the WebIDL API request being * processed. * - * @returns {SavedFrame?} + * @returns {nsIStackFrame} */ getCaller() { return this.webidlAPIRequest?.callerSavedFrame; @@ -719,7 +717,7 @@ export var ExtensionWorkerChild = { * Create an extension worker context (on a mozExtensionAPIRequest with * requestType "initWorkerContext"). * - * @param {BrowserExtensionContent} extension + * @param {ExtensionChild} extension * The extension for which the context should be created. * @param {mozIExtensionServiceWorkerInfo} serviceWorkerInfo */ @@ -751,7 +749,7 @@ export var ExtensionWorkerChild = { * Get an existing extension worker context for the given extension and * service worker. * - * @param {BrowserExtensionContent} extension + * @param {ExtensionChild} extension * The extension for which the context should be created. * @param {mozIExtensionServiceWorkerInfo} serviceWorkerInfo * diff --git a/toolkit/components/extensions/ExtensionXPCShellUtils.sys.mjs b/toolkit/components/extensions/ExtensionXPCShellUtils.sys.mjs index 27323dc8b3..3489a2caba 100644 --- a/toolkit/components/extensions/ExtensionXPCShellUtils.sys.mjs +++ b/toolkit/components/extensions/ExtensionXPCShellUtils.sys.mjs @@ -7,6 +7,7 @@ import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; import { XPCShellContentUtils } from "resource://testing-common/XPCShellContentUtils.sys.mjs"; +/** @type {Lazy} */ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { @@ -32,9 +33,9 @@ let BASE_MANIFEST = Object.freeze({ }); class ExtensionWrapper { - /** @type {AddonWrapper} */ + /** @type {import("resource://gre/modules/addons/XPIDatabase.sys.mjs").AddonWrapper} */ addon; - /** @type {Promise<AddonWrapper>} */ + /** @type {Promise} */ addonPromise; /** @type {nsIFile[]} */ cleanupFiles; diff --git a/toolkit/components/extensions/Schemas.sys.mjs b/toolkit/components/extensions/Schemas.sys.mjs index e98dfb36f0..b107036355 100644 --- a/toolkit/components/extensions/Schemas.sys.mjs +++ b/toolkit/components/extensions/Schemas.sys.mjs @@ -11,6 +11,7 @@ import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs"; var { DefaultMap, DefaultWeakMap } = ExtensionUtils; +/** @type {Lazy} */ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { @@ -262,6 +263,23 @@ const POSTPROCESSORS = { return string; }, + webRequestBlockingOrAuthProviderPermissionRequired(string, context) { + if ( + string === "blocking" && + !( + context.hasPermission("webRequestBlocking") || + context.hasPermission("webRequestAuthProvider") + ) + ) { + throw new context.cloneScope.Error( + "Using webRequest.onAuthRequired.addListener with the " + + "blocking option requires either the 'webRequestBlocking' " + + "or 'webRequestAuthProvider' permission." + ); + } + + return string; + }, requireBackgroundServiceWorkerEnabled(value, context) { if (WebExtensionPolicy.backgroundServiceWorkerEnabled) { return value; @@ -904,7 +922,8 @@ class InjectionContext extends Context { * @param {string} _namespace The full path to the namespace of the API, minus * the name of the method or property. E.g. "storage.local". * @param {string} _name The name of the method, property or event. - * @returns {SchemaAPIInterface} The implementation of the API. + * @returns {import("ExtensionCommon.sys.mjs").SchemaAPIInterface} + * The implementation of the API. */ getImplementation(_namespace, _name) { throw new Error("Not implemented"); @@ -1125,7 +1144,7 @@ const FORMATS = { }, strictRelativeUrl(string, context) { - void FORMATS.unresolvedRelativeUrl(string, context); + void FORMATS.unresolvedRelativeUrl(string); return FORMATS.relativeUrl(string, context); }, @@ -1220,8 +1239,8 @@ const FORMATS = { throw new Error(errorMessage); }, - manifestShortcutKeyOrEmpty(string, context) { - return string === "" ? "" : FORMATS.manifestShortcutKey(string, context); + manifestShortcutKeyOrEmpty(string) { + return string === "" ? "" : FORMATS.manifestShortcutKey(string); }, versionString(string, context) { @@ -1470,26 +1489,33 @@ class Type extends Entry { } } - // Takes a value, checks that it has the correct type, and returns a - // "normalized" version of the value. The normalized version will - // include "nulls" in place of omitted optional properties. The - // result of this function is either {error: "Some type error"} or - // {value: <normalized-value>}. + /** + * Takes a value, checks that it has the correct type, and returns a + * "normalized" version of the value. The normalized version will + * include "nulls" in place of omitted optional properties. The + * result of this function is either {error: "Some type error"} or + * {value: <normalized-value>}. + */ normalize(value, context) { return context.error("invalid type"); } - // Unlike normalize, this function does a shallow check to see if - // |baseType| (one of the possible getValueBaseType results) is - // valid for this type. It returns true or false. It's used to fill - // in optional arguments to functions before actually type checking - - checkBaseType() { + /** + * Unlike normalize, this function does a shallow check to see if + * |baseType| (one of the possible getValueBaseType results) is + * valid for this type. It returns true or false. It's used to fill + * in optional arguments to functions before actually type checking + * + * @param {string} _baseType + */ + checkBaseType(_baseType) { return false; } - // Helper method that simply relies on checkBaseType to implement - // normalize. Subclasses can choose to use it or not. + /** + * Helper method that simply relies on checkBaseType to implement + * normalize. Subclasses can choose to use it or not. + */ normalizeBase(type, value, context) { if (this.checkBaseType(getValueBaseType(value))) { this.checkDeprecated(context, value); @@ -3458,22 +3484,26 @@ class SchemaRoots extends Namespaces { * other schema roots. May extend a base namespace, in which case schemas in * this root may refer to types in a base, but not vice versa. * - * @param {SchemaRoot|Array<SchemaRoot>|null} base - * A base schema root (or roots) from which to derive, or null. - * @param {Map<string, Array|StructuredCloneHolder>} schemaJSON - * A map of schema URLs and corresponding JSON blobs from which to - * populate this root namespace. + * @implements {SchemaInject} */ export class SchemaRoot extends Namespace { + /** + * @param {SchemaRoot|SchemaRoot[]} base + * A base schema root (or roots) from which to derive, or null. + * @param {Map<string, Array|StructuredCloneHolder>} schemaJSON + * A map of schema URLs and corresponding JSON blobs from which to + * populate this root namespace. + */ constructor(base, schemaJSON) { super(null, "", []); if (Array.isArray(base)) { - base = new SchemaRoots(this, base); + this.base = new SchemaRoots(this, base); + } else { + this.base = base; } this.root = this; - this.base = base; this.schemaJSON = schemaJSON; } @@ -3555,7 +3585,7 @@ export class SchemaRoot extends Namespace { parseSchemas() { for (let [key, schema] of this.schemaJSON.entries()) { try { - if (typeof schema.deserialize === "function") { + if (StructuredCloneHolder.isInstance(schema)) { schema = schema.deserialize(globalThis, isParentProcess); // If we're in the parent process, we need to keep the @@ -3607,7 +3637,7 @@ export class SchemaRoot extends Namespace { * * @param {object} dest The root namespace for the APIs. * This object is usually exposed to extensions as "chrome" or "browser". - * @param {object} wrapperFuncs An implementation of the InjectionContext + * @param {InjectionContext} wrapperFuncs An implementation of the InjectionContext * interface, which runs the actual functionality of the generated API. */ inject(dest, wrapperFuncs) { @@ -3651,6 +3681,11 @@ export class SchemaRoot extends Namespace { } } +/** + * @typedef {{ inject: typeof Schemas.inject }} SchemaInject + * Interface SchemaInject as used by SchemaApiManager, + * with the one method shared across Schemas and SchemaRoot. + */ export var Schemas = { initialized: false, @@ -3676,6 +3711,7 @@ export var Schemas = { extContext => new Context(extContext) ), + /** @returns {SchemaRoot} */ get rootSchema() { if (!this.initialized) { this.init(); @@ -3833,7 +3869,7 @@ export var Schemas = { * * @param {object} dest The root namespace for the APIs. * This object is usually exposed to extensions as "chrome" or "browser". - * @param {object} wrapperFuncs An implementation of the InjectionContext + * @param {InjectionContext} wrapperFuncs An implementation of the InjectionContext * interface, which runs the actual functionality of the generated API. */ inject(dest, wrapperFuncs) { @@ -3898,6 +3934,7 @@ export var Schemas = { ? `${apiNamespace}.${apiName}.${requestType}` : `${apiNamespace}.${apiName}` ).split("."); + /** @type {Namespace|CallEntry} */ let apiSchema = this.getNamespace(ns); // Keep track of the current schema path, populated while navigating the nested API schema @@ -3926,7 +3963,7 @@ export var Schemas = { throw new Error(`API Schema not found for ${schemaPath.join(".")}`); } - if (!apiSchema.checkParameters) { + if (!(apiSchema instanceof CallEntry)) { throw new Error( `Unexpected API Schema type for ${schemaPath.join( "." diff --git a/toolkit/components/extensions/WebNavigation.sys.mjs b/toolkit/components/extensions/WebNavigation.sys.mjs index 7235aaeb4e..c33a45db81 100644 --- a/toolkit/components/extensions/WebNavigation.sys.mjs +++ b/toolkit/components/extensions/WebNavigation.sys.mjs @@ -4,6 +4,7 @@ import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; +/** @type {Lazy} */ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { @@ -23,9 +24,12 @@ function getBrowser(bc) { } export var WebNavigationManager = { - // Map[string -> Map[listener -> URLFilter]] + /** @type {Map<string, Set<callback>>} */ listeners: new Map(), + /** @type {WeakMap<XULBrowserElement, object>} */ + recentTabTransitionData: new WeakMap(), + init() { // Collect recent tab transition data in a WeakMap: // browser -> tabTransitionData @@ -123,9 +127,9 @@ export var WebNavigationManager = { * The data for the autocompleted item. * @param {object} [acData.result] * The result information associated with the navigation action. - * @param {UrlbarUtils.RESULT_TYPE} [acData.result.type] + * @param {typeof lazy.UrlbarUtils.RESULT_TYPE} [acData.result.type] * The result type associated with the navigation action. - * @param {UrlbarUtils.RESULT_SOURCE} [acData.result.source] + * @param {typeof lazy.UrlbarUtils.RESULT_SOURCE} [acData.result.source] * The result source associated with the navigation action. */ onURLBarUserStartNavigation(acData) { diff --git a/toolkit/components/extensions/WebNavigationFrames.sys.mjs b/toolkit/components/extensions/WebNavigationFrames.sys.mjs index 211698a88e..20db413bc0 100644 --- a/toolkit/components/extensions/WebNavigationFrames.sys.mjs +++ b/toolkit/components/extensions/WebNavigationFrames.sys.mjs @@ -45,7 +45,7 @@ function getParentFrameId(bc) { /** * Convert a BrowsingContext into internal FrameDetail json. * - * @param {BrowsingContext} bc + * @param {CanonicalBrowsingContext} bc * @returns {FrameDetail} */ function getFrameDetail(bc) { diff --git a/toolkit/components/extensions/extIWebNavigation.idl b/toolkit/components/extensions/extIWebNavigation.idl index 3095d93d9f..1db9e73808 100644 --- a/toolkit/components/extensions/extIWebNavigation.idl +++ b/toolkit/components/extensions/extIWebNavigation.idl @@ -17,8 +17,8 @@ interface extIWebNavigation : nsISupports void onHistoryChange(in BrowsingContext bc, in jsval transitionData, in nsIURI location, - in bool isHistoryStateUpdated, - in bool isReferenceFragmentUpdated); + in boolean isHistoryStateUpdated, + in boolean isReferenceFragmentUpdated); void onStateChange(in BrowsingContext bc, in nsIURI requestURI, diff --git a/toolkit/components/extensions/mozIExtensionAPIRequestHandling.idl b/toolkit/components/extensions/mozIExtensionAPIRequestHandling.idl index 0a2e3c7a5d..d7eb40345b 100644 --- a/toolkit/components/extensions/mozIExtensionAPIRequestHandling.idl +++ b/toolkit/components/extensions/mozIExtensionAPIRequestHandling.idl @@ -35,7 +35,7 @@ interface mozIExtensionListenerCallOptions : nsISupports // An optional boolean to be set to true if the api object should be // prepended to the rest of the call arguments (by default it is appended). - readonly attribute bool apiObjectPrepended; + readonly attribute boolean apiObjectPrepended; cenum CallbackType: 8 { // Default: no callback argument is passed to the call to the event listener. diff --git a/toolkit/components/extensions/mozIExtensionProcessScript.idl b/toolkit/components/extensions/mozIExtensionProcessScript.idl index 84b33a9d02..47e442149c 100644 --- a/toolkit/components/extensions/mozIExtensionProcessScript.idl +++ b/toolkit/components/extensions/mozIExtensionProcessScript.idl @@ -17,5 +17,5 @@ interface mozIExtensionProcessScript : nsISupports in mozIDOMWindow window); void initExtensionDocument(in nsISupports extension, in Document doc, - in bool privileged); + in boolean privileged); }; diff --git a/toolkit/components/extensions/parent/ext-identity.js b/toolkit/components/extensions/parent/ext-identity.js index bd53163305..f0f63dbf34 100644 --- a/toolkit/components/extensions/parent/ext-identity.js +++ b/toolkit/components/extensions/parent/ext-identity.js @@ -12,7 +12,7 @@ var { promiseDocumentLoaded } = ExtensionUtils; const checkRedirected = (url, redirectURI) => { return new Promise((resolve, reject) => { - let xhr = new XMLHttpRequest(); + let xhr = new XMLHttpRequest({ mozAnon: false }); xhr.open("GET", url); // We expect this if the user has not authenticated. xhr.onload = () => { diff --git a/toolkit/components/extensions/parent/ext-runtime.js b/toolkit/components/extensions/parent/ext-runtime.js index d1c03d9e0d..3f9c0f8857 100644 --- a/toolkit/components/extensions/parent/ext-runtime.js +++ b/toolkit/components/extensions/parent/ext-runtime.js @@ -248,7 +248,7 @@ this.runtime = class extends ExtensionAPIPersistent { }, openOptionsPage: function () { - if (!extension.manifest.options_ui) { + if (!extension.optionsPageProperties) { return Promise.reject({ message: "No `options_ui` declared" }); } diff --git a/toolkit/components/extensions/parent/ext-webRequest.js b/toolkit/components/extensions/parent/ext-webRequest.js index 4f0ea90abd..f94c773e2e 100644 --- a/toolkit/components/extensions/parent/ext-webRequest.js +++ b/toolkit/components/extensions/parent/ext-webRequest.js @@ -64,7 +64,11 @@ function registerEvent( filter2.incognito = filter.incognito; } - let blockingAllowed = extension.hasPermission("webRequestBlocking"); + let blockingAllowed = + eventName == "onAuthRequired" + ? extension.hasPermission("webRequestBlocking") || + extension.hasPermission("webRequestAuthProvider") + : extension.hasPermission("webRequestBlocking"); let info2 = []; if (info) { diff --git a/toolkit/components/extensions/schemas/manifest.json b/toolkit/components/extensions/schemas/manifest.json index 14f78ba564..384b168e39 100644 --- a/toolkit/components/extensions/schemas/manifest.json +++ b/toolkit/components/extensions/schemas/manifest.json @@ -173,11 +173,15 @@ "optional": true }, + "options_page": { + "$ref": "ExtensionURL", + "optional": true, + "description": "Alias property for options_ui.page, ignored when options_ui.page is set. When using this property the options page is always opened in a new tab." + }, + "options_ui": { "type": "object", - "optional": true, - "properties": { "page": { "$ref": "ExtensionURL" }, "browser_style": { diff --git a/toolkit/components/extensions/schemas/web_request.json b/toolkit/components/extensions/schemas/web_request.json index e4405f24c3..a1528d87f6 100644 --- a/toolkit/components/extensions/schemas/web_request.json +++ b/toolkit/components/extensions/schemas/web_request.json @@ -9,6 +9,7 @@ "type": "string", "enum": [ "webRequest", + "webRequestAuthProvider", "webRequestBlocking", "webRequestFilterResponse", "webRequestFilterResponse.serviceWorkerScript" @@ -82,7 +83,7 @@ "id": "OnAuthRequiredOptions", "type": "string", "enum": ["responseHeaders", "blocking", "asyncBlocking"], - "postprocess": "webRequestBlockingPermissionRequired" + "postprocess": "webRequestBlockingOrAuthProviderPermissionRequired" }, { "id": "OnResponseStartedOptions", diff --git a/toolkit/components/extensions/storage/webext_storage_bridge/src/area.rs b/toolkit/components/extensions/storage/webext_storage_bridge/src/area.rs index 1418ccca29..ec4f2057b9 100644 --- a/toolkit/components/extensions/storage/webext_storage_bridge/src/area.rs +++ b/toolkit/components/extensions/storage/webext_storage_bridge/src/area.rs @@ -13,7 +13,7 @@ use std::{ }; use golden_gate::{ApplyTask, BridgedEngine, FerryTask}; -use moz_task::{self, DispatchOptions, TaskRunnable}; +use moz_task::{DispatchOptions, TaskRunnable}; use nserror::{nsresult, NS_OK}; use nsstring::{nsACString, nsCString, nsString}; use thin_vec::ThinVec; diff --git a/toolkit/components/extensions/test/browser/browser.toml b/toolkit/components/extensions/test/browser/browser.toml index 33d54bddc2..33a3a6dc64 100644 --- a/toolkit/components/extensions/test/browser/browser.toml +++ b/toolkit/components/extensions/test/browser/browser.toml @@ -9,7 +9,6 @@ support-files = [ ["browser_ext_downloads_filters.js"] ["browser_ext_downloads_referrer.js"] -https_first_disabled = true ["browser_ext_eventpage_disableResetIdleForTest.js"] diff --git a/toolkit/components/extensions/test/browser/browser_ext_extension_page_tab_navigated.js b/toolkit/components/extensions/test/browser/browser_ext_extension_page_tab_navigated.js index 429d584a17..f85bbfe595 100644 --- a/toolkit/components/extensions/test/browser/browser_ext_extension_page_tab_navigated.js +++ b/toolkit/components/extensions/test/browser/browser_ext_extension_page_tab_navigated.js @@ -3,10 +3,6 @@ "use strict"; -const { AddonTestUtils } = ChromeUtils.importESModule( - "resource://testing-common/AddonTestUtils.sys.mjs" -); - // The test tasks in this test file tends to trigger an intermittent // exception raised from JSActor::AfterDestroy, because of a race between // when the WebExtensions API event is being emitted from the parent process @@ -18,23 +14,6 @@ PromiseTestUtils.allowMatchingRejectionsGlobally( /Actor 'Conduits' destroyed before query 'RunListener' was resolved/ ); -AddonTestUtils.initMochitest(this); - -const server = AddonTestUtils.createHttpServer({ - hosts: ["example.com", "anotherwebpage.org"], -}); - -server.registerPathHandler("/", (request, response) => { - response.write(`<!DOCTYPE html> - <html> - <head> - <meta charset="utf-8"> - <title>test webpage</title> - </head> - </html> - `); -}); - function createTestExtPage({ script }) { return `<!DOCTYPE html> <html> @@ -55,7 +34,7 @@ function createTestExtPageScript(name) { ); browser.test.sendMessage(`event-received:${pageName}`); }, - { types: ["main_frame"], urls: ["http://example.com/*"] } + { types: ["main_frame"], urls: ["https://example.com/*"] } ); /* eslint-disable mozilla/balanced-listeners */ window.addEventListener("pageshow", () => { @@ -93,7 +72,7 @@ async function triggerWebRequestListener(webPageURL) { add_task(async function test_extension_page_sameprocess_navigation() { const extension = ExtensionTestUtils.loadExtension({ manifest: { - permissions: ["webRequest", "http://example.com/*"], + permissions: ["webRequest", "https://example.com/*"], }, files: { "extpage1.html": createTestExtPage({ script: "extpage1.js" }), @@ -116,7 +95,7 @@ add_task(async function test_extension_page_sameprocess_navigation() { info("Wait for the extension page to be loaded"); await extension.awaitMessage("pageshow:extpage1"); - await triggerWebRequestListener("http://example.com"); + await triggerWebRequestListener("https://example.com"); await extension.awaitMessage("event-received:extpage1"); ok(true, "extpage1 got a webRequest event as expected"); @@ -131,7 +110,7 @@ add_task(async function test_extension_page_sameprocess_navigation() { info( "Trigger a web request event and expect extpage2 to be the only one receiving it" ); - await triggerWebRequestListener("http://example.com"); + await triggerWebRequestListener("https://example.com"); await extension.awaitMessage("event-received:extpage2"); ok(true, "extpage2 got a webRequest event as expected"); @@ -146,7 +125,7 @@ add_task(async function test_extension_page_sameprocess_navigation() { await extension.awaitMessage("pagehide:extpage2"); // We only expect extpage1 to be able to receive API events. - await triggerWebRequestListener("http://example.com"); + await triggerWebRequestListener("https://example.com"); await extension.awaitMessage("event-received:extpage1"); ok(true, "extpage1 got a webRequest event as expected"); @@ -159,7 +138,7 @@ add_task(async function test_extension_page_sameprocess_navigation() { add_task(async function test_extension_page_context_navigated_to_web_page() { const extension = ExtensionTestUtils.loadExtension({ manifest: { - permissions: ["webRequest", "http://example.com/*"], + permissions: ["webRequest", "https://example.com/*"], }, files: { "extpage.html": createTestExtPage({ script: "extpage.js" }), @@ -178,8 +157,8 @@ add_task(async function test_extension_page_context_navigated_to_web_page() { // navigated will be intermittently able to receive an event before it // is navigated to the webpage url (and moved into the BFCache or destroyed) // and trigger an intermittent failure of this test. - const webPageURL = "http://anotherwebpage.org/"; - const triggerWebRequestURL = "http://example.com/"; + const webPageURL = "https://example.net/"; + const triggerWebRequestURL = "https://example.com/"; info("Opening extension page in a new tab"); const extPageTab1 = await BrowserTestUtils.addTab(gBrowser, extPageURL); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_ntp_colors.js b/toolkit/components/extensions/test/browser/browser_ext_themes_ntp_colors.js index 8e2f5446c9..4ee92bf798 100644 --- a/toolkit/components/extensions/test/browser/browser_ext_themes_ntp_colors.js +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_ntp_colors.js @@ -26,17 +26,20 @@ async function test_ntp_theme(theme, isBrightText) { }); let browser = gBrowser.selectedBrowser; - let { originalBackground, originalCardBackground, originalColor } = await SpecialPowers.spawn(browser, [], function () { let doc = content.document; ok( !doc.documentElement.hasAttribute("lwt-newtab"), - "New tab page should not have lwt-newtab attribute" + `New tab page should not have lwt-newtab attribute` + ); + ok( + !doc.documentElement.hasAttribute("lwtheme"), + `New tab page should not have lwtheme attribute` ); ok( !doc.documentElement.hasAttribute("lwt-newtab-brighttext"), - `New tab page should not have lwt-newtab-brighttext attribute` + `New tab page not should have lwt-newtab-brighttext attribute` ); return { @@ -64,22 +67,39 @@ async function test_ntp_theme(theme, isBrightText) { Services.ppmm.sharedData.flush(); + let hasNtpColors = !!( + theme.colors.ntp_background || + theme.colors.ntp_text || + theme.colors.ntp_card_background + ); await SpecialPowers.spawn( browser, [ { isBrightText, + hasNtpColors, background: hexToCSS(theme.colors.ntp_background), card_background: hexToCSS(theme.colors.ntp_card_background), color: hexToCSS(theme.colors.ntp_text), }, ], - async function ({ isBrightText, background, card_background, color }) { + async function ({ + isBrightText, + hasNtpColors, + background, + card_background, + color, + }) { let doc = content.document; - ok( + is( doc.documentElement.hasAttribute("lwt-newtab"), + hasNtpColors, "New tab page should have lwt-newtab attribute" ); + ok( + doc.documentElement.hasAttribute("lwtheme"), + "New tab page should have lwtheme attribute" + ); is( doc.documentElement.hasAttribute("lwt-newtab-brighttext"), isBrightText, @@ -88,22 +108,24 @@ async function test_ntp_theme(theme, isBrightText) { } have lwt-newtab-brighttext attribute` ); - is( - content.getComputedStyle(doc.body).backgroundColor, - background, - "New tab page background should be set." - ); - is( - content.getComputedStyle(doc.querySelector(".top-site-outer .tile")) - .backgroundColor, - card_background, - "New tab page card background should be set." - ); - is( - content.getComputedStyle(doc.querySelector(".outer-wrapper")).color, - color, - "New tab page text color should be set." - ); + if (hasNtpColors) { + is( + content.getComputedStyle(doc.body).backgroundColor, + background, + "New tab page background should be set." + ); + is( + content.getComputedStyle(doc.querySelector(".top-site-outer .tile")) + .backgroundColor, + card_background, + "New tab page card background should be set." + ); + is( + content.getComputedStyle(doc.querySelector(".outer-wrapper")).color, + color, + "New tab page text color should be set." + ); + } } ); @@ -127,6 +149,10 @@ async function test_ntp_theme(theme, isBrightText) { "New tab page should not have lwt-newtab attribute" ); ok( + !doc.documentElement.hasAttribute("lwtheme"), + "New tab page should not have lwtheme attribute" + ); + ok( !doc.documentElement.hasAttribute("lwt-newtab-brighttext"), `New tab page should not have lwt-newtab-brighttext attribute` ); @@ -151,6 +177,17 @@ async function test_ntp_theme(theme, isBrightText) { ); } +async function waitForDarkMode(value) { + info(`waiting for dark mode: ${value}`); + const mq = matchMedia("(prefers-color-scheme: dark)"); + if (mq.matches == value) { + return; + } + await new Promise(r => { + mq.addEventListener("change", r, { once: true }); + }); +} + add_task(async function test_support_ntp_colors() { await SpecialPowers.pushPrefEnv({ set: [ @@ -163,11 +200,13 @@ add_task(async function test_support_ntp_colors() { ["layout.css.prefers-color-scheme.content-override", 1], // Override the system color scheme to light so this test passes on // machines with dark system color scheme. + // FIXME(emilio): This doesn't seem working reliably, at least on macOS. ["ui.systemUsesDarkTheme", 0], ], }); NewTabPagePreloading.removePreloadedBrowser(window); for (let url of ["about:newtab", "about:home"]) { + await waitForDarkMode(false); info("Opening url: " + url); await BrowserTestUtils.withNewTab({ gBrowser, url }, async browser => { await waitForAboutNewTabReady(browser, url); @@ -185,6 +224,7 @@ add_task(async function test_support_ntp_colors() { url ); + await waitForDarkMode(false); await test_ntp_theme( { colors: { @@ -198,6 +238,19 @@ add_task(async function test_support_ntp_colors() { true, url ); + + // Test a theme without any new tab page colors + await waitForDarkMode(false); + await test_ntp_theme( + { + colors: { + frame: ACCENT_COLOR, + tab_background_text: TEXT_COLOR, + }, + }, + false, + url + ); }); } }); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_ntp_colors_perwindow.js b/toolkit/components/extensions/test/browser/browser_ext_themes_ntp_colors_perwindow.js index 3b739322d6..54152d005c 100644 --- a/toolkit/components/extensions/test/browser/browser_ext_themes_ntp_colors_perwindow.js +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_ntp_colors_perwindow.js @@ -39,6 +39,10 @@ function test_ntp_theme(browser, theme, isBrightText) { doc.documentElement.hasAttribute("lwt-newtab"), "New tab page should have lwt-newtab attribute" ); + ok( + doc.documentElement.hasAttribute("lwtheme"), + "New tab page should have lwtheme attribute" + ); is( doc.documentElement.hasAttribute("lwt-newtab-brighttext"), isBrightText, @@ -90,6 +94,10 @@ function test_ntp_default_theme(browser) { "New tab page should not have lwt-newtab attribute" ); ok( + !doc.documentElement.hasAttribute("lwtheme"), + "New tab page should not have lwtheme attribute" + ); + ok( !doc.documentElement.hasAttribute("lwt-newtab-brighttext"), `New tab page should not have lwt-newtab-brighttext attribute` ); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_pbm.js b/toolkit/components/extensions/test/browser/browser_ext_themes_pbm.js index 3b36a256d0..d2dfb16e72 100644 --- a/toolkit/components/extensions/test/browser/browser_ext_themes_pbm.js +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_pbm.js @@ -336,7 +336,7 @@ add_task(async function test_pbm_dark_page_info() { await BrowserTestUtils.withNewTab( { gBrowser: win.gBrowser, url: "https://example.com" }, async () => { - let pageInfo = win.BrowserPageInfo(null, "securityTab"); + let pageInfo = win.BrowserCommands.pageInfo(null, "securityTab"); await BrowserTestUtils.waitForEvent(pageInfo, "page-info-init"); let prefersColorScheme = await getPrefersColorSchemeInfo({ diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_sanitization.js b/toolkit/components/extensions/test/browser/browser_ext_themes_sanitization.js index 89ebd3ae68..fdee1eb72d 100644 --- a/toolkit/components/extensions/test/browser/browser_ext_themes_sanitization.js +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_sanitization.js @@ -114,8 +114,9 @@ add_task(async function test_sanitization_transparent() { await extension.startup(); let navbar = document.querySelector("#nav-bar"); - Assert.ok( - window.getComputedStyle(navbar).boxShadow.includes("rgba(0, 0, 0, 0)"), + Assert.equal( + window.getComputedStyle(navbar).borderTopColor, + "rgba(0, 0, 0, 0)", "Top separator should be transparent" ); diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_separators.js b/toolkit/components/extensions/test/browser/browser_ext_themes_separators.js index 4da4927ccf..1b953269b6 100644 --- a/toolkit/components/extensions/test/browser/browser_ext_themes_separators.js +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_separators.js @@ -48,10 +48,9 @@ add_task(async function test_support_separator_properties() { await deprecatedMessagePromise; let navbar = document.querySelector("#nav-bar"); - Assert.ok( - window - .getComputedStyle(navbar) - .boxShadow.includes(`rgb(${hexToRGB(SEPARATOR_TOP_COLOR).join(", ")})`), + Assert.equal( + window.getComputedStyle(navbar).borderTopColor, + `rgb(${hexToRGB(SEPARATOR_TOP_COLOR).join(", ")})`, "Top separator color properly set" ); diff --git a/toolkit/components/extensions/test/mochitest/test_check_startupcache.html b/toolkit/components/extensions/test/mochitest/test_check_startupcache.html index 8cb529d18d..d1157472ec 100644 --- a/toolkit/components/extensions/test/mochitest/test_check_startupcache.html +++ b/toolkit/components/extensions/test/mochitest/test_check_startupcache.html @@ -41,7 +41,7 @@ add_task(async function check_ExtensionParent_StartupCache_is_non_empty() { let map = await chromeScript.promiseOneMessage("StartupCache_data"); chromeScript.destroy(); - // "manifests" is populated by Extension's parseManifest in Extension.jsm. + // "manifests" is populated by Extension's parseManifest in Extension.sys.mjs. const keys = ["manifests", "mochikit@mozilla.org", "2.0", "en-US"]; for (let key of keys) { map = map.get(key); diff --git a/toolkit/components/extensions/test/mochitest/test_ext_async_clipboard.html b/toolkit/components/extensions/test/mochitest/test_ext_async_clipboard.html index 708b5522c3..e7fdd6aad0 100644 --- a/toolkit/components/extensions/test/mochitest/test_ext_async_clipboard.html +++ b/toolkit/components/extensions/test/mochitest/test_ext_async_clipboard.html @@ -354,11 +354,11 @@ add_task(async function test_contentscript_clipboard_nocontents_readtext() { await extension.unload(); }); -// Test that performing read(...) when the clipboard is empty returns an empty ClipboardItem +// Test that performing read(...) when the clipboard is empty returns no ClipboardItem add_task(async function test_contentscript_clipboard_nocontents_read() { function contentScript() { clipboardRead().then(function(items) { - if (items[0].types.length) { + if (items.length) { browser.test.fail("Read read the wrong thing from clipboard, " + "ClipboardItem has this many entries: " + items[0].types.length); } else { diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_securecontext.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_securecontext.html index 093c26898f..f9230c9a5f 100644 --- a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_securecontext.html +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_securecontext.html @@ -14,6 +14,8 @@ await SpecialPowers.pushPrefEnv({ "set": [ ["dom.w3c_pointer_events.getcoalescedevents_only_in_securecontext", true], + // Test is intentionally testing in non-secure contexts. + ["dom.security.https_first", false] ] }); }); diff --git a/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect.html b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect.html index 85f98d5034..d474a7caee 100644 --- a/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect.html +++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect.html @@ -18,6 +18,7 @@ function background() { browser.test.assertTrue(port.sender.url.endsWith("file_sample.html"), "URL correct"); browser.test.assertTrue(port.sender.tab.url.endsWith("file_sample.html"), "tab URL correct"); browser.test.assertEq(port.sender.frameId, 0, "frameId of top frame"); + browser.test.assertEq(new URL(port.sender.url).origin, port.sender.origin, "sender origin correct"); let expected = "message 1"; port.onMessage.addListener(msg => { diff --git a/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect2.html b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect2.html index 13b9029c48..32620b5b3b 100644 --- a/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect2.html +++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect2.html @@ -21,6 +21,7 @@ function backgroundScript(token) { browser.runtime.onConnect.addListener(port => { browser.test.assertTrue(port.sender.url.endsWith("file_sample.html"), "sender url correct"); browser.test.assertTrue(port.sender.tab.url.endsWith("file_sample.html"), "sender url correct"); + browser.test.assertEq(new URL(port.sender.url).origin, port.sender.origin, "sender origin correct"); let tabId = port.sender.tab.id; browser.tabs.connect(tabId, {name: token}); diff --git a/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_iframe.html b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_iframe.html index 9c64635063..e3e0e80f2a 100644 --- a/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_iframe.html +++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_iframe.html @@ -30,12 +30,15 @@ add_task(async function connect_from_background_frame() { } async function background() { const FRAME_URL = "https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html"; + const FRAME_ORIGIN = new URL(FRAME_URL).origin; + browser.runtime.onConnect.addListener(port => { // The next two assertions are the reason for this being a mochitest // instead of a xpcshell test. browser.test.assertEq(port.sender.tab, undefined, "Sender is not a tab"); browser.test.assertEq(port.sender.frameId, undefined, "frameId unset"); browser.test.assertEq(port.sender.url, FRAME_URL, "Expected sender URL"); + browser.test.assertEq(port.sender.origin, FRAME_ORIGIN, "Expected sender origin"); port.onMessage.addListener(msg => { browser.test.assertEq("pong", msg, "Reply from content script"); port.disconnect(); @@ -88,6 +91,8 @@ add_task(async function connect_from_content_script_in_frame() { async function background() { const TAB_URL = "https://example.org/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html"; const FRAME_URL = "https://example.org/tests/toolkit/components/extensions/test/mochitest/file_contains_img.html"; + const FRAME_ORIGIN = new URL(FRAME_URL).origin; + let createdTab; browser.runtime.onConnect.addListener(port => { // The next two assertions are the reason for this being a mochitest @@ -95,6 +100,7 @@ add_task(async function connect_from_content_script_in_frame() { browser.test.assertEq(port.sender.tab.url, TAB_URL, "Sender is the tab"); browser.test.assertTrue(port.sender.frameId > 0, "frameId is set"); browser.test.assertEq(port.sender.url, FRAME_URL, "Expected sender URL"); + browser.test.assertEq(port.sender.origin, FRAME_ORIGIN, "Expected sender origin"); browser.test.assertEq(createdTab.id, port.sender.tab.id, "Tab to close"); browser.tabs.remove(port.sender.tab.id).then(() => { diff --git a/toolkit/components/extensions/test/mochitest/test_ext_tabs_captureTab.html b/toolkit/components/extensions/test/mochitest/test_ext_tabs_captureTab.html index ab06a965ed..20f368a0ae 100644 --- a/toolkit/components/extensions/test/mochitest/test_ext_tabs_captureTab.html +++ b/toolkit/components/extensions/test/mochitest/test_ext_tabs_captureTab.html @@ -319,6 +319,141 @@ add_task(async function testCaptureVisibleTabPermissions() { await extension.awaitFinish("captureVisibleTabPermissions"); await extension.unload(); }); + +add_task(async function testCaptureVisibleTabWithActiveTab() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_action: { + default_area: "navbar", + }, + permissions: ["webNavigation", "tabs", "activeTab"], + }, + + async background() { + // Wait for the page (in the test window) to load. + await new Promise(resolve => { + browser.webNavigation.onCompleted.addListener( + () => resolve(), + {url: [{schemes: ["data"]}]}); + }); + + browser.browserAction.onClicked.addListener(async tab => { + await browser.tabs.captureVisibleTab(tab.windowId); + browser.test.notifyPass("captureVisibleTabPermissions"); + }); + + browser.test.sendMessage("ready"); + }, + }); + + let html = ` + <!DOCTYPE html> + <html lang="en"> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + </head> + <body><h1>hello</h1></body> + </html> + `; + + await extension.startup(); + + let testWindow = window.open(`data:text/html,${encodeURIComponent(html)}#scroll`); + await extension.awaitMessage("ready"); + await AppTestDelegate.clickBrowserAction(testWindow, extension); + await extension.awaitFinish("captureVisibleTabPermissions"); + await AppTestDelegate.closeBrowserAction(testWindow, extension); + testWindow.close(); + + await extension.unload(); +}); + +add_task(async function testCaptureVisibleTabWithActiveTabAndNotUserInteraction() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["webNavigation", "tabs", "activeTab"], + }, + + async background() { + // Wait for the page (in the test window) to load. + await new Promise(resolve => { + browser.webNavigation.onCompleted.addListener( + () => resolve(), + {url: [{schemes: ["data"]}]}); + }); + + let [tab] = await browser.tabs.query({ currentWindow: true, active: true }); + await browser.test.assertRejects( + browser.tabs.captureVisibleTab(tab.windowId), + /Missing activeTab permission/, + "Expected rejection because activeTab permission isn't set" + ); + + browser.test.notifyPass("captureVisibleTabPermissions"); + }, + }); + + let html = ` + <!DOCTYPE html> + <html lang="en"> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + </head> + <body><h1>hello</h1></body> + </html> + `; + + await extension.startup(); + + let testWindow = window.open(`data:text/html,${encodeURIComponent(html)}#scroll`); + await extension.awaitFinish("captureVisibleTabPermissions"); + testWindow.close(); + + await extension.unload(); +}); + +add_task(async function testCaptureVisibleTabWithActiveTabAndAllURLs() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["webNavigation", "tabs", "activeTab", "<all_urls>"], + }, + + async background() { + // Wait for the page (in the test window) to load. + await new Promise(resolve => { + browser.webNavigation.onCompleted.addListener( + () => resolve(), + {url: [{schemes: ["data"]}]}); + }); + + let [tab] = await browser.tabs.query({ currentWindow: true, active: true }); + await browser.tabs.captureVisibleTab(tab.windowId); + + browser.test.notifyPass("captureVisibleTabPermissions"); + }, + }); + + let html = ` + <!DOCTYPE html> + <html lang="en"> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + </head> + <body><h1>hello</h1></body> + </html> + `; + + await extension.startup(); + + let testWindow = window.open(`data:text/html,${encodeURIComponent(html)}#scroll`); + await extension.awaitFinish("captureVisibleTabPermissions"); + testWindow.close(); + + await extension.unload(); +}); </script> </body> </html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_tabs_sendMessage.html b/toolkit/components/extensions/test/mochitest/test_ext_tabs_sendMessage.html index 4b230c258c..8c6dfeee7c 100644 --- a/toolkit/components/extensions/test/mochitest/test_ext_tabs_sendMessage.html +++ b/toolkit/components/extensions/test/mochitest/test_ext_tabs_sendMessage.html @@ -11,6 +11,10 @@ <script> "use strict"; +const { + WebExtensionPolicy +} = SpecialPowers.Cu.getGlobalForObject(SpecialPowers.Services); + add_task(async function test_tabs_sendMessage_to_extension_page_frame() { let extension = ExtensionTestUtils.loadExtension({ manifest: { @@ -27,6 +31,10 @@ add_task(async function test_tabs_sendMessage_to_extension_page_frame() { browser.runtime.onMessage.addListener(async (msg, sender) => { browser.test.assertEq(msg, "page-script-ready"); browser.test.assertEq(sender.url, browser.runtime.getURL("page.html")); + browser.test.assertEq( + sender.origin, + new URL(browser.runtime.getURL("/")).origin + ); let tabId = sender.tab.id; let response = await browser.tabs.sendMessage(tabId, "tab-sendMessage"); @@ -147,6 +155,178 @@ add_task(async function test_tabs_sendMessage_using_frameId() { await extension.unload(); }); +add_task(async function test_tabs_sendMessage_aboutBlank() { + const PATH = "tests/toolkit/components/extensions/test/mochitest/file_with_about_blank.html"; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [ + { + match_about_blank: true, + matches: ["*://mochi.test/*/file_with_about_blank.html"], + all_frames: true, + js: ["cs.js"], + }, + ], + }, + + background() { + browser.runtime.onMessage.addListener((msg, { url, origin }) => { + browser.test.assertEq("cs", msg, "expected message from cs.js"); + + const kind = url.startsWith("about:") ? url : "top"; + switch (kind) { + case "top": + browser.test.assertTrue( + url.endsWith("/file_with_about_blank.html"), + "expected correct url" + ); + browser.test.assertEq( + "http://mochi.test:8888", + origin, + "expected correct origin" + ); + break; + + case "about:blank": + browser.test.assertEq("about:blank", url, "expected correct url"); + browser.test.assertEq( + "http://mochi.test:8888", + origin, + "expected correct origin" + ); + break; + + case "about:srcdoc": + browser.test.assertEq("about:srcdoc", url, "expected correct url"); + browser.test.assertEq( + "http://mochi.test:8888", + origin, + "expected correct origin" + ); + break; + + default: + browser.test.fail(`Unexpected kind: ${kind}`); + } + + browser.test.sendMessage(`done:${kind}`); + }); + + browser.test.sendMessage("ready"); + }, + + files: { + "cs.js"() { + browser.runtime.sendMessage("cs"); + }, + }, + }); + + await extension.startup(); + await extension.awaitMessage("ready"); + + let win = window.open("http://mochi.test:8888/" + PATH); + await extension.awaitMessage("done:top"); + await extension.awaitMessage("done:about:blank"); + await extension.awaitMessage("done:about:srcdoc"); + win.close(); + + await extension.unload(); +}); + +add_task(async function test_tabs_sendMessage_opaqueOrigin() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [ + { + match_about_blank: true, + // The combination of `matches` and `exclude_matches` below allows us + // to only inject in the top-level about:blank. + matches: ["*://*/*"], + exclude_matches: ["<all_urls>"], + js: ["cs.js"], + }, + ], + }, + + async background() { + let tab; + + browser.runtime.onMessage.addListener(async (msg, { url, origin }) => { + browser.test.assertEq("cs", msg, "expected message from cs.js"); + browser.test.assertEq("about:blank", url, "expected correct url"); + browser.test.assertEq("null", origin, "expected correct origin"); + + await browser.tabs.remove(tab.id); + browser.test.sendMessage("done"); + }); + + tab = await browser.tabs.create({ url: "about:blank" }); + }, + + files: { + "cs.js"() { + browser.runtime.sendMessage("cs"); + }, + }, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); +}); + +add_task(async function test_tabs_sendMessage_blob() { + // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1724099 + if (!WebExtensionPolicy.useRemoteWebExtensions) { + await SpecialPowers.pushPrefEnv({ + set: [["security.allow_unsafe_parent_loads", true]], + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + async background() { + browser.runtime.onMessage.addListener(async (msg, { url, origin }) => { + browser.test.assertEq("script", msg, "expected message from script.js"); + browser.test.assertTrue( + url.startsWith("blob:moz-extension://"), + "expected correct url" + ); + browser.test.assertEq( + new URL(browser.runtime.getURL("/")).origin, + origin, + "expected correct origin" + ); + + browser.test.sendMessage("done"); + }); + + const blob = new Blob( + [`<script src="${browser.runtime.getURL("script.js")}"><\/script>`], + { type: "text/html" } + ); + const iframe = document.createElement("iframe"); + iframe.src = URL.createObjectURL(blob); + document.body.appendChild(iframe); + }, + + files: { + "script.js"() { + browser.runtime.sendMessage("script"); + }, + }, + }); + + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); + + if (!WebExtensionPolicy.useRemoteWebExtensions) { + await SpecialPowers.popPrefEnv(); + } +}); + </script> </body> </html> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_test.html b/toolkit/components/extensions/test/mochitest/test_ext_test.html index bf68786465..21093c9abf 100644 --- a/toolkit/components/extensions/test/mochitest/test_ext_test.html +++ b/toolkit/components/extensions/test/mochitest/test_ext_test.html @@ -126,7 +126,7 @@ function testScript() { // The WebIDL version of assertDeepEq structurally clones before sending the // params to the main thread. This check verifies that the behavior is - // consistent between the WebIDL and Schemas.jsm-generated API bindings. + // consistent between the WebIDL and Schemas.sys.mjs-generated API bindings. browser.test.assertThrows( () => browser.test.assertDeepEq(obj, obj, "obj with func"), /An unexpected error occurred/, diff --git a/toolkit/components/extensions/test/xpcshell/test_csp_validator.js b/toolkit/components/extensions/test/xpcshell/test_csp_validator.js index 011628f027..0e665b9730 100644 --- a/toolkit/components/extensions/test/xpcshell/test_csp_validator.js +++ b/toolkit/components/extensions/test/xpcshell/test_csp_validator.js @@ -202,8 +202,8 @@ add_task(async function test_csp_validator_extension_pages() { let checkPolicy = (policy, expectedResult) => { info(`Checking policy: ${policy}`); - // While Schemas.jsm uses Ci.nsIAddonContentPolicy.CSP_ALLOW_WASM, we don't - // pass that here because we are only verifying that remote scripts are + // While Schemas.sys.mjs uses Ci.nsIAddonContentPolicy.CSP_ALLOW_WASM, we + // don't pass that here because we are only verifying that remote scripts are // blocked here. let result = cps.validateAddonCSP(policy, 0); equal(result, expectedResult); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_adoption_with_private_field_xrays.js b/toolkit/components/extensions/test/xpcshell/test_ext_adoption_with_private_field_xrays.js index 2d8b02bcd9..76b6644d44 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_adoption_with_private_field_xrays.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_adoption_with_private_field_xrays.js @@ -1,6 +1,6 @@ "use strict"; -// ExtensionContent.jsm needs to know when it's running from xpcshell, +// ExtensionContent.sys.mjs needs to know when it's running from xpcshell, // to use the right timeout for content scripts executed at document_idle. ExtensionTestUtils.mockAppInfo(); const server = createHttpServer(); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_adoption_with_xrays.js b/toolkit/components/extensions/test/xpcshell/test_ext_adoption_with_xrays.js index 9655c157d1..b909223302 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_adoption_with_xrays.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_adoption_with_xrays.js @@ -1,6 +1,6 @@ "use strict"; -// ExtensionContent.jsm needs to know when it's running from xpcshell, +// ExtensionContent.sys.mjs needs to know when it's running from xpcshell, // to use the right timeout for content scripts executed at document_idle. ExtensionTestUtils.mockAppInfo(); const server = createHttpServer(); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_clear_cached_resources.js b/toolkit/components/extensions/test/xpcshell/test_ext_clear_cached_resources.js index 95bef23383..bd05398736 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_clear_cached_resources.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_clear_cached_resources.js @@ -297,7 +297,7 @@ add_task( // This temporary directory is going to be removed from the // cleanup function, but also make it unique as we do for the // other temporary files (e.g. like getTemporaryFile as defined - // in XPInstall.jsm). + // in XPIInstall.sys.mjs). const random = Math.round(Math.random() * 36 ** 3).toString(36); const tmpDirName = `xpcshelltest_unpacked_addons_${random}`; let tmpExtPath = FileUtils.getDir("TmpD", [tmpDirName]); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_dynamic_registration.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_dynamic_registration.js index 0133b5d86c..20f6ece95a 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_dynamic_registration.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_dynamic_registration.js @@ -5,7 +5,7 @@ server.registerDirectory("/data/", do_get_file("data")); const BASE_URL = `http://localhost:${server.identity.primaryPort}/data`; -// ExtensionContent.jsm needs to know when it's running from xpcshell, to use +// ExtensionContent.sys.mjs needs to know when it's running from xpcshell, to use // the right timeout for content scripts executed at document_idle. ExtensionTestUtils.mockAppInfo(); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_triggeringPrincipal.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_triggeringPrincipal.js index 4ebe6df636..ce7f293142 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_triggeringPrincipal.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_triggeringPrincipal.js @@ -30,7 +30,7 @@ Services.prefs.setBoolPref( false ); -// ExtensionContent.jsm needs to know when it's running from xpcshell, +// ExtensionContent.sys.mjs needs to know when it's running from xpcshell, // to use the right timeout for content scripts executed at document_idle. ExtensionTestUtils.mockAppInfo(); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_xorigin_frame.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_xorigin_frame.js index 8a58b2475c..420fa7689c 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_xorigin_frame.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_xorigin_frame.js @@ -5,6 +5,12 @@ const server = createHttpServer({ }); server.registerDirectory("/data/", do_get_file("data")); +// By default, fission.webContentIsolationStrategy=1 (IsolateHighValue). When +// Fission is enabled on Android, the pref value 2 (IsolateHighValue) will be +// used instead. Set to 1 (IsolateEverything) to make sure the subframe in this +// test gets its own process, independently of the default prefs on Android. +Services.prefs.setIntPref("fission.webContentIsolationStrategy", 1); + add_task(async function test_process_switch_cross_origin_frame() { const extension = ExtensionTestUtils.loadExtension({ manifest: { diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_xrays.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_xrays.js index 7b92d5c4b7..30ec8ceced 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_xrays.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_xrays.js @@ -1,6 +1,6 @@ "use strict"; -// ExtensionContent.jsm needs to know when it's running from xpcshell, +// ExtensionContent.sys.mjs needs to know when it's running from xpcshell, // to use the right timeout for content scripts executed at document_idle. ExtensionTestUtils.mockAppInfo(); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contexts_gc.js b/toolkit/components/extensions/test/xpcshell/test_ext_contexts_gc.js index 2a36f51637..b6f955f7ba 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_contexts_gc.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contexts_gc.js @@ -5,7 +5,7 @@ server.registerDirectory("/data/", do_get_file("data")); const BASE_URL = `http://localhost:${server.identity.primaryPort}/data`; -// ExtensionContent.jsm needs to know when it's running from xpcshell, +// ExtensionContent.sys.mjs needs to know when it's running from xpcshell, // to use the right timeout for content scripts executed at document_idle. ExtensionTestUtils.mockAppInfo(); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_dnr_modifyHeaders.js b/toolkit/components/extensions/test/xpcshell/test_ext_dnr_modifyHeaders.js index 4b7bebe188..df9e9d77ef 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_dnr_modifyHeaders.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_dnr_modifyHeaders.js @@ -29,6 +29,7 @@ server.registerPathHandler("/echoheaders", (req, res) => { dropDefaultHeader("accept-language"); dropDefaultHeader("accept-encoding"); dropDefaultHeader("connection"); + dropDefaultHeader("priority"); res.write(JSON.stringify(headers)); }); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_file_access.js b/toolkit/components/extensions/test/xpcshell/test_ext_file_access.js index c05188cd38..bea1e76c0f 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_file_access.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_file_access.js @@ -4,7 +4,7 @@ const FILE_DUMMY_URL = Services.io.newFileURI( do_get_file("data/dummy_page.html") ).spec; -// ExtensionContent.jsm needs to know when it's running from xpcshell, +// ExtensionContent.sys.mjs needs to know when it's running from xpcshell, // to use the right timeout for content scripts executed at document_idle. ExtensionTestUtils.mockAppInfo(); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_i18n.js b/toolkit/components/extensions/test/xpcshell/test_ext_i18n.js index 1e46e19527..69ba326f75 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_i18n.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_i18n.js @@ -4,7 +4,7 @@ const { Preferences } = ChromeUtils.importESModule( "resource://gre/modules/Preferences.sys.mjs" ); -// ExtensionContent.jsm needs to know when it's running from xpcshell, +// ExtensionContent.sys.mjs needs to know when it's running from xpcshell, // to use the right timeout for content scripts executed at document_idle. ExtensionTestUtils.mockAppInfo(); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_ipcBlob.js b/toolkit/components/extensions/test/xpcshell/test_ext_ipcBlob.js index dd90d9bbc8..2c5b3378fd 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_ipcBlob.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_ipcBlob.js @@ -5,7 +5,7 @@ server.registerDirectory("/data/", do_get_file("data")); const BASE_URL = `http://localhost:${server.identity.primaryPort}/data`; -// ExtensionContent.jsm needs to know when it's running from xpcshell, +// ExtensionContent.sys.mjs needs to know when it's running from xpcshell, // to use the right timeout for content scripts executed at document_idle. ExtensionTestUtils.mockAppInfo(); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js b/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js index 948b75978a..ae6ce3d27e 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js @@ -12,8 +12,8 @@ const { ExtensionPermissions } = ChromeUtils.importESModule( Services.prefs.setBoolPref("extensions.manifestV3.enabled", true); -// ExtensionParent.jsm is being imported lazily because when it is imported Services.appinfo will be -// retrieved and cached (as a side-effect of Schemas.jsm being imported), and so Services.appinfo +// ExtensionParent.sys.mjs is being imported lazily because when it is imported Services.appinfo will be +// retrieved and cached (as a side-effect of Schemas.sys.mjs being imported), and so Services.appinfo // will not be returning the version set by AddonTestUtils.createAppInfo and this test will // fail on non-nightly builds (because the cached appinfo.version will be undefined and // AddonManager startup will fail). @@ -704,6 +704,7 @@ const GRANTED_WITHOUT_USER_PROMPT = [ "theme", "unlimitedStorage", "webRequest", + "webRequestAuthProvider", "webRequestBlocking", "webRequestFilterResponse", "webRequestFilterResponse.serviceWorkerScript", @@ -1058,3 +1059,47 @@ add_task(async function test_internal_permissions() { await extension.unload(); }); + +add_task(function test_normalizeOptional() { + const optional1 = { + origins: ["*://site.com/", "*://*.domain.com/"], + permissions: ["downloads", "tabs"], + }; + + function normalize(perms, optional) { + perms = { origins: [], permissions: [], ...perms }; + optional = { origins: [], permissions: [], ...optional }; + return ExtensionPermissions.normalizeOptional(perms, optional); + } + + normalize({ origins: ["http://site.com/"] }, optional1); + normalize({ origins: ["https://site.com/"] }, optional1); + normalize({ origins: ["*://blah.domain.com/"] }, optional1); + normalize({ permissions: ["downloads", "tabs"] }, optional1); + + Assert.throws( + () => normalize({ origins: ["http://www.example.com/"] }, optional1), + /was not declared in the manifest/ + ); + Assert.throws( + () => normalize({ permissions: ["proxy"] }, optional1), + /was not declared in optional_permissions/ + ); + + const optional2 = { + origins: ["<all_urls>", "*://*/*"], + permissions: ["idle", "clipboardWrite"], + }; + + normalize({ origins: ["http://site.com/"] }, optional2); + normalize({ origins: ["https://site.com/"] }, optional2); + normalize({ origins: ["*://blah.domain.com/"] }, optional2); + normalize({ permissions: ["idle", "clipboardWrite"] }, optional2); + + let perms = normalize({ origins: ["<all_urls>"] }, optional2); + equal( + perms.origins.sort().join(), + optional2.origins.sort().join(), + `Expect both "all sites" permissions` + ); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_permissions_api.js b/toolkit/components/extensions/test/xpcshell/test_ext_permissions_api.js index 0211787fee..97db78cf81 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_permissions_api.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_permissions_api.js @@ -58,6 +58,7 @@ add_task(async function setup() { "search", "tabHide", "tabs", + "webRequestAuthProvider", "webRequestBlocking", "webRequestFilterResponse", "webRequestFilterResponse.serviceWorkerScript", diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_proxy_onauthrequired.js b/toolkit/components/extensions/test/xpcshell/test_ext_proxy_onauthrequired.js index 4b1128a349..719fd219fe 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_proxy_onauthrequired.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_proxy_onauthrequired.js @@ -251,7 +251,7 @@ add_task(async function test_webRequest_auth_proxy_system() { () => { browser.test.sendMessage("onAuthRequired"); // cancel is silently ignored, if it were not (e.g someone messes up in - // WebRequest.jsm and allows cancel) this test would fail. + // WebRequest.sys.mjs and allows cancel) this test would fail. return { cancel: true, authCredentials: { username: "puser", password: "ppass" }, diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_schemas_manifest_permissions.js b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_manifest_permissions.js index 85e645e67b..e5fc746e60 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas_manifest_permissions.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_manifest_permissions.js @@ -86,7 +86,7 @@ add_task(async function () { a_manifest_property: {}, }, background() { - // Test hasPermission method implemented in ExtensionChild.jsm. + // Test hasPermission method implemented in ExtensionChild.sys.mjs. browser.test.assertTrue( "testManifestPermission" in browser, "The API namespace is defined as expected" @@ -105,7 +105,7 @@ add_task(async function () { "test-extension-manifest-without-nested-prop" ); - // Test hasPermission method implemented in Extension.jsm. + // Test hasPermission method implemented in Extension.sys.mjs. equal( extension.extension.hasPermission("manifest:a_manifest_property"), true, @@ -129,7 +129,7 @@ add_task(async function () { }, }, background() { - // Test hasPermission method implemented in ExtensionChild.jsm. + // Test hasPermission method implemented in ExtensionChild.sys.mjs. browser.test.assertTrue( "testManifestPermission" in browser, "The API namespace is defined as expected" @@ -146,7 +146,7 @@ add_task(async function () { async extension => { await extension.awaitFinish("test-extension-manifest-with-nested-prop"); - // Test hasPermission method implemented in Extension.jsm. + // Test hasPermission method implemented in Extension.sys.mjs. equal( extension.extension.hasPermission("manifest:a_manifest_property"), true, diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_schemas_privileged.js b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_privileged.js index 14cbca7443..496607968e 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas_privileged.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_privileged.js @@ -144,7 +144,7 @@ add_task( } ); -// Test that Extension.jsm and schema correctly match. +// Test that Extension.sys.mjs and schema correctly match. add_task(function test_privileged_permissions_match() { const { PRIVILEGED_PERMS } = ChromeUtils.importESModule( "resource://gre/modules/Extension.sys.mjs" diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_scripting_contentScripts.js b/toolkit/components/extensions/test/xpcshell/test_ext_scripting_contentScripts.js index a06f34a1b4..c47cdcad4d 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_scripting_contentScripts.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_scripting_contentScripts.js @@ -5,7 +5,7 @@ server.registerDirectory("/data/", do_get_file("data")); const BASE_URL = `http://localhost:${server.identity.primaryPort}/data`; -// ExtensionContent.jsm needs to know when it's running from xpcshell, to use +// ExtensionContent.sys.mjs needs to know when it's running from xpcshell, to use // the right timeout for content scripts executed at document_idle. ExtensionTestUtils.mockAppInfo(); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_scripting_contentScripts_css.js b/toolkit/components/extensions/test/xpcshell/test_ext_scripting_contentScripts_css.js index 21190d2d59..a3c79b1cc0 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_scripting_contentScripts_css.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_scripting_contentScripts_css.js @@ -5,7 +5,7 @@ server.registerDirectory("/data/", do_get_file("data")); const BASE_URL = `http://localhost:${server.identity.primaryPort}/data`; -// ExtensionContent.jsm needs to know when it's running from xpcshell, to use +// ExtensionContent.sys.mjs needs to know when it's running from xpcshell, to use // the right timeout for content scripts executed at document_idle. ExtensionTestUtils.mockAppInfo(); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_scripting_contentScripts_file.js b/toolkit/components/extensions/test/xpcshell/test_ext_scripting_contentScripts_file.js index 3c806439ce..02ef9b0fa5 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_scripting_contentScripts_file.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_scripting_contentScripts_file.js @@ -4,7 +4,7 @@ const FILE_DUMMY_URL = Services.io.newFileURI( do_get_file("data/dummy_page.html") ).spec; -// ExtensionContent.jsm needs to know when it's running from xpcshell, to use +// ExtensionContent.sys.mjs needs to know when it's running from xpcshell, to use // the right timeout for content scripts executed at document_idle. ExtensionTestUtils.mockAppInfo(); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_scripting_updateContentScripts.js b/toolkit/components/extensions/test/xpcshell/test_ext_scripting_updateContentScripts.js index 9d3bf1576c..85c2376715 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_scripting_updateContentScripts.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_scripting_updateContentScripts.js @@ -5,7 +5,7 @@ server.registerDirectory("/data/", do_get_file("data")); const BASE_URL = `http://localhost:${server.identity.primaryPort}/data`; -// ExtensionContent.jsm needs to know when it's running from xpcshell, to use +// ExtensionContent.sys.mjs needs to know when it's running from xpcshell, to use // the right timeout for content scripts executed at document_idle. ExtensionTestUtils.mockAppInfo(); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_shadowdom.js b/toolkit/components/extensions/test/xpcshell/test_ext_shadowdom.js index 626d8de22d..e8316aa652 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_shadowdom.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_shadowdom.js @@ -1,6 +1,6 @@ "use strict"; -// ExtensionContent.jsm needs to know when it's running from xpcshell, +// ExtensionContent.sys.mjs needs to know when it's running from xpcshell, // to use the right timeout for content scripts executed at document_idle. ExtensionTestUtils.mockAppInfo(); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_userScripts.js b/toolkit/components/extensions/test/xpcshell/test_ext_userScripts.js index 3108c7b9b4..57ca08aca3 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_userScripts.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_userScripts.js @@ -200,7 +200,7 @@ add_task(async function test_userScripts_no_webext_apis() { let script = await browser.userScripts.register(userScriptOptions); // Unregister and then register the same js code again, to verify that the last registered - // userScript doesn't get assigned a revoked blob url (otherwise Extensioncontent.jsm + // userScript doesn't get assigned a revoked blob url (otherwise Extensioncontent.sys.mjs // ScriptCache raises an error because it fails to compile the revoked blob url and the user // script will never be loaded). script.unregister(); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_webRequest_auth.js b/toolkit/components/extensions/test/xpcshell/test_ext_webRequest_auth.js index 578e69ebdf..ef07817d00 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_webRequest_auth.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_webRequest_auth.js @@ -63,7 +63,7 @@ server.registerPathHandler("/authenticate.sjs", (request, response) => { } }); -function getExtension(bgConfig) { +function getExtension(bgConfig, permissions = ["webRequestBlocking"]) { function background(config) { let path = config.path; browser.webRequest.onBeforeRequest.addListener( @@ -125,18 +125,18 @@ function getExtension(bgConfig) { return ExtensionTestUtils.loadExtension({ manifest: { - permissions: ["webRequest", "webRequestBlocking", bgConfig.path], + permissions: [bgConfig.path, "webRequest", ...permissions], }, background: `(${background})(${JSON.stringify(bgConfig)})`, }); } -add_task(async function test_webRequest_auth() { +async function test_webRequest_auth(permissions) { let config = { path: `${BASE_URL}/*`, realm: `webRequest_auth${Math.random()}`, onBeforeRequest: { - extra: ["blocking"], + extra: permissions.includes("webRequestBlocking") ? ["blocking"] : [], }, onAuthRequired: { extra: ["blocking"], @@ -149,7 +149,7 @@ add_task(async function test_webRequest_auth() { }, }; - let extension = getExtension(config); + let extension = getExtension(config, permissions); await extension.startup(); let requestUrl = `${BASE_URL}/authenticate.sjs?realm=${config.realm}`; @@ -174,6 +174,14 @@ add_task(async function test_webRequest_auth() { await contentPage.close(); await extension.unload(); +} + +add_task(async function test_webRequest_auth_with_webRequestBlocking() { + await test_webRequest_auth(["webRequestBlocking"]); +}); + +add_task(async function test_webRequest_auth_with_webRequestAuthProvider() { + await test_webRequest_auth(["webRequestAuthProvider"]); }); add_task(async function test_webRequest_auth_cancelled() { diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_webRequest_permission.js b/toolkit/components/extensions/test/xpcshell/test_ext_webRequest_permission.js index 17c22e156d..97e44498c1 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_webRequest_permission.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_webRequest_permission.js @@ -18,6 +18,18 @@ function sendMessage(page, msg, data) { return MessageChannel.sendMessage(page.browser.messageManager, msg, data); } +add_setup(() => { + // Make sure to invalidate WebExtensions API schemas that may be cached + // in the StartupCache when this test runs with conditioned-profiles. + // + // These tests are subject to be hitting failures consistently on + // landing API schema changes to the WebExtensions API permissions. + // or other API schema properties that are explicitly covered by + // this tests (e.g. errors expected to be emitted by postprocess + // helper functions). + Services.obs.notifyObservers(null, "startupcache-invalidate"); +}); + add_task(async function test_permissions() { function background() { browser.webRequest.onBeforeRequest.addListener( @@ -113,11 +125,14 @@ add_task(async function test_permissions() { await contentPage.close(); }); -add_task(async function test_no_webRequestBlocking_error() { +add_task(async function test_missing_required_perm_for_blocking_error() { function background() { const expectedError = "Using webRequest.addListener with the blocking option " + "requires the 'webRequestBlocking' permission."; + const expectedErrorOnAuthRequired = + "Using webRequest.onAuthRequired.addListener with the blocking option " + + "requires either the 'webRequestBlocking' or 'webRequestAuthProvider' permission."; const blockingEvents = [ "onBeforeRequest", @@ -135,7 +150,9 @@ add_task(async function test_no_webRequestBlocking_error() { ["blocking"] ); }, - expectedError, + eventName === "onAuthRequired" + ? expectedErrorOnAuthRequired + : expectedError, `Got the expected exception for a blocking webRequest.${eventName} listener` ); } diff --git a/toolkit/components/extensions/test/xpcshell/test_load_all_api_modules.js b/toolkit/components/extensions/test/xpcshell/test_load_all_api_modules.js index 32299fb04e..3d28971352 100644 --- a/toolkit/components/extensions/test/xpcshell/test_load_all_api_modules.js +++ b/toolkit/components/extensions/test/xpcshell/test_load_all_api_modules.js @@ -18,7 +18,7 @@ let schemaURLs = new Set(); schemaURLs.add("chrome://extensions/content/schemas/experiments.json"); // Helper class used to load the API modules similarly to the apiManager -// defined in ExtensionParent.jsm. +// defined in ExtensionParent.sys.mjs. class FakeAPIManager extends ExtensionCommon.SchemaAPIManager { constructor(processType = "main") { super(processType, Schemas); @@ -102,7 +102,8 @@ class FakeAPIManager extends ExtensionCommon.SchemaAPIManager { } // Specialized helper class used to test loading "child process" modules (similarly to the -// SchemaAPIManagers sub-classes defined in ExtensionPageChild.jsm and ExtensionContent.jsm). +// SchemaAPIManagers sub-classes defined in ExtensionPageChild.sys.mjs and +// ExtensionContent.sys.mjs). class FakeChildProcessAPIManager extends FakeAPIManager { constructor({ processType, categoryScripts }) { super(processType, Schemas); diff --git a/toolkit/components/extensions/test/xpcshell/test_native_manifests.js b/toolkit/components/extensions/test/xpcshell/test_native_manifests.js index 1f5bc88740..d4f3ae7243 100644 --- a/toolkit/components/extensions/test/xpcshell/test_native_manifests.js +++ b/toolkit/components/extensions/test/xpcshell/test_native_manifests.js @@ -248,7 +248,7 @@ add_task( "lookupApplication returns the correct path with platform-native slash" ); // Side note: manifest.path does not contain a platform-native path, - // but it is normalized when used in NativeMessaging.jsm. + // but it is normalized when used in NativeMessaging.sys.mjs. deepEqual( result.manifest, manifest, diff --git a/toolkit/components/extensions/test/xpcshell/test_webRequest_ancestors.js b/toolkit/components/extensions/test/xpcshell/test_webRequest_ancestors.js index 509f821828..abbb814ac7 100644 --- a/toolkit/components/extensions/test/xpcshell/test_webRequest_ancestors.js +++ b/toolkit/components/extensions/test/xpcshell/test_webRequest_ancestors.js @@ -11,7 +11,7 @@ const server = createHttpServer({ hosts: ["example.com"] }); server.registerDirectory("/data/", do_get_file("data")); add_task(async function setup() { - // When WebRequest.jsm is used directly instead of through ext-webRequest.js, + // When WebRequest.sys.mjs is used directly instead of through ext-webRequest.js, // ExtensionParent.apiManager is not automatically initialized. Do it here. await ExtensionParent.apiManager.lazyInit(); }); diff --git a/toolkit/components/extensions/test/xpcshell/test_webRequest_cookies.js b/toolkit/components/extensions/test/xpcshell/test_webRequest_cookies.js index 53ed465786..f4c8d75690 100644 --- a/toolkit/components/extensions/test/xpcshell/test_webRequest_cookies.js +++ b/toolkit/components/extensions/test/xpcshell/test_webRequest_cookies.js @@ -75,7 +75,7 @@ function onResponseStarted(details) { } add_task(async function setup() { - // When WebRequest.jsm is used directly instead of through ext-webRequest.js, + // When WebRequest.sys.mjs is used directly instead of through ext-webRequest.js, // ExtensionParent.apiManager is not automatically initialized. Do it here. await ExtensionParent.apiManager.lazyInit(); }); diff --git a/toolkit/components/extensions/test/xpcshell/test_webRequest_filtering.js b/toolkit/components/extensions/test/xpcshell/test_webRequest_filtering.js index 46a72a5926..86b2410e33 100644 --- a/toolkit/components/extensions/test/xpcshell/test_webRequest_filtering.js +++ b/toolkit/components/extensions/test/xpcshell/test_webRequest_filtering.js @@ -89,7 +89,7 @@ add_task(async function setup() { // Disable rcwn to make cache behavior deterministic. Services.prefs.setBoolPref("network.http.rcwn.enabled", false); - // When WebRequest.jsm is used directly instead of through ext-webRequest.js, + // When WebRequest.sys.mjs is used directly instead of through ext-webRequest.js, // ExtensionParent.apiManager is not automatically initialized. Do it here. await ExtensionParent.apiManager.lazyInit(); }); diff --git a/toolkit/components/extensions/test/xpcshell/xpcshell-common.toml b/toolkit/components/extensions/test/xpcshell/xpcshell-common.toml index 7cf8d79409..6d47012eca 100644 --- a/toolkit/components/extensions/test/xpcshell/xpcshell-common.toml +++ b/toolkit/components/extensions/test/xpcshell/xpcshell-common.toml @@ -533,7 +533,7 @@ skip-if = ["os == 'android'"] # Bug 1564871 ["test_ext_storage_sanitizer.js"] skip-if = [ "appname == 'thunderbird'", - "os == 'android'", # Sanitizer.jsm is not in toolkit. + "os == 'android'", # Sanitizer.sys.mjs is not in toolkit. ] ["test_ext_storage_session.js"] @@ -587,7 +587,9 @@ skip-if = [ ["test_ext_wasm.js"] ["test_ext_webRequest_auth.js"] -skip-if = ["os == 'android' && debug"] +skip-if = [ + "os == 'android' && debug", +] ["test_ext_webRequest_cached.js"] skip-if = ["os == 'android'"] # Bug 1573511 @@ -616,7 +618,9 @@ skip-if = ["os == 'android' && debug"] skip-if = ["tsan"] # Bug 1683730 ["test_ext_webRequest_permission.js"] -skip-if = ["os == 'android' && debug"] +skip-if = [ + "os == 'android' && debug", +] ["test_ext_webRequest_redirectProperty.js"] skip-if = ["os == 'android' && processor == 'x86_64'"] # Bug 1683253 diff --git a/toolkit/components/extensions/tsconfig.json b/toolkit/components/extensions/tsconfig.json index d569ba9eca..992018f4a8 100644 --- a/toolkit/components/extensions/tsconfig.json +++ b/toolkit/components/extensions/tsconfig.json @@ -1,15 +1,11 @@ { - "include": ["*.mjs", "types/globals.ts"], - "exclude": [], + "include": ["*.mjs", "types/*.ts"], "compilerOptions": { "checkJs": true, - "target": "ESNEXT", - - "declaration": true, - "outDir": "./types", - "typeRoots": [], "noEmit": true, + "target": "es2022", + "types": ["gecko"], // prettier-ignore "paths": { @@ -39,11 +35,12 @@ "resource://gre/modules/Schemas.sys.mjs": ["./Schemas.sys.mjs"], "resource://gre/modules/WebNavigationFrames.sys.mjs": ["./WebNavigationFrames.sys.mjs"], "resource://gre/modules/WebRequest.sys.mjs": ["./webrequest/WebRequest.sys.mjs"], + "resource://testing-common/ExtensionTestCommon.sys.mjs": ["./ExtensionTestCommon.sys.mjs"], // External. "resource://gre/modules/addons/crypto-utils.sys.mjs": ["../../mozapps/extensions/internal/crypto-utils.sys.mjs"], "resource://gre/modules/XPCOMUtils.sys.mjs": ["../../../js/xpconnect/loader/XPCOMUtils.sys.mjs"], - "resource://testing-common/ExtensionTestCommon.sys.mjs": ["./ExtensionTestCommon.sys.mjs"], + "resource://devtools/server/actors/descriptors/webextension.js": ["./types/globals.ts"], // Types for external modules which need fixing, but we don't wanna touch. "resource://testing-common/XPCShellContentUtils.sys.mjs": ["./types/XPCShellContentUtils.sys.d.mts"], diff --git a/toolkit/components/extensions/types/README.md b/toolkit/components/extensions/types/README.md index ebd01dec60..1d4588389c 100644 --- a/toolkit/components/extensions/types/README.md +++ b/toolkit/components/extensions/types/README.md @@ -21,11 +21,11 @@ viability and benefits of doing this for a "typical" component. ## How to use and expectations -Use [npm or yarn to install][download] TypeScript. -Then run `tsc` in the extensions directory to check types: +Mach now comes with a `ts` command for type checking code using the built-in +gecko typelibs: ``` -mozilla-central/toolkit/components/extensions $ tsc +mozilla-central $ ./mach ts check toolkit/components/extensions/ ``` You can also use an editor which supports the [language server][langserv]. @@ -64,6 +64,9 @@ These fall under 5 main categories: 5) Don't re-use local variables unnecessarily with different types. * (general good practice, local variables are "free") + 6) Use `export` on individual classes instead of grouping into "namespaces". + * (idiomatic/ergonomic/modern JS, grouping was a leftover from JSMs) + ### @ts-ignore recommendations *Don't* use `@ts-ignore` for class fields and function or method signatures. @@ -75,7 +78,7 @@ These fall under 5 main categories: parts of the codebase. -[handbook]: https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html +[handbook]: https://www.typescriptlang.org/docs/handbook/intro-to-js-ts.html [jsdoc]: https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html diff --git a/toolkit/components/extensions/types/ext-tabs-base.d.ts b/toolkit/components/extensions/types/ext-tabs-base.d.ts new file mode 100644 index 0000000000..c889fd6620 --- /dev/null +++ b/toolkit/components/extensions/types/ext-tabs-base.d.ts @@ -0,0 +1,1447 @@ +// @ts-nocheck + +declare namespace tabs_base { + +declare function getUserContextIdForCookieStoreId(extension: any, cookieStoreId: any, isPrivateBrowsing: any): any; +declare var DefaultMap: any; +declare var DefaultWeakMap: any; +declare var ExtensionError: any; +declare var parseMatchPatterns: any; +declare var defineLazyGetter: any; +/** + * The platform-specific type of native tab objects, which are wrapped by + * TabBase instances. + * + * @typedef {object | XULElement} NativeTab + */ +/** + * @typedef {object} MutedInfo + * @property {boolean} muted + * True if the tab is currently muted, false otherwise. + * @property {string} [reason] + * The reason the tab is muted. Either "user", if the tab was muted by a + * user, or "extension", if it was muted by an extension. + * @property {string} [extensionId] + * If the tab was muted by an extension, contains the internal ID of that + * extension. + */ +/** + * A platform-independent base class for extension-specific wrappers around + * native tab objects. + * + * @param {Extension} extension + * The extension object for which this wrapper is being created. Used to + * determine permissions for access to certain properties and + * functionality. + * @param {NativeTab} nativeTab + * The native tab object which is being wrapped. The type of this object + * varies by platform. + * @param {integer} id + * The numeric ID of this tab object. This ID should be the same for + * every extension, and for the lifetime of the tab. + */ +declare class TabBase { + constructor(extension: any, nativeTab: any, id: any); + extension: any; + tabManager: any; + id: any; + nativeTab: any; + activeTabWindowID: any; + /** + * Capture the visible area of this tab, and return the result as a data: URI. + * + * @param {BaseContext} context + * The extension context for which to perform the capture. + * @param {number} zoom + * The current zoom for the page. + * @param {object} [options] + * The options with which to perform the capture. + * @param {string} [options.format = "png"] + * The image format in which to encode the captured data. May be one of + * "png" or "jpeg". + * @param {integer} [options.quality = 92] + * The quality at which to encode the captured image data, ranging from + * 0 to 100. Has no effect for the "png" format. + * @param {DOMRectInit} [options.rect] + * Area of the document to render, in CSS pixels, relative to the page. + * If null, the currently visible viewport is rendered. + * @param {number} [options.scale] + * The scale to render at, defaults to devicePixelRatio. + * @returns {Promise<string>} + */ + capture(context: BaseContext, zoom: number, options?: { + format?: string; + quality?: integer; + rect?: DOMRectInit; + scale?: number; + }): Promise<string>; + /** + * @property {integer | null} innerWindowID + * The last known innerWindowID loaded into this tab's docShell. This + * property must remain in sync with the last known values of + * properties such as `url` and `title`. Any operations on the content + * of an out-of-process tab will automatically fail if the + * innerWindowID of the tab when the message is received does not match + * the value of this property when the message was sent. + * @readonly + */ + readonly get innerWindowID(): any; + /** + * @property {boolean} hasTabPermission + * Returns true if the extension has permission to access restricted + * properties of this tab, such as `url`, `title`, and `favIconUrl`. + * @readonly + */ + readonly get hasTabPermission(): any; + /** + * @property {boolean} hasActiveTabPermission + * Returns true if the extension has the "activeTab" permission, and + * has been granted access to this tab due to a user executing an + * extension action. + * + * If true, the extension may load scripts and CSS into this tab, and + * access restricted properties, such as its `url`. + * @readonly + */ + readonly get hasActiveTabPermission(): boolean; + /** + * @property {boolean} matchesHostPermission + * Returns true if the extensions host permissions match the current tab url. + * @readonly + */ + readonly get matchesHostPermission(): any; + /** + * @property {boolean} incognito + * Returns true if this is a private browsing tab, false otherwise. + * @readonly + */ + readonly get _incognito(): any; + /** + * @property {string} _url + * Returns the current URL of this tab. Does not do any permission + * checks. + * @readonly + */ + readonly get _url(): any; + /** + * @property {string | null} url + * Returns the current URL of this tab if the extension has permission + * to read it, or null otherwise. + * @readonly + */ + readonly get url(): any; + /** + * @property {nsIURI} _uri + * Returns the current URI of this tab. + * @readonly + */ + readonly get _uri(): any; + /** + * @property {string} _title + * Returns the current title of this tab. Does not do any permission + * checks. + * @readonly + */ + readonly get _title(): any; + /** + * @property {nsIURI | null} title + * Returns the current title of this tab if the extension has permission + * to read it, or null otherwise. + * @readonly + */ + readonly get title(): any; + /** + * @property {string} _favIconUrl + * Returns the current favicon URL of this tab. Does not do any permission + * checks. + * @readonly + * @abstract + */ + readonly get _favIconUrl(): void; + /** + * @property {nsIURI | null} faviconUrl + * Returns the current faviron URL of this tab if the extension has permission + * to read it, or null otherwise. + * @readonly + */ + readonly get favIconUrl(): void; + /** + * @property {integer} lastAccessed + * Returns the last time the tab was accessed as the number of + * milliseconds since epoch. + * @readonly + * @abstract + */ + readonly get lastAccessed(): void; + /** + * @property {boolean} audible + * Returns true if the tab is currently playing audio, false otherwise. + * @readonly + * @abstract + */ + readonly get audible(): void; + /** + * @property {boolean} autoDiscardable + * Returns true if the tab can be discarded on memory pressure, false otherwise. + * @readonly + * @abstract + */ + readonly get autoDiscardable(): void; + /** + * @property {XULElement} browser + * Returns the XUL browser for the given tab. + * @readonly + * @abstract + */ + readonly get browser(): XULBrowserElement; + /** + * @property {BrowsingContext} browsingContext + * Returns the BrowsingContext for the given tab. + * @readonly + */ + readonly get browsingContext(): any; + /** + * @property {FrameLoader} frameLoader + * Returns the frameloader for the given tab. + * @readonly + */ + readonly get frameLoader(): void; + /** + * @property {string} cookieStoreId + * Returns the cookie store identifier for the given tab. + * @readonly + * @abstract + */ + readonly get cookieStoreId(): void; + /** + * @property {integer} openerTabId + * Returns the ID of the tab which opened this one. + * @readonly + */ + readonly get openerTabId(): any; + /** + * @property {integer} discarded + * Returns true if the tab is discarded. + * @readonly + * @abstract + */ + readonly get discarded(): void; + /** + * @property {integer} height + * Returns the pixel height of the visible area of the tab. + * @readonly + * @abstract + */ + readonly get height(): void; + /** + * @property {integer} hidden + * Returns true if the tab is hidden. + * @readonly + * @abstract + */ + readonly get hidden(): void; + /** + * @property {integer} index + * Returns the index of the tab in its window's tab list. + * @readonly + * @abstract + */ + readonly get index(): void; + /** + * @property {MutedInfo} mutedInfo + * Returns information about the tab's current audio muting status. + * @readonly + * @abstract + */ + readonly get mutedInfo(): void; + /** + * @property {SharingState} sharingState + * Returns object with tab sharingState. + * @readonly + * @abstract + */ + readonly get sharingState(): void; + /** + * @property {boolean} pinned + * Returns true if the tab is pinned, false otherwise. + * @readonly + * @abstract + */ + readonly get pinned(): void; + /** + * @property {boolean} active + * Returns true if the tab is the currently-selected tab, false + * otherwise. + * @readonly + * @abstract + */ + readonly get active(): void; + /** + * @property {boolean} highlighted + * Returns true if the tab is highlighted. + * @readonly + * @abstract + */ + readonly get highlighted(): void; + /** + * @property {string} status + * Returns the current loading status of the tab. May be either + * "loading" or "complete". + * @readonly + * @abstract + */ + readonly get status(): void; + /** + * @property {integer} height + * Returns the pixel height of the visible area of the tab. + * @readonly + * @abstract + */ + readonly get width(): void; + /** + * @property {DOMWindow} window + * Returns the browser window to which the tab belongs. + * @readonly + * @abstract + */ + readonly get window(): void; + /** + * @property {integer} window + * Returns the numeric ID of the browser window to which the tab belongs. + * @readonly + * @abstract + */ + readonly get windowId(): void; + /** + * @property {boolean} attention + * Returns true if the tab is drawing attention. + * @readonly + * @abstract + */ + readonly get attention(): void; + /** + * @property {boolean} isArticle + * Returns true if the document in the tab can be rendered in reader + * mode. + * @readonly + * @abstract + */ + readonly get isArticle(): void; + /** + * @property {boolean} isInReaderMode + * Returns true if the document in the tab is being rendered in reader + * mode. + * @readonly + * @abstract + */ + readonly get isInReaderMode(): void; + /** + * @property {integer} successorTabId + * @readonly + * @abstract + */ + readonly get successorTabId(): void; + /** + * Returns true if this tab matches the the given query info object. Omitted + * or null have no effect on the match. + * + * @param {object} queryInfo + * The query info against which to match. + * @param {boolean} [queryInfo.active] + * Matches against the exact value of the tab's `active` attribute. + * @param {boolean} [queryInfo.audible] + * Matches against the exact value of the tab's `audible` attribute. + * @param {boolean} [queryInfo.autoDiscardable] + * Matches against the exact value of the tab's `autoDiscardable` attribute. + * @param {string} [queryInfo.cookieStoreId] + * Matches against the exact value of the tab's `cookieStoreId` attribute. + * @param {boolean} [queryInfo.discarded] + * Matches against the exact value of the tab's `discarded` attribute. + * @param {boolean} [queryInfo.hidden] + * Matches against the exact value of the tab's `hidden` attribute. + * @param {boolean} [queryInfo.highlighted] + * Matches against the exact value of the tab's `highlighted` attribute. + * @param {integer} [queryInfo.index] + * Matches against the exact value of the tab's `index` attribute. + * @param {boolean} [queryInfo.muted] + * Matches against the exact value of the tab's `mutedInfo.muted` attribute. + * @param {boolean} [queryInfo.pinned] + * Matches against the exact value of the tab's `pinned` attribute. + * @param {string} [queryInfo.status] + * Matches against the exact value of the tab's `status` attribute. + * @param {string} [queryInfo.title] + * Matches against the exact value of the tab's `title` attribute. + * @param {string|boolean } [queryInfo.screen] + * Matches against the exact value of the tab's `sharingState.screen` attribute, or use true to match any screen sharing tab. + * @param {boolean} [queryInfo.camera] + * Matches against the exact value of the tab's `sharingState.camera` attribute. + * @param {boolean} [queryInfo.microphone] + * Matches against the exact value of the tab's `sharingState.microphone` attribute. + * + * Note: Per specification, this should perform a pattern match, rather + * than an exact value match, and will do so in the future. + * @param {MatchPattern} [queryInfo.url] + * Requires the tab's URL to match the given MatchPattern object. + * + * @returns {boolean} + * True if the tab matches the query. + */ + matches(queryInfo: { + active?: boolean; + audible?: boolean; + autoDiscardable?: boolean; + cookieStoreId?: string; + discarded?: boolean; + hidden?: boolean; + highlighted?: boolean; + index?: integer; + muted?: boolean; + pinned?: boolean; + status?: string; + title?: string; + screen?: string | boolean; + camera?: boolean; + microphone?: boolean; + url?: MatchPattern; + }): boolean; + /** + * Converts this tab object to a JSON-compatible object containing the values + * of its properties which the extension is permitted to access, in the format + * required to be returned by WebExtension APIs. + * + * @param {object} [fallbackTabSize] + * A geometry data if the lazy geometry data for this tab hasn't been + * initialized yet. + * @returns {object} + */ + convert(fallbackTabSize?: object): object; + /** + * Query each content process hosting subframes of the tab, return results. + * + * @param {string} message + * @param {object} options + * These options are also sent to the message handler in the + * `ExtensionContentChild`. + * @param {number[]} options.frameIds + * When omitted, all frames will be queried. + * @param {boolean} options.returnResultsWithFrameIds + * @returns {Promise[]} + */ + queryContent(message: string, options: { + frameIds: number[]; + returnResultsWithFrameIds: boolean; + }): Promise<any>[]; + /** + * Inserts a script or stylesheet in the given tab, and returns a promise + * which resolves when the operation has completed. + * + * @param {BaseContext} context + * The extension context for which to perform the injection. + * @param {InjectDetails} details + * The InjectDetails object, specifying what to inject, where, and + * when. + * @param {string} kind + * The kind of data being injected. Either "script" or "css". + * @param {string} method + * The name of the method which was called to trigger the injection. + * Used to generate appropriate error messages on failure. + * + * @returns {Promise} + * Resolves to the result of the execution, once it has completed. + * @private + */ + private _execute; + /** + * Executes a script in the tab's content window, and returns a Promise which + * resolves to the result of the evaluation, or rejects to the value of any + * error the injection generates. + * + * @param {BaseContext} context + * The extension context for which to inject the script. + * @param {InjectDetails} details + * The InjectDetails object, specifying what to inject, where, and + * when. + * + * @returns {Promise} + * Resolves to the result of the evaluation of the given script, once + * it has completed, or rejects with any error the evaluation + * generates. + */ + executeScript(context: BaseContext, details: InjectDetails): Promise<any>; + /** + * Injects CSS into the tab's content window, and returns a Promise which + * resolves when the injection is complete. + * + * @param {BaseContext} context + * The extension context for which to inject the script. + * @param {InjectDetails} details + * The InjectDetails object, specifying what to inject, and where. + * + * @returns {Promise} + * Resolves when the injection has completed. + */ + insertCSS(context: BaseContext, details: InjectDetails): Promise<any>; + /** + * Removes CSS which was previously into the tab's content window via + * `insertCSS`, and returns a Promise which resolves when the operation is + * complete. + * + * @param {BaseContext} context + * The extension context for which to remove the CSS. + * @param {InjectDetails} details + * The InjectDetails object, specifying what to remove, and from where. + * + * @returns {Promise} + * Resolves when the operation has completed. + */ + removeCSS(context: BaseContext, details: InjectDetails): Promise<any>; +} +declare const WINDOW_ID_NONE: -1; +declare const WINDOW_ID_CURRENT: -2; +/** + * A platform-independent base class for extension-specific wrappers around + * native browser windows + * + * @param {Extension} extension + * The extension object for which this wrapper is being created. + * @param {DOMWindow} window + * The browser DOM window which is being wrapped. + * @param {integer} id + * The numeric ID of this DOM window object. This ID should be the same for + * every extension, and for the lifetime of the window. + */ +declare class WindowBase { + /** + * Returns the window state of the given window. + * + * @param {DOMWindow} window + * The window for which to return a state. + * + * @returns {string} + * The window's state. One of "normal", "minimized", "maximized", + * "fullscreen", or "docked". + * @static + * @abstract + */ + static getState(window: DOMWindow): string; + constructor(extension: any, window: any, id: any); + extension: any; + window: any; + id: any; + /** + * @property {nsIAppWindow} appWindow + * The nsIAppWindow object for this browser window. + * @readonly + */ + readonly get appWindow(): any; + /** + * Returns true if this window is the current window for the given extension + * context, false otherwise. + * + * @param {BaseContext} context + * The extension context for which to perform the check. + * + * @returns {boolean} + */ + isCurrentFor(context: BaseContext): boolean; + /** + * @property {string} type + * The type of the window, as defined by the WebExtension API. May be + * either "normal" or "popup". + * @readonly + */ + readonly get type(): "popup" | "normal"; + /** + * Converts this window object to a JSON-compatible object which may be + * returned to an extension, in the format required to be returned by + * WebExtension APIs. + * + * @param {object} [getInfo] + * An optional object, the properties of which determine what data is + * available on the result object. + * @param {boolean} [getInfo.populate] + * Of true, the result object will contain a `tabs` property, + * containing an array of converted Tab objects, one for each tab in + * the window. + * + * @returns {object} + */ + convert(getInfo?: { + populate?: boolean; + }): object; + /** + * Returns true if this window matches the the given query info object. Omitted + * or null have no effect on the match. + * + * @param {object} queryInfo + * The query info against which to match. + * @param {boolean} [queryInfo.currentWindow] + * Matches against against the return value of `isCurrentFor()` for the + * given context. + * @param {boolean} [queryInfo.lastFocusedWindow] + * Matches against the exact value of the window's `isLastFocused` attribute. + * @param {boolean} [queryInfo.windowId] + * Matches against the exact value of the window's ID, taking into + * account the special WINDOW_ID_CURRENT value. + * @param {string} [queryInfo.windowType] + * Matches against the exact value of the window's `type` attribute. + * @param {BaseContext} context + * The extension context for which the matching is being performed. + * Used to determine the current window for relevant properties. + * + * @returns {boolean} + * True if the window matches the query. + */ + matches(queryInfo: { + currentWindow?: boolean; + lastFocusedWindow?: boolean; + windowId?: boolean; + windowType?: string; + }, context: BaseContext): boolean; + /** + * @property {boolean} focused + * Returns true if the browser window is currently focused. + * @readonly + * @abstract + */ + readonly get focused(): void; + /** + * @property {integer} top + * Returns the pixel offset of the top of the window from the top of + * the screen. + * @readonly + * @abstract + */ + readonly get top(): void; + /** + * @property {integer} left + * Returns the pixel offset of the left of the window from the left of + * the screen. + * @readonly + * @abstract + */ + readonly get left(): void; + /** + * @property {integer} width + * Returns the pixel width of the window. + * @readonly + * @abstract + */ + readonly get width(): void; + /** + * @property {integer} height + * Returns the pixel height of the window. + * @readonly + * @abstract + */ + readonly get height(): void; + /** + * @property {boolean} incognito + * Returns true if this is a private browsing window, false otherwise. + * @readonly + * @abstract + */ + readonly get incognito(): void; + /** + * @property {boolean} alwaysOnTop + * Returns true if this window is constrained to always remain above + * other windows. + * @readonly + * @abstract + */ + readonly get alwaysOnTop(): void; + /** + * @property {boolean} isLastFocused + * Returns true if this is the browser window which most recently had + * focus. + * @readonly + * @abstract + */ + readonly get isLastFocused(): void; + set state(state: void); + /** + * @property {string} state + * Returns or sets the current state of this window, as determined by + * `getState()`. + * @abstract + */ + get state(): void; + /** + * @property {nsIURI | null} title + * Returns the current title of this window if the extension has permission + * to read it, or null otherwise. + * @readonly + */ + readonly get title(): any; + /** + * Returns an iterator of TabBase objects for each tab in this window. + * + * @returns {Iterator<TabBase>} + */ + getTabs(): Iterator<TabBase>; + /** + * Returns an iterator of TabBase objects for each highlighted tab in this window. + * + * @returns {Iterator<TabBase>} + */ + getHighlightedTabs(): Iterator<TabBase>; + /** + * @property {TabBase} The window's currently active tab. + */ + get activeTab(): void; + /** + * Returns the window's tab at the specified index. + * + * @param {integer} index + * The index of the desired tab. + * + * @returns {TabBase|undefined} + */ + getTabAtIndex(index: integer): TabBase | undefined; +} +/** + * The parameter type of "tab-attached" events, which are emitted when a + * pre-existing tab is attached to a new window. + * + * @typedef {object} TabAttachedEvent + * @property {NativeTab} tab + * The native tab object in the window to which the tab is being + * attached. This may be a different object than was used to represent + * the tab in the old window. + * @property {integer} tabId + * The ID of the tab being attached. + * @property {integer} newWindowId + * The ID of the window to which the tab is being attached. + * @property {integer} newPosition + * The position of the tab in the tab list of the new window. + */ +/** + * The parameter type of "tab-detached" events, which are emitted when a + * pre-existing tab is detached from a window, in order to be attached to a new + * window. + * + * @typedef {object} TabDetachedEvent + * @property {NativeTab} tab + * The native tab object in the window from which the tab is being + * detached. This may be a different object than will be used to + * represent the tab in the new window. + * @property {NativeTab} adoptedBy + * The native tab object in the window to which the tab will be attached, + * and is adopting the contents of this tab. This may be a different + * object than the tab in the previous window. + * @property {integer} tabId + * The ID of the tab being detached. + * @property {integer} oldWindowId + * The ID of the window from which the tab is being detached. + * @property {integer} oldPosition + * The position of the tab in the tab list of the window from which it is + * being detached. + */ +/** + * The parameter type of "tab-created" events, which are emitted when a + * new tab is created. + * + * @typedef {object} TabCreatedEvent + * @property {NativeTab} tab + * The native tab object for the tab which is being created. + */ +/** + * The parameter type of "tab-removed" events, which are emitted when a + * tab is removed and destroyed. + * + * @typedef {object} TabRemovedEvent + * @property {NativeTab} tab + * The native tab object for the tab which is being removed. + * @property {integer} tabId + * The ID of the tab being removed. + * @property {integer} windowId + * The ID of the window from which the tab is being removed. + * @property {boolean} isWindowClosing + * True if the tab is being removed because the window is closing. + */ +/** + * An object containing basic, extension-independent information about the window + * and tab that a XUL <browser> belongs to. + * + * @typedef {object} BrowserData + * @property {integer} tabId + * The numeric ID of the tab that a <browser> belongs to, or -1 if it + * does not belong to a tab. + * @property {integer} windowId + * The numeric ID of the browser window that a <browser> belongs to, or -1 + * if it does not belong to a browser window. + */ +/** + * A platform-independent base class for the platform-specific TabTracker + * classes, which track the opening and closing of tabs, and manage the mapping + * of them between numeric IDs and native tab objects. + * + * Instances of this class are EventEmitters which emit the following events, + * each with an argument of the given type: + * + * - "tab-attached" {@link TabAttacheEvent} + * - "tab-detached" {@link TabDetachedEvent} + * - "tab-created" {@link TabCreatedEvent} + * - "tab-removed" {@link TabRemovedEvent} + */ +declare class TabTrackerBase { + on(...args: any[]): any; + /** + * Called to initialize the tab tracking listeners the first time that an + * event listener is added. + * + * @protected + * @abstract + */ + protected init(): void; + /** + * Returns the numeric ID for the given native tab. + * + * @param {NativeTab} nativeTab + * The native tab for which to return an ID. + * + * @returns {integer} + * The tab's numeric ID. + * @abstract + */ + getId(nativeTab: NativeTab): integer; + /** + * Returns the native tab with the given numeric ID. + * + * @param {integer} tabId + * The numeric ID of the tab to return. + * @param {*} default_ + * The value to return if no tab exists with the given ID. + * + * @returns {NativeTab} + * @throws {ExtensionError} + * If no tab exists with the given ID and a default return value is not + * provided. + * @abstract + */ + getTab(tabId: integer, default_?: any): NativeTab; + /** + * Returns basic information about the tab and window that the given browser + * belongs to. + * + * @param {XULElement} browser + * The XUL browser element for which to return data. + * + * @returns {BrowserData} + * @abstract + */ + getBrowserData(browser: XULElement): BrowserData; + /** + * @property {NativeTab} activeTab + * Returns the native tab object for the active tab in the + * most-recently focused window, or null if no live tabs currently + * exist. + * @abstract + */ + get activeTab(): void; +} +/** + * A browser progress listener instance which calls a given listener function + * whenever the status of the given browser changes. + * + * @param {function(object): void} listener + * A function to be called whenever the status of a tab's top-level + * browser. It is passed an object with a `browser` property pointing to + * the XUL browser, and a `status` property with a string description of + * the browser's status. + * @private + */ +declare class StatusListener { + constructor(listener: any); + listener: any; + onStateChange(browser: any, webProgress: any, request: any, stateFlags: any, statusCode: any): void; + onLocationChange(browser: any, webProgress: any, request: any, locationURI: any, flags: any): void; +} +/** + * A platform-independent base class for the platform-specific WindowTracker + * classes, which track the opening and closing of windows, and manage the + * mapping of them between numeric IDs and native tab objects. + */ +declare class WindowTrackerBase { + /** + * A private method which is called whenever a new browser window is opened, + * and adds the necessary listeners to it. + * + * @param {DOMWindow} window + * The window being opened. + * @private + */ + private _handleWindowOpened; + _openListeners: Set<any>; + _closeListeners: Set<any>; + _listeners: any; + _statusListeners: any; + _windowIds: any; + isBrowserWindow(window: any): boolean; + /** + * Returns an iterator for all currently active browser windows. + * + * @param {boolean} [includeInomplete = false] + * If true, include browser windows which are not yet fully loaded. + * Otherwise, only include windows which are. + * + * @returns {Iterator<DOMWindow>} + */ + browserWindows(includeIncomplete?: boolean): Iterator<DOMWindow>; + /** + * @property {DOMWindow|null} topWindow + * The currently active, or topmost, browser window, or null if no + * browser window is currently open. + * @readonly + */ + readonly get topWindow(): any; + /** + * @property {DOMWindow|null} topWindow + * The currently active, or topmost, browser window that is not + * private browsing, or null if no browser window is currently open. + * @readonly + */ + readonly get topNonPBWindow(): any; + /** + * Returns the top window accessible by the extension. + * + * @param {BaseContext} context + * The extension context for which to return the current window. + * + * @returns {DOMWindow|null} + */ + getTopWindow(context: BaseContext): DOMWindow | null; + /** + * Returns the numeric ID for the given browser window. + * + * @param {DOMWindow} window + * The DOM window for which to return an ID. + * + * @returns {integer} + * The window's numeric ID. + */ + getId(window: DOMWindow): integer; + /** + * Returns the browser window to which the given context belongs, or the top + * browser window if the context does not belong to a browser window. + * + * @param {BaseContext} context + * The extension context for which to return the current window. + * + * @returns {DOMWindow|null} + */ + getCurrentWindow(context: BaseContext): DOMWindow | null; + /** + * Returns the browser window with the given ID. + * + * @param {integer} id + * The ID of the window to return. + * @param {BaseContext} context + * The extension context for which the matching is being performed. + * Used to determine the current window for relevant properties. + * @param {boolean} [strict = true] + * If false, undefined will be returned instead of throwing an error + * in case no window exists with the given ID. + * + * @returns {DOMWindow|undefined} + * @throws {ExtensionError} + * If no window exists with the given ID and `strict` is true. + */ + getWindow(id: integer, context: BaseContext, strict?: boolean): DOMWindow | undefined; + /** + * @property {boolean} _haveListeners + * Returns true if any window open or close listeners are currently + * registered. + * @private + */ + private get _haveListeners(); + /** + * Register the given listener function to be called whenever a new browser + * window is opened. + * + * @param {function(DOMWindow): void} listener + * The listener function to register. + */ + addOpenListener(listener: (arg0: DOMWindow) => void): void; + /** + * Unregister a listener function registered in a previous addOpenListener + * call. + * + * @param {function(DOMWindow): void} listener + * The listener function to unregister. + */ + removeOpenListener(listener: (arg0: DOMWindow) => void): void; + /** + * Register the given listener function to be called whenever a browser + * window is closed. + * + * @param {function(DOMWindow): void} listener + * The listener function to register. + */ + addCloseListener(listener: (arg0: DOMWindow) => void): void; + /** + * Unregister a listener function registered in a previous addCloseListener + * call. + * + * @param {function(DOMWindow): void} listener + * The listener function to unregister. + */ + removeCloseListener(listener: (arg0: DOMWindow) => void): void; + /** + * Handles load events for recently-opened windows, and adds additional + * listeners which may only be safely added when the window is fully loaded. + * + * @param {Event} event + * A DOM event to handle. + * @private + */ + private handleEvent; + /** + * Observes "domwindowopened" and "domwindowclosed" events, notifies the + * appropriate listeners, and adds necessary additional listeners to the new + * windows. + * + * @param {DOMWindow} window + * A DOM window. + * @param {string} topic + * The topic being observed. + * @private + */ + private observe; + /** + * Add an event listener to be called whenever the given DOM event is received + * at the top level of any browser window. + * + * @param {string} type + * The type of event to listen for. May be any valid DOM event name, or + * one of the following special cases: + * + * - "progress": Adds a tab progress listener to every browser window. + * - "status": Adds a StatusListener to every tab of every browser + * window. + * - "domwindowopened": Acts as an alias for addOpenListener. + * - "domwindowclosed": Acts as an alias for addCloseListener. + * @param {Function | object} listener + * The listener to invoke in response to the given events. + * + * @returns {undefined} + */ + addListener(type: string, listener: Function | object): undefined; + /** + * Removes an event listener previously registered via an addListener call. + * + * @param {string} type + * The type of event to stop listening for. + * @param {Function | object} listener + * The listener to remove. + * + * @returns {undefined} + */ + removeListener(type: string, listener: Function | object): undefined; + /** + * Adds a listener for the given event to the given window. + * + * @param {DOMWindow} window + * The browser window to which to add the listener. + * @param {string} eventType + * The type of DOM event to listen for, or "progress" to add a tab + * progress listener. + * @param {Function | object} listener + * The listener to add. + * @private + */ + private _addWindowListener; + /** + * Adds a tab progress listener to the given browser window. + * + * @param {DOMWindow} window + * The browser window to which to add the listener. + * @param {object} listener + * The tab progress listener to add. + * @abstract + */ + addProgressListener(window: DOMWindow, listener: object): void; + /** + * Removes a tab progress listener from the given browser window. + * + * @param {DOMWindow} window + * The browser window from which to remove the listener. + * @param {object} listener + * The tab progress listener to remove. + * @abstract + */ + removeProgressListener(window: DOMWindow, listener: object): void; +} +/** + * Manages native tabs, their wrappers, and their dynamic permissions for a + * particular extension. + * + * @param {Extension} extension + * The extension for which to manage tabs. + */ +declare class TabManagerBase { + constructor(extension: any); + extension: any; + _tabs: any; + /** + * If the extension has requested activeTab permission, grant it those + * permissions for the current inner window in the given native tab. + * + * @param {NativeTab} nativeTab + * The native tab for which to grant permissions. + */ + addActiveTabPermission(nativeTab: NativeTab): void; + /** + * Revoke the extension's activeTab permissions for the current inner window + * of the given native tab. + * + * @param {NativeTab} nativeTab + * The native tab for which to revoke permissions. + */ + revokeActiveTabPermission(nativeTab: NativeTab): void; + /** + * Returns true if the extension has requested activeTab permission, and has + * been granted permissions for the current inner window if this tab. + * + * @param {NativeTab} nativeTab + * The native tab for which to check permissions. + * @returns {boolean} + * True if the extension has activeTab permissions for this tab. + */ + hasActiveTabPermission(nativeTab: NativeTab): boolean; + /** + * Activate MV3 content scripts if the extension has activeTab or an + * (ungranted) host permission. + * + * @param {NativeTab} nativeTab + */ + activateScripts(nativeTab: NativeTab): void; + /** + * Returns true if the extension has permissions to access restricted + * properties of the given native tab. In practice, this means that it has + * either requested the "tabs" permission or has activeTab permissions for the + * given tab. + * + * NOTE: Never use this method on an object that is not a native tab + * for the current platform: this method implicitly generates a wrapper + * for the passed nativeTab parameter and the platform-specific tabTracker + * instance is likely to store it in a map which is cleared only when the + * tab is closed (and so, if nativeTab is not a real native tab, it will + * never be cleared from the platform-specific tabTracker instance), + * See Bug 1458918 for a rationale. + * + * @param {NativeTab} nativeTab + * The native tab for which to check permissions. + * @returns {boolean} + * True if the extension has permissions for this tab. + */ + hasTabPermission(nativeTab: NativeTab): boolean; + /** + * Returns this extension's TabBase wrapper for the given native tab. This + * method will always return the same wrapper object for any given native tab. + * + * @param {NativeTab} nativeTab + * The tab for which to return a wrapper. + * + * @returns {TabBase|undefined} + * The wrapper for this tab. + */ + getWrapper(nativeTab: NativeTab): TabBase | undefined; + /** + * Determines access using extension context. + * + * @param {NativeTab} nativeTab + * The tab to check access on. + * @returns {boolean} + * True if the extension has permissions for this tab. + * @protected + * @abstract + */ + protected canAccessTab(nativeTab: NativeTab): boolean; + /** + * Converts the given native tab to a JSON-compatible object, in the format + * required to be returned by WebExtension APIs, which may be safely passed to + * extension code. + * + * @param {NativeTab} nativeTab + * The native tab to convert. + * @param {object} [fallbackTabSize] + * A geometry data if the lazy geometry data for this tab hasn't been + * initialized yet. + * + * @returns {object} + */ + convert(nativeTab: NativeTab, fallbackTabSize?: object): object; + /** + * Returns an iterator of TabBase objects which match the given query info. + * + * @param {object | null} [queryInfo = null] + * An object containing properties on which to filter. May contain any + * properties which are recognized by {@link TabBase#matches} or + * {@link WindowBase#matches}. Unknown properties will be ignored. + * @param {BaseContext|null} [context = null] + * The extension context for which the matching is being performed. + * Used to determine the current window for relevant properties. + * + * @returns {Iterator<TabBase>} + */ + query(queryInfo?: object | null, context?: BaseContext | null): Iterator<TabBase>; + /** + * Returns a TabBase wrapper for the tab with the given ID. + * + * @param {integer} tabId + * The ID of the tab for which to return a wrapper. + * + * @returns {TabBase} + * @throws {ExtensionError} + * If no tab exists with the given ID. + * @abstract + */ + get(tabId: integer): TabBase; + /** + * Returns a new TabBase instance wrapping the given native tab. + * + * @param {NativeTab} nativeTab + * The native tab for which to return a wrapper. + * + * @returns {TabBase} + * @protected + * @abstract + */ + protected wrapTab(nativeTab: NativeTab): TabBase; +} +/** + * Manages native browser windows and their wrappers for a particular extension. + * + * @param {Extension} extension + * The extension for which to manage windows. + */ +declare class WindowManagerBase { + constructor(extension: any); + extension: any; + _windows: any; + /** + * Converts the given browser window to a JSON-compatible object, in the + * format required to be returned by WebExtension APIs, which may be safely + * passed to extension code. + * + * @param {DOMWindow} window + * The browser window to convert. + * @param {*} args + * Additional arguments to be passed to {@link WindowBase#convert}. + * + * @returns {object} + */ + convert(window: DOMWindow, ...args: any): object; + /** + * Returns this extension's WindowBase wrapper for the given browser window. + * This method will always return the same wrapper object for any given + * browser window. + * + * @param {DOMWindow} window + * The browser window for which to return a wrapper. + * + * @returns {WindowBase|undefined} + * The wrapper for this tab. + */ + getWrapper(window: DOMWindow): WindowBase | undefined; + /** + * Returns whether this window can be accessed by the extension in the given + * context. + * + * @param {DOMWindow} window + * The browser window that is being tested + * @param {BaseContext|null} context + * The extension context for which this test is being performed. + * @returns {boolean} + */ + canAccessWindow(window: DOMWindow, context: BaseContext | null): boolean; + /** + * Returns an iterator of WindowBase objects which match the given query info. + * + * @param {object | null} [queryInfo = null] + * An object containing properties on which to filter. May contain any + * properties which are recognized by {@link WindowBase#matches}. + * Unknown properties will be ignored. + * @param {BaseContext|null} [context = null] + * The extension context for which the matching is being performed. + * Used to determine the current window for relevant properties. + * + * @returns {Iterator<WindowBase>} + */ + query(queryInfo?: object | null, context?: BaseContext | null): Iterator<WindowBase>; + /** + * Returns a WindowBase wrapper for the browser window with the given ID. + * + * @param {integer} windowId + * The ID of the browser window for which to return a wrapper. + * @param {BaseContext} context + * The extension context for which the matching is being performed. + * Used to determine the current window for relevant properties. + * + * @returns {WindowBase} + * @throws {ExtensionError} + * If no window exists with the given ID. + * @abstract + */ + get(windowId: integer, context: BaseContext): WindowBase; + /** + * Returns an iterator of WindowBase wrappers for each currently existing + * browser window. + * + * @returns {Iterator<WindowBase>} + * @abstract + */ + getAll(): Iterator<WindowBase>; + /** + * Returns a new WindowBase instance wrapping the given browser window. + * + * @param {DOMWindow} window + * The browser window for which to return a wrapper. + * + * @returns {WindowBase} + * @protected + * @abstract + */ + protected wrapWindow(window: DOMWindow): WindowBase; +} +/** + * The platform-specific type of native tab objects, which are wrapped by + * TabBase instances. + */ +type NativeTab = object | XULElement; +type MutedInfo = { + /** + * True if the tab is currently muted, false otherwise. + */ + muted: boolean; + /** + * The reason the tab is muted. Either "user", if the tab was muted by a + * user, or "extension", if it was muted by an extension. + */ + reason?: string; + /** + * If the tab was muted by an extension, contains the internal ID of that + * extension. + */ + extensionId?: string; +}; +/** + * The parameter type of "tab-attached" events, which are emitted when a + * pre-existing tab is attached to a new window. + */ +type TabAttachedEvent = { + /** + * The native tab object in the window to which the tab is being + * attached. This may be a different object than was used to represent + * the tab in the old window. + */ + tab: NativeTab; + /** + * The ID of the tab being attached. + */ + tabId: integer; + /** + * The ID of the window to which the tab is being attached. + */ + newWindowId: integer; + /** + * The position of the tab in the tab list of the new window. + */ + newPosition: integer; +}; +/** + * The parameter type of "tab-detached" events, which are emitted when a + * pre-existing tab is detached from a window, in order to be attached to a new + * window. + */ +type TabDetachedEvent = { + /** + * The native tab object in the window from which the tab is being + * detached. This may be a different object than will be used to + * represent the tab in the new window. + */ + tab: NativeTab; + /** + * The native tab object in the window to which the tab will be attached, + * and is adopting the contents of this tab. This may be a different + * object than the tab in the previous window. + */ + adoptedBy: NativeTab; + /** + * The ID of the tab being detached. + */ + tabId: integer; + /** + * The ID of the window from which the tab is being detached. + */ + oldWindowId: integer; + /** + * The position of the tab in the tab list of the window from which it is + * being detached. + */ + oldPosition: integer; +}; +/** + * The parameter type of "tab-created" events, which are emitted when a + * new tab is created. + */ +type TabCreatedEvent = { + /** + * The native tab object for the tab which is being created. + */ + tab: NativeTab; +}; +/** + * The parameter type of "tab-removed" events, which are emitted when a + * tab is removed and destroyed. + */ +type TabRemovedEvent = { + /** + * The native tab object for the tab which is being removed. + */ + tab: NativeTab; + /** + * The ID of the tab being removed. + */ + tabId: integer; + /** + * The ID of the window from which the tab is being removed. + */ + windowId: integer; + /** + * True if the tab is being removed because the window is closing. + */ + isWindowClosing: boolean; +}; +/** + * An object containing basic, extension-independent information about the window + * and tab that a XUL <browser> belongs to. + */ +type BrowserData = { + /** + * The numeric ID of the tab that a <browser> belongs to, or -1 if it + * does not belong to a tab. + */ + tabId: integer; + /** + * The numeric ID of the browser window that a <browser> belongs to, or -1 + * if it does not belong to a browser window. + */ + windowId: integer; +}; + +} // namespace tabs_base + +declare global { + type TabTrackerBase = tabs_base.TabTrackerBase; + type TabManagerBase = tabs_base.TabManagerBase; + type TabBase = tabs_base.TabBase; + type WindowTrackerBase = tabs_base.WindowTrackerBase; + type WindowManagerBase = tabs_base.WindowManagerBase; + type WindowBase = tabs_base.WindowBase; + type getUserContextIdForCookieStoreId = tabs_base.getUserContextIdForCookieStoreId; + type NativeTab = tabs_base.NativeTab; +} + +export {}; diff --git a/toolkit/components/extensions/types/extensions.ts b/toolkit/components/extensions/types/extensions.ts index 8f9555421b..e232ab4a3f 100644 --- a/toolkit/components/extensions/types/extensions.ts +++ b/toolkit/components/extensions/types/extensions.ts @@ -1,5 +1,5 @@ /** - * Type declarations for WebExtensions framework code. + * Types specific to toolkit/extensions code. */ // This has every possible property we import from all modules, which is not @@ -42,39 +42,37 @@ type LazyAll = { getTrimmedString: typeof import("ExtensionTelemetry.sys.mjs").getTrimmedString, }; -// Utility type to extract all strings from a const array, to use as keys. -type Items<A> = A extends ReadonlyArray<infer U extends string> ? U : never; - declare global { - type Lazy<Keys extends keyof LazyAll = keyof LazyAll> = Pick<LazyAll, Keys> & { [k: string]: any }; + type Lazy = Partial<LazyAll> & { [k: string]: any }; - // Export JSDoc types, and make other classes available globally. - type ConduitAddress = import("ConduitsParent.sys.mjs").ConduitAddress; - type ConduitID = import("ConduitsParent.sys.mjs").ConduitID; + type BaseContext = import("ExtensionCommon.sys.mjs").BaseContext; + type ExtensionChild = import("ExtensionChild.sys.mjs").ExtensionChild; type Extension = import("Extension.sys.mjs").Extension; + type callback = (...any) => any; + + interface nsIDOMProcessChild { + getActor(name: "ProcessConduits"): ProcessConduitsChild; + } - // Something about Class type not being exported when nested in a namespace? - type BaseContext = InstanceType<typeof import("ExtensionCommon.sys.mjs").ExtensionCommon.BaseContext>; - type BrowserExtensionContent = InstanceType<typeof import("ExtensionContent.sys.mjs").ExtensionContent.BrowserExtensionContent>; - type EventEmitter = InstanceType<typeof import("ExtensionCommon.sys.mjs").ExtensionCommon.EventEmitter>; - type ExtensionAPI = InstanceType<typeof import("ExtensionCommon.sys.mjs").ExtensionCommon.ExtensionAPI>; - type ExtensionError = InstanceType<typeof import("ExtensionUtils.sys.mjs").ExtensionUtils.ExtensionError>; - type LocaleData = InstanceType<typeof import("ExtensionCommon.sys.mjs").ExtensionCommon.LocaleData>; - type ProxyAPIImplementation = InstanceType<typeof import("ExtensionChild.sys.mjs").ExtensionChild.ProxyAPIImplementation>; - type SchemaAPIInterface = InstanceType<typeof import("ExtensionCommon.sys.mjs").ExtensionCommon.SchemaAPIInterface>; - type WorkerExtensionError = InstanceType<typeof import("ExtensionUtils.sys.mjs").ExtensionUtils.WorkerExtensionError>; + interface WebExtensionContentScript { + userScriptOptions: { scriptMetadata: object }; + } - // Other misc types. - type AddonWrapper = any; - type Context = BaseContext; - type NativeTab = Element; - type SavedFrame = object; + interface WebExtensionPolicy { + extension: Extension; + debugName: string; + instanceId: string; + optionalPermissions: string[]; + } // Can't define a const generic parameter in jsdocs yet. // https://github.com/microsoft/TypeScript/issues/56634 - type ConduitInit<Send> = ConduitAddress & { send: Send; }; - type Conduit<Send> = import("../ConduitsChild.sys.mjs").PointConduit & { [s in `send${Items<Send>}`]: callback }; - type ConduitOpen = <const Send>(subject: object, address: ConduitInit<Send>) => Conduit<Send>; + function ConduitGen<const Send>(_, init: Init<Send>, _actor?): Conduit<Send>; + type Items<A> = A extends ReadonlyArray<infer U extends string> ? U : never; } -export {} +import { PointConduit, ProcessConduitsChild } from "ConduitsChild.sys.mjs"; +import { ConduitAddress } from "ConduitsParent.sys.mjs"; + +type Conduit<Send> = PointConduit & { [s in `send${Items<Send>}`]: callback }; +type Init<Send> = ConduitAddress & { send: Send; }; diff --git a/toolkit/components/extensions/types/gecko.ts b/toolkit/components/extensions/types/gecko.ts index f6b5190f8d..720919d794 100644 --- a/toolkit/components/extensions/types/gecko.ts +++ b/toolkit/components/extensions/types/gecko.ts @@ -1,163 +1,94 @@ /** - * Global Gecko type declarations. + * Gecko generic/specialized adjustments for xpcom and webidl types. */ -// @ts-ignore -import type { CiClass } from "lib.gecko.xpidl" - -declare global { - // Other misc types. - type Browser = InstanceType<typeof XULBrowserElement>; - type bytestring = string; - type callback = (...args: any[]) => any; - type ColorArray = number[]; - type integer = number; - type JSONValue = null | boolean | number | string | JSONValue[] | { [key: string]: JSONValue }; - - interface Document { - createXULElement(name: string): Element; - documentReadyForIdle: Promise<void>; - } - interface EventTarget { - ownerGlobal: Window; - } - interface Error { - code; - } - interface ErrorConstructor { - new (message?: string, options?: ErrorOptions, lineNo?: number): Error; - } - interface Window { - gBrowser; - } - // HACK to get the static isInstance for DOMException and Window? - interface Object { - isInstance(object: any): boolean; - } +// More specific types for parent process browsing contexts. +interface CanonicalBrowsingContext extends LoadContextMixin { + embedderElement: XULBrowserElement; + currentWindowContext: WindowGlobalParent; + parent: CanonicalBrowsingContext; + parentWindowContext: WindowGlobalParent; + top: CanonicalBrowsingContext; + topWindowContext: WindowGlobalParent; +} - // XPIDL additions/overrides. +interface ChromeWindow extends Window { + isChromeWindow: true; +} - interface nsISupports { - // OMG it works! - QueryInterface?<T extends CiClass<nsISupports>>(aCiClass: T): T['prototype']; - wrappedJSObject?: object; - } - interface nsIProperties { - get<T extends CiClass<nsISupports>>(prop: string, aCiClass: T): T['prototype']; - } - interface nsIPrefBranch { - getComplexValue<T extends CiClass<nsISupports>>(aPrefName: string, aCiClass: T): T['prototype']; - } - // TODO: incorporate above into lib.xpidl.d.ts generation, somehow? +interface Document { + createXULElement(name: "browser"): XULBrowserElement; +} - type Sandbox = typeof globalThis; - interface nsIXPCComponents_utils_Sandbox { - (principal: nsIPrincipal | nsIPrincipal[], options: object): Sandbox; - } - interface nsIXPCComponents_Utils { - cloneInto<T>(obj: T, ...args: any[]): T; - createObjectIn<T>(Sandbox, options?: T): T; - exportFunction<T extends callback>(f: T, ...args: any[]): T; - getWeakReference<T extends object>(T): { get(): T }; - readonly Sandbox: nsIXPCComponents_utils_Sandbox; - waiveXrays<T>(obj: T): T; - } - interface nsIDOMWindow extends Window { - docShell: nsIDocShell; - } - interface Document { - documentURIObject: nsIURI; - createXULElement(name: string): Element; - } +interface MessageListenerManagerMixin { + // Overloads that define `data` arg as required, since it's ~always expected. + addMessageListener(msg: string, listener: { receiveMessage(_: ReceiveMessageArgument & { data })}); + removeMessageListener(msg: string, listener: { receiveMessage(_: ReceiveMessageArgument & { data })}); +} + +interface MozQueryInterface { + <T>(iid: T): nsQIResult<T>; +} - // nsDocShell is the only thing implementing nsIDocShell, but it also - // implements nsIWebNavigation, and a few others, so this is "ok". - interface nsIDocShell extends nsIWebNavigation {} - interface nsISimpleEnumerator extends Iterable<any> {} +interface nsICryptoHash extends nsISupports { + // Accepts a TypedArray. + update(aData: ArrayLike<number>, aLen: number): void; +} - namespace Components { - type Exception = Error; - } - namespace UrlbarUtils { - type RESULT_TYPE = any; - type RESULT_SOURCE = any; +interface nsIDOMWindow extends Window {} + +interface nsISimpleEnumerator extends Iterable<any> {} + +interface nsISupports { + wrappedJSObject?: object; +} + +interface nsIXPCComponents_Constructor { + <const T, IIDs = nsIXPCComponents_Interfaces>(cid, id: T, init?): { + new (...any): nsQIResult<T extends keyof IIDs ? IIDs[T] : T>; + (...any): nsQIResult<T extends keyof IIDs ? IIDs[T] : T>; } +} + +interface nsIXPCComponents_Exception { + (...args: ConstructorParameters<typeof Error>): Error; +} + +interface nsIXPCComponents_utils_Sandbox { + (principal: nsIPrincipal | nsIPrincipal[], options: object): typeof globalThis; +} - // Various mozilla globals. - var Cc, Cr, ChromeUtils, Components, dump, uneval; - - // [ChromeOnly] WebIDL, to be generated. - var BrowsingContext, ChannelWrapper, ChromeWindow, ChromeWorker, - ClonedErrorHolder, Glean, InspectorUtils, IOUtils, JSProcessActorChild, - JSProcessActorParent, JSWindowActor, JSWindowActorChild, - JSWindowActorParent, L10nRegistry, L10nFileSource, Localization, - MatchGlob, MatchPattern, MatchPatternSet, PathUtils, PreloadedScript, - StructuredCloneHolder, TelemetryStopwatch, WindowGlobalChild, - WebExtensionContentScript, WebExtensionParentActor, WebExtensionPolicy, - XULBrowserElement, nsIMessageListenerManager; - - interface XULElement extends Element {} - - // nsIServices is not a thing. - interface nsIServices { - scriptloader: mozIJSSubScriptLoader; - locale: mozILocaleService; - intl: mozIMozIntl; - storage: mozIStorageService; - appShell: nsIAppShellService; - startup: nsIAppStartup; - blocklist: nsIBlocklistService; - cache2: nsICacheStorageService; - catMan: nsICategoryManager; - clearData: nsIClearDataService; - clipboard: nsIClipboard; - console: nsIConsoleService; - cookieBanners: nsICookieBannerService; - cookies: nsICookieManager & nsICookieService; - appinfo: nsICrashReporter & nsIXULAppInfo & nsIXULRuntime; - DAPTelemetry: nsIDAPTelemetry; - DOMRequest: nsIDOMRequestService; - dns: nsIDNSService; - dirsvc: nsIDirectoryService & nsIProperties; - droppedLinkHandler: nsIDroppedLinkHandler; - eTLD: nsIEffectiveTLDService; - policies: nsIEnterprisePolicies; - env: nsIEnvironment; - els: nsIEventListenerService; - fog: nsIFOG; - focus: nsIFocusManager; - io: nsIIOService & nsINetUtil & nsISpeculativeConnect; - loadContextInfo: nsILoadContextInfoFactory; - domStorageManager: nsIDOMStorageManager & nsILocalStorageManager; - logins: nsILoginManager; - obs: nsIObserverService; - perms: nsIPermissionManager; - prefs: nsIPrefBranch & nsIPrefService; - profiler: nsIProfiler; - prompt: nsIPromptService; - sysinfo: nsISystemInfo & nsIPropertyBag2; - qms: nsIQuotaManagerService; - rfp: nsIRFPService; - scriptSecurityManager: nsIScriptSecurityManager; - search: nsISearchService; - sessionStorage: nsISessionStorageService; - strings: nsIStringBundleService; - telemetry: nsITelemetry; - textToSubURI: nsITextToSubURI; - tm: nsIThreadManager; - uriFixup: nsIURIFixup; - urlFormatter: nsIURLFormatter; - uuid: nsIUUIDGenerator; - vc: nsIVersionComparator; - wm: nsIWindowMediator; - ww: nsIWindowWatcher; - xulStore: nsIXULStore; - ppmm: any; - cpmm: any; - mm: any; +interface nsXPCComponents_Classes { + [cid: string]: { + createInstance<T>(aID: T): nsQIResult<T>; + getService<T>(aID?: T): unknown extends T ? nsISupports : nsQIResult<T>; } +} - var Ci: nsIXPCComponents_Interfaces; - var Cu: nsIXPCComponents_Utils; - var Services: nsIServices; +// Generic overloads. +interface nsXPCComponents_Utils { + cloneInto<T>(value: T, ...any): T; + createObjectIn<T = object>(_, object?: T): T; + exportFunction<T>(func: T, ...any): T; + getWeakReference<T>(value: T): { get(): T }; + waiveXrays<T>(object: T): T; } + +// TODO: remove after next TS update. +interface PromiseConstructor { + withResolvers<T>(): { + promise: Promise<T>; + resolve: (value: T | PromiseLike<T>) => void; + reject: (reason?: any) => void; + }; +} + +// Hand-crafted artisanal types. +interface XULBrowserElement extends XULFrameElement, FrameLoader { + currentURI: nsIURI; + docShellIsActive: boolean; + isRemoteBrowser: boolean; + remoteType: string; +} + +type nsQIResult<iid> = import("gecko/lib.gecko.xpcom").nsQIResult<iid>; diff --git a/toolkit/components/extensions/types/glean.d.ts b/toolkit/components/extensions/types/glean.d.ts new file mode 100644 index 0000000000..842a5a56de --- /dev/null +++ b/toolkit/components/extensions/types/glean.d.ts @@ -0,0 +1,79 @@ +/** + * NOTE: Do not modify this file by hand. + * Content was generated from source metrics.yaml files. + */ + +interface GleanImpl { + + // toolkit/mozapps/extensions/metrics.yaml + + addonsManager: { + install: GleanEvent; + update: GleanEvent; + installStats: GleanEvent; + manage: GleanEvent; + report: GleanEvent; + reportSuspiciousSite: GleanEvent; + } + + blocklist: { + lastModifiedRsAddonsMblf: GleanDatetime; + mlbfSource: GleanString; + mlbfGenerationTime: GleanDatetime; + mlbfStashTimeOldest: GleanDatetime; + mlbfStashTimeNewest: GleanDatetime; + addonBlockChange: GleanEvent; + } + + // toolkit/components/extensions/metrics.yaml + + extensions: { + useRemotePref: GleanBoolean; + useRemotePolicy: GleanBoolean; + startupCacheLoadTime: GleanTimespan; + startupCacheReadErrors: Record<string, GleanCounter>; + startupCacheWriteBytelength: GleanQuantity; + processEvent: Record<string, GleanCounter>; + } + + extensionsApisDnr: { + startupCacheReadSize: GleanMemoryDistribution; + startupCacheReadTime: GleanTimingDistribution; + startupCacheWriteSize: GleanMemoryDistribution; + startupCacheWriteTime: GleanTimingDistribution; + startupCacheEntries: Record<string, GleanCounter>; + validateRulesTime: GleanTimingDistribution; + evaluateRulesTime: GleanTimingDistribution; + evaluateRulesCountMax: GleanQuantity; + } + + extensionsData: { + migrateResult: GleanEvent; + storageLocalError: GleanEvent; + } + + extensionsQuarantinedDomains: { + listsize: GleanQuantity; + listhash: GleanString; + remotehash: GleanString; + } + + extensionsCounters: { + browserActionPreloadResult: Record<string, GleanCounter>; + eventPageIdleResult: Record<string, GleanCounter>; + } + + extensionsTiming: { + backgroundPageLoad: GleanTimingDistribution; + browserActionPopupOpen: GleanTimingDistribution; + contentScriptInjection: GleanTimingDistribution; + eventPageRunningTime: GleanCustomDistribution; + extensionStartup: GleanTimingDistribution; + pageActionPopupOpen: GleanTimingDistribution; + storageLocalGetJson: GleanTimingDistribution; + storageLocalSetJson: GleanTimingDistribution; + storageLocalGetIdb: GleanTimingDistribution; + storageLocalSetIdb: GleanTimingDistribution; + } + +} diff --git a/toolkit/components/extensions/types/globals.ts b/toolkit/components/extensions/types/globals.ts index 45722828e2..dee9de0cf4 100644 --- a/toolkit/components/extensions/types/globals.ts +++ b/toolkit/components/extensions/types/globals.ts @@ -1,33 +1,32 @@ /** - * Support types for toolkit/components/extensions code. + * Gecko globals. */ +declare global { + const Cc: nsXPCComponents_Classes; + const Ci: nsIXPCComponents_Interfaces; + const Cr: nsIXPCComponents_Results; + const Components: nsIXPCComponents; -/// <reference lib="dom" /> -/// <reference path="./gecko.ts" /> -/// <reference path="./extensions.ts" /> + // Resolve typed generic overloads before the generated ones. + const Cu: nsXPCComponents_Utils & nsIXPCComponents_Utils; -// This now relies on types generated in bug 1872918, or get the built -// artifact tslib directly and put it in your src/node_modules/@types: -// https://phabricator.services.mozilla.com/D197620 -/// <reference types="lib.gecko.xpidl" /> + const Glean: GleanImpl; + const Services: JSServices; + const uneval: (any) => string; +} -// Exports for all other external modules redirected to globals.ts. -export var AppConstants, - GeckoViewConnection, GeckoViewWebExtension, IndexedDB, JSONFile, Log; +// Exports for all modules redirected here by a catch-all rule in tsconfig.json. +export var + AddonWrapper, AppConstants, GeckoViewConnection, GeckoViewWebExtension, + IndexedDB, JSONFile, Log, UrlbarUtils, WebExtensionDescriptorActor; /** - * This is a mock for the "class" from EventEmitter.sys.mjs. When we import - * it in extensions code using resource://gre/modules/EventEmitter.sys.mjs, - * the catch-all rule from tsconfig.json redirects it to this file. The export - * of the class below fulfills the import. The mock is needed when we subclass - * that EventEmitter, typescript gets confused because it's an old style - * function-and-prototype-based "class", and some types don't match up. - * + * A stub type for the "class" from EventEmitter.sys.mjs. * TODO: Convert EventEmitter.sys.mjs into a proper class. */ export declare class EventEmitter { + emit(event: string, ...args: any[]): void; on(event: string, listener: callback): void; once(event: string, listener: callback): Promise<any>; off(event: string, listener: callback): void; - emit(event: string, ...args: any[]): void; } diff --git a/toolkit/components/extensions/webrequest/StreamFilterParent.cpp b/toolkit/components/extensions/webrequest/StreamFilterParent.cpp index 467616ed0f..ba8700e7f1 100644 --- a/toolkit/components/extensions/webrequest/StreamFilterParent.cpp +++ b/toolkit/components/extensions/webrequest/StreamFilterParent.cpp @@ -230,7 +230,7 @@ StreamFilterParent::CheckListenerChain() { if (trsl) { return trsl->CheckListenerChain(); } - return NS_ERROR_FAILURE; + return NS_ERROR_NO_INTERFACE; } NS_IMETHODIMP diff --git a/toolkit/components/forgetaboutsite/test/unit/head_forgetaboutsite.js b/toolkit/components/forgetaboutsite/test/unit/head_forgetaboutsite.js index 0e0e178e96..93d72aece3 100644 --- a/toolkit/components/forgetaboutsite/test/unit/head_forgetaboutsite.js +++ b/toolkit/components/forgetaboutsite/test/unit/head_forgetaboutsite.js @@ -21,7 +21,7 @@ async function cleanUp() { await new Promise(resolve => { Services.clearData.deleteData( Ci.nsIClearDataService.CLEAR_PERMISSIONS, - value => resolve() + () => resolve() ); }); } diff --git a/toolkit/components/formautofill/AutofillProfileAutoComplete.sys.mjs b/toolkit/components/formautofill/AutofillProfileAutoComplete.sys.mjs index fc3f0454b0..7594fc8fcf 100644 --- a/toolkit/components/formautofill/AutofillProfileAutoComplete.sys.mjs +++ b/toolkit/components/formautofill/AutofillProfileAutoComplete.sys.mjs @@ -6,7 +6,10 @@ * Form Autofill content process module. */ -import { GenericAutocompleteItem } from "resource://gre/modules/FillHelpers.sys.mjs"; +import { + GenericAutocompleteItem, + sendFillRequestToParent, +} from "resource://gre/modules/FillHelpers.sys.mjs"; /* eslint-disable no-use-before-define */ @@ -31,16 +34,6 @@ const autocompleteController = Cc[ ChromeUtils.defineLazyGetter( lazy, - "ADDRESSES_COLLECTION_NAME", - () => lazy.FormAutofillUtils.ADDRESSES_COLLECTION_NAME -); -ChromeUtils.defineLazyGetter( - lazy, - "CREDITCARDS_COLLECTION_NAME", - () => lazy.FormAutofillUtils.CREDITCARDS_COLLECTION_NAME -); -ChromeUtils.defineLazyGetter( - lazy, "FIELD_STATES", () => lazy.FormAutofillUtils.FIELD_STATES ); @@ -191,14 +184,8 @@ AutofillProfileAutoCompleteSearch.prototype = { isInputAutofilled, }); } else { - let infoWithoutElement = { ...activeFieldDetail }; - delete infoWithoutElement.elementWeakRef; - - let data = { - collectionName: isAddressField - ? lazy.ADDRESSES_COLLECTION_NAME - : lazy.CREDITCARDS_COLLECTION_NAME, - info: infoWithoutElement, + const data = { + fieldName: activeFieldDetail.fieldName, searchString, }; @@ -284,12 +271,10 @@ AutofillProfileAutoCompleteSearch.prototype = { * Input element for autocomplete. * @param {object} data * Parameters for querying the corresponding result. - * @param {string} data.collectionName - * The name used to specify which collection to retrieve records. * @param {string} data.searchString * The typed string for filtering out the matched records. - * @param {string} data.info - * The input autocomplete property's information. + * @param {string} data.fieldName + * The identified field name for the input * @returns {Promise} * Promise that resolves when addresses returned from parent process. */ @@ -368,44 +353,6 @@ export const ProfileAutocomplete = { } }, - fillRequestId: 0, - - async sendFillRequestToFormAutofillParent(input, comment) { - if (!comment) { - return false; - } - - if (!input || input != autocompleteController?.input.focusedInput) { - return false; - } - - const { fillMessageName, fillMessageData } = JSON.parse(comment ?? "{}"); - if (!fillMessageName) { - return false; - } - - this.fillRequestId++; - const fillRequestId = this.fillRequestId; - const actor = getActorFromWindow(input.ownerGlobal, "FormAutofill"); - const value = await actor.sendQuery(fillMessageName, fillMessageData ?? {}); - - // skip fill if another fill operation started during await - if (fillRequestId != this.fillRequestId) { - return false; - } - - if (typeof value !== "string") { - return false; - } - - // If AutoFillParent returned a string to fill, we must do it here because - // nsAutoCompleteController.cpp already finished it's work before we finished await. - input.setUserInput(value); - input.select(value.length, value.length); - - return true; - }, - _getSelectedIndex(contentWindow) { let actor = getActorFromWindow(contentWindow, "AutoComplete"); if (!actor) { @@ -431,18 +378,31 @@ export const ProfileAutocomplete = { ? this.lastProfileAutoCompleteResult.getCommentAt(selectedIndex) : null; + let profile = JSON.parse(comment); if ( selectedIndex == -1 || !this.lastProfileAutoCompleteResult || - this.lastProfileAutoCompleteResult.getStyleAt(selectedIndex) != - "autofill-profile" + this.lastProfileAutoCompleteResult.getStyleAt(selectedIndex) != "autofill" ) { - await this.sendFillRequestToFormAutofillParent(focusedInput, comment); + if ( + focusedInput && + focusedInput == autocompleteController?.input.focusedInput + ) { + if (profile?.fillMessageName == "FormAutofill:ClearForm") { + // The child can do this directly. + getActorFromWindow(focusedInput.ownerGlobal)?.clearForm(); + } else { + // Pass focusedInput as both input arguments. + await sendFillRequestToParent( + "FormAutofill", + autocompleteController.input, + comment + ); + } + } return; } - let profile = JSON.parse(comment); - await lazy.FormAutofillContent.activeHandler.autofillFormFields(profile); }, @@ -468,8 +428,7 @@ export const ProfileAutocomplete = { if ( !this.lastProfileAutoCompleteResult || - this.lastProfileAutoCompleteResult.getStyleAt(selectedIndex) != - "autofill-profile" + this.lastProfileAutoCompleteResult.getStyleAt(selectedIndex) != "autofill" ) { return; } diff --git a/toolkit/components/formautofill/FormAutofillChild.sys.mjs b/toolkit/components/formautofill/FormAutofillChild.sys.mjs index 8678a7bd45..af84459432 100644 --- a/toolkit/components/formautofill/FormAutofillChild.sys.mjs +++ b/toolkit/components/formautofill/FormAutofillChild.sys.mjs @@ -124,33 +124,18 @@ export class FormAutofillChild extends JSWindowActorChild { lazy.AutoCompleteChild.removePopupStateListener(this); } - popupStateChanged(messageName, data, _target) { - let docShell; - try { - docShell = this.docShell; - } catch (ex) { - lazy.AutoCompleteChild.removePopupStateListener(this); - return; - } - + popupStateChanged(messageName, _data, _target) { if (!lazy.FormAutofill.isAutofillEnabled) { return; } - const { chromeEventHandler } = docShell; - switch (messageName) { - case "FormAutoComplete:PopupClosed": { - this.onPopupClosed(data.selectedRowStyle); - Services.tm.dispatchToMainThread(() => { - chromeEventHandler.removeEventListener("keydown", this, true); - }); - + case "AutoComplete:PopupClosed": { + this.onPopupClosed(); break; } - case "FormAutoComplete:PopupOpened": { + case "AutoComplete:PopupOpened": { this.onPopupOpened(); - chromeEventHandler.addEventListener("keydown", this, true); break; } } @@ -385,10 +370,6 @@ export class FormAutofillChild extends JSWindowActorChild { } switch (evt.type) { - case "keydown": { - this._onKeyDown(evt); - break; - } case "focusin": { if (lazy.FormAutofill.isAutofillEnabled) { this.onFocusIn(evt); @@ -497,11 +478,9 @@ export class FormAutofillChild extends JSWindowActorChild { return; } - const doc = this.document; - switch (message.name) { case "FormAutofill:PreviewProfile": { - this.previewProfile(doc); + this.previewProfile(message.data.selectedIndex); break; } case "FormAutofill:ClearForm": { @@ -677,9 +656,7 @@ export class FormAutofillChild extends JSWindowActorChild { } } - previewProfile(doc) { - let docWin = doc.ownerGlobal; - let selectedIndex = lazy.ProfileAutocomplete._getSelectedIndex(docWin); + previewProfile(selectedIndex) { let lastAutoCompleteResult = lazy.ProfileAutocomplete.lastProfileAutoCompleteResult; let focusedInput = this.activeInput; @@ -688,55 +665,17 @@ export class FormAutofillChild extends JSWindowActorChild { selectedIndex === -1 || !focusedInput || !lastAutoCompleteResult || - lastAutoCompleteResult.getStyleAt(selectedIndex) != "autofill-profile" + lastAutoCompleteResult.getStyleAt(selectedIndex) != "autofill" ) { - this.sendAsyncMessage("FormAutofill:UpdateWarningMessage", {}); - lazy.ProfileAutocomplete._clearProfilePreview(); } else { - let focusedInputDetails = this.activeFieldDetail; - let profile = JSON.parse( - lastAutoCompleteResult.getCommentAt(selectedIndex) - ); - let allFieldNames = this.activeSection.allFieldNames; - let profileFields = allFieldNames.filter( - fieldName => !!profile[fieldName] - ); - - let focusedCategory = lazy.FormAutofillUtils.getCategoryFromFieldName( - focusedInputDetails.fieldName - ); - let categories = - lazy.FormAutofillUtils.getCategoriesFromFieldNames(profileFields); - this.sendAsyncMessage("FormAutofill:UpdateWarningMessage", { - focusedCategory, - categories, - }); - lazy.ProfileAutocomplete._previewSelectedProfile(selectedIndex); } } - onPopupClosed(selectedRowStyle) { + onPopupClosed() { this.debug("Popup has closed."); lazy.ProfileAutocomplete._clearProfilePreview(); - - let lastAutoCompleteResult = - lazy.ProfileAutocomplete.lastProfileAutoCompleteResult; - let focusedInput = this.activeInput; - if ( - lastAutoCompleteResult && - this._keyDownEnterForInput && - focusedInput === this._keyDownEnterForInput && - focusedInput === - lazy.ProfileAutocomplete.lastProfileAutoCompleteFocusedInput - ) { - if (selectedRowStyle == "autofill-footer") { - this.sendAsyncMessage("FormAutofill:OpenPreferences"); - } else if (selectedRowStyle == "autofill-clear-button") { - this.clearForm(); - } - } } onPopupOpened() { @@ -764,21 +703,4 @@ export class FormAutofillChild extends JSWindowActorChild { formFillController.markAsAutofillField(field); } - - _onKeyDown(e) { - delete this._keyDownEnterForInput; - let lastAutoCompleteResult = - lazy.ProfileAutocomplete.lastProfileAutoCompleteResult; - let focusedInput = this.activeInput; - if ( - e.keyCode != e.DOM_VK_RETURN || - !lastAutoCompleteResult || - !focusedInput || - focusedInput != - lazy.ProfileAutocomplete.lastProfileAutoCompleteFocusedInput - ) { - return; - } - this._keyDownEnterForInput = focusedInput; - } } diff --git a/toolkit/components/formautofill/FormAutofillParent.sys.mjs b/toolkit/components/formautofill/FormAutofillParent.sys.mjs index 61c4bd2943..34dac8ce15 100644 --- a/toolkit/components/formautofill/FormAutofillParent.sys.mjs +++ b/toolkit/components/formautofill/FormAutofillParent.sys.mjs @@ -84,21 +84,6 @@ export let FormAutofillStatus = { Services.prefs.addObserver(ENABLED_AUTOFILL_CREDITCARDS_PREF, this); } - // We have to use empty window type to get all opened windows here because the - // window type parameter may not be available during startup. - for (let win of Services.wm.getEnumerator("")) { - let { documentElement } = win.document; - if (documentElement?.getAttribute("windowtype") == "navigator:browser") { - this.injectElements(win.document); - } else { - // Manually call onOpenWindow for windows that are already opened but not - // yet have the window type set. This ensures we inject the elements we need - // when its docuemnt is ready. - this.onOpenWindow(win); - } - } - Services.wm.addListener(this); - Services.telemetry.setEventRecordingEnabled("creditcard", true); Services.telemetry.setEventRecordingEnabled("address", true); }, @@ -198,31 +183,6 @@ export let FormAutofillStatus = { this.updateStatus(); }, - injectElements(doc) { - Services.scriptloader.loadSubScript( - "chrome://formautofill/content/customElements.js", - doc.ownerGlobal - ); - }, - - onOpenWindow(xulWindow) { - const win = xulWindow.docShell.domWindow; - win.addEventListener( - "load", - () => { - if ( - win.document.documentElement.getAttribute("windowtype") == - "navigator:browser" - ) { - this.injectElements(win.document); - } - }, - { once: true } - ); - }, - - onCloseWindow() {}, - async observe(subject, topic, data) { lazy.log.debug("observe:", topic, "with data:", data); switch (topic) { @@ -312,7 +272,7 @@ export class FormAutofillParent extends JSWindowActorParent { scenarioName: data.scenarioName, hasInput: !!data.searchString?.length, }); - const recordsPromise = FormAutofillParent._getRecords(data); + const recordsPromise = FormAutofillParent.getRecords(data); const [records, externalEntries] = await Promise.all([ recordsPromise, relayPromise, @@ -448,42 +408,42 @@ export class FormAutofillParent extends JSWindowActorParent { * * This is static as a unit test calls this. * - * @private * @param {object} data - * @param {string} data.collectionName - * The name used to specify which collection to retrieve records. * @param {string} data.searchString * The typed string for filtering out the matched records. - * @param {string} data.info - * The input autocomplete property's information. + * @param {string} data.collectionName + * The name used to specify which collection to retrieve records. + * @param {string} data.fieldName + * The field name to search. If not specified, return all records in + * the collection */ - static async _getRecords({ collectionName, searchString, info }) { - let collection = lazy.gFormAutofillStorage[collectionName]; + static async getRecords({ searchString, collectionName, fieldName }) { + // Derive the collection name from field name if it doesn't exist + collectionName ||= + FormAutofillUtils.getCollectionNameFromFieldName(fieldName); + + const collection = lazy.gFormAutofillStorage[collectionName]; if (!collection) { return []; } - let recordsInCollection = await collection.getAll(); - if (!info || !info.fieldName || !recordsInCollection.length) { - return recordsInCollection; + const records = await collection.getAll(); + if (!fieldName || !records.length) { + return records; } - let isCC = collectionName == CREDITCARDS_COLLECTION_NAME; // We don't filter "cc-number" - if (isCC && info.fieldName == "cc-number") { - recordsInCollection = recordsInCollection.filter( - record => !!record["cc-number"] - ); - return recordsInCollection; + if (collectionName == CREDITCARDS_COLLECTION_NAME) { + if (fieldName == "cc-number") { + return records.filter(record => !!record["cc-number"]); + } } - let records = []; - let lcSearchString = searchString.toLowerCase(); - - for (let record of recordsInCollection) { - let fieldValue = record[info.fieldName]; + const lcSearchString = searchString.toLowerCase(); + return records.filter(record => { + const fieldValue = record[fieldName]; if (!fieldValue) { - continue; + return false; } if ( @@ -493,19 +453,14 @@ export class FormAutofillParent extends JSWindowActorParent { ) { // Address autofill isn't supported for the record's country so we don't // want to attempt to potentially incorrectly fill the address fields. - continue; - } - - if ( - lcSearchString && - !String(fieldValue).toLowerCase().startsWith(lcSearchString) - ) { - continue; + return false; } - records.push(record); - } - return records; + return ( + !lcSearchString || + String(fieldValue).toLowerCase().startsWith(lcSearchString) + ); + }); } async _onAddressSubmit(address, browser) { diff --git a/toolkit/components/formautofill/Helpers.ios.mjs b/toolkit/components/formautofill/Helpers.ios.mjs index 56bb49f0e9..83137331f1 100644 --- a/toolkit/components/formautofill/Helpers.ios.mjs +++ b/toolkit/components/formautofill/Helpers.ios.mjs @@ -45,12 +45,6 @@ HTMLElement.prototype.getAutocompleteInfo = function () { }; }; -// Bug 1835024. Webkit doesn't support `checkVisibility` API -// https://drafts.csswg.org/cssom-view-1/#dom-element-checkvisibility -HTMLElement.prototype.checkVisibility = function (_options) { - throw new Error(`Not implemented: WebKit doesn't support checkVisibility `); -}; - // This function helps us debug better when an error occurs because a certain mock is missing const withNotImplementedError = obj => new Proxy(obj, { @@ -106,7 +100,7 @@ export const XPCOMUtils = withNotImplementedError({ prop, pref, defaultValue = null, - onUpdate = null, + onUpdate, transform = val => val ) => { if (!Object.keys(IOSAppConstants.prefs).includes(pref)) { diff --git a/toolkit/components/formautofill/ProfileAutoCompleteResult.sys.mjs b/toolkit/components/formautofill/ProfileAutoCompleteResult.sys.mjs index 52ed8bed03..68df30f8b5 100644 --- a/toolkit/components/formautofill/ProfileAutoCompleteResult.sys.mjs +++ b/toolkit/components/formautofill/ProfileAutoCompleteResult.sys.mjs @@ -131,6 +131,12 @@ class ProfileAutoCompleteResult { if (typeof label == "string") { return label; } + + let type = this.getTypeOfIndex(index); + if (type == "clear" || type == "manage") { + return label.primary; + } + return JSON.stringify(label); } @@ -141,6 +147,16 @@ class ProfileAutoCompleteResult { * @returns {string} The comment at the specified index */ getCommentAt(index) { + let type = this.getTypeOfIndex(index); + switch (type) { + case "clear": + return '{"fillMessageName": "FormAutofill:ClearForm"}'; + case "manage": + return '{"fillMessageName": "FormAutofill:OpenPreferences"}'; + case "insecure": + return '{"noLearnMore": true }'; + } + const item = this.getAt(index); return item.comment ?? JSON.stringify(this._matchingProfiles[index]); } @@ -157,14 +173,16 @@ class ProfileAutoCompleteResult { return itemStyle; } - if (index == this._popupLabels.length - 1) { - return "autofill-footer"; - } - if (this._isInputAutofilled) { - return "autofill-clear-button"; + switch (this.getTypeOfIndex(index)) { + case "manage": + return "action"; + case "clear": + return "action"; + case "insecure": + return "insecureWarning"; + default: + return "autofill"; } - - return "autofill-profile"; } /** @@ -205,6 +223,24 @@ class ProfileAutoCompleteResult { removeValueAt(_index) { // There is no plan to support removing profiles via autocomplete. } + + /** + * Returns a type string that identifies te type of row at the given index. + * + * @param {number} index The index of the result requested + * @returns {string} The type at the specified index + */ + getTypeOfIndex(index) { + if (this._isInputAutofilled && index == 0) { + return "clear"; + } + + if (index == this._popupLabels.length - 1) { + return "manage"; + } + + return "item"; + } } export class AddressResult extends ProfileAutoCompleteResult { @@ -281,18 +317,26 @@ export class AddressResult extends ProfileAutoCompleteResult { "autofill-manage-addresses-label" ); + let footerItem = { + primary: manageLabel, + secondary: "", + }; + if (this._isInputAutofilled) { - return [ - { primary: "", secondary: "" }, // Clear button - // Footer + const clearLabel = lazy.l10n.formatValueSync("autofill-clear-form-label"); + + let labels = [ { - primary: "", - secondary: "", - manageLabel, + primary: clearLabel, }, ]; + labels.push(footerItem); + return labels; } + let focusedCategory = + lazy.FormAutofillUtils.getCategoryFromFieldName(focusedFieldName); + // Skip results without a primary label. let labels = profiles .filter(profile => { @@ -306,35 +350,88 @@ export class AddressResult extends ProfileAutoCompleteResult { ) { primaryLabel = profile["-moz-street-address-one-line"]; } + + let profileFields = allFieldNames.filter( + fieldName => !!profile[fieldName] + ); + + let categories = + lazy.FormAutofillUtils.getCategoriesFromFieldNames(profileFields); + let status = this.getStatusNote(categories, focusedCategory); + let secondary = this._getSecondaryLabel( + focusedFieldName, + allFieldNames, + profile + ); + const ariaLabel = [primaryLabel, secondary, status] + .filter(chunk => !!chunk) // Exclude empty chunks. + .join(" "); return { primary: primaryLabel, - secondary: this._getSecondaryLabel( - focusedFieldName, - allFieldNames, - profile - ), + secondary, + status, + ariaLabel, }; }); - const focusedCategory = - lazy.FormAutofillUtils.getCategoryFromFieldName(focusedFieldName); + let allCategories = + lazy.FormAutofillUtils.getCategoriesFromFieldNames(allFieldNames); + + if (allCategories && allCategories.length) { + let statusItem = { + primary: "", + secondary: "", + status: this.getStatusNote(allCategories, focusedCategory), + style: "status", + }; + labels.push(statusItem); + } - // Add an empty result entry for footer. Its content will come from - // the footer binding, so don't assign any value to it. - // The additional properties: categories and focusedCategory are required of - // the popup to generate autofill hint on the footer. - labels.push({ - primary: "", - secondary: "", - manageLabel, - categories: lazy.FormAutofillUtils.getCategoriesFromFieldNames( - this._allFieldNames - ), - focusedCategory, - }); + labels.push(footerItem); return labels; } + + getStatusNote(categories, focusedCategory) { + if (!categories || !categories.length) { + return ""; + } + + // If the length of categories is 1, that means all the fillable fields are in the same + // category. We will change the way to inform user according to this flag. When the value + // is true, we show "Also autofills ...", otherwise, show "Autofills ..." only. + let hasExtraCategories = categories.length > 1; + // Show the categories in certain order to conform with the spec. + let orderedCategoryList = [ + "address", + "name", + "organization", + "tel", + "email", + ]; + let showCategories = hasExtraCategories + ? orderedCategoryList.filter( + category => + categories.includes(category) && category != focusedCategory + ) + : [orderedCategoryList.find(category => category == focusedCategory)]; + + let formatter = new Intl.ListFormat(undefined, { + style: "narrow", + }); + + let categoriesText = showCategories.map(category => + lazy.l10n.formatValueSync("autofill-category-" + category) + ); + categoriesText = formatter.format(categoriesText); + + let statusTextTmplKey = hasExtraCategories + ? "autofill-phishing-warningmessage-extracategory" + : "autofill-phishing-warningmessage"; + return lazy.l10n.formatValueSync(statusTextTmplKey, { + categories: categoriesText, + }); + } } export class CreditCardResult extends ProfileAutoCompleteResult { @@ -401,16 +498,20 @@ export class CreditCardResult extends ProfileAutoCompleteResult { "autofill-manage-payment-methods-label" ); + let footerItem = { + primary: manageLabel, + }; + if (this._isInputAutofilled) { - return [ - { primary: "", secondary: "" }, // Clear button - // Footer + const clearLabel = lazy.l10n.formatValueSync("autofill-clear-form-label"); + + let labels = [ { - primary: "", - secondary: "", - manageLabel, + primary: clearLabel, }, ]; + labels.push(footerItem); + return labels; } // Skip results without a primary label. @@ -446,37 +547,23 @@ export class CreditCardResult extends ProfileAutoCompleteResult { .filter(chunk => !!chunk) // Exclude empty chunks. .join(" "); return { - primary, - secondary, + primary: primary.toString().replaceAll("*", "•"), + secondary: secondary.toString().replaceAll("*", "•"), ariaLabel, image, }; }); - const focusedCategory = - lazy.FormAutofillUtils.getCategoryFromFieldName(focusedFieldName); - - // Add an empty result entry for footer. - labels.push({ - primary: "", - secondary: "", - manageLabel, - focusedCategory, - }); + labels.push(footerItem); return labels; } - getStyleAt(index) { - const itemStyle = this.getAt(index).style; - if (itemStyle) { - return itemStyle; - } - + getTypeOfIndex(index) { if (!this._isSecure) { - return "autofill-insecureWarning"; + return "insecure"; } - return super.getStyleAt(index); + return super.getTypeOfIndex(index); } } diff --git a/toolkit/components/formautofill/android/FormAutofillStorage.sys.mjs b/toolkit/components/formautofill/android/FormAutofillStorage.sys.mjs index 964be31d06..17a50de7eb 100644 --- a/toolkit/components/formautofill/android/FormAutofillStorage.sys.mjs +++ b/toolkit/components/formautofill/android/FormAutofillStorage.sys.mjs @@ -68,7 +68,7 @@ class Addresses extends AddressesBase { this._initializePromise = Promise.resolve(); } - async _saveRecord(record, { sourceSync = false } = {}) { + async _saveRecord(record) { lazy.GeckoViewAutocomplete.onAddressSave(lazy.Address.fromGecko(record)); } @@ -136,7 +136,7 @@ class CreditCards extends CreditCardsBase { this._initializePromise = Promise.resolve(); } - async _saveRecord(record, { sourceSync = false } = {}) { + async _saveRecord(record) { lazy.GeckoViewAutocomplete.onCreditCardSave( lazy.CreditCard.fromGecko(record) ); diff --git a/toolkit/components/formautofill/default/FormAutofillPrompter.sys.mjs b/toolkit/components/formautofill/default/FormAutofillPrompter.sys.mjs index f166716de5..05dcf5bace 100644 --- a/toolkit/components/formautofill/default/FormAutofillPrompter.sys.mjs +++ b/toolkit/components/formautofill/default/FormAutofillPrompter.sys.mjs @@ -789,16 +789,15 @@ export class AddressEditDoorhanger extends AutofillDoorhanger { input.setAttribute("id", inputId); - const value = this.newRecord[fieldName] ?? ""; if (popup) { - const menuitem = Array.from(popup.childNodes).find( - item => - item.label.toLowerCase() === value?.toLowerCase() || - item.value.toLowerCase() === value?.toLowerCase() - ); - input.selectedItem = menuitem; + input.selectedItem = + FormAutofillUtils.findAddressSelectOptionWithMenuPopup( + popup, + this.newRecord, + fieldName + ); } else { - input.value = value; + input.value = this.newRecord[fieldName] ?? ""; } div.appendChild(input); diff --git a/toolkit/components/formautofill/shared/AddressComponent.sys.mjs b/toolkit/components/formautofill/shared/AddressComponent.sys.mjs index 40e00b66a0..e83cd22251 100644 --- a/toolkit/components/formautofill/shared/AddressComponent.sys.mjs +++ b/toolkit/components/formautofill/shared/AddressComponent.sys.mjs @@ -412,7 +412,6 @@ class State extends AddressField { const options = { merge_whitespace: true, - remove_punctuation: true, }; this.#state = lazy.FormAutofillUtils.getAbbreviatedSubregionName( this.normalizeUserValue(options), @@ -991,7 +990,7 @@ export class AddressComparison { * country, postal code, etc. The class provides a compare methods * to compare another AddressComponent against the current instance. * - * Note. This class assumes records that pass to it have already been normalized. + * Note: This class assumes records that pass to it have already been normalized. */ export class AddressComponent { /** diff --git a/toolkit/components/formautofill/shared/FormAutofillSection.sys.mjs b/toolkit/components/formautofill/shared/FormAutofillSection.sys.mjs index 7bda4c167b..1a5b3014c9 100644 --- a/toolkit/components/formautofill/shared/FormAutofillSection.sys.mjs +++ b/toolkit/components/formautofill/shared/FormAutofillSection.sys.mjs @@ -136,15 +136,15 @@ export class FormAutofillSection { * specific case. Return the original value in the default case. * @param {String} value * The original field value. - * @param {Object} fieldDetail + * @param {Object} _fieldName * A fieldDetail of the related element. - * @param {HTMLElement} element + * @param {HTMLElement} _element * A element for checking converting value. * * @returns {String} * A string of the converted value. */ - computeFillingValue(value, fieldName, element) { + computeFillingValue(value, _fieldName, _element) { return value; } diff --git a/toolkit/components/formautofill/shared/FormAutofillUtils.sys.mjs b/toolkit/components/formautofill/shared/FormAutofillUtils.sys.mjs index e86f14975c..c2b48a53a3 100644 --- a/toolkit/components/formautofill/shared/FormAutofillUtils.sys.mjs +++ b/toolkit/components/formautofill/shared/FormAutofillUtils.sys.mjs @@ -176,6 +176,12 @@ FormAutofillUtils = { return Array.from(categories); }, + getCollectionNameFromFieldName(fieldName) { + return this.isCreditCardField(fieldName) + ? CREDITCARDS_COLLECTION_NAME + : ADDRESSES_COLLECTION_NAME; + }, + getAddressSeparator() { // The separator should be based on the L10N address format, and using a // white space is a temporary solution. @@ -799,6 +805,42 @@ FormAutofillUtils = { return null; }, + /** + * Find the option element from xul menu popups, as used in address capture + * doorhanger. + * + * This is a proxy to `findAddressSelectOption`, which expects HTML select + * DOM nodes and operates on options instead of xul menuitems. + * + * NOTE: This is a temporary solution until Bug 1886949 is landed. This + * method will then be removed `findAddressSelectOption` will be used + * directly. + * + * @param {XULPopupElement} menupopup + * @param {object} address + * @param {string} fieldName + * @returns {XULElement} + */ + findAddressSelectOptionWithMenuPopup(menupopup, address, fieldName) { + class MenuitemProxy { + constructor(menuitem) { + this.menuitem = menuitem; + } + get text() { + return this.menuitem.label; + } + get value() { + return this.menuitem.value; + } + } + const selectEl = { + options: Array.from(menupopup.childNodes).map( + menuitem => new MenuitemProxy(menuitem) + ), + }; + return this.findAddressSelectOption(selectEl, address, fieldName)?.menuitem; + }, + findCreditCardSelectOption(selectEl, creditCard, fieldName) { let oneDigitMonth = creditCard["cc-exp-month"] ? creditCard["cc-exp-month"].toString() diff --git a/toolkit/components/gfx/SanityTest.sys.mjs b/toolkit/components/gfx/SanityTest.sys.mjs index f519735a1a..6fe7c4e0a4 100644 --- a/toolkit/components/gfx/SanityTest.sys.mjs +++ b/toolkit/components/gfx/SanityTest.sys.mjs @@ -397,7 +397,7 @@ SanityTest.prototype = { return true; }, - observe(subject, topic, data) { + observe(subject, topic) { if (topic != "profile-after-change") { return; } diff --git a/toolkit/components/gfx/content/gfxFrameScript.js b/toolkit/components/gfx/content/gfxFrameScript.js index c423fb8a2d..0587f2fad7 100644 --- a/toolkit/components/gfx/content/gfxFrameScript.js +++ b/toolkit/components/gfx/content/gfxFrameScript.js @@ -45,7 +45,7 @@ const gfxFrameScript = { return aUri.endsWith("/sanitytest.html"); }, - onStateChange(webProgress, req, flags, status) { + onStateChange(webProgress, req, flags) { if ( webProgress.isTopLevel && flags & Ci.nsIWebProgressListener.STATE_STOP && diff --git a/toolkit/components/glean/Cargo.toml b/toolkit/components/glean/Cargo.toml index 98b8d8a95b..ba563f514a 100644 --- a/toolkit/components/glean/Cargo.toml +++ b/toolkit/components/glean/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" license = "MPL-2.0" [dependencies] -glean = "58.1.0" +glean = "59.0.0" log = "0.4" nserror = { path = "../../../xpcom/rust/nserror" } nsstring = { path = "../../../xpcom/rust/nsstring" } diff --git a/toolkit/components/glean/api/Cargo.toml b/toolkit/components/glean/api/Cargo.toml index dc688dc41e..af507f499f 100644 --- a/toolkit/components/glean/api/Cargo.toml +++ b/toolkit/components/glean/api/Cargo.toml @@ -9,7 +9,7 @@ license = "MPL-2.0" [dependencies] bincode = "1.0" chrono = "0.4.10" -glean = "58.1.0" +glean = "59.0.0" inherent = "1.0.0" log = "0.4" nsstring = { path = "../../../../xpcom/rust/nsstring", optional = true } diff --git a/toolkit/components/glean/api/src/common_test.rs b/toolkit/components/glean/api/src/common_test.rs index 3c8c2c71e1..46062736f2 100644 --- a/toolkit/components/glean/api/src/common_test.rs +++ b/toolkit/components/glean/api/src/common_test.rs @@ -43,6 +43,7 @@ fn setup_glean(tempdir: Option<tempfile::TempDir>) -> tempfile::TempDir { rate_limit: None, enable_event_timestamps: false, experimentation_id: None, + enable_internal_pings: true }; let client_info = glean::ClientInfoMetrics { diff --git a/toolkit/components/glean/api/src/ffi/custom_distribution.rs b/toolkit/components/glean/api/src/ffi/custom_distribution.rs index 643ebfbff5..6f369fd268 100644 --- a/toolkit/components/glean/api/src/ffi/custom_distribution.rs +++ b/toolkit/components/glean/api/src/ffi/custom_distribution.rs @@ -69,6 +69,26 @@ pub extern "C" fn fog_custom_distribution_accumulate_samples_signed( } #[no_mangle] +pub extern "C" fn fog_custom_distribution_accumulate_single_sample(id: u32, sample: u64) { + with_metric!( + CUSTOM_DISTRIBUTION_MAP, + id, + metric, + metric.accumulate_single_sample_signed(sample as i64) + ); +} + +#[no_mangle] +pub extern "C" fn fog_custom_distribution_accumulate_single_sample_signed(id: u32, sample: i64) { + with_metric!( + CUSTOM_DISTRIBUTION_MAP, + id, + metric, + metric.accumulate_single_sample_signed(sample) + ); +} + +#[no_mangle] pub extern "C" fn fog_custom_distribution_test_get_error( id: u32, diff --git a/toolkit/components/glean/api/src/ffi/event.rs b/toolkit/components/glean/api/src/ffi/event.rs index bd167021d6..5d1e2dc0f1 100644 --- a/toolkit/components/glean/api/src/ffi/event.rs +++ b/toolkit/components/glean/api/src/ffi/event.rs @@ -4,8 +4,6 @@ #![cfg(feature = "with_gecko")] -use std::collections::HashMap; - use nsstring::{nsACString, nsCString}; use thin_vec::ThinVec; @@ -59,7 +57,6 @@ pub extern "C" fn fog_event_record( Some(m) => m.record_raw(extra), None => panic!("No (dynamic) metric for event with id {}", id), } - return; } else { match metric_maps::record_event_by_id(id, extra) { Ok(()) => {} @@ -148,7 +145,7 @@ pub extern "C" fn fog_event_test_get_value( }; for event in events { - let extra = event.extra.unwrap_or_else(HashMap::new); + let extra = event.extra.unwrap_or_default(); let extra_len = extra.len(); let mut extras = ThinVec::with_capacity(extra_len * 2); for (k, v) in extra.into_iter() { diff --git a/toolkit/components/glean/api/src/private/custom_distribution.rs b/toolkit/components/glean/api/src/private/custom_distribution.rs index aeaf9b58c2..85121fca1e 100644 --- a/toolkit/components/glean/api/src/private/custom_distribution.rs +++ b/toolkit/components/glean/api/src/private/custom_distribution.rs @@ -92,8 +92,21 @@ impl CustomDistribution for CustomDistributionMetric { } } - pub fn accumulate_single_sample_signed(&self, _sample: i64) { - unimplemented!("bug 1884183: expose this to FOG") + pub fn accumulate_single_sample_signed(&self, sample: i64) { + match self { + CustomDistributionMetric::Parent { inner, .. } => { + inner.accumulate_single_sample(sample) + } + CustomDistributionMetric::Child(c) => { + with_ipc_payload(move |payload| { + if let Some(v) = payload.custom_samples.get_mut(&c.0) { + v.push(sample); + } else { + payload.custom_samples.insert(c.0, vec![sample]); + } + }); + } + } } pub fn test_get_value<'a, S: Into<Option<&'a str>>>( diff --git a/toolkit/components/glean/api/src/private/timing_distribution.rs b/toolkit/components/glean/api/src/private/timing_distribution.rs index 6707560e41..2807b87c4f 100644 --- a/toolkit/components/glean/api/src/private/timing_distribution.rs +++ b/toolkit/components/glean/api/src/private/timing_distribution.rs @@ -374,8 +374,16 @@ impl TimingDistribution for TimingDistributionMetric { } } - pub fn accumulate_single_sample(&self, _sample: i64) { - unimplemented!("bug 1884183: expose this to FOG") + pub fn accumulate_single_sample(&self, sample: i64) { + match self { + TimingDistributionMetric::Parent { id: _id, inner } => { + inner.accumulate_single_sample(sample) + } + TimingDistributionMetric::Child(_c) => { + // TODO: Instrument this error + log::error!("Can't record samples for a timing distribution from a child metric"); + } + } } /// **Exported for test purposes.** diff --git a/toolkit/components/glean/bindings/private/Boolean.cpp b/toolkit/components/glean/bindings/private/Boolean.cpp index 8300990b49..168179f569 100644 --- a/toolkit/components/glean/bindings/private/Boolean.cpp +++ b/toolkit/components/glean/bindings/private/Boolean.cpp @@ -21,7 +21,7 @@ void BooleanMetric::Set(bool aValue) const { if (scalarId) { Telemetry::ScalarSet(scalarId.extract(), aValue); } else if (IsSubmetricId(mId)) { - GetLabeledMirrorLock().apply([&](auto& lock) { + GetLabeledMirrorLock().apply([&](const auto& lock) { auto tuple = lock.ref()->MaybeGet(mId); if (tuple) { Telemetry::ScalarSet(std::get<0>(tuple.ref()), std::get<1>(tuple.ref()), diff --git a/toolkit/components/glean/bindings/private/Counter.cpp b/toolkit/components/glean/bindings/private/Counter.cpp index f7f70f29eb..4eeeb3de7a 100644 --- a/toolkit/components/glean/bindings/private/Counter.cpp +++ b/toolkit/components/glean/bindings/private/Counter.cpp @@ -22,7 +22,7 @@ void CounterMetric::Add(int32_t aAmount) const { if (scalarId) { Telemetry::ScalarAdd(scalarId.extract(), aAmount); } else if (IsSubmetricId(mId)) { - GetLabeledMirrorLock().apply([&](auto& lock) { + GetLabeledMirrorLock().apply([&](const auto& lock) { auto tuple = lock.ref()->MaybeGet(mId); if (tuple && aAmount > 0) { Telemetry::ScalarAdd(std::get<0>(tuple.ref()), diff --git a/toolkit/components/glean/bindings/private/CustomDistribution.cpp b/toolkit/components/glean/bindings/private/CustomDistribution.cpp index a5a821a558..88e96236db 100644 --- a/toolkit/components/glean/bindings/private/CustomDistribution.cpp +++ b/toolkit/components/glean/bindings/private/CustomDistribution.cpp @@ -33,6 +33,15 @@ void CustomDistributionMetric::AccumulateSamples( fog_custom_distribution_accumulate_samples(mId, &aSamples); } +void CustomDistributionMetric::AccumulateSingleSample(uint64_t aSample) const { + auto hgramId = HistogramIdForMetric(mId); + if (hgramId) { + auto id = hgramId.extract(); + Telemetry::Accumulate(id, aSample); + } + fog_custom_distribution_accumulate_single_sample(mId, aSample); +} + void CustomDistributionMetric::AccumulateSamplesSigned( const nsTArray<int64_t>& aSamples) const { auto hgramId = HistogramIdForMetric(mId); @@ -47,6 +56,16 @@ void CustomDistributionMetric::AccumulateSamplesSigned( fog_custom_distribution_accumulate_samples_signed(mId, &aSamples); } +void CustomDistributionMetric::AccumulateSingleSampleSigned( + int64_t aSample) const { + auto hgramId = HistogramIdForMetric(mId); + if (hgramId) { + auto id = hgramId.extract(); + Telemetry::Accumulate(id, aSample); + } + fog_custom_distribution_accumulate_single_sample_signed(mId, aSample); +} + Result<Maybe<DistributionData>, nsCString> CustomDistributionMetric::TestGetValue(const nsACString& aPingName) const { nsCString err; @@ -78,6 +97,10 @@ void GleanCustomDistribution::AccumulateSamples( mCustomDist.AccumulateSamplesSigned(aSamples); } +void GleanCustomDistribution::AccumulateSingleSample(const int64_t aSample) { + mCustomDist.AccumulateSingleSampleSigned(aSample); +} + void GleanCustomDistribution::TestGetValue( const nsACString& aPingName, dom::Nullable<dom::GleanDistributionData>& aRetval, ErrorResult& aRv) { diff --git a/toolkit/components/glean/bindings/private/CustomDistribution.h b/toolkit/components/glean/bindings/private/CustomDistribution.h index 8227b024ad..8074a0542e 100644 --- a/toolkit/components/glean/bindings/private/CustomDistribution.h +++ b/toolkit/components/glean/bindings/private/CustomDistribution.h @@ -35,6 +35,13 @@ class CustomDistributionMetric { void AccumulateSamples(const nsTArray<uint64_t>& aSamples) const; /** + * Accumulates the provided sample in the metric. + * + * @param aSamples The sample to be recorded by the metric. + */ + void AccumulateSingleSample(uint64_t aSample) const; + + /** * Accumulates the provided samples in the metric. * * @param aSamples The vector holding the samples to be recorded by the @@ -46,6 +53,14 @@ class CustomDistributionMetric { void AccumulateSamplesSigned(const nsTArray<int64_t>& aSamples) const; /** + * Accumulates the provided sample in the metric. + * + * @param aSamples The signed integer sample to be recorded by the + * metric. + */ + void AccumulateSingleSampleSigned(int64_t aSample) const; + + /** * **Test-only API** * * Gets the currently stored value as a DistributionData. @@ -80,6 +95,8 @@ class GleanCustomDistribution final : public GleanMetric { void AccumulateSamples(const dom::Sequence<int64_t>& aSamples); + void AccumulateSingleSample(const int64_t aSample); + void TestGetValue(const nsACString& aPingName, dom::Nullable<dom::GleanDistributionData>& aRetval, ErrorResult& aRv); diff --git a/toolkit/components/glean/bindings/private/DistributionData.h b/toolkit/components/glean/bindings/private/DistributionData.h index fb9bba720e..782fe17c98 100644 --- a/toolkit/components/glean/bindings/private/DistributionData.h +++ b/toolkit/components/glean/bindings/private/DistributionData.h @@ -27,6 +27,28 @@ struct DistributionData final { this->values.InsertOrUpdate(aBuckets[i], aCounts[i]); } } + + friend std::ostream& operator<<(std::ostream& aStream, + const DistributionData& aDist) { + aStream << "DistributionData("; + aStream << "sum=" << aDist.sum << ", "; + aStream << "count=" << aDist.count << ", "; + aStream << "values={"; + bool first = true; + for (const auto& entry : aDist.values) { + if (!first) { + aStream << ", "; + } + first = false; + + const uint64_t bucket = entry.GetKey(); + const uint64_t count = entry.GetData(); + aStream << bucket << "=" << count; + } + aStream << "}"; + aStream << ")"; + return aStream; + } }; } // namespace mozilla::glean diff --git a/toolkit/components/glean/bindings/private/Labeled.cpp b/toolkit/components/glean/bindings/private/Labeled.cpp index 23527708e0..1af9b870ae 100644 --- a/toolkit/components/glean/bindings/private/Labeled.cpp +++ b/toolkit/components/glean/bindings/private/Labeled.cpp @@ -31,7 +31,7 @@ already_AddRefed<GleanMetric> GleanLabeled::NamedGetter(const nsAString& aName, auto mirrorId = ScalarIdForMetric(mId); if (mirrorId) { - GetLabeledMirrorLock().apply([&](auto& lock) { + GetLabeledMirrorLock().apply([&](const auto& lock) { auto tuple = std::make_tuple<Telemetry::ScalarID, nsString>( mirrorId.extract(), nsString(aName)); lock.ref()->InsertOrUpdate(submetricId, std::move(tuple)); diff --git a/toolkit/components/glean/bindings/private/Labeled.h b/toolkit/components/glean/bindings/private/Labeled.h index 65e31bd2bd..0e3aafba05 100644 --- a/toolkit/components/glean/bindings/private/Labeled.h +++ b/toolkit/components/glean/bindings/private/Labeled.h @@ -60,7 +60,7 @@ class Labeled { static inline void UpdateLabeledMirror(Telemetry::ScalarID aMirrorId, uint32_t aSubmetricId, const nsACString& aLabel) { - GetLabeledMirrorLock().apply([&](auto& lock) { + GetLabeledMirrorLock().apply([&](const auto& lock) { auto tuple = std::make_tuple<Telemetry::ScalarID, nsString>( std::move(aMirrorId), NS_ConvertUTF8toUTF16(aLabel)); lock.ref()->InsertOrUpdate(aSubmetricId, std::move(tuple)); diff --git a/toolkit/components/glean/bindings/private/Ping.cpp b/toolkit/components/glean/bindings/private/Ping.cpp index 19f4fb5f77..8dbb316128 100644 --- a/toolkit/components/glean/bindings/private/Ping.cpp +++ b/toolkit/components/glean/bindings/private/Ping.cpp @@ -42,7 +42,7 @@ void Ping::Submit(const nsACString& aReason) const { { auto callback = Maybe<PingTestCallback>(); GetCallbackMapLock().apply( - [&](auto& lock) { callback = lock.ref()->Extract(mId); }); + [&](const auto& lock) { callback = lock.ref()->Extract(mId); }); // Calling the callback outside of the lock allows it to register a new // callback itself. if (callback) { @@ -55,7 +55,7 @@ void Ping::Submit(const nsACString& aReason) const { void Ping::TestBeforeNextSubmit(PingTestCallback&& aCallback) const { { GetCallbackMapLock().apply( - [&](auto& lock) { lock.ref()->InsertOrUpdate(mId, aCallback); }); + [&](const auto& lock) { lock.ref()->InsertOrUpdate(mId, aCallback); }); } } diff --git a/toolkit/components/glean/bindings/private/Timespan.cpp b/toolkit/components/glean/bindings/private/Timespan.cpp index 2ab1f0dbba..7f154152eb 100644 --- a/toolkit/components/glean/bindings/private/Timespan.cpp +++ b/toolkit/components/glean/bindings/private/Timespan.cpp @@ -93,7 +93,7 @@ void TimespanMetric::Start() const { auto optScalarId = ScalarIdForMetric(mId); if (optScalarId) { auto scalarId = optScalarId.extract(); - GetTimesToStartsLock().apply([&](auto& lock) { + GetTimesToStartsLock().apply([&](const auto& lock) { (void)NS_WARN_IF(lock.ref()->Remove(scalarId)); lock.ref()->InsertOrUpdate(scalarId, TimeStamp::Now()); }); @@ -105,7 +105,7 @@ void TimespanMetric::Stop() const { auto optScalarId = ScalarIdForMetric(mId); if (optScalarId) { auto scalarId = optScalarId.extract(); - GetTimesToStartsLock().apply([&](auto& lock) { + GetTimesToStartsLock().apply([&](const auto& lock) { auto optStart = lock.ref()->Extract(scalarId); if (!NS_WARN_IF(!optStart)) { double delta = (TimeStamp::Now() - optStart.extract()).ToMilliseconds(); @@ -127,7 +127,7 @@ void TimespanMetric::Cancel() const { if (optScalarId) { auto scalarId = optScalarId.extract(); GetTimesToStartsLock().apply( - [&](auto& lock) { lock.ref()->Remove(scalarId); }); + [&](const auto& lock) { lock.ref()->Remove(scalarId); }); } fog_timespan_cancel(mId); } diff --git a/toolkit/components/glean/bindings/private/TimingDistribution.cpp b/toolkit/components/glean/bindings/private/TimingDistribution.cpp index 036db5f9db..7273d3fd2f 100644 --- a/toolkit/components/glean/bindings/private/TimingDistribution.cpp +++ b/toolkit/components/glean/bindings/private/TimingDistribution.cpp @@ -106,7 +106,7 @@ extern "C" NS_EXPORT void GIFFT_TimingDistributionStart( uint32_t aMetricId, mozilla::glean::TimerId aTimerId) { auto mirrorId = mozilla::glean::HistogramIdForMetric(aMetricId); if (mirrorId) { - mozilla::glean::GetTimerIdToStartsLock().apply([&](auto& lock) { + mozilla::glean::GetTimerIdToStartsLock().apply([&](const auto& lock) { auto tuple = mozilla::glean::MetricTimerTuple{aMetricId, aTimerId}; // It should be all but impossible for anyone to have already inserted // this timer for this metric given the monotonicity of timer ids. @@ -121,7 +121,7 @@ extern "C" NS_EXPORT void GIFFT_TimingDistributionStopAndAccumulate( uint32_t aMetricId, mozilla::glean::TimerId aTimerId) { auto mirrorId = mozilla::glean::HistogramIdForMetric(aMetricId); if (mirrorId) { - mozilla::glean::GetTimerIdToStartsLock().apply([&](auto& lock) { + mozilla::glean::GetTimerIdToStartsLock().apply([&](const auto& lock) { auto tuple = mozilla::glean::MetricTimerTuple{aMetricId, aTimerId}; auto optStart = lock.ref()->Extract(tuple); // The timer might not be in the map to be removed if it's already been @@ -147,7 +147,7 @@ extern "C" NS_EXPORT void GIFFT_TimingDistributionCancel( uint32_t aMetricId, mozilla::glean::TimerId aTimerId) { auto mirrorId = mozilla::glean::HistogramIdForMetric(aMetricId); if (mirrorId) { - mozilla::glean::GetTimerIdToStartsLock().apply([&](auto& lock) { + mozilla::glean::GetTimerIdToStartsLock().apply([&](const auto& lock) { // The timer might not be in the map to be removed if it's already been // cancelled or stop_and_accumulate'd. auto tuple = mozilla::glean::MetricTimerTuple{aMetricId, aTimerId}; @@ -172,8 +172,20 @@ void TimingDistributionMetric::StopAndAccumulate(const TimerId&& aId) const { // type. void TimingDistributionMetric::AccumulateRawDuration( const TimeDuration& aDuration) const { + // `* 1000.0` is an acceptable overflow risk as durations are unlikely to be + // on the order of (-)10^282 years. + double durationNs = aDuration.ToMicroseconds() * 1000.0; + double roundedDurationNs = std::round(durationNs); + if (MOZ_UNLIKELY( + roundedDurationNs < + static_cast<double>(std::numeric_limits<uint64_t>::min()) || + roundedDurationNs > + static_cast<double>(std::numeric_limits<uint64_t>::max()))) { + // TODO(bug 1691073): Instrument this error. + return; + } fog_timing_distribution_accumulate_raw_nanos( - mId, uint64_t(aDuration.ToMicroseconds() * 1000.00)); + mId, static_cast<uint64_t>(roundedDurationNs)); } void TimingDistributionMetric::Cancel(const TimerId&& aId) const { diff --git a/toolkit/components/glean/build_scripts/mach_commands.py b/toolkit/components/glean/build_scripts/mach_commands.py index 4a0f6dbc68..b8d270c088 100644 --- a/toolkit/components/glean/build_scripts/mach_commands.py +++ b/toolkit/components/glean/build_scripts/mach_commands.py @@ -167,9 +167,18 @@ def update_glean(command_context, version): topsrcdir = Path(command_context.topsrcdir) replace_in_file_or_die( - topsrcdir / "build.gradle", - r'gleanVersion = "[0-9.]+"', - f'gleanVersion = "{version}"', + topsrcdir + / "mobile" + / "android" + / "android-components" + / "plugins" + / "dependencies" + / "src" + / "main" + / "java" + / "DependenciesPlugin.kt", + r'mozilla_glean = "[0-9.]+"', + f'mozilla_glean = "{version}"', ) replace_in_file_or_die( topsrcdir / "toolkit" / "components" / "glean" / "Cargo.toml", @@ -226,3 +235,47 @@ def update_glean(command_context, version): """ print(textwrap.dedent(instructions)) + + +@Command( + "event-into-legacy", + category="misc", + description="Create a Legacy Telemetry compatible event definition from an existing Glean Event metric.", +) +@CommandArgument( + "--append", + "-a", + action="store_true", + help="Append to toolkit/components/telemetry/Events.yaml (note: verify and make any necessary modifications before landing).", +) +@CommandArgument("event", default=None, nargs="?", type=str, help="Event name.") +def event_into_legacy(command_context, event=None, append=False): + # Get the metrics_index's list of metrics indices + # by loading the index as a module. + import sys + from os import path + + sys.path.append(path.join(path.dirname(__file__), path.pardir)) + + from metrics_index import metrics_yamls + + sys.path.append(path.dirname(__file__)) + + from pathlib import Path + + from translate_events import translate_event + + legacy_yaml_path = path.join( + Path(command_context.topsrcdir), + "toolkit", + "components", + "telemetry", + "Events.yaml", + ) + + return translate_event( + event, + append, + [Path(command_context.topsrcdir) / x for x in metrics_yamls], + legacy_yaml_path, + ) diff --git a/toolkit/components/glean/build_scripts/translate_events.py b/toolkit/components/glean/build_scripts/translate_events.py new file mode 100644 index 0000000000..5936a67132 --- /dev/null +++ b/toolkit/components/glean/build_scripts/translate_events.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- + +# 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/. + +""" +Create a Legacy Telemetry event definition for the provided, named Glean event metric. +""" + +import re +import sys +from os import path +from pathlib import Path +from typing import Sequence + +import yaml +from glean_parser import parser, util + +TC_ROOT_PATH = path.abspath(path.join(path.dirname(__file__), path.pardir, path.pardir)) +sys.path.append(TC_ROOT_PATH) +# The parsers live in a subdirectory of "build_scripts", account for that. +# NOTE: if the parsers are moved, this logic will need to be updated. +sys.path.append(path.join(TC_ROOT_PATH, "telemetry", "build_scripts")) + +from mozparsers.parse_events import convert_to_cpp_identifier # noqa: E402 + +bug_number_pattern = re.compile(r"\d+") + + +class IndentingDumper(yaml.Dumper): + def increase_indent(self, flow=False, indentless=False): + return super(IndentingDumper, self).increase_indent(flow, False) + + +def get_bug_number_from_url(url: str) -> int: + bug = bug_number_pattern.search(url) + # Python lacks a safe cast, so we will return 1 if we fail to bubble up. + if bug is not None: + try: + bug = int(bug[0]) + except TypeError: + print(f"Failed to parse {bug[0]} to an integer") + return 1 + return bug + print(f"Failed to find a valid bug in the url {url}") + return 1 + + +def create_legacy_mirror_def(category_name: str, metric_name: str, event_objects: list): + event_cpp_enum = convert_to_cpp_identifier(category_name, "_") + "_" + event_cpp_enum += convert_to_cpp_identifier(metric_name, ".") + "_" + event_cpp_enum += convert_to_cpp_identifier(event_objects[0], ".") + + print( + f"""The Glean event {category_name}.{metric_name} has generated the {event_cpp_enum} Legacy Telemetry event. To link Glean to Legacy, please include in {category_name}.{metric_name}'s definition the following property: +telemetry_mirror: {event_cpp_enum} +See https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/gifft.html#the-telemetry-mirror-property-in-metrics-yaml +for more information. +""" + ) + + +def translate_event( + event_category_and_name: str, + append: bool, + metrics_files: Sequence[Path], + legacy_yaml_file: Path, +) -> int: + """ + Commandline helper for translating Glean events to Legacy. + + :param event_name: the event to look for + :param metrics_files: List of Path objects to load metrics from. + :return: Non-zero if there were any errors. + """ + + # Event objects are a Legacy Telemetry event field that is rarely used. We + # always broadcast into a single pre-defined object. + event_objects = ["events"] + + if event_category_and_name is not None: + *event_category, event_name = event_category_and_name.rsplit(".", maxsplit=1) + else: + print("Please provide an event (in category.metricName format) to translate.") + return 1 + + if len(event_category) > 0: + event_category = util.snake_case(event_category[0]) + event_name = util.snake_case(event_name) + else: + print( + f"Your event '{event_category_and_name}' did not conform to the a.category.a_metric_name format." + ) + return 1 + + metrics_files = util.ensure_list(metrics_files) + + # Accept any value of expires. + parser_options = { + "allow_reserved": True, + "custom_is_expired": lambda expires: False, + "custom_validate_expires": lambda expires: True, + } + all_objects = parser.parse_objects(metrics_files, parser_options) + + if util.report_validation_errors(all_objects): + return 1 + + for category_name, metrics in all_objects.value.items(): + for metric in metrics.values(): + metric_name = util.snake_case(metric.name) + category_name = util.snake_case(category_name) + + if metric_name != event_name or category_name != event_category: + continue + + if metric.type != "event": + print( + f"Metric {event_category_and_name} was found, but was a {metric.type} metric, not an event metric." + ) + return 1 + + bugs_list = [get_bug_number_from_url(m) for m in metric.bugs] + # Bail out if there was a parse error (error printed in get_bug_number_from_url) + if 1 in bugs_list: + return 1 + + metric_dict = { + "objects": event_objects, + "description": str(metric.description).strip(), + "bug_numbers": bugs_list, + "notification_emails": metric.notification_emails, + "expiry_version": str(metric.expires), + "products": ["firefox"], + "record_in_processes": ["all"], + } + + if len(metric.extra_keys.items()) > 0: + # Convert extra keys into a nested dictionary that YAML can understand + extra_keys = {} + for extra_key_pair in metric.extra_keys.items(): + description = ( + extra_key_pair[1]["description"].replace("\n", " ").strip() + ) + extra_keys[extra_key_pair[0]] = description + + metric_dict["extra_keys"] = extra_keys + + metric_dict = {metric_name: metric_dict} + metric_dict = {category_name: metric_dict} + + metric_yaml = yaml.dump( + metric_dict, + default_flow_style=False, + width=78, + indent=2, + Dumper=IndentingDumper, + ) + + if append: + with open(legacy_yaml_file, "a") as file: + print("", file=file) + print(metric_yaml, file=file) + print( + f"Apended {event_category_and_name} to the Legacy Events.yaml file. Please confirm the details by opening" + ) + print(legacy_yaml_file) + print("and checking that all fields are correct.") + + create_legacy_mirror_def(event_name, category_name, event_objects) + return 0 + + print(metric_yaml) + create_legacy_mirror_def(event_name, category_name, event_objects) + + return 0 diff --git a/toolkit/components/glean/metrics_index.py b/toolkit/components/glean/metrics_index.py index 8e20658810..28759a15fa 100644 --- a/toolkit/components/glean/metrics_index.py +++ b/toolkit/components/glean/metrics_index.py @@ -18,6 +18,8 @@ gecko_metrics = [ "browser/base/content/metrics.yaml", "docshell/base/metrics.yaml", "dom/base/use_counter_metrics.yaml", + "dom/media/eme/metrics.yaml", + "dom/media/hls/metrics.yaml", "dom/media/metrics.yaml", "dom/media/webrtc/metrics.yaml", "dom/metrics.yaml", @@ -31,6 +33,7 @@ gecko_metrics = [ "mobile/android/modules/geckoview/metrics.yaml", "netwerk/metrics.yaml", "netwerk/protocol/http/metrics.yaml", + "security/certverifier/metrics.yaml", "security/manager/ssl/metrics.yaml", "toolkit/components/cookiebanners/metrics.yaml", "toolkit/components/extensions/metrics.yaml", diff --git a/toolkit/components/glean/src/init/mod.rs b/toolkit/components/glean/src/init/mod.rs index f430cd7384..2c938899c0 100644 --- a/toolkit/components/glean/src/init/mod.rs +++ b/toolkit/components/glean/src/init/mod.rs @@ -38,6 +38,7 @@ use viaduct_uploader::ViaductUploader; pub extern "C" fn fog_init( data_path_override: &nsACString, app_id_override: &nsACString, + disable_internal_pings: bool, ) -> nsresult { let upload_enabled = static_prefs::pref!("datareporting.healthreport.uploadEnabled"); let recording_enabled = static_prefs::pref!("telemetry.fog.test.localhost_port") < 0; @@ -48,6 +49,9 @@ pub extern "C" fn fog_init( app_id_override, upload_enabled || recording_enabled, uploader, + // Flipping it around, because no value = defaults to false, + // so we take in `disable` but pass on `enable`. + !disable_internal_pings, ) .into() } @@ -64,6 +68,7 @@ pub extern "C" fn fog_init( pub extern "C" fn fog_init( data_path_override: &nsACString, app_id_override: &nsACString, + disable_internal_pings: bool, ) -> nsresult { // On Android always enable Glean upload. let upload_enabled = true; @@ -75,6 +80,7 @@ pub extern "C" fn fog_init( app_id_override, upload_enabled, uploader, + !disable_internal_pings, ) .into() } @@ -84,6 +90,7 @@ fn fog_init_internal( app_id_override: &nsACString, upload_enabled: bool, uploader: Option<Box<dyn glean::net::PingUploader>>, + enable_internal_pings: bool, ) -> Result<(), nsresult> { metrics::fog::initialization.start(); @@ -95,6 +102,7 @@ fn fog_init_internal( conf.upload_enabled = upload_enabled; conf.uploader = uploader; + conf.enable_internal_pings = enable_internal_pings; // If we're operating in automation without any specific source tags to set, // set the tag "automation" so any pings that escape don't clutter the tables. @@ -155,16 +163,12 @@ fn build_configuration( extern "C" { fn FOG_MaxPingLimit() -> u32; - fn FOG_EventTimestampsEnabled() -> bool; } // SAFETY NOTE: Safe because it returns a primitive by value. let pings_per_interval = unsafe { FOG_MaxPingLimit() }; metrics::fog::max_pings_per_minute.set(pings_per_interval.into()); - // SAFETY NOTE: Safe because it returns a primitive by value. - let enable_event_timestamps = unsafe { FOG_EventTimestampsEnabled() }; - let rate_limit = Some(glean::PingRateLimit { seconds_per_interval: 60, pings_per_interval, @@ -182,8 +186,9 @@ fn build_configuration( trim_data_to_registered_pings: true, log_level: None, rate_limit, - enable_event_timestamps, + enable_event_timestamps: true, experimentation_id: None, + enable_internal_pings: true, }; Ok((configuration, client_info)) diff --git a/toolkit/components/glean/tests/gtest/TestFog.cpp b/toolkit/components/glean/tests/gtest/TestFog.cpp index 0c1621911e..e6f4f7a2c2 100644 --- a/toolkit/components/glean/tests/gtest/TestFog.cpp +++ b/toolkit/components/glean/tests/gtest/TestFog.cpp @@ -11,6 +11,7 @@ #include "mozilla/Maybe.h" #include "mozilla/Result.h" #include "mozilla/ResultVariant.h" +#include "mozilla/TimeStamp.h" #include "nsTArray.h" @@ -20,6 +21,7 @@ #include "prtime.h" using mozilla::Preferences; +using mozilla::TimeDuration; using namespace mozilla::glean; using namespace mozilla::glean::impl; @@ -277,6 +279,15 @@ TEST_F(FOGFixture, TestCppTimingDistWorks) { ASSERT_EQ(sampleCount, (uint64_t)2); } +TEST_F(FOGFixture, TestCppTimingDistNegativeDuration) { + // Intentionally a negative duration to test the error case. + auto negDuration = TimeDuration::FromSeconds(-1); + test_only::what_time_is_it.AccumulateRawDuration(negDuration); + + ASSERT_EQ(mozilla::Nothing(), + test_only::what_time_is_it.TestGetValue().unwrap()); +} + TEST_F(FOGFixture, TestLabeledBooleanWorks) { ASSERT_EQ(mozilla::Nothing(), test_only::mabels_like_balloons.Get("hot_air"_ns) diff --git a/toolkit/components/glean/xpcom/FOG.cpp b/toolkit/components/glean/xpcom/FOG.cpp index d4c03a5b9e..955f2511b6 100644 --- a/toolkit/components/glean/xpcom/FOG.cpp +++ b/toolkit/components/glean/xpcom/FOG.cpp @@ -95,7 +95,7 @@ already_AddRefed<FOG> FOG::GetSingleton() { glean::fog::inits_during_shutdown.Add(1); // It's enough to call init before shutting down. // We don't need to (and can't) wait for it to complete. - glean::impl::fog_init(&VoidCString(), &VoidCString()); + glean::impl::fog_init(&VoidCString(), &VoidCString(), false); } gFOG->Shutdown(); gFOG = nullptr; @@ -123,19 +123,13 @@ extern "C" uint32_t FOG_MaxPingLimit(void) { "gleanMaxPingsPerMinute"_ns, 15); } -// This allows us to pass whether to enable precise event timestamps to Rust. -// Default is false. -extern "C" bool FOG_EventTimestampsEnabled(void) { - return NimbusFeatures::GetBool("gleanInternalSdk"_ns, - "enableEventTimestamps"_ns, false); -} - // Called when knowing if we're in automation is necessary. extern "C" bool FOG_IPCIsInAutomation(void) { return xpc::IsInAutomation(); } NS_IMETHODIMP FOG::InitializeFOG(const nsACString& aDataPathOverride, - const nsACString& aAppIdOverride) { + const nsACString& aAppIdOverride, + const bool aDisableInternalPings) { MOZ_ASSERT(XRE_IsParentProcess()); gInitializeCalled = true; RunOnShutdown( @@ -147,7 +141,8 @@ FOG::InitializeFOG(const nsACString& aDataPathOverride, }, ShutdownPhase::AppShutdownConfirmed); - return glean::impl::fog_init(&aDataPathOverride, &aAppIdOverride); + return glean::impl::fog_init(&aDataPathOverride, &aAppIdOverride, + aDisableInternalPings); } NS_IMETHODIMP diff --git a/toolkit/components/glean/xpcom/nsIFOG.idl b/toolkit/components/glean/xpcom/nsIFOG.idl index dd5d0ec21a..67041d6228 100644 --- a/toolkit/components/glean/xpcom/nsIFOG.idl +++ b/toolkit/components/glean/xpcom/nsIFOG.idl @@ -17,8 +17,10 @@ interface nsIFOG : nsISupports * instead of the profile dir. * @param aAppIdOverride - The application_id to use instead of * "firefox.desktop". + * @param aDisableInternalPings - Whether to disable internal pings (baseline, events, metrics). + * Default: false. */ - void initializeFOG([optional] in AUTF8String aDataPathOverride, [optional] in AUTF8String aAppIdOverride); + void initializeFOG([optional] in AUTF8String aDataPathOverride, [optional] in AUTF8String aAppIdOverride, [optional] in boolean aDisableInternalPings); /** * Register custom pings. diff --git a/toolkit/components/httpsonlyerror/tests/browser/browser_exception.js b/toolkit/components/httpsonlyerror/tests/browser/browser_exception.js index 7cf98b467f..0c2bd4fff5 100644 --- a/toolkit/components/httpsonlyerror/tests/browser/browser_exception.js +++ b/toolkit/components/httpsonlyerror/tests/browser/browser_exception.js @@ -139,7 +139,7 @@ function setupFileServer() { "GET", `${SECURE_ROOT_PATH}file_upgrade_insecure_server.sjs?queryresult=${INSECURE_ROOT_PATH}` ); - xhrRequest.onload = function (e) { + xhrRequest.onload = function () { var results = xhrRequest.responseText.split(","); resolve(results); }; diff --git a/toolkit/components/kvstore/nsIKeyValue.idl b/toolkit/components/kvstore/nsIKeyValue.idl index 08cd548af2..3d890eaa96 100644 --- a/toolkit/components/kvstore/nsIKeyValue.idl +++ b/toolkit/components/kvstore/nsIKeyValue.idl @@ -174,7 +174,7 @@ interface nsIKeyValuePair : nsISupports { */ [scriptable, builtinclass, rust_sync, uuid(b9ba7116-b7ff-4717-9a28-a08e6879b199)] interface nsIKeyValueEnumerator : nsISupports { - bool hasMoreElements(); + boolean hasMoreElements(); nsIKeyValuePair getNext(); }; diff --git a/toolkit/components/mediasniffer/test/unit/test_mediasniffer.js b/toolkit/components/mediasniffer/test/unit/test_mediasniffer.js index 12aa56a855..dc92c90a45 100644 --- a/toolkit/components/mediasniffer/test/unit/test_mediasniffer.js +++ b/toolkit/components/mediasniffer/test/unit/test_mediasniffer.js @@ -71,7 +71,7 @@ var listener = { ); }, - onDataAvailable(request, stream, offset, count) { + onDataAvailable(request, stream) { try { var bis = Cc["@mozilla.org/binaryinputstream;1"].createInstance( Ci.nsIBinaryInputStream @@ -83,7 +83,7 @@ var listener = { } }, - onStopRequest(request, status) { + onStopRequest() { testRan++; runNext(); }, diff --git a/toolkit/components/mediasniffer/test/unit/test_mediasniffer_ext.js b/toolkit/components/mediasniffer/test/unit/test_mediasniffer_ext.js index f48ffbb75a..5b6b400ed1 100644 --- a/toolkit/components/mediasniffer/test/unit/test_mediasniffer_ext.js +++ b/toolkit/components/mediasniffer/test/unit/test_mediasniffer_ext.js @@ -61,7 +61,7 @@ var listener = { ); }, - onDataAvailable(request, stream, offset, count) { + onDataAvailable(request, stream) { try { var bis = Cc["@mozilla.org/binaryinputstream;1"].createInstance( Ci.nsIBinaryInputStream @@ -73,7 +73,7 @@ var listener = { } }, - onStopRequest(request, status) { + onStopRequest() { testRan++; runNext(); }, diff --git a/toolkit/components/messaging-system/lib/SpecialMessageActions.sys.mjs b/toolkit/components/messaging-system/lib/SpecialMessageActions.sys.mjs index 8ed488ff88..c1a229e9c3 100644 --- a/toolkit/components/messaging-system/lib/SpecialMessageActions.sys.mjs +++ b/toolkit/components/messaging-system/lib/SpecialMessageActions.sys.mjs @@ -198,6 +198,8 @@ export const SpecialMessageActions = { "browser.firefox-view.feature-tour", "browser.pdfjs.feature-tour", "browser.newtab.feature-tour", + "browser.newtabpage.activity-stream.newtabWallpapers.wallpaper-light", + "browser.newtabpage.activity-stream.newtabWallpapers.wallpaper-dark", "cookiebanners.service.mode", "cookiebanners.service.mode.privateBrowsing", "cookiebanners.service.detectOnly", @@ -291,7 +293,7 @@ export const SpecialMessageActions = { Ci.nsISupportsWeakReference, ]), - observe(aSubject, aTopic, aData) { + observe() { let state = lazy.UIState.get(); if (state.status === lazy.UIState.STATUS_SIGNED_IN) { // We completed sign-in, so tear down our listener / observer and resolve diff --git a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_open_protection_panel.js b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_open_protection_panel.js index c9522426a2..bddc456539 100644 --- a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_open_protection_panel.js +++ b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_open_protection_panel.js @@ -4,7 +4,7 @@ "use strict"; add_task(async function test_OPEN_PROTECTION_PANEL() { - await BrowserTestUtils.withNewTab(EXAMPLE_URL, async browser => { + await BrowserTestUtils.withNewTab(EXAMPLE_URL, async () => { const popupshown = BrowserTestUtils.waitForEvent( window, "popupshown", diff --git a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_pin_current_tab.js b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_pin_current_tab.js index 4425325526..2cb0e3000f 100644 --- a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_pin_current_tab.js +++ b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_pin_current_tab.js @@ -4,7 +4,7 @@ "use strict"; add_task(async function test_PIN_CURRENT_TAB() { - await BrowserTestUtils.withNewTab("about:blank", async browser => { + await BrowserTestUtils.withNewTab("about:blank", async () => { await SMATestUtils.executeAndValidateAction({ type: "PIN_CURRENT_TAB" }); ok(gBrowser.selectedTab.pinned, "should pin current tab"); diff --git a/toolkit/components/messaging-system/schemas/TriggerActionSchemas/TriggerActionSchemas.json b/toolkit/components/messaging-system/schemas/TriggerActionSchemas/TriggerActionSchemas.json index 6a7d2328d7..773b84aa4a 100644 --- a/toolkit/components/messaging-system/schemas/TriggerActionSchemas/TriggerActionSchemas.json +++ b/toolkit/components/messaging-system/schemas/TriggerActionSchemas/TriggerActionSchemas.json @@ -169,6 +169,18 @@ "properties": { "id": { "type": "string", + "enum": ["deeplinkedToWindowsSettingsUI"] + } + }, + "additionalProperties": false, + "required": ["id"], + "description": "Occurs when user indicates they want to set Firefox to the default browser and they need to interact with Windows Settings to do so." + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", "enum": ["captivePortalLogin"] } }, diff --git a/toolkit/components/messaging-system/schemas/TriggerActionSchemas/index.md b/toolkit/components/messaging-system/schemas/TriggerActionSchemas/index.md index 85be613392..45df09249a 100644 --- a/toolkit/components/messaging-system/schemas/TriggerActionSchemas/index.md +++ b/toolkit/components/messaging-system/schemas/TriggerActionSchemas/index.md @@ -44,6 +44,7 @@ let patterns: string[]; * [formAutofill](#formautofill) * [contentBlocking](#contentblocking) * [defaultBrowserCheck](#defaultbrowsercheck) +* [deeplinkedToWindowsSettingsUI](#deeplinkedtowindowssettingsui) * [captivePortalLogin](#captiveportallogin) * [preferenceObserver](#preferenceobserver) * [featureCalloutCheck](#featurecalloutcheck) @@ -161,6 +162,12 @@ let willShowDefaultPrompt = boolean | undefined; } ``` +### `deeplinkedToWindowsSettingsUI` + +Triggers when the user has indicated they want to set Firefox as the default web +browser and interaction with Windows Settings is necessary to finish setting +Firefox as default. + ### `captivePortalLogin` Happens when the user successfully goes through a captive portal authentication flow. diff --git a/toolkit/components/ml/actors/MLEngineParent.sys.mjs b/toolkit/components/ml/actors/MLEngineParent.sys.mjs index 10b4eed4fa..05203e5f69 100644 --- a/toolkit/components/ml/actors/MLEngineParent.sys.mjs +++ b/toolkit/components/ml/actors/MLEngineParent.sys.mjs @@ -91,7 +91,7 @@ export class MLEngineParent extends JSWindowActorParent { } // eslint-disable-next-line consistent-return - async receiveMessage({ name, data }) { + async receiveMessage({ name }) { switch (name) { case "MLEngine:Ready": if (lazy.EngineProcess.resolveMLEngineParent) { diff --git a/toolkit/components/ml/content/ModelHub.sys.mjs b/toolkit/components/ml/content/ModelHub.sys.mjs new file mode 100644 index 0000000000..4c2181ff14 --- /dev/null +++ b/toolkit/components/ml/content/ModelHub.sys.mjs @@ -0,0 +1,690 @@ +/* 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 lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + clearTimeout: "resource://gre/modules/Timer.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(lazy, "console", () => { + return console.createInstance({ + maxLogLevelPref: "browser.ml.logLevel", + prefix: "ML", + }); +}); + +const ALLOWED_HUBS = [ + "chrome://*", + "resource://*", + "http://localhost", + "https://localhost", + "https://model-hub.mozilla.org", +]; + +const ALLOWED_HEADERS_KEYS = ["Content-Type", "ETag", "status"]; +const DEFAULT_URL_TEMPLATE = + "${organization}/${modelName}/resolve/${modelVersion}/${file}"; + +/** + * Checks if a given URL string corresponds to an allowed hub. + * + * This function validates a URL against a list of allowed hubs, ensuring that it: + * - Is well-formed according to the URL standard. + * - Does not include a username or password. + * - Matches the allowed scheme and hostname. + * + * @param {string} urlString The URL string to validate. + * @returns {boolean} True if the URL is allowed; false otherwise. + */ +function allowedHub(urlString) { + try { + const url = new URL(urlString); + // Check for username or password in the URL + if (url.username !== "" || url.password !== "") { + return false; // Reject URLs with username or password + } + const scheme = url.protocol; + const host = url.hostname; + const fullPrefix = `${scheme}//${host}`; + + return ALLOWED_HUBS.some(allowedHub => { + const [allowedScheme, allowedHost] = allowedHub.split("://"); + if (allowedHost === "*") { + return `${allowedScheme}:` === scheme; + } + const allowedPrefix = `${allowedScheme}://${allowedHost}`; + return fullPrefix === allowedPrefix; + }); + } catch (error) { + lazy.console.error("Error parsing URL:", error); + return false; + } +} + +const NO_ETAG = "NO_ETAG"; + +/** + * Class for managing a cache stored in IndexedDB. + */ +export class IndexedDBCache { + /** + * Reference to the IndexedDB database. + * + * @type {IDBDatabase|null} + */ + db = null; + + /** + * Version of the database. Null if not set. + * + * @type {number|null} + */ + dbVersion = null; + + /** + * Total size of the files stored in the cache. + * + * @type {number} + */ + totalSize = 0; + + /** + * Name of the database used by IndexedDB. + * + * @type {string} + */ + dbName; + + /** + * Name of the object store for storing files. + * + * @type {string} + */ + fileStoreName; + + /** + * Name of the object store for storing headers. + * + * @type {string} + */ + headersStoreName; + /** + * Maximum size of the cache in bytes. Defaults to 1GB. + * + * @type {number} + */ + #maxSize = 1_073_741_824; // 1GB in bytes + + /** + * Private constructor to prevent direct instantiation. + * Use IndexedDBCache.init to create an instance. + * + * @param {string} dbName - The name of the database file. + * @param {number} version - The version number of the database. + */ + constructor(dbName = "modelFiles", version = 1) { + this.dbName = dbName; + this.dbVersion = version; + this.fileStoreName = "files"; + this.headersStoreName = "headers"; + } + + /** + * Static method to create and initialize an instance of IndexedDBCache. + * + * @param {string} [dbName="modelFiles"] - The name of the database. + * @param {number} [version=1] - The version number of the database. + * @returns {Promise<IndexedDBCache>} An initialized instance of IndexedDBCache. + */ + static async init(dbName = "modelFiles", version = 1) { + const cacheInstance = new IndexedDBCache(dbName, version); + cacheInstance.db = await cacheInstance.#openDB(); + const storedSize = await cacheInstance.#getData( + cacheInstance.headersStoreName, + "totalSize" + ); + cacheInstance.totalSize = storedSize ? storedSize.size : 0; + return cacheInstance; + } + + /** + * Called to close the DB connection and dispose the instance + * + */ + async dispose() { + if (this.db) { + this.db.close(); + this.db = null; + } + } + + /** + * Opens or creates the IndexedDB database. + * + * @returns {Promise<IDBDatabase>} + */ + async #openDB() { + return new Promise((resolve, reject) => { + const request = indexedDB.open(this.dbName, this.dbVersion); + request.onerror = event => reject(event.target.error); + request.onsuccess = event => resolve(event.target.result); + request.onupgradeneeded = event => { + const db = event.target.result; + if (!db.objectStoreNames.contains(this.fileStoreName)) { + db.createObjectStore(this.fileStoreName, { keyPath: "id" }); + } + if (!db.objectStoreNames.contains(this.headersStoreName)) { + db.createObjectStore(this.headersStoreName, { keyPath: "id" }); + } + }; + }); + } + + /** + * Generic method to get the data from a specified object store. + * + * @param {string} storeName - The name of the object store. + * @param {string} key - The key within the object store to retrieve the data from. + * @returns {Promise<any>} + */ + async #getData(storeName, key) { + return new Promise((resolve, reject) => { + const transaction = this.db.transaction([storeName], "readonly"); + const store = transaction.objectStore(storeName); + const request = store.get(key); + request.onerror = event => reject(event.target.error); + request.onsuccess = event => resolve(event.target.result); + }); + } + + // Used in tests + async _testGetData(storeName, key) { + return this.#getData(storeName, key); + } + + /** + * Generic method to update data in a specified object store. + * + * @param {string} storeName - The name of the object store. + * @param {object} data - The data to store. + * @returns {Promise<void>} + */ + async #updateData(storeName, data) { + return new Promise((resolve, reject) => { + const transaction = this.db.transaction([storeName], "readwrite"); + const store = transaction.objectStore(storeName); + const request = store.put(data); + request.onerror = event => reject(event.target.error); + request.onsuccess = () => resolve(); + }); + } + + /** + * Deletes a specific cache entry. + * + * @param {string} storeName - The name of the object store. + * @param {string} key - The key of the entry to delete. + * @returns {Promise<void>} + */ + async #deleteData(storeName, key) { + return new Promise((resolve, reject) => { + const transaction = this.db.transaction([storeName], "readwrite"); + const store = transaction.objectStore(storeName); + const request = store.delete(key); + request.onerror = event => reject(event.target.error); + request.onsuccess = () => resolve(); + }); + } + + /** + * Retrieves the headers for a specific cache entry. + * + * @param {string} organization - The organization name. + * @param {string} modelName - The model name. + * @param {string} modelVersion - The model version. + * @param {string} file - The file name. + * @returns {Promise<object|null>} The headers or null if not found. + */ + async getHeaders(organization, modelName, modelVersion, file) { + const headersKey = `${organization}/${modelName}/${modelVersion}`; + const cacheKey = `${organization}/${modelName}/${modelVersion}/${file}`; + const headers = await this.#getData(this.headersStoreName, headersKey); + if (headers && headers.files[cacheKey]) { + return headers.files[cacheKey]; + } + return null; // Return null if no headers is found + } + + /** + * Retrieves the file for a specific cache entry. + * + * @param {string} organization - The organization name. + * @param {string} modelName - The model name. + * @param {string} modelVersion - The model version. + * @param {string} file - The file name. + * @returns {Promise<[ArrayBuffer, object]|null>} The file ArrayBuffer and its headers or null if not found. + */ + async getFile(organization, modelName, modelVersion, file) { + const cacheKey = `${organization}/${modelName}/${modelVersion}/${file}`; + const stored = await this.#getData(this.fileStoreName, cacheKey); + if (stored) { + const headers = await this.getHeaders( + organization, + modelName, + modelVersion, + file + ); + return [stored.data, headers]; + } + return null; // Return null if no file is found + } + + /** + * Adds or updates a cache entry. + * + * @param {string} organization - The organization name. + * @param {string} modelName - The model name. + * @param {string} modelVersion - The model version. + * @param {string} file - The file name. + * @param {ArrayBuffer} arrayBuffer - The data to cache. + * @param {object} [headers] - The headers for the file. + * @returns {Promise<void>} + */ + async put( + organization, + modelName, + modelVersion, + file, + arrayBuffer, + headers = {} + ) { + const cacheKey = `${organization}/${modelName}/${modelVersion}/${file}`; + const newSize = this.totalSize + arrayBuffer.byteLength; + if (newSize > this.#maxSize) { + throw new Error("Exceeding total cache size limit of 1GB"); + } + + const headersKey = `${organization}/${modelName}/${modelVersion}`; + const data = { id: cacheKey, data: arrayBuffer }; + + // Store the file data + await this.#updateData(this.fileStoreName, data); + + // Update headers store - whith defaults for ETag and Content-Type + headers = headers || {}; + headers["Content-Type"] = + headers["Content-Type"] ?? "application/octet-stream"; + headers.ETag = headers.ETag ?? NO_ETAG; + + // filter out any keys that are not allowed + headers = Object.keys(headers) + .filter(key => ALLOWED_HEADERS_KEYS.includes(key)) + .reduce((obj, key) => { + obj[key] = headers[key]; + return obj; + }, {}); + + const headersStore = (await this.#getData( + this.headersStoreName, + headersKey + )) || { + id: headersKey, + files: {}, + }; + headersStore.files[cacheKey] = headers; + await this.#updateData(this.headersStoreName, headersStore); + + // Update size + await this.#updateTotalSize(arrayBuffer.byteLength); + } + + /** + * Updates the total size of the cache. + * + * @param {number} sizeToAdd - The size to add to the total. + * @returns {Promise<void>} + */ + async #updateTotalSize(sizeToAdd) { + this.totalSize += sizeToAdd; + await this.#updateData(this.headersStoreName, { + id: "totalSize", + size: this.totalSize, + }); + } + /** + * Deletes all data related to a specific model. + * + * @param {string} organization - The organization name. + * @param {string} modelName - The model name. + * @param {string} modelVersion - The model version. + * @returns {Promise<void>} + */ + async deleteModel(organization, modelName, modelVersion) { + const headersKey = `${organization}/${modelName}/${modelVersion}`; + const headers = await this.#getData(this.headersStoreName, headersKey); + if (headers) { + for (const fileKey in headers.files) { + await this.#deleteData(this.fileStoreName, fileKey); + } + await this.#deleteData(this.headersStoreName, headersKey); // Remove headers entry after files are deleted + } + } + + /** + * Lists all models stored in the cache. + * + * @returns {Promise<Array<string>>} An array of model identifiers. + */ + async listModels() { + const models = []; + return new Promise((resolve, reject) => { + const transaction = this.db.transaction( + [this.headersStoreName], + "readonly" + ); + const store = transaction.objectStore(this.headersStoreName); + const request = store.openCursor(); + request.onerror = event => reject(event.target.error); + request.onsuccess = event => { + const cursor = event.target.result; + if (cursor) { + models.push(cursor.value.id); // Assuming id is the organization/modelName + cursor.continue(); + } else { + resolve(models); + } + }; + }); + } +} + +export class ModelHub { + constructor({ rootUrl, urlTemplate = DEFAULT_URL_TEMPLATE }) { + if (!allowedHub(rootUrl)) { + throw new Error(`Invalid model hub root url: ${rootUrl}`); + } + this.rootUrl = rootUrl; + this.cache = null; + + // Ensures the URL template is well-formed and does not contain any invalid characters. + const pattern = /^(?:\$\{\w+\}|\w+)(?:\/(?:\$\{\w+\}|\w+))*$/; + // ^ $ Start and end of string + // (?:\$\{\w+\}|\w+) Match a ${placeholder} or alphanumeric characters + // (?:\/(?:\$\{\w+\}|\w+))* Zero or more groups of a forward slash followed by a ${placeholder} or alphanumeric characters + if (!pattern.test(urlTemplate)) { + throw new Error(`Invalid URL template: ${urlTemplate}`); + } + this.urlTemplate = urlTemplate; + } + + async #initCache() { + if (this.cache) { + return; + } + this.cache = await IndexedDBCache.init(); + } + + /** Creates the file URL from the organization, model, and version. + * + * @param {string} organization + * @param {string} modelName + * @param {string} modelVersion + * @param {string} file + * @returns {string} The full URL + */ + #fileUrl(organization, modelName, modelVersion, file) { + const baseUrl = new URL(this.rootUrl); + if (!baseUrl.pathname.endsWith("/")) { + baseUrl.pathname += "/"; + } + + // Replace placeholders in the URL template with the provided data. + // If some keys are missing in the data object, the placeholder is left as is. + // If the placeholder is not found in the data object, it is left as is. + const data = { + organization, + modelName, + modelVersion, + file, + }; + const path = this.urlTemplate.replace( + /\$\{(\w+)\}/g, + (match, key) => data[key] || match + ); + const fullPath = `${baseUrl.pathname}${ + path.startsWith("/") ? path.slice(1) : path + }`; + + const urlObject = new URL(fullPath, baseUrl.origin); + urlObject.searchParams.append("download", "true"); + return urlObject.toString(); + } + + /** Checks the organization, model, and version inputs. + * + * @param { string } organization + * @param { string } modelName + * @param { string } modelVersion + * @param { string } file + * @returns { Error } The error instance(can be null) + */ + #checkInput(organization, modelName, modelVersion, file) { + // Ensures string consists only of letters, digits, and hyphens without starting/ending + // with a hyphen or containing consecutive hyphens. + // + // ^ $ Start and end of string + // (?!-) (?<!-) Negative lookahead/behind for not starting or ending with hyphen + // (?!.*--) Negative lookahead for not containing consecutive hyphens + // [A-Za-z0-9-]+ Alphanum characters or hyphens, one or more + const orgRegex = /^(?!-)(?!.*--)[A-Za-z0-9-]+(?<!-)$/; + + // Matches strings containing letters, digits, hyphens, underscores, or periods. + // ^ $ Start and end of string + // [A-Za-z0-9-_.]+ Alphanum characters, hyphens, underscores, or periods, one or more times + const modelRegex = /^[A-Za-z0-9-_.]+$/; + + // Matches strings consisting of alphanumeric characters, hyphens, or periods. + // + // ^ $ Start and end of string + // [A-Za-z0-9-.]+ Alphanum characters, hyphens, or periods, one or more times + const versionRegex = /^[A-Za-z0-9-.]+$/; + + // Matches filenames with subdirectories, starting with alphanumeric or underscore, + // and optionally ending with a dot followed by a 2-4 letter extension. + // + // ^ $ Start and end of string + // (?:\/)? Optional leading slash (for absolute paths or root directory) + // (?!\/) Negative lookahead for not starting with a slash + // [A-Za-z0-9-_]+ First directory or filename + // (?: Begin non-capturing group for additional directories or file + // \/ Directory separator + // [A-Za-z0-9-_]+ Directory or file name + // )* Zero or more times + // (?:[.][A-Za-z]{2,4})? Optional non-capturing group for file extension + const fileRegex = + /^(?:\/)?(?!\/)[A-Za-z0-9-_]+(?:\/[A-Za-z0-9-_]+)*(?:[.][A-Za-z]{2,4})?$/; + + if (!orgRegex.test(organization) || !isNaN(parseInt(organization))) { + return new Error(`Invalid organization name ${organization}`); + } + + if (!modelRegex.test(modelName)) { + return new Error("Invalid model name."); + } + + if ( + !versionRegex.test(modelVersion) || + modelVersion.includes(" ") || + /[\^$]/.test(modelVersion) + ) { + return new Error("Invalid version identifier."); + } + + if (!fileRegex.test(file)) { + return new Error("Invalid file name"); + } + + return null; + } + + /** + * Returns the ETag value given an URL + * + * @param {string} url + * @param {number} timeout in ms. Default is 1000 + * @returns {Promise<string>} ETag (can be null) + */ + async #getETag(url, timeout = 1000) { + const controller = new AbortController(); + const id = lazy.setTimeout(() => controller.abort(), timeout); + + try { + const headResponse = await fetch(url, { + method: "HEAD", + signal: controller.signal, + }); + const currentEtag = headResponse.headers.get("ETag"); + return currentEtag; + } catch (error) { + lazy.console.warn("An error occurred when calling HEAD:", error); + return null; + } finally { + lazy.clearTimeout(id); + } + } + + /** + * Given an organization, model, and version, fetch a model file in the hub as a Response. + * + * @param {object} config + * @param {string} config.organization + * @param {string} config.modelName + * @param {string} config.modelVersion + * @param {string} config.file + * @returns {Promise<Response>} The file content + */ + async getModelFileAsResponse({ + organization, + modelName, + modelVersion, + file, + }) { + const [blob, headers] = await this.getModelFileAsBlob({ + organization, + modelName, + modelVersion, + file, + }); + return new Response(blob, { headers }); + } + + /** + * Given an organization, model, and version, fetch a model file in the hub as an ArrayBuffer. + * + * @param {object} config + * @param {string} config.organization + * @param {string} config.modelName + * @param {string} config.modelVersion + * @param {string} config.file + * @returns {Promise<[ArrayBuffer, headers]>} The file content + */ + async getModelFileAsArrayBuffer({ + organization, + modelName, + modelVersion, + file, + }) { + const [blob, headers] = await this.getModelFileAsBlob({ + organization, + modelName, + modelVersion, + file, + }); + return [await blob.arrayBuffer(), headers]; + } + + /** + * Given an organization, model, and version, fetch a model file in the hub as blob. + * + * @param {object} config + * @param {string} config.organization + * @param {string} config.modelName + * @param {string} config.modelVersion + * @param {string} config.file + * @returns {Promise<[Blob, object]>} The file content + */ + async getModelFileAsBlob({ organization, modelName, modelVersion, file }) { + // Make sure inputs are clean. We don't sanitize them but throw an exception + let checkError = this.#checkInput( + organization, + modelName, + modelVersion, + file + ); + if (checkError) { + throw checkError; + } + + const url = this.#fileUrl(organization, modelName, modelVersion, file); + lazy.console.debug(`Getting model file from ${url}`); + + await this.#initCache(); + + // this can be null if no ETag was found or there were a network error + const hubETag = await this.#getETag(url); + + lazy.console.debug( + `Checking the cache for ${organization}/${modelName}/${modelVersion}/${file}` + ); + + // storage lookup + const cachedHeaders = await this.cache.getHeaders( + organization, + modelName, + modelVersion, + file + ); + const cachedEtag = cachedHeaders ? cachedHeaders.ETag : null; + + // If we have something in store, and the hub ETag is null or it matches the cached ETag, return the cached response + if (cachedEtag !== null && (hubETag === null || cachedEtag === hubETag)) { + lazy.console.debug(`Cache Hit`); + return await this.cache.getFile( + organization, + modelName, + modelVersion, + file + ); + } + + lazy.console.debug(`Fetching ${url}`); + try { + const response = await fetch(url); + if (response.ok) { + const clone = response.clone(); + const headers = { + // We don't store the boundary or the charset, just the content type, + // so we drop what's after the semicolon. + "Content-Type": response.headers.get("Content-Type").split(";")[0], + ETag: hubETag, + }; + + await this.cache.put( + organization, + modelName, + modelVersion, + file, + await clone.blob(), + headers + ); + return [await response.blob(), headers]; + } + } catch (error) { + lazy.console.error(`Failed to fetch ${url}:`, error); + } + + throw new Error(`Failed to fetch the model file: ${url}`); + } +} diff --git a/toolkit/components/ml/jar.mn b/toolkit/components/ml/jar.mn index 56bfb0d469..c9c82e0b26 100644 --- a/toolkit/components/ml/jar.mn +++ b/toolkit/components/ml/jar.mn @@ -6,4 +6,5 @@ toolkit.jar: content/global/ml/EngineProcess.sys.mjs (content/EngineProcess.sys.mjs) content/global/ml/MLEngine.worker.mjs (content/MLEngine.worker.mjs) content/global/ml/MLEngine.html (content/MLEngine.html) + content/global/ml/ModelHub.sys.mjs (content/ModelHub.sys.mjs) content/global/ml/SummarizerModel.sys.mjs (content/SummarizerModel.sys.mjs) diff --git a/toolkit/components/ml/tests/browser/browser.toml b/toolkit/components/ml/tests/browser/browser.toml index 9ccda0beaa..57637c8bda 100644 --- a/toolkit/components/ml/tests/browser/browser.toml +++ b/toolkit/components/ml/tests/browser/browser.toml @@ -1,5 +1,9 @@ [DEFAULT] support-files = [ "head.js", + "data/**/*.*" ] + +["browser_ml_cache.js"] + ["browser_ml_engine.js"] diff --git a/toolkit/components/ml/tests/browser/browser_ml_cache.js b/toolkit/components/ml/tests/browser/browser_ml_cache.js new file mode 100644 index 0000000000..d8725368bd --- /dev/null +++ b/toolkit/components/ml/tests/browser/browser_ml_cache.js @@ -0,0 +1,361 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +// Root URL of the fake hub, see the `data` dir in the tests. +const FAKE_HUB = + "chrome://mochitests/content/browser/toolkit/components/ml/tests/browser/data"; + +const FAKE_MODEL_ARGS = { + organization: "acme", + modelName: "bert", + modelVersion: "main", + file: "config.json", +}; + +const FAKE_ONNX_MODEL_ARGS = { + organization: "acme", + modelName: "bert", + modelVersion: "main", + file: "onnx/config.json", +}; + +const badHubs = [ + "https://my.cool.hub", + "https://sub.localhost/myhub", // Subdomain of allowed domain + "https://model-hub.mozilla.org.evil.com", // Manipulating path to mimic domain + "httpsz://localhost/myhub", // Similar-looking scheme + "https://localhost.", // Trailing dot in domain + "resource://user@localhost", // User info in URL + "ftp://localhost/myhub", // Disallowed scheme with allowed host + "https://model-hub.mozilla.org.hack", // Domain that contains allowed domain +]; + +add_task(async function test_bad_hubs() { + for (const badHub of badHubs) { + Assert.throws( + () => new ModelHub({ rootUrl: badHub }), + new RegExp(`Error: Invalid model hub root url: ${badHub}`), + `Should throw with ${badHub}` + ); + } +}); + +let goodHubs = [ + "https:///localhost/myhub", // Triple slashes, see https://stackoverflow.com/a/22775589 + "https://localhost:8080/myhub", + "http://localhost/myhub", + "https://model-hub.mozilla.org", + "chrome://gre/somewhere/in/the/code/base", +]; + +add_task(async function test_allowed_hub() { + goodHubs.forEach(url => new ModelHub({ rootUrl: url })); +}); + +const badInputs = [ + [ + { + organization: "ac me", + modelName: "bert", + modelVersion: "main", + file: "config.json", + }, + "Org can only contain letters, numbers, and hyphens", + ], + [ + { + organization: "1111", + modelName: "bert", + modelVersion: "main", + file: "config.json", + }, + "Org cannot contain only numbers", + ], + [ + { + organization: "-acme", + modelName: "bert", + modelVersion: "main", + file: "config.json", + }, + "Org start or end with a hyphen, or use consecutive hyphens", + ], + [ + { + organization: "a-c-m-e", + modelName: "#bert", + modelVersion: "main", + file: "config.json", + }, + "Models can only contain letters, numbers, and hyphens, underscord, periods", + ], + [ + { + organization: "a-c-m-e", + modelName: "b$ert", + modelVersion: "main", + file: "config.json", + }, + "Models cannot contain spaces or control characters", + ], + [ + { + organization: "a-c-m-e", + modelName: "b$ert", + modelVersion: "main", + file: ".filename", + }, + "File", + ], +]; + +add_task(async function test_bad_inputs() { + const hub = new ModelHub({ rootUrl: FAKE_HUB }); + + for (const badInput of badInputs) { + const params = badInput[0]; + const errorMsg = badInput[1]; + try { + await hub.getModelFileAsArrayBuffer(params); + } catch (error) { + continue; + } + throw new Error(errorMsg); + } +}); + +add_task(async function test_getting_file() { + const hub = new ModelHub({ rootUrl: FAKE_HUB }); + + let [array, headers] = await hub.getModelFileAsArrayBuffer(FAKE_MODEL_ARGS); + + Assert.equal(headers["Content-Type"], "application/json"); + + // check the content of the file. + let jsonData = JSON.parse( + String.fromCharCode.apply(null, new Uint8Array(array)) + ); + + Assert.equal(jsonData.hidden_size, 768); +}); + +add_task(async function test_getting_file_in_subdir() { + const hub = new ModelHub({ rootUrl: FAKE_HUB }); + + let [array, metadata] = await hub.getModelFileAsArrayBuffer( + FAKE_ONNX_MODEL_ARGS + ); + + Assert.equal(metadata["Content-Type"], "application/json"); + + // check the content of the file. + let jsonData = JSON.parse( + String.fromCharCode.apply(null, new Uint8Array(array)) + ); + + Assert.equal(jsonData.hidden_size, 768); +}); + +add_task(async function test_getting_file_custom_path() { + const hub = new ModelHub({ + rootUrl: FAKE_HUB, + urlTemplate: "${organization}/${modelName}/resolve/${modelVersion}/${file}", + }); + + let res = await hub.getModelFileAsArrayBuffer(FAKE_MODEL_ARGS); + + Assert.equal(res[1]["Content-Type"], "application/json"); +}); + +add_task(async function test_getting_file_custom_path_rogue() { + const urlTemplate = + "${organization}/${modelName}/resolve/${modelVersion}/${file}?some_id=bedqwdw"; + Assert.throws( + () => new ModelHub({ rootUrl: FAKE_HUB, urlTemplate }), + /Invalid URL template/, + `Should throw with ${urlTemplate}` + ); +}); + +add_task(async function test_getting_file_as_response() { + const hub = new ModelHub({ rootUrl: FAKE_HUB }); + + let response = await hub.getModelFileAsResponse(FAKE_MODEL_ARGS); + + // check the content of the file. + let jsonData = await response.json(); + Assert.equal(jsonData.hidden_size, 768); +}); + +add_task(async function test_getting_file_from_cache() { + const hub = new ModelHub({ rootUrl: FAKE_HUB }); + let array = await hub.getModelFileAsArrayBuffer(FAKE_MODEL_ARGS); + + // stub to verify that the data was retrieved from IndexDB + let matchMethod = hub.cache._testGetData; + + sinon.stub(hub.cache, "_testGetData").callsFake(function () { + return matchMethod.apply(this, arguments).then(result => { + Assert.notEqual(result, null); + return result; + }); + }); + + // exercises the cache + let array2 = await hub.getModelFileAsArrayBuffer(FAKE_MODEL_ARGS); + hub.cache._testGetData.restore(); + + Assert.deepEqual(array, array2); +}); + +// IndexedDB tests + +/** + * Helper function to initialize the cache + */ +async function initializeCache() { + const randomSuffix = Math.floor(Math.random() * 10000); + return await IndexedDBCache.init(`modelFiles-${randomSuffix}`); +} + +/** + * Helper function to delete the cache database + */ +async function deleteCache(cache) { + await cache.dispose(); + indexedDB.deleteDatabase(cache.dbName); +} + +/** + * Test the initialization and creation of the IndexedDBCache instance. + */ +add_task(async function test_Init() { + const cache = await initializeCache(); + Assert.ok( + cache instanceof IndexedDBCache, + "The cache instance should be created successfully." + ); + Assert.ok( + IDBDatabase.isInstance(cache.db), + `The cache should have an IDBDatabase instance. Found ${cache.db}` + ); + await deleteCache(cache); +}); + +/** + * Test adding data to the cache and retrieving it. + */ +add_task(async function test_PutAndGet() { + const cache = await initializeCache(); + const testData = new ArrayBuffer(8); // Example data + await cache.put("org", "model", "v1", "file.txt", testData, { + ETag: "ETAG123", + }); + + const [retrievedData, headers] = await cache.getFile( + "org", + "model", + "v1", + "file.txt" + ); + Assert.deepEqual( + retrievedData, + testData, + "The retrieved data should match the stored data." + ); + Assert.equal( + headers.ETag, + "ETAG123", + "The retrieved ETag should match the stored ETag." + ); + + await deleteCache(cache); +}); + +/** + * Test retrieving the headers for a cache entry. + */ +add_task(async function test_GetHeaders() { + const cache = await initializeCache(); + const testData = new ArrayBuffer(8); + const headers = { + ETag: "ETAG123", + status: 200, + extra: "extra", + }; + + await cache.put("org", "model", "v1", "file.txt", testData, headers); + + const storedHeaders = await cache.getHeaders( + "org", + "model", + "v1", + "file.txt" + ); + + // The `extra` field should be removed from the stored headers because + // it's not part of the allowed keys. + // The content-type one is added when not present + Assert.deepEqual( + { + ETag: "ETAG123", + status: 200, + "Content-Type": "application/octet-stream", + }, + storedHeaders, + "The retrieved headers should match the stored headers." + ); + await deleteCache(cache); +}); + +/** + * Test listing all models stored in the cache. + */ +add_task(async function test_ListModels() { + const cache = await initializeCache(); + await cache.put( + "org1", + "modelA", + "v1", + "file1.txt", + new ArrayBuffer(8), + null + ); + await cache.put( + "org2", + "modelB", + "v1", + "file2.txt", + new ArrayBuffer(8), + null + ); + + const models = await cache.listModels(); + Assert.ok( + models.includes("org1/modelA/v1") && models.includes("org2/modelB/v1"), + "All models should be listed." + ); + await deleteCache(cache); +}); + +/** + * Test deleting a model and its data from the cache. + */ +add_task(async function test_DeleteModel() { + const cache = await initializeCache(); + await cache.put("org", "model", "v1", "file.txt", new ArrayBuffer(8), null); + await cache.deleteModel("org", "model", "v1"); + + const dataAfterDelete = await cache.getFile("org", "model", "v1", "file.txt"); + Assert.equal( + dataAfterDelete, + null, + "The data for the deleted model should not exist." + ); + await deleteCache(cache); +}); diff --git a/toolkit/components/ml/tests/browser/data/README.md b/toolkit/components/ml/tests/browser/data/README.md new file mode 100644 index 0000000000..d826cf7ee6 --- /dev/null +++ b/toolkit/components/ml/tests/browser/data/README.md @@ -0,0 +1,5 @@ +# fake hub + +This directory is a fake hub that is served via chrome://global/content/ml/tests. + +All files in this directory are included with a wildcard in the component `jar.md`. diff --git a/toolkit/components/ml/tests/browser/data/acme/bert/resolve/main/config.json b/toolkit/components/ml/tests/browser/data/acme/bert/resolve/main/config.json new file mode 100644 index 0000000000..50dbb760bb --- /dev/null +++ b/toolkit/components/ml/tests/browser/data/acme/bert/resolve/main/config.json @@ -0,0 +1,21 @@ +{ + "architectures": ["BertForMaskedLM"], + "attention_probs_dropout_prob": 0.1, + "gradient_checkpointing": false, + "hidden_act": "gelu", + "hidden_dropout_prob": 0.1, + "hidden_size": 768, + "initializer_range": 0.02, + "intermediate_size": 3072, + "layer_norm_eps": 1e-12, + "max_position_embeddings": 512, + "model_type": "bert", + "num_attention_heads": 12, + "num_hidden_layers": 12, + "pad_token_id": 0, + "position_embedding_type": "absolute", + "transformers_version": "4.6.0.dev0", + "type_vocab_size": 2, + "use_cache": true, + "vocab_size": 30522 +} diff --git a/toolkit/components/ml/tests/browser/data/acme/bert/resolve/main/onnx/config.json b/toolkit/components/ml/tests/browser/data/acme/bert/resolve/main/onnx/config.json new file mode 100644 index 0000000000..50dbb760bb --- /dev/null +++ b/toolkit/components/ml/tests/browser/data/acme/bert/resolve/main/onnx/config.json @@ -0,0 +1,21 @@ +{ + "architectures": ["BertForMaskedLM"], + "attention_probs_dropout_prob": 0.1, + "gradient_checkpointing": false, + "hidden_act": "gelu", + "hidden_dropout_prob": 0.1, + "hidden_size": 768, + "initializer_range": 0.02, + "intermediate_size": 3072, + "layer_norm_eps": 1e-12, + "max_position_embeddings": 512, + "model_type": "bert", + "num_attention_heads": 12, + "num_hidden_layers": 12, + "pad_token_id": 0, + "position_embedding_type": "absolute", + "transformers_version": "4.6.0.dev0", + "type_vocab_size": 2, + "use_cache": true, + "vocab_size": 30522 +} diff --git a/toolkit/components/ml/tests/browser/head.js b/toolkit/components/ml/tests/browser/head.js index 99d27ce18a..9fc20c0e84 100644 --- a/toolkit/components/ml/tests/browser/head.js +++ b/toolkit/components/ml/tests/browser/head.js @@ -19,6 +19,10 @@ const { MLEngineParent } = ChromeUtils.importESModule( "resource://gre/actors/MLEngineParent.sys.mjs" ); +const { ModelHub, IndexedDBCache } = ChromeUtils.importESModule( + "chrome://global/content/ml/ModelHub.sys.mjs" +); + // This test suite shares some utility functions with translations as they work in a very // similar fashion. Eventually, the plan is to unify these two components. Services.scriptloader.loadSubScript( diff --git a/toolkit/components/moz.build b/toolkit/components/moz.build index d574f1305b..c1785e21e5 100644 --- a/toolkit/components/moz.build +++ b/toolkit/components/moz.build @@ -30,6 +30,7 @@ DIRS += [ "commandlines", "contentanalysis", "contentprefs", + "contentrelevancy", "contextualidentity", "crashes", "crashmonitor", @@ -137,10 +138,6 @@ DIRS += ["nimbus"] if CONFIG["MOZ_BACKGROUNDTASKS"]: DIRS += ["backgroundtasks"] -# This is only packaged for browser since corrupt JAR and XPI files tend to be a desktop-OS problem. -if CONFIG["MOZ_BUILD_APP"] == "browser": - DIRS += ["corroborator"] - if CONFIG["MOZ_UNIFFI_FIXTURES"]: DIRS += ["uniffi-bindgen-gecko-js/fixtures"] diff --git a/toolkit/components/narrate/NarrateControls.sys.mjs b/toolkit/components/narrate/NarrateControls.sys.mjs index ed2f8ff124..19551fa59f 100644 --- a/toolkit/components/narrate/NarrateControls.sys.mjs +++ b/toolkit/components/narrate/NarrateControls.sys.mjs @@ -38,7 +38,7 @@ export function NarrateControls(win, languagePromise) { toggleButton.dataset.telemetryId = "reader-listen"; let tip = win.document.createElement("span"); let shortcutNarrateKey = gStrings.GetStringFromName("narrate-key-shortcut"); - let labelText = gStrings.formatStringFromName("listen-label", [ + let labelText = gStrings.formatStringFromName("read-aloud-label", [ shortcutNarrateKey, ]); tip.textContent = labelText; @@ -65,10 +65,6 @@ export function NarrateControls(win, languagePromise) { narrateVoices.className = "narrate-row narrate-voices"; dropdownList.appendChild(narrateVoices); - let dropdownArrow = win.document.createElement("div"); - dropdownArrow.className = "dropdown-arrow"; - dropdownList.appendChild(dropdownArrow); - let narrateSkipPrevious = win.document.createElement("button"); narrateSkipPrevious.className = "narrate-skip-previous"; narrateSkipPrevious.disabled = true; diff --git a/toolkit/components/nimbus/FeatureManifest.yaml b/toolkit/components/nimbus/FeatureManifest.yaml index 7612ef2dc2..c540e0e387 100644 --- a/toolkit/components/nimbus/FeatureManifest.yaml +++ b/toolkit/components/nimbus/FeatureManifest.yaml @@ -136,6 +136,14 @@ search: branch: default pref: browser.urlbar.trending.maxResultsNoSearchMode description: The maximum number of trending results mode outside search mode. + newSearchConfigEnabled: + type: boolean + setPref: + branch: user + pref: browser.search.newSearchConfig.enabled + description: >- + Whether search-config-v2 is enabled for the user. This will only take + effect when the user restarts. # `searchConfiguration` is for search experiment features for items that require # isEarlyStartup to be true. These items may require a reload of the search @@ -256,6 +264,13 @@ urlbar: will be able to click the "Show less frequently" command for Pocket suggestions. If undefined or zero, the user will be able to click the command without any limit. + potentialExposureKeywords: + type: json + fallbackPref: browser.urlbar.potentialExposureKeywords + description: >- + An array of keyword strings that will trigger the + `urlbar-potential-exposure` ping when the user types one during a urlbar + session. quickSuggestAllowPositionInSuggestions: type: boolean fallbackPref: browser.urlbar.quicksuggest.allowPositionInSuggestions @@ -825,6 +840,13 @@ pocketNewtab: browser.newtabpage.activity-stream.discoverystream.sendToPocket.enabled description: >- Decides what to do when a logged out user click "Save to Pocket" from a Pocket card. + wallpapers: + type: boolean + setPref: + branch: user + pref: browser.newtabpage.activity-stream.newtabWallpapers.enabled + description: >- + Turns on and off wallpaper support. recsPersonalized: type: boolean fallbackPref: >- @@ -926,27 +948,36 @@ saveToPocket: description: The save to Pocket feature owner: sdowne@getpocket.com hasExposure: false - isEarlyStartup: true variables: emailButton: type: boolean - fallbackPref: extensions.pocket.refresh.emailButton.enabled + setPref: + branch: user + pref: extensions.pocket.refresh.emailButton.enabled description: Just for the new Pocket panels, enables the email signup button. hideRecentSaves: type: boolean - fallbackPref: extensions.pocket.refresh.hideRecentSaves.enabled + setPref: + branch: user + pref: extensions.pocket.refresh.hideRecentSaves.enabled description: Hides the recently saved section in the home panel. bffRecentSaves: type: boolean - fallbackPref: "extensions.pocket.bffRecentSaves" + setPref: + branch: user + pref: extensions.pocket.bffRecentSaves description: Use the new BFF Proxy Service instead of the legacy Pocket Service for Recent Saves bffApi: type: string - fallbackPref: "extensions.pocket.bffApi" + setPref: + branch: user + pref: extensions.pocket.bffApi description: BFF Proxy Service domain oAuthConsumerKeyBff: type: string - fallbackPref: "extensions.pocket.oAuthConsumerKeyBff" + setPref: + branch: user + pref: extensions.pocket.oAuthConsumerKeyBff description: BFF Proxy Service OAuth Consumer Key password-autocomplete: @@ -1378,9 +1409,6 @@ gleanInternalSdk: type: int description: >- Maximum number of pings that can be sent in a 60 second interval - enableEventTimestamps: - type: "boolean" - description: "Enables precise event timestamps for Glean events" majorRelease2022: description: Major Release 2022 @@ -1543,6 +1571,12 @@ dohPrefs: setPref: branch: default pref: "network.trr_ui.show_fallback_warning_option" + nativeHTTPSRecords: + description: Whether we can perform native DNS HTTPS lookups + type: boolean + setPref: + branch: default + pref: "network.dns.native_https_query" dooh: description: "DNS over Oblivious HTTP" @@ -2295,7 +2329,6 @@ updatePrompt: exposureDescription: >- Exposure is sent at most once per browsing session when an update notification prompt is displayed. - isEarlyStartup: true variables: showReleaseNotesLink: type: boolean @@ -2647,3 +2680,69 @@ httpsFirst: setPref: branch: default pref: dom.security.https_only_fire_http_request_background_timer_ms + +contentRelevancy: + description: >- + A feature for interest-based content relevance ranking and personalization + for Firefox. + owner: disco-team@mozilla.com + hasExposure: false + variables: + enabled: + description: Enable this feature + type: boolean + fallbackPref: toolkit.contentRelevancy.enabled + maxInputUrls: + description: The maximum number of input URLs for interest classification + type: int + minInputUrls: + description: The minimal number of input URLs for interest classification + type: int + timerInterval: + description: >- + The interval (in seconds) of the background update timer for the content + relevancy manager + type: int + setPref: + branch: user + pref: toolkit.contentRelevancy.timerInterval + +tabPreview: + description: Prefs to control Tab Previews + owner: dwalker@mozilla.com + hasExposure: false + variables: + tabPreviewsEnabled: + type: boolean + setPref: + branch: default + pref: browser.tabs.cardPreview.enabled + description: >- + When true, users will see the new card preview when hovering a tab, instead of the standard tooltip + +backupService: + description: Prefs to control the profile backup service + owner: mconley@mozilla.com + hasExposure: false + variables: + enabled: + type: boolean + setPref: + branch: default + pref: browser.backup.enabled + description: >- + When true, the profile backup service will be initialized soon after + startup. + +pqcrypto: + description: Prefs that control the use of post-quantum cryptography. + owner: jschanck@mozilla.com + hasExposure: false + variables: + tlsEnableXyber: + type: boolean + setPref: + branch: default + pref: security.tls.enable_kyber + description: >- + Whether to enable Xyber768 for TLS. diff --git a/toolkit/components/nimbus/generate/generate_feature_manifest.py b/toolkit/components/nimbus/generate/generate_feature_manifest.py index 14057493c5..55e681fdf8 100644 --- a/toolkit/components/nimbus/generate/generate_feature_manifest.py +++ b/toolkit/components/nimbus/generate/generate_feature_manifest.py @@ -31,11 +31,9 @@ ALLOWED_ISEARLYSTARTUP_FEATURE_IDS = { "majorRelease2022", "newtab", "pocketNewtab", - "saveToPocket", "searchConfiguration", "shellService", "testFeature", - "updatePrompt", "upgradeDialog", } diff --git a/toolkit/components/nimbus/lib/ExperimentManager.sys.mjs b/toolkit/components/nimbus/lib/ExperimentManager.sys.mjs index d0f313a2ae..8bd847b067 100644 --- a/toolkit/components/nimbus/lib/ExperimentManager.sys.mjs +++ b/toolkit/components/nimbus/lib/ExperimentManager.sys.mjs @@ -95,8 +95,8 @@ export class _ExperimentManager { get studiesEnabled() { return ( - Services.prefs.getBoolPref(UPLOAD_ENABLED_PREF) && - Services.prefs.getBoolPref(STUDIES_OPT_OUT_PREF) + Services.prefs.getBoolPref(UPLOAD_ENABLED_PREF, false) && + Services.prefs.getBoolPref(STUDIES_OPT_OUT_PREF, false) ); } @@ -974,7 +974,7 @@ export class _ExperimentManager { // need to check if we have another enrollment for the same feature. const conflictingEnrollment = getConflictingEnrollment(featureId); - for (const [variable, value] of Object.entries(featureValue)) { + for (let [variable, value] of Object.entries(featureValue)) { const setPref = feature.getSetPref(variable); if (setPref) { @@ -1012,6 +1012,13 @@ export class _ExperimentManager { // An experiment takes precedence if there is already a pref set. if (!isRollout || !conflictingPref) { + if ( + lazy.NimbusFeatures[featureId].manifest.variables[variable] + .type === "json" + ) { + value = JSON.stringify(value); + } + prefsToSet.push({ name: prefName, value, @@ -1250,7 +1257,13 @@ export class _ExperimentManager { } } - const value = featuresById[featureId].value[variable]; + let value = featuresById[featureId].value[variable]; + if ( + lazy.NimbusFeatures[featureId].manifest.variables[variable].type === + "json" + ) { + value = JSON.stringify(value); + } if (prefBranch !== "user") { lazy.PrefUtils.setPref(name, value, { branch: prefBranch }); diff --git a/toolkit/components/nimbus/test/unit/test_ExperimentManager_prefs.js b/toolkit/components/nimbus/test/unit/test_ExperimentManager_prefs.js index 7ae0e1b76f..f2f5a25836 100644 --- a/toolkit/components/nimbus/test/unit/test_ExperimentManager_prefs.js +++ b/toolkit/components/nimbus/test/unit/test_ExperimentManager_prefs.js @@ -3348,3 +3348,207 @@ add_task(async function test_nested_prefs_enroll_both() { branch: DEFAULT, }); }); + +const TYPED_FEATURE = new ExperimentFeature("test-typed-prefs", { + description: "Test feature that sets each type of pref", + owner: "test@test.test", + hasExposure: false, + variables: { + string: { + type: "string", + description: "test string variable", + setPref: { + branch: "default", + pref: "nimbus.test-only.types.string", + }, + }, + int: { + type: "int", + description: "test int variable", + setPref: { + branch: "default", + pref: "nimbus.test-only.types.int", + }, + }, + boolean: { + type: "boolean", + description: "test boolean variable", + setPref: { + branch: "default", + pref: "nimbus.test-only.types.boolean", + }, + }, + json: { + type: "json", + description: "test json variable", + setPref: { + branch: "default", + pref: "nimbus.test-only.types.json", + }, + }, + }, +}); + +add_task(async function test_setPref_types() { + const featureCleanup = ExperimentTestUtils.addTestFeatures(TYPED_FEATURE); + + const store = ExperimentFakes.store(); + const manager = ExperimentFakes.manager(store); + + await manager.onStartup(); + await assertEmptyStore(store); + + const json = { + foo: "foo", + bar: 12345, + baz: true, + qux: null, + quux: ["corge"], + }; + + const experimentCleanup = await ExperimentFakes.enrollWithFeatureConfig( + { + featureId: TYPED_FEATURE.featureId, + value: { + string: "hello, world", + int: 12345, + boolean: true, + json, + }, + }, + { manager } + ); + + const defaultBranch = Services.prefs.getDefaultBranch(null); + + Assert.equal( + defaultBranch.getPrefType("nimbus.test-only.types.string"), + Services.prefs.PREF_STRING + ); + Assert.equal( + defaultBranch.getStringPref("nimbus.test-only.types.string"), + "hello, world" + ); + + Assert.equal( + defaultBranch.getPrefType("nimbus.test-only.types.int"), + Services.prefs.PREF_INT + ); + Assert.equal(defaultBranch.getIntPref("nimbus.test-only.types.int"), 12345); + + Assert.equal( + defaultBranch.getPrefType("nimbus.test-only.types.boolean"), + Services.prefs.PREF_BOOL + ); + Assert.equal( + defaultBranch.getBoolPref("nimbus.test-only.types.boolean"), + true + ); + + Assert.equal( + defaultBranch.getPrefType("nimbus.test-only.types.json"), + Services.prefs.PREF_STRING + ); + + const jsonPrefValue = JSON.parse( + defaultBranch.getStringPref("nimbus.test-only.types.json") + ); + + Assert.deepEqual(json, jsonPrefValue); + + await experimentCleanup(); + featureCleanup(); + + await assertEmptyStore(store, { cleanup: true }); +}); + +add_task(async function test_setPref_types_restore() { + const featureCleanup = ExperimentTestUtils.addTestFeatures(TYPED_FEATURE); + + const json = { + foo: "foo", + bar: 12345, + baz: true, + qux: null, + quux: ["corge"], + }; + + { + const store = ExperimentFakes.store(); + const manager = ExperimentFakes.manager(store); + + await manager.onStartup(); + await assertEmptyStore(store); + + await ExperimentFakes.enrollWithFeatureConfig( + { + featureId: TYPED_FEATURE.featureId, + value: { + string: "hello, world", + int: 12345, + boolean: true, + json, + }, + }, + { manager } + ); + + store._store.saveSoon(); + await store._store.finalize(); + + for (const varDef of Object.values(TYPED_FEATURE.manifest.variables)) { + Services.prefs.deleteBranch(varDef.setPref.pref); + } + + removePrefObservers(manager); + assertNoObservers(manager); + } + + const store = ExperimentFakes.store(); + const manager = ExperimentFakes.manager(store); + + await manager.onStartup(); + + const defaultBranch = Services.prefs.getDefaultBranch(null); + Assert.equal( + defaultBranch.getPrefType("nimbus.test-only.types.string"), + Services.prefs.PREF_STRING + ); + Assert.equal( + defaultBranch.getStringPref("nimbus.test-only.types.string"), + "hello, world" + ); + + Assert.equal( + defaultBranch.getPrefType("nimbus.test-only.types.int"), + Services.prefs.PREF_INT + ); + Assert.equal(defaultBranch.getIntPref("nimbus.test-only.types.int"), 12345); + + Assert.equal( + defaultBranch.getPrefType("nimbus.test-only.types.boolean"), + Services.prefs.PREF_BOOL + ); + Assert.equal( + defaultBranch.getBoolPref("nimbus.test-only.types.boolean"), + true + ); + + Assert.equal( + defaultBranch.getPrefType("nimbus.test-only.types.json"), + Services.prefs.PREF_STRING + ); + + const jsonPrefValue = JSON.parse( + defaultBranch.getStringPref("nimbus.test-only.types.json") + ); + + Assert.deepEqual(json, jsonPrefValue); + + const enrollment = store.getExperimentForFeature(TYPED_FEATURE.featureId); + manager.unenroll(enrollment.slug); + store._deleteForTests(enrollment.slug); + + await assertEmptyStore(store, { cleanup: true }); + featureCleanup(); +}); diff --git a/toolkit/components/normandy/lib/RecipeRunner.sys.mjs b/toolkit/components/normandy/lib/RecipeRunner.sys.mjs index f9578b37d2..087e4ed51e 100644 --- a/toolkit/components/normandy/lib/RecipeRunner.sys.mjs +++ b/toolkit/components/normandy/lib/RecipeRunner.sys.mjs @@ -197,7 +197,7 @@ export var RecipeRunner = { }, checkPrefs() { - if (!Services.prefs.getBoolPref(SHIELD_ENABLED_PREF)) { + if (!Services.prefs.getBoolPref(SHIELD_ENABLED_PREF, false)) { log.debug( `Disabling Shield because ${SHIELD_ENABLED_PREF} is set to false` ); @@ -205,7 +205,7 @@ export var RecipeRunner = { return; } - const apiUrl = Services.prefs.getCharPref(API_URL_PREF); + const apiUrl = Services.prefs.getCharPref(API_URL_PREF, ""); if (!apiUrl) { log.warn(`Disabling Shield because ${API_URL_PREF} is not set.`); this.disable(); @@ -287,7 +287,7 @@ export var RecipeRunner = { // Run once every `runInterval` wall-clock seconds. This is managed by setting a "last ran" // timestamp, and running if it is more than `runInterval` seconds ago. Even with very short // intervals, the timer will only fire at most once every few minutes. - const runInterval = Services.prefs.getIntPref(RUN_INTERVAL_PREF); + const runInterval = Services.prefs.getIntPref(RUN_INTERVAL_PREF, 21600); // 6h lazy.timerManager.registerTimer(TIMER_NAME, () => this.run(), runInterval); }, @@ -597,7 +597,7 @@ export var RecipeRunner = { * executed in the browser console. */ async testRun(baseApiUrl) { - const oldApiUrl = Services.prefs.getCharPref(API_URL_PREF); + const oldApiUrl = Services.prefs.getCharPref(API_URL_PREF, ""); Services.prefs.setCharPref(API_URL_PREF, baseApiUrl); try { diff --git a/toolkit/components/normandy/test/unit/test_NormandyApi.js b/toolkit/components/normandy/test/unit/test_NormandyApi.js index 5b0ede1701..cd15aff965 100644 --- a/toolkit/components/normandy/test/unit/test_NormandyApi.js +++ b/toolkit/components/normandy/test/unit/test_NormandyApi.js @@ -195,7 +195,7 @@ decorate_task( // response that sets a cookie. // send a request, to store a cookie in the cookie store - await fetch(serverUrl); + await fetch(serverUrl, { credentials: "same-origin" }); // A normal request should send that cookie const cookieExpectedDeferred = Promise.withResolvers(); @@ -218,7 +218,7 @@ decorate_task( cookieExpectedDeferred.resolve(); } Services.obs.addObserver(cookieExpectedObserver, "http-on-modify-request"); - await fetch(serverUrl); + await fetch(serverUrl, { credentials: "same-origin" }); await cookieExpectedDeferred.promise; // A request through the NormandyApi method should not send that cookie diff --git a/toolkit/components/normandy/test/unit/test_RecipeRunner.js b/toolkit/components/normandy/test/unit/test_RecipeRunner.js index 710ac4d507..cfc83714f1 100644 --- a/toolkit/components/normandy/test/unit/test_RecipeRunner.js +++ b/toolkit/components/normandy/test/unit/test_RecipeRunner.js @@ -12,6 +12,9 @@ const { RecipeRunner } = ChromeUtils.importESModule( // Test that new build IDs trigger immediate recipe runs add_task(async () => { + // This test assumes normandy is enabled. + Services.prefs.setBoolPref("app.normandy.enabled", true); + updateAppInfo({ appBuildID: "new-build-id", lastAppBuildID: "old-build-id", diff --git a/toolkit/components/passwordmgr/LoginAutoComplete.sys.mjs b/toolkit/components/passwordmgr/LoginAutoComplete.sys.mjs index 411d9249db..187caf9d62 100644 --- a/toolkit/components/passwordmgr/LoginAutoComplete.sys.mjs +++ b/toolkit/components/passwordmgr/LoginAutoComplete.sys.mjs @@ -7,7 +7,10 @@ */ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; -import { GenericAutocompleteItem } from "resource://gre/modules/FillHelpers.sys.mjs"; +import { + GenericAutocompleteItem, + sendFillRequestToParent, +} from "resource://gre/modules/FillHelpers.sys.mjs"; const lazy = {}; @@ -487,7 +490,7 @@ export class LoginAutoComplete { * @param {string} aSearchString The value typed in the field. * @param {nsIAutoCompleteResult} aPreviousResult * @param {HTMLInputElement} aElement - * @param {nsIFormAutoCompleteObserver} aCallback + * @param {nsIFormFillCompleteObserver} aCallback */ startSearch(aSearchString, aPreviousResult, aElement, aCallback) { let { isNullPrincipal } = aElement.nodePrincipal; @@ -717,7 +720,6 @@ export class LoginAutoComplete { let gAutoCompleteListener = { added: false, - fillRequestId: 0, init() { if (!this.added) { @@ -729,45 +731,11 @@ let gAutoCompleteListener = { async observe(subject, topic, data) { switch (topic) { case "autocomplete-will-enter-text": { - await this.sendFillRequestToLoginManagerParent(subject, data); + if (subject && subject == lazy.formFillController.controller.input) { + await sendFillRequestToParent("LoginManager", subject, data); + } break; } } }, - - async sendFillRequestToLoginManagerParent(input, comment) { - if (!comment) { - return; - } - - if (input != lazy.formFillController.controller.input) { - return; - } - - const { fillMessageName, fillMessageData } = JSON.parse(comment ?? "{}"); - if (!fillMessageName) { - return; - } - - this.fillRequestId++; - const fillRequestId = this.fillRequestId; - const child = lazy.LoginManagerChild.forWindow( - input.focusedInput.ownerGlobal - ); - const value = await child.sendQuery(fillMessageName, fillMessageData ?? {}); - - // skip fill if another fill operation started during await - if (fillRequestId != this.fillRequestId) { - return; - } - - if (typeof value !== "string") { - return; - } - - // If LoginManagerParent returned a string to fill, we must do it here because - // nsAutoCompleteController.cpp already finished it's work before we finished await. - input.textValue = value; - input.selectTextRange(value.length, value.length); - }, }; diff --git a/toolkit/components/passwordmgr/LoginManagerAuthPrompter.sys.mjs b/toolkit/components/passwordmgr/LoginManagerAuthPrompter.sys.mjs index f56ec80d5e..8cc501b79d 100644 --- a/toolkit/components/passwordmgr/LoginManagerAuthPrompter.sys.mjs +++ b/toolkit/components/passwordmgr/LoginManagerAuthPrompter.sys.mjs @@ -711,7 +711,7 @@ LoginManagerAuthPrompter.prototype = { ok = await Services.prompt.asyncPromptAuth( this._browser?.browsingContext, - LoginManagerAuthPrompter.promptAuthModalType, + Ci.nsIPrompt.MODAL_TYPE_TAB, aChannel, aLevel, aAuthInfo @@ -1115,10 +1115,3 @@ ChromeUtils.defineLazyGetter(LoginManagerAuthPrompter.prototype, "log", () => { let logger = lazy.LoginHelper.createLogger("LoginManagerAuthPrompter"); return logger.log.bind(logger); }); - -XPCOMUtils.defineLazyPreferenceGetter( - LoginManagerAuthPrompter, - "promptAuthModalType", - "prompts.modalType.httpAuth", - Services.prompt.MODAL_TYPE_WINDOW -); diff --git a/toolkit/components/passwordmgr/LoginManagerChild.sys.mjs b/toolkit/components/passwordmgr/LoginManagerChild.sys.mjs index c89852b56c..66f45416f8 100644 --- a/toolkit/components/passwordmgr/LoginManagerChild.sys.mjs +++ b/toolkit/components/passwordmgr/LoginManagerChild.sys.mjs @@ -2435,15 +2435,7 @@ export class LoginManagerChild extends JSWindowActorChild { if (isSubmission) { // Notify `PasswordManager:onFormSubmit` as long as we detect submission event on a // valid form with a password field. - this.sendAsyncMessage( - "PasswordManager:onFormSubmit", - {}, - { - fields, - isSubmission, - triggeredByFillingGenerated, - } - ); + this.sendAsyncMessage("PasswordManager:onFormSubmit", {}); } } diff --git a/toolkit/components/passwordmgr/nsILoginAutoCompleteSearch.idl b/toolkit/components/passwordmgr/nsILoginAutoCompleteSearch.idl index 6c0e94b445..586292d04d 100644 --- a/toolkit/components/passwordmgr/nsILoginAutoCompleteSearch.idl +++ b/toolkit/components/passwordmgr/nsILoginAutoCompleteSearch.idl @@ -5,7 +5,7 @@ #include "nsISupports.idl" interface nsIAutoCompleteResult; -interface nsIFormAutoCompleteObserver; +interface nsIFormFillCompleteObserver; webidl HTMLInputElement; @@ -22,7 +22,7 @@ interface nsILoginAutoCompleteSearch : nsISupports { void startSearch(in AString aSearchString, in nsIAutoCompleteResult aPreviousResult, in HTMLInputElement aElement, - in nsIFormAutoCompleteObserver aListener); + in nsIFormFillCompleteObserver aListener); /** * Stop a previously-started search. diff --git a/toolkit/components/passwordmgr/test/LoginTestUtils.sys.mjs b/toolkit/components/passwordmgr/test/LoginTestUtils.sys.mjs index 94e48aac6a..c16a44735e 100644 --- a/toolkit/components/passwordmgr/test/LoginTestUtils.sys.mjs +++ b/toolkit/components/passwordmgr/test/LoginTestUtils.sys.mjs @@ -67,6 +67,13 @@ export const LoginTestUtils = { return Services.logins.addLoginAsync(login); }, + /** + * Removes a login from the store + */ + async removeLogin(login) { + return Services.logins.removeLogin(login); + }, + async modifyLogin(oldLogin, newLogin) { const storageChangedPromise = TestUtils.topicObserved( "passwordmgr-storage-changed", diff --git a/toolkit/components/passwordmgr/test/browser/browser_basicAuth_multiTab.js b/toolkit/components/passwordmgr/test/browser/browser_basicAuth_multiTab.js index 0069c653a5..2115272abe 100644 --- a/toolkit/components/passwordmgr/test/browser/browser_basicAuth_multiTab.js +++ b/toolkit/components/passwordmgr/test/browser/browser_basicAuth_multiTab.js @@ -88,13 +88,6 @@ async function testTabAuthed(expectAuthed, { tab, loadPromise, authOptions }) { ); } -add_setup(async function () { - await SpecialPowers.pushPrefEnv({ - // This test relies on tab auth prompts. - set: [["prompts.modalType.httpAuth", Services.prompt.MODAL_TYPE_TAB]], - }); -}); - add_task(async function test() { let tabA = await openTabWithAuthPrompt(ORIGIN1, { user: "userA", diff --git a/toolkit/components/passwordmgr/test/browser/browser_basicAuth_rateLimit.js b/toolkit/components/passwordmgr/test/browser/browser_basicAuth_rateLimit.js index b0a5b8b335..214ae5d299 100644 --- a/toolkit/components/passwordmgr/test/browser/browser_basicAuth_rateLimit.js +++ b/toolkit/components/passwordmgr/test/browser/browser_basicAuth_rateLimit.js @@ -5,12 +5,10 @@ // This tests that the basic auth dialog can not be used for DOS attacks // and that the protections are reset on user-initiated navigation/reload. -let promptModalType = Services.prefs.getIntPref("prompts.modalType.httpAuth"); - function promiseAuthWindowShown() { return PromptTestUtils.handleNextPrompt( window, - { modalType: promptModalType, promptType: "promptUserAndPass" }, + { modalType: Ci.nsIPrompt.MODAL_TYPE_TAB, promptType: "promptUserAndPass" }, { buttonNumClick: 1 } ); } diff --git a/toolkit/components/passwordmgr/test/browser/browser_basicAuth_switchTab.js b/toolkit/components/passwordmgr/test/browser/browser_basicAuth_switchTab.js index 8fddea4c93..8e993388e7 100644 --- a/toolkit/components/passwordmgr/test/browser/browser_basicAuth_switchTab.js +++ b/toolkit/components/passwordmgr/test/browser/browser_basicAuth_switchTab.js @@ -2,14 +2,12 @@ * 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/. */ -let modalType = Services.prefs.getIntPref("prompts.modalType.httpAuth"); - -add_task(async function test() { +add_task(async function test_auth_switchtab() { let tab = BrowserTestUtils.addTab(gBrowser); isnot(tab, gBrowser.selectedTab, "New tab shouldn't be selected"); let authPromptShown = PromptTestUtils.waitForPrompt(tab.linkedBrowser, { - modalType, + modalType: Ci.nsIPrompt.MODAL_TYPE_TAB, promptType: "promptUserAndPass", }); diff --git a/toolkit/components/passwordmgr/test/browser/browser_doorhanger_form_password_edit.js b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_form_password_edit.js index 23afd2c6ab..ad91270eb5 100644 --- a/toolkit/components/passwordmgr/test/browser/browser_doorhanger_form_password_edit.js +++ b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_form_password_edit.js @@ -30,7 +30,7 @@ let testCases = [ doorhanger: { type: "password-save", dismissed: true, - anchorExtraAttr: "", + anchorExtraAttr: null, username: "", password: "abcXYZ", toggle: "visible", @@ -56,7 +56,7 @@ let testCases = [ doorhanger: { type: "password-save", dismissed: true, - anchorExtraAttr: "", + anchorExtraAttr: null, username: "", password: "pass-changed", toggle: "visible", @@ -80,7 +80,7 @@ let testCases = [ doorhanger: { type: "password-change", dismissed: true, - anchorExtraAttr: "", + anchorExtraAttr: null, username: "user1", password: "autopass-changed", }, @@ -104,7 +104,7 @@ let testCases = [ doorhanger: { type: "password-save", dismissed: true, - anchorExtraAttr: "", + anchorExtraAttr: null, username: "user2", password: "pass2", toggle: "visible", @@ -147,7 +147,7 @@ let testCases = [ doorhanger: { type: "password-save", dismissed: true, - anchorExtraAttr: "", + anchorExtraAttr: null, username: "user2", password: "pass1", toggle: "visible", @@ -174,7 +174,7 @@ let testCases = [ doorhanger: { type: "password-change", dismissed: true, - anchorExtraAttr: "", + anchorExtraAttr: null, username: "user-saved", password: "pass2", toggle: "visible", @@ -198,7 +198,7 @@ let testCases = [ doorhanger: { type: "password-change", dismissed: true, - anchorExtraAttr: "", + anchorExtraAttr: null, username: "user1", password: "pass1", toggle: "visible", @@ -244,7 +244,7 @@ let testCases = [ doorhanger: { type: "password-save", dismissed: true, - anchorExtraAttr: "", + anchorExtraAttr: null, username: "", password: "a", toggle: "visible", @@ -271,7 +271,7 @@ let testCases = [ doorhanger: { type: "password-save", dismissed: true, - anchorExtraAttr: "", + anchorExtraAttr: null, username: "", password: "abc", toggle: "visible", @@ -296,7 +296,7 @@ let testCases = [ doorhanger: { type: "password-change", dismissed: true, - anchorExtraAttr: "", + anchorExtraAttr: null, username: "", password: "pass", toggle: "visible", diff --git a/toolkit/components/passwordmgr/test/browser/browser_doorhanger_generated_password.js b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_generated_password.js index 798337ddda..dd3a1a216e 100644 --- a/toolkit/components/passwordmgr/test/browser/browser_doorhanger_generated_password.js +++ b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_generated_password.js @@ -481,7 +481,7 @@ add_task(async function autocomplete_generated_password_saved_empty_username() { info("Waiting to openAndVerifyDoorhanger"); await openAndVerifyDoorhanger(browser, "password-change", { dismissed: true, - anchorExtraAttr: "", + anchorExtraAttr: null, usernameValue: "", passwordLength: LoginTestUtils.generation.LENGTH, }); @@ -493,7 +493,7 @@ add_task(async function autocomplete_generated_password_saved_empty_username() { await submitForm(browser); let notif = await openAndVerifyDoorhanger(browser, "password-change", { dismissed: false, - anchorExtraAttr: "", + anchorExtraAttr: null, usernameValue: "", passwordLength: LoginTestUtils.generation.LENGTH, }); @@ -648,7 +648,7 @@ add_task(async function ac_gen_pw_saved_empty_un_stored_non_empty_un_in_form() { info("Waiting to openAndVerifyDoorhanger"); await openAndVerifyDoorhanger(browser, "password-save", { dismissed: true, - anchorExtraAttr: "", + anchorExtraAttr: null, usernameValue: "myusername", passwordLength: LoginTestUtils.generation.LENGTH, }); @@ -660,7 +660,7 @@ add_task(async function ac_gen_pw_saved_empty_un_stored_non_empty_un_in_form() { await submitForm(browser); let notif = await openAndVerifyDoorhanger(browser, "password-save", { dismissed: false, - anchorExtraAttr: "", + anchorExtraAttr: null, usernameValue: "myusername", passwordLength: LoginTestUtils.generation.LENGTH, }); @@ -719,7 +719,7 @@ add_task(async function contextfill_generated_password_saved_empty_username() { info("Waiting to openAndVerifyDoorhanger"); await openAndVerifyDoorhanger(browser, "password-change", { dismissed: true, - anchorExtraAttr: "", + anchorExtraAttr: null, usernameValue: "", passwordLength: LoginTestUtils.generation.LENGTH, }); @@ -731,7 +731,7 @@ add_task(async function contextfill_generated_password_saved_empty_username() { await submitForm(browser); let notif = await openAndVerifyDoorhanger(browser, "password-change", { dismissed: false, - anchorExtraAttr: "", + anchorExtraAttr: null, usernameValue: "", passwordLength: LoginTestUtils.generation.LENGTH, }); @@ -789,7 +789,7 @@ async function autocomplete_generated_password_edited_no_auto_save( info("Waiting to openAndVerifyDoorhanger"); let notif = await openAndVerifyDoorhanger(browser, "password-change", { dismissed: true, - anchorExtraAttr: "", + anchorExtraAttr: null, usernameValue: "", passwordLength: LoginTestUtils.generation.LENGTH, }); @@ -812,7 +812,7 @@ async function autocomplete_generated_password_edited_no_auto_save( info("Waiting to openAndVerifyDoorhanger"); notif = await openAndVerifyDoorhanger(browser, "password-change", { dismissed: true, - anchorExtraAttr: "", + anchorExtraAttr: null, usernameValue: "", passwordLength: LoginTestUtils.generation.LENGTH + 2, }); @@ -836,7 +836,7 @@ async function autocomplete_generated_password_edited_no_auto_save( await submitForm(browser); notif = await openAndVerifyDoorhanger(browser, "password-change", { dismissed: false, - anchorExtraAttr: "", + anchorExtraAttr: null, usernameValue: "", passwordLength: LoginTestUtils.generation.LENGTH + 2, }); @@ -957,7 +957,7 @@ add_task(async function contextmenu_fill_generated_password_and_set_username() { await submitForm(browser); let notif = await openAndVerifyDoorhanger(browser, "password-change", { dismissed: false, - anchorExtraAttr: "", + anchorExtraAttr: null, usernameValue: "differentuser", passwordLength: LoginTestUtils.generation.LENGTH, }); @@ -1386,7 +1386,7 @@ add_task(async function autosaved_login_updated_to_existing_login_onsubmit() { await waitForDoorhanger(browser, "password-change"); notif = await openAndVerifyDoorhanger(browser, "password-change", { dismissed: false, - anchorExtraAttr: "", + anchorExtraAttr: null, usernameValue: "user1", password: autoSavedLogin.password, }); @@ -1611,7 +1611,7 @@ add_task(async function form_change_from_autosaved_login_to_existing_login() { // the previous doorhanger would have old values, verify it was updated/replaced with new values from the form notif = await openAndVerifyDoorhanger(browser, "password-change", { dismissed: true, - anchorExtraAttr: "", + anchorExtraAttr: null, usernameValue: user1LoginSnapshot.username, passwordLength: user1LoginSnapshot.password.length, }); @@ -1818,7 +1818,7 @@ add_task(async function form_edit_username_and_password_of_generated_login() { info("Verifying the doorhanger"); notif = await openAndVerifyDoorhanger(browser, "password-change", { dismissed: true, - anchorExtraAttr: expectedConfirmation ? "attention" : "", + anchorExtraAttr: expectedConfirmation ? "attention" : null, usernameValue: expectedDoorhangerUsername, passwordLength: expectedDoorhangerPassword.length, }); @@ -1835,7 +1835,7 @@ add_task(async function form_edit_username_and_password_of_generated_login() { await passwordChangeDoorhangerPromise; notif = await openAndVerifyDoorhanger(browser, "password-change", { dismissed: false, - anchorExtraAttr: "", + anchorExtraAttr: null, usernameValue: "someuser", passwordLength: LoginTestUtils.generation.LENGTH + 2, }); diff --git a/toolkit/components/passwordmgr/test/browser/browser_entry_point_telemetry.js b/toolkit/components/passwordmgr/test/browser/browser_entry_point_telemetry.js index 9241f18612..2fbca0381b 100644 --- a/toolkit/components/passwordmgr/test/browser/browser_entry_point_telemetry.js +++ b/toolkit/components/passwordmgr/test/browser/browser_entry_point_telemetry.js @@ -68,7 +68,7 @@ add_task(async function pageInfo_entryPoint() { }, async function (_browser) { info("pageInfo_entryPoint, opening pageinfo"); - let pageInfo = BrowserPageInfo(TEST_ORIGIN, "securityTab", {}); + let pageInfo = BrowserCommands.pageInfo(TEST_ORIGIN, "securityTab", {}); await BrowserTestUtils.waitForEvent(pageInfo, "page-info-init"); info( "pageInfo_entryPoint, got pageinfo, wait until password button is visible" diff --git a/toolkit/components/passwordmgr/test/browser/browser_private_window.js b/toolkit/components/passwordmgr/test/browser/browser_private_window.js index 31fe82cf8b..8e1e22c20f 100644 --- a/toolkit/components/passwordmgr/test/browser/browser_private_window.js +++ b/toolkit/components/passwordmgr/test/browser/browser_private_window.js @@ -77,7 +77,7 @@ async function loadAccessRestrictedURL(browser, url, username, password) { // Wait for the auth prompt, enter the login details and close the prompt await PromptTestUtils.handleNextPrompt( browser, - { modalType: authPromptModalType, promptType: "promptUserAndPass" }, + { modalType: Ci.nsIPrompt.MODAL_TYPE_TAB, promptType: "promptUserAndPass" }, { buttonNumClick: 0, loginInput: username, passwordInput: password } ); @@ -106,13 +106,11 @@ const authUrl = `https://example.com/${DIRECTORY_PATH}authenticate.sjs`; let normalWin; let privateWin; -let authPromptModalType; // XXX: Note that tasks are currently run in sequence. Some tests may assume the state // resulting from successful or unsuccessful logins in previous tasks add_task(async function test_setup() { - authPromptModalType = Services.prefs.getIntPref("prompts.modalType.httpAuth"); normalWin = await BrowserTestUtils.openNewBrowserWindow({ private: false }); privateWin = await BrowserTestUtils.openNewBrowserWindow({ private: true }); Services.logins.removeAllUserFacingLogins(); diff --git a/toolkit/components/passwordmgr/test/browser/browser_proxyAuth_prompt.js b/toolkit/components/passwordmgr/test/browser/browser_proxyAuth_prompt.js index dfa78ff28e..6dfe7d4021 100644 --- a/toolkit/components/passwordmgr/test/browser/browser_proxyAuth_prompt.js +++ b/toolkit/components/passwordmgr/test/browser/browser_proxyAuth_prompt.js @@ -60,10 +60,6 @@ function initProxy() { } add_setup(async function () { - await SpecialPowers.pushPrefEnv({ - // This test relies on tab auth prompts. - set: [["prompts.modalType.httpAuth", Services.prompt.MODAL_TYPE_TAB]], - }); proxyChannel = await initProxy(); }); diff --git a/toolkit/components/passwordmgr/test/mochitest/pwmgr_common.js b/toolkit/components/passwordmgr/test/mochitest/pwmgr_common.js index 1494c60bbd..1e02e6a368 100644 --- a/toolkit/components/passwordmgr/test/mochitest/pwmgr_common.js +++ b/toolkit/components/passwordmgr/test/mochitest/pwmgr_common.js @@ -25,11 +25,6 @@ const { LENGTH: GENERATED_PASSWORD_LENGTH, REGEX: GENERATED_PASSWORD_REGEX } = const LOGIN_FIELD_UTILS = LoginTestUtils.loginField; const TESTS_DIR = "/tests/toolkit/components/passwordmgr/test/"; -// Depending on pref state we either show auth prompts as windows or on tab level. -let authPromptModalType = SpecialPowers.Services.prefs.getIntPref( - "prompts.modalType.httpAuth" -); - /** * Recreate a DOM tree using the outerHTML to ensure that any event listeners * and internal state for the elements are removed. @@ -1051,6 +1046,23 @@ SimpleTest.registerCleanupFunction(() => { }); }); +// This is a version of LoginHelper.loginToVanillaObject that is adapted to run +// as content JS instead of chrome JS. This is needed to make it return a +// content JS object because the structured cloning we use to send it over +// JS IPC can't deal with a cross compartment wrapper. +function loginToVanillaObject(login) { + let obj = {}; + for (let i in SpecialPowers.do_QueryInterface( + login, + SpecialPowers.Ci.nsILoginMetaInfo + )) { + if (typeof login[i] !== "function") { + obj[i] = login[i]; + } + } + return obj; +} + /** * Proxy for Services.logins (nsILoginManager). * Only supports arguments which support structured clone plus {nsILoginInfo} @@ -1067,7 +1079,7 @@ this.LoginManager = new Proxy( SpecialPowers.call_Instanceof(val, SpecialPowers.Ci.nsILoginInfo) ) { loginInfoIndices.push(index); - return LoginHelper.loginToVanillaObject(val); + return loginToVanillaObject(val); } return val; diff --git a/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_form_removal.html b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_form_removal.html index b2ff2d8845..83e2f1d3c6 100644 --- a/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_form_removal.html +++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_form_removal.html @@ -161,7 +161,7 @@ const TESTCASES = [ }, /* XXX - Bug 1698498 : - This test case fails because when we call querySelector in LoginRecipes.jsm + This test case fails because when we call querySelector in LoginRecipes.sys.mjs after the form is removed, querySelector can't find the element. { document: `<!-- recipe field override --> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt_async.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt_async.html index 1edbf8cf18..84f8c4aafb 100644 --- a/toolkit/components/passwordmgr/test/mochitest/test_prompt_async.html +++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_async.html @@ -20,8 +20,8 @@ const EXAMPLE_ORG = "http://example.org/tests/toolkit/components/passwordmgr/test/mochitest/"; let mozproxyOrigin; - // Let prompt_common know what kind of modal type is enabled for auth prompts. - modalType = authPromptModalType; + // Let prompt_common know what kind of modal type is used for auth prompts. + modalType = Ci.nsIPrompt.MODAL_TYPE_TAB; // These are magically defined on the window due to the iframe IDs /* global iframe1, iframe2a, iframe2b */ diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt_http.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt_http.html index f6f6e681dc..f008950c2b 100644 --- a/toolkit/components/passwordmgr/test/mochitest/test_prompt_http.html +++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_http.html @@ -19,8 +19,8 @@ <script class="testbody" type="text/javascript"> var iframe = document.getElementById("iframe"); -// Let prompt_common know what kind of modal type is enabled for auth prompts. -modalType = authPromptModalType; +// Let prompt_common know what kind of modal type is used for auth prompts. +modalType = Ci.nsIPrompt.MODAL_TYPE_TAB; const AUTHENTICATE_PATH = new URL("authenticate.sjs", window.location.href).pathname; diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt_noWindow.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt_noWindow.html index 19d05e47e5..0ece6f306c 100644 --- a/toolkit/components/passwordmgr/test/mochitest/test_prompt_noWindow.html +++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_noWindow.html @@ -17,8 +17,8 @@ <pre id="test"> <script class="testbody" type="text/javascript"> -// Let prompt_common know what kind of modal type is enabled for auth prompts. -modalType = authPromptModalType; +// Let prompt_common know what kind of modal type is used for auth prompts. +modalType = Ci.nsIPrompt.MODAL_TYPE_TAB; add_setup(async () => { await setStoredLoginsAsync( diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth.html index b82ea67faa..2ce85a61a9 100644 --- a/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth.html +++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth.html @@ -27,8 +27,8 @@ const authinfo = { realm: "", }; -// Let prompt_common know what kind of modal type is enabled for auth prompts. -modalType = authPromptModalType; +// Let prompt_common know what kind of modal type is used for auth prompts. +modalType = Ci.nsIPrompt.MODAL_TYPE_TAB; const prompterParent = runInParent(() => { const promptFac = Cc["@mozilla.org/passwordmanager/authpromptfactory;1"]. diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth_proxy.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth_proxy.html index 75342cd4cb..e5e77d0bcc 100644 --- a/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth_proxy.html +++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth_proxy.html @@ -31,8 +31,8 @@ let proxyAuthinfo = { realm: "", }; -// Let prompt_common know what kind of modal type is enabled for auth prompts. -modalType = authPromptModalType; +// Let prompt_common know what kind of modal type is used for auth prompts. +modalType = Ci.nsIPrompt.MODAL_TYPE_TAB; let chromeScript = runInParent(() => { const promptFac = Cc[ diff --git a/toolkit/components/passwordmgr/test/mochitest/test_xhr.html b/toolkit/components/passwordmgr/test/mochitest/test_xhr.html index 4461195359..b481099314 100644 --- a/toolkit/components/passwordmgr/test/mochitest/test_xhr.html +++ b/toolkit/components/passwordmgr/test/mochitest/test_xhr.html @@ -40,8 +40,8 @@ function xhrLoad(xmlDoc) { return {username, password, authok}; } -// Let prompt_common know what kind of modal type is enabled for auth prompts. -modalType = authPromptModalType; +// Let prompt_common know what kind of modal type is used for auth prompts. +modalType = Ci.nsIPrompt.MODAL_TYPE_TAB; let prompterParent = runInParent(() => { const promptFac = Cc["@mozilla.org/passwordmanager/authpromptfactory;1"]. @@ -120,17 +120,6 @@ add_task(async function test2() { defButton: "button0", }; - // For window prompts check that the dialog is modal, chrome and dependent; - // We can't just check window.opener because that'll be - // a content window, which therefore isn't exposed (it'll lie and - // be null). - if (authPromptModalType === SpecialPowers.Services.prompt.MODAL_TYPE_WINDOW) { - state.chrome = true; - state.dialog = true; - state.chromeDependent = true; - state.isWindowModal = true; - } - let action = { buttonClick: "ok", }; diff --git a/toolkit/components/passwordmgr/test/unit/test_PasswordRulesManager_generatePassword.js b/toolkit/components/passwordmgr/test/unit/test_PasswordRulesManager_generatePassword.js index 88520769cf..43717f36c5 100644 --- a/toolkit/components/passwordmgr/test/unit/test_PasswordRulesManager_generatePassword.js +++ b/toolkit/components/passwordmgr/test/unit/test_PasswordRulesManager_generatePassword.js @@ -238,7 +238,7 @@ add_task( const TEST_BASE_ORIGIN = "example.com"; const REQUIRED_ARBITRARY_CHARACTERS = "!#$@*()_+="; // We use an extremely long password to ensure there are no invalid characters generated in the password. - // This ensures we exhaust all of "allRequiredCharacters" in PasswordGenerator.jsm. + // This ensures we exhaust all of "allRequiredCharacters" in PasswordGenerator.sys.mjs. // Otherwise, there's a small chance a "," may have been added to "allRequiredCharacters" // which will generate an invalid password in this case. const TEST_RULES = `required: [${REQUIRED_ARBITRARY_CHARACTERS}], upper, lower; maxlength: 255; minlength: 255;`; diff --git a/toolkit/components/pdfjs/content/PdfJs.sys.mjs b/toolkit/components/pdfjs/content/PdfJs.sys.mjs index fd6757737e..430a153ae2 100644 --- a/toolkit/components/pdfjs/content/PdfJs.sys.mjs +++ b/toolkit/components/pdfjs/content/PdfJs.sys.mjs @@ -242,7 +242,7 @@ export var PdfJs = { }, // nsIObserver - observe(aSubject, aTopic, aData) { + observe() { this.checkIsDefault(); }, }; diff --git a/toolkit/components/pdfjs/content/PdfJsNetwork.sys.mjs b/toolkit/components/pdfjs/content/PdfJsNetwork.sys.mjs index dd3ebc6bf5..a36f889539 100644 --- a/toolkit/components/pdfjs/content/PdfJsNetwork.sys.mjs +++ b/toolkit/components/pdfjs/content/PdfJsNetwork.sys.mjs @@ -33,7 +33,7 @@ export class NetworkManager { this.getXhr = args.getXhr || function NetworkManager_getXhr() { - return new XMLHttpRequest(); + return new XMLHttpRequest({ mozAnon: false }); }; this.currXhrId = 0; @@ -79,7 +79,7 @@ export class NetworkManager { xhr.responseType = "arraybuffer"; if (args.onError) { - xhr.onerror = function (evt) { + xhr.onerror = function () { args.onError(xhr.status); }; } @@ -109,7 +109,7 @@ export class NetworkManager { } } - onStateChange(xhrId, evt) { + onStateChange(xhrId) { var pendingRequest = this.pendingRequests[xhrId]; if (!pendingRequest) { // Maybe abortRequest was called... diff --git a/toolkit/components/pdfjs/content/PdfJsTelemetry.sys.mjs b/toolkit/components/pdfjs/content/PdfJsTelemetry.sys.mjs index 16e4e85d58..4724f25c33 100644 --- a/toolkit/components/pdfjs/content/PdfJsTelemetry.sys.mjs +++ b/toolkit/components/pdfjs/content/PdfJsTelemetry.sys.mjs @@ -47,7 +47,7 @@ export class PdfJsTelemetry { } static onTimeToView(ms) { - Glean.pdfjs.timeToView.accumulateSamples([ms]); + Glean.pdfjs.timeToView.accumulateSingleSample(ms); } static onEditing({ type, data }) { @@ -99,9 +99,9 @@ export class PdfJsTelemetry { Glean.pdfjsEditingHighlight.method[data.methodOfCreation].add(1); Glean.pdfjsEditingHighlight.color[data.color].add(1); if (data.type === "free_highlight") { - Glean.pdfjsEditingHighlight.thickness.accumulateSamples([ - data.thickness, - ]); + Glean.pdfjsEditingHighlight.thickness.accumulateSingleSample( + data.thickness + ); } break; case "color_changed": @@ -109,9 +109,9 @@ export class PdfJsTelemetry { Glean.pdfjsEditingHighlight.colorChanged.add(1); break; case "thickness_changed": - Glean.pdfjsEditingHighlight.thickness.accumulateSamples([ - data.thickness, - ]); + Glean.pdfjsEditingHighlight.thickness.accumulateSingleSample( + data.thickness + ); Glean.pdfjsEditingHighlight.thicknessChanged.add(1); break; case "deleted": diff --git a/toolkit/components/pdfjs/content/PdfStreamConverter.sys.mjs b/toolkit/components/pdfjs/content/PdfStreamConverter.sys.mjs index 2a56cbb935..ce9c6898e3 100644 --- a/toolkit/components/pdfjs/content/PdfStreamConverter.sys.mjs +++ b/toolkit/components/pdfjs/content/PdfStreamConverter.sys.mjs @@ -296,7 +296,7 @@ class ChromeActions { } } - download(data, sendResponse) { + download(data) { const { originalUrl, options } = data; const blobUrl = data.blobUrl || originalUrl; let { filename } = data; @@ -361,7 +361,7 @@ class ChromeActions { actor.sendAsyncMessage("PDFJS:Parent:getNimbus"); Services.obs.addObserver( { - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { if (aTopic === "pdfjs-getNimbus") { Services.obs.removeObserver(this, aTopic); sendResponse(aSubject && JSON.stringify(aSubject.wrappedJSObject)); @@ -545,7 +545,7 @@ class RangedChromeActions extends ChromeActions { } }; var getXhr = function getXhr() { - var xhr = new XMLHttpRequest(); + var xhr = new XMLHttpRequest({ mozAnon: false }); xhr.addEventListener("readystatechange", xhr_onreadystatechange); return xhr; }; @@ -790,7 +790,7 @@ PdfStreamConverter.prototype = { */ // nsIStreamConverter::convert - convert(aFromStream, aFromType, aToType, aCtxt) { + convert() { throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); }, @@ -1063,7 +1063,7 @@ PdfStreamConverter.prototype = { // request(aRequest) below so we don't overwrite the original channel and // trigger an assertion. var proxy = { - onStartRequest(request) { + onStartRequest() { listener.onStartRequest(aRequest); }, onDataAvailable(request, inputStream, offset, count) { diff --git a/toolkit/components/pdfjs/content/PdfjsParent.sys.mjs b/toolkit/components/pdfjs/content/PdfjsParent.sys.mjs index a11d68ef02..32bb3d65b7 100644 --- a/toolkit/components/pdfjs/content/PdfjsParent.sys.mjs +++ b/toolkit/components/pdfjs/content/PdfjsParent.sys.mjs @@ -187,7 +187,7 @@ export class PdfjsParent extends JSWindowActorParent { let newBrowser = aEvent.detail; newBrowser.addEventListener( "EndSwapDocShells", - evt => { + () => { this._hookupEventListeners(newBrowser); }, { once: true } diff --git a/toolkit/components/pdfjs/content/build/pdf.mjs b/toolkit/components/pdfjs/content/build/pdf.mjs index 4e51efdb78..dbba618862 100644 --- a/toolkit/components/pdfjs/content/build/pdf.mjs +++ b/toolkit/components/pdfjs/content/build/pdf.mjs @@ -70,7 +70,6 @@ __webpack_require__.d(__webpack_exports__, { PasswordResponses: () => (/* reexport */ PasswordResponses), PermissionFlag: () => (/* reexport */ PermissionFlag), PixelsPerInch: () => (/* reexport */ PixelsPerInch), - PromiseCapability: () => (/* reexport */ PromiseCapability), RenderingCancelledException: () => (/* reexport */ RenderingCancelledException), UnexpectedResponseException: () => (/* reexport */ UnexpectedResponseException), Util: () => (/* reexport */ Util), @@ -789,24 +788,6 @@ function getModificationDate(date = new Date()) { const buffer = [date.getUTCFullYear().toString(), (date.getUTCMonth() + 1).toString().padStart(2, "0"), date.getUTCDate().toString().padStart(2, "0"), date.getUTCHours().toString().padStart(2, "0"), date.getUTCMinutes().toString().padStart(2, "0"), date.getUTCSeconds().toString().padStart(2, "0")]; return buffer.join(""); } -class PromiseCapability { - #settled = false; - constructor() { - this.promise = new Promise((resolve, reject) => { - this.resolve = data => { - this.#settled = true; - resolve(data); - }; - this.reject = reason => { - this.#settled = true; - reject(reason); - }; - }); - } - get settled() { - return this.#settled; - } -} let NormalizeRegex = null; let NormalizationMap = null; function normalizeUnicode(str) { @@ -820,6 +801,17 @@ function getUuid() { return crypto.randomUUID(); } const AnnotationPrefix = "pdfjs_internal_id_"; +const FontRenderOps = { + BEZIER_CURVE_TO: 0, + MOVE_TO: 1, + LINE_TO: 2, + QUADRATIC_CURVE_TO: 3, + RESTORE: 4, + SAVE: 5, + SCALE: 6, + TRANSFORM: 7, + TRANSLATE: 8 +}; ;// CONCATENATED MODULE: ./src/display/base_factory.js @@ -1742,11 +1734,11 @@ class HighlightToolbar { const button = document.createElement("button"); button.className = "highlightButton"; button.tabIndex = 0; - button.setAttribute("data-l10n-id", `pdfjs-highlight-floating-button`); + button.setAttribute("data-l10n-id", `pdfjs-highlight-floating-button1`); const span = document.createElement("span"); button.append(span); span.className = "visuallyHidden"; - span.setAttribute("data-l10n-id", "pdfjs-editor-highlight-button-label"); + span.setAttribute("data-l10n-id", "pdfjs-highlight-floating-button-label"); button.addEventListener("contextmenu", noContextMenu); button.addEventListener("click", () => { this.#uiManager.highlightSelection("floating_button"); @@ -3431,6 +3423,7 @@ class AnnotationEditor { #editToolbar = null; #focusedResizerName = ""; #hasBeenClicked = false; + #initialPosition = null; #isEditing = false; #isInEditMode = false; #isResizerEnabledForKeyboard = false; @@ -3656,12 +3649,14 @@ class AnnotationEditor { this.#translate(this.parentDimensions, x, y); } translateInPage(x, y) { + this.#initialPosition ||= [this.x, this.y]; this.#translate(this.pageDimensions, x, y); this.div.scrollIntoView({ block: "nearest" }); } drag(tx, ty) { + this.#initialPosition ||= [this.x, this.y]; const [parentWidth, parentHeight] = this.parentDimensions; this.x += tx / parentWidth; this.y += ty / parentHeight; @@ -3688,6 +3683,9 @@ class AnnotationEditor { block: "nearest" }); } + get _hasBeenMoved() { + return !!this.#initialPosition && (this.#initialPosition[0] !== this.x || this.#initialPosition[1] !== this.y); + } getBaseTranslation() { const [parentWidth, parentHeight] = this.parentDimensions; const { @@ -4911,7 +4909,6 @@ class FontLoader { } class FontFaceObject { constructor(translatedData, { - isEvalSupported = true, disableFontFace = false, ignoreErrors = false, inspectFont = null @@ -4920,7 +4917,6 @@ class FontFaceObject { for (const i in translatedData) { this[i] = translatedData[i]; } - this.isEvalSupported = isEvalSupported !== false; this.disableFontFace = disableFontFace === true; this.ignoreErrors = ignoreErrors === true; this._inspectFont = inspectFont; @@ -4975,22 +4971,72 @@ class FontFaceObject { throw ex; } warn(`getPathGenerator - ignoring character: "${ex}".`); + } + if (!Array.isArray(cmds) || cmds.length === 0) { return this.compiledGlyphs[character] = function (c, size) {}; } - if (this.isEvalSupported && util_FeatureTest.isEvalSupported) { - const jsBuf = []; - for (const current of cmds) { - const args = current.args !== undefined ? current.args.join(",") : ""; - jsBuf.push("c.", current.cmd, "(", args, ");\n"); + const commands = []; + for (let i = 0, ii = cmds.length; i < ii;) { + switch (cmds[i++]) { + case FontRenderOps.BEZIER_CURVE_TO: + { + const [a, b, c, d, e, f] = cmds.slice(i, i + 6); + commands.push(ctx => ctx.bezierCurveTo(a, b, c, d, e, f)); + i += 6; + } + break; + case FontRenderOps.MOVE_TO: + { + const [a, b] = cmds.slice(i, i + 2); + commands.push(ctx => ctx.moveTo(a, b)); + i += 2; + } + break; + case FontRenderOps.LINE_TO: + { + const [a, b] = cmds.slice(i, i + 2); + commands.push(ctx => ctx.lineTo(a, b)); + i += 2; + } + break; + case FontRenderOps.QUADRATIC_CURVE_TO: + { + const [a, b, c, d] = cmds.slice(i, i + 4); + commands.push(ctx => ctx.quadraticCurveTo(a, b, c, d)); + i += 4; + } + break; + case FontRenderOps.RESTORE: + commands.push(ctx => ctx.restore()); + break; + case FontRenderOps.SAVE: + commands.push(ctx => ctx.save()); + break; + case FontRenderOps.SCALE: + assert(commands.length === 2, "Scale command is only valid at the third position."); + break; + case FontRenderOps.TRANSFORM: + { + const [a, b, c, d, e, f] = cmds.slice(i, i + 6); + commands.push(ctx => ctx.transform(a, b, c, d, e, f)); + i += 6; + } + break; + case FontRenderOps.TRANSLATE: + { + const [a, b] = cmds.slice(i, i + 2); + commands.push(ctx => ctx.translate(a, b)); + i += 2; + } + break; } - return this.compiledGlyphs[character] = new Function("c", "size", jsBuf.join("")); } - return this.compiledGlyphs[character] = function (c, size) { - for (const current of cmds) { - if (current.cmd === "scale") { - current.args = [size, -size]; - } - c[current.cmd].apply(c, current.args); + return this.compiledGlyphs[character] = function glyphDrawer(ctx, size) { + commands[0](ctx); + commands[1](ctx); + ctx.scale(size, -size); + for (let i = 2, ii = commands.length; i < ii; i++) { + commands[i](ctx); } }; } @@ -7951,7 +7997,7 @@ class TextLayerRenderTask { this._reader = null; this._textDivProperties = textDivProperties || new WeakMap(); this._canceled = false; - this._capability = new PromiseCapability(); + this._capability = Promise.withResolvers(); this._layoutTextParams = { prevFontSize: null, prevFontFamily: null, @@ -8019,7 +8065,11 @@ class TextLayerRenderTask { } } _render() { - const capability = new PromiseCapability(); + const { + promise, + resolve, + reject + } = Promise.withResolvers(); let styleCache = Object.create(null); if (this._isReadableStream) { const pump = () => { @@ -8028,13 +8078,13 @@ class TextLayerRenderTask { done }) => { if (done) { - capability.resolve(); + resolve(); return; } Object.assign(styleCache, value.styles); this._processItems(value.items, styleCache); pump(); - }, capability.reject); + }, reject); }; this._reader = this._textContentSource.getReader(); pump(); @@ -8044,11 +8094,11 @@ class TextLayerRenderTask { styles } = this._textContentSource; this._processItems(items, styles); - capability.resolve(); + resolve(); } else { throw new Error('No "textContentSource" parameter specified.'); } - capability.promise.then(() => { + promise.then(() => { styleCache = null; render(this); }, this._capability.reject); @@ -8241,7 +8291,7 @@ class MessageHandler { } sendWithPromise(actionName, data, transfers) { const callbackId = this.callbackId++; - const capability = new PromiseCapability(); + const capability = Promise.withResolvers(); this.callbackCapabilities[callbackId] = capability; try { this.comObj.postMessage({ @@ -8263,7 +8313,7 @@ class MessageHandler { comObj = this.comObj; return new ReadableStream({ start: controller => { - const startCapability = new PromiseCapability(); + const startCapability = Promise.withResolvers(); this.streamControllers[streamId] = { controller, startCall: startCapability, @@ -8282,7 +8332,7 @@ class MessageHandler { return startCapability.promise; }, pull: controller => { - const pullCapability = new PromiseCapability(); + const pullCapability = Promise.withResolvers(); this.streamControllers[streamId].pullCall = pullCapability; comObj.postMessage({ sourceName, @@ -8295,7 +8345,7 @@ class MessageHandler { }, cancel: reason => { assert(reason instanceof Error, "cancel must have a valid reason"); - const cancelCapability = new PromiseCapability(); + const cancelCapability = Promise.withResolvers(); this.streamControllers[streamId].cancelCall = cancelCapability; this.streamControllers[streamId].isClosed = true; comObj.postMessage({ @@ -8324,7 +8374,7 @@ class MessageHandler { const lastDesiredSize = this.desiredSize; this.desiredSize -= size; if (lastDesiredSize > 0 && this.desiredSize <= 0) { - this.sinkCapability = new PromiseCapability(); + this.sinkCapability = Promise.withResolvers(); this.ready = this.sinkCapability.promise; } comObj.postMessage({ @@ -8362,7 +8412,7 @@ class MessageHandler { reason: wrapReason(reason) }); }, - sinkCapability: new PromiseCapability(), + sinkCapability: Promise.withResolvers(), onPull: null, onCancel: null, isCancelled: false, @@ -8975,7 +9025,7 @@ class PDFDataTransportStreamReader { done: true }; } - const requestCapability = new PromiseCapability(); + const requestCapability = Promise.withResolvers(); this._requests.push(requestCapability); return requestCapability.promise; } @@ -9047,7 +9097,7 @@ class PDFDataTransportStreamRangeReader { done: true }; } - const requestCapability = new PromiseCapability(); + const requestCapability = Promise.withResolvers(); this._requests.push(requestCapability); return requestCapability.promise; } @@ -9205,7 +9255,7 @@ function getDocument(src) { } const fetchDocParams = { docId, - apiVersion: "4.1.342", + apiVersion: "4.1.379", data, password, disableAutoFetch, @@ -9228,7 +9278,6 @@ function getDocument(src) { }; const transportParams = { ignoreErrors, - isEvalSupported, disableFontFace, fontExtraProperties, enableXfa, @@ -9294,7 +9343,7 @@ function getDataProp(val) { class PDFDocumentLoadingTask { static #docId = 0; constructor() { - this._capability = new PromiseCapability(); + this._capability = Promise.withResolvers(); this._transport = null; this._worker = null; this.docId = `d${PDFDocumentLoadingTask.#docId++}`; @@ -9335,7 +9384,7 @@ class PDFDataRangeTransport { this._progressListeners = []; this._progressiveReadListeners = []; this._progressiveDoneListeners = []; - this._readyCapability = new PromiseCapability(); + this._readyCapability = Promise.withResolvers(); } addRangeListener(listener) { this._rangeListeners.push(listener); @@ -9588,7 +9637,7 @@ class PDFPageProxy { } const intentPrint = !!(renderingIntent & RenderingIntentFlag.PRINT); if (!intentState.displayReadyCapability) { - intentState.displayReadyCapability = new PromiseCapability(); + intentState.displayReadyCapability = Promise.withResolvers(); intentState.operatorList = { fnArray: [], argsArray: [], @@ -9934,7 +9983,7 @@ class PDFWorker { this.name = name; this.destroyed = false; this.verbosity = verbosity; - this._readyCapability = new PromiseCapability(); + this._readyCapability = Promise.withResolvers(); this._port = null; this._webWorker = null; this._messageHandler = null; @@ -10110,7 +10159,7 @@ class WorkerTransport { this._networkStream = networkStream; this._fullReader = null; this._lastProgress = null; - this.downloadInfoCapability = new PromiseCapability(); + this.downloadInfoCapability = Promise.withResolvers(); this.setupMessageHandler(); } #cacheSimpleMethod(name, data = null) { @@ -10171,7 +10220,7 @@ class WorkerTransport { return this.destroyCapability.promise; } this.destroyed = true; - this.destroyCapability = new PromiseCapability(); + this.destroyCapability = Promise.withResolvers(); this.#passwordCapability?.reject(new Error("Worker was destroyed during onPassword callback")); const waitOn = []; for (const page of this.#pageCache.values()) { @@ -10239,7 +10288,7 @@ class WorkerTransport { }; }); messageHandler.on("ReaderHeadersReady", data => { - const headersCapability = new PromiseCapability(); + const headersCapability = Promise.withResolvers(); const fullReader = this._fullReader; fullReader.headersReady.then(() => { if (!fullReader.isStreamingSupported || !fullReader.isRangeSupported) { @@ -10325,7 +10374,7 @@ class WorkerTransport { loadingTask._capability.reject(reason); }); messageHandler.on("PasswordRequest", exception => { - this.#passwordCapability = new PromiseCapability(); + this.#passwordCapability = Promise.withResolvers(); if (loadingTask.onPassword) { const updatePassword = password => { if (password instanceof Error) { @@ -10378,7 +10427,6 @@ class WorkerTransport { } const inspectFont = params.pdfBug && globalThis.FontInspector?.enabled ? (font, url) => globalThis.FontInspector.fontAdded(font, url) : null; const font = new FontFaceObject(exportedData, { - isEvalSupported: params.isEvalSupported, disableFontFace: params.disableFontFace, ignoreErrors: params.ignoreErrors, inspectFont @@ -10639,34 +10687,35 @@ class WorkerTransport { }); } } +const INITIAL_DATA = Symbol("INITIAL_DATA"); class PDFObjects { #objs = Object.create(null); #ensureObj(objId) { return this.#objs[objId] ||= { - capability: new PromiseCapability(), - data: null + ...Promise.withResolvers(), + data: INITIAL_DATA }; } get(objId, callback = null) { if (callback) { const obj = this.#ensureObj(objId); - obj.capability.promise.then(() => callback(obj.data)); + obj.promise.then(() => callback(obj.data)); return null; } const obj = this.#objs[objId]; - if (!obj?.capability.settled) { + if (!obj || obj.data === INITIAL_DATA) { throw new Error(`Requesting object that isn't resolved yet ${objId}.`); } return obj.data; } has(objId) { const obj = this.#objs[objId]; - return obj?.capability.settled ?? false; + return !!obj && obj.data !== INITIAL_DATA; } resolve(objId, data = null) { const obj = this.#ensureObj(objId); obj.data = data; - obj.capability.resolve(); + obj.resolve(); } clear() { for (const objId in this.#objs) { @@ -10680,10 +10729,9 @@ class PDFObjects { *[Symbol.iterator]() { for (const objId in this.#objs) { const { - capability, data } = this.#objs[objId]; - if (!capability.settled) { + if (data === INITIAL_DATA) { continue; } yield [objId, data]; @@ -10748,7 +10796,7 @@ class InternalRenderTask { this.graphicsReady = false; this._useRequestAnimationFrame = useRequestAnimationFrame === true && typeof window !== "undefined"; this.cancelled = false; - this.capability = new PromiseCapability(); + this.capability = Promise.withResolvers(); this.task = new RenderTask(this); this._cancelBound = this.cancel.bind(this); this._continueBound = this._continue.bind(this); @@ -10849,8 +10897,8 @@ class InternalRenderTask { } } } -const version = "4.1.342"; -const build = "e384df6f1"; +const version = "4.1.379"; +const build = "017e49244"; ;// CONCATENATED MODULE: ./src/shared/scripting_utils.js function makeColorComp(n) { @@ -13708,7 +13756,6 @@ class FreeTextEditor extends AnnotationEditor { } onceAdded() { if (this.width) { - this.#cheatInitialRect(); return; } this.enableEditMode(); @@ -14085,22 +14132,9 @@ class FreeTextEditor extends AnnotationEditor { value, fontSize, color, - rect, pageIndex } = this.#initialData; - return serialized.value !== value || serialized.fontSize !== fontSize || serialized.rect.some((x, i) => Math.abs(x - rect[i]) >= 1) || serialized.color.some((c, i) => c !== color[i]) || serialized.pageIndex !== pageIndex; - } - #cheatInitialRect(delayed = false) { - if (!this.annotationElementId) { - return; - } - this.#setEditorDimensions(); - if (!delayed && (this.width === 0 || this.height === 0)) { - setTimeout(() => this.#cheatInitialRect(true), 0); - return; - } - const padding = FreeTextEditor._internalPadding * this.parentScale; - this.#initialData.rect = this.getRect(padding, padding); + return this._hasBeenMoved || serialized.value !== value || serialized.fontSize !== fontSize || serialized.color.some((c, i) => c !== color[i]) || serialized.pageIndex !== pageIndex; } } @@ -15315,10 +15349,8 @@ class HighlightEditor extends AnnotationEditor { } const div = super.render(); if (this.#text) { - const mark = document.createElement("mark"); - div.append(mark); - mark.append(document.createTextNode(this.#text)); - mark.className = "visuallyHidden"; + div.setAttribute("aria-label", this.#text); + div.setAttribute("role", "mark"); } if (this.#isFreeHighlight) { div.classList.add("free"); @@ -17534,8 +17566,8 @@ class DrawLayer { -const pdfjsVersion = "4.1.342"; -const pdfjsBuild = "e384df6f1"; +const pdfjsVersion = "4.1.379"; +const pdfjsBuild = "017e49244"; var __webpack_exports__AbortException = __webpack_exports__.AbortException; var __webpack_exports__AnnotationEditorLayer = __webpack_exports__.AnnotationEditorLayer; @@ -17561,7 +17593,6 @@ var __webpack_exports__PDFWorker = __webpack_exports__.PDFWorker; var __webpack_exports__PasswordResponses = __webpack_exports__.PasswordResponses; var __webpack_exports__PermissionFlag = __webpack_exports__.PermissionFlag; var __webpack_exports__PixelsPerInch = __webpack_exports__.PixelsPerInch; -var __webpack_exports__PromiseCapability = __webpack_exports__.PromiseCapability; var __webpack_exports__RenderingCancelledException = __webpack_exports__.RenderingCancelledException; var __webpack_exports__UnexpectedResponseException = __webpack_exports__.UnexpectedResponseException; var __webpack_exports__Util = __webpack_exports__.Util; @@ -17583,4 +17614,4 @@ var __webpack_exports__setLayerDimensions = __webpack_exports__.setLayerDimensio var __webpack_exports__shadow = __webpack_exports__.shadow; var __webpack_exports__updateTextLayer = __webpack_exports__.updateTextLayer; var __webpack_exports__version = __webpack_exports__.version; -export { __webpack_exports__AbortException as AbortException, __webpack_exports__AnnotationEditorLayer as AnnotationEditorLayer, __webpack_exports__AnnotationEditorParamsType as AnnotationEditorParamsType, __webpack_exports__AnnotationEditorType as AnnotationEditorType, __webpack_exports__AnnotationEditorUIManager as AnnotationEditorUIManager, __webpack_exports__AnnotationLayer as AnnotationLayer, __webpack_exports__AnnotationMode as AnnotationMode, __webpack_exports__CMapCompressionType as CMapCompressionType, __webpack_exports__ColorPicker as ColorPicker, __webpack_exports__DOMSVGFactory as DOMSVGFactory, __webpack_exports__DrawLayer as DrawLayer, __webpack_exports__FeatureTest as FeatureTest, __webpack_exports__GlobalWorkerOptions as GlobalWorkerOptions, __webpack_exports__ImageKind as ImageKind, __webpack_exports__InvalidPDFException as InvalidPDFException, __webpack_exports__MissingPDFException as MissingPDFException, __webpack_exports__OPS as OPS, __webpack_exports__Outliner as Outliner, __webpack_exports__PDFDataRangeTransport as PDFDataRangeTransport, __webpack_exports__PDFDateString as PDFDateString, __webpack_exports__PDFWorker as PDFWorker, __webpack_exports__PasswordResponses as PasswordResponses, __webpack_exports__PermissionFlag as PermissionFlag, __webpack_exports__PixelsPerInch as PixelsPerInch, __webpack_exports__PromiseCapability as PromiseCapability, __webpack_exports__RenderingCancelledException as RenderingCancelledException, __webpack_exports__UnexpectedResponseException as UnexpectedResponseException, __webpack_exports__Util as Util, __webpack_exports__VerbosityLevel as VerbosityLevel, __webpack_exports__XfaLayer as XfaLayer, __webpack_exports__build as build, __webpack_exports__createValidAbsoluteUrl as createValidAbsoluteUrl, __webpack_exports__fetchData as fetchData, __webpack_exports__getDocument as getDocument, __webpack_exports__getFilenameFromUrl as getFilenameFromUrl, __webpack_exports__getPdfFilenameFromUrl as getPdfFilenameFromUrl, __webpack_exports__getXfaPageViewport as getXfaPageViewport, __webpack_exports__isDataScheme as isDataScheme, __webpack_exports__isPdfFile as isPdfFile, __webpack_exports__noContextMenu as noContextMenu, __webpack_exports__normalizeUnicode as normalizeUnicode, __webpack_exports__renderTextLayer as renderTextLayer, __webpack_exports__setLayerDimensions as setLayerDimensions, __webpack_exports__shadow as shadow, __webpack_exports__updateTextLayer as updateTextLayer, __webpack_exports__version as version }; +export { __webpack_exports__AbortException as AbortException, __webpack_exports__AnnotationEditorLayer as AnnotationEditorLayer, __webpack_exports__AnnotationEditorParamsType as AnnotationEditorParamsType, __webpack_exports__AnnotationEditorType as AnnotationEditorType, __webpack_exports__AnnotationEditorUIManager as AnnotationEditorUIManager, __webpack_exports__AnnotationLayer as AnnotationLayer, __webpack_exports__AnnotationMode as AnnotationMode, __webpack_exports__CMapCompressionType as CMapCompressionType, __webpack_exports__ColorPicker as ColorPicker, __webpack_exports__DOMSVGFactory as DOMSVGFactory, __webpack_exports__DrawLayer as DrawLayer, __webpack_exports__FeatureTest as FeatureTest, __webpack_exports__GlobalWorkerOptions as GlobalWorkerOptions, __webpack_exports__ImageKind as ImageKind, __webpack_exports__InvalidPDFException as InvalidPDFException, __webpack_exports__MissingPDFException as MissingPDFException, __webpack_exports__OPS as OPS, __webpack_exports__Outliner as Outliner, __webpack_exports__PDFDataRangeTransport as PDFDataRangeTransport, __webpack_exports__PDFDateString as PDFDateString, __webpack_exports__PDFWorker as PDFWorker, __webpack_exports__PasswordResponses as PasswordResponses, __webpack_exports__PermissionFlag as PermissionFlag, __webpack_exports__PixelsPerInch as PixelsPerInch, __webpack_exports__RenderingCancelledException as RenderingCancelledException, __webpack_exports__UnexpectedResponseException as UnexpectedResponseException, __webpack_exports__Util as Util, __webpack_exports__VerbosityLevel as VerbosityLevel, __webpack_exports__XfaLayer as XfaLayer, __webpack_exports__build as build, __webpack_exports__createValidAbsoluteUrl as createValidAbsoluteUrl, __webpack_exports__fetchData as fetchData, __webpack_exports__getDocument as getDocument, __webpack_exports__getFilenameFromUrl as getFilenameFromUrl, __webpack_exports__getPdfFilenameFromUrl as getPdfFilenameFromUrl, __webpack_exports__getXfaPageViewport as getXfaPageViewport, __webpack_exports__isDataScheme as isDataScheme, __webpack_exports__isPdfFile as isPdfFile, __webpack_exports__noContextMenu as noContextMenu, __webpack_exports__normalizeUnicode as normalizeUnicode, __webpack_exports__renderTextLayer as renderTextLayer, __webpack_exports__setLayerDimensions as setLayerDimensions, __webpack_exports__shadow as shadow, __webpack_exports__updateTextLayer as updateTextLayer, __webpack_exports__version as version }; diff --git a/toolkit/components/pdfjs/content/build/pdf.scripting.mjs b/toolkit/components/pdfjs/content/build/pdf.scripting.mjs index f590e83af3..8f4c2a7762 100644 --- a/toolkit/components/pdfjs/content/build/pdf.scripting.mjs +++ b/toolkit/components/pdfjs/content/build/pdf.scripting.mjs @@ -3957,8 +3957,8 @@ function initSandbox(params) { ;// CONCATENATED MODULE: ./src/pdf.scripting.js -const pdfjsVersion = "4.1.342"; -const pdfjsBuild = "e384df6f1"; +const pdfjsVersion = "4.1.379"; +const pdfjsBuild = "017e49244"; globalThis.pdfjsScripting = { initSandbox: initSandbox }; diff --git a/toolkit/components/pdfjs/content/build/pdf.worker.mjs b/toolkit/components/pdfjs/content/build/pdf.worker.mjs index e2cd54cf56..76770f6556 100644 --- a/toolkit/components/pdfjs/content/build/pdf.worker.mjs +++ b/toolkit/components/pdfjs/content/build/pdf.worker.mjs @@ -744,24 +744,6 @@ function getModificationDate(date = new Date()) { const buffer = [date.getUTCFullYear().toString(), (date.getUTCMonth() + 1).toString().padStart(2, "0"), date.getUTCDate().toString().padStart(2, "0"), date.getUTCHours().toString().padStart(2, "0"), date.getUTCMinutes().toString().padStart(2, "0"), date.getUTCSeconds().toString().padStart(2, "0")]; return buffer.join(""); } -class PromiseCapability { - #settled = false; - constructor() { - this.promise = new Promise((resolve, reject) => { - this.resolve = data => { - this.#settled = true; - resolve(data); - }; - this.reject = reason => { - this.#settled = true; - reject(reason); - }; - }); - } - get settled() { - return this.#settled; - } -} let NormalizeRegex = null; let NormalizationMap = null; function normalizeUnicode(str) { @@ -775,6 +757,17 @@ function getUuid() { return crypto.randomUUID(); } const AnnotationPrefix = "pdfjs_internal_id_"; +const FontRenderOps = { + BEZIER_CURVE_TO: 0, + MOVE_TO: 1, + LINE_TO: 2, + QUADRATIC_CURVE_TO: 3, + RESTORE: 4, + SAVE: 5, + SCALE: 6, + TRANSFORM: 7, + TRANSLATE: 8 +}; ;// CONCATENATED MODULE: ./src/core/primitives.js @@ -1789,7 +1782,7 @@ class ChunkedStreamManager { this._promisesByRequest = new Map(); this.progressiveDataLength = 0; this.aborted = false; - this._loadedStreamCapability = new PromiseCapability(); + this._loadedStreamCapability = Promise.withResolvers(); } sendRequest(begin, end) { const rangeReader = this.pdfNetworkStream.getRangeReader(begin, end); @@ -1852,7 +1845,7 @@ class ChunkedStreamManager { if (chunksNeeded.size === 0) { return Promise.resolve(); } - const capability = new PromiseCapability(); + const capability = Promise.withResolvers(); this._promisesByRequest.set(requestId, capability); const chunksToRequest = []; for (const chunk of chunksNeeded) { @@ -5075,6 +5068,18 @@ function decodeTextRegion(huffman, refinement, width, height, defaultPixelValue, symbolHeight += rdh; symbolBitmap = decodeRefinement(symbolWidth, symbolHeight, refinementTemplateIndex, symbolBitmap, (rdw >> 1) + rdx, (rdh >> 1) + rdy, false, refinementAt, decodingContext); } + let increment = 0; + if (!transposed) { + if (referenceCorner > 1) { + currentS += symbolWidth - 1; + } else { + increment = symbolWidth - 1; + } + } else if (!(referenceCorner & 1)) { + currentS += symbolHeight - 1; + } else { + increment = symbolHeight - 1; + } const offsetT = t - (referenceCorner & 1 ? 0 : symbolHeight - 1); const offsetS = currentS - (referenceCorner & 2 ? symbolWidth - 1 : 0); let s2, t2, symbolRow; @@ -5101,7 +5106,6 @@ function decodeTextRegion(huffman, refinement, width, height, defaultPixelValue, throw new Jbig2Error(`operator ${combinationOperator} is not supported`); } } - currentS += symbolHeight - 1; } else { for (t2 = 0; t2 < symbolHeight; t2++) { row = bitmap[offsetT + t2]; @@ -5124,14 +5128,13 @@ function decodeTextRegion(huffman, refinement, width, height, defaultPixelValue, throw new Jbig2Error(`operator ${combinationOperator} is not supported`); } } - currentS += symbolWidth - 1; } i++; const deltaS = huffman ? huffmanTables.tableDeltaS.decode(huffmanInput) : decodeInteger(contextCache, "IADS", decoder); if (deltaS === null) { break; } - currentS += deltaS + dsOffset; + currentS += increment + deltaS + dsOffset; } while (true); } return bitmap; @@ -18702,22 +18705,13 @@ function lookupCmap(ranges, unicode) { } function compileGlyf(code, cmds, font) { function moveTo(x, y) { - cmds.push({ - cmd: "moveTo", - args: [x, y] - }); + cmds.add(FontRenderOps.MOVE_TO, [x, y]); } function lineTo(x, y) { - cmds.push({ - cmd: "lineTo", - args: [x, y] - }); + cmds.add(FontRenderOps.LINE_TO, [x, y]); } function quadraticCurveTo(xa, ya, x, y) { - cmds.push({ - cmd: "quadraticCurveTo", - args: [xa, ya, x, y] - }); + cmds.add(FontRenderOps.QUADRATIC_CURVE_TO, [xa, ya, x, y]); } let i = 0; const numberOfContours = getInt16(code, i); @@ -18774,17 +18768,11 @@ function compileGlyf(code, cmds, font) { } const subglyph = font.glyphs[glyphIndex]; if (subglyph) { - cmds.push({ - cmd: "save" - }, { - cmd: "transform", - args: [scaleX, scale01, scale10, scaleY, x, y] - }); + cmds.add(FontRenderOps.SAVE); + cmds.add(FontRenderOps.TRANSFORM, [scaleX, scale01, scale10, scaleY, x, y]); if (!(flags & 0x02)) {} compileGlyf(subglyph, cmds, font); - cmds.push({ - cmd: "restore" - }); + cmds.add(FontRenderOps.RESTORE); } } while (flags & 0x20); } else { @@ -18874,22 +18862,13 @@ function compileGlyf(code, cmds, font) { } function compileCharString(charStringCode, cmds, font, glyphId) { function moveTo(x, y) { - cmds.push({ - cmd: "moveTo", - args: [x, y] - }); + cmds.add(FontRenderOps.MOVE_TO, [x, y]); } function lineTo(x, y) { - cmds.push({ - cmd: "lineTo", - args: [x, y] - }); + cmds.add(FontRenderOps.LINE_TO, [x, y]); } function bezierCurveTo(x1, y1, x2, y2, x, y) { - cmds.push({ - cmd: "bezierCurveTo", - args: [x1, y1, x2, y2, x, y] - }); + cmds.add(FontRenderOps.BEZIER_CURVE_TO, [x1, y1, x2, y2, x, y]); } const stack = []; let x = 0, @@ -19059,17 +19038,11 @@ function compileCharString(charStringCode, cmds, font, glyphId) { const bchar = stack.pop(); y = stack.pop(); x = stack.pop(); - cmds.push({ - cmd: "save" - }, { - cmd: "translate", - args: [x, y] - }); + cmds.add(FontRenderOps.SAVE); + cmds.add(FontRenderOps.TRANSLATE, [x, y]); let cmap = lookupCmap(font.cmap, String.fromCharCode(font.glyphNameMap[StandardEncoding[achar]])); compileCharString(font.glyphs[cmap.glyphId], cmds, font, cmap.glyphId); - cmds.push({ - cmd: "restore" - }); + cmds.add(FontRenderOps.RESTORE); cmap = lookupCmap(font.cmap, String.fromCharCode(font.glyphNameMap[StandardEncoding[bchar]])); compileCharString(font.glyphs[cmap.glyphId], cmds, font, cmap.glyphId); } @@ -19236,6 +19209,22 @@ function compileCharString(charStringCode, cmds, font, glyphId) { parse(charStringCode); } const NOOP = []; +class Commands { + cmds = []; + add(cmd, args) { + if (args) { + if (args.some(arg => typeof arg !== "number")) { + warn(`Commands.add - "${cmd}" has at least one non-number arg: "${args}".`); + const newArgs = args.map(arg => typeof arg === "number" ? arg : 0); + this.cmds.push(cmd, ...newArgs); + } else { + this.cmds.push(cmd, ...args); + } + } else { + this.cmds.push(cmd); + } + } +} class CompiledFont { constructor(fontMatrix) { if (this.constructor === CompiledFont) { @@ -19253,8 +19242,7 @@ class CompiledFont { let fn = this.compiledGlyphs[glyphId]; if (!fn) { try { - fn = this.compileGlyph(this.glyphs[glyphId], glyphId); - this.compiledGlyphs[glyphId] = fn; + fn = this.compiledGlyphs[glyphId] = this.compileGlyph(this.glyphs[glyphId], glyphId); } catch (ex) { this.compiledGlyphs[glyphId] = NOOP; if (this.compiledCharCodeToGlyphId[charCode] === undefined) { @@ -19282,20 +19270,13 @@ class CompiledFont { warn("Invalid fd index for glyph index."); } } - const cmds = [{ - cmd: "save" - }, { - cmd: "transform", - args: fontMatrix.slice() - }, { - cmd: "scale", - args: ["size", "-size"] - }]; + const cmds = new Commands(); + cmds.add(FontRenderOps.SAVE); + cmds.add(FontRenderOps.TRANSFORM, fontMatrix.slice()); + cmds.add(FontRenderOps.SCALE); this.compileGlyphImpl(code, cmds, glyphId); - cmds.push({ - cmd: "restore" - }); - return cmds; + cmds.add(FontRenderOps.RESTORE); + return cmds.cmds; } compileGlyphImpl() { unreachable("Children classes should implement this."); @@ -25941,14 +25922,18 @@ class Font { charCodeToGlyphId[mapping.charCode] = mapping.glyphId; } forcePostTable = true; - } else { + } else if (cmapPlatformId === 3 && cmapEncodingId === 0) { for (const mapping of cmapMappings) { let charCode = mapping.charCode; - if (cmapPlatformId === 3 && charCode >= 0xf000 && charCode <= 0xf0ff) { + if (charCode >= 0xf000 && charCode <= 0xf0ff) { charCode &= 0xff; } charCodeToGlyphId[charCode] = mapping.glyphId; } + } else { + for (const mapping of cmapMappings) { + charCodeToGlyphId[mapping.charCode] = mapping.glyphId; + } } if (properties.glyphNames && (baseEncoding.length || this.differences.length)) { for (let i = 0; i < 256; ++i) { @@ -27080,7 +27065,13 @@ class MeshShading extends BaseShading { } } getIR() { - return ["Mesh", this.shadingType, this.coords, this.colors, this.figures, this.bounds, this.bbox, this.background]; + const { + bounds + } = this; + if (bounds[2] - bounds[0] === 0 || bounds[3] - bounds[1] === 0) { + throw new FormatError(`Invalid MeshShading bounds: [${bounds}].`); + } + return ["Mesh", this.shadingType, this.coords, this.colors, this.figures, bounds, this.bbox, this.background]; } } class DummyShading extends BaseShading { @@ -31794,7 +31785,10 @@ class PartialEvaluator { if (font.cacheKey && this.fontCache.has(font.cacheKey)) { return this.fontCache.get(font.cacheKey); } - const fontCapability = new PromiseCapability(); + const { + promise, + resolve + } = Promise.withResolvers(); let preEvaluatedFont; try { preEvaluatedFont = this.preEvaluateFont(font); @@ -31831,14 +31825,14 @@ class PartialEvaluator { } assert(fontID?.startsWith("f"), 'The "fontID" must be (correctly) defined.'); if (fontRefIsRef) { - this.fontCache.put(fontRef, fontCapability.promise); + this.fontCache.put(fontRef, promise); } else { font.cacheKey = `cacheKey_${fontID}`; - this.fontCache.put(font.cacheKey, fontCapability.promise); + this.fontCache.put(font.cacheKey, promise); } font.loadedName = `${this.idFactory.getDocId()}_${fontID}`; this.translateFont(preEvaluatedFont).then(translatedFont => { - fontCapability.resolve(new TranslatedFont({ + resolve(new TranslatedFont({ loadedName: font.loadedName, font: translatedFont, dict: font, @@ -31846,14 +31840,14 @@ class PartialEvaluator { })); }).catch(reason => { warn(`loadFont - translateFont failed: "${reason}".`); - fontCapability.resolve(new TranslatedFont({ + resolve(new TranslatedFont({ loadedName: font.loadedName, font: new ErrorFont(reason instanceof Error ? reason.message : reason), dict: font, evaluatorOptions: this.options })); }); - return fontCapability.promise; + return promise; } buildPath(operatorList, fn, args, parsingText = false) { const lastIndex = operatorList.length - 1; @@ -31937,19 +31931,33 @@ class PartialEvaluator { localShadingPatternCache }) { let id = localShadingPatternCache.get(shading); - if (!id) { - var shadingFill = Pattern.parseShading(shading, this.xref, resources, this._pdfFunctionFactory, localColorSpaceCache); - const patternIR = shadingFill.getIR(); - id = `pattern_${this.idFactory.createObjId()}`; - if (this.parsingType3Font) { - id = `${this.idFactory.getDocId()}_type3_${id}`; + if (id) { + return id; + } + let patternIR; + try { + const shadingFill = Pattern.parseShading(shading, this.xref, resources, this._pdfFunctionFactory, localColorSpaceCache); + patternIR = shadingFill.getIR(); + } catch (reason) { + if (reason instanceof AbortException) { + return null; } - localShadingPatternCache.set(shading, id); - if (this.parsingType3Font) { - this.handler.send("commonobj", [id, "Pattern", patternIR]); - } else { - this.handler.send("obj", [id, this.pageIndex, "Pattern", patternIR]); + if (this.options.ignoreErrors) { + warn(`parseShading - ignoring shading: "${reason}".`); + localShadingPatternCache.set(shading, null); + return null; } + throw reason; + } + id = `pattern_${this.idFactory.createObjId()}`; + if (this.parsingType3Font) { + id = `${this.idFactory.getDocId()}_type3_${id}`; + } + localShadingPatternCache.set(shading, id); + if (this.parsingType3Font) { + this.handler.send("commonobj", [id, "Pattern", patternIR]); + } else { + this.handler.send("obj", [id, this.pageIndex, "Pattern", patternIR]); } return id; } @@ -31975,14 +31983,16 @@ class PartialEvaluator { return this.handleTilingType(fn, color, resources, pattern, dict, operatorList, task, localTilingPatternCache); } else if (typeNum === PatternType.SHADING) { const shading = dict.get("Shading"); - const matrix = dict.getArray("Matrix"); const objId = this.parseShading({ shading, resources, localColorSpaceCache, localShadingPatternCache }); - operatorList.addOp(fn, ["Shading", objId, matrix]); + if (objId) { + const matrix = dict.getArray("Matrix"); + operatorList.addOp(fn, ["Shading", objId, matrix]); + } return undefined; } throw new FormatError(`Unknown PatternType: ${typeNum}`); @@ -32393,6 +32403,9 @@ class PartialEvaluator { localColorSpaceCache, localShadingPatternCache }); + if (!patternId) { + continue; + } args = [patternId]; fn = OPS.shadingFill; break; @@ -33998,6 +34011,10 @@ class PartialEvaluator { systemFontInfo = getFontSubstitution(this.systemFontCache, this.idFactory, this.options.standardFontDataUrl, fontName.name, standardFontName); } } + let fontMatrix = dict.getArray("FontMatrix"); + if (!Array.isArray(fontMatrix) || fontMatrix.length !== 6 || fontMatrix.some(x => typeof x !== "number")) { + fontMatrix = FONT_IDENTITY_MATRIX; + } const properties = { type, name: fontName.name, @@ -34010,7 +34027,7 @@ class PartialEvaluator { loadedName: baseDict.loadedName, composite, fixedPitch: false, - fontMatrix: dict.getArray("FontMatrix") || FONT_IDENTITY_MATRIX, + fontMatrix, firstChar, lastChar, toUnicode, @@ -35327,7 +35344,8 @@ function pickPlatformItem(dict) { return null; } class FileSpec { - constructor(root, xref) { + #contentAvailable = false; + constructor(root, xref, skipContent = false) { if (!(root instanceof Dict)) { return; } @@ -35340,10 +35358,12 @@ class FileSpec { if (root.has("RF")) { warn("Related file specifications are not supported"); } - this.contentAvailable = true; - if (!root.has("EF")) { - this.contentAvailable = false; - warn("Non-embedded file specifications are not supported"); + if (!skipContent) { + if (root.has("EF")) { + this.#contentAvailable = true; + } else { + warn("Non-embedded file specifications are not supported"); + } } } get filename() { @@ -35354,7 +35374,7 @@ class FileSpec { return this._filename; } get content() { - if (!this.contentAvailable) { + if (!this.#contentAvailable) { return null; } if (!this.contentRef && this.root) { @@ -38563,7 +38583,7 @@ class Catalog { continue; } if (!outlineDict.has("Title")) { - throw new FormatError("Invalid outline item encountered."); + warn("Invalid outline item encountered."); } const data = { url: null, @@ -38592,7 +38612,7 @@ class Catalog { unsafeUrl: data.unsafeUrl, newWindow: data.newWindow, setOCGState: data.setOCGState, - title: stringToPDFString(title), + title: typeof title === "string" ? stringToPDFString(title) : "", color: rgbColor, count: Number.isInteger(count) ? count : undefined, bold: !!(flags & 2), @@ -39598,7 +39618,11 @@ class Catalog { case "GoToR": const urlDict = action.get("F"); if (urlDict instanceof Dict) { - url = urlDict.get("F") || null; + const fs = new FileSpec(urlDict, null, true); + const { + filename + } = fs.serializable; + url = filename; } else if (typeof urlDict === "string") { url = urlDict; } @@ -51360,7 +51384,7 @@ class AnnotationBorderStyle { } } setDashArray(dashArray, forceStyle = false) { - if (Array.isArray(dashArray) && dashArray.length > 0) { + if (Array.isArray(dashArray)) { let isValid = true; let allZeros = true; for (const element of dashArray) { @@ -51372,7 +51396,7 @@ class AnnotationBorderStyle { allZeros = false; } } - if (isValid && !allZeros) { + if (dashArray.length === 0 || isValid && !allZeros) { this.dashArray = dashArray; if (forceStyle) { this.setStyle(Name.get("D")); @@ -56282,7 +56306,7 @@ class MessageHandler { } sendWithPromise(actionName, data, transfers) { const callbackId = this.callbackId++; - const capability = new PromiseCapability(); + const capability = Promise.withResolvers(); this.callbackCapabilities[callbackId] = capability; try { this.comObj.postMessage({ @@ -56304,7 +56328,7 @@ class MessageHandler { comObj = this.comObj; return new ReadableStream({ start: controller => { - const startCapability = new PromiseCapability(); + const startCapability = Promise.withResolvers(); this.streamControllers[streamId] = { controller, startCall: startCapability, @@ -56323,7 +56347,7 @@ class MessageHandler { return startCapability.promise; }, pull: controller => { - const pullCapability = new PromiseCapability(); + const pullCapability = Promise.withResolvers(); this.streamControllers[streamId].pullCall = pullCapability; comObj.postMessage({ sourceName, @@ -56336,7 +56360,7 @@ class MessageHandler { }, cancel: reason => { assert(reason instanceof Error, "cancel must have a valid reason"); - const cancelCapability = new PromiseCapability(); + const cancelCapability = Promise.withResolvers(); this.streamControllers[streamId].cancelCall = cancelCapability; this.streamControllers[streamId].isClosed = true; comObj.postMessage({ @@ -56365,7 +56389,7 @@ class MessageHandler { const lastDesiredSize = this.desiredSize; this.desiredSize -= size; if (lastDesiredSize > 0 && this.desiredSize <= 0) { - this.sinkCapability = new PromiseCapability(); + this.sinkCapability = Promise.withResolvers(); this.ready = this.sinkCapability.promise; } comObj.postMessage({ @@ -56403,7 +56427,7 @@ class MessageHandler { reason: wrapReason(reason) }); }, - sinkCapability: new PromiseCapability(), + sinkCapability: Promise.withResolvers(), onPull: null, onCancel: null, isCancelled: false, @@ -56681,7 +56705,7 @@ class WorkerTask { constructor(name) { this.name = name; this.terminated = false; - this._capability = new PromiseCapability(); + this._capability = Promise.withResolvers(); } get finished() { return this._capability.promise; @@ -56725,7 +56749,7 @@ class WorkerMessageHandler { docId, apiVersion } = docParams; - const workerVersion = "4.1.342"; + const workerVersion = "4.1.379"; if (apiVersion !== workerVersion) { throw new Error(`The API version "${apiVersion}" does not match ` + `the Worker version "${workerVersion}".`); } @@ -56785,7 +56809,7 @@ class WorkerMessageHandler { password, rangeChunkSize }; - const pdfManagerCapability = new PromiseCapability(); + const pdfManagerCapability = Promise.withResolvers(); let newPdfManager; if (data) { try { @@ -57287,8 +57311,8 @@ if (typeof window === "undefined" && !isNodeJS && typeof self !== "undefined" && ;// CONCATENATED MODULE: ./src/pdf.worker.js -const pdfjsVersion = "4.1.342"; -const pdfjsBuild = "e384df6f1"; +const pdfjsVersion = "4.1.379"; +const pdfjsBuild = "017e49244"; var __webpack_exports__WorkerMessageHandler = __webpack_exports__.WorkerMessageHandler; export { __webpack_exports__WorkerMessageHandler as WorkerMessageHandler }; diff --git a/toolkit/components/pdfjs/content/web/viewer-geckoview.mjs b/toolkit/components/pdfjs/content/web/viewer-geckoview.mjs index 866c4a405a..c309f8cf12 100644 --- a/toolkit/components/pdfjs/content/web/viewer-geckoview.mjs +++ b/toolkit/components/pdfjs/content/web/viewer-geckoview.mjs @@ -529,7 +529,6 @@ function toggleExpandedBtn(button, toggle, view = null) { } ;// CONCATENATED MODULE: ./web/app_options.js -const compatibilityParams = Object.create(null); const OptionKind = { BROWSER: 0x01, VIEWER: 0x02, @@ -772,11 +771,8 @@ class AppOptions { constructor() { throw new Error("Cannot initialize AppOptions."); } - static getCompat(name) { - return compatibilityParams[name] ?? undefined; - } static get(name) { - return userOptions[name] ?? compatibilityParams[name] ?? defaultOptions[name]?.value ?? undefined; + return userOptions[name] ?? defaultOptions[name]?.value ?? undefined; } static getAll(kind = null, defaultOnly = false) { const options = Object.create(null); @@ -785,7 +781,7 @@ class AppOptions { if (kind && !(kind & defaultOption.kind)) { continue; } - options[name] = defaultOnly ? defaultOption.value : userOptions[name] ?? compatibilityParams[name] ?? defaultOption.value; + options[name] = defaultOnly ? defaultOption.value : userOptions[name] ?? defaultOption.value; } return options; } @@ -1252,7 +1248,6 @@ const { PDFWorker, PermissionFlag, PixelsPerInch, - PromiseCapability, RenderingCancelledException, renderTextLayer, setLayerDimensions, @@ -1270,35 +1265,38 @@ const WaitOnType = { EVENT: "event", TIMEOUT: "timeout" }; -function waitOnEventOrTimeout({ +async function waitOnEventOrTimeout({ target, name, delay = 0 }) { - return new Promise(function (resolve, reject) { - if (typeof target !== "object" || !(name && typeof name === "string") || !(Number.isInteger(delay) && delay >= 0)) { - throw new Error("waitOnEventOrTimeout - invalid parameters."); - } - function handler(type) { - if (target instanceof EventBus) { - target._off(name, eventHandler); - } else { - target.removeEventListener(name, eventHandler); - } - if (timeout) { - clearTimeout(timeout); - } - resolve(type); - } - const eventHandler = handler.bind(null, WaitOnType.EVENT); + if (typeof target !== "object" || !(name && typeof name === "string") || !(Number.isInteger(delay) && delay >= 0)) { + throw new Error("waitOnEventOrTimeout - invalid parameters."); + } + const { + promise, + resolve + } = Promise.withResolvers(); + function handler(type) { if (target instanceof EventBus) { - target._on(name, eventHandler); + target._off(name, eventHandler); } else { - target.addEventListener(name, eventHandler); + target.removeEventListener(name, eventHandler); } - const timeoutHandler = handler.bind(null, WaitOnType.TIMEOUT); - const timeout = setTimeout(timeoutHandler, delay); - }); + if (timeout) { + clearTimeout(timeout); + } + resolve(type); + } + const eventHandler = handler.bind(null, WaitOnType.EVENT); + if (target instanceof EventBus) { + target._on(name, eventHandler); + } else { + target.addEventListener(name, eventHandler); + } + const timeoutHandler = handler.bind(null, WaitOnType.TIMEOUT); + const timeout = setTimeout(timeoutHandler, delay); + return promise; } class EventBus { #listeners = Object.create(null); @@ -2200,10 +2198,8 @@ class PasswordPrompt { this.dialog.addEventListener("close", this.#cancel.bind(this)); } async open() { - if (this.#activeCapability) { - await this.#activeCapability.promise; - } - this.#activeCapability = new PromiseCapability(); + await this.#activeCapability?.promise; + this.#activeCapability = Promise.withResolvers(); try { await this.overlayManager.open(this.dialog); } catch (ex) { @@ -2326,7 +2322,6 @@ function getNormalizeWithNFKC() { ;// CONCATENATED MODULE: ./web/pdf_find_controller.js - const FindState = { FOUND: 0, NOT_FOUND: 1, @@ -2664,7 +2659,7 @@ class PDFFindController { this._dirtyMatch = false; clearTimeout(this._findTimeout); this._findTimeout = null; - this._firstPageCapability = new PromiseCapability(); + this._firstPageCapability = Promise.withResolvers(); } get #query() { const { @@ -2827,14 +2822,17 @@ class PDFFindController { if (this._extractTextPromises.length > 0) { return; } - let promise = Promise.resolve(); + let deferred = Promise.resolve(); const textOptions = { disableNormalization: true }; for (let i = 0, ii = this._linkService.pagesCount; i < ii; i++) { - const extractTextCapability = new PromiseCapability(); - this._extractTextPromises[i] = extractTextCapability.promise; - promise = promise.then(() => { + const { + promise, + resolve + } = Promise.withResolvers(); + this._extractTextPromises[i] = promise; + deferred = deferred.then(() => { return this._pdfDocument.getPage(i + 1).then(pdfPage => pdfPage.getTextContent(textOptions)).then(textContent => { const strBuf = []; for (const textItem of textContent.items) { @@ -2844,13 +2842,13 @@ class PDFFindController { } } [this._pageContents[i], this._pageDiffs[i], this._hasDiacritics[i]] = normalize(strBuf.join("")); - extractTextCapability.resolve(); + resolve(); }, reason => { console.error(`Unable to get text content for page ${i + 1}`, reason); this._pageContents[i] = ""; this._pageDiffs[i] = null; this._hasDiacritics[i] = false; - extractTextCapability.resolve(); + resolve(); }); }); } @@ -3982,7 +3980,7 @@ class PDFScriptingManager { return; } await this.#willPrintCapability?.promise; - this.#willPrintCapability = new PromiseCapability(); + this.#willPrintCapability = Promise.withResolvers(); try { await this.#scripting.dispatchEventInSandbox({ id: "doc", @@ -4111,7 +4109,7 @@ class PDFScriptingManager { const pdfDocument = this.#pdfDocument, visitedPages = this._visitedPages; if (initialize) { - this.#closeCapability = new PromiseCapability(); + this.#closeCapability = Promise.withResolvers(); } if (!this.#closeCapability) { return; @@ -4161,7 +4159,7 @@ class PDFScriptingManager { }); } #initScripting() { - this.#destroyCapability = new PromiseCapability(); + this.#destroyCapability = Promise.withResolvers(); if (this.#scripting) { throw new Error("#initScripting: Scripting already exists."); } @@ -5116,7 +5114,7 @@ class PDFPageView { this.#textLayerMode = options.textLayerMode ?? TextLayerMode.ENABLE; this.#annotationMode = options.annotationMode ?? AnnotationMode.ENABLE_FORMS; this.imageResourcesPath = options.imageResourcesPath || ""; - this.maxCanvasPixels = options.maxCanvasPixels ?? (AppOptions.getCompat("maxCanvasPixels") || 2 ** 25); + this.maxCanvasPixels = options.maxCanvasPixels ?? AppOptions.get("maxCanvasPixels"); this.pageColors = options.pageColors || null; this.eventBus = options.eventBus; this.renderingQueue = options.renderingQueue; @@ -5922,7 +5920,7 @@ class PDFViewer { #scaleTimeoutId = null; #textLayerMode = TextLayerMode.ENABLE; constructor(options) { - const viewerVersion = "4.1.342"; + const viewerVersion = "4.1.379"; if (version !== viewerVersion) { throw new Error(`The API version "${version}" does not match the Viewer version "${viewerVersion}".`); } @@ -5977,7 +5975,7 @@ class PDFViewer { return new Set(this.#buffer); } get pageViewsReady() { - return this._pagesCapability.settled && this._pages.every(pageView => pageView?.pdfPage); + return this._pages.every(pageView => pageView?.pdfPage); } get renderForms() { return this.#annotationMode === AnnotationMode.ENABLE_FORMS; @@ -6280,7 +6278,7 @@ class PDFViewer { }; this.eventBus._on("pagerender", this._onBeforeDraw); this._onAfterDraw = evt => { - if (evt.cssTransform || this._onePageRenderedCapability.settled) { + if (evt.cssTransform) { return; } this._onePageRenderedCapability.resolve({ @@ -6452,9 +6450,9 @@ class PDFViewer { this._location = null; this._pagesRotation = 0; this._optionalContentConfigPromise = null; - this._firstPageCapability = new PromiseCapability(); - this._onePageRenderedCapability = new PromiseCapability(); - this._pagesCapability = new PromiseCapability(); + this._firstPageCapability = Promise.withResolvers(); + this._onePageRenderedCapability = Promise.withResolvers(); + this._pagesCapability = Promise.withResolvers(); this._scrollMode = ScrollMode.VERTICAL; this._previousScrollMode = ScrollMode.UNKNOWN; this._spreadMode = SpreadMode.NONE; @@ -7475,7 +7473,10 @@ const ViewOnLoad = { }; const PDFViewerApplication = { initialBookmark: document.location.hash.substring(1), - _initializedCapability: new PromiseCapability(), + _initializedCapability: { + ...Promise.withResolvers(), + settled: false + }, appConfig: null, pdfDocument: null, pdfLoadingTask: null, @@ -7550,6 +7551,7 @@ const PDFViewerApplication = { await this._initializeViewerComponents(); this.bindEvents(); this.bindWindowEvents(); + this._initializedCapability.settled = true; this._initializedCapability.resolve(); }, async _parseHashParams() { @@ -8608,6 +8610,10 @@ const PDFViewerApplication = { mediaQueryList.addEventListener("change", addWindowResolutionChange, { once: true }); + _boundEvents.removeWindowResolutionChange ||= function () { + mediaQueryList.removeEventListener("change", addWindowResolutionChange); + _boundEvents.removeWindowResolutionChange = null; + }; } addWindowResolutionChange(); _boundEvents.windowResize = () => { @@ -9531,8 +9537,8 @@ function webViewerReportTelemetry({ -const pdfjsVersion = "4.1.342"; -const pdfjsBuild = "e384df6f1"; +const pdfjsVersion = "4.1.379"; +const pdfjsBuild = "017e49244"; const AppConstants = null; window.PDFViewerApplication = PDFViewerApplication; window.PDFViewerApplicationConstants = AppConstants; diff --git a/toolkit/components/pdfjs/content/web/viewer.css b/toolkit/components/pdfjs/content/web/viewer.css index 2999c89f3a..063fa1632b 100644 --- a/toolkit/components/pdfjs/content/web/viewer.css +++ b/toolkit/components/pdfjs/content/web/viewer.css @@ -3513,7 +3513,7 @@ a:is(.toolbarButton, .secondaryToolbarButton)[href="#"]{ transition-property:none; .loadingInput:has(> &.loading)::after{ - display: block; + display:block; visibility:visible; transition-property:visibility; @@ -3525,7 +3525,7 @@ a:is(.toolbarButton, .secondaryToolbarButton)[href="#"]{ &::after{ position:absolute; visibility:hidden; - display: none; + display:none; top:calc(50% - 8px); width:16px; height:16px; diff --git a/toolkit/components/pdfjs/content/web/viewer.mjs b/toolkit/components/pdfjs/content/web/viewer.mjs index 778ce57e1a..864140b624 100644 --- a/toolkit/components/pdfjs/content/web/viewer.mjs +++ b/toolkit/components/pdfjs/content/web/viewer.mjs @@ -529,7 +529,6 @@ function toggleExpandedBtn(button, toggle, view = null) { } ;// CONCATENATED MODULE: ./web/app_options.js -const compatibilityParams = Object.create(null); const OptionKind = { BROWSER: 0x01, VIEWER: 0x02, @@ -772,11 +771,8 @@ class AppOptions { constructor() { throw new Error("Cannot initialize AppOptions."); } - static getCompat(name) { - return compatibilityParams[name] ?? undefined; - } static get(name) { - return userOptions[name] ?? compatibilityParams[name] ?? defaultOptions[name]?.value ?? undefined; + return userOptions[name] ?? defaultOptions[name]?.value ?? undefined; } static getAll(kind = null, defaultOnly = false) { const options = Object.create(null); @@ -785,7 +781,7 @@ class AppOptions { if (kind && !(kind & defaultOption.kind)) { continue; } - options[name] = defaultOnly ? defaultOption.value : userOptions[name] ?? compatibilityParams[name] ?? defaultOption.value; + options[name] = defaultOnly ? defaultOption.value : userOptions[name] ?? defaultOption.value; } return options; } @@ -1252,7 +1248,6 @@ const { PDFWorker, PermissionFlag, PixelsPerInch, - PromiseCapability, RenderingCancelledException, renderTextLayer, setLayerDimensions, @@ -1270,35 +1265,38 @@ const WaitOnType = { EVENT: "event", TIMEOUT: "timeout" }; -function waitOnEventOrTimeout({ +async function waitOnEventOrTimeout({ target, name, delay = 0 }) { - return new Promise(function (resolve, reject) { - if (typeof target !== "object" || !(name && typeof name === "string") || !(Number.isInteger(delay) && delay >= 0)) { - throw new Error("waitOnEventOrTimeout - invalid parameters."); - } - function handler(type) { - if (target instanceof EventBus) { - target._off(name, eventHandler); - } else { - target.removeEventListener(name, eventHandler); - } - if (timeout) { - clearTimeout(timeout); - } - resolve(type); - } - const eventHandler = handler.bind(null, WaitOnType.EVENT); + if (typeof target !== "object" || !(name && typeof name === "string") || !(Number.isInteger(delay) && delay >= 0)) { + throw new Error("waitOnEventOrTimeout - invalid parameters."); + } + const { + promise, + resolve + } = Promise.withResolvers(); + function handler(type) { if (target instanceof EventBus) { - target._on(name, eventHandler); + target._off(name, eventHandler); } else { - target.addEventListener(name, eventHandler); + target.removeEventListener(name, eventHandler); } - const timeoutHandler = handler.bind(null, WaitOnType.TIMEOUT); - const timeout = setTimeout(timeoutHandler, delay); - }); + if (timeout) { + clearTimeout(timeout); + } + resolve(type); + } + const eventHandler = handler.bind(null, WaitOnType.EVENT); + if (target instanceof EventBus) { + target._on(name, eventHandler); + } else { + target.addEventListener(name, eventHandler); + } + const timeoutHandler = handler.bind(null, WaitOnType.TIMEOUT); + const timeout = setTimeout(timeoutHandler, delay); + return promise; } class EventBus { #listeners = Object.create(null); @@ -2480,10 +2478,8 @@ class PasswordPrompt { this.dialog.addEventListener("close", this.#cancel.bind(this)); } async open() { - if (this.#activeCapability) { - await this.#activeCapability.promise; - } - this.#activeCapability = new PromiseCapability(); + await this.#activeCapability?.promise; + this.#activeCapability = Promise.withResolvers(); try { await this.overlayManager.open(this.dialog); } catch (ex) { @@ -2642,7 +2638,7 @@ class PDFAttachmentViewer extends BaseTreeViewer { super.reset(); this._attachments = null; if (!keepRenderedCapability) { - this._renderedCapability = new PromiseCapability(); + this._renderedCapability = Promise.withResolvers(); } this._pendingDispatchEvent = false; } @@ -3039,7 +3035,7 @@ class PDFDocumentProperties { #reset() { this.pdfDocument = null; this.#fieldData = null; - this._dataAvailableCapability = new PromiseCapability(); + this._dataAvailableCapability = Promise.withResolvers(); this._currentPageNumber = 1; this._pagesRotation = 0; } @@ -3214,7 +3210,6 @@ function getNormalizeWithNFKC() { ;// CONCATENATED MODULE: ./web/pdf_find_controller.js - const FindState = { FOUND: 0, NOT_FOUND: 1, @@ -3552,7 +3547,7 @@ class PDFFindController { this._dirtyMatch = false; clearTimeout(this._findTimeout); this._findTimeout = null; - this._firstPageCapability = new PromiseCapability(); + this._firstPageCapability = Promise.withResolvers(); } get #query() { const { @@ -3715,14 +3710,17 @@ class PDFFindController { if (this._extractTextPromises.length > 0) { return; } - let promise = Promise.resolve(); + let deferred = Promise.resolve(); const textOptions = { disableNormalization: true }; for (let i = 0, ii = this._linkService.pagesCount; i < ii; i++) { - const extractTextCapability = new PromiseCapability(); - this._extractTextPromises[i] = extractTextCapability.promise; - promise = promise.then(() => { + const { + promise, + resolve + } = Promise.withResolvers(); + this._extractTextPromises[i] = promise; + deferred = deferred.then(() => { return this._pdfDocument.getPage(i + 1).then(pdfPage => pdfPage.getTextContent(textOptions)).then(textContent => { const strBuf = []; for (const textItem of textContent.items) { @@ -3732,13 +3730,13 @@ class PDFFindController { } } [this._pageContents[i], this._pageDiffs[i], this._hasDiacritics[i]] = normalize(strBuf.join("")); - extractTextCapability.resolve(); + resolve(); }, reason => { console.error(`Unable to get text content for page ${i + 1}`, reason); this._pageContents[i] = ""; this._pageDiffs[i] = null; this._hasDiacritics[i] = false; - extractTextCapability.resolve(); + resolve(); }); }); } @@ -4722,7 +4720,6 @@ class PDFLayerViewer extends BaseTreeViewer { ;// CONCATENATED MODULE: ./web/pdf_outline_viewer.js - class PDFOutlineViewer extends BaseTreeViewer { constructor(options) { super(options); @@ -4735,9 +4732,7 @@ class PDFOutlineViewer extends BaseTreeViewer { }); this.eventBus._on("pagesloaded", evt => { this._isPagesLoaded = !!evt.pagesCount; - if (this._currentOutlineItemCapability && !this._currentOutlineItemCapability.settled) { - this._currentOutlineItemCapability.resolve(this._isPagesLoaded); - } + this._currentOutlineItemCapability?.resolve(this._isPagesLoaded); }); this.eventBus._on("sidebarviewchanged", evt => { this._sidebarView = evt.view; @@ -4749,13 +4744,11 @@ class PDFOutlineViewer extends BaseTreeViewer { this._pageNumberToDestHashCapability = null; this._currentPageNumber = 1; this._isPagesLoaded = null; - if (this._currentOutlineItemCapability && !this._currentOutlineItemCapability.settled) { - this._currentOutlineItemCapability.resolve(false); - } + this._currentOutlineItemCapability?.resolve(false); this._currentOutlineItemCapability = null; } _dispatchEvent(outlineCount) { - this._currentOutlineItemCapability = new PromiseCapability(); + this._currentOutlineItemCapability = Promise.withResolvers(); if (outlineCount === 0 || this._pdfDocument?.loadingParams.disableAutoFetch) { this._currentOutlineItemCapability.resolve(false); } else if (this._isPagesLoaded !== null) { @@ -4937,7 +4930,7 @@ class PDFOutlineViewer extends BaseTreeViewer { if (this._pageNumberToDestHashCapability) { return this._pageNumberToDestHashCapability.promise; } - this._pageNumberToDestHashCapability = new PromiseCapability(); + this._pageNumberToDestHashCapability = Promise.withResolvers(); const pageNumberToDestHash = new Map(), pageNumberNesting = new Map(); const queue = [{ @@ -5752,7 +5745,7 @@ class PDFScriptingManager { return; } await this.#willPrintCapability?.promise; - this.#willPrintCapability = new PromiseCapability(); + this.#willPrintCapability = Promise.withResolvers(); try { await this.#scripting.dispatchEventInSandbox({ id: "doc", @@ -5881,7 +5874,7 @@ class PDFScriptingManager { const pdfDocument = this.#pdfDocument, visitedPages = this._visitedPages; if (initialize) { - this.#closeCapability = new PromiseCapability(); + this.#closeCapability = Promise.withResolvers(); } if (!this.#closeCapability) { return; @@ -5931,7 +5924,7 @@ class PDFScriptingManager { }); } #initScripting() { - this.#destroyCapability = new PromiseCapability(); + this.#destroyCapability = Promise.withResolvers(); if (this.#scripting) { throw new Error("#initScripting: Scripting already exists."); } @@ -6409,7 +6402,7 @@ class PDFThumbnailView { } this.resume = null; } - _getPageDrawContext(upscaleFactor = 1) { + #getPageDrawContext(upscaleFactor = 1) { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d", { alpha: false @@ -6424,11 +6417,11 @@ class PDFThumbnailView { transform }; } - _convertCanvasToImage(canvas) { + #convertCanvasToImage(canvas) { if (this.renderingState !== RenderingStates.FINISHED) { - throw new Error("_convertCanvasToImage: Rendering has not finished."); + throw new Error("#convertCanvasToImage: Rendering has not finished."); } - const reducedCanvas = this._reduceImage(canvas); + const reducedCanvas = this.#reduceImage(canvas); const image = document.createElement("img"); image.className = "thumbnailImage"; image.setAttribute("data-l10n-id", "pdfjs-thumb-page-canvas"); @@ -6448,7 +6441,7 @@ class PDFThumbnailView { return; } this.renderingState = RenderingStates.FINISHED; - this._convertCanvasToImage(canvas); + this.#convertCanvasToImage(canvas); if (error) { throw error; } @@ -6470,7 +6463,7 @@ class PDFThumbnailView { ctx, canvas, transform - } = this._getPageDrawContext(DRAW_UPSCALE_FACTOR); + } = this.#getPageDrawContext(DRAW_UPSCALE_FACTOR); const drawViewport = this.viewport.clone({ scale: DRAW_UPSCALE_FACTOR * this.scale }); @@ -6525,13 +6518,13 @@ class PDFThumbnailView { return; } this.renderingState = RenderingStates.FINISHED; - this._convertCanvasToImage(canvas); + this.#convertCanvasToImage(canvas); } - _reduceImage(img) { + #reduceImage(img) { const { ctx, canvas - } = this._getPageDrawContext(); + } = this.#getPageDrawContext(); if (img.width <= 2 * canvas.width) { ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, canvas.width, canvas.height); return canvas; @@ -6585,16 +6578,16 @@ class PDFThumbnailViewer { this.linkService = linkService; this.renderingQueue = renderingQueue; this.pageColors = pageColors || null; - this.scroll = watchScroll(this.container, this._scrollUpdated.bind(this)); - this._resetView(); + this.scroll = watchScroll(this.container, this.#scrollUpdated.bind(this)); + this.#resetView(); } - _scrollUpdated() { + #scrollUpdated() { this.renderingQueue.renderHighestPriority(); } getThumbnail(index) { return this._thumbnails[index]; } - _getVisibleThumbs() { + #getVisibleThumbs() { return getVisibleElements({ scrollEl: this.container, views: this._thumbnails @@ -6618,7 +6611,7 @@ class PDFThumbnailViewer { first, last, views - } = this._getVisibleThumbs(); + } = this.#getVisibleThumbs(); if (views.length > 0) { let shouldScroll = false; if (pageNumber <= first.id || pageNumber >= last.id) { @@ -6672,7 +6665,7 @@ class PDFThumbnailViewer { } TempImageFactory.destroyCanvas(); } - _resetView() { + #resetView() { this._thumbnails = []; this._currentPageNumber = 1; this._pageLabels = null; @@ -6681,8 +6674,8 @@ class PDFThumbnailViewer { } setDocument(pdfDocument) { if (this.pdfDocument) { - this._cancelRendering(); - this._resetView(); + this.#cancelRendering(); + this.#resetView(); } this.pdfDocument = pdfDocument; if (!pdfDocument) { @@ -6717,7 +6710,7 @@ class PDFThumbnailViewer { console.error("Unable to initialize thumbnail viewer", reason); }); } - _cancelRendering() { + #cancelRendering() { for (const thumbnail of this._thumbnails) { thumbnail.cancelRendering(); } @@ -6762,7 +6755,7 @@ class PDFThumbnailViewer { return this.scroll.down; } forceRendering() { - const visibleThumbs = this._getVisibleThumbs(); + const visibleThumbs = this.#getVisibleThumbs(); const scrollAhead = this.#getScrollAhead(visibleThumbs); const thumbView = this.renderingQueue.getHighestPriority(visibleThumbs, this._thumbnails, scrollAhead); if (thumbView) { @@ -7694,7 +7687,7 @@ class PDFPageView { this.#textLayerMode = options.textLayerMode ?? TextLayerMode.ENABLE; this.#annotationMode = options.annotationMode ?? AnnotationMode.ENABLE_FORMS; this.imageResourcesPath = options.imageResourcesPath || ""; - this.maxCanvasPixels = options.maxCanvasPixels ?? (AppOptions.getCompat("maxCanvasPixels") || 2 ** 25); + this.maxCanvasPixels = options.maxCanvasPixels ?? AppOptions.get("maxCanvasPixels"); this.pageColors = options.pageColors || null; this.eventBus = options.eventBus; this.renderingQueue = options.renderingQueue; @@ -8500,7 +8493,7 @@ class PDFViewer { #scaleTimeoutId = null; #textLayerMode = TextLayerMode.ENABLE; constructor(options) { - const viewerVersion = "4.1.342"; + const viewerVersion = "4.1.379"; if (version !== viewerVersion) { throw new Error(`The API version "${version}" does not match the Viewer version "${viewerVersion}".`); } @@ -8555,7 +8548,7 @@ class PDFViewer { return new Set(this.#buffer); } get pageViewsReady() { - return this._pagesCapability.settled && this._pages.every(pageView => pageView?.pdfPage); + return this._pages.every(pageView => pageView?.pdfPage); } get renderForms() { return this.#annotationMode === AnnotationMode.ENABLE_FORMS; @@ -8858,7 +8851,7 @@ class PDFViewer { }; this.eventBus._on("pagerender", this._onBeforeDraw); this._onAfterDraw = evt => { - if (evt.cssTransform || this._onePageRenderedCapability.settled) { + if (evt.cssTransform) { return; } this._onePageRenderedCapability.resolve({ @@ -9030,9 +9023,9 @@ class PDFViewer { this._location = null; this._pagesRotation = 0; this._optionalContentConfigPromise = null; - this._firstPageCapability = new PromiseCapability(); - this._onePageRenderedCapability = new PromiseCapability(); - this._pagesCapability = new PromiseCapability(); + this._firstPageCapability = Promise.withResolvers(); + this._onePageRenderedCapability = Promise.withResolvers(); + this._pagesCapability = Promise.withResolvers(); this._scrollMode = ScrollMode.VERTICAL; this._previousScrollMode = ScrollMode.UNKNOWN; this._spreadMode = SpreadMode.NONE; @@ -10543,7 +10536,10 @@ const ViewOnLoad = { }; const PDFViewerApplication = { initialBookmark: document.location.hash.substring(1), - _initializedCapability: new PromiseCapability(), + _initializedCapability: { + ...Promise.withResolvers(), + settled: false + }, appConfig: null, pdfDocument: null, pdfLoadingTask: null, @@ -10617,6 +10613,7 @@ const PDFViewerApplication = { await this._initializeViewerComponents(); this.bindEvents(); this.bindWindowEvents(); + this._initializedCapability.settled = true; this._initializedCapability.resolve(); }, async _parseHashParams() { @@ -11708,6 +11705,10 @@ const PDFViewerApplication = { mediaQueryList.addEventListener("change", addWindowResolutionChange, { once: true }); + _boundEvents.removeWindowResolutionChange ||= function () { + mediaQueryList.removeEventListener("change", addWindowResolutionChange); + _boundEvents.removeWindowResolutionChange = null; + }; } addWindowResolutionChange(); _boundEvents.windowResize = () => { @@ -12631,8 +12632,8 @@ function webViewerReportTelemetry({ -const pdfjsVersion = "4.1.342"; -const pdfjsBuild = "e384df6f1"; +const pdfjsVersion = "4.1.379"; +const pdfjsBuild = "017e49244"; const AppConstants = null; window.PDFViewerApplication = PDFViewerApplication; window.PDFViewerApplicationConstants = AppConstants; diff --git a/toolkit/components/pdfjs/moz.yaml b/toolkit/components/pdfjs/moz.yaml index bd85f67f5a..0938b8d987 100644 --- a/toolkit/components/pdfjs/moz.yaml +++ b/toolkit/components/pdfjs/moz.yaml @@ -20,8 +20,8 @@ origin: # Human-readable identifier for this version/release # Generally "version NNN", "tag SSS", "bookmark SSS" - release: e384df6f16b9ad1c55d1bc325bcae7e096ef3f9b (2024-03-27T19:54:30Z). - revision: e384df6f16b9ad1c55d1bc325bcae7e096ef3f9b + release: a208d6bca72f8cb700f7bb506ea1bc3e76cbaec0 (2024-04-09T07:48:17Z). + revision: a208d6bca72f8cb700f7bb506ea1bc3e76cbaec0 # The package's license, where possible using the mnemonic from # https://spdx.org/licenses/ diff --git a/toolkit/components/pdfjs/test/browser_pdfjs_editing_contextmenu.js b/toolkit/components/pdfjs/test/browser_pdfjs_editing_contextmenu.js index 779a3a6ad4..f13d03d643 100644 --- a/toolkit/components/pdfjs/test/browser_pdfjs_editing_contextmenu.js +++ b/toolkit/components/pdfjs/test/browser_pdfjs_editing_contextmenu.js @@ -130,7 +130,7 @@ function assertMenuitems(menuitems, expected) { elmt => !elmt.id.includes("-sep-") && !elmt.hidden && - ["", "false"].includes(elmt.getAttribute("disabled")) + [null, "false"].includes(elmt.getAttribute("disabled")) ) .map(elmt => elmt.id), expected diff --git a/toolkit/components/pdfjs/test/browser_pdfjs_find.js b/toolkit/components/pdfjs/test/browser_pdfjs_find.js index 24a3a50967..b3b3e11152 100644 --- a/toolkit/components/pdfjs/test/browser_pdfjs_find.js +++ b/toolkit/components/pdfjs/test/browser_pdfjs_find.js @@ -13,7 +13,7 @@ const OS_PDF_URL = TESTROOT + "file_pdfjs_object_stream.pdf"; const TEST_PDF_URL = TESTROOT + "file_pdfjs_test.pdf"; add_task(async function test_find_octet_stream_pdf() { - await BrowserTestUtils.withNewTab(OS_PDF_URL, async browser => { + await BrowserTestUtils.withNewTab(OS_PDF_URL, async () => { let findEls = ["cmd_find", "cmd_findAgain", "cmd_findPrevious"].map(id => document.getElementById(id) ); diff --git a/toolkit/components/pdfjs/test/browser_pdfjs_force_opening_files.js b/toolkit/components/pdfjs/test/browser_pdfjs_force_opening_files.js index 0490a6eec3..f09581daa7 100644 --- a/toolkit/components/pdfjs/test/browser_pdfjs_force_opening_files.js +++ b/toolkit/components/pdfjs/test/browser_pdfjs_force_opening_files.js @@ -13,7 +13,7 @@ add_task(async function test_file_opening() { // the default - because files from disk should always use pdfjs, unless // it is forcibly disabled. let openedWindow = false; - let windowOpenedPromise = new Promise((resolve, reject) => { + let windowOpenedPromise = new Promise(resolve => { addWindowListener( "chrome://mozapps/content/downloads/unknownContentType.xhtml", () => { @@ -82,7 +82,7 @@ function addWindowListener(aURL, aCallback) { aCallback(); }, domwindow); }, - onCloseWindow(aXULWindow) {}, + onCloseWindow() {}, }; Services.wm.addListener(listener); listenerCleanup = () => Services.wm.removeListener(listener); diff --git a/toolkit/components/pdfjs/test/browser_pdfjs_fullscreen.js b/toolkit/components/pdfjs/test/browser_pdfjs_fullscreen.js index d53e4cfa74..4add7a4715 100644 --- a/toolkit/components/pdfjs/test/browser_pdfjs_fullscreen.js +++ b/toolkit/components/pdfjs/test/browser_pdfjs_fullscreen.js @@ -8,7 +8,7 @@ function waitForFullScreenState(browser, state) { return new Promise(resolve => { let eventReceived = false; - let observe = (subject, topic, data) => { + let observe = () => { if (!eventReceived) { return; } diff --git a/toolkit/components/pdfjs/test/browser_pdfjs_navigation.js b/toolkit/components/pdfjs/test/browser_pdfjs_navigation.js index 9851150e80..5fda1ac8b2 100644 --- a/toolkit/components/pdfjs/test/browser_pdfjs_navigation.js +++ b/toolkit/components/pdfjs/test/browser_pdfjs_navigation.js @@ -229,7 +229,7 @@ async function contentSetUp() { return new Promise(resolve => { document.addEventListener( "pagerendered", - function (e) { + function () { document.querySelector("#viewer").click(); resolve(); }, diff --git a/toolkit/components/pdfjs/test/browser_pdfjs_octet_stream.js b/toolkit/components/pdfjs/test/browser_pdfjs_octet_stream.js index d2b4fe310f..8e81ddb2c5 100644 --- a/toolkit/components/pdfjs/test/browser_pdfjs_octet_stream.js +++ b/toolkit/components/pdfjs/test/browser_pdfjs_octet_stream.js @@ -88,7 +88,7 @@ add_task(async function test_octet_stream_in_frame() { await BrowserTestUtils.withNewTab( { gBrowser, url: `data:text/html,<iframe src='${PDF_URL}'>` }, - async function (newTabBrowser) { + async function () { // wait until downloadsPanel opens before continuing with test info("Waiting for download panel to open"); await downloadsPanelPromise; diff --git a/toolkit/components/pdfjs/test/browser_pdfjs_savedialog.js b/toolkit/components/pdfjs/test/browser_pdfjs_savedialog.js index 4f0c952ebb..e514eaf530 100644 --- a/toolkit/components/pdfjs/test/browser_pdfjs_savedialog.js +++ b/toolkit/components/pdfjs/test/browser_pdfjs_savedialog.js @@ -48,6 +48,6 @@ function addWindowListener(aURL, aCallback) { aCallback(); }, domwindow); }, - onCloseWindow(aXULWindow) {}, + onCloseWindow() {}, }); } diff --git a/toolkit/components/places/PlacesFrecencyRecalculator.sys.mjs b/toolkit/components/places/PlacesFrecencyRecalculator.sys.mjs index 7c52c508ee..a1df8cac89 100644 --- a/toolkit/components/places/PlacesFrecencyRecalculator.sys.mjs +++ b/toolkit/components/places/PlacesFrecencyRecalculator.sys.mjs @@ -59,12 +59,31 @@ XPCOMUtils.defineLazyPreferenceGetter( 90 ); +// This pref stores whether recalculation should be faster. +// It is set when we detect that a lot of changes happened recently, and it +// will survive restarts. Once there's nothing left to recalculate, we unset +// the pref and return to the normal recalculation rate. +// Note this getter transforms the boolean pref value into an integer +// acceleration rate. +const PREF_ACCELERATE_RECALCULATION = "places.frecency.accelerateRecalculation"; +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "accelerationRate", + PREF_ACCELERATE_RECALCULATION, + false, + null, + accelerate => (accelerate ? 2 : 1) +); + // Time between deferred task executions. const DEFERRED_TASK_INTERVAL_MS = 2 * 60000; // Maximum time to wait for an idle before the task is executed anyway. const DEFERRED_TASK_MAX_IDLE_WAIT_MS = 5 * 60000; // Number of entries to update at once. const DEFAULT_CHUNK_SIZE = 50; +// Threshold used to evaluate whether the number of Places events from the last +// recalculation is high enough to deserve a recalculation rate increase. +const ACCELERATION_EVENTS_THRESHOLD = 250; export class PlacesFrecencyRecalculator { classID = Components.ID("1141fd31-4c1a-48eb-8f1a-2f05fad94085"); @@ -104,11 +123,8 @@ export class PlacesFrecencyRecalculator { return; } - this.#task = new lazy.DeferredTask( - this.#taskFn.bind(this), - DEFERRED_TASK_INTERVAL_MS, - DEFERRED_TASK_MAX_IDLE_WAIT_MS - ); + this.#createOrUpdateTask(); + lazy.AsyncShutdown.profileChangeTeardown.addBlocker( "PlacesFrecencyRecalculator: shutdown", () => this.#finalize() @@ -133,6 +149,22 @@ export class PlacesFrecencyRecalculator { this.maybeStartFrecencyRecalculation(); } + #createOrUpdateTask() { + let wasArmed = this.#task?.isArmed; + if (this.#task) { + this.#task.disarm(); + this.#task.finalize().catch(console.error); + } + this.#task = new lazy.DeferredTask( + this.#taskFn.bind(this), + DEFERRED_TASK_INTERVAL_MS / lazy.accelerationRate, + DEFERRED_TASK_MAX_IDLE_WAIT_MS / lazy.accelerationRate + ); + if (wasArmed) { + this.#task.arm(); + } + } + async #taskFn() { if (this.#task.isFinalized) { return; @@ -161,6 +193,40 @@ export class PlacesFrecencyRecalculator { this.#task.finalize().catch(console.error); } + #lastEventsCount = 0; + + /** + * Evaluates whether recalculation speed should be increased, and eventually + * accelerates. + * @returns {boolean} whether the recalculation rate is increased. + */ + maybeUpdateRecalculationSpeed() { + if (lazy.accelerationRate > 1) { + return true; + } + // We mostly care about additions to cover the common case of importing + // bookmarks or history. We may care about removals, but in most cases they + // reduce the number of entries to recalculate. + let eventsCount = + PlacesObservers.counts.get("page-visited") + + PlacesObservers.counts.get("bookmark-added"); + let accelerate = + eventsCount - this.#lastEventsCount > ACCELERATION_EVENTS_THRESHOLD; + if (accelerate) { + Services.prefs.setBoolPref(PREF_ACCELERATE_RECALCULATION, true); + this.#createOrUpdateTask(); + } + this.#lastEventsCount = eventsCount; + return accelerate; + } + + #resetRecalculationSpeed() { + if (lazy.accelerationRate > 1) { + Services.prefs.clearUserPref(PREF_ACCELERATE_RECALCULATION); + this.#createOrUpdateTask(); + } + } + /** * Updates a chunk of outdated frecency values. If there's more frecency * values to update at the end of the process, it may rearm the task. @@ -169,6 +235,8 @@ export class PlacesFrecencyRecalculator { * @resolves {boolean} Whether any entry was recalculated. */ async recalculateSomeFrecencies({ chunkSize = DEFAULT_CHUNK_SIZE } = {}) { + // In case of acceleration we don't bump up the chunkSize to avoid issues + // with slow disk systems. lazy.logger.trace( `Recalculate ${chunkSize >= 0 ? chunkSize : "infinite"} frecency values` ); @@ -182,7 +250,7 @@ export class PlacesFrecencyRecalculator { WHERE id IN ( SELECT id FROM moz_places WHERE recalc_frecency = 1 - ORDER BY frecency DESC + ORDER BY frecency DESC, visit_count DESC LIMIT ${chunkSize} ) RETURNING id` @@ -212,8 +280,10 @@ export class PlacesFrecencyRecalculator { if (chunkSize > 0 && shouldRestartRecalculation) { // There's more entries to recalculate, rearm the task. + this.maybeUpdateRecalculationSpeed(); this.#task.arm(); } else { + this.#resetRecalculationSpeed(); // There's nothing left to recalculate, wait for the next change. lazy.PlacesUtils.history.shouldStartFrecencyRecalculation = false; this.#task.disarm(); @@ -360,6 +430,7 @@ export class PlacesFrecencyRecalculator { return; case "frecency-recalculation-needed": lazy.logger.trace("Frecency recalculation requested"); + this.maybeUpdateRecalculationSpeed(); this.maybeStartFrecencyRecalculation(); return; case "test-execute-taskFn": diff --git a/toolkit/components/places/PlacesQuery.sys.mjs b/toolkit/components/places/PlacesQuery.sys.mjs index 674e49b0ba..7ddf5ee988 100644 --- a/toolkit/components/places/PlacesQuery.sys.mjs +++ b/toolkit/components/places/PlacesQuery.sys.mjs @@ -58,7 +58,7 @@ export class PlacesQuery { #historyListenerCallback = null; /** @type {DeferredTask} */ #historyObserverTask = null; - #searchInProgress = false; + searchInProgress = false; /** * Get a snapshot of history visits at this moment. @@ -169,11 +169,11 @@ export class PlacesQuery { GROUP BY url ORDER BY ${orderBy} LIMIT ${limit > 0 ? limit : -1}`; - if (this.#searchInProgress) { + if (this.searchInProgress) { db.interrupt(); } try { - this.#searchInProgress = true; + this.searchInProgress = true; const rows = await db.executeCached(sql, { query, matchBehavior: Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE_UNMODIFIED, @@ -181,7 +181,7 @@ export class PlacesQuery { }); return rows.map(row => this.formatRowAsVisit(row)); } finally { - this.#searchInProgress = false; + this.searchInProgress = false; } } diff --git a/toolkit/components/places/PlacesTransactions.sys.mjs b/toolkit/components/places/PlacesTransactions.sys.mjs index bd0b7654a3..be922ec53f 100644 --- a/toolkit/components/places/PlacesTransactions.sys.mjs +++ b/toolkit/components/places/PlacesTransactions.sys.mjs @@ -31,11 +31,12 @@ * A note about GUIDs and item-ids * ------------------------------- * There's an ongoing effort (see bug 1071511) to deprecate item-ids in Places - * in favor of GUIDs. Both because new APIs (e.g. Bookmark.jsm) expose them to - * the minimum necessary, and because GUIDs play much better with implementing - * |redo|, this API doesn't support item-ids at all, and only accepts bookmark - * GUIDs, both for input (e.g. for setting the parent folder for a new bookmark) - * and for output (when the GUID for such a bookmark is propagated). + * in favor of GUIDs. Both because new APIs (e.g. Bookmarks.sys.mjs) expose them + * to the minimum necessary, and because GUIDs play much better with + * implementing |redo|, this API doesn't support item-ids at all, and only + * accepts bookmark GUIDs, both for input (e.g. for setting the parent folder + * for a new bookmark) and for output (when the GUID for such a bookmark is + * propagated). * * Constructing transactions * ------------------------- diff --git a/toolkit/components/places/SQLFunctions.cpp b/toolkit/components/places/SQLFunctions.cpp index 85c5cc8d17..de12b73583 100644 --- a/toolkit/components/places/SQLFunctions.cpp +++ b/toolkit/components/places/SQLFunctions.cpp @@ -1134,16 +1134,15 @@ GetQueryParamFunction::OnFunctionCall(mozIStorageValueArray* aArguments, RefPtr<nsVariant> result = new nsVariant(); if (!queryString.IsEmpty() && !paramName.IsEmpty()) { - URLParams::Parse( - queryString, true, - [¶mName, &result](const nsAString& aName, const nsAString& aValue) { - NS_ConvertUTF16toUTF8 name(aName); - if (!paramName.Equals(name)) { - return true; - } - result->SetAsAString(aValue); - return false; - }); + URLParams::Parse(queryString, true, + [¶mName, &result](const nsACString& aName, + const nsACString& aValue) { + if (!paramName.Equals(aName)) { + return true; + } + result->SetAsACString(aValue); + return false; + }); } result.forget(_result); diff --git a/toolkit/components/places/SyncedBookmarksMirror.sys.mjs b/toolkit/components/places/SyncedBookmarksMirror.sys.mjs index a09e1b7eb3..c68761a086 100644 --- a/toolkit/components/places/SyncedBookmarksMirror.sys.mjs +++ b/toolkit/components/places/SyncedBookmarksMirror.sys.mjs @@ -2060,11 +2060,7 @@ function validateURL(rawURL) { if (typeof rawURL != "string" || rawURL.length > DB_URL_LENGTH_MAX) { return null; } - let url = null; - try { - url = new URL(rawURL); - } catch (ex) {} - return url; + return URL.parse(rawURL); } function validateKeyword(rawKeyword) { diff --git a/toolkit/components/places/bookmark_sync/src/store.rs b/toolkit/components/places/bookmark_sync/src/store.rs index 6e8d8024b4..e5cf973e5d 100644 --- a/toolkit/components/places/bookmark_sync/src/store.rs +++ b/toolkit/components/places/bookmark_sync/src/store.rs @@ -1219,7 +1219,6 @@ trait Column<T> { fn from_column(raw: T) -> Result<Self> where Self: Sized; - fn into_column(self) -> T; } impl Column<i64> for Kind { @@ -1233,16 +1232,6 @@ impl Column<i64> for Kind { _ => return Err(Error::UnknownItemKind(raw)), }) } - - fn into_column(self) -> i64 { - match self { - Kind::Bookmark => mozISyncedBookmarksMerger::KIND_BOOKMARK as i64, - Kind::Query => mozISyncedBookmarksMerger::KIND_QUERY as i64, - Kind::Folder => mozISyncedBookmarksMerger::KIND_FOLDER as i64, - Kind::Livemark => mozISyncedBookmarksMerger::KIND_LIVEMARK as i64, - Kind::Separator => mozISyncedBookmarksMerger::KIND_SEPARATOR as i64, - } - } } impl Column<i64> for Validity { @@ -1254,14 +1243,6 @@ impl Column<i64> for Validity { _ => return Err(Error::UnknownItemValidity(raw).into()), }) } - - fn into_column(self) -> i64 { - match self { - Validity::Valid => mozISyncedBookmarksMerger::VALIDITY_VALID as i64, - Validity::Reupload => mozISyncedBookmarksMerger::VALIDITY_REUPLOAD as i64, - Validity::Replace => mozISyncedBookmarksMerger::VALIDITY_REPLACE as i64, - } - } } /// Formats an optional value so that it can be included in a SQL statement. diff --git a/toolkit/components/places/mozIAsyncHistory.idl b/toolkit/components/places/mozIAsyncHistory.idl index 88690b4798..27487ebc9b 100644 --- a/toolkit/components/places/mozIAsyncHistory.idl +++ b/toolkit/components/places/mozIAsyncHistory.idl @@ -107,8 +107,8 @@ interface mozIVisitInfoCallback : nsISupports * handleResult and handleError, respectively, if/once * results/errors occur. */ - readonly attribute bool ignoreResults; - readonly attribute bool ignoreErrors; + readonly attribute boolean ignoreResults; + readonly attribute boolean ignoreErrors; }; [scriptable, function, uuid(994092bf-936f-449b-8dd6-0941e024360d)] diff --git a/toolkit/components/places/mozISyncedBookmarksMirror.idl b/toolkit/components/places/mozISyncedBookmarksMirror.idl index 0ceaeff5ad..701b7a128d 100644 --- a/toolkit/components/places/mozISyncedBookmarksMirror.idl +++ b/toolkit/components/places/mozISyncedBookmarksMirror.idl @@ -34,7 +34,7 @@ interface mozISyncedBookmarksMirrorProgressListener : nsISupports { // A callback called when the merge finishes. [scriptable, uuid(d23fdfea-92c8-409d-a516-08ae395d578f)] interface mozISyncedBookmarksMirrorCallback : nsISupports { - void handleSuccess(in bool result); + void handleSuccess(in boolean result); void handleError(in nsresult code, in AString message); }; diff --git a/toolkit/components/places/tests/bookmarks/test_sync_fields.js b/toolkit/components/places/tests/bookmarks/test_sync_fields.js index 0a6f53fb85..b7b2ab4a1a 100644 --- a/toolkit/components/places/tests/bookmarks/test_sync_fields.js +++ b/toolkit/components/places/tests/bookmarks/test_sync_fields.js @@ -330,7 +330,7 @@ async function findTagFolder(tag) { return results.length ? results[0].getResultByName("guid") : null; } -// Exercises the new, async calls implemented in `Bookmarks.jsm`. +// Exercises the new, async calls implemented in `Bookmarks.sys.mjs`. class AsyncTestCases extends TestCases { async createFolder(parentGuid, title, index) { let item = await PlacesUtils.bookmarks.insert({ diff --git a/toolkit/components/places/tests/browser/browser_visituri.js b/toolkit/components/places/tests/browser/browser_visituri.js index 529c010e63..6aafe084e7 100644 --- a/toolkit/components/places/tests/browser/browser_visituri.js +++ b/toolkit/components/places/tests/browser/browser_visituri.js @@ -139,7 +139,7 @@ add_task(async function test_userpass() { ); // Open the target link as background. - await ContentTask.spawn(gBrowser.selectedBrowser, null, async args => { + await ContentTask.spawn(gBrowser.selectedBrowser, null, async () => { let link = content.document.getElementById("target-userpass"); EventUtils.synthesizeMouseAtCenter( link, diff --git a/toolkit/components/places/tests/browser/previews/browser_thumbnails.js b/toolkit/components/places/tests/browser/previews/browser_thumbnails.js index 27f4fa3745..4fbbafccf9 100644 --- a/toolkit/components/places/tests/browser/previews/browser_thumbnails.js +++ b/toolkit/components/places/tests/browser/previews/browser_thumbnails.js @@ -2,7 +2,7 @@ * https://creativecommons.org/publicdomain/zero/1.0/ */ /** - * Tests PlacesPreviews.jsm + * Tests PlacesPreviews.sys.mjs */ const { PlacesPreviews } = ChromeUtils.importESModule( "resource://gre/modules/PlacesPreviews.sys.mjs" diff --git a/toolkit/components/places/tests/history/test_hasVisits.js b/toolkit/components/places/tests/history/test_hasVisits.js index 36fc9fd7be..5c7b56a8f3 100644 --- a/toolkit/components/places/tests/history/test_hasVisits.js +++ b/toolkit/components/places/tests/history/test_hasVisits.js @@ -1,7 +1,7 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ -// Tests for `History.hasVisits` as implemented in History.jsm +// Tests for `History.hasVisits` as implemented in History.sys.mjs "use strict"; diff --git a/toolkit/components/places/tests/history/test_insert.js b/toolkit/components/places/tests/history/test_insert.js index a3a820ade9..ce83199516 100644 --- a/toolkit/components/places/tests/history/test_insert.js +++ b/toolkit/components/places/tests/history/test_insert.js @@ -1,7 +1,7 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ -// Tests for `History.insert` as implemented in History.jsm +// Tests for `History.insert` as implemented in History.sys.mjs "use strict"; diff --git a/toolkit/components/places/tests/history/test_insertMany.js b/toolkit/components/places/tests/history/test_insertMany.js index 3d0774cbf2..4fe1481d85 100644 --- a/toolkit/components/places/tests/history/test_insertMany.js +++ b/toolkit/components/places/tests/history/test_insertMany.js @@ -1,7 +1,7 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ -// Tests for `History.insertMany` as implemented in History.jsm +// Tests for `History.insertMany` as implemented in History.sys.mjs "use strict"; diff --git a/toolkit/components/places/tests/history/test_remove.js b/toolkit/components/places/tests/history/test_remove.js index 8c5e941fd0..c018a996f3 100644 --- a/toolkit/components/places/tests/history/test_remove.js +++ b/toolkit/components/places/tests/history/test_remove.js @@ -1,7 +1,7 @@ /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* vim:set ts=2 sw=2 sts=2 et: */ -// Tests for `History.remove`, as implemented in History.jsm +// Tests for `History.remove`, as implemented in History.sys.mjs "use strict"; diff --git a/toolkit/components/places/tests/history/test_removeMany.js b/toolkit/components/places/tests/history/test_removeMany.js index ff8c3a21ee..5c955af0e1 100644 --- a/toolkit/components/places/tests/history/test_removeMany.js +++ b/toolkit/components/places/tests/history/test_removeMany.js @@ -2,7 +2,7 @@ /* vim:set ts=2 sw=2 sts=2 et: */ // Tests for `History.remove` with removing many urls, as implemented in -// History.jsm. +// History.sys.mjs. "use strict"; diff --git a/toolkit/components/places/tests/history/test_removeVisitsByFilter.js b/toolkit/components/places/tests/history/test_removeVisitsByFilter.js index be01fcb901..1045245d89 100644 --- a/toolkit/components/places/tests/history/test_removeVisitsByFilter.js +++ b/toolkit/components/places/tests/history/test_removeVisitsByFilter.js @@ -1,7 +1,7 @@ /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* vim:set ts=2 sw=2 sts=2 et: */ -// Tests for `History.removeVisitsByFilter`, as implemented in History.jsm +// Tests for `History.removeVisitsByFilter`, as implemented in History.sys.mjs "use strict"; diff --git a/toolkit/components/places/tests/history/test_update.js b/toolkit/components/places/tests/history/test_update.js index d7beafd368..df5bcc7195 100644 --- a/toolkit/components/places/tests/history/test_update.js +++ b/toolkit/components/places/tests/history/test_update.js @@ -3,7 +3,7 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ -// Tests for `History.update` as implemented in History.jsm +// Tests for `History.update` as implemented in History.sys.mjs "use strict"; diff --git a/toolkit/components/places/tests/sync/test_bookmark_mirror_migration.js b/toolkit/components/places/tests/sync/test_bookmark_mirror_migration.js index 86cf45eb0f..774ee9a1bb 100644 --- a/toolkit/components/places/tests/sync/test_bookmark_mirror_migration.js +++ b/toolkit/components/places/tests/sync/test_bookmark_mirror_migration.js @@ -1,7 +1,7 @@ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ -// Keep in sync with `SyncedBookmarksMirror.jsm`. +// Keep in sync with `SyncedBookmarksMirror.sys.mjs`. const CURRENT_MIRROR_SCHEMA_VERSION = 9; // The oldest schema version that we support. Any databases with schemas older diff --git a/toolkit/components/places/tests/unit/test_PlacesObservers_counts.js b/toolkit/components/places/tests/unit/test_PlacesObservers_counts.js new file mode 100644 index 0000000000..3eb7c2d2a9 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_PlacesObservers_counts.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test PlacesObservers.counts. + +add_task(async function test_counts() { + const url = "http://example.com/title"; + await PlacesUtils.history.insertMany([ + { + title: "will change", + url, + visits: [{ transition: TRANSITION_LINK }], + }, + { + title: "changed", + url, + referrer: url, + visits: [{ transition: TRANSITION_LINK }], + }, + { + title: "another", + url: "http://example.com/another", + visits: [{ transition: TRANSITION_LINK }], + }, + ]); + await PlacesUtils.bookmarks.insert({ + url, + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + }); + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything; + + Assert.strictEqual( + PlacesObservers.counts.get("non-existing"), + undefined, + "Check non existing event returns undefined" + ); + Assert.strictEqual( + PlacesObservers.counts.get("page-removed"), + 0, + "Check non fired event returns 0" + ); + Assert.strictEqual( + PlacesObservers.counts.get("page-visited"), + 3, + "Check fired event `page-visited`" + ); + Assert.strictEqual( + PlacesObservers.counts.get("history-cleared"), + 1, + "Check fired event `history-cleared`" + ); + Assert.strictEqual( + PlacesObservers.counts.get("bookmark-added"), + 1, + "Check fired event `bookmark-added`" + ); +}); diff --git a/toolkit/components/places/tests/unit/test_frecency_observers.js b/toolkit/components/places/tests/unit/test_frecency_observers.js index 44747b06f9..bc7a7ca099 100644 --- a/toolkit/components/places/tests/unit/test_frecency_observers.js +++ b/toolkit/components/places/tests/unit/test_frecency_observers.js @@ -37,7 +37,7 @@ add_task(async function test_nsNavHistory_UpdateFrecency() { await promise; }); -// History.jsm invalidateFrecencies() +// History.sys.mjs invalidateFrecencies() add_task(async function test_invalidateFrecencies() { let url = Services.io.newURI("http://test-invalidateFrecencies.com/"); // Bookmarking the URI is enough to add it to moz_places, and importantly, it @@ -54,7 +54,7 @@ add_task(async function test_invalidateFrecencies() { await promise; }); -// History.jsm clear() should not cause a frecency recalculation since pages +// History.sys.mjs clear() should not cause a frecency recalculation since pages // are removed. add_task(async function test_clear() { let received = []; diff --git a/toolkit/components/places/tests/unit/test_frecency_recalculator.js b/toolkit/components/places/tests/unit/test_frecency_recalculator.js index ed4501c56a..414882eaf5 100644 --- a/toolkit/components/places/tests/unit/test_frecency_recalculator.js +++ b/toolkit/components/places/tests/unit/test_frecency_recalculator.js @@ -176,3 +176,19 @@ add_task(async function test_chunk_time_telemetry() { "Should still not have set shouldStartFrecencyRecalculation" ); }); + +add_task(async function test_acceleration() { + await PlacesTestUtils.addVisits( + new Array(300).fill("https://www.mozilla.org/") + ); + + Assert.ok( + PlacesFrecencyRecalculator.maybeUpdateRecalculationSpeed(), + "Recalculation accelerated" + ); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + Assert.ok( + !PlacesFrecencyRecalculator.maybeUpdateRecalculationSpeed(), + "Recalculation back to normal rate" + ); +}); diff --git a/toolkit/components/places/tests/unit/test_origins_parsing.js b/toolkit/components/places/tests/unit/test_origins_parsing.js index bdeabce271..8372e9fb66 100644 --- a/toolkit/components/places/tests/unit/test_origins_parsing.js +++ b/toolkit/components/places/tests/unit/test_origins_parsing.js @@ -71,7 +71,7 @@ add_task(async function parsing() { // The history cannot be deleted at a URL with a user path. } else { expectedOrigins = expectedOrigins.filter( - ([prefix, hostPort]) => !prefix.startsWith(uri.scheme + ":") + ([prefix]) => !prefix.startsWith(uri.scheme + ":") ); } await checkDB(expectedOrigins); diff --git a/toolkit/components/places/tests/unit/xpcshell.toml b/toolkit/components/places/tests/unit/xpcshell.toml index 8a56fcc370..62de0c0db7 100644 --- a/toolkit/components/places/tests/unit/xpcshell.toml +++ b/toolkit/components/places/tests/unit/xpcshell.toml @@ -179,6 +179,8 @@ prefs = ["places.frecency.pages.alternative.featureGate=true"] ["test_pageGuid_bookmarkGuid.js"] +["test_PlacesObservers_counts.js"] + ["test_placeURIs.js"] ["test_promiseBookmarksTree.js"] diff --git a/toolkit/components/printing/content/print.css b/toolkit/components/printing/content/print.css index c510c58bbc..fe84c93eb0 100644 --- a/toolkit/components/printing/content/print.css +++ b/toolkit/components/printing/content/print.css @@ -2,7 +2,12 @@ * 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/. */ -html, body { +:root { + font: menu; +} + +:root, +body { height: 100vh; } @@ -13,21 +18,17 @@ body { overflow: hidden; } -body[loading] #print { - visibility: hidden; -} - *[hidden] { display: none !important; } .section-block { - margin: 16px; -} + margin: var(--space-large); -.section-block .block-label { - display: block; - margin-bottom: 8px; + .block-label { + display: block; + margin-bottom: var(--space-small); + } } .row { @@ -35,24 +36,23 @@ body[loading] #print { flex-direction: column; width: 100%; min-height: 1.8em; - margin-block: 2px; -} + margin-block: var(--space-xxsmall); -.row.cols-2 { - flex-direction: row; - align-items: center; -} + &.cols-2 { + flex-direction: row; + align-items: center; -#scale .row.cols-2, -#more-settings-options > .row.cols-2 { - /* The margin-block of the checkboxes/radiobuttons - already provide the needed vertical spacing. */ - margin-block: 0; + #scale &, + #more-settings-options > & { + /* The margin-block of the checkboxes/radiobuttons + already provide the needed vertical spacing. */ + margin-block: 0; + } + } } hr { - margin-inline: 8px; - margin-block: 0; + margin: 0 var(--space-small); } .header-container { @@ -61,236 +61,210 @@ hr { justify-content: space-between; align-items: center; flex: 0 1 auto; - padding: 8px 18px; -} -.header-container > h2 { - margin: 0; - font-size: 17px; - line-height: 1; + padding: var(--space-small) var(--space-large); + + > h2 { + margin: 0; + line-height: 1; + } } #sheet-count { - font-size: 11px; - padding: 3px 8px; + font: message-box; + padding-block: var(--space-xsmall); margin: 0; -} -#sheet-count[loading], -body[invalid] #sheet-count { - visibility: hidden; + &[loading], + body[invalid] & { + visibility: hidden; + } } -form#print { +#print { display: flex; flex: 1 1 auto; flex-direction: column; justify-content: flex-start; overflow: hidden; - font: menu; + + body[loading] & { + visibility: hidden; + } +} + +.body-container { + flex: 1 1 auto; + overflow: auto; } select { margin: 0; -} + width: 100%; -select:not([size], [multiple])[iconic] { - padding-inline-start: calc(8px + 16px + 4px); /* spacing before image + image width + spacing after image */ - background-size: auto 12px, 16px; - background-position: right 19px center, left 8px center; -} + &:not([size], [multiple])[iconic] { + padding-inline-start: calc(8px + 16px + 4px); /* spacing before image + image width + spacing after image */ + background-size: auto 12px, 16px; + background-position: right 19px center, left 8px center; -select:not([size], [multiple])[iconic]:dir(rtl) { - background-position-x: left 19px, right 8px; + &:dir(rtl) { + background-position-x: left 19px, right 8px; + } + } } #printer-picker { background-image: url("chrome://global/skin/icons/arrow-down-12.svg"), url("chrome://global/skin/icons/print.svg"); - width: 100%; -} - -#printer-picker[output="pdf"] { - background-image: url("chrome://global/skin/icons/arrow-down-12.svg"), url("chrome://global/skin/icons/page-portrait.svg"); -} -input[type="number"], -input[type="text"] { - padding: 2px; - padding-inline-start: 8px; + &[output="pdf"] { + background-image: url("chrome://global/skin/icons/arrow-down-12.svg"), url("chrome://global/skin/icons/page-portrait.svg"); + } } -.toggle-group-label { - padding: 4px 8px; +input:is([type="number"], [type="text"]) { + padding: var(--space-xxsmall); + padding-inline-start: var(--space-small); } -.body-container { - flex: 1 1 auto; - overflow: auto; +input[type="number"] { + box-sizing: content-box; + min-height: unset; + padding: 0; + padding-inline-start: var(--space-small); + margin: 0; + height: 1.5em; + width: 4em; } -.twisty > summary { - list-style: none; - display: flex; - cursor: pointer; - align-items: center; +input:is([type="checkbox"], [type="radio"]):disabled + label, +input[type="radio"]:disabled + span > label { + opacity: 0.5; } -#more-settings > summary > .twisty { - background-image: url("chrome://global/skin/icons/arrow-down-12.svg"); - background-repeat: no-repeat; - background-position: center; - -moz-context-properties: fill; - fill: currentColor; - width: 12px; - height: 12px; +#custom-range { + margin: 0; + margin-top: var(--space-xsmall); + width: 100%; } -#more-settings > summary > .label { - flex-grow: 1; -} +#percent-scale { + margin-inline-start: var(--space-xsmall); -#more-settings[open] > summary > .twisty { - background-image: url("chrome://global/skin/icons/arrow-up-12.svg"); + &:disabled { + /* Let clicks on the disabled input select the radio button */ + pointer-events: none; + } } -#open-dialog-link { - display: flex; - justify-content: space-between; - align-items: center; -} +.toggle-group-label { + padding: var(--space-xsmall) var(--space-small); -#open-dialog-link::after { - display: block; - flex-shrink: 0; - content: ""; - background: url(chrome://global/skin/icons/open-in-new.svg) no-repeat center; - background-size: 12px; - -moz-context-properties: fill; - fill: currentColor; - width: 12px; - height: 12px; -} + #landscape + &::before { + content: url("chrome://global/skin/icons/page-landscape.svg"); + } -#open-dialog-link:dir(rtl)::after { - scale: -1 1; + #portrait + &::before { + content: url("chrome://global/skin/icons/page-portrait.svg"); + } } -.footer-container { - flex: 0 1 auto; -} +#more-settings { + > summary { + list-style: none; + display: flex; + cursor: pointer; + align-items: center; -#print-progress { - background-image: image-set( - url("chrome://global/skin/icons/loading.png"), - url("chrome://global/skin/icons/loading@2x.png") 2x - ); - background-repeat: no-repeat; - background-size: 16px; - background-position: left center; - padding-inline-start: 20px; -} + > .twisty { + background-image: url("chrome://global/skin/icons/arrow-down-12.svg"); + background-repeat: no-repeat; + background-position: center; + -moz-context-properties: fill; + fill: currentColor; + width: 12px; + height: 12px; + } -#print-progress:dir(rtl) { - background-position-x: right; -} + > .label { + flex-grow: 1; + } + } -#custom-range { - margin-top: 4px; - margin-inline: 0; + &[open] > summary > .twisty { + background-image: url("chrome://global/skin/icons/arrow-up-12.svg"); + } } .vertical-margins, .horizontal-margins { display: flex; - gap: 20px; - margin-block: 5px; -} + gap: var(--space-large); + margin-block: var(--space-xsmall); -.margin-pair { - width: calc(50% - 10px); /* Half minus the gap */ -} + > .margin-pair { + width: calc(50% - (2 * var(--space-xsmall))); /* Half minus the gap */ -.margin-input { - /* FIXME: Important is needed to override the photon-number styles below, - * probably should be refactored */ - width: 100% !important; - box-sizing: border-box; -} + > .margin-descriptor { + display: block; + text-align: center; + margin-top: var(--space-xxsmall); + } -.margin-descriptor { - text-align: center; - display: block; - margin-top: 2px; + > .margin-input { + width: 100%; + box-sizing: border-box; + } + } } -.toggle-group #landscape + .toggle-group-label::before { - content: url("chrome://global/skin/icons/page-landscape.svg"); -} -.toggle-group #portrait + .toggle-group-label::before { - content: url("chrome://global/skin/icons/page-portrait.svg"); -} - -.error-message { - font-size: 12px; - color: var(--text-color-error); - margin-top: 4px; -} - -#percent-scale { - margin-inline-start: 4px; +.horizontal-margins:dir(rtl) { + /* Swap between the left and right margin inputs to match their position + * in the form to their physical location they represent. */ + flex-direction: row-reverse; } -#percent-scale:disabled { - /* Let clicks on the disabled input select the radio button */ - pointer-events: none; -} +#open-dialog-link { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--space-xsmall); -input[type="number"].photon-number { - padding: 0; - padding-inline-start: 8px; - margin: 0; - height: 1.5em; - width: 4em; -} + &::after { + display: block; + flex-shrink: 0; + content: ""; + background: url(chrome://global/skin/icons/open-in-new.svg) no-repeat center; + background-size: 12px; + -moz-context-properties: fill; + fill: currentColor; + width: 12px; + height: 12px; + } -input[type="number"].photon-number::-moz-number-spin-box { - height: 100%; - max-height: 100%; - border-inline-start: 1px solid var(--in-content-box-border-color); - width: 1em; - border-start-end-radius: 4px; - border-end-end-radius: 4px; + &:dir(rtl)::after { + transform: scaleX(-1); + } } -input[type="number"].photon-number:hover::-moz-number-spin-box { - border-color: var(--in-content-border-hover); +.footer-container { + flex: 0 1 auto; } -input[type="number"].photon-number::-moz-number-spin-up, -input[type="number"].photon-number::-moz-number-spin-down { - border: 0; - border-radius: 0; - background-color: var(--in-content-button-background); - background-image: url("chrome://global/skin/icons/arrow-down.svg"); +#print-progress { + background-image: image-set( + url("chrome://global/skin/icons/loading.png"), + url("chrome://global/skin/icons/loading@2x.png") 2x + ); background-repeat: no-repeat; - background-size: 8px; - background-position: center; - -moz-context-properties: fill; - fill: currentColor; - appearance: none; -} - -input[type="number"].photon-number::-moz-number-spin-up { - background-image: url("chrome://global/skin/icons/arrow-up.svg"); -} + background-size: var(--size-item-small); + background-position: left center; + padding-inline-start: calc(var(--size-item-small) + var(--space-xsmall)); -input[type="number"].photon-number::-moz-number-spin-up:hover, -input[type="number"].photon-number::-moz-number-spin-down:hover { - background-color: var(--in-content-button-background-hover); - color: var(--in-content-button-text-color-hover); + &:dir(rtl) { + background-position-x: right; + } } -input[type="checkbox"]:disabled + label, -input[type="radio"]:disabled + label, -input[type="radio"]:disabled + span > label { - opacity: 0.5; +.error-message { + color: var(--text-color-error); + margin-top: var(--space-xsmall); } diff --git a/toolkit/components/printing/content/print.html b/toolkit/components/printing/content/print.html index 62d214608c..4e129c3f66 100644 --- a/toolkit/components/printing/content/print.html +++ b/toolkit/components/printing/content/print.html @@ -136,7 +136,6 @@ will filter this down to visible items only. --> <input id="percent-scale" - class="photon-number" is="setting-number" min="10" max="200" @@ -166,7 +165,7 @@ data-setting-name="numCopies" min="1" max="10000" - class="copy-count-input photon-number" + class="copy-count-input" aria-errormessage="error-invalid-copies" required /> @@ -189,7 +188,6 @@ is="margins-select" id="margins-picker" name="margin-type" - class="row" data-setting-name="margins" > <option value="default" data-l10n-id="printui-margins-default"></option> @@ -206,7 +204,7 @@ <input is="setting-number" id="custom-margin-top" - class="margin-input photon-number" + class="margin-input" aria-describedby="margins-custom-margin-top-desc" min="0" step="0.01" @@ -227,7 +225,7 @@ <input is="setting-number" id="custom-margin-bottom" - class="margin-input photon-number" + class="margin-input" aria-describedby="margins-custom-margin-bottom-desc" min="0" step="0.01" @@ -250,7 +248,7 @@ <input is="setting-number" id="custom-margin-left" - class="margin-input photon-number" + class="margin-input" aria-describedby="margins-custom-margin-left-desc" min="0" step="0.01" @@ -271,7 +269,7 @@ <input is="setting-number" id="custom-margin-right" - class="margin-input photon-number" + class="margin-input" aria-describedby="margins-custom-margin-right-desc" min="0" step="0.01" @@ -322,14 +320,12 @@ class="block-label" data-l10n-id="printui-destination-label" ></label> - <div class="printer-picker-wrapper"> - <select - is="destination-picker" - id="printer-picker" - data-setting-name="printerName" - iconic - ></select> - </div> + <select + is="destination-picker" + id="printer-picker" + data-setting-name="printerName" + iconic + ></select> </section> <section id="settings"> <section id="copies" class="section-block"> @@ -357,15 +353,11 @@ <section id="pages" class="section-block"> <label - for="page-range-input" + for="range-picker" class="block-label" data-l10n-id="printui-page-range-label" ></label> - <div - id="page-range-input" - is="page-range-input" - class="page-range-input row" - ></div> + <div id="page-range-input" is="page-range-input"></div> </section> <section id="color-mode" class="section-block"> @@ -377,7 +369,6 @@ <select is="color-mode-select" id="color-mode-picker" - class="row" data-setting-name="printInColor" > <option @@ -408,7 +399,6 @@ <select is="paper-size-select" id="paper-size-picker" - class="row" data-setting-name="paperId" ></select> </section> @@ -432,7 +422,6 @@ <select is="setting-select" id="pages-per-sheet-picker" - class="row" data-setting-name="numPagesPerSheet" > <option value="1">1</option> @@ -445,11 +434,7 @@ </section> <section id="margins" class="section-block"> - <div - id="margins-select" - is="margins-select" - class="margins-select row" - ></div> + <div id="margins-select" is="margins-select"></div> </section> <section id="two-sided-printing" class="section-block"> @@ -462,7 +447,6 @@ is="setting-select" id="duplex-select" name="duplex-type" - class="row" data-setting-name="printDuplex" > <option diff --git a/toolkit/components/printing/content/print.js b/toolkit/components/printing/content/print.js index 3398b28ace..1c3be1728b 100644 --- a/toolkit/components/printing/content/print.js +++ b/toolkit/components/printing/content/print.js @@ -46,7 +46,7 @@ var logger = (function () { return _logger; })(); -function serializeSettings(settings, logPrefix) { +function serializeSettings(settings) { let re = /^(k[A-Z]|resolution)/; // accessing settings.resolution throws an exception? let types = new Set(["string", "boolean", "number", "undefined"]); let nameValues = {}; @@ -80,7 +80,7 @@ function cancelDeferredTasks() { document.addEventListener( "DOMContentLoaded", - e => { + () => { window._initialized = PrintEventHandler.init().catch(e => console.error(e)); ourBrowser.setAttribute("flex", "0"); ourBrowser.setAttribute("constrainpopups", "false"); @@ -96,7 +96,7 @@ window.addEventListener("dialogclosing", () => { window.addEventListener( "unload", - e => { + () => { document.textContent = ""; }, { once: true } @@ -1608,7 +1608,7 @@ function PrintUIControlMixin(superClass) { render() {} - update(settings) {} + update() {} dispatchSettingsChange(changedSettings) { this.dispatchEvent( @@ -1628,7 +1628,7 @@ function PrintUIControlMixin(superClass) { ); } - handleEvent(event) {} + handleEvent() {} }; } @@ -1863,7 +1863,7 @@ class PrintSettingCheckbox extends PrintUIControlMixin(HTMLInputElement) { this.checked = settings[this.settingName]; } - handleEvent(e) { + handleEvent() { this.dispatchSettingsChange({ [this.settingName]: this.checked, }); @@ -1884,7 +1884,7 @@ class PrintSettingRadio extends PrintUIControlMixin(HTMLInputElement) { this.checked = settings[this.settingName] == this.value; } - handleEvent(e) { + handleEvent() { this.dispatchSettingsChange({ [this.settingName]: this.value, }); @@ -2020,7 +2020,7 @@ class CopiesInput extends PrintUIControlMixin(HTMLElement) { this._copiesError.hidden = true; } - handleEvent(e) { + handleEvent() { this._copiesError.hidden = this._copiesInput.checkValidity(); } } @@ -2679,7 +2679,7 @@ class TwistySummary extends PrintUIControlMixin(HTMLElement) { this.updateSummary(shouldOpen); } - handleEvent(e) { + handleEvent() { let willOpen = !this.isOpen; Services.prefs.setBoolPref("print.more-settings.open", willOpen); this.updateSummary(willOpen); diff --git a/toolkit/components/printing/content/toggle-group.css b/toolkit/components/printing/content/toggle-group.css index 94922eb963..4416bfa512 100644 --- a/toolkit/components/printing/content/toggle-group.css +++ b/toolkit/components/printing/content/toggle-group.css @@ -2,7 +2,6 @@ * 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/. */ - /* * A radiogroup styled to hide the radio buttons * and present tab-like labels as buttons @@ -10,6 +9,7 @@ .toggle-group { display: inline-flex; + min-height: var(--button-min-height-small); } .toggle-group-label { @@ -26,21 +26,21 @@ } .toggle-group-label-iconic::before { - width: 16px; - height: 16px; + width: var(--size-item-small); + height: var(--size-item-small); margin-inline-end: 5px; -moz-context-properties: fill; fill: currentColor; } .toggle-group-label:first-of-type { - border-start-start-radius: 4px; - border-end-start-radius: 4px; + border-start-start-radius: var(--border-radius-small); + border-end-start-radius: var(--border-radius-small); } .toggle-group-label:last-of-type { - border-start-end-radius: 4px; - border-end-end-radius: 4px; + border-start-end-radius: var(--border-radius-small); + border-end-end-radius: var(--border-radius-small); } .toggle-group-input:disabled + .toggle-group-label { diff --git a/toolkit/components/printing/tests/browser_preview_navigation.js b/toolkit/components/printing/tests/browser_preview_navigation.js index 53e54eddb0..23b3a9b7cc 100644 --- a/toolkit/components/printing/tests/browser_preview_navigation.js +++ b/toolkit/components/printing/tests/browser_preview_navigation.js @@ -23,7 +23,7 @@ async function waitForPageStatusUpdate(elem, expected, message) { ); } -async function waitUntilVisible(elem, visible = true) { +async function waitUntilVisible(elem) { await TestUtils.waitForCondition( () => BrowserTestUtils.isVisible(elem) && getComputedStyle(elem).opacity == "1", diff --git a/toolkit/components/printing/tests/file_window_print_reentrant.html b/toolkit/components/printing/tests/file_window_print_reentrant.html index 7eebd8c2fe..2008ffaf27 100644 --- a/toolkit/components/printing/tests/file_window_print_reentrant.html +++ b/toolkit/components/printing/tests/file_window_print_reentrant.html @@ -3,7 +3,7 @@ <script> let count = 0; onload = function() { - window.addEventListener('beforeprint', (event) => { + window.addEventListener('beforeprint', () => { document.getElementById("before-print-count").innerText = ++count; print() }); diff --git a/toolkit/components/printing/tests/head.js b/toolkit/components/printing/tests/head.js index 4c8c0b26b6..e2a83463a6 100644 --- a/toolkit/components/printing/tests/head.js +++ b/toolkit/components/printing/tests/head.js @@ -60,6 +60,9 @@ class PrintHelper { } static getTestPageUrl(pathName) { + if (pathName.startsWith("http://")) { + return pathName; + } const testPath = getRootDirectory(gTestPath).replace( "chrome://mochitests/content", "http://example.com" @@ -68,6 +71,9 @@ class PrintHelper { } static getTestPageUrlHTTPS(pathName) { + if (pathName.startsWith("https://")) { + return pathName; + } const testPath = getRootDirectory(gTestPath).replace( "chrome://mochitests/content", "https://example.com" @@ -232,11 +238,7 @@ class PrintHelper { }); // Mock PrintEventHandler with our Promises. - this.win.PrintEventHandler._showPrintDialog = ( - window, - haveSelection, - settings - ) => { + this.win.PrintEventHandler._showPrintDialog = (window, haveSelection) => { this.systemDialogOpenedWithSelection = haveSelection; return showSystemDialogPromise; }; diff --git a/toolkit/components/processsingleton/MainProcessSingleton.sys.mjs b/toolkit/components/processsingleton/MainProcessSingleton.sys.mjs index a10493f79b..2c93bcdc65 100644 --- a/toolkit/components/processsingleton/MainProcessSingleton.sys.mjs +++ b/toolkit/components/processsingleton/MainProcessSingleton.sys.mjs @@ -10,7 +10,7 @@ MainProcessSingleton.prototype = { "nsISupportsWeakReference", ]), - observe(subject, topic, data) { + observe(subject, topic) { switch (topic) { case "app-startup": { // Imported for side-effects. diff --git a/toolkit/components/processtools/tests/browser/browser_test_powerMetrics.js b/toolkit/components/processtools/tests/browser/browser_test_powerMetrics.js index 93833a5207..0ebaaa969e 100644 --- a/toolkit/components/processtools/tests/browser/browser_test_powerMetrics.js +++ b/toolkit/components/processtools/tests/browser/browser_test_powerMetrics.js @@ -87,16 +87,12 @@ add_task(async () => { let initalCpuTime = await getChildCpuTime(pid); let afterCpuTime; do { - await SpecialPowers.spawn( - firstBrowser, - [kBusyWaitForMs], - async kBusyWaitForMs => { - let startTime = Date.now(); - while (Date.now() - startTime < 10) { - // Burn CPU time... - } + await SpecialPowers.spawn(firstBrowser, [kBusyWaitForMs], async () => { + let startTime = Date.now(); + while (Date.now() - startTime < 10) { + // Burn CPU time... } - ); + }); afterCpuTime = await getChildCpuTime(pid); } while (afterCpuTime - initalCpuTime < kBusyWaitForMs * kNS_PER_MS); cpuTimeSpentOnBackgroundTab = Math.floor( diff --git a/toolkit/components/processtools/tests/browser/browser_test_procinfo.js b/toolkit/components/processtools/tests/browser/browser_test_procinfo.js index 07c7d5d2d0..5a301c664a 100644 --- a/toolkit/components/processtools/tests/browser/browser_test_procinfo.js +++ b/toolkit/components/processtools/tests/browser/browser_test_procinfo.js @@ -58,7 +58,7 @@ add_task(async function test_proc_info() { await BrowserTestUtils.withNewTab( { gBrowser, url: DUMMY_URL }, - async function (browser) { + async function () { // We test `SAMPLE_SIZE` times to increase a tad the chance of encountering race conditions. for (let z = 0; z < SAMPLE_SIZE; z++) { let parentProc = await ChromeUtils.requestProcInfo(); diff --git a/toolkit/components/promiseworker/tests/xpcshell/data/worker.js b/toolkit/components/promiseworker/tests/xpcshell/data/worker.js index 94b6dd17ad..d9939bd163 100644 --- a/toolkit/components/promiseworker/tests/xpcshell/data/worker.js +++ b/toolkit/components/promiseworker/tests/xpcshell/data/worker.js @@ -43,7 +43,7 @@ var Agent = { return args; }, - throwError(msg, ...args) { + throwError(msg) { throw new Error(msg); }, }; diff --git a/toolkit/components/promiseworker/tests/xpcshell/data/worker.mjs b/toolkit/components/promiseworker/tests/xpcshell/data/worker.mjs index 98a4b0ee5b..91b27ac8f1 100644 --- a/toolkit/components/promiseworker/tests/xpcshell/data/worker.mjs +++ b/toolkit/components/promiseworker/tests/xpcshell/data/worker.mjs @@ -30,7 +30,7 @@ var Agent = { return args; }, - throwError(msg, ...args) { + throwError(msg) { throw new Error(msg); }, }; diff --git a/toolkit/components/prompts/content/tabprompts.css b/toolkit/components/prompts/content/tabprompts.css deleted file mode 100644 index 539e8792cc..0000000000 --- a/toolkit/components/prompts/content/tabprompts.css +++ /dev/null @@ -1,119 +0,0 @@ -/* 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/. */ - -/* Tab Modal Prompt boxes */ - -.tabModalBackground { - justify-content: center; - flex-direction: column; -} - -.tabModalBackground, -tabmodalprompt { - width: 100%; - height: 100%; -} - -tabmodalprompt { - --tabmodalprompt-padding: 20px; - overflow: hidden; - text-shadow: none; /* remove lightweight theme text-shadow */ -} - -tabmodalprompt:not([hidden]) { - display: grid; - grid-template-rows: 1fr [dialog-start] auto [dialog-end] 2fr; - justify-items: center; -} - -/* - Adjustments for chrome level tab-prompts to make them - overlap with the upper chrome UI and move them in - front of content prompts. -*/ -tabmodalprompt.tab-prompt { - overflow: visible; - z-index: 1; - grid-template-rows: [dialog-start] auto [dialog-end] 1fr; -} - -tabmodalprompt.tab-prompt .tabmodalprompt-mainContainer { - margin-top: -5px; -} - -.tabmodalprompt-mainContainer { - min-width: 20em; - min-height: 12em; - max-width: calc(60% + calc(var(--tabmodalprompt-padding) * 2)); - -moz-user-focus: normal; - grid-row: dialog; - - display: flex; - flex-direction: column; -} - -.tabmodalprompt-topContainer { - flex-grow: 1; - padding: var(--tabmodalprompt-padding); - display: grid; - grid-template-columns: auto 1fr; - align-items: baseline; - align-content: center; /* center content vertically */ - max-width: 100%; - min-height: 0; - max-height: 60vh; - box-sizing: border-box; -} - -.tabmodalprompt-topContainer > div:not(.tabmodalprompt-infoContainer, [hidden]) { - display: contents; -} - -.tabmodalprompt-infoContainer { - grid-column: span 2; - - display: block; - margin-block: auto; - max-width: 100%; - height: 100%; - min-height: 0; - justify-self: center; /* center text, but only when it fits in one line */ -} - -/* When all elements in the first column are hidden, prevent the second column - from becoming the first one because it won't have the right fraction */ -.tabmodalprompt-topContainer > div > *:nth-child(2) { - grid-column: 2; -} - -.infoTitle { - margin-bottom: 1em !important; - font-weight: bold; -} - -.infoBody { - margin: 0 !important; - -moz-user-focus: normal; - user-select: text; - cursor: text !important; - white-space: pre-wrap; - unicode-bidi: plaintext; - outline: none; /* remove focus outline */ - overflow: auto; - max-width: 100%; - max-height: 100%; -} - -tabmodalprompt label[value=""] { - display: none; -} - -.tabmodalprompt-buttonContainer { - display: flex; - padding: 12px var(--tabmodalprompt-padding) 15px; -} - -.tabmodalprompt-buttonSpacer { - flex-grow: 1; -} diff --git a/toolkit/components/prompts/content/tabprompts.sys.mjs b/toolkit/components/prompts/content/tabprompts.sys.mjs deleted file mode 100644 index 7edb024fa8..0000000000 --- a/toolkit/components/prompts/content/tabprompts.sys.mjs +++ /dev/null @@ -1,298 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; - -export var TabModalPrompt = class { - constructor(win) { - this.win = win; - let newPrompt = (this.element = - win.document.createElement("tabmodalprompt")); - win.MozXULElement.insertFTLIfNeeded("toolkit/global/tabprompts.ftl"); - newPrompt.setAttribute("role", "dialog"); - let randomIdSuffix = Math.random().toString(32).substring(2); - newPrompt.setAttribute("aria-describedby", `infoBody-${randomIdSuffix}`); - newPrompt.appendChild( - win.MozXULElement.parseXULToFragment( - ` - <div class="tabmodalprompt-mainContainer" xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> - <div class="tabmodalprompt-topContainer"> - <div class="tabmodalprompt-infoContainer"> - <div class="tabmodalprompt-infoTitle infoTitle" hidden="hidden"/> - <div class="tabmodalprompt-infoBody infoBody" id="infoBody-${randomIdSuffix}" tabindex="-1"/> - </div> - - <div class="tabmodalprompt-loginContainer" hidden="hidden"> - <xul:label class="tabmodalprompt-loginLabel" data-l10n-id="tabmodalprompt-username" control="loginTextbox-${randomIdSuffix}"/> - <input class="tabmodalprompt-loginTextbox" id="loginTextbox-${randomIdSuffix}"/> - </div> - - <div class="tabmodalprompt-password1Container" hidden="hidden"> - <xul:label class="tabmodalprompt-password1Label" data-l10n-id="tabmodalprompt-password" control="password1Textbox-${randomIdSuffix}"/> - <input class="tabmodalprompt-password1Textbox" type="password" id="password1Textbox-${randomIdSuffix}"/> - </div> - - <div class="tabmodalprompt-checkboxContainer" hidden="hidden"> - <div/> - <xul:checkbox class="tabmodalprompt-checkbox"/> - </div> - - <!-- content goes here --> - </div> - <div class="tabmodalprompt-buttonContainer"> - <xul:button class="tabmodalprompt-button3" hidden="true"/> - <div class="tabmodalprompt-buttonSpacer"/> - <xul:button class="tabmodalprompt-button0" data-l10n-id="tabmodalprompt-ok-button"/> - <xul:button class="tabmodalprompt-button2" hidden="true"/> - <xul:button class="tabmodalprompt-button1" data-l10n-id="tabmodalprompt-cancel-button"/> - </div> - </div>` - ) - ); - - this.ui = { - prompt: this, - promptContainer: this.element, - mainContainer: newPrompt.querySelector(".tabmodalprompt-mainContainer"), - loginContainer: newPrompt.querySelector(".tabmodalprompt-loginContainer"), - loginTextbox: newPrompt.querySelector(".tabmodalprompt-loginTextbox"), - loginLabel: newPrompt.querySelector(".tabmodalprompt-loginLabel"), - password1Container: newPrompt.querySelector( - ".tabmodalprompt-password1Container" - ), - password1Textbox: newPrompt.querySelector( - ".tabmodalprompt-password1Textbox" - ), - password1Label: newPrompt.querySelector(".tabmodalprompt-password1Label"), - infoContainer: newPrompt.querySelector(".tabmodalprompt-infoContainer"), - infoBody: newPrompt.querySelector(".tabmodalprompt-infoBody"), - infoTitle: newPrompt.querySelector(".tabmodalprompt-infoTitle"), - infoIcon: null, - rows: newPrompt.querySelector(".tabmodalprompt-topContainer"), - checkbox: newPrompt.querySelector(".tabmodalprompt-checkbox"), - checkboxContainer: newPrompt.querySelector( - ".tabmodalprompt-checkboxContainer" - ), - button3: newPrompt.querySelector(".tabmodalprompt-button3"), - button2: newPrompt.querySelector(".tabmodalprompt-button2"), - button1: newPrompt.querySelector(".tabmodalprompt-button1"), - button0: newPrompt.querySelector(".tabmodalprompt-button0"), - // focusTarget (for BUTTON_DELAY_ENABLE) not yet supported - }; - - if (AppConstants.XP_UNIX) { - // Reorder buttons on Linux - let buttonContainer = newPrompt.querySelector( - ".tabmodalprompt-buttonContainer" - ); - buttonContainer.appendChild(this.ui.button3); - buttonContainer.appendChild(this.ui.button2); - buttonContainer.appendChild( - newPrompt.querySelector(".tabmodalprompt-buttonSpacer") - ); - buttonContainer.appendChild(this.ui.button1); - buttonContainer.appendChild(this.ui.button0); - } - - this.ui.button0.addEventListener( - "command", - this.onButtonClick.bind(this, 0) - ); - this.ui.button1.addEventListener( - "command", - this.onButtonClick.bind(this, 1) - ); - this.ui.button2.addEventListener( - "command", - this.onButtonClick.bind(this, 2) - ); - this.ui.button3.addEventListener( - "command", - this.onButtonClick.bind(this, 3) - ); - // Anonymous wrapper used here because |Dialog| doesn't exist until init() is called! - this.ui.checkbox.addEventListener("command", () => { - this.Dialog.onCheckbox(); - }); - - /** - * Based on dialog.xml handlers - */ - this.element.addEventListener( - "keypress", - event => { - switch (event.keyCode) { - case KeyEvent.DOM_VK_RETURN: - this.onKeyAction("default", event); - break; - - case KeyEvent.DOM_VK_ESCAPE: - this.onKeyAction("cancel", event); - break; - - default: - if ( - AppConstants.platform == "macosx" && - event.key == "." && - event.metaKey - ) { - this.onKeyAction("cancel", event); - } - break; - } - }, - { mozSystemGroup: true } - ); - - this.element.addEventListener( - "focus", - event => { - let bnum = this.args.defaultButtonNum || 0; - let defaultButton = this.ui["button" + bnum]; - - if (AppConstants.platform == "macosx") { - // On OS X, the default button always stays marked as such (until - // the entire prompt blurs). - defaultButton.setAttribute("default", "true"); - } else { - // On other platforms, the default button is only marked as such - // when no other button has focus. XUL buttons on not-OSX will - // react to pressing enter as a command, so you can't trigger the - // default without tabbing to it or something that isn't a button. - let focusedDefault = event.originalTarget == defaultButton; - let someButtonFocused = - event.originalTarget.localName == "button" || - event.originalTarget.localName == "toolbarbutton"; - if (focusedDefault || !someButtonFocused) { - defaultButton.setAttribute("default", "true"); - } - } - }, - true - ); - - this.element.addEventListener("blur", () => { - // If focus shifted to somewhere else in the browser, don't make - // the default button look active. - let bnum = this.args.defaultButtonNum || 0; - let button = this.ui["button" + bnum]; - button.removeAttribute("default"); - }); - } - - init(args, linkedTab, onCloseCallback) { - this.args = args; - this.linkedTab = linkedTab; - this.onCloseCallback = onCloseCallback; - - if (args.enableDelay) { - throw new Error( - "BUTTON_DELAY_ENABLE not yet supported for tab-modal prompts" - ); - } - - // Apply styling depending on modalType (content or tab prompt) - if (args.modalType === Ci.nsIPrompt.MODAL_TYPE_TAB) { - this.element.classList.add("tab-prompt"); - } else { - this.element.classList.add("content-prompt"); - } - - // We need to remove the prompt when the tab or browser window is closed or - // the page navigates, else we never unwind the event loop and that's sad times. - // Remember to cleanup in shutdownPrompt()! - this.win.addEventListener("resize", this); - this.win.addEventListener("unload", this); - if (linkedTab) { - linkedTab.addEventListener("TabClose", this); - } - // Note: - // nsPrompter.js or in e10s mode browser-parent.js call abortPrompt, - // when the domWindow, for which the prompt was created, generates - // a "pagehide" event. - - let { CommonDialog } = ChromeUtils.importESModule( - "resource://gre/modules/CommonDialog.sys.mjs" - ); - this.Dialog = new CommonDialog(args, this.ui); - this.Dialog.onLoad(null); - - // For content prompts display the tabprompt title that shows the prompt origin when - // the prompt origin is not the same as that of the top window. - if ( - args.modalType == Ci.nsIPrompt.MODAL_TYPE_CONTENT && - args.showCallerOrigin - ) { - this.ui.infoTitle.removeAttribute("hidden"); - } - - // TODO: should unhide buttonSpacer on Windows when there are 4 buttons. - // Better yet, just drop support for 4-button dialogs. (bug 609510) - } - - shutdownPrompt() { - // remove our event listeners - try { - this.win.removeEventListener("resize", this); - this.win.removeEventListener("unload", this); - if (this.linkedTab) { - this.linkedTab.removeEventListener("TabClose", this); - } - } catch (e) {} - // invoke callback - this.onCloseCallback(); - this.win = null; - this.ui = null; - // Intentionally not cleaning up |this.element| here -- - // TabModalPromptBox.removePrompt() would need it and it might not - // be called yet -- see browser_double_close_tabs.js. - } - - abortPrompt() { - // Called from other code when the page changes. - this.Dialog.abortPrompt(); - this.shutdownPrompt(); - } - - handleEvent(aEvent) { - switch (aEvent.type) { - case "unload": - case "TabClose": - this.abortPrompt(); - break; - } - } - - onButtonClick(buttonNum) { - // We want to do all the work her asynchronously off a Gecko - // runnable, because of situations like the one described in - // https://bugzilla.mozilla.org/show_bug.cgi?id=1167575#c35 : we - // get here off processing of an OS event and will also process - // one more Gecko runnable before we break out of the event loop - // spin whoever posted the prompt is doing. If we do all our - // work sync, we will exit modal state _before_ processing that - // runnable, and if exiting moral state posts a runnable we will - // incorrectly process that runnable before leaving our event - // loop spin. - Services.tm.dispatchToMainThread(() => { - this.Dialog["onButton" + buttonNum](); - this.shutdownPrompt(); - }); - } - - onKeyAction(action, event) { - if (event.defaultPrevented) { - return; - } - - event.stopPropagation(); - if (action == "default") { - let bnum = this.args.defaultButtonNum || 0; - this.onButtonClick(bnum); - } else { - // action == "cancel" - this.onButtonClick(1); // Cancel button - } - } -}; diff --git a/toolkit/components/prompts/jar.mn b/toolkit/components/prompts/jar.mn index 819af6dc1d..7a904c4363 100644 --- a/toolkit/components/prompts/jar.mn +++ b/toolkit/components/prompts/jar.mn @@ -9,5 +9,3 @@ toolkit.jar: content/global/commonDialog.css (content/commonDialog.css) content/global/selectDialog.js (content/selectDialog.js) content/global/selectDialog.xhtml (content/selectDialog.xhtml) - content/global/tabprompts.css (content/tabprompts.css) - content/global/tabprompts.sys.mjs (content/tabprompts.sys.mjs) diff --git a/toolkit/components/prompts/src/CommonDialog.sys.mjs b/toolkit/components/prompts/src/CommonDialog.sys.mjs index a0812aa8ec..c788248442 100644 --- a/toolkit/components/prompts/src/CommonDialog.sys.mjs +++ b/toolkit/components/prompts/src/CommonDialog.sys.mjs @@ -30,11 +30,10 @@ CommonDialog.prototype = { initialFocusResolver: null, /** - * @param [commonDialogEl] - Dialog element from commonDialog.xhtml, - * null for TabModalPrompts. + * @param [commonDialogEl] - Dialog element from commonDialog.xhtml. */ - async onLoad(commonDialogEl = null) { - let isEmbedded = !!commonDialogEl?.ownerGlobal.docShell.chromeEventHandler; + async onLoad(commonDialogEl) { + let isEmbedded = !!commonDialogEl.ownerGlobal.docShell.chromeEventHandler; switch (this.args.promptType) { case "alert": @@ -106,36 +105,19 @@ CommonDialog.prototype = { throw new Error("unknown dialog type"); } - if (commonDialogEl) { - commonDialogEl.setAttribute( - "windowtype", - "prompt:" + this.args.promptType - ); - } + commonDialogEl.setAttribute("windowtype", "prompt:" + this.args.promptType); // set the document title let title = this.args.title; let infoTitle = this.ui.infoTitle; infoTitle.appendChild(infoTitle.ownerDocument.createTextNode(title)); - // Specific check to prevent showing the title on the old content prompts for macOS. - // This should be removed when the old content prompts are removed. - let contentSubDialogPromptEnabled = Services.prefs.getBoolPref( - "prompts.contentPromptSubDialog" - ); - let isOldContentPrompt = - !contentSubDialogPromptEnabled && - this.args.modalType == Ci.nsIPrompt.MODAL_TYPE_CONTENT; - // After making these preventative checks, we can determine to show it if we're on // macOS (where there is no titlebar) or if the prompt is a common dialog document // and has been embedded (has a chromeEventHandler). - infoTitle.hidden = - isOldContentPrompt || !(AppConstants.platform === "macosx" || isEmbedded); + infoTitle.hidden = !(AppConstants.platform === "macosx" || isEmbedded); - if (commonDialogEl) { - commonDialogEl.ownerDocument.title = title; - } + commonDialogEl.ownerDocument.title = title; // Set button labels and visibility // @@ -193,7 +175,7 @@ CommonDialog.prototype = { // set the icon let icon = this.ui.infoIcon; if (icon) { - this.iconClass.forEach((el, idx, arr) => icon.classList.add(el)); + this.iconClass.forEach(el => icon.classList.add(el)); } // set default result to cancelled @@ -202,15 +184,7 @@ CommonDialog.prototype = { // Set the default button let b = this.args.defaultButtonNum || 0; - let button = this.ui["button" + b]; - - if (commonDialogEl) { - commonDialogEl.defaultButton = ["accept", "cancel", "extra1", "extra2"][ - b - ]; - } else { - button.setAttribute("default", "true"); - } + commonDialogEl.defaultButton = ["accept", "cancel", "extra1", "extra2"][b]; if (!isEmbedded && !this.ui.promptContainer?.hidden) { // Set default focus and select textbox contents if applicable. If we're @@ -229,7 +203,7 @@ CommonDialog.prototype = { // Play a sound (unless we're showing a content prompt -- don't want those // to feel like OS prompts). try { - if (commonDialogEl && this.soundID && !this.args.openedWithTabDialog) { + if (this.soundID && !this.args.openedWithTabDialog) { Cc["@mozilla.org/sound;1"] .getService(Ci.nsISound) .playEventSound(this.soundID); @@ -238,20 +212,12 @@ CommonDialog.prototype = { console.error("Couldn't play common dialog event sound: ", e); } - if (commonDialogEl) { - if (isEmbedded) { - // If we delayed default focus above, wait for it to be ready before - // sending the notification. - await this.initialFocusPromise; - } - Services.obs.notifyObservers(this.ui.prompt, "common-dialog-loaded"); - } else { - // ui.promptContainer is the <tabmodalprompt> element. - Services.obs.notifyObservers( - this.ui.promptContainer, - "tabmodal-dialog-loaded" - ); + if (isEmbedded) { + // If we delayed default focus above, wait for it to be ready before + // sending the notification. + await this.initialFocusPromise; } + Services.obs.notifyObservers(this.ui.prompt, "common-dialog-loaded"); }, setLabelForNode(aNode, aLabel) { diff --git a/toolkit/components/prompts/src/Prompter.sys.mjs b/toolkit/components/prompts/src/Prompter.sys.mjs index a0b5edca2f..2174ba10c6 100644 --- a/toolkit/components/prompts/src/Prompter.sys.mjs +++ b/toolkit/components/prompts/src/Prompter.sys.mjs @@ -1257,7 +1257,7 @@ class ModalPrompter { } async openInternalWindowPrompt(parentWindow, args) { - if (!parentWindow?.gDialogBox || !ModalPrompter.windowPromptSubDialog) { + if (!parentWindow?.gDialogBox) { this.openWindowPrompt(parentWindow, args); return; } @@ -1720,15 +1720,7 @@ class ModalPrompter { return result; } - asyncPromptAuth( - channel, - callback, - context, - level, - authInfo, - checkLabel, - checkValue - ) { + asyncPromptAuth() { // Nothing calls this directly; netwerk ends up going through // nsIPromptService::GetPrompt, which delegates to login manager. // Login manger handles the async bits itself, and only calls out @@ -1757,13 +1749,6 @@ XPCOMUtils.defineLazyPreferenceGetter( MODAL_TYPE_WINDOW ); -XPCOMUtils.defineLazyPreferenceGetter( - ModalPrompter, - "windowPromptSubDialog", - "prompts.windowPromptSubDialog", - false -); - export function AuthPromptAdapterFactory() {} AuthPromptAdapterFactory.prototype = { classID: Components.ID("{6e134924-6c3a-4d86-81ac-69432dd971dc}"), @@ -1786,7 +1771,7 @@ AuthPromptAdapter.prototype = { /* ---------- nsIAuthPrompt2 ---------- */ - promptAuth(channel, level, authInfo, checkLabel, checkValue) { + promptAuth(channel, level, authInfo) { let message = InternalPromptUtils.makeAuthMessage( this.oldPrompter, channel, @@ -1833,15 +1818,7 @@ AuthPromptAdapter.prototype = { return ok; }, - asyncPromptAuth( - channel, - callback, - context, - level, - authInfo, - checkLabel, - checkValue - ) { + asyncPromptAuth() { throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); }, }; diff --git a/toolkit/components/prompts/test/chromeScript.js b/toolkit/components/prompts/test/chromeScript.js index 9a6bacc8a4..f873c5d04b 100644 --- a/toolkit/components/prompts/test/chromeScript.js +++ b/toolkit/components/prompts/test/chromeScript.js @@ -31,52 +31,6 @@ async function handlePromptWhenItAppears(action, modalType, isSelect) { } } -function checkTabModal(prompt, browser) { - let doc = browser.ownerDocument; - - let { bottom: toolboxBottom } = doc - .getElementById("navigator-toolbox") - .getBoundingClientRect(); - - let { mainContainer } = prompt.ui; - - let { x, y } = mainContainer.getBoundingClientRect(); - ok(y > 0, "Container should have y > 0"); - // Inset by 1px since the corner point doesn't return the frame due to the - // border-radius. - is( - doc.elementFromPoint(x + 1, y + 1).parentNode, - mainContainer, - "Check tabmodalprompt is visible" - ); - - info("Click to the left of the dialog over the content area"); - isnot( - doc.elementFromPoint(x - 10, y + 50), - browser, - "Check clicks on the content area don't go to the browser" - ); - is( - doc.elementFromPoint(x - 10, y + 50), - prompt.element, - "Check clicks on the content area go to the prompt dialog background" - ); - - if (prompt.args.modalType == Ci.nsIPrompt.MODAL_TYPE_TAB) { - ok( - y <= toolboxBottom - 5, - "Dialog should overlap the toolbox by at least 5px" - ); - } else { - ok(y >= toolboxBottom, "Dialog must not overlap with toolbox."); - } - - ok( - browser.hasAttribute("tabmodalPromptShowing"), - "Check browser has @tabmodalPromptShowing" - ); -} - async function handlePrompt(action, modalType, isSelect) { let ui; let browserWin = Services.wm.getMostRecentWindow("navigator:browser"); @@ -142,8 +96,7 @@ function getPromptState(ui) { state.checkHidden = ui.checkboxContainer.hidden; state.checkMsg = state.checkHidden ? "" : ui.checkbox.label; state.checked = state.checkHidden ? false : ui.checkbox.checked; - // TabModalPrompts don't have an infoIcon - state.iconClass = ui.infoIcon ? ui.infoIcon.className : null; + state.iconClass = ui.infoIcon.className; state.textValue = ui.loginTextbox.value; state.passValue = ui.password1Textbox.value; diff --git a/toolkit/components/prompts/test/prompt_common.js b/toolkit/components/prompts/test/prompt_common.js index 8d583060da..25b0a4c438 100644 --- a/toolkit/components/prompts/test/prompt_common.js +++ b/toolkit/components/prompts/test/prompt_common.js @@ -17,10 +17,6 @@ function propBagToObject(bag) { } var modalType; -var tabSubDialogsEnabled = SpecialPowers.Services.prefs.getBoolPref( - "prompts.tabChromePromptSubDialog", - false -); var isSelectDialog = false; var isOSX = "nsILocalFileMac" in SpecialPowers.Ci; var isE10S = SpecialPowers.Services.appinfo.processType == 2; @@ -160,7 +156,7 @@ function onloadPromiseFor(id) { return new Promise(resolve => { iframe.addEventListener( "load", - function (e) { + function () { resolve(true); }, { once: true } @@ -227,7 +223,7 @@ function checkPromptState(promptState, expectedState) { is(promptState.checked, expectedState.checked, "Checking checkbox checked"); if ( modalType === Ci.nsIPrompt.MODAL_TYPE_WINDOW || - (modalType === Ci.nsIPrompt.MODAL_TYPE_TAB && tabSubDialogsEnabled) + modalType === Ci.nsIPrompt.MODAL_TYPE_TAB ) { is( promptState.iconClass, @@ -366,7 +362,7 @@ function PrompterProxy(chromeScript) { return new Proxy( {}, { - get(target, prop, receiver) { + get(target, prop) { return (...args) => { // Array of indices of out/inout params to copy from the parent back to the caller. let outParams = []; diff --git a/toolkit/components/prompts/test/test_bug619644.html b/toolkit/components/prompts/test/test_bug619644.html index 2b424c71a6..03650d6d6d 100644 --- a/toolkit/components/prompts/test/test_bug619644.html +++ b/toolkit/components/prompts/test/test_bug619644.html @@ -31,7 +31,7 @@ function inittest() { SimpleTest.waitForExplicitFinish(); } -function runtest(e) { +function runtest() { modalType = Ci.nsIPrompt.MODAL_TYPE_CONTENT; window.removeEventListener("message", runtest); diff --git a/toolkit/components/prompts/test/test_subresources_prompts.html b/toolkit/components/prompts/test/test_subresources_prompts.html index b71ad0694e..134af95c6d 100644 --- a/toolkit/components/prompts/test/test_subresources_prompts.html +++ b/toolkit/components/prompts/test/test_subresources_prompts.html @@ -33,11 +33,6 @@ var iframe1Loaded = onloadPromiseFor("iframe_diff_origin"); var iframe2Loaded = onloadPromiseFor("iframe_same_origin"); var iframe_prompt = document.getElementById("iframe_prompt"); -// Depending on pref state we either show auth prompts as windows or on tab level. -let authPromptModalType = SpecialPowers.Services.prefs.getIntPref( - "prompts.modalType.httpAuth" -); - add_task(async function runTest() { modalType = Ci.nsIPrompt.MODAL_TYPE_CONTENT; @@ -104,7 +99,7 @@ add_task(async function runTestAuth() { // cross-origin subresources load // Let prompt_common know what kind of modal type is enabled for auth prompts. - modalType = authPromptModalType; + modalType = Ci.nsIPrompt.MODAL_TYPE_TAB; let state, action; diff --git a/toolkit/components/protobuf/regenerate_cpp_files.sh b/toolkit/components/protobuf/regenerate_cpp_files.sh index 848e6f538e..84aaf25801 100755 --- a/toolkit/components/protobuf/regenerate_cpp_files.sh +++ b/toolkit/components/protobuf/regenerate_cpp_files.sh @@ -25,6 +25,7 @@ cd ../../.. # Top level. regenerate devtools/shared/heapsnapshot/ CoreDump.proto regenerate toolkit/components/reputationservice/chromium/chrome/common/safe_browsing/ csd.proto regenerate toolkit/components/url-classifier/chromium/ safebrowsing.proto +regenerate toolkit/components/cookiebanners/ cookieBanner.proto command cp third_party/content_analysis_sdk/proto/content_analysis/sdk/analysis.proto toolkit/components/contentanalysis/content_analysis/sdk/analysis.proto regenerate toolkit/components/contentanalysis/content_analysis/sdk/ analysis.proto command cp third_party/rust/viaduct/src/fetch_msg_types.proto toolkit/components/viaduct/fetch_msg_types.proto diff --git a/toolkit/components/reader/.eslintrc.js b/toolkit/components/reader/.eslintrc.js index 4df95dd5f8..bf83f31ab1 100644 --- a/toolkit/components/reader/.eslintrc.js +++ b/toolkit/components/reader/.eslintrc.js @@ -8,6 +8,6 @@ module.exports = { rules: { "no-inner-declarations": "error", "no-shadow": "error", - "no-unused-vars": ["error", { vars: "all", args: "none" }], + "no-unused-vars": ["error", { vars: "all", argsIgnorePattern: "^_" }], }, }; diff --git a/toolkit/components/reader/AboutReader.sys.mjs b/toolkit/components/reader/AboutReader.sys.mjs index 6b1c28c228..75776b619f 100644 --- a/toolkit/components/reader/AboutReader.sys.mjs +++ b/toolkit/components/reader/AboutReader.sys.mjs @@ -8,6 +8,7 @@ import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; const lazy = {}; let gScrollPositions = new Map(); +let lastSelectedTheme = "auto"; ChromeUtils.defineESModuleGetters(lazy, { AsyncPrefs: "resource://gre/modules/AsyncPrefs.sys.mjs", @@ -26,10 +27,31 @@ ChromeUtils.defineLazyGetter( ); const COLORSCHEME_L10N_IDS = { - light: "about-reader-color-scheme-light", - dark: "about-reader-color-scheme-dark", - sepia: "about-reader-color-scheme-sepia", - auto: "about-reader-color-scheme-auto", + auto: "about-reader-color-theme-auto", + light: "about-reader-color-theme-light", + dark: "about-reader-color-theme-dark", + sepia: "about-reader-color-theme-sepia", + contrast: "about-reader-color-theme-contrast", + gray: "about-reader-color-theme-gray", + custom: "about-reader-color-theme-custom", +}; + +const CUSTOM_THEME_COLOR_INPUTS = [ + "foreground", + "background", + "unvisited-links", + "visited-links", + "selection-highlight", +]; + +const COLORS_MENU_TABS = ["fxtheme", "customtheme"]; + +const DEFAULT_COLORS = { + background: "#FFFFFF", + foreground: "#14151A", + "unvisited-links": "#0060DF", + "visited-links": "#321C64", + "selection-highlight": "#FFFFCC", }; Services.telemetry.setEventRecordingEnabled("readermode", true); @@ -147,9 +169,27 @@ export var AboutReader = function ( // we're ready for any external setup, send a signal for that. this._actor.sendAsyncMessage("Reader:OnSetup"); - let colorSchemeValues = JSON.parse( + // set up segmented tab controls for colors menu. + this._setupColorsTabs( + COLORS_MENU_TABS, + this._handleColorsTabClick.bind(this) + ); + + // fetch color scheme values from prefs. + let colorsMenuColorSchemeValues = JSON.parse( Services.prefs.getCharPref("reader.color_scheme.values") ); + // remove contrast and gray options from regular menu. + let colorSchemeValues = [...colorsMenuColorSchemeValues]; + colorSchemeValues.splice(colorSchemeValues.length - 2, 2); + + let colorsMenuColorSchemeOptions = colorsMenuColorSchemeValues.map(value => ({ + l10nId: COLORSCHEME_L10N_IDS[value], + groupName: "color-scheme", + value, + itemClass: value + "-button", + })); + let colorSchemeOptions = colorSchemeValues.map(value => ({ l10nId: COLORSCHEME_L10N_IDS[value], groupName: "color-scheme", @@ -158,12 +198,33 @@ export var AboutReader = function ( })); let colorScheme = Services.prefs.getCharPref("reader.color_scheme"); - this._setupSegmentedButton( - "color-scheme-buttons", - colorSchemeOptions, - colorScheme, - this._setColorSchemePref.bind(this) - ); + if (Services.prefs.getBoolPref("reader.colors_menu.enabled", false)) { + doc.getElementById("regular-color-scheme").hidden = true; + doc.getElementById("custom-colors-color-scheme").hidden = false; + this._setupSegmentedButton( + "colors-menu-color-scheme-buttons", + colorsMenuColorSchemeOptions, + colorScheme, + this._setColorSchemePref.bind(this) + ); + this._setupCustomColors( + CUSTOM_THEME_COLOR_INPUTS, + "custom-colors-selection", + "about-reader-custom-colors" + ); + this._setupButton( + "custom-colors-reset-button", + this._resetCustomColors.bind(this) + ); + } else { + this._setupSegmentedButton( + "color-scheme-buttons", + colorSchemeOptions, + colorScheme, + this._setColorSchemePref.bind(this) + ); + } + this._setColorSchemePref(colorScheme); let fontTypeOptions = [ @@ -738,19 +799,43 @@ AboutReader.prototype = { this._colorScheme = "hcm"; } + if (this._colorScheme == "custom") { + const colorInputs = this._doc.querySelectorAll("color-input"); + colorInputs.forEach(input => { + // Set document body styles to pref values. + let property = input.getAttribute("prop-name"); + let pref = `reader.custom_colors.${property}`; + let customColor = Services.prefs.getStringPref(pref, ""); + // If customColor is truthy, set the value from pref. + if (customColor) { + let cssProp = `--custom-theme-${property}`; + this._doc.body.style.setProperty(cssProp, customColor); + } + }); + } + bodyClasses.add(this._colorScheme); }, - // Pref values include "dark", "light", "sepia", and "auto" - _setColorSchemePref(colorSchemePref) { + // Pref values include "auto", "dark", "light", "sepia", + // "gray", "contrast", and "custom" + _setColorSchemePref(colorSchemePref, fromInputEvent = false) { + // The input event for the last selected segmented button is fired + // upon loading a reader article in the same session. To prevent it + // from overwriting custom colors, we return false. + if (this._colorScheme == "custom" && fromInputEvent) { + lastSelectedTheme = colorSchemePref; + return false; + } this._setColorScheme(colorSchemePref); lazy.AsyncPrefs.set("reader.color_scheme", colorSchemePref); + return true; }, _setFontType(newFontType) { if (this._fontType === newFontType) { - return; + return false; } let bodyClasses = this._doc.body.classList; @@ -763,6 +848,8 @@ AboutReader.prototype = { bodyClasses.add(this._fontType); lazy.AsyncPrefs.set("reader.font_type", this._fontType); + + return true; }, async _loadArticle(docContentType = "document") { @@ -1094,14 +1181,17 @@ AboutReader.prototype = { label.removeAttribute("checked"); } - aEvent.target.nextElementSibling.setAttribute("checked", "true"); - callback(option.value); + let setOption = callback(option.value, true); + if (setOption) { + aEvent.target.setAttribute("checked", "true"); + aEvent.target.nextElementSibling.setAttribute("checked", "true"); + } }, true ); if (option.value === initialValue) { - radioButton.checked = true; + radioButton.setAttribute("checked", "true"); item.setAttribute("checked", "true"); } } @@ -1124,6 +1214,121 @@ AboutReader.prototype = { ); }, + _handleColorsTabClick(option) { + let doc = this._doc; + if (option == "customtheme") { + this._setColorSchemePref("custom"); + lazy.AsyncPrefs.set("reader.color_scheme", "custom"); + + // Store the last selected preset theme button. + const colorSchemePresets = doc.querySelector( + ".colors-menu-color-scheme-buttons" + ); + const labels = colorSchemePresets.querySelectorAll("label"); + labels.forEach(label => { + if (label.hasAttribute("checked")) { + lastSelectedTheme = label.className.split("-")[0]; + } + }); + } + if (option == "fxtheme") { + this._setColorSchemePref(lastSelectedTheme); + lazy.AsyncPrefs.set("reader.color_scheme", lastSelectedTheme); + // set the last selected button to checked. + const colorSchemePresets = doc.querySelector( + ".colors-menu-color-scheme-buttons" + ); + const labels = colorSchemePresets.querySelectorAll("label"); + labels.forEach(label => { + if (label.className == `${lastSelectedTheme}-button`) { + label.setAttribute("checked", "true"); + label.previousElementSibling.setAttribute("checked", "true"); + } + }); + } + }, + + _setupColorsTabs(options, callback) { + let doc = this._doc; + let colorScheme = Services.prefs.getCharPref("reader.color_scheme"); + for (let option of options) { + let tabButton = doc.getElementById(`tabs-deck-button-${option}`); + // Open custom theme tab if color scheme is set to custom. + if (option == "customtheme" && colorScheme == "custom") { + tabButton.click(); + } + tabButton.addEventListener( + "click", + function (aEvent) { + if (!aEvent.isTrusted) { + return; + } + + callback(option); + }, + true + ); + } + }, + + _setupColorInput(prop) { + let doc = this._doc; + let input = doc.createElement("color-input"); + input.setAttribute("prop-name", prop); + let labelL10nId = `about-reader-custom-colors-${prop}`; + input.setAttribute("data-l10n-id", labelL10nId); + + let pref = `reader.custom_colors.${prop}`; + let customColor = Services.prefs.getStringPref(pref, ""); + // Set the swatch color from prefs if one has been set. + if (customColor) { + input.setAttribute("color", customColor); + } else { + let defaultColor = DEFAULT_COLORS[prop]; + input.setAttribute("color", defaultColor); + } + + // Attach event listener to update the pref and page colors on input. + input.addEventListener("color-picked", e => { + const cssPropToUpdate = `--custom-theme-${prop}`; + this._doc.body.style.setProperty(cssPropToUpdate, e.detail); + + const prefToUpdate = `reader.custom_colors.${prop}`; + lazy.AsyncPrefs.set(prefToUpdate, e.detail); + }); + + return input; + }, + + _setupCustomColors(options, id) { + let doc = this._doc; + const list = doc.getElementsByClassName(id)[0]; + + for (let option of options) { + let listItem = doc.createElement("li"); + let colorInput = this._setupColorInput(option); + listItem.appendChild(colorInput); + list.appendChild(listItem); + } + }, + + _resetCustomColors() { + // Need to reset prefs, page colors, and color inputs. + const colorInputs = this._doc.querySelectorAll("color-input"); + colorInputs.forEach(input => { + let property = input.getAttribute("prop-name"); + let pref = `reader.custom_colors.${property}`; + lazy.AsyncPrefs.set(pref, ""); + + // Set css props to empty strings so they use fallback value. + let cssProp = `--custom-theme-${property}`; + this._doc.body.style.setProperty(cssProp, ""); + + let defaultColor = DEFAULT_COLORS[property]; + input.setAttribute("color", defaultColor); + }); + }, + _toggleDropdownClicked(event) { let dropdown = event.target.closest(".dropdown"); @@ -1143,7 +1348,7 @@ AboutReader.prototype = { /* * If the ReaderView banner font-dropdown is closed, open it. */ - _openDropdown(dropdown, window) { + _openDropdown(dropdown) { if (dropdown.classList.contains("open")) { return; } diff --git a/toolkit/components/reader/ReaderMode.sys.mjs b/toolkit/components/reader/ReaderMode.sys.mjs index 4b754d88b0..c2561bfb23 100644 --- a/toolkit/components/reader/ReaderMode.sys.mjs +++ b/toolkit/components/reader/ReaderMode.sys.mjs @@ -273,11 +273,11 @@ export var ReaderMode = { "READER_MODE_DOWNLOAD_RESULT" ); return new Promise((resolve, reject) => { - let xhr = new XMLHttpRequest(); + let xhr = new XMLHttpRequest({ mozAnon: false }); xhr.open("GET", url, true); xhr.onerror = evt => reject(evt.error); xhr.responseType = docContentType === "text/plain" ? "text" : "document"; - xhr.onload = evt => { + xhr.onload = () => { if (xhr.status !== 200) { reject("Reader mode XHR failed with status: " + xhr.status); histogram.add(DOWNLOAD_ERROR_XHR); diff --git a/toolkit/components/reader/color-input.css b/toolkit/components/reader/color-input.css new file mode 100644 index 0000000000..93e269dd16 --- /dev/null +++ b/toolkit/components/reader/color-input.css @@ -0,0 +1,47 @@ +/* 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/. */ + +.color-input-container { + display: flex; + position: relative; + justify-content: flex-start; + align-items: center; + gap: var(--space-small); + min-height: 46px; + border: 1px solid rgba(0, 0, 0, 0.2); + padding: 0 var(--space-small); + border-radius: var(--border-radius-small); +} + +.color-input-container:hover { + background-color: var(--toolbar-button-background-hover); +} + +#color-swatch:focus-visible { + outline: none; +} + +.color-input-container:focus-within { + outline: 2px solid var(--primary-color); + outline-offset: var(--focus-outline-offset); +} + +.icon-container { + display: flex; + margin-inline: auto var(--space-xsmall); +} + +#color-swatch { + appearance: none; + width: 34px; + height: 34px; + background-color: transparent; + border: none; + cursor: pointer; +} + +#color-swatch::-moz-color-swatch { + border-radius: var(--border-radius-circle); + border: 1px solid rgba(0, 0, 0, 0.25); +} diff --git a/toolkit/components/reader/color-input.mjs b/toolkit/components/reader/color-input.mjs new file mode 100644 index 0000000000..1c460dac3e --- /dev/null +++ b/toolkit/components/reader/color-input.mjs @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { html } from "chrome://global/content/vendor/lit.all.mjs"; +import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; + +/** + * @tagname color-input + * @property {string} color - The initial color value as a hex code. + * @property {string} propName - The property that the color input sets. + * @property {string} l10nId - l10nId for label text. + */ +export default class ColorInput extends MozLitElement { + static properties = { + color: { type: String }, + propName: { type: String, attribute: "prop-name" }, + l10nId: { type: String, attribute: "data-l10n-id" }, + }; + + static queries = { + inputEl: "#color-swatch", + }; + + handleColorInput(event) { + this.color = event.target.value; + this.dispatchEvent( + new CustomEvent("color-picked", { + detail: this.color, + }) + ); + } + + /* Function to launch color picker when the user clicks anywhere in the container. */ + handleClick(event) { + // If the user directly clicks the color swatch, no need to propagate click. + if (event.target.matches("input")) { + return; + } + this.inputEl.click(); + } + + render() { + return html` + <link + rel="stylesheet" + href="chrome://global/content/reader/color-input.css" + /> + <div class="color-input-container" @click="${this.handleClick}"> + <input + type="color" + name="${this.propName}" + .value="${this.color}" + id="color-swatch" + @input="${this.handleColorInput}" + /> + <label for="color-swatch" data-l10n-id=${this.l10nId}></label> + <div class="icon-container"> + <img + class="icon" + role="presentation" + src="chrome://global/skin/icons/edit-outline.svg" + /> + </div> + </div> + `; + } +} +customElements.define("color-input", ColorInput); diff --git a/toolkit/components/reader/color-input.stories.mjs b/toolkit/components/reader/color-input.stories.mjs new file mode 100644 index 0000000000..cec85a8d98 --- /dev/null +++ b/toolkit/components/reader/color-input.stories.mjs @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { html } from "../../content/widgets/vendor/lit.all.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://global/content/reader/color-input.mjs"; + +export default { + title: "Domain-specific UI Widgets/Reader View/Color Input", + component: "color-input", + argTypes: {}, + parameters: { + status: "stable", + fluent: `moz-color-input-label = Background`, + }, +}; + +const Template = ({ color, propName, labelL10nId }) => html` + <color-input + color=${color} + data-l10n-id=${labelL10nId} + prop-name=${propName} + ></color-input> +`; + +export const Default = Template.bind({}); +Default.args = { + propName: "background", + color: "#7293C9", + labelL10nId: "moz-color-input-label", +}; diff --git a/toolkit/components/reader/content/aboutReader.html b/toolkit/components/reader/content/aboutReader.html index db93e9f80b..d1c9164d42 100644 --- a/toolkit/components/reader/content/aboutReader.html +++ b/toolkit/components/reader/content/aboutReader.html @@ -14,11 +14,23 @@ <meta name="viewport" content="width=device-width; user-scalable=0" /> <link rel="stylesheet" + href="chrome://global/skin/design-system/tokens-brand.css" + /> + <link + rel="stylesheet" href="chrome://global/skin/aboutReader.css" type="text/css" /> <link rel="localization" href="toolkit/about/aboutReader.ftl" /> <link rel="localization" href="toolkit/branding/brandings.ftl" /> + <script + type="module" + src="chrome://global/content/reader/color-input.mjs" + ></script> + <script + type="module" + src="chrome://global/content/elements/named-deck.js" + ></script> </head> <body> @@ -53,7 +65,6 @@ </button> </li> <li class="dropdown-popup"> - <div class="dropdown-arrow"></div> <div class="font-type-buttons radiorow"></div> <div class="font-size-buttons buttonrow"> <button @@ -88,7 +99,63 @@ data-l10n-id="about-reader-toolbar-lineheightplus" ></button> </div> - <div class="color-scheme-buttons radiorow"></div> + <div + class="color-scheme-buttons radiorow" + id="regular-color-scheme" + hidden="false" + ></div> + </li> + </ul> + <ul + class="dropdown colors-dropdown" + id="custom-colors-color-scheme" + hidden="true" + > + <li> + <button + class="dropdown-toggle toolbar-button colors-button" + aria-labelledby="toolbar-color-controls" + data-telemetry-id="reader-color-controls" + > + <span + class="hover-label" + id="toolbar-color-controls" + data-l10n-id="about-reader-toolbar-color-controls" + ></span> + </button> + </li> + <li class="dropdown-popup" id="color-controls"> + <h2 + data-l10n-id="about-reader-colors-menu-header" + id="about-reader-colors-menu-header" + ></h2> + <button-group aria-labelledby="about-reader-colors-menu-header"> + <button + is="named-deck-button" + deck="tabs-deck" + name="fxtheme" + data-l10n-id="about-reader-fxtheme-tab" + ></button> + <button + is="named-deck-button" + deck="tabs-deck" + name="customtheme" + data-l10n-id="about-reader-customtheme-tab" + ></button> + </button-group> + <named-deck id="tabs-deck" is-tabbed> + <div + name="fxtheme" + class="colors-menu-color-scheme-buttons radiorow" + ></div> + <div name="customtheme"> + <ul class="custom-colors-selection"></ul> + <button + class="custom-colors-reset-button" + data-l10n-id="about-reader-custom-colors-reset-button" + ></button> + </div> + </named-deck> </li> </ul> </div> diff --git a/toolkit/components/reader/jar.mn b/toolkit/components/reader/jar.mn index 4b72136192..587dd45cf8 100644 --- a/toolkit/components/reader/jar.mn +++ b/toolkit/components/reader/jar.mn @@ -4,3 +4,5 @@ toolkit.jar: content/global/reader/aboutReader.html (content/aboutReader.html) + content/global/reader/color-input.css (color-input.css) + content/global/reader/color-input.mjs (color-input.mjs) diff --git a/toolkit/components/reader/moz.build b/toolkit/components/reader/moz.build index e57a0fe705..dba7a5a2ef 100644 --- a/toolkit/components/reader/moz.build +++ b/toolkit/components/reader/moz.build @@ -22,7 +22,9 @@ EXTRA_JS_MODULES.reader = [ "ReaderWorker.sys.mjs", ] -BROWSER_CHROME_MANIFESTS += ["test/browser.toml"] +BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.toml"] + +MOCHITEST_CHROME_MANIFESTS += ["tests/chrome/chrome.toml"] with Files("**"): BUG_COMPONENT = ("Toolkit", "Reader Mode") diff --git a/toolkit/components/reader/test/browser.toml b/toolkit/components/reader/tests/browser/browser.toml index 9382e6d60f..9382e6d60f 100644 --- a/toolkit/components/reader/test/browser.toml +++ b/toolkit/components/reader/tests/browser/browser.toml diff --git a/toolkit/components/reader/test/browser_bug1124271_readerModePinnedTab.js b/toolkit/components/reader/tests/browser/browser_bug1124271_readerModePinnedTab.js index 346d503675..346d503675 100644 --- a/toolkit/components/reader/test/browser_bug1124271_readerModePinnedTab.js +++ b/toolkit/components/reader/tests/browser/browser_bug1124271_readerModePinnedTab.js diff --git a/toolkit/components/reader/test/browser_bug1453818_samesite_cookie.js b/toolkit/components/reader/tests/browser/browser_bug1453818_samesite_cookie.js index 1fbfdeabfb..1fbfdeabfb 100644 --- a/toolkit/components/reader/test/browser_bug1453818_samesite_cookie.js +++ b/toolkit/components/reader/tests/browser/browser_bug1453818_samesite_cookie.js diff --git a/toolkit/components/reader/test/browser_bug1780350_readerModeSaveScroll.js b/toolkit/components/reader/tests/browser/browser_bug1780350_readerModeSaveScroll.js index 76add5511e..76add5511e 100644 --- a/toolkit/components/reader/test/browser_bug1780350_readerModeSaveScroll.js +++ b/toolkit/components/reader/tests/browser/browser_bug1780350_readerModeSaveScroll.js diff --git a/toolkit/components/reader/test/browser_drag_url_readerMode.js b/toolkit/components/reader/tests/browser/browser_drag_url_readerMode.js index 2dae1872c3..2dae1872c3 100644 --- a/toolkit/components/reader/test/browser_drag_url_readerMode.js +++ b/toolkit/components/reader/tests/browser/browser_drag_url_readerMode.js diff --git a/toolkit/components/reader/test/browser_localfile_readerMode.js b/toolkit/components/reader/tests/browser/browser_localfile_readerMode.js index 118e4bb23f..118e4bb23f 100644 --- a/toolkit/components/reader/test/browser_localfile_readerMode.js +++ b/toolkit/components/reader/tests/browser/browser_localfile_readerMode.js diff --git a/toolkit/components/reader/test/browser_readerMode.js b/toolkit/components/reader/tests/browser/browser_readerMode.js index a38e0a6de6..9093aa7fc1 100644 --- a/toolkit/components/reader/test/browser_readerMode.js +++ b/toolkit/components/reader/tests/browser/browser_readerMode.js @@ -23,7 +23,7 @@ ChromeUtils.defineESModuleGetters(this, { add_task(async function test_reader_button() { registerCleanupFunction(function () { // Reset test prefs. - TEST_PREFS.forEach(([name, value]) => { + TEST_PREFS.forEach(([name]) => { Services.prefs.clearUserPref(name); }); while (gBrowser.tabs.length > 1) { diff --git a/toolkit/components/reader/test/browser_readerMode_bc_reuse.js b/toolkit/components/reader/tests/browser/browser_readerMode_bc_reuse.js index 9ac0e367ca..9ac0e367ca 100644 --- a/toolkit/components/reader/test/browser_readerMode_bc_reuse.js +++ b/toolkit/components/reader/tests/browser/browser_readerMode_bc_reuse.js diff --git a/toolkit/components/reader/test/browser_readerMode_cached.js b/toolkit/components/reader/tests/browser/browser_readerMode_cached.js index 7f36a15dbb..ca9dd4a447 100644 --- a/toolkit/components/reader/test/browser_readerMode_cached.js +++ b/toolkit/components/reader/tests/browser/browser_readerMode_cached.js @@ -7,7 +7,7 @@ // contained within it, so if the article gets reloaded instead of using // the cached version, it would have a different value in it. const URL = - "http://mochi.test:8888/browser/toolkit/components/reader/test/readerModeRandom.sjs"; + "http://mochi.test:8888/browser/toolkit/components/reader/tests/browser/readerModeRandom.sjs"; add_task(async function () { let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL); diff --git a/toolkit/components/reader/test/browser_readerMode_colorSchemePref.js b/toolkit/components/reader/tests/browser/browser_readerMode_colorSchemePref.js index 0bee6eaf05..73d3f160b8 100644 --- a/toolkit/components/reader/test/browser_readerMode_colorSchemePref.js +++ b/toolkit/components/reader/tests/browser/browser_readerMode_colorSchemePref.js @@ -65,3 +65,57 @@ add_task(async function () { await testColorScheme(0, "sepia"); await testColorScheme(1, "sepia"); }); + +async function testCustomColors(aPref, color) { + // Set the theme selection to custom. + Services.prefs.setBoolPref("reader.colors_menu.enabled", true); + Services.prefs.setCharPref("reader.color_scheme", "custom"); + + // Set the custom pref to the color value. + Services.prefs.setCharPref(`reader.custom_colors.${aPref}`, color); + + // Open a browser tab, enter reader mode, and test if the page colors + // reflect the pref selection. + await BrowserTestUtils.withNewTab( + TEST_PATH + "readerModeArticle.html", + async function (browser) { + let pageShownPromise = BrowserTestUtils.waitForContentEvent( + browser, + "AboutReaderContentReady" + ); + + let readerButton = document.getElementById("reader-mode-button"); + readerButton.click(); + await pageShownPromise; + + let colorScheme = Services.prefs.getCharPref("reader.color_scheme"); + Assert.equal(colorScheme, "custom"); + let prefValue = Services.prefs.getStringPref( + `reader.custom_colors.${aPref}` + ); + let cssProp = `--custom-theme-${aPref}`; + + await SpecialPowers.spawn( + browser, + [prefValue, cssProp], + (customColor, prop) => { + let style = content.window.getComputedStyle(content.document.body); + let actualColor = style.getPropertyValue(prop); + Assert.equal(customColor, actualColor); + } + ); + } + ); +} + +/** + * Test that the custom color scheme selection updates the document colors correctly. + */ +add_task(async function () { + await testCustomColors("foreground", "#ffffff"); + await testCustomColors("background", "#000000"); + await testCustomColors("unvisited-links", "#ffffff"); + await testCustomColors("visited-links", "#ffffff"); + await testCustomColors("visited-links", "#ffffff"); + await testCustomColors("selection-highlight", "#ffffff"); +}); diff --git a/toolkit/components/reader/test/browser_readerMode_hidden_nodes.js b/toolkit/components/reader/tests/browser/browser_readerMode_hidden_nodes.js index d8e432a164..a6d058dfb1 100644 --- a/toolkit/components/reader/test/browser_readerMode_hidden_nodes.js +++ b/toolkit/components/reader/tests/browser/browser_readerMode_hidden_nodes.js @@ -18,7 +18,7 @@ var readerButton = document.getElementById("reader-mode-button"); add_task(async function test_reader_button() { registerCleanupFunction(function () { // Reset test prefs. - TEST_PREFS.forEach(([name, value]) => { + TEST_PREFS.forEach(([name]) => { Services.prefs.clearUserPref(name); }); while (gBrowser.tabs.length > 1) { diff --git a/toolkit/components/reader/test/browser_readerMode_menu.js b/toolkit/components/reader/tests/browser/browser_readerMode_menu.js index 304fefa5d0..2c08faf4f9 100644 --- a/toolkit/components/reader/test/browser_readerMode_menu.js +++ b/toolkit/components/reader/tests/browser/browser_readerMode_menu.js @@ -41,28 +41,34 @@ add_task(async function () { dispatchMouseEvent(win, target, "click"); } - let doc = content.document; - let win = content.window; - let styleButton = doc.querySelector(".style-button"); + async function testOpenCloseDropdown(target) { + let button = doc.querySelector(`.${target}-button`); - let styleDropdown = doc.querySelector(".style-dropdown"); - ok(!styleDropdown.classList.contains("open"), "dropdown is closed"); + let dropdown = doc.querySelector(`.${target}-dropdown`); + ok(!dropdown.classList.contains("open"), "dropdown is closed"); - simulateClick(styleButton); - ok(styleDropdown.classList.contains("open"), "dropdown is open"); + simulateClick(button); + ok(dropdown.classList.contains("open"), "dropdown is open"); - // simulate clicking on the article title to close the dropdown - let title = doc.querySelector("h1"); - simulateClick(title); - ok(!styleDropdown.classList.contains("open"), "dropdown is closed"); + // simulate clicking on the article title to close the dropdown + let title = doc.querySelector(".reader-title"); + simulateClick(title); + ok(!dropdown.classList.contains("open"), "dropdown is closed"); - // reopen the dropdown - simulateClick(styleButton); - ok(styleDropdown.classList.contains("open"), "dropdown is open"); + // reopen the dropdown + simulateClick(button); + ok(dropdown.classList.contains("open"), "dropdown is open"); + + // now click on the button again to close it + simulateClick(button); + ok(!dropdown.classList.contains("open"), "dropdown is closed"); + } + + let doc = content.document; + let win = content.window; - // now click on the button again to close it - simulateClick(styleButton); - ok(!styleDropdown.classList.contains("open"), "dropdown is closed"); + testOpenCloseDropdown("style"); + testOpenCloseDropdown("colors"); }); } ); diff --git a/toolkit/components/reader/test/browser_readerMode_pocket.js b/toolkit/components/reader/tests/browser/browser_readerMode_pocket.js index 43426383ff..43426383ff 100644 --- a/toolkit/components/reader/test/browser_readerMode_pocket.js +++ b/toolkit/components/reader/tests/browser/browser_readerMode_pocket.js diff --git a/toolkit/components/reader/test/browser_readerMode_readingTime.js b/toolkit/components/reader/tests/browser/browser_readerMode_readingTime.js index 91631b6234..91631b6234 100644 --- a/toolkit/components/reader/test/browser_readerMode_readingTime.js +++ b/toolkit/components/reader/tests/browser/browser_readerMode_readingTime.js diff --git a/toolkit/components/reader/test/browser_readerMode_refresh.js b/toolkit/components/reader/tests/browser/browser_readerMode_refresh.js index 00b4557f70..00b4557f70 100644 --- a/toolkit/components/reader/test/browser_readerMode_refresh.js +++ b/toolkit/components/reader/tests/browser/browser_readerMode_refresh.js diff --git a/toolkit/components/reader/test/browser_readerMode_remoteType.js b/toolkit/components/reader/tests/browser/browser_readerMode_remoteType.js index c2510667c8..c2510667c8 100644 --- a/toolkit/components/reader/test/browser_readerMode_remoteType.js +++ b/toolkit/components/reader/tests/browser/browser_readerMode_remoteType.js diff --git a/toolkit/components/reader/test/browser_readerMode_samesite_cookie_redirect.js b/toolkit/components/reader/tests/browser/browser_readerMode_samesite_cookie_redirect.js index 22703b9a81..22703b9a81 100644 --- a/toolkit/components/reader/test/browser_readerMode_samesite_cookie_redirect.js +++ b/toolkit/components/reader/tests/browser/browser_readerMode_samesite_cookie_redirect.js diff --git a/toolkit/components/reader/test/browser_readerMode_with_anchor.js b/toolkit/components/reader/tests/browser/browser_readerMode_with_anchor.js index 229daaed9d..229daaed9d 100644 --- a/toolkit/components/reader/test/browser_readerMode_with_anchor.js +++ b/toolkit/components/reader/tests/browser/browser_readerMode_with_anchor.js diff --git a/toolkit/components/reader/test/getCookies.sjs b/toolkit/components/reader/tests/browser/getCookies.sjs index 02e29fd877..02e29fd877 100644 --- a/toolkit/components/reader/test/getCookies.sjs +++ b/toolkit/components/reader/tests/browser/getCookies.sjs diff --git a/toolkit/components/reader/test/head.js b/toolkit/components/reader/tests/browser/head.js index 5f9baf8fcd..5f9baf8fcd 100644 --- a/toolkit/components/reader/test/head.js +++ b/toolkit/components/reader/tests/browser/head.js diff --git a/toolkit/components/reader/test/linkToGetCookies.html b/toolkit/components/reader/tests/browser/linkToGetCookies.html index 341046a21d..a05d32af4e 100644 --- a/toolkit/components/reader/test/linkToGetCookies.html +++ b/toolkit/components/reader/tests/browser/linkToGetCookies.html @@ -7,7 +7,7 @@ <article> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.</p> - <p><a href="http://example.com/browser/toolkit/components/reader/test/getCookies.sjs" id="link">Cross-origin link to getCookies.html</a></p> + <p><a href="http://example.com/browser/toolkit/components/reader/tests/browser/getCookies.sjs" id="link">Cross-origin link to getCookies.html</a></p> </article> </body> </html> diff --git a/toolkit/components/reader/test/readerModeArticle.html b/toolkit/components/reader/tests/browser/readerModeArticle.html index a0f1c64da0..a0f1c64da0 100644 --- a/toolkit/components/reader/test/readerModeArticle.html +++ b/toolkit/components/reader/tests/browser/readerModeArticle.html diff --git a/toolkit/components/reader/test/readerModeArticleContainsLink.html b/toolkit/components/reader/tests/browser/readerModeArticleContainsLink.html index 871349adcd..be2d7d6469 100644 --- a/toolkit/components/reader/test/readerModeArticleContainsLink.html +++ b/toolkit/components/reader/tests/browser/readerModeArticleContainsLink.html @@ -12,7 +12,7 @@ <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.</p> <p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p> <p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p> -<p><a href="http://example.com/browser/toolkit/components/reader/test/readerModeArticle.html" id="link">Link to another page.</a></p> +<p><a href="http://example.com/browser/toolkit/components/reader/tests/browser/readerModeArticle.html" id="link">Link to another page.</a></p> <p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p> <p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p> </div> diff --git a/toolkit/components/reader/test/readerModeArticleHiddenNodes.html b/toolkit/components/reader/tests/browser/readerModeArticleHiddenNodes.html index 92441b7978..92441b7978 100644 --- a/toolkit/components/reader/test/readerModeArticleHiddenNodes.html +++ b/toolkit/components/reader/tests/browser/readerModeArticleHiddenNodes.html diff --git a/toolkit/components/reader/test/readerModeArticleMedium.html b/toolkit/components/reader/tests/browser/readerModeArticleMedium.html index 70b172cf63..70b172cf63 100644 --- a/toolkit/components/reader/test/readerModeArticleMedium.html +++ b/toolkit/components/reader/tests/browser/readerModeArticleMedium.html diff --git a/toolkit/components/reader/test/readerModeArticleShort.html b/toolkit/components/reader/tests/browser/readerModeArticleShort.html index 692471f27f..692471f27f 100644 --- a/toolkit/components/reader/test/readerModeArticleShort.html +++ b/toolkit/components/reader/tests/browser/readerModeArticleShort.html diff --git a/toolkit/components/reader/test/readerModeArticleTextPlain.txt b/toolkit/components/reader/tests/browser/readerModeArticleTextPlain.txt index c5b7861b73..c5b7861b73 100644 --- a/toolkit/components/reader/test/readerModeArticleTextPlain.txt +++ b/toolkit/components/reader/tests/browser/readerModeArticleTextPlain.txt diff --git a/toolkit/components/reader/test/readerModeNonArticle.html b/toolkit/components/reader/tests/browser/readerModeNonArticle.html index e216af3c1f..e216af3c1f 100644 --- a/toolkit/components/reader/test/readerModeNonArticle.html +++ b/toolkit/components/reader/tests/browser/readerModeNonArticle.html diff --git a/toolkit/components/reader/test/readerModeRandom.sjs b/toolkit/components/reader/tests/browser/readerModeRandom.sjs index f6bb15c06a..f6bb15c06a 100644 --- a/toolkit/components/reader/test/readerModeRandom.sjs +++ b/toolkit/components/reader/tests/browser/readerModeRandom.sjs diff --git a/toolkit/components/reader/test/setSameSiteCookie.html b/toolkit/components/reader/tests/browser/setSameSiteCookie.html index 67bb714922..67bb714922 100644 --- a/toolkit/components/reader/test/setSameSiteCookie.html +++ b/toolkit/components/reader/tests/browser/setSameSiteCookie.html diff --git a/toolkit/components/reader/test/setSameSiteCookie.html^headers^ b/toolkit/components/reader/tests/browser/setSameSiteCookie.html^headers^ index c0229c93b6..c0229c93b6 100644 --- a/toolkit/components/reader/test/setSameSiteCookie.html^headers^ +++ b/toolkit/components/reader/tests/browser/setSameSiteCookie.html^headers^ diff --git a/toolkit/components/reader/tests/chrome/chrome.toml b/toolkit/components/reader/tests/chrome/chrome.toml new file mode 100644 index 0000000000..e416c49181 --- /dev/null +++ b/toolkit/components/reader/tests/chrome/chrome.toml @@ -0,0 +1,3 @@ +[DEFAULT] + +["test_color_input.html"] diff --git a/toolkit/components/reader/tests/chrome/test_color_input.html b/toolkit/components/reader/tests/chrome/test_color_input.html new file mode 100644 index 0000000000..4cd8cde77b --- /dev/null +++ b/toolkit/components/reader/tests/chrome/test_color_input.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>ColorInput Tests</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script type="module" src="chrome://global/content/reader/color-input.mjs"></script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: block"> + <color-input color="#ffffff" prop-name="test-prop" data-l10n-id="color-input-test-label"></color-input> +</div> +<pre id="test"> + <script class="testbody" type="application/javascript"> + add_task(async function testColorInput() { + const colorInput = document.querySelector("color-input"); + ok(colorInput, "color input element is rendered"); + + let input = colorInput.shadowRoot.querySelector("input"); + is(input.value, "#ffffff", "color input has the correct initial value"); + }); + add_task(async function testColorInputEvents() { + const colorInput = document.querySelector("color-input"); + let input = colorInput.shadowRoot.querySelector("input"); + + const pickedColor = new Promise((resolve) => { + colorInput.addEventListener("color-picked", (event) => resolve(event.detail), { once: true }); + }); + input.value = "#0000ff"; + input.dispatchEvent(new Event("input")); + let color = await pickedColor; + is(color, "#0000ff", "color-picked event dispatches on input"); + }); + </script> +</pre> +</body> +</html> diff --git a/toolkit/components/remotebrowserutils/tests/browser/browser_documentChannel.js b/toolkit/components/remotebrowserutils/tests/browser/browser_documentChannel.js index f32216010b..87bf94a59b 100644 --- a/toolkit/components/remotebrowserutils/tests/browser/browser_documentChannel.js +++ b/toolkit/components/remotebrowserutils/tests/browser/browser_documentChannel.js @@ -37,14 +37,14 @@ const EXTENSION_DATA = { async background() { browser.test.log("background script running"); browser.webRequest.onAuthRequired.addListener( - async details => { + async () => { browser.test.log("webRequest onAuthRequired"); // A blocking request that returns a promise exercises a codepath that // sets the notificationCallbacks on the channel to a JS object that we // can't do directly QueryObject on with expected results. // This triggered a crash which was fixed in bug 1528188. - return new Promise((resolve, reject) => { + return new Promise(resolve => { setTimeout(resolve, 0); }); }, diff --git a/toolkit/components/remotebrowserutils/tests/browser/browser_httpCrossOriginOpenerPolicy.js b/toolkit/components/remotebrowserutils/tests/browser/browser_httpCrossOriginOpenerPolicy.js index 5f3eade823..7ef8204246 100644 --- a/toolkit/components/remotebrowserutils/tests/browser/browser_httpCrossOriginOpenerPolicy.js +++ b/toolkit/components/remotebrowserutils/tests/browser/browser_httpCrossOriginOpenerPolicy.js @@ -106,7 +106,7 @@ function waitForDownloadWindow() { var domwindow = aXULWindow.docShell.domWindow; domwindow.addEventListener("load", downloadOnLoad, true); }, - onCloseWindow: aXULWindow => {}, + onCloseWindow: () => {}, }; Services.wm.addListener(listener); diff --git a/toolkit/components/remotebrowserutils/tests/browser/browser_oopProcessSwap.js b/toolkit/components/remotebrowserutils/tests/browser/browser_oopProcessSwap.js index 3286227d37..a30a919507 100644 --- a/toolkit/components/remotebrowserutils/tests/browser/browser_oopProcessSwap.js +++ b/toolkit/components/remotebrowserutils/tests/browser/browser_oopProcessSwap.js @@ -63,14 +63,11 @@ add_task(async function oopProcessSwap() { ); is(browser.browsingContext.children.length, 1); - - if (Services.prefs.getBoolPref("fission.preserve_browsing_contexts")) { - is( - frameId, - oopinfo.browsingContextId, - `BrowsingContext should not have changed (${frameId} != ${oopinfo.browsingContextId})` - ); - } + is( + frameId, + oopinfo.browsingContextId, + `BrowsingContext should not have changed (${frameId} != ${oopinfo.browsingContextId})` + ); is(oopinfo.location, WEB, "correct location"); } ); @@ -146,13 +143,11 @@ add_task(async function oopOriginProcessSwap() { ); is(browser.browsingContext.children.length, 1); - if (Services.prefs.getBoolPref("fission.preserve_browsing_contexts")) { - is( - frameId, - oopinfo.browsingContextId, - `BrowsingContext should not have changed (${frameId} != ${oopinfo.browsingContextId})` - ); - } + is( + frameId, + oopinfo.browsingContextId, + `BrowsingContext should not have changed (${frameId} != ${oopinfo.browsingContextId})` + ); is(oopinfo.location, ORG_POSTMSG, "correct location"); } ); diff --git a/toolkit/components/reportbrokensite/ReportBrokenSiteChild.sys.mjs b/toolkit/components/reportbrokensite/ReportBrokenSiteChild.sys.mjs index 291b1defc8..5f8ccaec3e 100644 --- a/toolkit/components/reportbrokensite/ReportBrokenSiteChild.sys.mjs +++ b/toolkit/components/reportbrokensite/ReportBrokenSiteChild.sys.mjs @@ -236,7 +236,7 @@ export class ReportBrokenSiteChild extends JSWindowActorChild { }); } - async #getConsoleLogs(docShell) { + async #getConsoleLogs() { return this.#getLoggedMessages() .flat() .sort((a, b) => a.timeStamp - b.timeStamp) diff --git a/toolkit/components/reputationservice/nsIApplicationReputation.idl b/toolkit/components/reputationservice/nsIApplicationReputation.idl index ea7f92038e..50a923ad2d 100644 --- a/toolkit/components/reputationservice/nsIApplicationReputation.idl +++ b/toolkit/components/reputationservice/nsIApplicationReputation.idl @@ -59,7 +59,7 @@ interface nsIApplicationReputationService : nsISupports { * @param aFilename * The filename to check. */ - bool isBinary(in AUTF8String aFilename); + boolean isBinary(in AUTF8String aFilename); /** * Check if a file with this name should be treated as an executable, @@ -70,7 +70,7 @@ interface nsIApplicationReputationService : nsISupports { * @param aFilename * The filename to check. */ - bool isExecutable(in AUTF8String aFilename); + boolean isExecutable(in AUTF8String aFilename); }; /** @@ -141,7 +141,7 @@ interface nsIApplicationReputationCallback : nsISupports { * This may be set to a value different than "VERDICT_SAFE" even if * aShouldBlock is false, so you should always check aShouldBlock. */ - void onComplete(in bool aShouldBlock, + void onComplete(in boolean aShouldBlock, in nsresult aStatus, in unsigned long aVerdict); }; diff --git a/toolkit/components/reputationservice/test/unit/test_app_rep.js b/toolkit/components/reputationservice/test/unit/test_app_rep.js index 9c381a7beb..d28b2100ef 100644 --- a/toolkit/components/reputationservice/test/unit/test_app_rep.js +++ b/toolkit/components/reputationservice/test/unit/test_app_rep.js @@ -115,7 +115,7 @@ add_task(async function test_setup() { gHttpServ = new HttpServer(); gHttpServ.registerDirectory("/", do_get_cwd()); - gHttpServ.registerPathHandler("/download", function (request, response) { + gHttpServ.registerPathHandler("/download", function (request) { if (gExpectedRemote) { let body = NetUtil.readInputStreamToString( request.bodyInputStream, diff --git a/toolkit/components/reputationservice/test/unit/test_app_rep_maclinux.js b/toolkit/components/reputationservice/test/unit/test_app_rep_maclinux.js index a89197e8ec..6274f8f75d 100644 --- a/toolkit/components/reputationservice/test/unit/test_app_rep_maclinux.js +++ b/toolkit/components/reputationservice/test/unit/test_app_rep_maclinux.js @@ -106,7 +106,7 @@ add_task(function test_setup() { return blob; } - gHttpServer.registerPathHandler("/throw", function (request, response) { + gHttpServer.registerPathHandler("/throw", function () { do_throw("We shouldn't be getting here"); }); diff --git a/toolkit/components/reputationservice/test/unit/test_app_rep_windows.js b/toolkit/components/reputationservice/test/unit/test_app_rep_windows.js index 597810859f..7ab164cd2a 100644 --- a/toolkit/components/reputationservice/test/unit/test_app_rep_windows.js +++ b/toolkit/components/reputationservice/test/unit/test_app_rep_windows.js @@ -212,7 +212,7 @@ add_task(async function test_setup() { return blob; } - gHttpServer.registerPathHandler("/throw", function (request, response) { + gHttpServer.registerPathHandler("/throw", function () { do_throw("We shouldn't be getting here"); }); diff --git a/toolkit/components/resistfingerprinting/FingerprintingWebCompatService.sys.mjs b/toolkit/components/resistfingerprinting/FingerprintingWebCompatService.sys.mjs index b6d5bed59c..56f6435e47 100644 --- a/toolkit/components/resistfingerprinting/FingerprintingWebCompatService.sys.mjs +++ b/toolkit/components/resistfingerprinting/FingerprintingWebCompatService.sys.mjs @@ -1,7 +1,7 @@ // -*- indent-tabs-mode: nil; js-indent-level: 2 -*- /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ + * You can obtain one at https://mozilla.org/MPL/2.0/. */ const lazy = {}; diff --git a/toolkit/components/resistfingerprinting/KeyCodeConsensus_En_US.h b/toolkit/components/resistfingerprinting/KeyCodeConsensus_En_US.h index c8fafcf22e..6558bcdc29 100644 --- a/toolkit/components/resistfingerprinting/KeyCodeConsensus_En_US.h +++ b/toolkit/components/resistfingerprinting/KeyCodeConsensus_En_US.h @@ -1,7 +1,7 @@ /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /** * This file contains the spoofed keycodes of en-US for fingerprinting diff --git a/toolkit/components/resistfingerprinting/RFPHelper.sys.mjs b/toolkit/components/resistfingerprinting/RFPHelper.sys.mjs index 223c0259b7..c586873a74 100644 --- a/toolkit/components/resistfingerprinting/RFPHelper.sys.mjs +++ b/toolkit/components/resistfingerprinting/RFPHelper.sys.mjs @@ -1,7 +1,7 @@ // -*- indent-tabs-mode: nil; js-indent-level: 2 -*- /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ + * You can obtain one at https://mozilla.org/MPL/2.0/. */ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; @@ -43,6 +43,8 @@ class _RFPHelper { this._initialized = true; // Add unconditional observers + Services.obs.addObserver(this, "user-characteristics-populating-data"); + Services.obs.addObserver(this, "user-characteristics-populating-data-done"); Services.prefs.addObserver(kPrefResistFingerprinting, this); Services.prefs.addObserver(kPrefLetterboxing, this); XPCOMUtils.defineLazyPreferenceGetter( @@ -80,6 +82,12 @@ class _RFPHelper { observe(subject, topic, data) { switch (topic) { + case "user-characteristics-populating-data": + this._registerUserCharacteristicsActor(); + break; + case "user-characteristics-populating-data-done": + this._unregisterUserCharacteristicsActor(); + break; case "nsPref:changed": this._handlePrefChanged(data); break; @@ -97,6 +105,28 @@ class _RFPHelper { } } + _registerUserCharacteristicsActor() { + log("_registerUserCharacteristicsActor()"); + ChromeUtils.registerWindowActor("UserCharacteristics", { + parent: { + esModuleURI: "resource://gre/actors/UserCharacteristicsParent.sys.mjs", + }, + child: { + esModuleURI: "resource://gre/actors/UserCharacteristicsChild.sys.mjs", + events: { + UserCharacteristicsDataDone: { wantUntrusted: true }, + }, + }, + matches: ["about:fingerprinting"], + remoteTypes: ["privilegedabout"], + }); + } + + _unregisterUserCharacteristicsActor() { + log("_unregisterUserCharacteristicsActor()"); + ChromeUtils.unregisterWindowActor("UserCharacteristics"); + } + handleEvent(aMessage) { switch (aMessage.type) { case "TabOpen": { @@ -186,7 +216,7 @@ class _RFPHelper { ); } - _handleHttpOnModifyRequest(subject, data) { + _handleHttpOnModifyRequest(subject) { // If we are loading an HTTP page from content, show the // "request English language web pages?" prompt. let httpChannel = subject.QueryInterface(Ci.nsIHttpChannel); @@ -283,16 +313,16 @@ class _RFPHelper { _handleLetterboxingPrefChanged() { if (Services.prefs.getBoolPref(kPrefLetterboxing, false)) { Services.ww.registerNotification(this); - this._registerActor(); + this._registerLetterboxingActor(); this._attachAllWindows(); } else { - this._unregisterActor(); + this._unregisterLetterboxingActor(); this._detachAllWindows(); Services.ww.unregisterNotification(this); } } - _registerActor() { + _registerLetterboxingActor() { ChromeUtils.registerWindowActor("RFPHelper", { parent: { esModuleURI: "resource:///actors/RFPHelperParent.sys.mjs", @@ -307,7 +337,7 @@ class _RFPHelper { }); } - _unregisterActor() { + _unregisterLetterboxingActor() { ChromeUtils.unregisterWindowActor("RFPHelper"); } diff --git a/toolkit/components/resistfingerprinting/RFPTargetIPCUtils.h b/toolkit/components/resistfingerprinting/RFPTargetIPCUtils.h index 198332e3f6..414aa94905 100644 --- a/toolkit/components/resistfingerprinting/RFPTargetIPCUtils.h +++ b/toolkit/components/resistfingerprinting/RFPTargetIPCUtils.h @@ -2,7 +2,7 @@ /* 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/. */ + * You can obtain one at https://mozilla.org/MPL/2.0/. */ #ifndef __RFPTargetIPCUtils_h__ #define __RFPTargetIPCUtils_h__ diff --git a/toolkit/components/resistfingerprinting/RFPTargets.inc b/toolkit/components/resistfingerprinting/RFPTargets.inc index d2327ffbfe..9bc9dcbb8e 100644 --- a/toolkit/components/resistfingerprinting/RFPTargets.inc +++ b/toolkit/components/resistfingerprinting/RFPTargets.inc @@ -1,7 +1,7 @@ /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ // Names should not be re-used. diff --git a/toolkit/components/resistfingerprinting/RelativeTimeline.cpp b/toolkit/components/resistfingerprinting/RelativeTimeline.cpp index dd2d304adb..84fdeb93f8 100644 --- a/toolkit/components/resistfingerprinting/RelativeTimeline.cpp +++ b/toolkit/components/resistfingerprinting/RelativeTimeline.cpp @@ -1,7 +1,7 @@ /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ #include "nsCOMPtr.h" #include "nsIRandomGenerator.h" diff --git a/toolkit/components/resistfingerprinting/RelativeTimeline.h b/toolkit/components/resistfingerprinting/RelativeTimeline.h index bf23de904d..c8ad1c7aad 100644 --- a/toolkit/components/resistfingerprinting/RelativeTimeline.h +++ b/toolkit/components/resistfingerprinting/RelativeTimeline.h @@ -1,7 +1,7 @@ /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ #ifndef __RelativeTimeline_h__ #define __RelativeTimeline_h__ diff --git a/toolkit/components/resistfingerprinting/UserCharacteristicsPageService.sys.mjs b/toolkit/components/resistfingerprinting/UserCharacteristicsPageService.sys.mjs new file mode 100644 index 0000000000..d24a2eae33 --- /dev/null +++ b/toolkit/components/resistfingerprinting/UserCharacteristicsPageService.sys.mjs @@ -0,0 +1,209 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + HiddenFrame: "resource://gre/modules/HiddenFrame.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(lazy, "console", () => { + return console.createInstance({ + prefix: "UserCharacteristicsPage", + maxLogLevelPref: "toolkit.telemetry.user_characteristics_ping.logLevel", + }); +}); + +const BACKGROUND_WIDTH = 1024; +const BACKGROUND_HEIGHT = 768; + +/** + * A manager for hidden browsers. Responsible for creating and destroying a + * hidden frame to hold them. + * All of this is copied from PageDataService.sys.mjs + */ +class HiddenBrowserManager { + /** + * The hidden frame if one has been created. + * + * @type {HiddenFrame | null} + */ + #frame = null; + /** + * The number of hidden browser elements currently in use. + * + * @type {number} + */ + #browsers = 0; + + /** + * Creates and returns a new hidden browser. + * + * @returns {Browser} + */ + async #acquireBrowser() { + this.#browsers++; + if (!this.#frame) { + this.#frame = new lazy.HiddenFrame(); + } + + let frame = await this.#frame.get(); + let doc = frame.document; + let browser = doc.createXULElement("browser"); + browser.setAttribute("remote", "true"); + browser.setAttribute("type", "content"); + browser.setAttribute( + "style", + ` + width: ${BACKGROUND_WIDTH}px; + min-width: ${BACKGROUND_WIDTH}px; + height: ${BACKGROUND_HEIGHT}px; + min-height: ${BACKGROUND_HEIGHT}px; + ` + ); + browser.setAttribute("maychangeremoteness", "true"); + doc.documentElement.appendChild(browser); + + return browser; + } + + /** + * Releases the given hidden browser. + * + * @param {Browser} browser + * The hidden browser element. + */ + #releaseBrowser(browser) { + browser.remove(); + + this.#browsers--; + if (this.#browsers == 0) { + this.#frame.destroy(); + this.#frame = null; + } + } + + /** + * Calls a callback function with a new hidden browser. + * This function will return whatever the callback function returns. + * + * @param {Callback} callback + * The callback function will be called with the browser element and may + * be asynchronous. + * @returns {T} + */ + async withHiddenBrowser(callback) { + let browser = await this.#acquireBrowser(); + try { + return await callback(browser); + } finally { + this.#releaseBrowser(browser); + } + } +} + +export class UserCharacteristicsPageService { + classId = Components.ID("{ce3e9659-e311-49fb-b18b-7f27c6659b23}"); + QueryInterface = ChromeUtils.generateQI([ + "nsIUserCharacteristicsPageService", + ]); + + _initialized = false; + _isParentProcess = false; + + /** + * A manager for hidden browsers. + * + * @type {HiddenBrowserManager} + */ + _browserManager = new HiddenBrowserManager(); + + /** + * A map of hidden browsers to a resolve function that should be passed the + * actor that was created for the browser. + * + * @type {WeakMap<Browser, function(PageDataParent): void>} + */ + _backgroundBrowsers = new WeakMap(); + + constructor() { + lazy.console.debug("Init"); + + if ( + Services.appinfo.processType !== Services.appinfo.PROCESS_TYPE_DEFAULT + ) { + throw new Error( + "Shouldn't init UserCharacteristicsPage in content processes." + ); + } + + // Return if we have initiated. + if (this._initialized) { + lazy.console.warn("preventing re-initilization..."); + return; + } + this._initialized = true; + } + + shutdown() {} + + createContentPage() { + lazy.console.debug("called createContentPage"); + return this._browserManager.withHiddenBrowser(async browser => { + lazy.console.debug(`In withHiddenBrowser`); + try { + let { promise, resolve } = Promise.withResolvers(); + this._backgroundBrowsers.set(browser, resolve); + + let principal = Services.scriptSecurityManager.getSystemPrincipal(); + let loadURIOptions = { + triggeringPrincipal: principal, + }; + + let userCharacteristicsPageURI = Services.io.newURI( + "about:fingerprinting" + ); + + browser.loadURI(userCharacteristicsPageURI, loadURIOptions); + + let data = await promise; + if (data.debug) { + lazy.console.debug(`Debugging Output:`); + for (let line of data.debug) { + lazy.console.debug(line); + } + lazy.console.debug(`(debugging output done)`); + } + lazy.console.debug(`Data:`, data.output); + + lazy.console.debug("Populating Glean metrics..."); + Glean.characteristics.timezone.set(data.output.foo); + + lazy.console.debug("Unregistering actor"); + Services.obs.notifyObservers( + null, + "user-characteristics-populating-data-done" + ); + } finally { + this._backgroundBrowsers.delete(browser); + } + }); + } + + async pageLoaded(browsingContext, data) { + lazy.console.debug( + `pageLoaded browsingContext=${browsingContext} data=${data}` + ); + + let browser = browsingContext.embedderElement; + + let backgroundResolve = this._backgroundBrowsers.get(browser); + if (backgroundResolve) { + backgroundResolve(data); + return; + } + throw new Error(`No backround resolve for ${browser} found`); + } +} diff --git a/toolkit/components/resistfingerprinting/components.conf b/toolkit/components/resistfingerprinting/components.conf index aae29becf4..d7a5f71578 100644 --- a/toolkit/components/resistfingerprinting/components.conf +++ b/toolkit/components/resistfingerprinting/components.conf @@ -2,7 +2,7 @@ # vim: set filetype=python: # 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/. +# file, You can obtain one at https://mozilla.org/MPL/2.0/. Classes = [ { @@ -34,4 +34,11 @@ Classes = [ 'constructor': 'FingerprintingWebCompatService', 'processes': ProcessSelector.MAIN_PROCESS_ONLY, }, + { + 'cid': '{ce3e9659-e311-49fb-b18b-7f27c6659b23}', + 'contract_ids': ['@mozilla.org/user-characteristics-page;1'], + 'esModule': 'resource://gre/modules/UserCharacteristicsPageService.sys.mjs', + 'constructor': 'UserCharacteristicsPageService', + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, ] diff --git a/toolkit/components/resistfingerprinting/content/usercharacteristics.html b/toolkit/components/resistfingerprinting/content/usercharacteristics.html new file mode 100644 index 0000000000..50071bb21e --- /dev/null +++ b/toolkit/components/resistfingerprinting/content/usercharacteristics.html @@ -0,0 +1,18 @@ +<!-- 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 https://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" /> + <meta + http-equiv="Content-Security-Policy" + content="default-src 'none'; object-src 'none'; script-src chrome:" + /> + <title>about:fingerprinting</title> + </head> + <body> + <script src="chrome://global/content/usercharacteristics/usercharacteristics.js"></script> + </body> +</html> diff --git a/toolkit/components/resistfingerprinting/content/usercharacteristics.js b/toolkit/components/resistfingerprinting/content/usercharacteristics.js new file mode 100644 index 0000000000..1f10ba9cbb --- /dev/null +++ b/toolkit/components/resistfingerprinting/content/usercharacteristics.js @@ -0,0 +1,41 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +var debugMsgs = []; +function debug(...args) { + let msg = ""; + if (!args.length) { + debugMsgs.push(""); + return; + } + + let stringify = o => { + if (typeof o == "string") { + return o; + } + return JSON.stringify(o); + }; + + let stringifiedArgs = args.map(stringify); + msg += stringifiedArgs.join(" "); + debugMsgs.push(msg); +} + +debug("Debug Line"); +debug("Another debug line, with", { an: "object" }); + +// The first time we put a real value in here, please update browser_usercharacteristics.js +let output = { + foo: "Hello World", +}; + +document.dispatchEvent( + new CustomEvent("UserCharacteristicsDataDone", { + bubbles: true, + detail: { + debug: debugMsgs, + output, + }, + }) +); diff --git a/toolkit/components/resistfingerprinting/jar.mn b/toolkit/components/resistfingerprinting/jar.mn new file mode 100644 index 0000000000..657797b6d0 --- /dev/null +++ b/toolkit/components/resistfingerprinting/jar.mn @@ -0,0 +1,7 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +toolkit.jar: + content/global/usercharacteristics/usercharacteristics.html (content/usercharacteristics.html) + content/global/usercharacteristics/usercharacteristics.js (content/usercharacteristics.js) diff --git a/toolkit/components/resistfingerprinting/metrics.yaml b/toolkit/components/resistfingerprinting/metrics.yaml index 3c706d20fa..51b251508c 100644 --- a/toolkit/components/resistfingerprinting/metrics.yaml +++ b/toolkit/components/resistfingerprinting/metrics.yaml @@ -1,6 +1,6 @@ # 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/. +# file, You can obtain one at https://mozilla.org/MPL/2.0/. # Adding a new metric? We have docs for that! # https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html @@ -337,6 +337,23 @@ characteristics: data_sensitivity: - interaction + system_locale: + type: string + description: > + The locale used by the host OS for localization. + lifetime: application + send_in_pings: + - user-characteristics + notification_emails: + - tom@mozilla.com + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1881744 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1881744#c7 + expires: never + data_sensitivity: + - technical + target_frame_rate: type: quantity unit: int @@ -409,3 +426,105 @@ characteristics: expires: never data_sensitivity: - interaction + + prefs_privacy_donottrackheader_enabled: + type: boolean + description: > + Sending "do not track" HTTP header + lifetime: application + send_in_pings: + - user-characteristics + notification_emails: + - tom@mozilla.com + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1884693 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1884693#c5 + expires: never + data_sensitivity: + - interaction + + prefs_privacy_globalprivacycontrol_enabled: + type: boolean + description: > + Sending "global privacy control" HTTP header + lifetime: application + send_in_pings: + - user-characteristics + notification_emails: + - tom@mozilla.com + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1884693 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1884693#c5 + expires: never + data_sensitivity: + - interaction + + prefs_general_autoscroll: # general.autoScroll + type: boolean + description: > + Use autoscrolling + lifetime: application + send_in_pings: + - user-characteristics + notification_emails: + - tom@mozilla.com + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1884693 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1884693#c5 + expires: never + data_sensitivity: + - interaction + + prefs_general_smoothscroll: # general.smoothScroll + type: boolean + description: > + Use smooth scrolling + lifetime: application + send_in_pings: + - user-characteristics + notification_emails: + - tom@mozilla.com + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1884693 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1884693#c5 + expires: never + data_sensitivity: + - interaction + + prefs_overlay_scrollbars: # widget.gtk.overlay-scrollbars.enabled + type: boolean + description: > + Use overlay scrollbars (or otherwise "Always show scrollbars") + lifetime: application + send_in_pings: + - user-characteristics + notification_emails: + - tom@mozilla.com + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1884693 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1884693#c5 + expires: never + data_sensitivity: + - interaction + + prefs_block_popups: # dom.disable_open_during_load + type: boolean + description: > + Block pop-up windows (The dom.disable_open_during_load pref) + lifetime: application + send_in_pings: + - user-characteristics + notification_emails: + - tom@mozilla.com + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1884693 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1884693#c5 + expires: never + data_sensitivity: + - interaction diff --git a/toolkit/components/resistfingerprinting/moz.build b/toolkit/components/resistfingerprinting/moz.build index 9b4e9cc8ed..4b71617b9b 100644 --- a/toolkit/components/resistfingerprinting/moz.build +++ b/toolkit/components/resistfingerprinting/moz.build @@ -2,13 +2,15 @@ # vim: set filetype=python: # 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/. +# file, You can obtain one at https://mozilla.org/MPL/2.0/. with Files("**"): BUG_COMPONENT = ("Core", "Privacy: Anti-Tracking") TEST_DIRS += ["tests"] +JAR_MANIFESTS += ["jar.mn"] + UNIFIED_SOURCES += [ "nsRFPService.cpp", "RelativeTimeline.cpp", @@ -39,6 +41,7 @@ EXPORTS.mozilla.gtest += ["nsUserCharacteristics.h"] EXTRA_JS_MODULES += [ "FingerprintingWebCompatService.sys.mjs", "RFPHelper.sys.mjs", + "UserCharacteristicsPageService.sys.mjs", ] XPIDL_MODULE = "toolkit_resistfingerprinting" @@ -50,6 +53,7 @@ XPCOM_MANIFESTS += [ XPIDL_SOURCES += [ "nsIFingerprintingWebCompatService.idl", "nsIRFPService.idl", + "nsIUserCharacteristicsPageService.idl", ] include("/ipc/chromium/chromium-config.mozbuild") diff --git a/toolkit/components/resistfingerprinting/nsIFingerprintingWebCompatService.idl b/toolkit/components/resistfingerprinting/nsIFingerprintingWebCompatService.idl index 01b1379e44..e8c7195a0e 100644 --- a/toolkit/components/resistfingerprinting/nsIFingerprintingWebCompatService.idl +++ b/toolkit/components/resistfingerprinting/nsIFingerprintingWebCompatService.idl @@ -1,6 +1,6 @@ /* 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/. */ + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ #include "nsISupports.idl" diff --git a/toolkit/components/resistfingerprinting/nsIRFPService.idl b/toolkit/components/resistfingerprinting/nsIRFPService.idl index b81868d202..8c83cc7e6e 100644 --- a/toolkit/components/resistfingerprinting/nsIRFPService.idl +++ b/toolkit/components/resistfingerprinting/nsIRFPService.idl @@ -1,6 +1,6 @@ /* 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/. */ + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ #include "nsISupports.idl" #include "nsIFingerprintingWebCompatService.idl" diff --git a/toolkit/components/resistfingerprinting/nsIUserCharacteristicsPageService.idl b/toolkit/components/resistfingerprinting/nsIUserCharacteristicsPageService.idl new file mode 100644 index 0000000000..ce58c1a14a --- /dev/null +++ b/toolkit/components/resistfingerprinting/nsIUserCharacteristicsPageService.idl @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +webidl BrowsingContext; + +[scriptable, uuid(ce3e9659-e311-49fb-b18b-7f27c6659b23)] +interface nsIUserCharacteristicsPageService : nsISupports { + + /* + * Create the UserCharacteristics about: page as a HiddenFrame + * and begin the data collection. + */ + Promise createContentPage(); + + /* + * Called when the UserCharacteristics about: page has been loaded + * and supplied data back to the actor, which is passed as `data` + */ + void pageLoaded(in BrowsingContext browsingContext, in jsval data); +}; diff --git a/toolkit/components/resistfingerprinting/nsRFPService.cpp b/toolkit/components/resistfingerprinting/nsRFPService.cpp index 643cc2cb7a..f39deb3283 100644 --- a/toolkit/components/resistfingerprinting/nsRFPService.cpp +++ b/toolkit/components/resistfingerprinting/nsRFPService.cpp @@ -1,7 +1,7 @@ /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ #include "nsRFPService.h" @@ -19,6 +19,7 @@ #include "MainThreadUtils.h" #include "ScopedNSSTypes.h" +#include "mozilla/AntiTrackingUtils.h" #include "mozilla/ArrayIterator.h" #include "mozilla/Assertions.h" #include "mozilla/Atomics.h" @@ -109,6 +110,8 @@ static mozilla::LazyLogModule gFingerprinterDetection("FingerprinterDetection"); #define LAST_PB_SESSION_EXITED_TOPIC "last-pb-context-exited" #define IDLE_TOPIC "browser-idle-startup-tasks-finished" #define GFX_FEATURES "gfx-features-ready" +#define USER_CHARACTERISTICS_TEST_REQUEST \ + "user-characteristics-testing-please-populate-data" static constexpr uint32_t kVideoFramesPerSec = 30; static constexpr uint32_t kVideoDroppedRatio = 5; @@ -190,6 +193,9 @@ nsresult nsRFPService::Init() { rv = obs->AddObserver(this, GFX_FEATURES, false); NS_ENSURE_SUCCESS(rv, rv); + + rv = obs->AddObserver(this, USER_CHARACTERISTICS_TEST_REQUEST, false); + NS_ENSURE_SUCCESS(rv, rv); } Preferences::RegisterCallbacks(nsRFPService::PrefChanged, gCallbackPrefs, @@ -289,6 +295,7 @@ void nsRFPService::StartShutdown() { obs->RemoveObserver(this, OBSERVER_TOPIC_IDLE_DAILY); obs->RemoveObserver(this, IDLE_TOPIC); obs->RemoveObserver(this, GFX_FEATURES); + obs->RemoveObserver(this, USER_CHARACTERISTICS_TEST_REQUEST); } } @@ -350,6 +357,12 @@ nsRFPService::Observe(nsISupports* aObject, const char* aTopic, } } + if (!strcmp(USER_CHARACTERISTICS_TEST_REQUEST, aTopic) && + xpc::IsInAutomation()) { + nsUserCharacteristics::PopulateDataAndEventuallySubmit( + /* aUpdatePref = */ false, /* aTesting = */ true); + } + if (!strcmp(OBSERVER_TOPIC_IDLE_DAILY, aTopic)) { if (StaticPrefs:: privacy_resistFingerprinting_randomization_daily_reset_enabled()) { @@ -1267,7 +1280,10 @@ Maybe<nsTArray<uint8_t>> nsRFPService::GenerateKey(nsIChannel* aChannel) { // Set the partitionKey using the top level URI to ensure that the key is // specific to the top level site. - attrs.SetPartitionKey(topLevelURI); + bool foreignByAncestorContext = + AntiTrackingUtils::IsThirdPartyChannel(aChannel) && + loadInfo->GetIsThirdPartyContextToTopWindow(); + attrs.SetPartitionKey(topLevelURI, foreignByAncestorContext); nsAutoCString oaSuffix; attrs.CreateSuffix(oaSuffix); @@ -1337,8 +1353,14 @@ nsRFPService::CleanRandomKeyByPrincipal(nsIPrincipal* aPrincipal) { OriginAttributes attrs = aPrincipal->OriginAttributesRef(); nsCOMPtr<nsIURI> uri = aPrincipal->GetURI(); - attrs.SetPartitionKey(uri); + attrs.SetPartitionKey(uri, false); + ClearBrowsingSessionKey(attrs); + + // We must also include the cross-site embeds of this principal that end up + // re-embedded back into the same principal's top level, otherwise state will + // persist for this target + attrs.SetPartitionKey(uri, true); ClearBrowsingSessionKey(attrs); return NS_OK; } @@ -1354,14 +1376,21 @@ nsRFPService::CleanRandomKeyByDomain(const nsACString& aDomain) { // Use the originAttributes to get the partitionKey. OriginAttributes attrs; - attrs.SetPartitionKey(httpURI); + attrs.SetPartitionKey(httpURI, false); // Create a originAttributesPattern and set the http partitionKey to the // pattern. OriginAttributesPattern pattern; pattern.mPartitionKey.Reset(); pattern.mPartitionKey.Construct(attrs.mPartitionKey); + ClearBrowsingSessionKey(pattern); + // We must also include the cross-site embeds of this principal that end up + // re-embedded back into the same principal's top level, otherwise state will + // persist for this target + attrs.SetPartitionKey(httpURI, true); + pattern.mPartitionKey.Reset(); + pattern.mPartitionKey.Construct(attrs.mPartitionKey); ClearBrowsingSessionKey(pattern); // Get https URI from the domain. @@ -1370,10 +1399,17 @@ nsRFPService::CleanRandomKeyByDomain(const nsACString& aDomain) { NS_ENSURE_SUCCESS(rv, rv); // Use the originAttributes to get the partitionKey and set to the pattern. - attrs.SetPartitionKey(httpsURI); + attrs.SetPartitionKey(httpsURI, false); pattern.mPartitionKey.Reset(); pattern.mPartitionKey.Construct(attrs.mPartitionKey); + ClearBrowsingSessionKey(pattern); + // We must also include the cross-site embeds of this principal that end up + // re-embedded back into the same principal's top level, otherwise state will + // persist for this target + attrs.SetPartitionKey(httpsURI, true); + pattern.mPartitionKey.Reset(); + pattern.mPartitionKey.Construct(attrs.mPartitionKey); ClearBrowsingSessionKey(pattern); return NS_OK; } @@ -1395,7 +1431,7 @@ nsRFPService::CleanRandomKeyByHost(const nsACString& aHost, // Use the originAttributes to get the partitionKey. OriginAttributes attrs; - attrs.SetPartitionKey(httpURI); + attrs.SetPartitionKey(httpURI, false); // Set the partitionKey to the pattern. pattern.mPartitionKey.Reset(); @@ -1403,16 +1439,31 @@ nsRFPService::CleanRandomKeyByHost(const nsACString& aHost, ClearBrowsingSessionKey(pattern); + // We must also include the cross-site embeds of this principal that end up + // re-embedded back into the same principal's top level, otherwise state will + // persist for this target + attrs.SetPartitionKey(httpURI, true); + pattern.mPartitionKey.Reset(); + pattern.mPartitionKey.Construct(attrs.mPartitionKey); + ClearBrowsingSessionKey(pattern); + // Get https URI from the host. nsCOMPtr<nsIURI> httpsURI; rv = NS_NewURI(getter_AddRefs(httpsURI), "https://"_ns + aHost); NS_ENSURE_SUCCESS(rv, rv); // Use the originAttributes to get the partitionKey and set to the pattern. - attrs.SetPartitionKey(httpsURI); + attrs.SetPartitionKey(httpsURI, false); pattern.mPartitionKey.Reset(); pattern.mPartitionKey.Construct(attrs.mPartitionKey); + ClearBrowsingSessionKey(pattern); + // We must also include the cross-site embeds of this principal that end up + // re-embedded back into the same principal's top level, otherwise state will + // persist for this target + attrs.SetPartitionKey(httpsURI, true); + pattern.mPartitionKey.Reset(); + pattern.mPartitionKey.Construct(attrs.mPartitionKey); ClearBrowsingSessionKey(pattern); return NS_OK; } @@ -2011,19 +2062,63 @@ Maybe<RFPTarget> nsRFPService::GetOverriddenFingerprintingSettingsForChannel( } // The channel is for the first-party load. - if (!loadInfo->GetIsThirdPartyContextToTopWindow()) { + if (!AntiTrackingUtils::IsThirdPartyChannel(aChannel)) { return GetOverriddenFingerprintingSettingsForURI(uri, nullptr); } // The channel is for the third-party load. We get the first-party URI from // the top-level window global parent. - RefPtr<dom::WindowGlobalParent> topWGP = - bc->Top()->Canonical()->GetCurrentWindowGlobal(); + RefPtr<dom::CanonicalBrowsingContext> topBC = bc->Top()->Canonical(); + RefPtr<dom::WindowGlobalParent> topWGP = topBC->GetCurrentWindowGlobal(); if (NS_WARN_IF(!topWGP)) { return Nothing(); } + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + DebugOnly<nsresult> rv = + loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + MOZ_ASSERT(cookieJarSettings); + + uint64_t topWindowContextIdFromCJS = + net::CookieJarSettings::Cast(cookieJarSettings) + ->GetTopLevelWindowContextId(); + + // The top-level window could be navigated away when we get the fingerprinting + // override here. For example, the beacon requests. In this case, the + // top-level windowContext id won't match the inner window id of the top-level + // windowGlobalParent. So, we cannot rely on the URI from the top-level + // windowGlobalParent because it could be different from the one that creates + // the channel. Instead, we fallback to use the partitionKey in the + // cookieJarSettings to get the top-level URI. + if (topWGP->InnerWindowId() != topWindowContextIdFromCJS) { + nsAutoString partitionKey; + rv = cookieJarSettings->GetPartitionKey(partitionKey); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // Bail out early if the partitionKey is empty. + if (partitionKey.IsEmpty()) { + return Nothing(); + } + + nsAutoString scheme; + nsAutoString domain; + int32_t unused; + bool unused2; + if (!OriginAttributes::ParsePartitionKey(partitionKey, scheme, domain, + unused, unused2)) { + MOZ_ASSERT(false); + return Nothing(); + } + + nsCOMPtr<nsIURI> topURI; + rv = NS_NewURI(getter_AddRefs(topURI), scheme + u"://"_ns + domain); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + return GetOverriddenFingerprintingSettingsForURI(topURI, uri); + } + nsCOMPtr<nsIPrincipal> topPrincipal = topWGP->DocumentPrincipal(); if (NS_WARN_IF(!topPrincipal)) { return Nothing(); @@ -2049,19 +2144,20 @@ Maybe<RFPTarget> nsRFPService::GetOverriddenFingerprintingSettingsForChannel( #ifdef DEBUG // Verify if the top URI matches the partitionKey of the channel. - nsCOMPtr<nsICookieJarSettings> cookieJarSettings; - Unused << loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)); - nsAutoString partitionKey; cookieJarSettings->GetPartitionKey(partitionKey); OriginAttributes attrs; - attrs.SetPartitionKey(topURI); + attrs.SetPartitionKey(topURI, false); + + OriginAttributes attrsForeignByAncestor; + attrsForeignByAncestor.SetPartitionKey(topURI, true); // The partitionKey of the channel could haven't been set here if the loading // channel is top-level. MOZ_ASSERT_IF(!partitionKey.IsEmpty(), - attrs.mPartitionKey.Equals(partitionKey)); + attrs.mPartitionKey.Equals(partitionKey) || + attrsForeignByAncestor.mPartitionKey.Equals(partitionKey)); #endif return GetOverriddenFingerprintingSettingsForURI(topURI, uri); diff --git a/toolkit/components/resistfingerprinting/nsRFPService.h b/toolkit/components/resistfingerprinting/nsRFPService.h index 9e0ae9b6af..d45f4172c9 100644 --- a/toolkit/components/resistfingerprinting/nsRFPService.h +++ b/toolkit/components/resistfingerprinting/nsRFPService.h @@ -1,7 +1,7 @@ /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ #ifndef __nsRFPService_h__ #define __nsRFPService_h__ diff --git a/toolkit/components/resistfingerprinting/nsUserCharacteristics.cpp b/toolkit/components/resistfingerprinting/nsUserCharacteristics.cpp index 9bce616f81..86de7d61a7 100644 --- a/toolkit/components/resistfingerprinting/nsUserCharacteristics.cpp +++ b/toolkit/components/resistfingerprinting/nsUserCharacteristics.cpp @@ -1,25 +1,34 @@ /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ #include "nsUserCharacteristics.h" #include "nsID.h" #include "nsIUUIDGenerator.h" +#include "nsIUserCharacteristicsPageService.h" #include "nsServiceManagerUtils.h" #include "mozilla/Logging.h" #include "mozilla/glean/GleanPings.h" #include "mozilla/glean/GleanMetrics.h" +#include "jsapi.h" +#include "mozilla/Components.h" +#include "mozilla/dom/Promise-inl.h" + +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPrefs_general.h" #include "mozilla/StaticPrefs_media.h" +#include "mozilla/StaticPrefs_widget.h" #include "mozilla/LookAndFeel.h" #include "mozilla/PreferenceSheet.h" #include "mozilla/RelativeLuminanceUtils.h" #include "mozilla/ServoStyleConsts.h" #include "mozilla/dom/ScreenBinding.h" +#include "mozilla/intl/OSPreferences.h" #include "mozilla/intl/TimeZone.h" #include "mozilla/widget/ScreenManager.h" @@ -33,7 +42,9 @@ # include "nsMacUtilsImpl.h" #endif -static mozilla::LazyLogModule gUserCharacteristicsLog("UserCharacteristics"); +using namespace mozilla; + +static LazyLogModule gUserCharacteristicsLog("UserCharacteristics"); // ================================================================== namespace testing { @@ -41,9 +52,9 @@ extern "C" { int MaxTouchPoints() { #if defined(XP_WIN) - return mozilla::widget::WinUtils::GetMaxTouchPoints(); + return widget::WinUtils::GetMaxTouchPoints(); #elif defined(MOZ_WIDGET_ANDROID) - return mozilla::java::GeckoAppShell::GetMaxTouchPoints(); + return java::GeckoAppShell::GetMaxTouchPoints(); #else return 0; #endif @@ -53,84 +64,113 @@ int MaxTouchPoints() { }; // namespace testing // ================================================================== +// ================================================================== +already_AddRefed<mozilla::dom::Promise> ContentPageStuff() { + nsCOMPtr<nsIUserCharacteristicsPageService> ucp = + do_GetService("@mozilla.org/user-characteristics-page;1"); + MOZ_ASSERT(ucp); + + RefPtr<mozilla::dom::Promise> promise; + nsresult rv = ucp->CreateContentPage(getter_AddRefs(promise)); + if (NS_FAILED(rv)) { + MOZ_LOG(gUserCharacteristicsLog, mozilla::LogLevel::Error, + ("Could not create Content Page")); + return nullptr; + } + MOZ_LOG(gUserCharacteristicsLog, mozilla::LogLevel::Debug, + ("Created Content Page")); + + return promise.forget(); +} + void PopulateCSSProperties() { - mozilla::glean::characteristics::video_dynamic_range.Set( - mozilla::LookAndFeel::GetInt( - mozilla::LookAndFeel::IntID::VideoDynamicRange)); - mozilla::glean::characteristics::prefers_reduced_transparency.Set( - mozilla::LookAndFeel::GetInt( - mozilla::LookAndFeel::IntID::PrefersReducedTransparency)); - mozilla::glean::characteristics::prefers_reduced_motion.Set( - mozilla::LookAndFeel::GetInt( - mozilla::LookAndFeel::IntID::PrefersReducedMotion)); - mozilla::glean::characteristics::inverted_colors.Set( - mozilla::LookAndFeel::GetInt( - mozilla::LookAndFeel::IntID::InvertedColors)); - mozilla::glean::characteristics::color_scheme.Set( - (int)mozilla::PreferenceSheet::ContentPrefs().mColorScheme); - - mozilla::StylePrefersContrast prefersContrast = [] { + glean::characteristics::prefers_reduced_transparency.Set( + LookAndFeel::GetInt(LookAndFeel::IntID::PrefersReducedTransparency)); + glean::characteristics::prefers_reduced_motion.Set( + LookAndFeel::GetInt(LookAndFeel::IntID::PrefersReducedMotion)); + glean::characteristics::inverted_colors.Set( + LookAndFeel::GetInt(LookAndFeel::IntID::InvertedColors)); + glean::characteristics::color_scheme.Set( + (int)PreferenceSheet::ContentPrefs().mColorScheme); + + StylePrefersContrast prefersContrast = [] { // Replicates Gecko_MediaFeatures_PrefersContrast but without a Document - if (!mozilla::PreferenceSheet::ContentPrefs().mUseAccessibilityTheme && - mozilla::PreferenceSheet::ContentPrefs().mUseDocumentColors) { - return mozilla::StylePrefersContrast::NoPreference; + if (!PreferenceSheet::ContentPrefs().mUseAccessibilityTheme && + PreferenceSheet::ContentPrefs().mUseDocumentColors) { + return StylePrefersContrast::NoPreference; } - const auto& colors = mozilla::PreferenceSheet::ContentPrefs().ColorsFor( - mozilla::ColorScheme::Light); - float ratio = mozilla::RelativeLuminanceUtils::ContrastRatio( + const auto& colors = + PreferenceSheet::ContentPrefs().ColorsFor(ColorScheme::Light); + float ratio = RelativeLuminanceUtils::ContrastRatio( colors.mDefaultBackground, colors.mDefault); // https://www.w3.org/TR/WCAG21/#contrast-minimum if (ratio < 4.5f) { - return mozilla::StylePrefersContrast::Less; + return StylePrefersContrast::Less; } // https://www.w3.org/TR/WCAG21/#contrast-enhanced if (ratio >= 7.0f) { - return mozilla::StylePrefersContrast::More; + return StylePrefersContrast::More; } - return mozilla::StylePrefersContrast::Custom; + return StylePrefersContrast::Custom; }(); - mozilla::glean::characteristics::prefers_contrast.Set((int)prefersContrast); + glean::characteristics::prefers_contrast.Set((int)prefersContrast); } void PopulateScreenProperties() { - auto& screenManager = mozilla::widget::ScreenManager::GetSingleton(); - RefPtr<mozilla::widget::Screen> screen = screenManager.GetPrimaryScreen(); + auto& screenManager = widget::ScreenManager::GetSingleton(); + RefPtr<widget::Screen> screen = screenManager.GetPrimaryScreen(); MOZ_ASSERT(screen); - mozilla::dom::ScreenColorGamut colorGamut; + dom::ScreenColorGamut colorGamut; screen->GetColorGamut(&colorGamut); - mozilla::glean::characteristics::color_gamut.Set((int)colorGamut); + glean::characteristics::color_gamut.Set((int)colorGamut); int32_t colorDepth; screen->GetColorDepth(&colorDepth); - mozilla::glean::characteristics::color_depth.Set(colorDepth); + glean::characteristics::color_depth.Set(colorDepth); + + glean::characteristics::color_gamut.Set((int)colorGamut); + glean::characteristics::color_depth.Set(colorDepth); + const LayoutDeviceIntRect rect = screen->GetRect(); + glean::characteristics::screen_height.Set(rect.Height()); + glean::characteristics::screen_width.Set(rect.Width()); - mozilla::glean::characteristics::color_gamut.Set((int)colorGamut); - mozilla::glean::characteristics::color_depth.Set(colorDepth); - const mozilla::LayoutDeviceIntRect rect = screen->GetRect(); - mozilla::glean::characteristics::screen_height.Set(rect.Height()); - mozilla::glean::characteristics::screen_width.Set(rect.Width()); + glean::characteristics::video_dynamic_range.Set(screen->GetIsHDR()); } void PopulateMissingFonts() { nsCString aMissingFonts; gfxPlatformFontList::PlatformFontList()->GetMissingFonts(aMissingFonts); - mozilla::glean::characteristics::missing_fonts.Set(aMissingFonts); + glean::characteristics::missing_fonts.Set(aMissingFonts); } void PopulatePrefs() { nsAutoCString acceptLang; - mozilla::Preferences::GetLocalizedCString("intl.accept_languages", - acceptLang); - mozilla::glean::characteristics::prefs_intl_accept_languages.Set(acceptLang); + Preferences::GetLocalizedCString("intl.accept_languages", acceptLang); + glean::characteristics::prefs_intl_accept_languages.Set(acceptLang); + + glean::characteristics::prefs_media_eme_enabled.Set( + StaticPrefs::media_eme_enabled()); + + glean::characteristics::prefs_zoom_text_only.Set( + !Preferences::GetBool("browser.zoom.full")); + + glean::characteristics::prefs_privacy_donottrackheader_enabled.Set( + StaticPrefs::privacy_donottrackheader_enabled()); + glean::characteristics::prefs_privacy_globalprivacycontrol_enabled.Set( + StaticPrefs::privacy_globalprivacycontrol_enabled()); - mozilla::glean::characteristics::prefs_media_eme_enabled.Set( - mozilla::StaticPrefs::media_eme_enabled()); + glean::characteristics::prefs_general_autoscroll.Set( + Preferences::GetBool("general.autoScroll")); + glean::characteristics::prefs_general_smoothscroll.Set( + StaticPrefs::general_smoothScroll()); + glean::characteristics::prefs_overlay_scrollbars.Set( + StaticPrefs::widget_gtk_overlay_scrollbars_enabled()); - mozilla::glean::characteristics::prefs_zoom_text_only.Set( - !mozilla::Preferences::GetBool("browser.zoom.full")); + glean::characteristics::prefs_block_popups.Set( + StaticPrefs::dom_disable_open_during_load()); } // ================================================================== @@ -138,12 +178,16 @@ void PopulatePrefs() { // metric is set, this variable should be incremented. It'll be a lot. It's // okay. We're going to need it to know (including during development) what is // the source of the data we are looking at. -const int kSubmissionSchema = 0; +const int kSubmissionSchema = 1; + +const auto* const kLastVersionPref = + "toolkit.telemetry.user_characteristics_ping.last_version_sent"; +const auto* const kCurrentVersionPref = + "toolkit.telemetry.user_characteristics_ping.current_version"; /* static */ void nsUserCharacteristics::MaybeSubmitPing() { - MOZ_LOG(gUserCharacteristicsLog, mozilla::LogLevel::Debug, - ("In MaybeSubmitPing()")); + MOZ_LOG(gUserCharacteristicsLog, LogLevel::Debug, ("In MaybeSubmitPing()")); MOZ_ASSERT(XRE_IsParentProcess()); /** @@ -161,14 +205,8 @@ void nsUserCharacteristics::MaybeSubmitPing() { * Sent = Current Version. * */ - const auto* const kLastVersionPref = - "toolkit.telemetry.user_characteristics_ping.last_version_sent"; - const auto* const kCurrentVersionPref = - "toolkit.telemetry.user_characteristics_ping.current_version"; - - auto lastSubmissionVersion = - mozilla::Preferences::GetInt(kLastVersionPref, 0); - auto currentVersion = mozilla::Preferences::GetInt(kCurrentVersionPref, 0); + auto lastSubmissionVersion = Preferences::GetInt(kLastVersionPref, 0); + auto currentVersion = Preferences::GetInt(kCurrentVersionPref, 0); MOZ_ASSERT(currentVersion == -1 || lastSubmissionVersion <= currentVersion, "lastSubmissionVersion is somehow greater than currentVersion " @@ -176,46 +214,40 @@ void nsUserCharacteristics::MaybeSubmitPing() { if (lastSubmissionVersion < 0) { // This is a way for users to opt out of this ping specifically. - MOZ_LOG(gUserCharacteristicsLog, mozilla::LogLevel::Debug, + MOZ_LOG(gUserCharacteristicsLog, LogLevel::Debug, ("Returning, User Opt-out")); return; } if (currentVersion == 0) { // Do nothing. We do not want any pings. - MOZ_LOG(gUserCharacteristicsLog, mozilla::LogLevel::Debug, + MOZ_LOG(gUserCharacteristicsLog, LogLevel::Debug, ("Returning, currentVersion == 0")); return; } if (currentVersion == -1) { // currentVersion = -1 is a development value to force a ping submission - MOZ_LOG(gUserCharacteristicsLog, mozilla::LogLevel::Debug, + MOZ_LOG(gUserCharacteristicsLog, LogLevel::Debug, ("Force-Submitting Ping")); - if (NS_SUCCEEDED(PopulateData())) { - SubmitPing(); - } + PopulateDataAndEventuallySubmit(false); return; } if (lastSubmissionVersion > currentVersion) { // This is an unexpected scneario that indicates something is wrong. We // asserted against it (in debug, above) We will try to sanity-correct // ourselves by setting it to the current version. - mozilla::Preferences::SetInt(kLastVersionPref, currentVersion); - MOZ_LOG(gUserCharacteristicsLog, mozilla::LogLevel::Warning, + Preferences::SetInt(kLastVersionPref, currentVersion); + MOZ_LOG(gUserCharacteristicsLog, LogLevel::Warning, ("Returning, lastSubmissionVersion > currentVersion")); return; } if (lastSubmissionVersion == currentVersion) { // We are okay, we've already submitted the most recent ping - MOZ_LOG(gUserCharacteristicsLog, mozilla::LogLevel::Warning, + MOZ_LOG(gUserCharacteristicsLog, LogLevel::Warning, ("Returning, lastSubmissionVersion == currentVersion")); return; } if (lastSubmissionVersion < currentVersion) { - if (NS_SUCCEEDED(PopulateData())) { - if (NS_SUCCEEDED(SubmitPing())) { - mozilla::Preferences::SetInt(kLastVersionPref, currentVersion); - } - } + PopulateDataAndEventuallySubmit(false); } else { MOZ_ASSERT_UNREACHABLE("Should never reach here"); } @@ -225,73 +257,128 @@ const auto* const kUUIDPref = "toolkit.telemetry.user_characteristics_ping.uuid"; /* static */ -nsresult nsUserCharacteristics::PopulateData(bool aTesting /* = false */) { - MOZ_LOG(gUserCharacteristicsLog, mozilla::LogLevel::Warning, - ("Populating Data")); +void nsUserCharacteristics::PopulateDataAndEventuallySubmit( + bool aUpdatePref /* = true */, bool aTesting /* = false */ +) { + MOZ_LOG(gUserCharacteristicsLog, LogLevel::Warning, ("Populating Data")); MOZ_ASSERT(XRE_IsParentProcess()); - mozilla::glean::characteristics::submission_schema.Set(kSubmissionSchema); + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (!obs) { + return; + } + + // This notification tells us to register the actor + obs->NotifyObservers(nullptr, "user-characteristics-populating-data", + nullptr); + + glean::characteristics::submission_schema.Set(kSubmissionSchema); nsAutoCString uuidString; - nsresult rv = mozilla::Preferences::GetCString(kUUIDPref, uuidString); + nsresult rv = Preferences::GetCString(kUUIDPref, uuidString); if (NS_FAILED(rv) || uuidString.Length() == 0) { nsCOMPtr<nsIUUIDGenerator> uuidgen = do_GetService("@mozilla.org/uuid-generator;1", &rv); - NS_ENSURE_SUCCESS(rv, rv); + if (NS_FAILED(rv)) { + return; + } nsIDToCString id(nsID::GenerateUUID()); uuidString = id.get(); - mozilla::Preferences::SetCString(kUUIDPref, uuidString); + Preferences::SetCString(kUUIDPref, uuidString); } - mozilla::glean::characteristics::client_identifier.Set(uuidString); - mozilla::glean::characteristics::max_touch_points.Set( - testing::MaxTouchPoints()); + glean::characteristics::client_identifier.Set(uuidString); + + glean::characteristics::max_touch_points.Set(testing::MaxTouchPoints()); + + // ------------------------------------------------------------------------ - if (aTesting) { + if (!aTesting) { // Many of the later peices of data do not work in a gtest - // so just populate something, and return - return NS_OK; - } + // so skip populating them + + // ------------------------------------------------------------------------ - PopulateMissingFonts(); - PopulateCSSProperties(); - PopulateScreenProperties(); - PopulatePrefs(); + PopulateMissingFonts(); + PopulateCSSProperties(); + PopulateScreenProperties(); + PopulatePrefs(); - mozilla::glean::characteristics::target_frame_rate.Set( - gfxPlatform::TargetFrameRate()); + glean::characteristics::target_frame_rate.Set( + gfxPlatform::TargetFrameRate()); - int32_t processorCount = 0; + int32_t processorCount = 0; #if defined(XP_MACOSX) - if (nsMacUtilsImpl::IsTCSMAvailable()) { - // On failure, zero is returned from GetPhysicalCPUCount() - // and we fallback to PR_GetNumberOfProcessors below. - processorCount = nsMacUtilsImpl::GetPhysicalCPUCount(); - } + if (nsMacUtilsImpl::IsTCSMAvailable()) { + // On failure, zero is returned from GetPhysicalCPUCount() + // and we fallback to PR_GetNumberOfProcessors below. + processorCount = nsMacUtilsImpl::GetPhysicalCPUCount(); + } #endif - if (processorCount == 0) { - processorCount = PR_GetNumberOfProcessors(); + if (processorCount == 0) { + processorCount = PR_GetNumberOfProcessors(); + } + glean::characteristics::processor_count.Set(processorCount); + + AutoTArray<char16_t, 128> tzBuffer; + auto result = intl::TimeZone::GetDefaultTimeZone(tzBuffer); + if (result.isOk()) { + NS_ConvertUTF16toUTF8 timeZone( + nsDependentString(tzBuffer.Elements(), tzBuffer.Length())); + glean::characteristics::timezone.Set(timeZone); + } else { + glean::characteristics::timezone.Set("<error>"_ns); + } + + nsAutoCString locale; + intl::OSPreferences::GetInstance()->GetSystemLocale(locale); + glean::characteristics::system_locale.Set(locale); } - mozilla::glean::characteristics::processor_count.Set(processorCount); - - AutoTArray<char16_t, 128> tzBuffer; - auto result = mozilla::intl::TimeZone::GetDefaultTimeZone(tzBuffer); - if (result.isOk()) { - NS_ConvertUTF16toUTF8 timeZone( - nsDependentString(tzBuffer.Elements(), tzBuffer.Length())); - mozilla::glean::characteristics::timezone.Set(timeZone); + + // When this promise resolves, everything succeeded and we can submit. + RefPtr<mozilla::dom::Promise> promise = ContentPageStuff(); + + // ------------------------------------------------------------------------ + + auto fulfillSteps = [aUpdatePref, aTesting]( + JSContext* aCx, JS::Handle<JS::Value> aPromiseResult, + mozilla::ErrorResult& aRv) { + MOZ_LOG(gUserCharacteristicsLog, mozilla::LogLevel::Debug, + ("ContentPageStuff Promise Resolved")); + + if (!aTesting) { + nsUserCharacteristics::SubmitPing(); + } + + if (aUpdatePref) { + MOZ_LOG(gUserCharacteristicsLog, mozilla::LogLevel::Debug, + ("Updating preference")); + auto current_version = + mozilla::Preferences::GetInt(kCurrentVersionPref, 0); + mozilla::Preferences::SetInt(kLastVersionPref, current_version); + } + }; + + // Something failed in the Content Page... + auto rejectSteps = [](JSContext* aCx, JS::Handle<JS::Value> aReason, + mozilla::ErrorResult& aRv) { + MOZ_LOG(gUserCharacteristicsLog, mozilla::LogLevel::Error, + ("ContentPageStuff Promise Rejected")); + }; + + if (promise) { + promise->AddCallbacksWithCycleCollectedArgs(std::move(fulfillSteps), + std::move(rejectSteps)); } else { - mozilla::glean::characteristics::timezone.Set("<error>"_ns); + MOZ_LOG(gUserCharacteristicsLog, mozilla::LogLevel::Error, + ("Did not get a Promise back from ContentPageStuff")); } - - return NS_OK; } /* static */ -nsresult nsUserCharacteristics::SubmitPing() { +void nsUserCharacteristics::SubmitPing() { MOZ_LOG(gUserCharacteristicsLog, mozilla::LogLevel::Warning, ("Submitting Ping")); - mozilla::glean_pings::UserCharacteristics.Submit(); - - return NS_OK; + glean_pings::UserCharacteristics.Submit(); } diff --git a/toolkit/components/resistfingerprinting/nsUserCharacteristics.h b/toolkit/components/resistfingerprinting/nsUserCharacteristics.h index a52bc9aea7..7d78dcd965 100644 --- a/toolkit/components/resistfingerprinting/nsUserCharacteristics.h +++ b/toolkit/components/resistfingerprinting/nsUserCharacteristics.h @@ -1,7 +1,7 @@ /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ #ifndef __nsUserCharacteristics_h__ #define __nsUserCharacteristics_h__ @@ -12,9 +12,15 @@ class nsUserCharacteristics { public: static void MaybeSubmitPing(); - // Public For testing - static nsresult PopulateData(bool aTesting = false); - static nsresult SubmitPing(); + /* + * These APIs are public only for testing using the gtest + * When PopulateDataAndEventuallySubmit is called with aTesting = true + * it will not submit the data, and SubmitPing must be called explicitly. + * This is perfect because that's what we want for the gtest. + */ + static void PopulateDataAndEventuallySubmit(bool aUpdatePref = true, + bool aTesting = false); + static void SubmitPing(); }; namespace testing { diff --git a/toolkit/components/resistfingerprinting/pings.yaml b/toolkit/components/resistfingerprinting/pings.yaml index 46a4b2da19..ddbd48d1d1 100644 --- a/toolkit/components/resistfingerprinting/pings.yaml +++ b/toolkit/components/resistfingerprinting/pings.yaml @@ -1,6 +1,6 @@ # 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/. +# file, You can obtain one at https://mozilla.org/MPL/2.0/. --- $schema: moz://mozilla.org/schemas/glean/pings/2-0-0 diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser.toml b/toolkit/components/resistfingerprinting/tests/browser/browser.toml index 213d6d4287..527e39ad89 100644 --- a/toolkit/components/resistfingerprinting/tests/browser/browser.toml +++ b/toolkit/components/resistfingerprinting/tests/browser/browser.toml @@ -31,3 +31,5 @@ support-files = [ support-files = ["file_pdf.pdf"] ["browser_serviceWorker_fingerprinting_webcompat.js"] + +["browser_usercharacteristics.js"] diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_fingerprinter_telemetry.js b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_fingerprinter_telemetry.js index d2a33a4347..1098d91138 100644 --- a/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_fingerprinter_telemetry.js +++ b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_fingerprinter_telemetry.js @@ -1,7 +1,7 @@ /* vim: set ts=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/. */ + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ "use strict"; diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js index 8b4be78d53..e526f7fff1 100644 --- a/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js +++ b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization.js @@ -1,6 +1,6 @@ /* 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/. */ + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /** * Bug 1816189 - Testing canvas randomization on canvas data extraction. diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization_worker.js b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization_worker.js index dd7c292fa4..723d7382f8 100644 --- a/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization_worker.js +++ b/toolkit/components/resistfingerprinting/tests/browser/browser_canvas_randomization_worker.js @@ -1,6 +1,6 @@ /* 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/. */ + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ const emptyPage = getRootDirectory(gTestPath).replace( diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingRemoteOverrides.js b/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingRemoteOverrides.js index 29c7c9170b..ac4722fd84 100644 --- a/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingRemoteOverrides.js +++ b/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingRemoteOverrides.js @@ -1,6 +1,6 @@ /* 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/. */ + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ "use strict"; @@ -20,6 +20,18 @@ const TARGET_CanvasRandomization = 0x000000100; const TARGET_WindowOuterSize = 0x002000000; const TARGET_Gamepad = 0x00800000; +const TEST_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "empty.html"; + +const TEST_ANOTHER_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.net" + ) + "empty.html"; + // A helper function to filter high 32 bits. function extractLow32Bits(value) { return value & 0xffffffff; @@ -404,3 +416,49 @@ add_task(async function test_pref_override_remote_settings() { db.clear(); }); + +// Bug 1873682 - Verify that a third-party beacon request won't hit the +// assertion in nsRFPService::GetOverriddenFingerprintingSettingsForChannel(). +add_task(async function test_beacon_request() { + // Open an empty page. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE); + + await SpecialPowers.spawn( + tab.linkedBrowser, + [TEST_ANOTHER_PAGE], + async url => { + // Create a third-party iframe + let ifr = content.document.createElement("iframe"); + + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + ifr.src = url; + }); + + await SpecialPowers.spawn(ifr, [url], url => { + // Sending the beacon request right before the tab navigates away. + content.addEventListener("unload", _ => { + let value = ["text"]; + let blob = new Blob(value, { + type: "application/x-www-form-urlencoded", + }); + content.navigator.sendBeacon(url, blob); + }); + }); + + // Navigate the tab to another page. + content.location = url; + } + ); + + await BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + TEST_ANOTHER_PAGE + ); + + ok(true, "Successfully navigates away."); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingWebCompat.js b/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingWebCompat.js index 1a882fc63d..62335b2fe7 100644 --- a/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingWebCompat.js +++ b/toolkit/components/resistfingerprinting/tests/browser/browser_fingerprintingWebCompat.js @@ -1,6 +1,6 @@ /* 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/. */ + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ "use strict"; diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_font_fingerprinter_telemetry.js b/toolkit/components/resistfingerprinting/tests/browser/browser_font_fingerprinter_telemetry.js index 2231197eed..0cccf0c5f3 100644 --- a/toolkit/components/resistfingerprinting/tests/browser/browser_font_fingerprinter_telemetry.js +++ b/toolkit/components/resistfingerprinting/tests/browser/browser_font_fingerprinter_telemetry.js @@ -1,7 +1,7 @@ /* vim: set ts=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/. */ + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ "use strict"; diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_fpiServiceWorkers_fingerprinting.js b/toolkit/components/resistfingerprinting/tests/browser/browser_fpiServiceWorkers_fingerprinting.js index 64279ae442..aaa40a7928 100644 --- a/toolkit/components/resistfingerprinting/tests/browser/browser_fpiServiceWorkers_fingerprinting.js +++ b/toolkit/components/resistfingerprinting/tests/browser/browser_fpiServiceWorkers_fingerprinting.js @@ -68,7 +68,7 @@ runTestInFirstAndThirdPartyContexts( }, async _ => { await new Promise(resolve => { - Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, () => resolve() ); }); diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_serviceWorker_fingerprinting_webcompat.js b/toolkit/components/resistfingerprinting/tests/browser/browser_serviceWorker_fingerprinting_webcompat.js index a9bab38e61..eb1ab8d795 100644 --- a/toolkit/components/resistfingerprinting/tests/browser/browser_serviceWorker_fingerprinting_webcompat.js +++ b/toolkit/components/resistfingerprinting/tests/browser/browser_serviceWorker_fingerprinting_webcompat.js @@ -59,7 +59,7 @@ runTestInFirstAndThirdPartyContexts( async _ => { await new Promise(resolve => { - Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, () => resolve() ); }); @@ -142,7 +142,7 @@ runTestInFirstAndThirdPartyContexts( async _ => { await new Promise(resolve => { - Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, () => resolve() ); }); diff --git a/toolkit/components/resistfingerprinting/tests/browser/browser_usercharacteristics.js b/toolkit/components/resistfingerprinting/tests/browser/browser_usercharacteristics.js new file mode 100644 index 0000000000..9896948311 --- /dev/null +++ b/toolkit/components/resistfingerprinting/tests/browser/browser_usercharacteristics.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const emptyPage = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "empty.html"; + +function promiseObserverNotification() { + return TestUtils.topicObserved( + "user-characteristics-populating-data-done", + _ => { + var submitted = false; + GleanPings.userCharacteristics.testBeforeNextSubmit(_ => { + submitted = true; + + // Did we assign a value we got out of about:fingerprinting? + // For now, we are sticking the test value in a random telemetry + // metric, but once we have a real metric, we'll update this + Assert.equal( + "Hello World", + Glean.characteristics.timezone.testGetValue() + ); + }); + GleanPings.userCharacteristics.submit(); + + return submitted; + } + ); +} + +add_task(async function run_test() { + info("Starting test..."); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: emptyPage }, + async function tabTask(_) { + let promise = promiseObserverNotification(); + + Services.obs.notifyObservers( + null, + "user-characteristics-testing-please-populate-data" + ); + + let submitted = await promise; + Assert.ok(submitted); + } + ); +}); diff --git a/toolkit/components/resistfingerprinting/tests/browser/head.js b/toolkit/components/resistfingerprinting/tests/browser/head.js index 9d8ec19956..e2fddaecc6 100644 --- a/toolkit/components/resistfingerprinting/tests/browser/head.js +++ b/toolkit/components/resistfingerprinting/tests/browser/head.js @@ -54,7 +54,7 @@ function countDifferencesInArrayBuffers(buffer1, buffer2) { function promiseObserver(topic) { return new Promise(resolve => { - let obs = (aSubject, aTopic, aData) => { + let obs = (aSubject, aTopic) => { Services.obs.removeObserver(obs, aTopic); resolve(aSubject); }; diff --git a/toolkit/components/resistfingerprinting/tests/gtest/test_reduceprecision.cpp b/toolkit/components/resistfingerprinting/tests/gtest/test_reduceprecision.cpp index 5d8b598ff0..a36b819d8f 100644 --- a/toolkit/components/resistfingerprinting/tests/gtest/test_reduceprecision.cpp +++ b/toolkit/components/resistfingerprinting/tests/gtest/test_reduceprecision.cpp @@ -2,7 +2,7 @@ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : * 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/. */ + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ #include <math.h> diff --git a/toolkit/components/resistfingerprinting/tests/gtest/test_usercharping.cpp b/toolkit/components/resistfingerprinting/tests/gtest/test_usercharping.cpp index dd1cbe7d46..2eefa8e7fe 100644 --- a/toolkit/components/resistfingerprinting/tests/gtest/test_usercharping.cpp +++ b/toolkit/components/resistfingerprinting/tests/gtest/test_usercharping.cpp @@ -2,7 +2,7 @@ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : * 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/. */ + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ #include "gtest/gtest.h" #include "mozilla/gtest/nsUserCharacteristics.h" @@ -35,7 +35,8 @@ TEST(ResistFingerprinting, UserCharacteristics_Simple) TEST(ResistFingerprinting, UserCharacteristics_Complex) { - nsUserCharacteristics::PopulateData(true); + nsUserCharacteristics::PopulateDataAndEventuallySubmit( + /* aUpdatePref = */ false, /* aTesting = */ true); bool submitted = false; mozilla::glean_pings::UserCharacteristics.TestBeforeNextSubmit( @@ -102,7 +103,8 @@ TEST(ResistFingerprinting, UserCharacteristics_ClearPref) .value() .get()); }); - nsUserCharacteristics::PopulateData(true); + nsUserCharacteristics::PopulateDataAndEventuallySubmit( + /* aUpdatePref = */ false, /* aTesting = */ true); nsUserCharacteristics::SubmitPing(); auto original_value = @@ -135,7 +137,8 @@ TEST(ResistFingerprinting, UserCharacteristics_ClearPref) Preferences::GetCString(kUUIDPref, uuidValue); ASSERT_STRNE("", uuidValue.get()); }); - nsUserCharacteristics::PopulateData(true); + nsUserCharacteristics::PopulateDataAndEventuallySubmit( + /* aUpdatePref = */ false, /* aTesting = */ true); nsUserCharacteristics::SubmitPing(); Preferences::SetBool("datareporting.healthreport.uploadEnabled", diff --git a/toolkit/components/satchel/FillHelpers.sys.mjs b/toolkit/components/satchel/FillHelpers.sys.mjs index 88a248adba..fd335f271e 100644 --- a/toolkit/components/satchel/FillHelpers.sys.mjs +++ b/toolkit/components/satchel/FillHelpers.sys.mjs @@ -39,3 +39,51 @@ export function showConfirmation( const anchor = browser.ownerDocument.getElementById(anchorId); anchor.ownerGlobal.ConfirmationHint.show(anchor, messageId, {}); } + +let fillRequestId = 0; + +/** + * Send a message encoded in the comment from an autocomplete item + * to the parent. + * + * @param {string} actorName name of the actor to send to + * @param {object} autocompleteInput current nsIAutoCompleteInput + * @param {string} comment serialized JSON comment containing fillMessageName and + * fillMessageData to send to the actor + */ +export async function sendFillRequestToParent( + actorName, + autocompleteInput, + comment +) { + if (!comment) { + return; + } + + const { fillMessageName, fillMessageData } = JSON.parse(comment); + if (!fillMessageName) { + return; + } + + fillRequestId++; + const currentFillRequestId = fillRequestId; + const actor = + autocompleteInput.focusedInput.ownerGlobal?.windowGlobalChild?.getActor( + actorName + ); + const value = await actor.sendQuery(fillMessageName, fillMessageData ?? {}); + + // skip fill if another fill operation started during await + if (currentFillRequestId != fillRequestId) { + return; + } + + if (typeof value !== "string") { + return; + } + + // If the parent returned a string to fill, we must do it here because + // nsAutoCompleteController.cpp already finished it's work before we finished await. + autocompleteInput.textValue = value; + autocompleteInput.selectTextRange(value.length, value.length); +} diff --git a/toolkit/components/satchel/FormHandlerChild.sys.mjs b/toolkit/components/satchel/FormHandlerChild.sys.mjs index 6b1af3dbc3..526066e46e 100644 --- a/toolkit/components/satchel/FormHandlerChild.sys.mjs +++ b/toolkit/components/satchel/FormHandlerChild.sys.mjs @@ -44,13 +44,13 @@ export class FormHandlerChild extends JSWindowActorChild { } // handle form-removal-after-fetch - processFormRemovalAfterFetch(params) {} + processFormRemovalAfterFetch(_params) {} // handle iframe-pagehide - processIframePagehide(params) {} + processIframePagehide(_params) {} // handle page-navigation - processPageNavigation(params) {} + processPageNavigation(_params) {} /** * Dispatch the CustomEvent form-submission-detected also transfer diff --git a/toolkit/components/satchel/FormAutoComplete.sys.mjs b/toolkit/components/satchel/FormHistoryAutoComplete.sys.mjs index 1cae8b07c1..2799d2e955 100644 --- a/toolkit/components/satchel/FormAutoComplete.sys.mjs +++ b/toolkit/components/satchel/FormHistoryAutoComplete.sys.mjs @@ -3,7 +3,10 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { GenericAutocompleteItem } from "resource://gre/modules/FillHelpers.sys.mjs"; +import { + GenericAutocompleteItem, + sendFillRequestToParent, +} from "resource://gre/modules/FillHelpers.sys.mjs"; const lazy = {}; @@ -33,10 +36,10 @@ function isAutocompleteDisabled(aField) { * figuring out the most appropriate message manager to use, * and what things to send. * - * It is assumed that nsFormAutoComplete will only ever use + * It is assumed that FormHistoryAutoComplete will only ever use * one instance at a time, and will not attempt to perform more * than one search request with the same instance at a time. - * However, nsFormAutoComplete might call remove() any number of + * However, FormHistoryAutoComplete might call remove() any number of * times with the same instance of the client. * * @param {object} clientInfo @@ -193,7 +196,7 @@ export class FormHistoryClient { * * @implements {nsIAutoCompleteResult} */ -export class FormAutoCompleteResult { +export class FormHistoryAutoCompleteResult { constructor(client, entries, fieldName, searchString) { this.client = client; this.entries = entries; @@ -395,7 +398,7 @@ export class FormAutoCompleteResult { } } -export class FormAutoComplete { +export class FormHistoryAutoComplete { constructor() { // Preferences. Add observer so we get notified of changes. this._prefBranch = Services.prefs.getBranch("browser.formfill."); @@ -407,9 +410,9 @@ export class FormAutoComplete { Services.obs.addObserver(this, "autocomplete-will-enter-text"); } - classID = Components.ID("{c11c21b2-71c9-4f87-a0f8-5e13f50495fd}"); + classID = Components.ID("{23530265-31d1-4ee9-864c-c081975fb7bc}"); QueryInterface = ChromeUtils.generateQI([ - "nsIFormAutoComplete", + "nsIFormHistoryAutoComplete", "nsISupportsWeakReference", ]); @@ -420,8 +423,6 @@ export class FormAutoComplete { // one is already pending, the existing one is cancelled. #pendingClient = null; - fillRequestId = 0; - observer = { _self: null, @@ -465,8 +466,7 @@ export class FormAutoComplete { if (!this._debug) { return; } - dump("FormAutoComplete: " + message + "\n"); - Services.console.logStringMessage("FormAutoComplete: " + message); + Services.console.logStringMessage("FormHistoryAutoComplete: " + message); } /* @@ -478,7 +478,7 @@ export class FormAutoComplete { * aField -- HTMLInputElement being autocompleted (may be null if from chrome) * aPreviousResult -- previous search result, if any. * aAddDataList -- add results from list=datalist for aField. - * aListener -- nsIFormAutoCompleteObserver that listens for the nsIAutoCompleteResult + * aListener -- nsIFormHistoryAutoCompleteObserver that listens for the nsIAutoCompleteResult * that may be returned asynchronously. */ autoCompleteSearchAsync( @@ -507,7 +507,7 @@ export class FormAutoComplete { } // If we have datalist results, they become our "empty" result. - const result = new FormAutoCompleteResult( + const result = new FormHistoryAutoCompleteResult( client, [], aInputName, @@ -650,44 +650,11 @@ export class FormAutoComplete { async observe(subject, topic, data) { switch (topic) { case "autocomplete-will-enter-text": { - await this.sendFillRequestToFormHistoryParent(subject, data); + if (subject && subject == formFillController.controller?.input) { + await sendFillRequestToParent("FormHistory", subject, data); + } break; } } } - - async sendFillRequestToFormHistoryParent(input, comment) { - if (!comment) { - return; - } - - if (!input || input != formFillController.controller?.input) { - return; - } - - const { fillMessageName, fillMessageData } = JSON.parse(comment ?? "{}"); - if (!fillMessageName) { - return; - } - - this.fillRequestId++; - const fillRequestId = this.fillRequestId; - const actor = - input.focusedInput.ownerGlobal.windowGlobalChild.getActor("FormHistory"); - const value = await actor.sendQuery(fillMessageName, fillMessageData ?? {}); - - // skip fill if another fill operation started during await - if (fillRequestId != this.fillRequestId) { - return; - } - - if (typeof value !== "string") { - return; - } - - // If FormHistoryParent returned a string to fill, we must do it here because - // nsAutoCompleteController.cpp already finished it's work before we finished await. - input.textValue = value; - input.selectTextRange(value.length, value.length); - } } diff --git a/toolkit/components/satchel/components.conf b/toolkit/components/satchel/components.conf index d843b869d6..d5a670efd9 100644 --- a/toolkit/components/satchel/components.conf +++ b/toolkit/components/satchel/components.conf @@ -18,10 +18,10 @@ Classes = [ }, { - 'cid': '{c11c21b2-71c9-4f87-a0f8-5e13f50495fd}', - 'contract_ids': ['@mozilla.org/satchel/form-autocomplete;1'], - 'esModule': 'resource://gre/modules/FormAutoComplete.sys.mjs', - 'constructor': 'FormAutoComplete', + 'cid': '{23530265-31d1-4ee9-864c-c081975fb7bc}', + 'contract_ids': ['@mozilla.org/satchel/form-history-autocomplete;1'], + 'esModule': 'resource://gre/modules/FormHistoryAutoComplete.sys.mjs', + 'constructor': 'FormHistoryAutoComplete', }, { 'cid': '{3a0012eb-007f-4bb8-aa81-a07385f77a25}', diff --git a/toolkit/components/satchel/jar.mn b/toolkit/components/satchel/jar.mn new file mode 100644 index 0000000000..a3f250f2e8 --- /dev/null +++ b/toolkit/components/satchel/jar.mn @@ -0,0 +1,10 @@ +# 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/. + +toolkit.jar: + content/global/megalist/megalist.css (megalist/content/megalist.css) + content/global/megalist/megalist.html (megalist/content/megalist.html) + content/global/megalist/MegalistView.mjs (megalist/content/MegalistView.mjs) + content/global/megalist/search-input.mjs (megalist/content/search-input.mjs) + content/global/megalist/VirtualizedList.mjs (megalist/content/VirtualizedList.mjs) diff --git a/toolkit/components/satchel/megalist/MegalistChild.sys.mjs b/toolkit/components/satchel/megalist/MegalistChild.sys.mjs new file mode 100644 index 0000000000..cd17798c95 --- /dev/null +++ b/toolkit/components/satchel/megalist/MegalistChild.sys.mjs @@ -0,0 +1,17 @@ +/* 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/. */ + +export class MegalistChild extends JSWindowActorChild { + receiveMessage(message) { + // Forward message to the View + const win = this.document.defaultView; + const ev = new win.CustomEvent("MessageFromViewModel", { + detail: message, + }); + win.dispatchEvent(ev); + } + + // Prevent TypeError: Property 'handleEvent' is not callable. + handleEvent() {} +} diff --git a/toolkit/components/satchel/megalist/MegalistParent.sys.mjs b/toolkit/components/satchel/megalist/MegalistParent.sys.mjs new file mode 100644 index 0000000000..de04af7ea6 --- /dev/null +++ b/toolkit/components/satchel/megalist/MegalistParent.sys.mjs @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { MegalistViewModel } from "resource://gre/modules/megalist/MegalistViewModel.sys.mjs"; + +/** + * MegalistParent integrates MegalistViewModel into Parent/Child model. + */ +export class MegalistParent extends JSWindowActorParent { + #viewModel; + + actorCreated() { + this.#viewModel = new MegalistViewModel((...args) => + this.sendAsyncMessage(...args) + ); + } + + didDestroy() { + this.#viewModel.willDestroy(); + this.#viewModel = null; + } + + receiveMessage(message) { + return this.#viewModel?.handleViewMessage(message); + } +} diff --git a/toolkit/components/satchel/megalist/MegalistViewModel.sys.mjs b/toolkit/components/satchel/megalist/MegalistViewModel.sys.mjs new file mode 100644 index 0000000000..f11a8a3198 --- /dev/null +++ b/toolkit/components/satchel/megalist/MegalistViewModel.sys.mjs @@ -0,0 +1,291 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { DefaultAggregator } from "resource://gre/modules/megalist/aggregator/DefaultAggregator.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs", +}); + +/** + * View Model for Megalist. + * + * Responsible for filtering, grouping, moving selection, editing. + * Refers to the same MegalistAggregator in the parent process to access data. + * Paired to exactly one MegalistView in the child process to present to the user. + * Receives user commands from MegalistView. + * + * There can be multiple snapshots of the same line displayed in different contexts. + * + * snapshotId - an id for a snapshot of a line used between View Model and View. + */ +export class MegalistViewModel { + /** + * + * View Model prepares snapshots in the parent process to be displayed + * by the View in the child process. View gets the firstSnapshotId + length of the + * list. Making it a very short message for each time we filter or refresh data. + * + * View requests line data by providing snapshotId = firstSnapshotId + index. + * + */ + #firstSnapshotId = 0; + #snapshots = []; + #selectedIndex = 0; + #searchText = ""; + #messageToView; + static #aggregator = new DefaultAggregator(); + + constructor(messageToView) { + this.#messageToView = messageToView; + MegalistViewModel.#aggregator.attachViewModel(this); + } + + willDestroy() { + MegalistViewModel.#aggregator.detachViewModel(this); + } + + refreshAllLinesOnScreen() { + this.#rebuildSnapshots(); + } + + refreshSingleLineOnScreen(line) { + if (this.#searchText) { + // Data is filtered, which may require rebuilding the whole list + //@sg check if current filter would affected by this line + //@sg throttle refresh operation + this.#rebuildSnapshots(); + } else { + const snapshotIndex = this.#snapshots.indexOf(line); + if (snapshotIndex >= 0) { + const snapshotId = snapshotIndex + this.#firstSnapshotId; + this.#sendSnapshotToView(snapshotId, line); + } + } + } + + /** + * + * Send snapshot of necessary line data across parent-child boundary. + * + * @param {number} snapshotId + * @param {object} snapshotData + */ + async #sendSnapshotToView(snapshotId, snapshotData) { + if (!snapshotData) { + return; + } + + // Only usable set of fields is sent over to the View. + // Line object may contain other data used by the Data Source. + const snapshot = { + label: snapshotData.label, + value: await snapshotData.value, + }; + if ("template" in snapshotData) { + snapshot.template = snapshotData.template; + } + if ("start" in snapshotData) { + snapshot.start = snapshotData.start; + } + if ("end" in snapshotData) { + snapshot.end = snapshotData.end; + } + if ("commands" in snapshotData) { + snapshot.commands = snapshotData.commands; + } + if ("valueIcon" in snapshotData) { + snapshot.valueIcon = snapshotData.valueIcon; + } + if ("href" in snapshotData) { + snapshot.href = snapshotData.href; + } + if (snapshotData.stickers) { + for (const sticker of snapshotData.stickers) { + snapshot.stickers ??= []; + snapshot.stickers.push(sticker); + } + } + + this.#messageToView("Snapshot", { snapshotId, snapshot }); + } + + receiveRequestSnapshot({ snapshotId }) { + const snapshotIndex = snapshotId - this.#firstSnapshotId; + const snapshot = this.#snapshots[snapshotIndex]; + if (!snapshot) { + // Ignore request for unknown line index or outdated list + return; + } + + if (snapshot.lineIsReady()) { + this.#sendSnapshotToView(snapshotId, snapshot); + } + } + + handleViewMessage({ name, data }) { + const handlerName = `receive${name}`; + if (!(handlerName in this)) { + throw new Error(`Received unknown message "${name}"`); + } + return this[handlerName](data); + } + + receiveRefresh() { + this.#rebuildSnapshots(); + } + + #rebuildSnapshots() { + // Remember current selection to attempt to restore it later + const prevSelected = this.#snapshots[this.#selectedIndex]; + + // Rebuild snapshots + this.#firstSnapshotId += this.#snapshots.length; + this.#snapshots = Array.from( + MegalistViewModel.#aggregator.enumerateLines(this.#searchText) + ); + + // Update snapshots on screen + this.#messageToView("ShowSnapshots", { + firstSnapshotId: this.#firstSnapshotId, + count: this.#snapshots.length, + }); + + // Restore selection + const usedToBeSelectedNewIndex = this.#snapshots.findIndex( + snapshot => snapshot == prevSelected + ); + if (usedToBeSelectedNewIndex >= 0) { + this.#selectSnapshotByIndex(usedToBeSelectedNewIndex); + } else { + // Make sure selection is within visible lines + this.#selectSnapshotByIndex( + Math.min(this.#selectedIndex, this.#snapshots.length - 1) + ); + } + } + + receiveUpdateFilter({ searchText } = { searchText: "" }) { + if (this.#searchText != searchText) { + this.#searchText = searchText; + this.#messageToView("MegalistUpdateFilter", { searchText }); + this.#rebuildSnapshots(); + } + } + + async receiveCommand({ commandId, snapshotId, value } = {}) { + const index = snapshotId + ? snapshotId - this.#firstSnapshotId + : this.#selectedIndex; + const snapshot = this.#snapshots[index]; + if (snapshot) { + commandId = commandId ?? snapshot.commands[0]?.id; + const mustVerify = snapshot.commands.find(c => c.id == commandId)?.verify; + if (!mustVerify || (await this.#verifyUser())) { + // TODO:Enter the prompt message and pref for #verifyUser() + await snapshot[`execute${commandId}`]?.(value); + } + } + } + + receiveSelectSnapshot({ snapshotId }) { + const index = snapshotId - this.#firstSnapshotId; + if (index >= 0) { + this.#selectSnapshotByIndex(index); + } + } + + receiveSelectNextSnapshot() { + this.#selectSnapshotByIndex(this.#selectedIndex + 1); + } + + receiveSelectPreviousSnapshot() { + this.#selectSnapshotByIndex(this.#selectedIndex - 1); + } + + receiveSelectNextGroup() { + let i = this.#selectedIndex + 1; + while (i < this.#snapshots.length - 1 && !this.#snapshots[i].start) { + i += 1; + } + this.#selectSnapshotByIndex(i); + } + + receiveSelectPreviousGroup() { + let i = this.#selectedIndex - 1; + while (i >= 0 && !this.#snapshots[i].start) { + i -= 1; + } + this.#selectSnapshotByIndex(i); + } + + #selectSnapshotByIndex(index) { + if (index >= 0 && index < this.#snapshots.length) { + this.#selectedIndex = index; + const selectedIndex = this.#selectedIndex; + this.#messageToView("UpdateSelection", { selectedIndex }); + } + } + + async #verifyUser(promptMessage, prefName) { + if (!this.getOSAuthEnabled(prefName)) { + promptMessage = false; + } + let result = await lazy.OSKeyStore.ensureLoggedIn(promptMessage); + return result.authenticated; + } + + /** + * Get the decrypted value for a string pref. + * + * @param {string} prefName -> The pref whose value is needed. + * @param {string} safeDefaultValue -> Value to be returned incase the pref is not yet set. + * @returns {string} + */ + #getSecurePref(prefName, safeDefaultValue) { + try { + let encryptedValue = Services.prefs.getStringPref(prefName, ""); + return this._crypto.decrypt(encryptedValue); + } catch { + return safeDefaultValue; + } + } + + /** + * Set the pref to the encrypted form of the value. + * + * @param {string} prefName -> The pref whose value is to be set. + * @param {string} value -> The value to be set in its encryoted form. + */ + #setSecurePref(prefName, value) { + let encryptedValue = this._crypto.encrypt(value); + Services.prefs.setStringPref(prefName, encryptedValue); + } + + /** + * Get whether the OSAuth is enabled or not. + * + * @param {string} prefName -> The name of the pref (creditcards or addresses) + * @returns {boolean} + */ + getOSAuthEnabled(prefName) { + return this.#getSecurePref(prefName, "") !== "opt out"; + } + + /** + * Set whether the OSAuth is enabled or not. + * + * @param {string} prefName -> The pref to encrypt. + * @param {boolean} enable -> Whether the pref is to be enabled. + */ + setOSAuthEnabled(prefName, enable) { + if (enable) { + Services.prefs.clearUserPref(prefName); + } else { + this.#setSecurePref(prefName, "opt out"); + } + } +} diff --git a/toolkit/components/satchel/megalist/aggregator/Aggregator.sys.mjs b/toolkit/components/satchel/megalist/aggregator/Aggregator.sys.mjs new file mode 100644 index 0000000000..e101fadd16 --- /dev/null +++ b/toolkit/components/satchel/megalist/aggregator/Aggregator.sys.mjs @@ -0,0 +1,78 @@ +/* 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/. */ + +/** + * Connects multiple Data Sources with multiple View Models. + * Aggregator owns Data Sources. + * Aggregator weakly refers to View Models. + */ +export class Aggregator { + #sources = []; + #attachedViewModels = []; + + attachViewModel(viewModel) { + // Weak reference the View Model so we do not keep it in memory forever + this.#attachedViewModels.push(new WeakRef(viewModel)); + } + + detachViewModel(viewModel) { + for (let i = this.#attachedViewModels.length - 1; i >= 0; i--) { + const knownViewModel = this.#attachedViewModels[i].deref(); + if (viewModel == knownViewModel || !knownViewModel) { + this.#attachedViewModels.splice(i, 1); + } + } + } + + /** + * Run action on each of the alive attached view models. + * Remove dead consumers. + * + * @param {Function} action to perform on each alive consumer + */ + forEachViewModel(action) { + for (let i = this.#attachedViewModels.length - 1; i >= 0; i--) { + const viewModel = this.#attachedViewModels[i].deref(); + if (viewModel) { + action(viewModel); + } else { + this.#attachedViewModels.splice(i, 1); + } + } + } + + *enumerateLines(searchText) { + for (let source of this.#sources) { + yield* source.enumerateLines(searchText); + } + } + + /** + * + * @param {Function} createSourceFn (aggregatorApi) used to create Data Source. + * aggregatorApi is the way for Data Source to push data + * to the Aggregator. + */ + addSource(createSourceFn) { + const api = this.#apiForDataSource(); + const source = createSourceFn(api); + this.#sources.push(source); + } + + /** + * Exposes interface for a datasource to communicate with Aggregator. + */ + #apiForDataSource() { + const aggregator = this; + return { + refreshSingleLineOnScreen(line) { + aggregator.forEachViewModel(vm => vm.refreshSingleLineOnScreen(line)); + }, + + refreshAllLinesOnScreen() { + aggregator.forEachViewModel(vm => vm.refreshAllLinesOnScreen()); + }, + }; + } +} diff --git a/toolkit/components/satchel/megalist/aggregator/DefaultAggregator.sys.mjs b/toolkit/components/satchel/megalist/aggregator/DefaultAggregator.sys.mjs new file mode 100644 index 0000000000..cf3a78a6a4 --- /dev/null +++ b/toolkit/components/satchel/megalist/aggregator/DefaultAggregator.sys.mjs @@ -0,0 +1,17 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Aggregator } from "resource://gre/modules/megalist/aggregator/Aggregator.sys.mjs"; +import { AddressesDataSource } from "resource://gre/modules/megalist/aggregator/datasources/AddressesDataSource.sys.mjs"; +import { BankCardDataSource } from "resource://gre/modules/megalist/aggregator/datasources/BankCardDataSource.sys.mjs"; +import { LoginDataSource } from "resource://gre/modules/megalist/aggregator/datasources/LoginDataSource.sys.mjs"; + +export class DefaultAggregator extends Aggregator { + constructor() { + super(); + this.addSource(aggregatorApi => new AddressesDataSource(aggregatorApi)); + this.addSource(aggregatorApi => new BankCardDataSource(aggregatorApi)); + this.addSource(aggregatorApi => new LoginDataSource(aggregatorApi)); + } +} diff --git a/toolkit/components/satchel/megalist/aggregator/datasources/AddressesDataSource.sys.mjs b/toolkit/components/satchel/megalist/aggregator/datasources/AddressesDataSource.sys.mjs new file mode 100644 index 0000000000..f00df0b40b --- /dev/null +++ b/toolkit/components/satchel/megalist/aggregator/datasources/AddressesDataSource.sys.mjs @@ -0,0 +1,258 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { DataSourceBase } from "resource://gre/modules/megalist/aggregator/datasources/DataSourceBase.sys.mjs"; +import { formAutofillStorage } from "resource://autofill/FormAutofillStorage.sys.mjs"; + +async function updateAddress(address, field, value) { + try { + const newAddress = { + ...address, + [field]: value ?? "", + }; + + formAutofillStorage.INTERNAL_FIELDS.forEach( + name => delete newAddress[name] + ); + formAutofillStorage.addresses.VALID_COMPUTED_FIELDS.forEach( + name => delete newAddress[name] + ); + + if (address.guid) { + await formAutofillStorage.addresses.update(address.guid, newAddress); + } else { + await formAutofillStorage.addresses.add(newAddress); + } + } catch (error) { + //todo + console.error("failed to modify address", error); + return false; + } + + return true; +} + +/** + * Data source for Addresses. + * + */ + +export class AddressesDataSource extends DataSourceBase { + #namePrototype; + #organizationPrototype; + #streetAddressPrototype; + #addressLevelOnePrototype; + #addressLevelTwoPrototype; + #addressLevelThreePrototype; + #postalCodePrototype; + #countryPrototype; + #phonePrototype; + #emailPrototype; + + #addressesDisabledMessage; + #enabled; + #header; + + constructor(...args) { + super(...args); + this.formatMessages( + "addresses-section-label", + "address-name-label", + "address-phone-label", + "address-email-label", + "command-copy", + "addresses-disabled", + "command-delete", + "command-edit", + "addresses-command-create" + ).then( + ([ + headerLabel, + nameLabel, + phoneLabel, + emailLabel, + copyLabel, + addressesDisabled, + deleteLabel, + editLabel, + createLabel, + ]) => { + const copyCommand = { id: "Copy", label: copyLabel }; + const editCommand = { id: "Edit", label: editLabel }; + const deleteCommand = { id: "Delete", label: deleteLabel }; + this.#addressesDisabledMessage = addressesDisabled; + this.#header = this.createHeaderLine(headerLabel); + this.#header.commands.push({ id: "Create", label: createLabel }); + + let self = this; + + function prototypeLine(label, key, options = {}) { + return self.prototypeDataLine({ + label: { value: label }, + value: { + get() { + return this.editingValue ?? this.record[key]; + }, + }, + commands: { + value: [copyCommand, editCommand, "-", deleteCommand], + }, + executeEdit: { + value() { + this.editingValue = this.record[key] ?? ""; + this.refreshOnScreen(); + }, + }, + executeSave: { + async value(value) { + if (await updateAddress(this.record, key, value)) { + this.executeCancel(); + } + }, + }, + ...options, + }); + } + + this.#namePrototype = prototypeLine(nameLabel, "name", { + start: { value: true }, + }); + this.#organizationPrototype = prototypeLine( + "Organization", + "organization" + ); + this.#streetAddressPrototype = prototypeLine( + "Street Address", + "street-address" + ); + this.#addressLevelThreePrototype = prototypeLine( + "Neighbourhood", + "address-level3" + ); + this.#addressLevelTwoPrototype = prototypeLine( + "City", + "address-level2" + ); + this.#addressLevelOnePrototype = prototypeLine( + "Province", + "address-level1" + ); + this.#postalCodePrototype = prototypeLine("Postal Code", "postal-code"); + this.#countryPrototype = prototypeLine("Country", "country"); + this.#phonePrototype = prototypeLine(phoneLabel, "tel"); + this.#emailPrototype = prototypeLine(emailLabel, "email", { + end: { value: true }, + }); + + Services.obs.addObserver(this, "formautofill-storage-changed"); + Services.prefs.addObserver( + "extensions.formautofill.addresses.enabled", + this + ); + this.#reloadDataSource(); + } + ); + } + + async #reloadDataSource() { + this.#enabled = Services.prefs.getBoolPref( + "extensions.formautofill.addresses.enabled" + ); + if (!this.#enabled) { + this.#reloadEmptyDataSource(); + return; + } + + await formAutofillStorage.initialize(); + const addresses = await formAutofillStorage.addresses.getAll(); + this.beforeReloadingDataSource(); + addresses.forEach(address => { + const lineId = `${address.name}:${address.tel}`; + + this.addOrUpdateLine(address, lineId + "0", this.#namePrototype); + this.addOrUpdateLine(address, lineId + "1", this.#organizationPrototype); + this.addOrUpdateLine(address, lineId + "2", this.#streetAddressPrototype); + this.addOrUpdateLine( + address, + lineId + "3", + this.#addressLevelThreePrototype + ); + this.addOrUpdateLine( + address, + lineId + "4", + this.#addressLevelTwoPrototype + ); + this.addOrUpdateLine( + address, + lineId + "5", + this.#addressLevelOnePrototype + ); + this.addOrUpdateLine(address, lineId + "6", this.#postalCodePrototype); + this.addOrUpdateLine(address, lineId + "7", this.#countryPrototype); + this.addOrUpdateLine(address, lineId + "8", this.#phonePrototype); + this.addOrUpdateLine(address, lineId + "9", this.#emailPrototype); + }); + this.afterReloadingDataSource(); + } + + /** + * Enumerate all the lines provided by this data source. + * + * @param {string} searchText used to filter data + */ + *enumerateLines(searchText) { + if (this.#enabled === undefined) { + // Async Fluent API makes it possible to have data source waiting + // for the localized strings, which can be detected by undefined in #enabled. + return; + } + + yield this.#header; + if (this.#header.collapsed || !this.#enabled) { + return; + } + + const stats = { total: 0, count: 0 }; + searchText = searchText.toUpperCase(); + yield* this.enumerateLinesForMatchingRecords(searchText, stats, address => + [ + "name", + "organization", + "street-address", + "address-level3", + "address-level2", + "address-level1", + "postal-code", + "country", + "tel", + "email", + ].some(key => address[key]?.toUpperCase().includes(searchText)) + ); + + this.formatMessages({ + id: + stats.count == stats.total + ? "addresses-count" + : "addresses-filtered-count", + args: stats, + }).then(([headerLabel]) => { + this.#header.value = headerLabel; + }); + } + + #reloadEmptyDataSource() { + this.lines.length = 0; + this.#header.value = this.#addressesDisabledMessage; + this.refreshAllLinesOnScreen(); + } + + observe(_subj, topic, message) { + if ( + topic == "formautofill-storage-changed" || + message == "extensions.formautofill.addresses.enabled" + ) { + this.#reloadDataSource(); + } + } +} diff --git a/toolkit/components/satchel/megalist/aggregator/datasources/BankCardDataSource.sys.mjs b/toolkit/components/satchel/megalist/aggregator/datasources/BankCardDataSource.sys.mjs new file mode 100644 index 0000000000..06266a7979 --- /dev/null +++ b/toolkit/components/satchel/megalist/aggregator/datasources/BankCardDataSource.sys.mjs @@ -0,0 +1,339 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { DataSourceBase } from "resource://gre/modules/megalist/aggregator/datasources/DataSourceBase.sys.mjs"; +import { CreditCardRecord } from "resource://gre/modules/shared/CreditCardRecord.sys.mjs"; +import { formAutofillStorage } from "resource://autofill/FormAutofillStorage.sys.mjs"; +import { OSKeyStore } from "resource://gre/modules/OSKeyStore.sys.mjs"; + +async function decryptCard(card) { + if (card["cc-number-encrypted"] && !card["cc-number-decrypted"]) { + try { + card["cc-number-decrypted"] = await OSKeyStore.decrypt( + card["cc-number-encrypted"], + false + ); + card["cc-number"] = card["cc-number-decrypted"]; + } catch (e) { + console.error(e); + } + } +} + +async function updateCard(card, field, value) { + try { + await decryptCard(card); + const newCard = { + ...card, + [field]: value ?? "", + }; + formAutofillStorage.INTERNAL_FIELDS.forEach(name => delete newCard[name]); + formAutofillStorage.creditCards.VALID_COMPUTED_FIELDS.forEach( + name => delete newCard[name] + ); + delete newCard["cc-number-decrypted"]; + CreditCardRecord.normalizeFields(newCard); + + if (card.guid) { + await formAutofillStorage.creditCards.update(card.guid, newCard); + } else { + await formAutofillStorage.creditCards.add(newCard); + } + } catch (error) { + //todo + console.error("failed to modify credit card", error); + return false; + } + + return true; +} + +/** + * Data source for Bank Cards. + * + * Each card is represented by 3 lines: card number, expiration date and holder name. + * + * Protypes are used to reduce memory need because for different records + * similar lines will differ in values only. + */ +export class BankCardDataSource extends DataSourceBase { + #cardNumberPrototype; + #expirationPrototype; + #holderNamePrototype; + #cardsDisabledMessage; + #enabled; + #header; + + constructor(...args) { + super(...args); + // Wait for Fluent to provide strings before loading data + this.formatMessages( + "payments-section-label", + "card-number-label", + "card-expiration-label", + "card-holder-label", + "command-copy", + "command-reveal", + "command-conceal", + "payments-disabled", + "command-delete", + "command-edit", + "payments-command-create" + ).then( + ([ + headerLabel, + numberLabel, + expirationLabel, + holderLabel, + copyCommandLabel, + revealCommandLabel, + concealCommandLabel, + cardsDisabled, + deleteCommandLabel, + editCommandLabel, + cardsCreateCommandLabel, + ]) => { + const copyCommand = { id: "Copy", label: copyCommandLabel }; + const editCommand = { + id: "Edit", + label: editCommandLabel, + verify: true, + }; + const deleteCommand = { + id: "Delete", + label: deleteCommandLabel, + verify: true, + }; + this.#cardsDisabledMessage = cardsDisabled; + this.#header = this.createHeaderLine(headerLabel); + this.#header.commands.push({ + id: "Create", + label: cardsCreateCommandLabel, + }); + this.#cardNumberPrototype = this.prototypeDataLine({ + label: { value: numberLabel }, + concealed: { value: true, writable: true }, + start: { value: true }, + value: { + async get() { + if (this.editingValue !== undefined) { + return this.editingValue; + } + + if (this.concealed) { + return ( + "••••••••" + + this.record["cc-number"].replaceAll("*", "").substr(-4) + ); + } + + await decryptCard(this.record); + return this.record["cc-number-decrypted"]; + }, + }, + valueIcon: { + get() { + const typeToImage = { + amex: "third-party/cc-logo-amex.png", + cartebancaire: "third-party/cc-logo-cartebancaire.png", + diners: "third-party/cc-logo-diners.svg", + discover: "third-party/cc-logo-discover.png", + jcb: "third-party/cc-logo-jcb.svg", + mastercard: "third-party/cc-logo-mastercard.svg", + mir: "third-party/cc-logo-mir.svg", + unionpay: "third-party/cc-logo-unionpay.svg", + visa: "third-party/cc-logo-visa.svg", + }; + return ( + "chrome://formautofill/content/" + + (typeToImage[this.record["cc-type"]] ?? + "icon-credit-card-generic.svg") + ); + }, + }, + commands: { + get() { + const commands = [ + { id: "Conceal", label: concealCommandLabel }, + { ...copyCommand, verify: true }, + editCommand, + "-", + deleteCommand, + ]; + if (this.concealed) { + commands[0] = { + id: "Reveal", + label: revealCommandLabel, + verify: true, + }; + } + return commands; + }, + }, + executeReveal: { + value() { + this.concealed = false; + this.refreshOnScreen(); + }, + }, + executeConceal: { + value() { + this.concealed = true; + this.refreshOnScreen(); + }, + }, + executeCopy: { + async value() { + await decryptCard(this.record); + this.copyToClipboard(this.record["cc-number-decrypted"]); + }, + }, + executeEdit: { + async value() { + await decryptCard(this.record); + this.editingValue = this.record["cc-number-decrypted"] ?? ""; + this.refreshOnScreen(); + }, + }, + executeSave: { + async value(value) { + if (updateCard(this.record, "cc-number", value)) { + this.executeCancel(); + } + }, + }, + }); + this.#expirationPrototype = this.prototypeDataLine({ + label: { value: expirationLabel }, + value: { + get() { + return `${this.record["cc-exp-month"]}/${this.record["cc-exp-year"]}`; + }, + }, + commands: { + value: [copyCommand, editCommand, "-", deleteCommand], + }, + }); + this.#holderNamePrototype = this.prototypeDataLine({ + label: { value: holderLabel }, + end: { value: true }, + value: { + get() { + return this.editingValue ?? this.record["cc-name"]; + }, + }, + commands: { + value: [copyCommand, editCommand, "-", deleteCommand], + }, + executeEdit: { + value() { + this.editingValue = this.record["cc-name"] ?? ""; + this.refreshOnScreen(); + }, + }, + executeSave: { + async value(value) { + if (updateCard(this.record, "cc-name", value)) { + this.executeCancel(); + } + }, + }, + }); + + Services.obs.addObserver(this, "formautofill-storage-changed"); + Services.prefs.addObserver( + "extensions.formautofill.creditCards.enabled", + this + ); + this.#reloadDataSource(); + } + ); + } + + /** + * Enumerate all the lines provided by this data source. + * + * @param {string} searchText used to filter data + */ + *enumerateLines(searchText) { + if (this.#enabled === undefined) { + // Async Fluent API makes it possible to have data source waiting + // for the localized strings, which can be detected by undefined in #enabled. + return; + } + + yield this.#header; + if (this.#header.collapsed || !this.#enabled) { + return; + } + + const stats = { count: 0, total: 0 }; + searchText = searchText.toUpperCase(); + yield* this.enumerateLinesForMatchingRecords( + searchText, + stats, + card => + (card["cc-number-decrypted"] || card["cc-number"]) + .toUpperCase() + .includes(searchText) || + `${card["cc-exp-month"]}/${card["cc-exp-year"]}` + .toUpperCase() + .includes(searchText) || + card["cc-name"].toUpperCase().includes(searchText) + ); + + this.formatMessages({ + id: + stats.count == stats.total + ? "payments-count" + : "payments-filtered-count", + args: stats, + }).then(([headerLabel]) => { + this.#header.value = headerLabel; + }); + } + + /** + * Sync lines array with the actual data source. + * This function reads all cards from the storage, adds or updates lines and + * removes lines for the removed cards. + */ + async #reloadDataSource() { + this.#enabled = Services.prefs.getBoolPref( + "extensions.formautofill.creditCards.enabled" + ); + if (!this.#enabled) { + this.#reloadEmptyDataSource(); + return; + } + + await formAutofillStorage.initialize(); + const cards = await formAutofillStorage.creditCards.getAll(); + this.beforeReloadingDataSource(); + cards.forEach(card => { + const lineId = `${card["cc-name"]}:${card.guid}`; + + this.addOrUpdateLine(card, lineId + "0", this.#cardNumberPrototype); + this.addOrUpdateLine(card, lineId + "1", this.#expirationPrototype); + this.addOrUpdateLine(card, lineId + "2", this.#holderNamePrototype); + }); + this.afterReloadingDataSource(); + } + + #reloadEmptyDataSource() { + this.lines.length = 0; + //todo: user can enable credit cards by activating header line + this.#header.value = this.#cardsDisabledMessage; + this.refreshAllLinesOnScreen(); + } + + observe(_subj, topic, message) { + if ( + topic == "formautofill-storage-changed" || + message == "extensions.formautofill.creditCards.enabled" + ) { + this.#reloadDataSource(); + } + } +} diff --git a/toolkit/components/satchel/megalist/aggregator/datasources/DataSourceBase.sys.mjs b/toolkit/components/satchel/megalist/aggregator/datasources/DataSourceBase.sys.mjs new file mode 100644 index 0000000000..49be733aef --- /dev/null +++ b/toolkit/components/satchel/megalist/aggregator/datasources/DataSourceBase.sys.mjs @@ -0,0 +1,291 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { BinarySearch } from "resource://gre/modules/BinarySearch.sys.mjs"; +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "ClipboardHelper", + "@mozilla.org/widget/clipboardhelper;1", + "nsIClipboardHelper" +); + +/** + * Create a function to format messages. + * + * @param {...any} ftlFiles to be used for formatting messages + * @returns {Function} a function that can be used to format messsages + */ +function createFormatMessages(...ftlFiles) { + const strings = new Localization(ftlFiles); + + return async (...ids) => { + for (const i in ids) { + if (typeof ids[i] == "string") { + ids[i] = { id: ids[i] }; + } + } + + const messages = await strings.formatMessages(ids); + return messages.map(message => { + if (message.attributes) { + return message.attributes.reduce( + (result, { name, value }) => ({ ...result, [name]: value }), + {} + ); + } + return message.value; + }); + }; +} + +/** + * Base datasource class + */ +export class DataSourceBase { + #aggregatorApi; + + constructor(aggregatorApi) { + this.#aggregatorApi = aggregatorApi; + } + + // proxy consumer api functions to datasource interface + + refreshSingleLineOnScreen(line) { + this.#aggregatorApi.refreshSingleLineOnScreen(line); + } + + refreshAllLinesOnScreen() { + this.#aggregatorApi.refreshAllLinesOnScreen(); + } + + formatMessages = createFormatMessages("preview/megalist.ftl"); + + /** + * Prototype for the each line. + * See this link for details: + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperties#props + */ + #linePrototype = { + /** + * Reference to the Data Source that owns this line. + */ + source: this, + + /** + * Each line has a reference to the actual data record. + */ + record: { writable: true }, + + /** + * Is line ready to be displayed? + * Used by the View Model. + * + * @returns {boolean} true if line can be sent to the view. + * false if line is not ready to be displayed. In this case + * data source will start pulling value from the underlying + * storage and will push data to screen when it's ready. + */ + lineIsReady() { + return true; + }, + + copyToClipboard(text) { + lazy.ClipboardHelper.copyString(text, lazy.ClipboardHelper.Sensitive); + }, + + openLinkInTab(url) { + const { BrowserWindowTracker } = ChromeUtils.importESModule( + "resource:///modules/BrowserWindowTracker.sys.mjs" + ); + const browser = BrowserWindowTracker.getTopWindow().gBrowser; + browser.addWebTab(url, { inBackground: false }); + }, + + /** + * Simple version of Copy command. Line still needs to add "Copy" command. + * Override if copied value != displayed value. + */ + executeCopy() { + this.copyToClipboard(this.value); + }, + + executeOpen() { + this.openLinkInTab(this.href); + }, + + executeEditInProgress(value) { + this.editingValue = value; + this.refreshOnScreen(); + }, + + executeCancel() { + delete this.editingValue; + this.refreshOnScreen(); + }, + + get template() { + return "editingValue" in this ? "editingLineTemplate" : undefined; + }, + + refreshOnScreen() { + this.source.refreshSingleLineOnScreen(this); + }, + }; + + /** + * Creates collapsible section header line. + * + * @param {string} label for the section + * @returns {object} section header line + */ + createHeaderLine(label) { + const toggleCommand = { id: "Toggle", label: "" }; + const result = { + label, + value: "", + collapsed: false, + start: true, + end: true, + source: this, + + /** + * Use different templates depending on the collapsed state. + */ + get template() { + return this.collapsed + ? "collapsedSectionTemplate" + : "expandedSectionTemplate"; + }, + + lineIsReady: () => true, + + commands: [toggleCommand], + + executeToggle() { + this.collapsed = !this.collapsed; + this.source.refreshAllLinesOnScreen(); + }, + }; + + this.formatMessages("command-toggle").then(([toggleLabel]) => { + toggleCommand.label = toggleLabel; + }); + + return result; + } + + /** + * Create a prototype to be used for data lines, + * provides common set of features like Copy command. + * + * @param {object} properties to customize data line + * @returns {object} data line prototype + */ + prototypeDataLine(properties) { + return Object.create(this.#linePrototype, properties); + } + + lines = []; + #collator = new Intl.Collator(); + #linesToForget; + + /** + * Code to run before reloading data source. + * It will start tracking which lines are no longer at the source so + * afterReloadingDataSource() can remove them. + */ + beforeReloadingDataSource() { + this.#linesToForget = new Set(this.lines); + } + + /** + * Code to run after reloading data source. + * It will forget lines that are no longer at the source and refresh screen. + */ + afterReloadingDataSource() { + if (this.#linesToForget.size) { + for (let i = this.lines.length; i >= 0; i--) { + if (this.#linesToForget.has(this.lines[i])) { + this.lines.splice(i, 1); + } + } + } + + this.#linesToForget = null; + this.refreshAllLinesOnScreen(); + } + + /** + * Add or update line associated with the record. + * + * @param {object} record with which line is associated + * @param {*} id sortable line id + * @param {*} fieldPrototype to be used when creating a line. + */ + addOrUpdateLine(record, id, fieldPrototype) { + let [found, index] = BinarySearch.search( + (target, value) => this.#collator.compare(target, value.id), + this.lines, + id + ); + + if (found) { + this.#linesToForget.delete(this.lines[index]); + } else { + const line = Object.create(fieldPrototype, { id: { value: id } }); + this.lines.splice(index, 0, line); + } + this.lines[index].record = record; + return this.lines[index]; + } + + *enumerateLinesForMatchingRecords(searchText, stats, match) { + stats.total = 0; + stats.count = 0; + + if (searchText) { + let i = 0; + while (i < this.lines.length) { + const currentRecord = this.lines[i].record; + stats.total += 1; + + if (match(currentRecord)) { + // Record matches, yield all it's lines + while ( + i < this.lines.length && + currentRecord == this.lines[i].record + ) { + yield this.lines[i]; + i += 1; + } + stats.count += 1; + } else { + // Record does not match, skip until the next one + while ( + i < this.lines.length && + currentRecord == this.lines[i].record + ) { + i += 1; + } + } + } + } else { + // No search text is provided - send all lines out, count records + let currentRecord; + for (const line of this.lines) { + yield line; + + if (line.record != currentRecord) { + stats.total += 1; + currentRecord = line.record; + } + } + stats.count = stats.total; + } + } +} diff --git a/toolkit/components/satchel/megalist/aggregator/datasources/LoginDataSource.sys.mjs b/toolkit/components/satchel/megalist/aggregator/datasources/LoginDataSource.sys.mjs new file mode 100644 index 0000000000..324bc4d141 --- /dev/null +++ b/toolkit/components/satchel/megalist/aggregator/datasources/LoginDataSource.sys.mjs @@ -0,0 +1,472 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; +import { LoginHelper } from "resource://gre/modules/LoginHelper.sys.mjs"; +import { DataSourceBase } from "resource://gre/modules/megalist/aggregator/datasources/DataSourceBase.sys.mjs"; +import { LoginCSVImport } from "resource://gre/modules/LoginCSVImport.sys.mjs"; + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + LoginBreaches: "resource:///modules/LoginBreaches.sys.mjs", +}); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "BREACH_ALERTS_ENABLED", + "signon.management.page.breach-alerts.enabled", + false +); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "VULNERABLE_PASSWORDS_ENABLED", + "signon.management.page.vulnerable-passwords.enabled", + false +); + +/** + * Data source for Logins. + * + * Each login is represented by 3 lines: origin, username and password. + * + * Protypes are used to reduce memory need because for different records + * similar lines will differ in values only. + */ +export class LoginDataSource extends DataSourceBase { + #originPrototype; + #usernamePrototype; + #passwordPrototype; + #loginsDisabledMessage; + #enabled; + #header; + + constructor(...args) { + super(...args); + // Wait for Fluent to provide strings before loading data + this.formatMessages( + "passwords-section-label", + "passwords-origin-label", + "passwords-username-label", + "passwords-password-label", + "command-open", + "command-copy", + "command-reveal", + "command-conceal", + "passwords-disabled", + "command-delete", + "command-edit", + "passwords-command-create", + "passwords-command-import", + "passwords-command-export", + "passwords-command-remove-all", + "passwords-command-settings", + "passwords-command-help", + "passwords-import-file-picker-title", + "passwords-import-file-picker-import-button", + "passwords-import-file-picker-csv-filter-title", + "passwords-import-file-picker-tsv-filter-title" + ).then( + ([ + headerLabel, + originLabel, + usernameLabel, + passwordLabel, + openCommandLabel, + copyCommandLabel, + revealCommandLabel, + concealCommandLabel, + passwordsDisabled, + deleteCommandLabel, + editCommandLabel, + passwordsCreateCommandLabel, + passwordsImportCommandLabel, + passwordsExportCommandLabel, + passwordsRemoveAllCommandLabel, + passwordsSettingsCommandLabel, + passwordsHelpCommandLabel, + passwordsImportFilePickerTitle, + passwordsImportFilePickerImportButton, + passwordsImportFilePickerCsvFilterTitle, + passwordsImportFilePickerTsvFilterTitle, + ]) => { + const copyCommand = { id: "Copy", label: copyCommandLabel }; + const editCommand = { id: "Edit", label: editCommandLabel }; + const deleteCommand = { id: "Delete", label: deleteCommandLabel }; + this.breachedSticker = { type: "warning", label: "BREACH" }; + this.vulnerableSticker = { type: "risk", label: "🤮 Vulnerable" }; + this.#loginsDisabledMessage = passwordsDisabled; + this.#header = this.createHeaderLine(headerLabel); + this.#header.commands.push( + { id: "Create", label: passwordsCreateCommandLabel }, + { id: "Import", label: passwordsImportCommandLabel }, + { id: "Export", label: passwordsExportCommandLabel }, + { id: "RemoveAll", label: passwordsRemoveAllCommandLabel }, + { id: "Settings", label: passwordsSettingsCommandLabel }, + { id: "Help", label: passwordsHelpCommandLabel } + ); + this.#header.executeImport = async () => { + await this.#importFromFile( + passwordsImportFilePickerTitle, + passwordsImportFilePickerImportButton, + passwordsImportFilePickerCsvFilterTitle, + passwordsImportFilePickerTsvFilterTitle + ); + }; + this.#header.executeSettings = () => { + this.#openPreferences(); + }; + this.#header.executeHelp = () => { + this.#getHelp(); + }; + + this.#originPrototype = this.prototypeDataLine({ + label: { value: originLabel }, + start: { value: true }, + value: { + get() { + return this.record.displayOrigin; + }, + }, + valueIcon: { + get() { + return `page-icon:${this.record.origin}`; + }, + }, + href: { + get() { + return this.record.origin; + }, + }, + commands: { + value: [ + { id: "Open", label: openCommandLabel }, + copyCommand, + "-", + deleteCommand, + ], + }, + executeCopy: { + value() { + this.copyToClipboard(this.record.origin); + }, + }, + }); + this.#usernamePrototype = this.prototypeDataLine({ + label: { value: usernameLabel }, + value: { + get() { + return this.editingValue ?? this.record.username; + }, + }, + commands: { value: [copyCommand, editCommand, "-", deleteCommand] }, + executeEdit: { + value() { + this.editingValue = this.record.username ?? ""; + this.refreshOnScreen(); + }, + }, + executeSave: { + value(value) { + try { + const modifiedLogin = this.record.clone(); + modifiedLogin.username = value; + Services.logins.modifyLogin(this.record, modifiedLogin); + } catch (error) { + //todo + console.error("failed to modify login", error); + } + this.executeCancel(); + }, + }, + }); + this.#passwordPrototype = this.prototypeDataLine({ + label: { value: passwordLabel }, + concealed: { value: true, writable: true }, + end: { value: true }, + value: { + get() { + return ( + this.editingValue ?? + (this.concealed ? "••••••••" : this.record.password) + ); + }, + }, + commands: { + get() { + const commands = [ + { id: "Conceal", label: concealCommandLabel }, + { + id: "Copy", + label: copyCommandLabel, + verify: true, + }, + editCommand, + "-", + deleteCommand, + ]; + if (this.concealed) { + commands[0] = { + id: "Reveal", + label: revealCommandLabel, + verify: true, + }; + } + return commands; + }, + }, + executeReveal: { + value() { + this.concealed = false; + this.refreshOnScreen(); + }, + }, + executeConceal: { + value() { + this.concealed = true; + this.refreshOnScreen(); + }, + }, + executeCopy: { + value() { + this.copyToClipboard(this.record.password); + }, + }, + executeEdit: { + value() { + this.editingValue = this.record.password ?? ""; + this.refreshOnScreen(); + }, + }, + executeSave: { + value(value) { + try { + const modifiedLogin = this.record.clone(); + modifiedLogin.password = value; + Services.logins.modifyLogin(this.record, modifiedLogin); + } catch (error) { + //todo + console.error("failed to modify login", error); + } + this.executeCancel(); + }, + }, + }); + + Services.obs.addObserver(this, "passwordmgr-storage-changed"); + Services.prefs.addObserver("signon.rememberSignons", this); + Services.prefs.addObserver( + "signon.management.page.breach-alerts.enabled", + this + ); + Services.prefs.addObserver( + "signon.management.page.vulnerable-passwords.enabled", + this + ); + this.#reloadDataSource(); + } + ); + } + + async #importFromFile(title, buttonLabel, csvTitle, tsvTitle) { + const { BrowserWindowTracker } = ChromeUtils.importESModule( + "resource:///modules/BrowserWindowTracker.sys.mjs" + ); + const browser = BrowserWindowTracker.getTopWindow().gBrowser; + let { result, path } = await this.openFilePickerDialog( + title, + buttonLabel, + [ + { + title: csvTitle, + extensionPattern: "*.csv", + }, + { + title: tsvTitle, + extensionPattern: "*.tsv", + }, + ], + browser.ownerGlobal + ); + + if (result != Ci.nsIFilePicker.returnCancel) { + let summary; + try { + summary = await LoginCSVImport.importFromCSV(path); + } catch (e) { + // TODO: Display error for import + } + if (summary) { + // TODO: Display successful import summary + } + } + } + + async openFilePickerDialog(title, okButtonLabel, appendFilters, ownerGlobal) { + return new Promise(resolve => { + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + fp.init(ownerGlobal, title, Ci.nsIFilePicker.modeOpen); + for (const appendFilter of appendFilters) { + fp.appendFilter(appendFilter.title, appendFilter.extensionPattern); + } + fp.appendFilters(Ci.nsIFilePicker.filterAll); + fp.okButtonLabel = okButtonLabel; + fp.open(async result => { + resolve({ result, path: fp.file.path }); + }); + }); + } + + #openPreferences() { + const { BrowserWindowTracker } = ChromeUtils.importESModule( + "resource:///modules/BrowserWindowTracker.sys.mjs" + ); + const browser = BrowserWindowTracker.getTopWindow().gBrowser; + browser.ownerGlobal.openPreferences("privacy-logins"); + } + + #getHelp() { + const { BrowserWindowTracker } = ChromeUtils.importESModule( + "resource:///modules/BrowserWindowTracker.sys.mjs" + ); + const browser = BrowserWindowTracker.getTopWindow().gBrowser; + const SUPPORT_URL = + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "password-manager-remember-delete-edit-logins"; + browser.ownerGlobal.openWebLinkIn(SUPPORT_URL, "tab", { + relatedToCurrent: true, + }); + } + + /** + * Enumerate all the lines provided by this data source. + * + * @param {string} searchText used to filter data + */ + *enumerateLines(searchText) { + if (this.#enabled === undefined) { + // Async Fluent API makes it possible to have data source waiting + // for the localized strings, which can be detected by undefined in #enabled. + return; + } + + yield this.#header; + if (this.#header.collapsed || !this.#enabled) { + return; + } + + const stats = { count: 0, total: 0 }; + searchText = searchText.toUpperCase(); + yield* this.enumerateLinesForMatchingRecords( + searchText, + stats, + login => + login.displayOrigin.toUpperCase().includes(searchText) || + login.username.toUpperCase().includes(searchText) || + login.password.toUpperCase().includes(searchText) + ); + + this.formatMessages({ + id: + stats.count == stats.total + ? "passwords-count" + : "passwords-filtered-count", + args: stats, + }).then(([headerLabel]) => { + this.#header.value = headerLabel; + }); + } + + /** + * Sync lines array with the actual data source. + * This function reads all logins from the storage, adds or updates lines and + * removes lines for the removed logins. + */ + async #reloadDataSource() { + this.#enabled = Services.prefs.getBoolPref("signon.rememberSignons"); + if (!this.#enabled) { + this.#reloadEmptyDataSource(); + return; + } + + const logins = await LoginHelper.getAllUserFacingLogins(); + this.beforeReloadingDataSource(); + + const breachesMap = lazy.BREACH_ALERTS_ENABLED + ? await lazy.LoginBreaches.getPotentialBreachesByLoginGUID(logins) + : new Map(); + + logins.forEach(login => { + // Similar domains will be grouped together + // www. will have least effect on the sorting + const parts = login.displayOrigin.split("."); + + // Exclude TLD domain + //todo support eTLD and use public suffix here https://publicsuffix.org + if (parts.length > 1) { + parts.length -= 1; + } + const domain = parts.reverse().join("."); + const lineId = `${domain}:${login.username}:${login.guid}`; + + let originLine = this.addOrUpdateLine( + login, + lineId + "0", + this.#originPrototype + ); + this.addOrUpdateLine(login, lineId + "1", this.#usernamePrototype); + let passwordLine = this.addOrUpdateLine( + login, + lineId + "2", + this.#passwordPrototype + ); + + let breachIndex = + originLine.stickers?.findIndex(s => s === this.breachedSticker) ?? -1; + let breach = breachesMap.get(login.guid); + if (breach && breachIndex < 0) { + originLine.stickers ??= []; + originLine.stickers.push(this.breachedSticker); + } else if (!breach && breachIndex >= 0) { + originLine.stickers.splice(breachIndex, 1); + } + + const vulnerable = lazy.VULNERABLE_PASSWORDS_ENABLED + ? lazy.LoginBreaches.getPotentiallyVulnerablePasswordsByLoginGUID([ + login, + ]).size + : 0; + + let vulnerableIndex = + passwordLine.stickers?.findIndex(s => s === this.vulnerableSticker) ?? + -1; + if (vulnerable && vulnerableIndex < 0) { + passwordLine.stickers ??= []; + passwordLine.stickers.push(this.vulnerableSticker); + } else if (!vulnerable && vulnerableIndex >= 0) { + passwordLine.stickers.splice(vulnerableIndex, 1); + } + }); + + this.afterReloadingDataSource(); + } + + #reloadEmptyDataSource() { + this.lines.length = 0; + //todo: user can enable passwords by activating Passwords header line + this.#header.value = this.#loginsDisabledMessage; + this.refreshAllLinesOnScreen(); + } + + observe(_subj, topic, message) { + if ( + topic == "passwordmgr-storage-changed" || + message == "signon.rememberSignons" || + message == "signon.management.page.breach-alerts.enabled" || + message == "signon.management.page.vulnerable-passwords.enabled" + ) { + this.#reloadDataSource(); + } + } +} diff --git a/toolkit/components/satchel/megalist/aggregator/moz.build b/toolkit/components/satchel/megalist/aggregator/moz.build new file mode 100644 index 0000000000..f244ade794 --- /dev/null +++ b/toolkit/components/satchel/megalist/aggregator/moz.build @@ -0,0 +1,17 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +EXTRA_JS_MODULES["megalist/aggregator"] += [ + "Aggregator.sys.mjs", + "DefaultAggregator.sys.mjs", +] + +EXTRA_JS_MODULES["megalist/aggregator/datasources"] += [ + "datasources/AddressesDataSource.sys.mjs", + "datasources/BankCardDataSource.sys.mjs", + "datasources/DataSourceBase.sys.mjs", + "datasources/LoginDataSource.sys.mjs", +] diff --git a/toolkit/components/satchel/megalist/content/MegalistView.mjs b/toolkit/components/satchel/megalist/content/MegalistView.mjs new file mode 100644 index 0000000000..44a0198692 --- /dev/null +++ b/toolkit/components/satchel/megalist/content/MegalistView.mjs @@ -0,0 +1,477 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { html } from "chrome://global/content/vendor/lit.all.mjs"; +import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; + +// eslint-disable-next-line import/no-unassigned-import +import "chrome://global/content/megalist/VirtualizedList.mjs"; + +// eslint-disable-next-line import/no-unassigned-import +import "chrome://global/content/megalist/search-input.mjs"; + +/** + * Map with limit on how many entries it can have. + * When over limit entries are added, oldest one are removed. + */ +class MostRecentMap { + constructor(maxSize) { + this.#maxSize = maxSize; + } + + get(id) { + const data = this.#map.get(id); + if (data) { + this.#keepAlive(id, data); + } + return data; + } + + has(id) { + this.#map.has(id); + } + + set(id, data) { + this.#keepAlive(id, data); + this.#enforceLimits(); + } + + clear() { + this.#map.clear(); + } + + #maxSize; + #map = new Map(); + + #keepAlive(id, data) { + // Re-insert data to the map so it will be less likely to be evicted + this.#map.delete(id); + this.#map.set(id, data); + } + + #enforceLimits() { + // Maps preserve order in which data was inserted, + // we use that fact to remove oldest data from it. + while (this.#map.size > this.#maxSize) { + this.#map.delete(this.#map.keys().next().value); + } + } +} + +/** + * MegalistView presents data pushed to it by the MegalistViewModel and + * notify MegalistViewModel of user commands. + */ +export class MegalistView extends MozLitElement { + static keyToMessage = { + ArrowUp: "SelectPreviousSnapshot", + ArrowDown: "SelectNextSnapshot", + PageUp: "SelectPreviousGroup", + PageDown: "SelectNextGroup", + Escape: "UpdateFilter", + }; + static LINE_HEIGHT = 64; + + constructor() { + super(); + this.selectedIndex = 0; + this.searchText = ""; + + window.addEventListener("MessageFromViewModel", ev => + this.#onMessageFromViewModel(ev) + ); + } + + static get properties() { + return { + listLength: { type: Number }, + selectedIndex: { type: Number }, + searchText: { type: String }, + }; + } + + /** + * View shows list of snapshots of lines stored in the View Model. + * View Model provides the first snapshot id in the list and list length. + * It's safe to combine firstSnapshotId+index to identify specific snapshot + * in the list. When the list changes, View Model will provide a new + * list with new first snapshot id (even if the content is the same). + */ + #firstSnapshotId = 0; + + /** + * Cache 120 most recently used lines. + * View lives in child and View Model in parent processes. + * By caching a few lines we reduce the need to send data between processes. + * This improves performance in nearby scrolling scenarios. + * 7680 is 8K vertical screen resolution. + * Typical line is under 1/4KB long, making around 30KB cache requirement. + */ + #snapshotById = new MostRecentMap(7680 / MegalistView.LINE_HEIGHT); + + #templates = {}; + + connectedCallback() { + super.connectedCallback(); + this.ownerDocument.addEventListener("keydown", e => this.#handleKeydown(e)); + for (const template of this.ownerDocument.getElementsByTagName( + "template" + )) { + this.#templates[template.id] = template.content.firstElementChild; + } + this.#messageToViewModel("Refresh"); + } + + createLineElement(index) { + if (index < 0 || index >= this.listLength) { + return null; + } + + const snapshotId = this.#firstSnapshotId + index; + const lineElement = this.#templates.lineElement.cloneNode(true); + lineElement.dataset.id = snapshotId; + lineElement.addEventListener("dblclick", e => { + this.#messageToViewModel("Command"); + e.preventDefault(); + }); + + const data = this.#snapshotById.get(snapshotId); + if (data !== "Loading") { + if (data) { + this.#applyData(snapshotId, data, lineElement); + } else { + // Put placeholder for this snapshot data to avoid requesting it again + this.#snapshotById.set(snapshotId, "Loading"); + + // Ask for snapshot data from the View Model. + // Note: we could have optimized it further by asking for a range of + // indices because any scroll in virtualized list can only add + // a continuous range at the top or bottom of the visible area. + // However, this optimization is not necessary at the moment as + // we typically will request under a 100 of lines at a time. + // If we feel like making this improvement, we need to enhance + // VirtualizedList to request a range of new elements instead. + this.#messageToViewModel("RequestSnapshot", { snapshotId }); + } + } + + return lineElement; + } + + /** + * Find snapshot element on screen and populate it with data + */ + receiveSnapshot({ snapshotId, snapshot }) { + this.#snapshotById.set(snapshotId, snapshot); + + const lineElement = this.shadowRoot.querySelector( + `.line[data-id="${snapshotId}"]` + ); + if (lineElement) { + this.#applyData(snapshotId, snapshot, lineElement); + } + } + + #applyData(snapshotId, snapshotData, lineElement) { + let elementToFocus; + const template = + this.#templates[snapshotData.template] ?? this.#templates.lineTemplate; + + const lineContent = template.cloneNode(true); + lineContent.querySelector(".label").textContent = snapshotData.label; + + const valueElement = lineContent.querySelector(".value"); + if (valueElement) { + const valueText = lineContent.querySelector("span"); + if (valueText) { + valueText.textContent = snapshotData.value; + } else { + const valueInput = lineContent.querySelector("input"); + if (valueInput) { + valueInput.value = snapshotData.value; + valueInput.addEventListener("keydown", e => { + switch (e.code) { + case "Enter": + this.#messageToViewModel("Command", { + snapshotId, + commandId: "Save", + value: valueInput.value, + }); + break; + case "Escape": + this.#messageToViewModel("Command", { + snapshotId, + commandId: "Cancel", + }); + break; + default: + return; + } + e.preventDefault(); + e.stopPropagation(); + }); + valueInput.addEventListener("input", () => { + // Update local cache so we don't override editing value + // while user scrolls up or down a little. + const snapshotDataInChild = this.#snapshotById.get(snapshotId); + if (snapshotDataInChild) { + snapshotDataInChild.value = valueInput.value; + } + this.#messageToViewModel("Command", { + snapshotId, + commandId: "EditInProgress", + value: valueInput.value, + }); + }); + elementToFocus = valueInput; + } else { + valueElement.textContent = snapshotData.value; + } + } + + if (snapshotData.valueIcon) { + const valueIcon = valueElement.querySelector(".icon"); + if (valueIcon) { + valueIcon.src = snapshotData.valueIcon; + } + } + + if (snapshotData.href) { + const linkElement = this.ownerDocument.createElement("a"); + linkElement.className = valueElement.className; + linkElement.href = snapshotData.href; + linkElement.replaceChildren(...valueElement.children); + valueElement.replaceWith(linkElement); + } + + if (snapshotData.stickers?.length) { + const stickersElement = lineContent.querySelector(".stickers"); + for (const sticker of snapshotData.stickers) { + const stickerElement = this.ownerDocument.createElement("span"); + stickerElement.textContent = sticker.label; + stickerElement.className = sticker.type; + stickersElement.appendChild(stickerElement); + } + } + } + + lineElement.querySelector(".content").replaceWith(lineContent); + lineElement.classList.toggle("start", !!snapshotData.start); + lineElement.classList.toggle("end", !!snapshotData.end); + elementToFocus?.focus(); + } + + #messageToViewModel(messageName, data) { + window.windowGlobalChild + .getActor("Megalist") + .sendAsyncMessage(messageName, data); + } + + #onMessageFromViewModel({ detail }) { + const functionName = `receive${detail.name}`; + if (!(functionName in this)) { + throw new Error(`Received unknown message "${detail.name}"`); + } + this[functionName](detail.data); + } + + receiveUpdateSelection({ selectedIndex }) { + this.selectedIndex = selectedIndex; + } + + receiveShowSnapshots({ firstSnapshotId, count }) { + this.#firstSnapshotId = firstSnapshotId; + this.listLength = count; + + // Each new display list starts with the new first snapshot id + // so we can forget previously known data. + this.#snapshotById.clear(); + this.shadowRoot.querySelector("virtualized-list").requestRefresh(); + this.requestUpdate(); + } + + receiveMegalistUpdateFilter({ searchText }) { + this.searchText = searchText; + this.requestUpdate(); + } + + #handleInputChange(e) { + const searchText = e.target.value; + this.#messageToViewModel("UpdateFilter", { searchText }); + } + + #handleKeydown(e) { + const message = MegalistView.keyToMessage[e.code]; + if (message) { + this.#messageToViewModel(message); + e.preventDefault(); + } else if (e.code == "Enter") { + // Do not handle Enter at the virtualized list level when line menu is open + if ( + this.shadowRoot.querySelector( + ".line.selected > .menuButton > .menuPopup" + ) + ) { + return; + } + + if (e.altKey) { + // Execute default command1 + this.#messageToViewModel("Command"); + } else { + // Show line level menu + this.shadowRoot + .querySelector(".line.selected > .menuButton > button") + ?.click(); + } + e.preventDefault(); + } else if (e.ctrlKey && e.key == "c" && !this.searchText.length) { + this.#messageToViewModel("Command", { commandId: "Copy" }); + e.preventDefault(); + } + } + + #handleClick(e) { + const lineElement = e.composedTarget.closest(".line"); + if (!lineElement) { + return; + } + + const snapshotId = Number(lineElement.dataset.id); + const snapshotData = this.#snapshotById.get(snapshotId); + if (!snapshotData) { + return; + } + + this.#messageToViewModel("SelectSnapshot", { snapshotId }); + const menuButton = e.composedTarget.closest(".menuButton"); + if (menuButton) { + this.#handleMenuButtonClick(menuButton, snapshotId, snapshotData); + } + + e.preventDefault(); + } + + #handleMenuButtonClick(menuButton, snapshotId, snapshotData) { + if (!snapshotData.commands?.length) { + return; + } + + const popup = this.ownerDocument.createElement("div"); + popup.className = "menuPopup"; + popup.addEventListener( + "keydown", + e => { + function focusInternal(next, wrapSelector) { + let element = e.composedTarget; + do { + element = element[next]; + } while (element && element.tagName != "BUTTON"); + + // If we can't find next/prev button, focus the first/last one + element ??= + e.composedTarget.parentElement.querySelector(wrapSelector); + element?.focus(); + } + + function focusNext() { + focusInternal("nextElementSibling", "button"); + } + + function focusPrev() { + focusInternal("previousElementSibling", "button:last-of-type"); + } + + switch (e.code) { + case "Escape": + popup.remove(); + break; + case "Tab": + if (e.shiftKey) { + focusPrev(); + } else { + focusNext(); + } + break; + case "ArrowUp": + focusPrev(); + break; + case "ArrowDown": + focusNext(); + break; + default: + return; + } + + e.preventDefault(); + e.stopPropagation(); + }, + { capture: true } + ); + popup.addEventListener( + "blur", + e => { + if ( + e.composedTarget?.closest(".menuPopup") != + e.relatedTarget?.closest(".menuPopup") + ) { + // TODO: this triggers on macOS before "click" event. Due to this, + // we are not receiving the command. + popup.remove(); + } + }, + { capture: true } + ); + + for (const command of snapshotData.commands) { + if (command == "-") { + const separator = this.ownerDocument.createElement("div"); + separator.className = "separator"; + popup.appendChild(separator); + continue; + } + + const menuItem = this.ownerDocument.createElement("button"); + menuItem.textContent = command.label; + menuItem.addEventListener("click", e => { + this.#messageToViewModel("Command", { + snapshotId, + commandId: command.id, + }); + popup.remove(); + e.preventDefault(); + }); + popup.appendChild(menuItem); + } + + menuButton.querySelector("button").after(popup); + popup.querySelector("button")?.focus(); + } + + render() { + return html` + <link + rel="stylesheet" + href="chrome://global/content/megalist/megalist.css" + /> + <div class="container"> + <search-input + .value=${this.searchText} + .change=${e => this.#handleInputChange(e)} + > + </search-input> + <virtualized-list + .lineCount=${this.listLength} + .lineHeight=${MegalistView.LINE_HEIGHT} + .selectedIndex=${this.selectedIndex} + .createLineElement=${index => this.createLineElement(index)} + @click=${e => this.#handleClick(e)} + > + </virtualized-list> + </div> + `; + } +} + +customElements.define("megalist-view", MegalistView); diff --git a/toolkit/components/satchel/megalist/content/VirtualizedList.mjs b/toolkit/components/satchel/megalist/content/VirtualizedList.mjs new file mode 100644 index 0000000000..7903a189eb --- /dev/null +++ b/toolkit/components/satchel/megalist/content/VirtualizedList.mjs @@ -0,0 +1,136 @@ +/* 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/. */ + +/** + * Virtualized List can efficiently show billions of lines provided + * that all of them have the same height. + * + * Caller is responsible for setting createLineElement(index) function to + * create elements as they are scrolled into the view. + */ +class VirtualizedList extends HTMLElement { + lineHeight = 64; + #lineCount = 0; + + get lineCount() { + return this.#lineCount; + } + + set lineCount(value) { + this.#lineCount = value; + this.#rebuildVisibleLines(); + } + + #selectedIndex = 0; + + get selectedIndex() { + return this.#selectedIndex; + } + + set selectedIndex(value) { + this.#selectedIndex = value; + if (this.#container) { + this.updateLineSelection(true); + } + } + + #container; + + connectedCallback() { + this.#container = this.ownerDocument.createElement("ul"); + this.#container.classList.add("lines-container"); + this.appendChild(this.#container); + + this.#rebuildVisibleLines(); + this.addEventListener("scroll", () => this.#rebuildVisibleLines()); + } + + requestRefresh() { + this.#container.replaceChildren(); + this.#rebuildVisibleLines(); + } + + updateLineSelection(scrollIntoView) { + const lineElements = this.#container.querySelectorAll(".line"); + let selectedElement; + + for (let lineElement of lineElements) { + let isSelected = Number(lineElement.dataset.index) === this.selectedIndex; + if (isSelected) { + selectedElement = lineElement; + } + lineElement.classList.toggle("selected", isSelected); + } + + if (scrollIntoView) { + if (selectedElement) { + selectedElement.scrollIntoView({ block: "nearest" }); + } else { + let selectedTop = this.selectedIndex * this.lineHeight; + if (this.scrollTop > selectedTop) { + this.scrollTop = selectedTop; + } else { + this.scrollTop = selectedTop - this.clientHeight + this.lineHeight; + } + } + } + } + + #rebuildVisibleLines() { + if (!this.isConnected || !this.createLineElement) { + return; + } + + this.#container.style.height = `${this.lineHeight * this.lineCount}px`; + + let firstLineIndex = Math.floor(this.scrollTop / this.lineHeight); + let visibleLineCount = Math.ceil(this.clientHeight / this.lineHeight); + let lastLineIndex = firstLineIndex + visibleLineCount; + let extraLines = Math.ceil(visibleLineCount / 2); // They are present in DOM, but not visible + + firstLineIndex = Math.max(0, firstLineIndex - extraLines); + lastLineIndex = Math.min(this.lineCount, lastLineIndex + extraLines); + + let previousChild = null; + let visibleLines = new Map(); + + for (let child of Array.from(this.#container.children)) { + let index = Number(child.dataset.index); + if (index < firstLineIndex || index > lastLineIndex) { + child.remove(); + } else { + visibleLines.set(index, child); + } + } + + for (let index = firstLineIndex; index <= lastLineIndex; index++) { + let child = visibleLines.get(index); + if (!child) { + child = this.createLineElement(index); + + if (!child) { + // Friday fix :-) + //todo: figure out what was on that Friday and how can we fix it + continue; + } + + child.style.top = `${index * this.lineHeight}px`; + child.dataset.index = index; + + if (previousChild) { + previousChild.after(child); + } else if (this.#container.firstElementChild?.offsetTop > top) { + this.#container.firstElementChild.before(child); + } else { + this.#container.appendChild(child); + } + } + previousChild = child; + } + + this.updateLineSelection(false); + } +} + +customElements.define("virtualized-list", VirtualizedList); diff --git a/toolkit/components/satchel/megalist/content/megalist.css b/toolkit/components/satchel/megalist/content/megalist.css new file mode 100644 index 0000000000..b442a7b60d --- /dev/null +++ b/toolkit/components/satchel/megalist/content/megalist.css @@ -0,0 +1,208 @@ +/* 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/. */ + +/* Bug 1869845 - Styles in this file are still experimental! */ + +.container { + display: flex; + flex-direction: column; + justify-content: center; + max-height: 100vh; + + > search-input { + margin: 20px; + } +} + +virtualized-list { + position: relative; + overflow: auto; + margin: 20px; + + .lines-container { + padding-inline-start: unset; + } +} + +.line { + display: flex; + align-items: stretch; + position: absolute; + width: 100%; + user-select: none; + box-sizing: border-box; + height: 64px; + + background-color: var(--in-content-box-background-odd); + border-inline: 1px solid var(--in-content-border-color); + + color: var(--in-content-text-color); + + &.start { + border-block-start: 1px solid var(--in-content-border-color); + border-start-start-radius: 8px; + border-start-end-radius: 8px; + } + + &.end { + border-block-end: 1px solid var(--in-content-border-color); + border-end-start-radius: 8px; + border-end-end-radius: 8px; + height: 54px; + } + + > .menuButton { + position: relative; + visibility: hidden; + + > button { + border: none; + margin-inline-start: 2px; + padding: 2px; + background-color: transparent; + /* Fix: too lazy to load the svg */ + width: 32px; + color: unset; + } + + > .menuPopup { + position: absolute; + inset-inline-end: 0; + box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); + z-index: 1; + background-color: var(--in-content-table-background); + padding: 4px; + + > .separator { + border-block-start: 1px solid var(--in-content-border-color); + margin: 4px 0; + } + + > button { + text-align: start; + border-style: none; + padding: 12px; + margin-block-end: 2px; + width: 100%; + text-wrap: nowrap; + } + } + } + + > .content { + flex-grow: 1; + + > div { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding-inline-start: 10px; + + &:last-child { + padding-block-end: 10px; + } + } + + > .icon { + margin-inline-start: 4px; + width: 16px; + height: 16px; + -moz-context-properties: fill; + fill: currentColor; + } + + > .label { + color: var(--text-color-deemphasized); + padding-block: 2px 4px; + } + + > .value { + user-select: text; + + > .icon { + -moz-context-properties: fill; + fill: currentColor; + width: auto; + height: 16px; + margin-inline: 4px; + vertical-align: text-bottom; + } + + > .icon:not([src]) { + display: none; + } + + &:is(a) { + color: currentColor; + } + } + + > .stickers { + text-align: end; + margin-block-start: 2px; + + > span { + padding: 2px; + margin-inline-end: 2px; + } + + /* Hard-coded colors will be addressed in FXCM-1013 */ + > span.risk { + background-color: slateblue; + border: 1px solid darkslateblue; + color: whitesmoke; + } + + > span.warning { + background-color: firebrick; + border: 1px solid maroon; + color: whitesmoke; + } + } + + &.section { + font-size: larger; + + > .label { + display: inline-block; + margin: 0; + color: unset; + } + + > .value { + margin-inline-end: 8px; + text-align: end; + font-size: smaller; + color: var(--text-color-deemphasized); + user-select: unset; + } + } + } + + &.selected { + color: var(--in-content-item-selected-text); + background-color: var(--in-content-item-selected); + + > .menuButton { + visibility: inherit; + } + } + + &:hover { + color: var(--in-content-item-hover-text); + background-color: var(--in-content-item-hover); + + > .menuButton { + visibility: visible; + } + } +} + +.search { + padding: 8px; + border-radius: 4px; + border: 1px solid var(--in-content-border-color); + box-sizing: border-box; + width: 100%; +} diff --git a/toolkit/components/satchel/megalist/content/megalist.ftl b/toolkit/components/satchel/megalist/content/megalist.ftl new file mode 100644 index 0000000000..69d085a7c5 --- /dev/null +++ b/toolkit/components/satchel/megalist/content/megalist.ftl @@ -0,0 +1,126 @@ +# 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/. + +filter-placeholder = + .placeholder = Search Your Data + .key = F + +## Commands + +command-copy = Copy +command-reveal = Reveal +command-conceal = Conceal +command-toggle = Toggle +command-open = Open +command-delete = Remove record +command-edit = Edit +command-save = Save +command-cancel = Cancel + +## Passwords + +passwords-section-label = Passwords +passwords-disabled = Passwords are disabled + +passwords-command-create = Add Password +passwords-command-import = Import from a File… +passwords-command-export = Export Passwords… +passwords-command-remove-all = Remove All Passwords… +passwords-command-settings = Settings +passwords-command-help = Help + +passwords-import-file-picker-title = Import Passwords +passwords-import-file-picker-import-button = Import + +# A description for the .csv file format that may be shown as the file type +# filter by the operating system. +passwords-import-file-picker-csv-filter-title = + { PLATFORM() -> + [macos] CSV Document + *[other] CSV File + } +# A description for the .tsv file format that may be shown as the file type +# filter by the operating system. TSV is short for 'tab separated values'. +passwords-import-file-picker-tsv-filter-title = + { PLATFORM() -> + [macos] TSV Document + *[other] TSV File + } + +# Variables +# $count (number) - Number of passwords +passwords-count = + { $count -> + [one] { $count } password + *[other] { $count } passwords + } + +# Variables +# $count (number) - Number of filtered passwords +# $total (number) - Total number of passwords +passwords-filtered-count = + { $total -> + [one] { $count } of { $total } password + *[other] { $count } of { $total } passwords + } + +passwords-origin-label = Website address +passwords-username-label = Username +passwords-password-label = Password + +## Payments + +payments-command-create = Add Payment Method + +payments-section-label = Payment methods +payments-disabled = Payments methods are disabled + +# Variables +# $count (number) - Number of payment methods +payments-count = + { $count -> + [one] { $count } payment method + *[other] { $count } payment methods + } + +# Variables +# $count (number) - Number of filtered payment methods +# $total (number) - Total number of payment methods +payments-filtered-count = + { $total -> + [one] { $count } of { $total } payment method + *[other] { $count } of { $total } payment methods + } + +card-number-label = Card Number +card-expiration-label = Expires on +card-holder-label = Name on Card + +## Addresses + +addresses-command-create = Add Address + +addresses-section-label = Addresses +addresses-disabled = Addresses are disabled + +# Variables +# $count (number) - Number of addresses +addresses-count = + { $count -> + [one] { $count } address + *[other] { $count } addresses + } + +# Variables +# $count (number) - Number of filtered addresses +# $total (number) - Total number of addresses +addresses-filtered-count = + { $total -> + [one] { $count } of { $total } address + *[other] { $count } of { $total } addresses + } + +address-name-label = Name +address-phone-label = Phone +address-email-label = Email diff --git a/toolkit/components/satchel/megalist/content/megalist.html b/toolkit/components/satchel/megalist/content/megalist.html new file mode 100644 index 0000000000..6ff3f089fc --- /dev/null +++ b/toolkit/components/satchel/megalist/content/megalist.html @@ -0,0 +1,78 @@ +<!-- 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/. --> + +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <meta + name="viewport" + content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" + /> + <script + type="module" + src="chrome://global/content/megalist/MegalistView.mjs" + ></script> + <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" /> + <link rel="localization" href="preview/megalist.ftl" /> + </head> + + <body> + <megalist-view></megalist-view> + + <template id="lineElement"> + <li class="line"> + <div class="content"></div> + <div class="menuButton"> + <button>…</button> + </div> + </li> + </template> + + <template id="collapsedSectionTemplate"> + <div class="content section"> + <img + class="icon collapsed" + draggable="false" + src="chrome://global/skin/icons/arrow-down.svg" + /> + <h4 class="label"></h4> + </div> + </template> + + <template id="expandedSectionTemplate"> + <div class="content section"> + <img + class="icon expanded" + draggable="false" + src="chrome://global/skin/icons/arrow-up.svg" + /> + <h4 class="label"></h4> + <div class="value"></div> + </div> + </template> + + <template id="lineTemplate"> + <div class="content"> + <div class="label"></div> + <div class="value"> + <img class="icon" /> + <span></span> + </div> + <div class="stickers"></div> + </div> + </template> + + <template id="editingLineTemplate"> + <div class="content"> + <div class="label"></div> + <div class="value"> + <img class="icon" /> + <input type="text" /> + </div> + <div class="stickers"></div> + </div> + </template> + </body> +</html> diff --git a/toolkit/components/satchel/megalist/content/search-input.mjs b/toolkit/components/satchel/megalist/content/search-input.mjs new file mode 100644 index 0000000000..e30d13ef2a --- /dev/null +++ b/toolkit/components/satchel/megalist/content/search-input.mjs @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { html } from "chrome://global/content/vendor/lit.all.mjs"; +import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; + +export default class SearchInput extends MozLitElement { + static get properties() { + return { + items: { type: Array }, + change: { type: Function }, + value: { type: String }, + }; + } + + render() { + return html` <link + rel="stylesheet" + href="chrome://global/content/megalist/megalist.css" + /> + <link + rel="stylesheet" + href="chrome://global/skin/in-content/common.css" + /> + <input + class="search" + type="search" + data-l10n-id="filter-placeholder" + @input=${this.change} + .value=${this.value} + />`; + } +} + +customElements.define("search-input", SearchInput); diff --git a/toolkit/components/satchel/megalist/content/tests/chrome/chrome.toml b/toolkit/components/satchel/megalist/content/tests/chrome/chrome.toml new file mode 100644 index 0000000000..2d7fd6bccd --- /dev/null +++ b/toolkit/components/satchel/megalist/content/tests/chrome/chrome.toml @@ -0,0 +1,3 @@ +[DEFAULT] + +["test_virtualized_list.html"] diff --git a/toolkit/components/satchel/megalist/content/tests/chrome/test_virtualized_list.html b/toolkit/components/satchel/megalist/content/tests/chrome/test_virtualized_list.html new file mode 100644 index 0000000000..65ddbcc40b --- /dev/null +++ b/toolkit/components/satchel/megalist/content/tests/chrome/test_virtualized_list.html @@ -0,0 +1,125 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>VirtualizedList Tests</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <link rel="stylesheet" href="chrome://global/skin/in-content/common.css"> + <link rel="stylesheet" href="chrome://global/content/megalist/megalist.css"> + <script type="module" src="chrome://global/content/megalist/VirtualizedList.mjs"></script> +</head> +<body> + <style> + </style> +<p id="display"></p> +<div id="content"> + <virtualized-list></virtualized-list> +</div> +<pre id="test"> +<script class="testbody" type="application/javascript"> + const virtualizedList = document.querySelector("virtualized-list"); + + function dispatchScrollEvent(target, scrollY) { + target.scrollTop = scrollY; + virtualizedList.dispatchEvent(new Event('scroll')); + } + + function updateVisibleItemBoundaries(visibleItemCount, value) { + if (value > visibleItemCount.max) { + visibleItemCount.max = value; + } + } + + // Setup + virtualizedList.lineHeight = 64; + virtualizedList.lineCount = 1000; + virtualizedList.selectedIndex = 0; + virtualizedList.createLineElement = index => { + const lineElement = document.createElement("div"); + lineElement.classList.add("line"); + lineElement.textContent = `Row ${index}`; + return lineElement; + } + + virtualizedList.style.display = "block"; + virtualizedList.style.height = "300px"; + virtualizedList.style.width = "500px"; + + /** + * Tests that the virtualized list renders expected number of items + */ + + add_task(async function test_rebuildVisibleLines() { + let container = virtualizedList.querySelector(".lines-container"); + let initialLines = container.querySelectorAll(".line"); + // Get boundaries of visible item count as they are rendered. + let visibleItemsCount = { + min: initialLines.length, + max: initialLines.length, + }; + + is( + container.style.height, + `${virtualizedList.lineHeight * virtualizedList.lineCount}px`, + "VirtualizedList is correct height." + ); + + // Scroll down 800px + dispatchScrollEvent(virtualizedList, 800); + let newRenderedLines = container.querySelectorAll(".line"); + updateVisibleItemBoundaries(visibleItemsCount, newRenderedLines.length); + let firstRow = container.querySelector(".line[data-index='0']"); + ok(!firstRow, "The first row should be removed."); + + // Scroll down another 800px + dispatchScrollEvent(virtualizedList, 800); + newRenderedLines = container.querySelectorAll(".line"); + updateVisibleItemBoundaries(visibleItemsCount, newRenderedLines.length); + let thirdRow = container.querySelector(".line[data-index='2']"); + ok(!thirdRow, "The third row should be removed."); + + // Check that amount of visible lines is within boundaries. This is to + // ensure the list is keeping a range of rendered items and + // not increasing the element count in the DOM. + ok( + newRenderedLines.length >= visibleItemsCount.min && + newRenderedLines.length <= visibleItemsCount.max, + "Virtual list is removing and adding lines as needed." + ); + + // Scroll back to top + dispatchScrollEvent(virtualizedList, 0); + newRenderedLines = container.querySelectorAll(".line"); + updateVisibleItemBoundaries(visibleItemsCount, newRenderedLines.length); + firstRow = container.querySelector(".line[data-index='0']"); + thirdRow = container.querySelector(".line[data-index='2']"); + ok(firstRow, "The first row should be rendered again."); + ok(firstRow, "The third row should be rendered again."); + }); + + /** + * Tests that item selection is preserved when list is rebuilt + */ + add_task(async function test_updateLineSelection() { + let container = virtualizedList.querySelector(".lines-container"); + let selectedLine = container.querySelector(".selected"); + is(selectedLine.dataset.index, "0", "The correct line is selected"); + + // Scroll down 800px + dispatchScrollEvent(virtualizedList, 800); + selectedLine = container.querySelector(".selected"); + ok(!selectedLine, "Selected line is not rendered because it's out of view"); + is(virtualizedList.selectedIndex, 0, "Selected line is still preserved in list."); + + // Scroll back to top + dispatchScrollEvent(virtualizedList, 0); + selectedLine = container.querySelector(".selected"); + is(selectedLine.dataset.index, "0", "The same selected line is rendered."); + }); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/satchel/megalist/moz.build b/toolkit/components/satchel/megalist/moz.build new file mode 100644 index 0000000000..266281a9a8 --- /dev/null +++ b/toolkit/components/satchel/megalist/moz.build @@ -0,0 +1,20 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +MOCHITEST_CHROME_MANIFESTS += ["content/tests/chrome/chrome.toml"] + +DIRS += [ + "aggregator", +] + +EXTRA_JS_MODULES["megalist"] += [ + "MegalistViewModel.sys.mjs", +] + +FINAL_TARGET_FILES.actors += [ + "MegalistChild.sys.mjs", + "MegalistParent.sys.mjs", +] diff --git a/toolkit/components/satchel/moz.build b/toolkit/components/satchel/moz.build index 4b6d08cdbf..fc5cabecd7 100644 --- a/toolkit/components/satchel/moz.build +++ b/toolkit/components/satchel/moz.build @@ -12,8 +12,8 @@ XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.toml"] BROWSER_CHROME_MANIFESTS += ["test/browser/browser.toml"] XPIDL_SOURCES += [ - "nsIFormAutoComplete.idl", "nsIFormFillController.idl", + "nsIFormHistoryAutoComplete.idl", ] XPIDL_MODULE = "satchel" @@ -26,10 +26,16 @@ LOCAL_INCLUDES += [ "../build", ] +JAR_MANIFESTS += ["jar.mn"] + +DIRS += [ + "megalist", +] + EXTRA_JS_MODULES += [ "FillHelpers.sys.mjs", - "FormAutoComplete.sys.mjs", "FormHistory.sys.mjs", + "FormHistoryAutoComplete.sys.mjs", "FormHistoryStartup.sys.mjs", "FormScenarios.sys.mjs", "integrations/FirefoxRelay.sys.mjs", diff --git a/toolkit/components/satchel/nsFormFillController.cpp b/toolkit/components/satchel/nsFormFillController.cpp index 1bcbde08df..61d23d157c 100644 --- a/toolkit/components/satchel/nsFormFillController.cpp +++ b/toolkit/components/satchel/nsFormFillController.cpp @@ -23,7 +23,7 @@ #include "mozilla/Services.h" #include "mozilla/StaticPrefs_ui.h" #include "nsCRT.h" -#include "nsIFormAutoComplete.h" +#include "nsIFormHistoryAutoComplete.h" #include "nsString.h" #include "nsPIDOMWindow.h" #include "nsIAutoCompleteResult.h" @@ -47,12 +47,13 @@ using mozilla::LogLevel; static mozilla::LazyLogModule sLogger("satchel"); -static nsIFormAutoComplete* GetFormAutoComplete() { - static nsCOMPtr<nsIFormAutoComplete> sInstance; +static nsIFormHistoryAutoComplete* GetFormHistoryAutoComplete() { + static nsCOMPtr<nsIFormHistoryAutoComplete> sInstance; static bool sInitialized = false; if (!sInitialized) { nsresult rv; - sInstance = do_GetService("@mozilla.org/satchel/form-autocomplete;1", &rv); + sInstance = + do_GetService("@mozilla.org/satchel/form-history-autocomplete;1", &rv); if (NS_SUCCEEDED(rv)) { ClearOnShutdown(&sInstance); @@ -64,14 +65,14 @@ static nsIFormAutoComplete* GetFormAutoComplete() { NS_IMPL_CYCLE_COLLECTION(nsFormFillController, mController, mLoginManagerAC, mFocusedPopup, mPopups, mLastListener, - mLastFormAutoComplete) + mLastFormHistoryAutoComplete) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsFormFillController) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIFormFillController) NS_INTERFACE_MAP_ENTRY(nsIFormFillController) NS_INTERFACE_MAP_ENTRY(nsIAutoCompleteInput) NS_INTERFACE_MAP_ENTRY(nsIAutoCompleteSearch) - NS_INTERFACE_MAP_ENTRY(nsIFormAutoCompleteObserver) + NS_INTERFACE_MAP_ENTRY(nsIFormFillCompleteObserver) NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener) NS_INTERFACE_MAP_ENTRY(nsIObserver) NS_INTERFACE_MAP_ENTRY(nsIMutationObserver) @@ -705,13 +706,13 @@ nsFormFillController::StartSearch(const nsAString& aSearchString, MaybeObserveDataListMutations(); } - auto formAutoComplete = GetFormAutoComplete(); - NS_ENSURE_TRUE(formAutoComplete, NS_ERROR_FAILURE); + auto* formHistoryAutoComplete = GetFormHistoryAutoComplete(); + NS_ENSURE_TRUE(formHistoryAutoComplete, NS_ERROR_FAILURE); - formAutoComplete->AutoCompleteSearchAsync(aSearchParam, aSearchString, - mFocusedInput, aPreviousResult, - addDataList, this); - mLastFormAutoComplete = formAutoComplete; + formHistoryAutoComplete->AutoCompleteSearchAsync( + aSearchParam, aSearchString, mFocusedInput, aPreviousResult, + addDataList, this); + mLastFormHistoryAutoComplete = formHistoryAutoComplete; } return NS_OK; @@ -760,10 +761,10 @@ void nsFormFillController::RevalidateDataList() { NS_IMETHODIMP nsFormFillController::StopSearch() { // Make sure to stop and clear this, otherwise the controller will prevent - // mLastFormAutoComplete from being deleted. - if (mLastFormAutoComplete) { - mLastFormAutoComplete->StopAutoCompleteSearch(); - mLastFormAutoComplete = nullptr; + // mLastFormHistoryAutoComplete from being deleted. + if (mLastFormHistoryAutoComplete) { + mLastFormHistoryAutoComplete->StopAutoCompleteSearch(); + mLastFormHistoryAutoComplete = nullptr; } if (mLoginManagerAC) { @@ -773,7 +774,7 @@ nsFormFillController::StopSearch() { } //////////////////////////////////////////////////////////////////////// -//// nsIFormAutoCompleteObserver +//// nsIFormFillCompleteObserver NS_IMETHODIMP nsFormFillController::OnSearchCompletion(nsIAutoCompleteResult* aResult) { diff --git a/toolkit/components/satchel/nsFormFillController.h b/toolkit/components/satchel/nsFormFillController.h index eef6addb7a..239c293352 100644 --- a/toolkit/components/satchel/nsFormFillController.h +++ b/toolkit/components/satchel/nsFormFillController.h @@ -13,7 +13,7 @@ #include "nsIAutoCompleteController.h" #include "nsIAutoCompletePopup.h" #include "nsIDOMEventListener.h" -#include "nsIFormAutoComplete.h" +#include "nsIFormHistoryAutoComplete.h" #include "nsCOMPtr.h" #include "nsStubMutationObserver.h" #include "nsTHashMap.h" @@ -37,7 +37,7 @@ class HTMLInputElement; class nsFormFillController final : public nsIFormFillController, public nsIAutoCompleteInput, public nsIAutoCompleteSearch, - public nsIFormAutoCompleteObserver, + public nsIFormFillCompleteObserver, public nsIDOMEventListener, public nsIObserver, public nsMultiMutationObserver { @@ -46,7 +46,7 @@ class nsFormFillController final : public nsIFormFillController, NS_DECL_NSIFORMFILLCONTROLLER NS_DECL_NSIAUTOCOMPLETESEARCH NS_DECL_NSIAUTOCOMPLETEINPUT - NS_DECL_NSIFORMAUTOCOMPLETEOBSERVER + NS_DECL_NSIFORMFILLCOMPLETEOBSERVER NS_DECL_NSIDOMEVENTLISTENER NS_DECL_NSIOBSERVER NS_DECL_NSIMUTATIONOBSERVER @@ -122,7 +122,7 @@ class nsFormFillController final : public nsIFormFillController, nsCOMPtr<nsIAutoCompleteObserver> mLastListener; // This is cleared by StopSearch(). - nsCOMPtr<nsIFormAutoComplete> mLastFormAutoComplete; + nsCOMPtr<nsIFormHistoryAutoComplete> mLastFormHistoryAutoComplete; nsString mLastSearchString; nsTHashMap<nsPtrHashKey<const nsINode>, bool> mPwmgrInputs; diff --git a/toolkit/components/satchel/nsIFormFillController.idl b/toolkit/components/satchel/nsIFormFillController.idl index 25bd2d6738..24d9bf8193 100644 --- a/toolkit/components/satchel/nsIFormFillController.idl +++ b/toolkit/components/satchel/nsIFormFillController.idl @@ -5,6 +5,7 @@ #include "nsISupports.idl" interface nsIAutoCompletePopup; +interface nsIAutoCompleteResult; webidl Document; webidl Element; @@ -67,3 +68,16 @@ interface nsIFormFillController : nsISupports */ [can_run_script] void showPopup(); }; + +[scriptable, function, uuid(604419ab-55a0-4831-9eca-1b9e67cc4751)] +interface nsIFormFillCompleteObserver : nsISupports +{ + /* + * Called when a search is complete and the results are ready even if the + * result set is empty. If the search is cancelled or a new search is + * started, this is not called. + * + * @param result - The search result object + */ + [can_run_script] void onSearchCompletion(in nsIAutoCompleteResult result); +}; diff --git a/toolkit/components/satchel/nsIFormAutoComplete.idl b/toolkit/components/satchel/nsIFormHistoryAutoComplete.idl index cc40872dd3..279b09f51e 100644 --- a/toolkit/components/satchel/nsIFormAutoComplete.idl +++ b/toolkit/components/satchel/nsIFormHistoryAutoComplete.idl @@ -6,13 +6,13 @@ #include "nsISupports.idl" interface nsIAutoCompleteResult; -interface nsIFormAutoCompleteObserver; +interface nsIFormFillCompleteObserver; interface nsIPropertyBag2; webidl HTMLInputElement; [scriptable, uuid(bfd9b82b-0ab3-4b6b-9e54-aa961ff4b732)] -interface nsIFormAutoComplete: nsISupports { +interface nsIFormHistoryAutoComplete: nsISupports { /** * Generate results for a form input autocomplete menu asynchronously. */ @@ -20,8 +20,8 @@ interface nsIFormAutoComplete: nsISupports { in AString aSearchString, in HTMLInputElement aField, in nsIAutoCompleteResult aPreviousResult, - in bool aAddDatalist, - in nsIFormAutoCompleteObserver aListener); + in boolean aAddDatalist, + in nsIFormFillCompleteObserver aListener); /** * If a search is in progress, stop it. Otherwise, do nothing. This is used @@ -29,16 +29,3 @@ interface nsIFormAutoComplete: nsISupports { */ void stopAutoCompleteSearch(); }; - -[scriptable, function, uuid(604419ab-55a0-4831-9eca-1b9e67cc4751)] -interface nsIFormAutoCompleteObserver : nsISupports -{ - /* - * Called when a search is complete and the results are ready even if the - * result set is empty. If the search is cancelled or a new search is - * started, this is not called. - * - * @param result - The search result object - */ - [can_run_script] void onSearchCompletion(in nsIAutoCompleteResult result); -}; diff --git a/toolkit/components/satchel/test/test_capture_limit.html b/toolkit/components/satchel/test/test_capture_limit.html index 8591544016..61b0a98418 100644 --- a/toolkit/components/satchel/test/test_capture_limit.html +++ b/toolkit/components/satchel/test/test_capture_limit.html @@ -21,7 +21,7 @@ add_setup(async () => { }); add_task(async function captureLimit() { - // Capture no more than 100 fields per submit. See FormHistoryChild.jsm. + // Capture no more than 100 fields per submit. See FormHistoryChild.sys.mjs. const inputsCount = 100 + 2; const form = document.getElementById("form1"); for (let i = 1; i <= inputsCount; i++) { diff --git a/toolkit/components/satchel/test/unit/test_autocomplete.js b/toolkit/components/satchel/test/unit/test_autocomplete.js index 13f66eb0f2..e64d34ea50 100644 --- a/toolkit/components/satchel/test/unit/test_autocomplete.js +++ b/toolkit/components/satchel/test/unit/test_autocomplete.js @@ -39,8 +39,8 @@ function run_test() { testfile.copyTo(profileDir, "formhistory.sqlite"); - fac = Cc["@mozilla.org/satchel/form-autocomplete;1"].getService( - Ci.nsIFormAutoComplete + fac = Cc["@mozilla.org/satchel/form-history-autocomplete;1"].getService( + Ci.nsIFormHistoryAutoComplete ); timeGroupingSize = diff --git a/toolkit/components/search/AppProvidedSearchEngine.sys.mjs b/toolkit/components/search/AppProvidedSearchEngine.sys.mjs index 7401ba115c..ed815b96d1 100644 --- a/toolkit/components/search/AppProvidedSearchEngine.sys.mjs +++ b/toolkit/components/search/AppProvidedSearchEngine.sys.mjs @@ -9,6 +9,8 @@ import { EngineURL, } from "resource://gre/modules/SearchEngine.sys.mjs"; +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { @@ -16,14 +18,56 @@ ChromeUtils.defineESModuleGetters(lazy, { SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs", }); +XPCOMUtils.defineLazyServiceGetter( + lazy, + "idleService", + "@mozilla.org/widget/useridleservice;1", + "nsIUserIdleService" +); + +// After the user has been idle for 30s, we'll update icons if we need to. +const ICON_UPDATE_ON_IDLE_DELAY = 30; + /** * Handles loading application provided search engine icons from remote settings. */ class IconHandler { - #iconList = null; + /** + * The remote settings client for the search engine icons. + * + * @type {?RemoteSettingsClient} + */ #iconCollection = null; /** + * The list of icon records from the remote settings collection. + * + * @type {?object[]} + */ + #iconList = null; + + /** + * A flag that indicates if we have queued an idle observer to update icons. + * + * @type {boolean} + */ + #queuedIdle = false; + + /** + * A map of pending updates that need to be applied to the engines. This is + * keyed via record id, so that if multiple updates are queued for the same + * record, then we will only update the engine once. + * + * @type {Map<string, object>} + */ + #pendingUpdatesMap = new Map(); + + constructor() { + this.#iconCollection = lazy.RemoteSettings("search-config-icons"); + this.#iconCollection.on("sync", this._onIconListUpdated.bind(this)); + } + + /** * Returns the icon for the record that matches the engine identifier * and the preferred width. * @@ -40,14 +84,9 @@ class IconHandler { await this.#getIconList(); } - let iconRecords = this.#iconList.filter(r => { - return r.engineIdentifiers.some(i => { - if (i.endsWith("*")) { - return engineIdentifier.startsWith(i.slice(0, -1)); - } - return engineIdentifier == i; - }); - }); + let iconRecords = this.#iconList.filter(r => + this._identifierMatches(engineIdentifier, r.engineIdentifiers) + ); if (!iconRecords.length) { console.warn("No icon found for", engineIdentifier); @@ -66,28 +105,110 @@ class IconHandler { } } - let iconURL; + let iconData; try { - iconURL = await this.#iconCollection.attachments.get(iconRecord); + iconData = await this.#iconCollection.attachments.get(iconRecord); } catch (ex) { console.error(ex); - return null; } - if (!iconURL) { + if (!iconData) { console.warn("Unable to find the icon for", engineIdentifier); + // Queue an update in case we haven't downloaded it yet. + this.#pendingUpdatesMap.set(iconRecord.id, iconRecord); + this.#maybeQueueIdle(); return null; } + + if (iconData.record.last_modified != iconRecord.last_modified) { + // The icon we have stored is out of date, queue an update so that we'll + // download the new icon. + this.#pendingUpdatesMap.set(iconRecord.id, iconRecord); + this.#maybeQueueIdle(); + } return URL.createObjectURL( - new Blob([iconURL.buffer]), + new Blob([iconData.buffer]), iconRecord.attachment.mimetype ); } + QueryInterface = ChromeUtils.generateQI(["nsIObserver"]); + + /** + * Called when there is an update queued and the user has been observed to be + * idle for ICON_UPDATE_ON_IDLE_DELAY seconds. + * + * This will always download new icons (added or updated), even if there is + * no current engine that matches the identifiers. This is to ensure that we + * have pre-populated the cache if the engine is added later for this user. + * + * We do not handle deletes, as remote settings will handle the cleanup of + * removed records. We also do not expect the case where an icon is removed + * for an active engine. + * + * @param {nsISupports} subject + * The subject of the observer. + * @param {string} topic + * The topic of the observer. + */ + async observe(subject, topic) { + if (topic != "idle") { + return; + } + + this.#queuedIdle = false; + lazy.idleService.removeIdleObserver(this, ICON_UPDATE_ON_IDLE_DELAY); + + // Update the icon list, in case engines will call getIcon() again. + await this.#getIconList(); + + let appProvidedEngines = await Services.search.getAppProvidedEngines(); + for (let record of this.#pendingUpdatesMap.values()) { + let iconData; + try { + iconData = await this.#iconCollection.attachments.download(record); + } catch (ex) { + console.error("Could not download new icon", ex); + continue; + } + + for (let engine of appProvidedEngines) { + await engine.maybeUpdateIconURL( + record.engineIdentifiers, + URL.createObjectURL( + new Blob([iconData.buffer]), + record.attachment.mimetype + ) + ); + } + } + + this.#pendingUpdatesMap.clear(); + } + + /** + * Checks if the identifier matches any of the engine identifiers. + * + * @param {string} identifier + * The identifier of the engine. + * @param {string[]} engineIdentifiers + * The list of engine identifiers to match against. This can include + * wildcards at the end of strings. + * @returns {boolean} + * Returns true if the identifier matches any of the engine identifiers. + */ + _identifierMatches(identifier, engineIdentifiers) { + return engineIdentifiers.some(i => { + if (i.endsWith("*")) { + return identifier.startsWith(i.slice(0, -1)); + } + return identifier == i; + }); + } + /** * Obtains the icon list from the remote settings collection. */ async #getIconList() { - this.#iconCollection = lazy.RemoteSettings("search-config-icons"); try { this.#iconList = await this.#iconCollection.get(); } catch (ex) { @@ -98,6 +219,41 @@ class IconHandler { console.error("Failed to obtain search engine icon list records"); } } + + /** + * Called via a callback when remote settings updates the icon list. This + * stores potential updates and queues an idle observer to apply them. + * + * @param {object} payload + * The payload from the remote settings collection. + * @param {object} payload.data + * The payload data from the remote settings collection. + * @param {object[]} payload.data.created + * The list of created records. + * @param {object[]} payload.data.updated + * The list of updated records. + */ + async _onIconListUpdated({ data: { created, updated } }) { + created.forEach(record => { + this.#pendingUpdatesMap.set(record.id, record); + }); + for (let record of updated) { + if (record.new) { + this.#pendingUpdatesMap.set(record.new.id, record.new); + } + } + this.#maybeQueueIdle(); + } + + /** + * Queues an idle observer if there are pending updates. + */ + #maybeQueueIdle() { + if (this.#pendingUpdatesMap && !this.#queuedIdle) { + this.#queuedIdle = true; + lazy.idleService.addIdleObserver(this, ICON_UPDATE_ON_IDLE_DELAY); + } + } } /** @@ -113,19 +269,28 @@ export class AppProvidedSearchEngine extends SearchEngine { static iconHandler = new IconHandler(); /** - * @typedef {?Promise<string>} - * A promise for the blob URL of the icon. We save the promise to avoid - * reentrancy issues. + * A promise for the blob URL of the icon. We save the promise to avoid + * reentrancy issues. + * + * @type {?Promise<string>} */ #blobURLPromise = null; /** - * @typedef {?string} - * The identifier from the configuration. + * The identifier from the configuration. + * + * @type {?string} */ #configurationId = null; /** + * Whether or not this is a general purpose search engine. + * + * @type {boolean} + */ + #isGeneralPurposeSearchEngine = false; + + /** * @param {object} options * The options for this search engine. * @param {object} options.config @@ -231,11 +396,15 @@ export class AppProvidedSearchEngine extends SearchEngine { return true; } + /** + * Whether or not this engine is a "general" search engine, e.g. is it for + * generally searching the web, or does it have a specific purpose like + * shopping. + * + * @returns {boolean} + */ get isGeneralPurposeEngine() { - return !!( - this._extensionID && - lazy.SearchUtils.GENERAL_SEARCH_ENGINE_IDS.has(this._extensionID) - ); + return this.#isGeneralPurposeSearchEngine; } /** @@ -258,6 +427,36 @@ export class AppProvidedSearchEngine extends SearchEngine { } /** + * This will update the icon URL for the search engine if the engine + * identifier matches the given engine identifiers. + * + * @param {string[]} engineIdentifiers + * The engine identifiers to check against. + * @param {string} blobURL + * The new icon URL for the search engine. + */ + async maybeUpdateIconURL(engineIdentifiers, blobURL) { + // TODO: Bug 1875912. Once newSearchConfigEnabled has been enabled, we will + // be able to use `this.id` instead of `this.#configurationId`. At that + // point, `IconHandler._identifierMatches` can be made into a private + // function, as this if statement can be handled within `IconHandler.observe`. + if ( + !AppProvidedSearchEngine.iconHandler._identifierMatches( + this.#configurationId, + engineIdentifiers + ) + ) { + return; + } + if (this.#blobURLPromise) { + URL.revokeObjectURL(await this.#blobURLPromise); + this.#blobURLPromise = null; + } + this.#blobURLPromise = Promise.resolve(blobURL); + lazy.SearchUtils.notifyAction(this, lazy.SearchUtils.MODIFIED_TYPE.CHANGED); + } + + /** * Creates a JavaScript object that represents this engine. * * @returns {object} @@ -283,6 +482,12 @@ export class AppProvidedSearchEngine extends SearchEngine { #init(engineConfig) { this._orderHint = engineConfig.orderHint; this._telemetryId = engineConfig.identifier; + this.#isGeneralPurposeSearchEngine = + engineConfig.classification == "general"; + + if (engineConfig.charset) { + this._queryCharset = engineConfig.charset; + } if (engineConfig.telemetrySuffix) { this._telemetryId += `-${engineConfig.telemetrySuffix}`; diff --git a/toolkit/components/search/SearchEngineSelector.sys.mjs b/toolkit/components/search/SearchEngineSelector.sys.mjs index 0c9fb7cb70..acc1044ced 100644 --- a/toolkit/components/search/SearchEngineSelector.sys.mjs +++ b/toolkit/components/search/SearchEngineSelector.sys.mjs @@ -261,20 +261,24 @@ export class SearchEngineSelector { continue; } - let variants = - config.variants?.filter(variant => - this.#matchesUserEnvironment(variant, userEnv) - ) ?? []; + let variant = config.variants?.findLast(variant => + this.#matchesUserEnvironment(variant, userEnv) + ); - if (!variants.length) { + if (!variant) { continue; } + let subVariant = variant.subVariants?.findLast(subVariant => + this.#matchesUserEnvironment(subVariant, userEnv) + ); + let engine = structuredClone(config.base); engine.identifier = config.identifier; + engine = this.#deepCopyObject(engine, variant); - for (let variant of variants) { - engine = this.#deepCopyObject(engine, variant); + if (subVariant) { + engine = this.#deepCopyObject(engine, subVariant); } for (let override of this._configurationOverrides) { @@ -359,6 +363,10 @@ export class SearchEngineSelector { continue; } + if (["subVariants"].includes(key)) { + continue; + } + if (typeof source[key] == "object" && !Array.isArray(source[key])) { if (key in target) { this.#deepCopyObject(target[key], source[key]); @@ -431,14 +439,15 @@ export class SearchEngineSelector { user.version ) && this.#matchesChannel(config.environment.channels, user.channel) && - this.#matchesApplication(config.environment.applications, user.appName) + this.#matchesApplication(config.environment.applications, user.appName) && + !this.#hasDeviceType(config.environment) ); } /** * @param {string} userDistro * The distribution from the user's environment. - * @param {Array} configDistro + * @param {string[]} configDistro * An array of distributions for the particular environment in the config. * @returns {boolean} * True if the user's distribution is included in the config distribution @@ -497,7 +506,7 @@ export class SearchEngineSelector { } /** - * @param {Array} configChannels + * @param {string[]} configChannels * Release channels such as nightly, beta, release, esr. * @param {string} userChannel * The user's channel. @@ -514,7 +523,7 @@ export class SearchEngineSelector { } /** - * @param {Array} configApps + * @param {string[]} configApps * The applications such as firefox, firefox-android, firefox-ios, * focus-android, and focus-ios. * @param {string} userApp @@ -532,6 +541,21 @@ export class SearchEngineSelector { } /** + * Generally the device type option should only be used when the application + * is selected to be on an android or iOS based product. However, we support + * rejecting if this is non-empty in case of future requirements that we haven't + * predicted. + * + * @param {object} environment + * An environment section from the engine configuration. + * @returns {boolean} + * Returns true if there is a device type section and it is not empty. + */ + #hasDeviceType(environment) { + return !!environment.deviceType?.length; + } + + /** * Determines whether the region and locale constraints in the config * environment applies to a user given what region and locale they are using. * diff --git a/toolkit/components/search/SearchService.sys.mjs b/toolkit/components/search/SearchService.sys.mjs index b9de8e0bb3..a645cc05e8 100644 --- a/toolkit/components/search/SearchService.sys.mjs +++ b/toolkit/components/search/SearchService.sys.mjs @@ -173,7 +173,7 @@ class ParseSubmissionResult { /** * String containing the sought terms. This can be an empty string in case no - * terms were specified or the URL does not represent a search submission.* + * terms were specified or the URL does not represent a search submission. * * @type {string} */ @@ -967,6 +967,10 @@ export class SearchService { ); } + getAlternateDomains(domain) { + return lazy.SearchStaticData.getAlternateDomains(domain); + } + /** * This is a nsITimerCallback for the timerManager notification that is * registered for handling updates to search engines. Only OpenSearch engines @@ -991,9 +995,10 @@ export class SearchService { /** * A deferred promise that is resolved when initialization has finished. * + * Resolved when initalization has successfully finished, and rejected if it + * has failed. + * * @type {Promise} - * Resolved when initalization has successfully finished, and rejected if it - * has failed. */ #initDeferredPromise = Promise.withResolvers(); @@ -1078,10 +1083,10 @@ export class SearchService { * engine, as suggested by the configuration. * For the legacy configuration, this is the user visible name. * - * @type {object} - * * This is prefixed with _ rather than # because it is * called in a test. + * + * @type {object} */ _searchDefault = null; @@ -1113,6 +1118,16 @@ export class SearchService { #startupRemovedExtensions = new Set(); /** + * Used in #parseSubmissionMap + * + * @typedef {object} submissionMapEntry + * @property {nsISearchEngine} engine + * The search engine. + * @property {string} termsParameterName + * The search term parameter name. + */ + + /** * This map is built lazily after the available search engines change. It * allows quick parsing of an URL representing a search submission into the * search engine name and original terms. @@ -1120,12 +1135,7 @@ export class SearchService { * The keys are strings containing the domain name and lowercase path of the * engine submission, for example "www.google.com/search". * - * The values are objects with these properties: - * { - * engine: The associated nsISearchEngine. - * termsParameterName: Name of the URL parameter containing the search - * terms, for example "q". - * } + * @type {Map<string, submissionMapEntry>|null} */ #parseSubmissionMap = null; diff --git a/toolkit/components/search/SearchSettings.sys.mjs b/toolkit/components/search/SearchSettings.sys.mjs index fed0dd1808..e355316595 100644 --- a/toolkit/components/search/SearchSettings.sys.mjs +++ b/toolkit/components/search/SearchSettings.sys.mjs @@ -94,11 +94,14 @@ export class SearchSettings { #settings = null; /** - * @type {object} A deep copy of #settings. - * #cachedSettings is updated when we read the settings from disk and when - * we write settings to disk. #cachedSettings is compared with #settings - * before we do a write to disk. If there's no change to the settings - * attributes, then we don't write the settings to disk. + * #cachedSettings is updated when we read the settings from disk and when + * we write settings to disk. #cachedSettings is compared with #settings + * before we do a write to disk. If there's no change to the settings + * attributes, then we don't write the settings to disk. + * + * This is a deep copy of #settings. + * + * @type {object} */ #cachedSettings = {}; diff --git a/toolkit/components/search/SearchSuggestions.sys.mjs b/toolkit/components/search/SearchSuggestions.sys.mjs index 32cd25a0cd..4a43975576 100644 --- a/toolkit/components/search/SearchSuggestions.sys.mjs +++ b/toolkit/components/search/SearchSuggestions.sys.mjs @@ -4,8 +4,9 @@ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { - FormAutoCompleteResult: "resource://gre/modules/FormAutoComplete.sys.mjs", - FormHistoryClient: "resource://gre/modules/FormAutoComplete.sys.mjs", + FormHistoryAutoCompleteResult: + "resource://gre/modules/FormHistoryAutoComplete.sys.mjs", + FormHistoryClient: "resource://gre/modules/FormHistoryAutoComplete.sys.mjs", SearchSuggestionController: "resource://gre/modules/SearchSuggestionController.sys.mjs", @@ -29,7 +30,7 @@ class SuggestAutoComplete { /** * Notifies the front end of new results. * - * @param {FormAutoCompleteResult} result + * @param {FormHistoryAutoCompleteResult} result * Any previous form history result. * @private */ @@ -168,7 +169,7 @@ class SuggestAutoComplete { ...historyEntry, }) ); - let autoCompleteResult = new lazy.FormAutoCompleteResult( + let autoCompleteResult = new lazy.FormHistoryAutoCompleteResult( client, formHistoryEntries, this.#suggestionController.formHistoryParam, diff --git a/toolkit/components/search/SearchUtils.sys.mjs b/toolkit/components/search/SearchUtils.sys.mjs index 27c8f8ad03..1262bb28b1 100644 --- a/toolkit/components/search/SearchUtils.sys.mjs +++ b/toolkit/components/search/SearchUtils.sys.mjs @@ -434,12 +434,12 @@ XPCOMUtils.defineLazyPreferenceGetter( false ); -XPCOMUtils.defineLazyPreferenceGetter( - SearchUtils, - "newSearchConfigEnabled", - "browser.search.newSearchConfig.enabled", - false -); +ChromeUtils.defineLazyGetter(SearchUtils, "newSearchConfigEnabled", () => { + return Services.prefs.getBoolPref( + "browser.search.newSearchConfig.enabled", + false + ); +}); // Can't use defineLazyPreferenceGetter because we want the value // from the default branch diff --git a/toolkit/components/search/docs/SearchEngineConfiguration.rst b/toolkit/components/search/docs/SearchEngineConfiguration.rst index c782f9f7c3..ca6009a0ef 100644 --- a/toolkit/components/search/docs/SearchEngineConfiguration.rst +++ b/toolkit/components/search/docs/SearchEngineConfiguration.rst @@ -9,19 +9,23 @@ user's region and locale. Configuration Management ======================== -The application stores a dump of the configuration that is used for first -initialisation. Subsequent updates to the configuration are either updates to the -static dump, or they may be served via remote servers. +The configuration is delivered and managed via `remote settings`_. There are +:searchfox:`dumps <services/settings/dumps/main/>` of the configuration +that are shipped with the application, for use on first startup of a fresh profile, +or when a client has not been able to receive remote settings updates for +whatever reason. -The mechanism of delivering the settings dumps to the Search Service is -`the remote settings`_. - -Remote settings ---------------- +Remote Settings Bucket +---------------------- The remote settings bucket for the search engine configuration list is -``search-config``. The version that is currently being delivered -to clients can be `viewed live`_. +``search-config-v2``. The version that is currently being delivered +to clients can be `viewed live`_. There are additional remote settings buckets +with information for each search engine. These buckets are listed below. + +- `search-config-icons`_ is a mapping of icons to a search engine. +- `search-config-overrides-v2`_ contains information that may override engines + properties in search-config-v2. Configuration Schema ==================== @@ -30,43 +34,13 @@ The configuration format is defined via a `JSON schema`_. The search engine configuration schema is `stored in mozilla-central`_ and is uploaded to the Remote Settings server at convenient times after it changes. -An outline of the schema may be found on the `Search Configuration Schema`_ page. - -Updating Search Engine WebExtensions -==================================== - -Updates for application provided search engine WebExtensions are provided via -`Normandy`_. - -It is likely that updates for search engine WebExtensions will be -received separately to configuration updates which may or may not be directly -related. As a result several situations may occur: - - - The updated WebExtension is for an app-provided engine already in-use by - the user. - - - In this case, the search service will apply the changes to the - app-provided engine's data. - - - A WebExtension addition/update is for an app-provided engine that is not - in-use by the user, or not in the configuration. - - - In this case, the search service will ignore the WebExtension. - - If the configuration (search or user) is updated later and the - new engine is added, then the Search Service will start to use the - new engine. - - - A configuration update is received that needs a WebExtension that is - not found locally. - - - In this case, the search service will ignore the missing engine and - continue without it. - - When the WebExtension is delivered, the search engine will then be - installed and added. +An outline of the schemas may be found on the `Search Configuration Schema`_ page. -.. _the remote settings: /services/settings/index.html +.. _remote settings: /services/settings/index.html .. _JSON schema: https://json-schema.org/ .. _stored in mozilla-central: https://searchfox.org/mozilla-central/source/toolkit/components/search/schema/ .. _Search Configuration Schema: SearchConfigurationSchema.html -.. _viewed live: https://firefox.settings.services.mozilla.com/v1/buckets/main/collections/search-config/records -.. _Normandy: /toolkit/components/normandy/normandy/services.html +.. _viewed live: https://firefox.settings.services.mozilla.com/v1/buckets/main/collections/search-config-v2/records +.. _search-config-icons: https://firefox.settings.services.mozilla.com/v1/buckets/main/collections/search-config-icons/records +.. _search-config-overrides-v2: https://firefox.settings.services.mozilla.com/v1/buckets/main/collections/search-config-overrides-v2/records +.. _search-default-override-allowlist: https://firefox.settings.services.mozilla.com/v1/buckets/main/collections/search-default-override-allowlist/records diff --git a/toolkit/components/search/docs/SearchEngineConfigurationArchive.rst b/toolkit/components/search/docs/SearchEngineConfigurationArchive.rst new file mode 100644 index 0000000000..a6aba91a42 --- /dev/null +++ b/toolkit/components/search/docs/SearchEngineConfigurationArchive.rst @@ -0,0 +1,72 @@ +====================================== +Search Engine Configuration (Archived) +====================================== + +The search engine configuration is a mapping that is used to determine the +list of search engines for each user. The mapping is primarily based on the +user's region and locale. + +Configuration Management +======================== + +The application stores a dump of the configuration that is used for first +initialisation. Subsequent updates to the configuration are either updates to the +static dump, or they may be served via remote servers. + +The mechanism of delivering the settings dumps to the Search Service is +`the remote settings`_. + +Remote settings +--------------- + +The remote settings bucket for the search engine configuration list is +``search-config``. The version that is currently being delivered +to clients can be `viewed live`_. + +Configuration Schema +==================== + +The configuration format is defined via a `JSON schema`_. The search engine +configuration schema is `stored in mozilla-central`_ and is uploaded to the +Remote Settings server at convenient times after it changes. + +An outline of the schema may be found on the `Search Configuration Schema`_ page. + +Updating Search Engine WebExtensions +==================================== + +Updates for application provided search engine WebExtensions are provided via +`Normandy`_. + +It is likely that updates for search engine WebExtensions will be +received separately to configuration updates which may or may not be directly +related. As a result several situations may occur: + + - The updated WebExtension is for an app-provided engine already in-use by + the user. + + - In this case, the search service will apply the changes to the + app-provided engine's data. + + - A WebExtension addition/update is for an app-provided engine that is not + in-use by the user, or not in the configuration. + + - In this case, the search service will ignore the WebExtension. + - If the configuration (search or user) is updated later and the + new engine is added, then the Search Service will start to use the + new engine. + + - A configuration update is received that needs a WebExtension that is + not found locally. + + - In this case, the search service will ignore the missing engine and + continue without it. + - When the WebExtension is delivered, the search engine will then be + installed and added. + +.. _the remote settings: /services/settings/index.html +.. _JSON schema: https://json-schema.org/ +.. _stored in mozilla-central: https://searchfox.org/mozilla-central/source/toolkit/components/search/schema/ +.. _Search Configuration Schema: SearchConfigurationSchema.html +.. _viewed live: https://firefox.settings.services.mozilla.com/v1/buckets/main/collections/search-config/records +.. _Normandy: /toolkit/components/normandy/normandy/services.html diff --git a/toolkit/components/search/docs/index.rst b/toolkit/components/search/docs/index.rst index d8d4bc8d8f..10ae8118bf 100644 --- a/toolkit/components/search/docs/index.rst +++ b/toolkit/components/search/docs/index.rst @@ -32,6 +32,14 @@ Contents Preferences Telemetry +Contents for search-config (archived) +===================================== + +.. toctree:: + :maxdepth: 2 + + SearchEngineConfigurationArchive + API Reference ------------- diff --git a/toolkit/components/search/nsISearchService.idl b/toolkit/components/search/nsISearchService.idl index 4421c25974..b59e9a9399 100644 --- a/toolkit/components/search/nsISearchService.idl +++ b/toolkit/components/search/nsISearchService.idl @@ -290,13 +290,13 @@ interface nsISearchService : nsISupports * @return |true| if the search service is now initialized, |false| if * initialization has not been triggered yet. */ - readonly attribute bool isInitialized; + readonly attribute boolean isInitialized; /** * Determine whether initialization has been completed successfully. * */ - readonly attribute bool hasSuccessfullyInitialized; + readonly attribute boolean hasSuccessfullyInitialized; /** @@ -548,4 +548,14 @@ interface nsISearchService : nsISupports * "https://www.google.com/search?q=terms". */ nsISearchParseSubmissionResult parseSubmissionURL(in AString url); + + /** + * Returns a list of alternate domains for a given search engine domain. + * + * @param domain + * The domain of the search engine. + * @returns {Array} + * An array which contains all alternate domains. + */ + Array<ACString> getAlternateDomains(in ACString domain); }; diff --git a/toolkit/components/search/schema/search-config-v2-schema.json b/toolkit/components/search/schema/search-config-v2-schema.json index cfd0124fa3..7b335ed2a6 100644 --- a/toolkit/components/search/schema/search-config-v2-schema.json +++ b/toolkit/components/search/schema/search-config-v2-schema.json @@ -102,13 +102,12 @@ }, "applications": { "title": "Application Identifiers", - "description": "The application(s) this section applies to (default/not specified is everywhere).", + "description": "The application(s) this section applies to (default/not specified is everywhere). `firefox` relates to Firefox Desktop.", "type": "array", "items": { "type": "string", "pattern": "^[a-z-]{0,100}$", "enum": [ - "", "firefox", "firefox-android", "firefox-ios", @@ -129,12 +128,23 @@ "description": "The maximum application version this section applies to (less-than comparison).", "type": "string", "pattern": "^[0-9a-z.]{0,20}$" + }, + "deviceType": { + "title": "Device Type", + "description": "The device type(s) this section applies to. On desktop when this property is specified and non-empty, the associated section will be ignored.", + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z-]{0,100}$", + "enum": ["smartphone", "tablet"] + }, + "uniqueItems": true } } }, "partnerCode": { "title": "Partner Code", - "description": "The partner code for the engine or variant. This will be inserted into parameters which include '{pc}'", + "description": "The partner code for the engine or variant. This will be inserted into parameters which include '{partnerCode}'", "type": "string", "pattern": "^[a-zA-Z0-9-_]*$" }, @@ -191,7 +201,7 @@ }, "value": { "title": "Value", - "description": "The parameter value, this may be a static value, or additionally contain a parameter replacement, e.g. {inputEncoding}. For the partner code parameter, this field should be {pc}.", + "description": "The parameter value, this may be a static value, or additionally contain a parameter replacement, e.g. {inputEncoding}. For the partner code parameter, this field should be {partnerCode}.", "type": "string", "pattern": "^[a-zA-Z0-9-_{}]*$" }, @@ -327,7 +337,7 @@ }, "variants": { "title": "Variants", - "description": "This section describes variations of this search engine that may occur depending on the user's environment. If multiple sections match a user's environment, then all matching sections are applied cumulatively in the order in the array.", + "description": "This section describes variations of this search engine that may occur depending on the user's environment. The last variant that matches the user's environment will be applied to the engine, subvariants may also be applied.", "type": "array", "items": { "type": "object", @@ -345,18 +355,46 @@ }, "telemetrySuffix": { "title": "Telemetry Suffix", - "description": "Suffix that is appended to the search engine identifier following a dash, i.e. `<identifier>-<suffix>`. There should always be a suffix supplied if the partner code is different.", + "description": "Suffix that is appended to the search engine identifier following a dash, i.e. `<identifier>-<suffix>`. There should always be a suffix supplied if the partner code is different for a reason other than being on a different platform.", "type": "string", "pattern": "^[a-zA-Z0-9-]*$" }, "urls": { "$ref": "#/definitions/urls" + }, + "subVariants": { + "title": "Subvariants", + "description": "This section describes subvariations of this search engine that may occur depending on the user's environment. The last subvariant that matches the user's environment will be applied to the engine.", + "type": "array", + "items": { + "type": "object", + "properties": { + "environment": { + "$ref": "#/definitions/environment" + }, + "partnerCode": { + "$ref": "#/definitions/partnerCode" + }, + "optional": { + "title": "Optional", + "description": "This search engine is presented as an option that the user may enable. It is not included in the initial list of search engines. If not specified, defaults to false.", + "type": "boolean" + }, + "telemetrySuffix": { + "title": "Telemetry Suffix", + "description": "Suffix that is appended to the search engine identifier following a dash, i.e. `<identifier>-<suffix>`. There should always be a suffix supplied if the partner code is different for a reason other than being on a different platform.", + "type": "string", + "pattern": "^[a-zA-Z0-9-]*$" + }, + "urls": { + "$ref": "#/definitions/urls" + } + }, + "required": ["environment"] + } } }, - "required": ["environment"], - "dependencies": { - "partnerCode": ["telemetrySuffix"] - } + "required": ["environment"] } } }, diff --git a/toolkit/components/search/tests/xpcshell/data/search-config-v2.json b/toolkit/components/search/tests/xpcshell/data/search-config-v2.json index 569e16dfe4..bca7cc3bdf 100644 --- a/toolkit/components/search/tests/xpcshell/data/search-config-v2.json +++ b/toolkit/components/search/tests/xpcshell/data/search-config-v2.json @@ -112,6 +112,7 @@ "recordType": "engine", "identifier": "engine-resourceicon", "base": { + "classification": "general", "name": "engine-resourceicon", "urls": { "search": { @@ -151,6 +152,7 @@ "recordType": "engine", "identifier": "engine-reordered", "base": { + "classification": "general", "name": "Test search engine (Reordered)", "urls": { "search": { diff --git a/toolkit/components/search/tests/xpcshell/data1/search-config-v2.json b/toolkit/components/search/tests/xpcshell/data1/search-config-v2.json index 98bdfa26ff..ac5f7f77cd 100644 --- a/toolkit/components/search/tests/xpcshell/data1/search-config-v2.json +++ b/toolkit/components/search/tests/xpcshell/data1/search-config-v2.json @@ -9,7 +9,7 @@ "searchTermParamName": "q" } }, - "classification": "unknown" + "classification": "general" }, "variants": [{ "environment": { "allRegionsAndLocales": true } }], "identifier": "engine1", @@ -24,7 +24,7 @@ "searchTermParamName": "q" } }, - "classification": "unknown" + "classification": "general" }, "variants": [{ "environment": { "allRegionsAndLocales": true } }], "identifier": "engine2", @@ -39,7 +39,7 @@ "searchTermParamName": "q" } }, - "classification": "unknown" + "classification": "general" }, "variants": [ { @@ -58,7 +58,7 @@ "searchTermParamName": "q" } }, - "classification": "unknown" + "classification": "general" }, "variants": [ { diff --git a/toolkit/components/search/tests/xpcshell/head_search.js b/toolkit/components/search/tests/xpcshell/head_search.js index 1c0504e277..72bc90185c 100644 --- a/toolkit/components/search/tests/xpcshell/head_search.js +++ b/toolkit/components/search/tests/xpcshell/head_search.js @@ -307,6 +307,101 @@ async function setupRemoteSettings() { } /** + * Reads the specified file from the data directory and returns its contents as + * an Uint8Array. + * + * @param {string} filename + * The name of the file to read. + * @returns {Promise<Uint8Array>} + * The contents of the file in an Uint8Array. + */ +async function getFileDataBuffer(filename) { + return IOUtils.read(PathUtils.join(do_get_cwd().path, "data", filename)); +} + +/** + * Creates a mock attachment record for use in remote settings related tests. + * + * @param {object} item + * An object containing the details of the attachment. + * @param {string} item.filename + * The name of the attachmnet file in the data directory. + * @param {string[]} item.engineIdentifiers + * The engine identifiers for the attachment. + * @param {number} item.imageSize + * The size of the image. + * @param {string} [item.id] + * The ID to use for the record. If not provided, a new UUID will be generated. + * @param {number} [item.lastModified] + * The last modified time for the record. Defaults to the current time. + */ +async function mockRecordWithAttachment({ + filename, + engineIdentifiers, + imageSize, + id = Services.uuid.generateUUID().toString(), + lastModified = Date.now(), +}) { + let buffer = await getFileDataBuffer(filename); + + // Generate a hash. + let hasher = Cc["@mozilla.org/security/hash;1"].createInstance( + Ci.nsICryptoHash + ); + hasher.init(Ci.nsICryptoHash.SHA256); + hasher.update(buffer, buffer.length); + + let hash = hasher.finish(false); + hash = Array.from(hash, (_, i) => + ("0" + hash.charCodeAt(i).toString(16)).slice(-2) + ).join(""); + + let record = { + id, + engineIdentifiers, + imageSize, + attachment: { + hash, + location: `${filename}`, + filename, + size: buffer.byteLength, + mimetype: "application/json", + }, + last_modified: lastModified, + }; + + let attachment = { + record, + blob: new Blob([buffer]), + }; + + return { record, attachment }; +} + +/** + * Inserts an attachment record into the remote settings collection. + * + * @param {RemoteSettingsClient} client + * The remote settings client to use. + * @param {object} item + * An object containing the details of the attachment - see mockRecordWithAttachment. + * @param {boolean} [addAttachmentToCache] + * Whether to add the attachment file to the cache. Defaults to true. + */ +async function insertRecordIntoCollection( + client, + item, + addAttachmentToCache = true +) { + let { record, attachment } = await mockRecordWithAttachment(item); + await client.db.create(record); + if (addAttachmentToCache) { + await client.attachments.cacheImpl.set(record.id, attachment); + } + await client.db.importChanges({}, record.last_modified); +} + +/** * Helper function that sets up a server and respnds to region * fetch requests. * diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/head_searchconfig.js b/toolkit/components/search/tests/xpcshell/searchconfigs/head_searchconfig.js index 72c4d4f04f..c58ac1c25b 100644 --- a/toolkit/components/search/tests/xpcshell/searchconfigs/head_searchconfig.js +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/head_searchconfig.js @@ -115,7 +115,7 @@ class SearchConfigTest { async setup(version = "42.0") { if (SearchUtils.newSearchConfigEnabled) { updateAppInfo({ - name: "XPCShell", + name: "firefox", ID: "xpcshell@tests.mozilla.org", version, platformVersion: version, diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_qwant.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_qwant.js index 4024385729..eab28a7ba9 100644 --- a/toolkit/components/search/tests/xpcshell/searchconfigs/test_qwant.js +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_qwant.js @@ -14,6 +14,9 @@ const test = new SearchConfigTest({ { locales: ["fr"], }, + { + regions: ["be", "ch", "es", "fr", "it", "nl"], + }, ], }, details: [ @@ -28,9 +31,30 @@ const test = new SearchConfigTest({ }); add_setup(async function () { - await test.setup(); + if (SearchUtils.newSearchConfigEnabled) { + await test.setup(); + } else { + await test.setup("124.0"); + } }); add_task(async function test_searchConfig_qwant() { await test.run(); }); + +add_task( + { skip_if: () => SearchUtils.newSearchConfigEnabled }, + async function test_searchConfig_qwant_pre124() { + const version = "123.0"; + AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + version, + version + ); + // For pre-124, Qwant is not available in the extra regions. + test._config.available.included.pop(); + + await test.run(); + } +); diff --git a/toolkit/components/search/tests/xpcshell/searchconfigs/test_searchconfig_validates.js b/toolkit/components/search/tests/xpcshell/searchconfigs/test_searchconfig_validates.js index 86686b62f7..f727d60719 100644 --- a/toolkit/components/search/tests/xpcshell/searchconfigs/test_searchconfig_validates.js +++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_searchconfig_validates.js @@ -172,6 +172,32 @@ add_task( add_task( { skip_if: () => !SearchUtils.newSearchConfigEnabled }, + async function test_search_config_valid_partner_codes() { + delete SearchUtils.newSearchConfigEnabled; + SearchUtils.newSearchConfigEnabled = true; + + let selector = new SearchEngineSelector(() => {}); + + for (let entry of await selector.getEngineConfiguration()) { + if (entry.recordType == "engine") { + for (let variant of entry.variants) { + if ( + "partnerCode" in variant && + "distributions" in variant.environment + ) { + Assert.ok( + variant.telemetrySuffix, + `${entry.identifier} should have a telemetrySuffix when a distribution is specified with a partnerCode.` + ); + } + } + } + } + } +); + +add_task( + { skip_if: () => !SearchUtils.newSearchConfigEnabled }, async function test_search_config_override_validates_to_schema() { let selector = new SearchEngineSelector(() => {}); diff --git a/toolkit/components/search/tests/xpcshell/test_appProvided_engine.js b/toolkit/components/search/tests/xpcshell/test_appProvided_engine.js new file mode 100644 index 0000000000..56297a9d2c --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_appProvided_engine.js @@ -0,0 +1,182 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that ensure application provided engines have all base fields set up + * correctly from the search configuration. + */ + +"use strict"; + +let CONFIG = [ + { + identifier: "testEngine", + recordType: "engine", + base: { + aliases: ["testEngine1", "testEngine2"], + charset: "EUC-JP", + classification: "general", + name: "testEngine name", + partnerCode: "pc", + urls: { + search: { + base: "https://example.com/1", + // Method defaults to GET + params: [ + { name: "partnerCode", value: "{partnerCode}" }, + { name: "starbase", value: "Regula I" }, + { name: "experiment", value: "Genesis" }, + { + name: "accessPoint", + searchAccessPoint: { + addressbar: "addressbar", + contextmenu: "contextmenu", + homepage: "homepage", + newtab: "newtab", + searchbar: "searchbar", + }, + }, + ], + searchTermParamName: "search", + }, + suggestions: { + base: "https://example.com/2", + method: "POST", + searchTermParamName: "suggestions", + }, + trending: { + base: "https://example.com/3", + searchTermParamName: "trending", + }, + }, + }, + variants: [{ environment: { allRegionsAndLocales: true } }], + }, + { + identifier: "testOtherValuesEngine", + recordType: "engine", + base: { + classification: "unknown", + name: "testOtherValuesEngine name", + urls: { + search: { + base: "https://example.com/1", + searchTermParamName: "search", + }, + }, + }, + variants: [{ environment: { allRegionsAndLocales: true } }], + }, + { + recordType: "defaultEngines", + globalDefault: "engine_no_initial_icon", + specificDefaults: [], + }, + { + recordType: "engineOrders", + orders: [], + }, +]; + +add_setup(async function () { + await SearchTestUtils.useTestEngines("simple-engines", null, CONFIG); + await Services.search.init(); +}); + +add_task(async function test_engine_with_all_params_set() { + let engine = Services.search.getEngineById( + "testEngine@search.mozilla.orgdefault" + ); + Assert.ok(engine, "Should have found the engine"); + + Assert.equal( + engine.name, + "testEngine name", + "Should have the correct engine name" + ); + Assert.deepEqual( + engine.aliases, + ["@testEngine1", "@testEngine2"], + "Should have the correct aliases" + ); + Assert.ok( + engine.isGeneralPurposeEngine, + "Should be a general purpose engine" + ); + Assert.equal( + engine.wrappedJSObject.queryCharset, + "EUC-JP", + "Should have the correct encoding" + ); + + let submission = engine.getSubmission("test"); + Assert.equal( + submission.uri.spec, + "https://example.com/1?partnerCode=pc&starbase=Regula%20I&experiment=Genesis&accessPoint=searchbar&search=test", + "Should have the correct search URL" + ); + Assert.ok(!submission.postData, "Should not have postData for a GET url"); + + let suggestSubmission = engine.getSubmission( + "test", + SearchUtils.URL_TYPE.SUGGEST_JSON + ); + Assert.equal( + suggestSubmission.uri.spec, + "https://example.com/2", + "Should have the correct suggestion URL" + ); + Assert.equal( + suggestSubmission.postData.data.data, + "suggestions=test", + "Should have the correct postData for a POST URL" + ); + + let trendingSubmission = engine.getSubmission( + "test", + SearchUtils.URL_TYPE.TRENDING_JSON + ); + Assert.equal( + trendingSubmission.uri.spec, + "https://example.com/3?trending=test" + ); + Assert.ok(!submission.postData, "Should not have postData for a GET url"); +}); + +add_task(async function test_engine_with_some_params_set() { + let engine = Services.search.getEngineById( + "testOtherValuesEngine@search.mozilla.orgdefault" + ); + Assert.ok(engine, "Should have found the engine"); + + Assert.equal( + engine.name, + "testOtherValuesEngine name", + "Should have the correct engine name" + ); + Assert.deepEqual(engine.aliases, [], "Should have no aliases"); + Assert.ok( + !engine.isGeneralPurposeEngine, + "Should not be a general purpose engine" + ); + Assert.equal( + engine.wrappedJSObject.queryCharset, + "UTF-8", + "Should default to UTF-8 charset" + ); + Assert.equal( + engine.getSubmission("test").uri.spec, + "https://example.com/1?search=test", + "Should have the correct search URL" + ); + Assert.equal( + engine.getSubmission("test", SearchUtils.URL_TYPE.SUGGEST_JSON), + null, + "Should not have a suggestions URL" + ); + Assert.equal( + engine.getSubmission("test", SearchUtils.URL_TYPE.TRENDING_JSON), + null, + "Should not have a trending URL" + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_appProvided_icons.js b/toolkit/components/search/tests/xpcshell/test_appProvided_icons.js index e4d8033993..f6cc8b2415 100644 --- a/toolkit/components/search/tests/xpcshell/test_appProvided_icons.js +++ b/toolkit/components/search/tests/xpcshell/test_appProvided_icons.js @@ -53,7 +53,11 @@ let TESTS = [ icons: [ { filename: "remoteIcon.ico", - engineIdentifiers: ["engine_non_default_sized_icon"], + engineIdentifiers: [ + // This also tests multiple engine idenifiers works. + "enterprise_shuttle", + "engine_non_default_sized_icon", + ], imageSize: 32, }, ], @@ -77,64 +81,6 @@ let TESTS = [ }, ]; -async function getFileDataBuffer(filename) { - let data = await IOUtils.read( - PathUtils.join(do_get_cwd().path, "data", filename) - ); - return new TextEncoder().encode(data).buffer; -} - -async function mockRecordWithAttachment({ - filename, - engineIdentifiers, - imageSize, -}) { - let buffer = await getFileDataBuffer(filename); - - let stream = Cc["@mozilla.org/io/arraybuffer-input-stream;1"].createInstance( - Ci.nsIArrayBufferInputStream - ); - stream.setData(buffer, 0, buffer.byteLength); - - // Generate a hash. - let hasher = Cc["@mozilla.org/security/hash;1"].createInstance( - Ci.nsICryptoHash - ); - hasher.init(Ci.nsICryptoHash.SHA256); - hasher.updateFromStream(stream, -1); - let hash = hasher.finish(false); - hash = Array.from(hash, (_, i) => - ("0" + hash.charCodeAt(i).toString(16)).slice(-2) - ).join(""); - - let record = { - id: Services.uuid.generateUUID().toString(), - engineIdentifiers, - imageSize, - attachment: { - hash, - location: `main-workspace/search-config-icons/${filename}`, - filename, - size: buffer.byteLength, - mimetype: "application/json", - }, - }; - - let attachment = { - record, - blob: new Blob([buffer]), - }; - - return { record, attachment }; -} - -async function insertRecordIntoCollection(client, db, item) { - let { record, attachment } = await mockRecordWithAttachment(item); - await db.create(record); - await client.attachments.cacheImpl.set(record.id, attachment); - await db.importChanges({}, Date.now()); -} - add_setup(async function () { let client = RemoteSettings("search-config-icons"); let db = client.db; @@ -159,10 +105,7 @@ add_setup(async function () { if ("icons" in test) { for (let icon of test.icons) { - await insertRecordIntoCollection(client, db, { - ...icon, - id: test.engineId, - }); + await insertRecordIntoCollection(client, { ...icon }); } } } diff --git a/toolkit/components/search/tests/xpcshell/test_appProvided_icons_updates.js b/toolkit/components/search/tests/xpcshell/test_appProvided_icons_updates.js new file mode 100644 index 0000000000..4159b9f0fb --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_appProvided_icons_updates.js @@ -0,0 +1,324 @@ +/* Any copyright is dedicated to the Public Domain. +https://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests to ensure that icons for application provided engines are correctly + * updated from remote settings. + */ + +"use strict"; + +// A skeleton configuration that gets filled in from TESTS during `add_setup`. +let CONFIG = [ + { + identifier: "engine_no_initial_icon", + recordType: "engine", + base: { + name: "engine_no_initial_icon name", + urls: { + search: { + base: "https://example.com/1", + searchTermParamName: "q", + }, + }, + }, + variants: [{ environment: { allRegionsAndLocales: true } }], + }, + { + identifier: "engine_icon_updates", + recordType: "engine", + base: { + name: "engine_icon_updates name", + urls: { + search: { + base: "https://example.com/2", + searchTermParamName: "q", + }, + }, + }, + variants: [{ environment: { allRegionsAndLocales: true } }], + }, + { + identifier: "engine_icon_not_local", + recordType: "engine", + base: { + name: "engine_icon_not_local name", + urls: { + search: { + base: "https://example.com/3", + searchTermParamName: "q", + }, + }, + }, + variants: [{ environment: { allRegionsAndLocales: true } }], + }, + { + identifier: "engine_icon_out_of_date", + recordType: "engine", + base: { + name: "engine_icon_out_of_date name", + urls: { + search: { + base: "https://example.com/4", + searchTermParamName: "q", + }, + }, + }, + variants: [{ environment: { allRegionsAndLocales: true } }], + }, + { + recordType: "defaultEngines", + globalDefault: "engine_no_initial_icon", + specificDefaults: [], + }, + { + recordType: "engineOrders", + orders: [], + }, +]; + +async function assertIconMatches(actualIconData, expectedIcon) { + let expectedBuffer = new Uint8Array(await getFileDataBuffer(expectedIcon)); + + Assert.equal( + actualIconData.length, + expectedBuffer.length, + "Should have received matching buffer lengths for the expected icon" + ); + Assert.ok( + actualIconData.every((value, index) => value === expectedBuffer[index]), + "Should have received matching data for the expected icon" + ); +} + +async function assertEngineIcon(engineName, expectedIcon) { + let engine = Services.search.getEngineByName(engineName); + let engineIconURL = await engine.getIconURL(16); + + if (expectedIcon) { + Assert.notEqual( + engineIconURL, + null, + "Should have an icon URL for the engine." + ); + + let response = await fetch(engineIconURL); + let buffer = new Uint8Array(await response.arrayBuffer()); + + await assertIconMatches(buffer, expectedIcon); + } else { + Assert.equal( + engineIconURL, + null, + "Should not have an icon URL for the engine." + ); + } +} + +let originalIconId = Services.uuid.generateUUID().toString(); +let client; + +add_setup(async function setup() { + useHttpServer(); + SearchTestUtils.useMockIdleService(); + + client = RemoteSettings("search-config-icons"); + await client.db.clear(); + + sinon.stub(client.attachments, "_baseAttachmentsURL").returns(gDataUrl); + + // Add some initial records and attachments into the remote settings collection. + await insertRecordIntoCollection(client, { + id: originalIconId, + filename: "remoteIcon.ico", + // This uses a wildcard match to test the icon is still applied correctly. + engineIdentifiers: ["engine_icon_upd*"], + imageSize: 16, + }); + // This attachment is not cached, so we don't have it locally. + await insertRecordIntoCollection( + client, + { + id: Services.uuid.generateUUID().toString(), + filename: "bigIcon.ico", + engineIdentifiers: [ + // This also tests multiple engine idenifiers works. + "enterprise", + "next_generation", + "engine_icon_not_local", + ], + imageSize: 16, + }, + false + ); + + // Add a record that is out of date, and update it with a newer one, but don't + // cache the attachment for the new one. + let outOfDateRecordId = Services.uuid.generateUUID().toString(); + await insertRecordIntoCollection( + client, + { + id: outOfDateRecordId, + filename: "remoteIcon.ico", + engineIdentifiers: ["engine_icon_out_of_date"], + imageSize: 16, + // 10 minutes ago. + lastModified: Date.now() - 600000, + }, + true + ); + let { record } = await mockRecordWithAttachment({ + id: outOfDateRecordId, + filename: "bigIcon.ico", + engineIdentifiers: ["engine_icon_out_of_date"], + imageSize: 16, + }); + await client.db.update(record); + await client.db.importChanges({}, record.lastModified); + + await SearchTestUtils.useTestEngines("simple-engines", null, CONFIG); + await Services.search.init(); + + // Testing that an icon is not local generates a `Could not find {id}...` + // message. + consoleAllowList.push("Could not find"); +}); + +add_task(async function test_icon_added_unknown_engine() { + // If the engine is unknown, and this is a new icon, we should still download + // the icon, in case the engine is added to the configuration later. + let newIconId = Services.uuid.generateUUID().toString(); + + let mock = await mockRecordWithAttachment({ + id: newIconId, + filename: "bigIcon.ico", + engineIdentifiers: ["engine_unknown"], + imageSize: 16, + }); + await client.db.update(mock.record, Date.now()); + + await client.emit("sync", { + data: { + current: [mock.record], + created: [mock.record], + updated: [], + deleted: [], + }, + }); + + SearchTestUtils.idleService._fireObservers("idle"); + + let icon; + await TestUtils.waitForCondition(async () => { + try { + icon = await client.attachments.get(mock.record); + } catch (ex) { + // Do nothing. + } + return !!icon; + }, "Should have loaded the icon into the attachments store."); + + await assertIconMatches(new Uint8Array(icon.buffer), "bigIcon.ico"); +}); + +add_task(async function test_icon_added_existing_engine() { + // If the engine is unknown, and this is a new icon, we should still download + // it, in case the engine is added to the configuration later. + let newIconId = Services.uuid.generateUUID().toString(); + + let mock = await mockRecordWithAttachment({ + id: newIconId, + filename: "bigIcon.ico", + engineIdentifiers: ["engine_no_initial_icon"], + imageSize: 16, + }); + await client.db.update(mock.record, Date.now()); + + let promiseEngineUpdated = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + + await client.emit("sync", { + data: { + current: [mock.record], + created: [mock.record], + updated: [], + deleted: [], + }, + }); + + SearchTestUtils.idleService._fireObservers("idle"); + + await promiseEngineUpdated; + await assertEngineIcon("engine_no_initial_icon name", "bigIcon.ico"); +}); + +add_task(async function test_icon_updated() { + // Test that when an update for an engine icon is received, the engine is + // correctly updated. + + // Check the engine has the expected icon to start with. + await assertEngineIcon("engine_icon_updates name", "remoteIcon.ico"); + + // Update the icon for the engine. + let mock = await mockRecordWithAttachment({ + id: originalIconId, + filename: "bigIcon.ico", + engineIdentifiers: ["engine_icon_upd*"], + imageSize: 16, + }); + await client.db.update(mock.record, Date.now()); + + let promiseEngineUpdated = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + + await client.emit("sync", { + data: { + current: [mock.record], + created: [], + updated: [{ new: mock.record }], + deleted: [], + }, + }); + SearchTestUtils.idleService._fireObservers("idle"); + + await promiseEngineUpdated; + await assertEngineIcon("engine_icon_updates name", "bigIcon.ico"); +}); + +add_task(async function test_icon_not_local() { + // Tests that a download is queued and triggered when the icon for an engine + // is not in either the local dump nor the cache. + + await assertEngineIcon("engine_icon_not_local name", null); + + // A download should have been queued, so fire idle to trigger it. + let promiseEngineUpdated = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + SearchTestUtils.idleService._fireObservers("idle"); + await promiseEngineUpdated; + + await assertEngineIcon("engine_icon_not_local name", "bigIcon.ico"); +}); + +add_task(async function test_icon_out_of_date() { + // Tests that a download is queued and triggered when the icon for an engine + // is not in either the local dump nor the cache. + + await assertEngineIcon("engine_icon_out_of_date name", "remoteIcon.ico"); + + // A download should have been queued, so fire idle to trigger it. + let promiseEngineUpdated = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + SearchTestUtils.idleService._fireObservers("idle"); + await promiseEngineUpdated; + + await assertEngineIcon("engine_icon_out_of_date name", "bigIcon.ico"); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_defaultEngine_fallback.js b/toolkit/components/search/tests/xpcshell/test_defaultEngine_fallback.js index 12cb6568e7..2f269cc016 100644 --- a/toolkit/components/search/tests/xpcshell/test_defaultEngine_fallback.js +++ b/toolkit/components/search/tests/xpcshell/test_defaultEngine_fallback.js @@ -17,6 +17,15 @@ let appDefault; let appPrivateDefault; +async function getSearchConfig() { + let workDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + let configFileName = + "file://" + PathUtils.join(workDir.path, "data", "search-config-v2.json"); + + let response = await fetch(configFileName); + return response.json(); +} + add_setup(async function () { useHttpServer(); await SearchTestUtils.useTestEngines(); @@ -292,10 +301,33 @@ add_task(async function test_default_fallback_remove_default_no_visible() { add_task( async function test_default_fallback_remove_default_no_visible_or_general() { - // Reset. Services.search.restoreDefaultEngines(); - Services.search.defaultEngine = Services.search.defaultPrivateEngine = - appPrivateDefault; + + // For this test, we need to change any general search engines to unknown, + // so that we can test what happens in the unlikely event that there are no + // general search engines. + if (SearchUtils.newSearchConfigEnabled) { + let searchConfig = await getSearchConfig(); + for (let entry of searchConfig.data) { + if ( + entry.recordType == "engine" && + entry.base.classification == "general" + ) { + entry.base.classification = "unknown"; + } + } + const settings = await RemoteSettings(SearchUtils.SETTINGS_KEY); + settings.get.returns(searchConfig.data); + Services.search.wrappedJSObject.reset(); + await Services.search.init(); + + appPrivateDefault = await Services.search.getDefaultPrivate(); + + Services.search.defaultEngine = appPrivateDefault; + } else { + Services.search.defaultEngine = Services.search.defaultPrivateEngine = + appPrivateDefault; + } // Remove all but the default engine. let visibleEngines = await Services.search.getVisibleEngines(); @@ -310,7 +342,9 @@ add_task( "Should only have one visible engine" ); - SearchUtils.GENERAL_SEARCH_ENGINE_IDS.clear(); + if (!SearchUtils.newSearchConfigEnabled) { + SearchUtils.GENERAL_SEARCH_ENGINE_IDS.clear(); + } const observer = new SearchObserver( [ diff --git a/toolkit/components/search/tests/xpcshell/test_engine_selector_environment.js b/toolkit/components/search/tests/xpcshell/test_engine_selector_environment.js index bf56984cde..401392b955 100644 --- a/toolkit/components/search/tests/xpcshell/test_engine_selector_environment.js +++ b/toolkit/components/search/tests/xpcshell/test_engine_selector_environment.js @@ -388,6 +388,55 @@ const CONFIG_VERSIONS = [ }, ]; +const CONFIG_DEVICE_TYPE_LAYOUT = [ + { + recordType: "engine", + identifier: "engine-no-device-type", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + }, + }, + ], + }, + { + recordType: "engine", + identifier: "engine-single-device-type", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + deviceType: ["tablet"], + }, + }, + ], + }, + { + recordType: "engine", + identifier: "engine-multiple-device-type", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + deviceType: ["tablet", "smartphone"], + }, + }, + ], + }, + { + recordType: "defaultEngines", + specificDefaults: [], + }, + { + recordType: "engineOrders", + orders: [], + }, +]; + const engineSelector = new SearchEngineSelector(); let settings; let settingOverrides; @@ -793,3 +842,15 @@ add_task(async function test_engine_selector_does_not_match_optional_engines() { "Should match engines where optional flag is false or undefined" ); }); + +add_task(async function test_engine_selector_match_device_type() { + await assertActualEnginesEqualsExpected( + CONFIG_DEVICE_TYPE_LAYOUT, + { + locale: "en-CA", + region: "CA", + }, + ["engine-no-device-type"], + "Should only match engines with no device type." + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_engine_selector_subvariants.js b/toolkit/components/search/tests/xpcshell/test_engine_selector_subvariants.js new file mode 100644 index 0000000000..ed61362ca6 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_engine_selector_subvariants.js @@ -0,0 +1,142 @@ +/* Any copyright is dedicated to the Public Domain. +https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const CONFIG = [ + { + recordType: "engine", + identifier: "engine-1", + base: {}, + variants: [ + { + environment: { + allRegionsAndLocales: true, + }, + partnerCode: "variant-partner-code", + subVariants: [ + { + environment: { regions: ["CA", "FR"] }, + telemetrySuffix: "subvariant-telemetry", + }, + { + environment: { regions: ["GB", "FR"] }, + partnerCode: "subvariant-partner-code", + }, + ], + }, + ], + }, + { + recordType: "defaultEngines", + specificDefaults: [], + }, + { + recordType: "engineOrders", + orders: [], + }, +]; + +const engineSelector = new SearchEngineSelector(); +let settings; +let configStub; + +/** + * This function asserts if the actual engines returned equals the expected + * engines. + * + * @param {object} config + * A fake search config containing engines. + * @param {object} userEnv + * A fake user's environment including locale and region, experiment, etc. + * @param {Array} expectedEngines + * The array of expected engines to be returned from the fake config. + * @param {string} message + * The assertion message. + */ +async function assertActualEnginesEqualsExpected( + config, + userEnv, + expectedEngines, + message +) { + engineSelector._configuration = null; + configStub.returns(config); + let { engines } = await engineSelector.fetchEngineConfiguration(userEnv); + + Assert.deepEqual(engines, expectedEngines, message); +} + +add_setup(async function () { + settings = await RemoteSettings(SearchUtils.NEW_SETTINGS_KEY); + configStub = sinon.stub(settings, "get"); +}); + +add_task(async function test_no_subvariants_match() { + await assertActualEnginesEqualsExpected( + CONFIG, + { + locale: "fi", + region: "FI", + }, + [ + { + identifier: "engine-1", + partnerCode: "variant-partner-code", + }, + ], + "Should match no subvariants." + ); +}); + +add_task(async function test_matching_subvariant_with_properties() { + await assertActualEnginesEqualsExpected( + CONFIG, + { + locale: "en-GB", + region: "GB", + }, + [ + { + identifier: "engine-1", + partnerCode: "subvariant-partner-code", + }, + ], + "Should match subvariant with subvariant properties." + ); +}); + +add_task(async function test_matching_variant_and_subvariant_with_properties() { + await assertActualEnginesEqualsExpected( + CONFIG, + { + locale: "en-CA", + region: "CA", + }, + [ + { + identifier: "engine-1", + partnerCode: "variant-partner-code", + telemetrySuffix: "subvariant-telemetry", + }, + ], + "Should match subvariant with subvariant properties." + ); +}); + +add_task(async function test_matching_two_subvariant_with_properties() { + await assertActualEnginesEqualsExpected( + CONFIG, + { + locale: "fr", + region: "FR", + }, + [ + { + identifier: "engine-1", + partnerCode: "subvariant-partner-code", + }, + ], + "Should match the last subvariant with subvariant properties." + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_engine_selector_variants.js b/toolkit/components/search/tests/xpcshell/test_engine_selector_variants.js index 0fd57f2094..51a7f0de09 100644 --- a/toolkit/components/search/tests/xpcshell/test_engine_selector_variants.js +++ b/toolkit/components/search/tests/xpcshell/test_engine_selector_variants.js @@ -122,7 +122,7 @@ add_task(async function test_no_variants_match() { ); }); -add_task(async function test_match_and_apply_all_variants() { +add_task(async function test_match_and_apply_last_variants() { await assertActualEnginesEqualsExpected( CONFIG, { @@ -133,11 +133,10 @@ add_task(async function test_match_and_apply_all_variants() { { identifier: "engine-1", urls: { search: { params: [{ name: "partner-code", value: "foo" }] } }, - telemetrySuffix: "telemetry", searchTermParamName: "search-param", }, ], - "Should match all variants and apply each variant property cumulatively." + "Should match and apply last variant." ); }); diff --git a/toolkit/components/search/tests/xpcshell/test_searchSuggest.js b/toolkit/components/search/tests/xpcshell/test_searchSuggest.js index 4a30eb741a..d24970534f 100644 --- a/toolkit/components/search/tests/xpcshell/test_searchSuggest.js +++ b/toolkit/components/search/tests/xpcshell/test_searchSuggest.js @@ -3,7 +3,7 @@ /* eslint-disable mozilla/no-arbitrary-setTimeout */ /** - * Testing search suggestions from SearchSuggestionController.jsm. + * Testing search suggestions from SearchSuggestionController.sys.mjs. */ "use strict"; @@ -23,7 +23,7 @@ const SEARCH_TELEMETRY_LATENCY = "SEARCH_SUGGESTIONS_LATENCY_MS"; // We must make sure the FormHistoryStartup component is // initialized in order for it to respond to FormHistory -// requests from nsFormAutoComplete.js. +// requests from FormHistoryAutoComplete.sys.mjs. var formHistoryStartup = Cc[ "@mozilla.org/satchel/form-history-startup;1" ].getService(Ci.nsIObserver); diff --git a/toolkit/components/search/tests/xpcshell/test_searchSuggest_cookies.js b/toolkit/components/search/tests/xpcshell/test_searchSuggest_cookies.js index 042c74d86a..bb4dda9e3f 100644 --- a/toolkit/components/search/tests/xpcshell/test_searchSuggest_cookies.js +++ b/toolkit/components/search/tests/xpcshell/test_searchSuggest_cookies.js @@ -2,7 +2,7 @@ * http://creativecommons.org/publicdomain/zero/1.0/ */ /** - * Test that search suggestions from SearchSuggestionController.jsm don't store + * Test that search suggestions from SearchSuggestionController.sys.mjs don't store * cookies. */ @@ -14,7 +14,7 @@ const { SearchSuggestionController } = ChromeUtils.importESModule( // We must make sure the FormHistoryStartup component is // initialized in order for it to respond to FormHistory -// requests from nsFormAutoComplete.js. +// requests from FormHistoryAutoComplete.sys.mjs. var formHistoryStartup = Cc[ "@mozilla.org/satchel/form-history-startup;1" ].getService(Ci.nsIObserver); diff --git a/toolkit/components/search/tests/xpcshell/test_searchSuggest_private.js b/toolkit/components/search/tests/xpcshell/test_searchSuggest_private.js index 063f3ada49..b7750c0f30 100644 --- a/toolkit/components/search/tests/xpcshell/test_searchSuggest_private.js +++ b/toolkit/components/search/tests/xpcshell/test_searchSuggest_private.js @@ -2,7 +2,7 @@ * http://creativecommons.org/publicdomain/zero/1.0/ */ /** - * Test that search suggestions from SearchSuggestionController.jsm operate + * Test that search suggestions from SearchSuggestionController.sys.mjs operate * correctly in private mode. */ diff --git a/toolkit/components/search/tests/xpcshell/test_search_config_v2_nimbus.js b/toolkit/components/search/tests/xpcshell/test_search_config_v2_nimbus.js new file mode 100644 index 0000000000..56746a614f --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_search_config_v2_nimbus.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. +https://creativecommons.org/publicdomain/zero/1.0/ */ + +/* Test to verify search-config-v2 preference is correctly toggled via a Nimbus + variable. */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + SearchTestUtils: "resource://testing-common/SearchTestUtils.sys.mjs", + ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs", + ExperimentFakes: "resource://testing-common/NimbusTestUtils.sys.mjs", + ExperimentManager: "resource://nimbus/lib/ExperimentManager.sys.mjs", + AppProvidedSearchEngine: + "resource://gre/modules/AppProvidedSearchEngine.sys.mjs", +}); + +add_task(async function test_nimbus_experiment_enabled() { + Assert.equal( + Services.prefs.getBoolPref("browser.search.newSearchConfig.enabled"), + false, + "newSearchConfig.enabled PREF should initially be false." + ); + + await ExperimentManager.onStartup(); + await ExperimentAPI.ready(); + + let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig( + { + featureId: "search", + value: { + newSearchConfigEnabled: true, + }, + }, + { isRollout: true } + ); + + Assert.equal( + Services.prefs.getBoolPref("browser.search.newSearchConfig.enabled"), + true, + "After toggling the Nimbus variable, the current value of newSearchConfig.enabled PREF should be true." + ); + + Assert.equal( + SearchUtils.newSearchConfigEnabled, + true, + "After toggling the Nimbus variable, newSearchConfig.enabled should be cached as true." + ); + + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); + await SearchTestUtils.useTestEngines(); + + let { engines: engines2 } = + await Services.search.wrappedJSObject._fetchEngineSelectorEngines(); + + Assert.ok( + engines2.some(engine => engine.identifier), + "Engines in the search-config-v2 format should have an identifier." + ); + + let appProvidedEngines = + await Services.search.wrappedJSObject.getAppProvidedEngines(); + + Assert.ok( + appProvidedEngines.every( + engine => engine instanceof AppProvidedSearchEngine + ), + "All application provided engines for search-config-v2 should be instances of AppProvidedSearchEngine." + ); + + await doExperimentCleanup(); + + Assert.equal( + Services.prefs.getBoolPref("browser.search.newSearchConfig.enabled"), + false, + "After experiment unenrollment, the newSearchConfig.enabled should be false." + ); + + Assert.equal( + SearchUtils.newSearchConfigEnabled, + true, + "After experiment unenrollment, newSearchConfig.enabled should be cached as true." + ); +}); diff --git a/toolkit/components/search/tests/xpcshell/test_settings_persist.js b/toolkit/components/search/tests/xpcshell/test_settings_persist.js index 5c2cbd85c4..e3310a1fa2 100644 --- a/toolkit/components/search/tests/xpcshell/test_settings_persist.js +++ b/toolkit/components/search/tests/xpcshell/test_settings_persist.js @@ -81,7 +81,7 @@ add_setup(async function () { registerCleanupFunction(AddonTestUtils.promiseShutdownManager); await AddonTestUtils.promiseStartupManager(); // This is only needed as otherwise events will not be properly notified - // due to https://searchfox.org/mozilla-central/source/toolkit/components/search/SearchUtils.jsm#186 + // due to https://searchfox.org/mozilla-central/rev/5f0a7ca8968ac5cef8846e1d970ef178b8b76dcc/toolkit/components/search/SearchSettings.sys.mjs#41-42 let settingsFileWritten = promiseAfterSettings(); await Services.search.init(false); Services.search.wrappedJSObject._removeObservers(); diff --git a/toolkit/components/search/tests/xpcshell/xpcshell.toml b/toolkit/components/search/tests/xpcshell/xpcshell.toml index 7dd023cbec..899ac2d711 100644 --- a/toolkit/components/search/tests/xpcshell/xpcshell.toml +++ b/toolkit/components/search/tests/xpcshell/xpcshell.toml @@ -78,9 +78,18 @@ support-files = [ ["test_appDefaultEngine.js"] +["test_appProvided_engine.js"] +prefs = ["browser.search.newSearchConfig.enabled=true"] +support-files = [ + "../../schema/search-config-v2-schema.json", +] + ["test_appProvided_icons.js"] prefs = ["browser.search.newSearchConfig.enabled=true"] +["test_appProvided_icons_updates.js"] +prefs = ["browser.search.newSearchConfig.enabled=true"] + ["test_async.js"] ["test_config_engine_params.js"] @@ -128,6 +137,8 @@ tags = "remotesettings searchmain" ["test_engine_selector_environment.js"] +["test_engine_selector_subvariants.js"] + ["test_engine_selector_variants.js"] ["test_engine_set_alias.js"] @@ -254,6 +265,10 @@ support-files = [ ["test_searchUrlDomain.js"] +["test_search_config_v2_nimbus.js"] +prefs = ["browser.search.newSearchConfig.enabled=false"] +skip-if = ["appname == 'thunderbird'"] # Test relies on normandy. + ["test_selectedEngine.js"] ["test_sendSubmissionURL.js"] diff --git a/toolkit/components/shopping/content/ShoppingProduct.mjs b/toolkit/components/shopping/content/ShoppingProduct.mjs index d457ac1579..f3890a0d74 100644 --- a/toolkit/components/shopping/content/ShoppingProduct.mjs +++ b/toolkit/components/shopping/content/ShoppingProduct.mjs @@ -552,7 +552,7 @@ export class ShoppingProduct extends EventEmitter { false ); } - let abortHandler = e => { + let abortHandler = () => { Glean?.shoppingProduct?.requestAborted.record(); obliviousHttpChannel.cancel(Cr.NS_BINDING_ABORTED); }; diff --git a/toolkit/components/shopping/test/browser/browser_shopping_ads_test.js b/toolkit/components/shopping/test/browser/browser_shopping_ads_test.js index 1061cf7fa6..05426a08ff 100644 --- a/toolkit/components/shopping/test/browser/browser_shopping_ads_test.js +++ b/toolkit/components/shopping/test/browser/browser_shopping_ads_test.js @@ -16,7 +16,7 @@ function recommendedAdsEventListener(eventName, sidebar) { content.document.querySelector("shopping-container").wrappedJSObject; let adEl = shoppingContainer.recommendedAdEl; return ContentTaskUtils.waitForEvent(adEl, name, false, null, true).then( - ev => null + () => null ); } ); diff --git a/toolkit/components/shopping/test/browser/browser_shopping_sidebar_messages.js b/toolkit/components/shopping/test/browser/browser_shopping_sidebar_messages.js index 969b49481d..8cab6b49d8 100644 --- a/toolkit/components/shopping/test/browser/browser_shopping_sidebar_messages.js +++ b/toolkit/components/shopping/test/browser/browser_shopping_sidebar_messages.js @@ -33,7 +33,7 @@ add_task(async function test_sidebar_error() { await SpecialPowers.spawn( sidebar.querySelector("browser"), [], - async prodInfo => { + async () => { let doc = content.document; let shoppingContainer = doc.querySelector("shopping-container").wrappedJSObject; @@ -79,7 +79,7 @@ add_task(async function test_sidebar_analysis_status_page_not_supported() { await SpecialPowers.spawn( sidebar.querySelector("browser"), [], - async prodInfo => { + async () => { let doc = content.document; let shoppingContainer = doc.querySelector("shopping-container").wrappedJSObject; @@ -125,7 +125,7 @@ add_task(async function test_sidebar_analysis_status_unprocessable() { await SpecialPowers.spawn( sidebar.querySelector("browser"), [], - async prodInfo => { + async () => { let doc = content.document; let shoppingContainer = doc.querySelector("shopping-container").wrappedJSObject; @@ -174,7 +174,7 @@ add_task(async function test_sidebar_analysis_status_not_enough_reviews() { await SpecialPowers.spawn( sidebar.querySelector("browser"), [], - async prodInfo => { + async () => { let doc = content.document; let shoppingContainer = doc.querySelector("shopping-container").wrappedJSObject; diff --git a/toolkit/components/shopping/test/browser/head.js b/toolkit/components/shopping/test/browser/head.js index af676bbc33..f2b303ce12 100644 --- a/toolkit/components/shopping/test/browser/head.js +++ b/toolkit/components/shopping/test/browser/head.js @@ -46,7 +46,7 @@ async function promiseSidebarUpdated(sidebar, expectedProduct) { return !!e.detail.data && isProductCurrent(); }, true - ).then(e => true); + ).then(() => true); }); } @@ -65,7 +65,7 @@ async function promiseSidebarAdsUpdated(sidebar, expectedProduct) { true, null, true - ).then(e => true); + ).then(() => true); }); } diff --git a/toolkit/components/startup/public/nsIAppStartup.idl b/toolkit/components/startup/public/nsIAppStartup.idl index dec948b503..e8b407faeb 100644 --- a/toolkit/components/startup/public/nsIAppStartup.idl +++ b/toolkit/components/startup/public/nsIAppStartup.idl @@ -81,7 +81,7 @@ interface nsIAppStartup : nsISupports * was called (and therefore the application crashed). * @return whether safe mode is necessary */ - bool trackStartupCrashBegin(); + boolean trackStartupCrashBegin(); /** * We have succesfully started without crashing. Clear flags that were @@ -149,7 +149,7 @@ interface nsIAppStartup : nsISupports * of a hidden window or if the user disallowed a window * to be closed. */ - bool quit(in uint32_t aMode, [optional] in int32_t aExitCode); + boolean quit(in uint32_t aMode, [optional] in int32_t aExitCode); /** * These values must match the xpcom/base/ShutdownPhase.h values. @@ -190,7 +190,7 @@ interface nsIAppStartup : nsISupports * * @return true if we are in or beyond the given phase. */ - bool isInOrBeyondShutdownPhase(in nsIAppStartup_IDLShutdownPhase aPhase); + boolean isInOrBeyondShutdownPhase(in nsIAppStartup_IDLShutdownPhase aPhase); /** * True if the application is in the process of shutting down. @@ -240,7 +240,7 @@ interface nsIAppStartup : nsISupports /** * Whether or not we showed the startup skeleton UI. */ - readonly attribute bool showedPreXULSkeletonUI; + readonly attribute boolean showedPreXULSkeletonUI; /** * Returns an object with main, process, firstPaint, sessionRestored properties. diff --git a/toolkit/components/startup/tests/browser/browser_bug511456.js b/toolkit/components/startup/tests/browser/browser_bug511456.js index c080a1596f..04152a6103 100644 --- a/toolkit/components/startup/tests/browser/browser_bug511456.js +++ b/toolkit/components/startup/tests/browser/browser_bug511456.js @@ -45,7 +45,7 @@ function test() { win2.close(); // Leave the page the second time. - waitForOnBeforeUnloadDialog(browser, (btnLeave, btnStay) => { + waitForOnBeforeUnloadDialog(browser, btnLeave => { btnLeave.click(); }); diff --git a/toolkit/components/startup/tests/browser/browser_bug537449.js b/toolkit/components/startup/tests/browser/browser_bug537449.js index 1b54117a7d..10e54ea6d2 100644 --- a/toolkit/components/startup/tests/browser/browser_bug537449.js +++ b/toolkit/components/startup/tests/browser/browser_bug537449.js @@ -44,7 +44,7 @@ function test() { win2.close(); // Leave the page the second time. - waitForOnBeforeUnloadDialog(browser, (btnLeave, btnStay) => { + waitForOnBeforeUnloadDialog(browser, btnLeave => { btnLeave.click(); }); diff --git a/toolkit/components/taskscheduler/TaskSchedulerMacOSImpl.sys.mjs b/toolkit/components/taskscheduler/TaskSchedulerMacOSImpl.sys.mjs index 3cd5b5c51d..ae15738513 100644 --- a/toolkit/components/taskscheduler/TaskSchedulerMacOSImpl.sys.mjs +++ b/toolkit/components/taskscheduler/TaskSchedulerMacOSImpl.sys.mjs @@ -292,12 +292,12 @@ export var MacOSImpl = { return serializer.serializeToString(doc); }, - _formatLabelForThisApp(id, options) { + _formatLabelForThisApp(id) { let installHash = lazy.XreDirProvider.getInstallHash(); return `${AppConstants.MOZ_MACBUNDLE_ID}.${installHash}.${id}`; }, - _labelMatchesThisApp(label, options) { + _labelMatchesThisApp(label) { let installHash = lazy.XreDirProvider.getInstallHash(); return ( label && diff --git a/toolkit/components/telemetry/Events.yaml b/toolkit/components/telemetry/Events.yaml index ddba1c690d..5d3d433a05 100644 --- a/toolkit/components/telemetry/Events.yaml +++ b/toolkit/components/telemetry/Events.yaml @@ -4270,3 +4270,40 @@ screenshots: - 1801019 expiry_version: "never" release_channel_collection: opt-out + +session_restore: + backup_can_be_loaded: + objects: ["session_file"] + description: > + Recorded when a file is able to be successfully read on startup + record_in_processes: + - main + products: + - "firefox" + extra_keys: + can_load: Whether or not the startup file can be read/loaded + path_key: A symbolic name for the backup file, should be one of "clean", "recovery", "recoveryBackup", "cleanBackup", or "upgradeBackup" + loadfail_reason: Reason why the file cannot be loaded, N/A if can be loaded + notification_emails: + - "session-restore-telemetry-alerts@mozilla.com" + bug_numbers: + - 1874742 + expiry_version: "never" + release_channel_collection: opt-out + shutdown_success: + objects: ["session_startup"] + description: > + Report shutdown success + record_in_processes: + - main + products: + - "firefox" + extra_keys: + shutdown_ok: Whether or not the shutdown happened successfully, unsuccessful if previous session crashed + shutdown_reason: Reason why shutdown didn't happen successfully, N/A if previous session didn't crashed + notification_emails: + - "session-restore-telemetry-alerts@mozilla.com" + bug_numbers: + - 1874742 + expiry_version: "never" + release_channel_collection: opt-out diff --git a/toolkit/components/telemetry/Histograms.json b/toolkit/components/telemetry/Histograms.json index 3be0da3295..4f0f0e2329 100644 --- a/toolkit/components/telemetry/Histograms.json +++ b/toolkit/components/telemetry/Histograms.json @@ -794,6 +794,7 @@ "jcoppeard@mozilla.com" ], "expires_in_version": "never", + "releaseChannelCollection": "opt-out", "kind": "exponential", "high": 100, "n_buckets": 20, @@ -808,6 +809,7 @@ "jcoppeard@mozilla.com" ], "expires_in_version": "never", + "releaseChannelCollection": "opt-out", "kind": "exponential", "high": 100, "n_buckets": 20, @@ -837,6 +839,7 @@ "smaug@mozilla.com" ], "expires_in_version": "never", + "releaseChannelCollection": "opt-out", "kind": "exponential", "high": 10000, "n_buckets": 50, @@ -851,7 +854,6 @@ "jcoppeard@mozilla.com" ], "expires_in_version": "never", - "releaseChannelCollection": "opt-out", "kind": "exponential", "high": 200, "n_buckets": 50, @@ -893,7 +895,6 @@ "sdetar@mozilla.com" ], "expires_in_version": "never", - "releaseChannelCollection": "opt-out", "kind": "exponential", "high": 10000, "n_buckets": 50, @@ -932,6 +933,7 @@ "products": ["firefox"], "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], "expires_in_version": "never", + "releaseChannelCollection": "opt-out", "kind": "exponential", "high": 10000, "n_buckets": 50, @@ -943,6 +945,7 @@ "products": ["firefox"], "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], "expires_in_version": "never", + "releaseChannelCollection": "opt-out", "kind": "exponential", "high": 10000, "n_buckets": 50, @@ -954,6 +957,7 @@ "products": ["firefox"], "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], "expires_in_version": "never", + "releaseChannelCollection": "opt-out", "kind": "exponential", "high": 10000, "n_buckets": 50, @@ -968,7 +972,6 @@ "jcoppeard@mozilla.com" ], "expires_in_version": "never", - "releaseChannelCollection": "opt-out", "kind": "exponential", "high": 150000, "n_buckets": 50, @@ -983,7 +986,6 @@ "jcoppeard@mozilla.com" ], "expires_in_version": "never", - "releaseChannelCollection": "opt-out", "kind": "exponential", "high": 10000, "n_buckets": 50, @@ -998,7 +1000,6 @@ "jcoppeard@mozilla.com" ], "expires_in_version": "never", - "releaseChannelCollection": "opt-out", "kind": "exponential", "high": 10000, "n_buckets": 50, @@ -1010,6 +1011,7 @@ "products": ["firefox"], "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], "expires_in_version": "never", + "releaseChannelCollection": "opt-out", "kind": "exponential", "high": 10000, "n_buckets": 50, @@ -1044,6 +1046,7 @@ "products": ["firefox"], "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], "expires_in_version": "never", + "releaseChannelCollection": "opt-out", "kind": "linear", "high": 100, "n_buckets": 20, @@ -1113,6 +1116,7 @@ "products": ["firefox"], "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], "expires_in_version": "never", + "releaseChannelCollection": "opt-out", "kind": "exponential", "high": 1000000, "n_buckets": 100, @@ -1142,7 +1146,6 @@ "jcoppeard@mozilla.com" ], "expires_in_version": "never", - "releaseChannelCollection": "opt-out", "kind": "exponential", "high": 100, "n_buckets": 20, @@ -1199,7 +1202,6 @@ "kind": "linear", "high": 100, "n_buckets": 50, - "releaseChannelCollection": "opt-out", "bug_numbers": [1563755], "description": "The percentage of tenured GC things that survived a collection." }, @@ -1229,7 +1231,6 @@ "kind": "exponential", "high": 120, "n_buckets": 50, - "releaseChannelCollection": "opt-out", "bug_numbers": [1556467], "description": "Time spent in between garbage collections for the main runtime (seconds)" }, @@ -1272,7 +1273,6 @@ ], "expires_in_version": "never", "kind": "exponential", - "releaseChannelCollection": "opt-out", "low": 1, "high": 50000, "n_buckets": 100, @@ -1625,6 +1625,7 @@ "products": ["firefox", "fennec", "thunderbird"], "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"], "expires_in_version": "never", + "releaseChannelCollection": "opt-out", "kind": "exponential", "low": 1024, "high": 16777216, @@ -3643,7 +3644,7 @@ "releaseChannelCollection": "opt-out", "description": "Recorded once for each HTTP 401 response. The value records the type of authentication and the TLS-enabled status. (0=basic/clear, 1=basic/tls, 2=digest/clear, 3=digest/tls, 4=ntlm/clear, 5=ntlm/tls, 6=negotiate/clear, 7=negotiate/tls)" }, - "HTTP_CHILD_OMT_STATS": { + "HTTP_CHILD_OMT_STATS_2": { "record_in_processes": ["content"], "products": ["firefox", "fennec"], "alert_emails": ["necko@mozilla.com"], @@ -3657,7 +3658,8 @@ "successMainThread", "failListener", "failListenerChain", - "notRequested" + "notRequested", + "successOnlyDecomp" ] }, "TLS_EARLY_DATA_NEGOTIATED": { @@ -4814,7 +4816,7 @@ "expires_in_version": "never", "kind": "enumerated", "n_values": 6, - "description": "encoding removed: 0=unknown, 1=gzip, 2=deflate, 3=brotli" + "description": "encoding removed: 0=unknown, 1=gzip, 2=deflate, 3=brotli, 4=zstd" }, "CACHE_LM_INCONSISTENT": { "record_in_processes": ["main", "content"], @@ -8742,6 +8744,7 @@ "record_in_processes": ["main", "content"], "products": ["firefox", "fennec"], "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"], + "releaseChannelCollection": "opt-out", "expires_in_version": "default", "kind": "boolean", "description": "Session restore: Whether none of the backup files contained parse-able JSON" @@ -8816,17 +8819,6 @@ "n_values": 50, "description": "Session restore: Number of tabs restored eagerly in the session that has just been restored." }, - "FX_SESSION_RESTORE_CLOSED_TABS_NOT_SAVED": { - "record_in_processes": ["main", "content"], - "products": ["firefox"], - "expires_in_version": "127", - "alert_emails": ["firefox-view-engineers@mozilla.com"], - "releaseChannelCollection": "opt-out", - "bug_numbers": [1848459], - "kind": "enumerated", - "n_values": 25, - "description": "Session restore: Number of closed tabs that are NOT saved due to lack of open tabs worth saving on window close." - }, "FX_TABLETMODE_PAGE_LOAD": { "record_in_processes": ["main", "content"], "products": ["firefox", "fennec"], @@ -13182,83 +13174,6 @@ "alert_emails": ["perf-telemetry-alerts@mozilla.com"], "description": "Scaling percentage for the display where the first window is opened" }, - "SHUTDOWN_PHASE_DURATION_TICKS_QUIT_APPLICATION": { - "record_in_processes": ["main", "content"], - "products": ["firefox", "fennec", "thunderbird"], - "expires_in_version": "never", - "kind": "exponential", - "high": 65, - "n_buckets": 10, - "bug_numbers": [1689953], - "alert_emails": ["dothayer@mozilla.com, jstutte@mozilla.com"], - "description": "Duration of shutdown phase quit-application, as measured by the shutdown terminator in ticks of 100ms" - }, - "SHUTDOWN_PHASE_DURATION_TICKS_PROFILE_CHANGE_NET_TEARDOWN": { - "record_in_processes": ["main", "content"], - "products": ["firefox", "fennec", "thunderbird"], - "expires_in_version": "never", - "kind": "exponential", - "high": 65, - "n_buckets": 10, - "bug_numbers": [1689953], - "alert_emails": ["dothayer@mozilla.com, jstutte@mozilla.com"], - "description": "Duration of shutdown phase profile-change-net-teardown, as measured by the shutdown terminator in ticks of 100ms" - }, - "SHUTDOWN_PHASE_DURATION_TICKS_PROFILE_CHANGE_TEARDOWN": { - "record_in_processes": ["main", "content"], - "products": ["firefox", "fennec", "thunderbird"], - "expires_in_version": "never", - "kind": "exponential", - "high": 65, - "n_buckets": 10, - "bug_numbers": [1689953], - "alert_emails": ["dothayer@mozilla.com, jstutte@mozilla.com"], - "description": "Duration of shutdown phase profile-change-teardown, as measured by the shutdown terminator in ticks of 100ms" - }, - "SHUTDOWN_PHASE_DURATION_TICKS_PROFILE_BEFORE_CHANGE": { - "record_in_processes": ["main", "content"], - "products": ["firefox", "fennec", "thunderbird"], - "expires_in_version": "never", - "kind": "exponential", - "high": 65, - "n_buckets": 10, - "bug_numbers": [1689953], - "alert_emails": ["dothayer@mozilla.com, jstutte@mozilla.com"], - "description": "Duration of shutdown phase profile-before-change, as measured by the shutdown terminator in ticks of 100ms" - }, - "SHUTDOWN_PHASE_DURATION_TICKS_PROFILE_BEFORE_CHANGE_QM": { - "record_in_processes": ["main", "content"], - "products": ["firefox", "fennec", "thunderbird"], - "expires_in_version": "never", - "kind": "exponential", - "high": 65, - "n_buckets": 10, - "bug_numbers": [1689953], - "alert_emails": ["dothayer@mozilla.com, jstutte@mozilla.com"], - "description": "Duration of shutdown phase profile-before-change-qm, as measured by the shutdown terminator in ticks of 100ms" - }, - "SHUTDOWN_PHASE_DURATION_TICKS_XPCOM_WILL_SHUTDOWN": { - "record_in_processes": ["main", "content"], - "products": ["firefox", "fennec", "thunderbird"], - "expires_in_version": "never", - "kind": "exponential", - "high": 65, - "n_buckets": 10, - "bug_numbers": [1689953], - "alert_emails": ["dothayer@mozilla.com, jstutte@mozilla.com"], - "description": "Duration of shutdown phase xpcom-will-shutdown, as measured by the shutdown terminator in ticks of 100ms" - }, - "SHUTDOWN_PHASE_DURATION_TICKS_XPCOM_SHUTDOWN": { - "record_in_processes": ["main", "content"], - "products": ["firefox", "fennec", "thunderbird"], - "expires_in_version": "never", - "kind": "exponential", - "high": 65, - "n_buckets": 10, - "bug_numbers": [1689953], - "alert_emails": ["dothayer@mozilla.com, jstutte@mozilla.com"], - "description": "Duration of shutdown phase xpcom-shutdown, as measured by the shutdown terminator in ticks of 100ms" - }, "TAP_TO_LOAD_ENABLED": { "record_in_processes": ["main", "content"], "products": ["firefox", "fennec"], diff --git a/toolkit/components/telemetry/Scalars.yaml b/toolkit/components/telemetry/Scalars.yaml index fee6594b81..f63b8b5fdb 100644 --- a/toolkit/components/telemetry/Scalars.yaml +++ b/toolkit/components/telemetry/Scalars.yaml @@ -172,6 +172,246 @@ browser.backup: - 'firefox' record_in_processes: - 'main' + places_size: + bug_numbers: + - 1883642 + description: > + The total file size of the places.sqlite db located in the current profile + directory, in kilobytes. + expires: never + kind: uint + notification_emails: + - mconley@mozilla.com + release_channel_collection: opt-out + products: + - 'firefox' + record_in_processes: + - 'main' + favicons_size: + bug_numbers: + - 1883642 + description: > + The total file size of the favicons.sqlite db located in the current profile + directory, in kilobytes. + expires: never + kind: uint + notification_emails: + - mconley@mozilla.com + release_channel_collection: opt-out + products: + - 'firefox' + record_in_processes: + - 'main' + credentials_data_size: + bug_numbers: + - 1883736 + description: > + The total size of logins, payment method, and form autofill related files + in the current profile directory, in kilobytes. + expires: never + kind: uint + notification_emails: + - mconley@mozilla.com + release_channel_collection: opt-out + products: + - 'firefox' + record_in_processes: + - 'main' + security_data_size: + bug_numbers: + - 1883736 + description: > + The total size of files needed for NSS initialization parameters and security + certificate settings in the current profile directory, in kilobytes. + expires: never + kind: uint + notification_emails: + - mconley@mozilla.com + release_channel_collection: opt-out + products: + - 'firefox' + record_in_processes: + - 'main' + preferences_size: + bug_numbers: + - 1883739 + description: > + The total size of files relating to user preferences and permissions in the current profile + directory, in kilobytes. + expires: never + kind: uint + notification_emails: + - mconley@mozilla.com + release_channel_collection: opt-out + products: + - 'firefox' + record_in_processes: + - 'main' + misc_data_size: + bug_numbers: + - 1883747 + - 1887746 + description: > + The total size of files for telemetry, site storage, media device origin mapping, + chrome privileged IndexedDB databases, and Mozilla Accounts in the current profile directory, + rounded to the nearest tenth kilobyte. + expires: never + kind: uint + notification_emails: + - mconley@mozilla.com + release_channel_collection: opt-out + products: + - 'firefox' + record_in_processes: + - 'main' + cookies_size: + bug_numbers: + - 1883740 + description: > + The total file size of the cookies.sqlite db located in the current profile + directory, in kilobytes. + expires: never + kind: uint + notification_emails: + - mconley@mozilla.com + release_channel_collection: opt-out + products: + - 'firefox' + record_in_processes: + - 'main' + form_history_size: + bug_numbers: + - 1883740 + description: > + The file size of the formhistory.sqlite db located in the current profile + directory, in kilobytes. + expires: never + kind: uint + notification_emails: + - mconley@mozilla.com + release_channel_collection: opt-out + products: + - 'firefox' + record_in_processes: + - 'main' + session_store_backups_directory_size: + bug_numbers: + - 1883740 + description: > + The total size of the session store backups directory, in kilobytes. + expires: never + kind: uint + notification_emails: + - mconley@mozilla.com + release_channel_collection: opt-out + products: + - 'firefox' + record_in_processes: + - 'main' + session_store_size: + bug_numbers: + - 1883740 + description: > + The size of uncompressed session store json, in kilobytes. + expires: never + kind: uint + notification_emails: + - mconley@mozilla.com + release_channel_collection: opt-out + products: + - 'firefox' + record_in_processes: + - 'main' + extensions_json_size: + bug_numbers: + - 1883655 + description: > + The total file size of the extensions json files located in the current + profile directory,rounded to the nearest 10 kilobytes. + expires: never + kind: uint + notification_emails: + - mconley@mozilla.com + release_channel_collection: opt-out + products: + - 'firefox' + record_in_processes: + - 'main' + extension_store_permissions_data_size: + bug_numbers: + - 1883655 + description: > + The file size of the current profiles extension-store-permissions/data.safe.bin + file, rounded to the nearest 10 kilobytes. + expires: never + kind: uint + notification_emails: + - mconley@mozilla.com + release_channel_collection: opt-out + products: + - 'firefox' + record_in_processes: + - 'main' + storage_sync_size: + bug_numbers: + - 1883655 + description: > + The file size of the current profiles storage-sync-v2.sqlite database, + rounded to the nearest 10 kilobytes. + expires: never + kind: uint + notification_emails: + - mconley@mozilla.com + release_channel_collection: opt-out + products: + - 'firefox' + record_in_processes: + - 'main' + browser_extension_data_size: + bug_numbers: + - 1883655 + description: > + The total size of the current profiles storage.local legacy JSON backend + in the browser-extension-data directory, rounded to the nearest 10 kilobytes. + expires: never + kind: uint + notification_emails: + - mconley@mozilla.com + release_channel_collection: opt-out + products: + - 'firefox' + record_in_processes: + - 'main' + extensions_xpi_directory_size: + bug_numbers: + - 1883655 + description: > + The total size of the current profiles extensions directory, + rounded to the nearest 10 kilobytes. + expires: never + kind: uint + notification_emails: + - mconley@mozilla.com + release_channel_collection: opt-out + products: + - 'firefox' + record_in_processes: + - 'main' + extensions_storage_size: + bug_numbers: + - 1883655 + description: > + The total size of all extensions storage directories, + rounded to the nearest 10 kilobytes. + expires: never + kind: uint + notification_emails: + - mconley@mozilla.com + release_channel_collection: opt-out + products: + - 'firefox' + record_in_processes: + - 'main' # The following section contains the browser engagement scalars. browser.engagement: @@ -1682,23 +1922,6 @@ pwmgr: record_in_processes: - main -bloburl: - resolve_stopped: - bug_numbers: - - 1843158 - description: > - Counts how many times we do not resolve a blob URL - because of different partition keys - expires: "127" - kind: uint - notification_emails: - - amadan@mozilla.com - release_channel_collection: opt-out - products: - - 'firefox' - record_in_processes: - - content - contentblocking: cryptomining_blocking_enabled: bug_numbers: diff --git a/toolkit/components/telemetry/app/TelemetryEnvironment.sys.mjs b/toolkit/components/telemetry/app/TelemetryEnvironment.sys.mjs index 76bc2579eb..18d46a3565 100644 --- a/toolkit/components/telemetry/app/TelemetryEnvironment.sys.mjs +++ b/toolkit/components/telemetry/app/TelemetryEnvironment.sys.mjs @@ -358,6 +358,10 @@ const DEFAULT_ENVIRONMENT_PREFS = new Map([ { what: RECORD_DEFAULTPREF_VALUE }, ], ["xpinstall.signatures.required", { what: RECORD_PREF_VALUE }], + [ + "xpinstall.signatures.weakSignaturesTemporarilyAllowed", + { what: RECORD_PREF_VALUE }, + ], ["nimbus.debug", { what: RECORD_PREF_VALUE }], ]); @@ -837,6 +841,7 @@ EnvironmentAddonBuilder.prototype = { hasBinaryComponents: false, installDay: Utils.millisecondsToDays(installDate.getTime()), signedState: addon.signedState, + signedTypes: JSON.stringify(addon.signedTypes), quarantineIgnoredByApp: enforceBoolean( addon.quarantineIgnoredByApp ), diff --git a/toolkit/components/telemetry/app/TelemetryUtils.sys.mjs b/toolkit/components/telemetry/app/TelemetryUtils.sys.mjs index 809460aebc..f56b8229dc 100644 --- a/toolkit/components/telemetry/app/TelemetryUtils.sys.mjs +++ b/toolkit/components/telemetry/app/TelemetryUtils.sys.mjs @@ -29,7 +29,8 @@ export var TelemetryUtils = { * * Here is an example of listening for that event: * - * const { TelemetryUtils } = ChromeUtils.import("resource://gre/modules/TelemetryUtils.jsm"); + * const { TelemetryUtils } = + * ChromeUtils.importESModule("resource://gre/modules/TelemetryUtils.sys.mjs"); * * class YourClass { * constructor() { diff --git a/toolkit/components/telemetry/core/ipc/TelemetryComms.h b/toolkit/components/telemetry/core/ipc/TelemetryComms.h index 75f59209b0..57c875dedb 100644 --- a/toolkit/components/telemetry/core/ipc/TelemetryComms.h +++ b/toolkit/components/telemetry/core/ipc/TelemetryComms.h @@ -12,6 +12,7 @@ #include "mozilla/TelemetryProcessEnums.h" #include "mozilla/TimeStamp.h" #include "mozilla/Variant.h" +#include "mozilla/dom/WebGLIpdl.h" #include "nsITelemetry.h" namespace mozilla { @@ -96,6 +97,13 @@ struct DiscardedData { uint32_t mDiscardedScalarActions; uint32_t mDiscardedKeyedScalarActions; uint32_t mDiscardedChildEvents; + + auto MutTiedFields() { + return std::tie(mDiscardedHistogramAccumulations, + mDiscardedKeyedHistogramAccumulations, + mDiscardedScalarActions, mDiscardedKeyedScalarActions, + mDiscardedChildEvents); + } }; } // namespace Telemetry @@ -393,7 +401,7 @@ struct ParamTraits<mozilla::Telemetry::EventExtraEntry> { template <> struct ParamTraits<mozilla::Telemetry::DiscardedData> - : public PlainOldDataSerializer<mozilla::Telemetry::DiscardedData> {}; + : public ParamTraits_TiedFields<mozilla::Telemetry::DiscardedData> {}; } // namespace IPC diff --git a/toolkit/components/telemetry/docs/data/environment.rst b/toolkit/components/telemetry/docs/data/environment.rst index 77d3d1ea6e..9e966fe707 100644 --- a/toolkit/components/telemetry/docs/data/environment.rst +++ b/toolkit/components/telemetry/docs/data/environment.rst @@ -282,6 +282,7 @@ Structure: installDay: <number>, // days since UNIX epoch, 0 on failure updateDay: <number>, // days since UNIX epoch, 0 on failure signedState: <integer>, // whether the add-on is signed by AMO, only present for extensions + signedTypes: <string>, // JSON-stringified array of signature types found (see nsIAppSignatureInfo's SignatureAlgorithm enum) isSystem: <bool>, // true if this is a System Add-on isWebExtension: <bool>, // true if this is a WebExtension multiprocessCompatible: <bool>, // true if this add-on does *not* require e10s shims @@ -475,6 +476,10 @@ The following is a partial list of `collected preferences <https://searchfox.org - ``intl.ime.use_composition_events_for_insert_text``: Whether a set of composition events is fired when user inserts text without keyboard events nor composing state of a composition (only on Linux and macOS). +- ``xpinstall.signatures.required``: Whether XPI files cryptographic signatures are being verified and enforced. + +- ``xpinstall.signatures.weakSignaturesTemporarilyAllowed``: Whether new XPI files only signed with weak signature algorithms are still allowed to be installed + attribution ~~~~~~~~~~~ diff --git a/toolkit/components/telemetry/tests/addons/signed-webext/.web-extension-id b/toolkit/components/telemetry/tests/addons/signed-webext/.web-extension-id deleted file mode 100644 index e78cecf6d6..0000000000 --- a/toolkit/components/telemetry/tests/addons/signed-webext/.web-extension-id +++ /dev/null @@ -1,3 +0,0 @@ -# This file was created by https://github.com/mozilla/web-ext -# Your auto-generated extension ID for addons.mozilla.org is: -tel-signed-webext@tests.mozilla.org
\ No newline at end of file diff --git a/toolkit/components/telemetry/tests/addons/signed-webext/META-INF/manifest.mf b/toolkit/components/telemetry/tests/addons/signed-webext/META-INF/manifest.mf deleted file mode 100644 index 6be3a6e32f..0000000000 --- a/toolkit/components/telemetry/tests/addons/signed-webext/META-INF/manifest.mf +++ /dev/null @@ -1,7 +0,0 @@ -Manifest-Version: 1.0 - -Name: manifest.json -Digest-Algorithms: MD5 SHA1 -MD5-Digest: vh0VF5quc9YIhMhIsZgKcg== -SHA1-Digest: DNXAbrHJ4ncET5W+qtJl4+45D6s= - diff --git a/toolkit/components/telemetry/tests/addons/signed-webext/META-INF/mozilla.rsa b/toolkit/components/telemetry/tests/addons/signed-webext/META-INF/mozilla.rsa Binary files differdeleted file mode 100644 index b202c515a8..0000000000 --- a/toolkit/components/telemetry/tests/addons/signed-webext/META-INF/mozilla.rsa +++ /dev/null diff --git a/toolkit/components/telemetry/tests/addons/signed-webext/META-INF/mozilla.sf b/toolkit/components/telemetry/tests/addons/signed-webext/META-INF/mozilla.sf deleted file mode 100644 index 72af14d816..0000000000 --- a/toolkit/components/telemetry/tests/addons/signed-webext/META-INF/mozilla.sf +++ /dev/null @@ -1,4 +0,0 @@ -Signature-Version: 1.0 -MD5-Digest-Manifest: tbeA48G5pe86kvUbd4rsXA== -SHA1-Digest-Manifest: jyvWt2v9XUnYHrvrlSi4BdyZV/0= - diff --git a/toolkit/components/telemetry/tests/addons/signed-webext/manifest.json b/toolkit/components/telemetry/tests/addons/signed-webext/manifest.json deleted file mode 100644 index 437b415fef..0000000000 --- a/toolkit/components/telemetry/tests/addons/signed-webext/manifest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "manifest_version": 2, - "name": "XPI Telemetry Signed Test", - "description": "A signed webextension", - "version": "1.0", - - "applications": { - "gecko": { - "id": "tel-signed-webext@tests.mozilla.org" - } - } -} diff --git a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_testcase.py b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_testcase.py index c5bc54e9d2..9bcf29d2f4 100644 --- a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_testcase.py +++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_testcase.py @@ -55,6 +55,8 @@ class FOGTestCase(TelemetryTestCase): "logging.fog_control::*": 5, "logging.glean::*": 5, "logging.glean_core::*": 5, + # Slow down the user inactivity timeout (bug 1690728) + "dom.events.user_interaction_interval": 60000, } ) diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js index a67788623d..9990d83fdf 100644 --- a/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js +++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js @@ -715,16 +715,18 @@ add_task(async function test_addons() { add_task(async function test_signedAddon() { AddonTestUtils.useRealCertChecks = true; - const ADDON_INSTALL_URL = gDataRoot + "signed-webext.xpi"; - const ADDON_ID = "tel-signed-webext@tests.mozilla.org"; + const { PKCS7_WITH_SHA1, COSE_WITH_SHA256 } = Ci.nsIAppSignatureInfo; + + const ADDON_INSTALL_URL = gDataRoot + "amosigned.xpi"; + const ADDON_ID = "amosigned-xpi@tests.mozilla.org"; const ADDON_INSTALL_DATE = truncateToDays(Date.now()); const EXPECTED_ADDON_DATA = { blocklisted: false, - description: "A signed webextension", - name: "XPI Telemetry Signed Test", + description: null, + name: "XPI Test", userDisabled: false, appDisabled: false, - version: "1.0", + version: "2.2", scope: 1, type: "extension", foreignInstall: false, @@ -732,6 +734,7 @@ add_task(async function test_signedAddon() { installDay: ADDON_INSTALL_DATE, updateDay: ADDON_INSTALL_DATE, signedState: AddonManager.SIGNEDSTATE_SIGNED, + signedTypes: JSON.stringify([COSE_WITH_SHA256, PKCS7_WITH_SHA1]), quarantineIgnoredByUser: false, // quarantineIgnoredByApp expected to be false because // the test addon is signed as a non-privileged (see signedState), diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryReportingPolicy.js b/toolkit/components/telemetry/tests/unit/test_TelemetryReportingPolicy.js index 01041172d0..df5dc151ad 100644 --- a/toolkit/components/telemetry/tests/unit/test_TelemetryReportingPolicy.js +++ b/toolkit/components/telemetry/tests/unit/test_TelemetryReportingPolicy.js @@ -199,6 +199,12 @@ add_task(async function test_prefs() { "Accepting the policy again should let us upload data." ); + // macOS has the app.update.channel pref locked. Check if it needs to be + // unlocked before proceeding with the test. + if (Services.prefs.getDefaultBranch("").prefIsLocked("app.update.channel")) { + Services.prefs.getDefaultBranch("").unlockPref("app.update.channel"); + } + // Set a new, per channel, minimum policy version. Start by setting a test current channel. Services.prefs .getDefaultBranch("") diff --git a/toolkit/components/telemetry/tests/unit/xpcshell.toml b/toolkit/components/telemetry/tests/unit/xpcshell.toml index dbff9c627c..0c660888ed 100644 --- a/toolkit/components/telemetry/tests/unit/xpcshell.toml +++ b/toolkit/components/telemetry/tests/unit/xpcshell.toml @@ -17,6 +17,7 @@ support-files = [ "testUnicodePDBAArch64.dll", "testNoPDBAArch64.dll", "!/toolkit/mozapps/extensions/test/xpcshell/head_addons.js", + "../../../../mozapps/extensions/test/xpinstall/amosigned.xpi", ] generated-files = [ "system.xpi", diff --git a/toolkit/components/terminator/TerminatorTelemetry.sys.mjs b/toolkit/components/terminator/TerminatorTelemetry.sys.mjs deleted file mode 100644 index 9903027436..0000000000 --- a/toolkit/components/terminator/TerminatorTelemetry.sys.mjs +++ /dev/null @@ -1,119 +0,0 @@ -/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ -/* 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/. */ - -/** - * Read the data saved by nsTerminator during shutdown and feed it to the - * relevant telemetry histograms. - */ - -const lazy = {}; - -ChromeUtils.defineESModuleGetters(lazy, { - setTimeout: "resource://gre/modules/Timer.sys.mjs", -}); - -export function nsTerminatorTelemetry() { - this._wasNotified = false; - this._deferred = Promise.withResolvers(); - - IOUtils.sendTelemetry.addBlocker( - "TerminatoryTelemetry: Waiting to submit telemetry", - this._deferred.promise, - () => ({ - wasNotified: this._wasNotified, - }) - ); -} - -var HISTOGRAMS = { - "quit-application": "SHUTDOWN_PHASE_DURATION_TICKS_QUIT_APPLICATION", - "profile-change-net-teardown": - "SHUTDOWN_PHASE_DURATION_TICKS_PROFILE_CHANGE_NET_TEARDOWN", - "profile-change-teardown": - "SHUTDOWN_PHASE_DURATION_TICKS_PROFILE_CHANGE_TEARDOWN", - "profile-before-change": - "SHUTDOWN_PHASE_DURATION_TICKS_PROFILE_BEFORE_CHANGE", - "profile-before-change-qm": - "SHUTDOWN_PHASE_DURATION_TICKS_PROFILE_BEFORE_CHANGE_QM", - "xpcom-will-shutdown": "SHUTDOWN_PHASE_DURATION_TICKS_XPCOM_WILL_SHUTDOWN", - "xpcom-shutdown": "SHUTDOWN_PHASE_DURATION_TICKS_XPCOM_SHUTDOWN", - - // The following keys appear in the JSON, but do not have associated - // histograms. - "xpcom-shutdown-threads": null, - XPCOMShutdownFinal: null, - CCPostLastCycleCollection: null, -}; - -nsTerminatorTelemetry.prototype = { - classID: Components.ID("{3f78ada1-cba2-442a-82dd-d5fb300ddea7}"), - - // nsISupports - - QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), - - // nsIObserver - - observe: function DS_observe(aSubject, aTopic, aData) { - this._wasNotified = true; - - (async () => { - try { - // - // This data is hardly critical, reading it can wait for a few seconds. - // - await new Promise(resolve => lazy.setTimeout(resolve, 3000)); - - let PATH = PathUtils.join( - Services.dirsvc.get("ProfLD", Ci.nsIFile).path, - "ShutdownDuration.json" - ); - let data; - try { - data = await IOUtils.readJSON(PATH); - } catch (ex) { - if (DOMException.isInstance(ex) && ex.name == "NotFoundError") { - return; - } - // Let other errors be reported by Promise's error-reporting. - throw ex; - } - - // Clean up - await IOUtils.remove(PATH); - await IOUtils.remove(PATH + ".tmp"); - - for (let k of Object.keys(data)) { - let id = HISTOGRAMS[k]; - if (id === null) { - // No histogram associated with this entry. - continue; - } - - try { - let histogram = Services.telemetry.getHistogramById(id); - histogram.add(Number.parseInt(data[k])); - } catch (ex) { - // Make sure that the error is reported and causes test failures, - // but otherwise, ignore it. - Promise.reject(ex); - continue; - } - } - - // Inform observers that we are done. - Services.obs.notifyObservers( - null, - "shutdown-terminator-telemetry-updated" - ); - } finally { - this._deferred.resolve(); - } - })(); - }, -}; - -// Module diff --git a/toolkit/components/terminator/components.conf b/toolkit/components/terminator/components.conf deleted file mode 100644 index 14c4c2c562..0000000000 --- a/toolkit/components/terminator/components.conf +++ /dev/null @@ -1,15 +0,0 @@ -# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- -# vim: set filetype=python: -# 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/. - -Classes = [ - { - 'cid': '{3f78ada1-cba2-442a-82dd-d5fb300ddea7}', - 'contract_ids': ['@mozilla.org/toolkit/shutdown-terminator-telemetry;1'], - 'esModule': 'resource://gre/modules/TerminatorTelemetry.sys.mjs', - 'constructor': 'nsTerminatorTelemetry', - 'categories': {'profile-after-change': 'nsTerminatorTelemetry'}, - }, -] diff --git a/toolkit/components/terminator/moz.build b/toolkit/components/terminator/moz.build index f869b86d95..a13789c2da 100644 --- a/toolkit/components/terminator/moz.build +++ b/toolkit/components/terminator/moz.build @@ -21,12 +21,10 @@ EXTRA_COMPONENTS += [ "terminator.manifest", ] -EXTRA_JS_MODULES += [ - "TerminatorTelemetry.sys.mjs", +XPIDL_SOURCES += [ + "nsITerminatorTest.idl", ] -XPCOM_MANIFESTS += [ - "components.conf", -] +XPIDL_MODULE = "toolkit_terminator" FINAL_LIBRARY = "xul" diff --git a/toolkit/components/terminator/nsITerminatorTest.idl b/toolkit/components/terminator/nsITerminatorTest.idl new file mode 100644 index 0000000000..22f06a11dc --- /dev/null +++ b/toolkit/components/terminator/nsITerminatorTest.idl @@ -0,0 +1,17 @@ +/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */ +/* 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 "nsISupports.idl" + +[scriptable, uuid(a76599ef-78a4-441f-b258-4908dedef42d)] +interface nsITerminatorTest : nsISupports +{ + + /** + * This method is used to test the shutdown phases of nsTerminator. It is + * not meant to be used outside of tests. + */ + [implicit_jscontext] jsval getTicksForShutdownPhases(); +}; diff --git a/toolkit/components/terminator/nsTerminator.cpp b/toolkit/components/terminator/nsTerminator.cpp index 5a1f9693b8..6339962022 100644 --- a/toolkit/components/terminator/nsTerminator.cpp +++ b/toolkit/components/terminator/nsTerminator.cpp @@ -245,131 +245,9 @@ void RunWatchdog(void* arg) { } } -//////////////////////////////////////////// -// -// Writer thread -// -// This nspr thread is in charge of writing to disk statistics produced by the -// watchdog thread and collected by the main thread. Note that we use a nspr -// thread rather than usual XPCOM I/O simply because we outlive XPCOM and its -// threads. -// - -// -// Communication between the main thread and the writer thread. -// -// Main thread: -// -// * Whenever a shutdown step has been completed, the main thread -// obtains the number of ticks from the watchdog threads, builds -// a string representing all the data gathered so far, places -// this string in `gWriteData`, and wakes up the writer thread -// using `gWriteReady`. If `gWriteData` already contained a non-null -// pointer, this means that the writer thread is lagging behind the -// main thread, and the main thread cleans up the memory. -// -// Writer thread: -// -// * When awake, the writer thread swaps `gWriteData` to nullptr. If -// `gWriteData` contained data to write, the . If so, the writer -// thread writes the data to a file named "ShutdownDuration.json.tmp", -// then moves that file to "ShutdownDuration.json" and cleans up the -// data. If `gWriteData` contains a nullptr, the writer goes to sleep -// until it is awkened using `gWriteReady`. -// -// -// The data written by the writer thread will be read by another -// module upon the next restart and fed to Telemetry. -// -Atomic<nsCString*> gWriteData(nullptr); -PRMonitor* gWriteReady = nullptr; - -void RunWriter(void* arg) { - AUTO_PROFILER_REGISTER_THREAD("Shutdown Statistics Writer"); - NS_SetCurrentThreadName("Shutdown Statistics Writer"); - - MOZ_LSAN_INTENTIONALLY_LEAK_OBJECT(arg); - // Shutdown will generally complete before we have a chance to - // deallocate. This is not a leak. - - // Setup destinationPath and tmpFilePath - - nsCString destinationPath; - destinationPath.Adopt(static_cast<char*>(arg)); - nsAutoCString tmpFilePath; - tmpFilePath.Append(destinationPath); - tmpFilePath.AppendLiteral(".tmp"); - - // Cleanup any file leftover from a previous run - Unused << PR_Delete(tmpFilePath.get()); - Unused << PR_Delete(destinationPath.get()); - - while (true) { - // - // Check whether we have received data from the main thread. - // - // We perform the check before waiting on `gWriteReady` as we may - // have received data while we were busy writing. - // - // Also note that gWriteData may have been modified several times - // since we last checked. That's ok, we are not losing any important - // data (since we keep adding data), and we are not leaking memory - // (since the main thread deallocates any data that hasn't been - // consumed by the writer thread). - // - UniquePtr<nsCString> data(gWriteData.exchange(nullptr)); - if (!data) { - // Data is not available yet. - // Wait until the main thread provides it. - PR_EnterMonitor(gWriteReady); - PR_Wait(gWriteReady, PR_INTERVAL_NO_TIMEOUT); - PR_ExitMonitor(gWriteReady); - continue; - } - - MOZ_LSAN_INTENTIONALLY_LEAK_OBJECT(data.get()); - // Shutdown may complete before we have a chance to deallocate. - // This is not a leak. - - // - // Write to a temporary file - // - // In case of any error, we simply give up. Since the data is - // hardly critical, we don't want to spend too much effort - // salvaging it. - // - UniquePtr<PRFileDesc, PR_CloseDelete> tmpFileDesc(PR_Open( - tmpFilePath.get(), PR_WRONLY | PR_TRUNCATE | PR_CREATE_FILE, 00600)); - - // Shutdown may complete before we have a chance to close the file. - // This is not a leak. - MOZ_LSAN_INTENTIONALLY_LEAK_OBJECT(tmpFileDesc.get()); - - if (tmpFileDesc == nullptr) { - break; - } - if (PR_Write(tmpFileDesc.get(), data->get(), data->Length()) == -1) { - break; - } - tmpFileDesc.reset(); - - // - // Rename on top of destination file. - // - // This is not sufficient to guarantee that the destination file - // will be written correctly, but, again, we don't care enough - // about the data to make more efforts. - // - Unused << PR_Delete(destinationPath.get()); - if (PR_Rename(tmpFilePath.get(), destinationPath.get()) != PR_SUCCESS) { - break; - } - } -} - } // namespace -NS_IMPL_ISUPPORTS(nsTerminator, nsIObserver) +NS_IMPL_ISUPPORTS(nsTerminator, nsIObserver, nsITerminatorTest) nsTerminator::nsTerminator() : mInitialized(false), mCurrentStep(-1) {} @@ -379,12 +257,6 @@ void nsTerminator::Start() { MOZ_ASSERT(!mInitialized); StartWatchdog(); -#if !defined(NS_FREE_PERMANENT_DATA) - // Only allow nsTerminator to write on non-leak-checked builds so we don't - // get leak warnings on shutdown for intentional leaks (see bug 1242084). - // This will be enabled again by bug 1255484 when 1255478 lands. - StartWriter(); -#endif // !defined(NS_FREE_PERMANENT_DATA) mInitialized = true; } @@ -470,41 +342,6 @@ void nsTerminator::StartWatchdog() { MOZ_ASSERT(watchdogThread); } -// Prepare, allocate and start the writer thread. By design, it will never -// finish, nor be deallocated. In case of error, we degrade -// gracefully to not writing Telemetry data. -void nsTerminator::StartWriter() { - if (!Telemetry::CanRecordExtended()) { - return; - } - nsCOMPtr<nsIFile> profLD; - nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_LOCAL_50_DIR, - getter_AddRefs(profLD)); - if (NS_FAILED(rv)) { - return; - } - - rv = profLD->Append(u"ShutdownDuration.json"_ns); - if (NS_FAILED(rv)) { - return; - } - - nsAutoString path; - rv = profLD->GetPath(path); - if (NS_FAILED(rv)) { - return; - } - - gWriteReady = PR_NewMonitor(); - MOZ_LSAN_INTENTIONALLY_LEAK_OBJECT( - gWriteReady); // We will never deallocate this object - PRThread* writerThread = CreateSystemThread(RunWriter, ToNewUTF8String(path)); - - if (!writerThread) { - return; - } -} - // This helper is here to preserve the existing crash reporting behavior // based on observer topic names, using the shutdown phase name only for // phases without associated topic. @@ -531,12 +368,6 @@ void nsTerminator::AdvancePhase(mozilla::ShutdownPhase aPhase) { } UpdateHeartbeat(step); -#if !defined(NS_FREE_PERMANENT_DATA) - // Only allow nsTerminator to write on non-leak checked builds so we don't get - // leak warnings on shutdown for intentional leaks (see bug 1242084). This - // will be enabled again by bug 1255484 when 1255478 lands. - UpdateTelemetry(); -#endif // !defined(NS_FREE_PERMANENT_DATA) UpdateCrashReport(GetReadableNameForPhase(aPhase)); } @@ -555,58 +386,25 @@ void nsTerminator::UpdateHeartbeat(int32_t aStep) { } } -void nsTerminator::UpdateTelemetry() { - if (!Telemetry::CanRecordExtended() || !gWriteReady) { - return; - } - - // - // We need Telemetry data on the effective duration of each step, - // to be able to tune the time-to-crash of each of both the - // Terminator and AsyncShutdown. However, at this stage, it is too - // late to record such data into Telemetry, so we write it to disk - // and read it upon the next startup. - // - - // Build JSON. - UniquePtr<nsCString> telemetryData(new nsCString()); - telemetryData->AppendLiteral("{"); - size_t fields = 0; - for (auto& shutdownStep : sShutdownSteps) { - if (shutdownStep.mTicks < 0) { - // Ignore this field. - continue; - } - if (fields++ > 0) { - telemetryData->AppendLiteral(", "); - } - telemetryData->AppendLiteral(R"(")"); - telemetryData->Append(GetReadableNameForPhase(shutdownStep.mPhase)); - telemetryData->AppendLiteral(R"(": )"); - telemetryData->AppendInt(shutdownStep.mTicks); - } - telemetryData->AppendLiteral("}"); - - if (fields == 0) { - // Nothing to write - return; - } - - // - // Send data to the worker thread. - // - delete gWriteData.exchange( - telemetryData.release()); // Clear any data that hasn't been written yet - - // In case the worker thread was sleeping, wake it up. - PR_EnterMonitor(gWriteReady); - PR_Notify(gWriteReady); - PR_ExitMonitor(gWriteReady); -} - void nsTerminator::UpdateCrashReport(const char* aTopic) { // In case of crash, we wish to know where in shutdown we are CrashReporter::RecordAnnotationCString( CrashReporter::Annotation::ShutdownProgress, aTopic); } + +NS_IMETHODIMP +nsTerminator::GetTicksForShutdownPhases(JSContext* aCx, + JS::MutableHandle<JS::Value> aRetval) { + JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx)); + aRetval.setObject(*obj); + + for (auto& shutdownStep : sShutdownSteps) { + if (shutdownStep.mTicks >= 0) { + JS_DefineProperty(aCx, obj, GetReadableNameForPhase(shutdownStep.mPhase), + shutdownStep.mTicks, JSPROP_ENUMERATE); + } + } + + return NS_OK; +} // namespace mozilla } // namespace mozilla diff --git a/toolkit/components/terminator/nsTerminator.h b/toolkit/components/terminator/nsTerminator.h index 46c3e17b92..0de6ebe778 100644 --- a/toolkit/components/terminator/nsTerminator.h +++ b/toolkit/components/terminator/nsTerminator.h @@ -9,13 +9,15 @@ #include "nsISupports.h" #include "nsIObserver.h" +#include "nsITerminatorTest.h" namespace mozilla { -class nsTerminator final : public nsIObserver { +class nsTerminator final : public nsIObserver, public nsITerminatorTest { public: NS_DECL_ISUPPORTS NS_DECL_NSIOBSERVER + NS_DECL_NSITERMINATORTEST nsTerminator(); void AdvancePhase(mozilla::ShutdownPhase aPhase); diff --git a/toolkit/components/terminator/tests/xpcshell/test_terminator_record.js b/toolkit/components/terminator/tests/xpcshell/test_terminator_advance_phases.js index 62cee636f3..42f0e3686f 100644 --- a/toolkit/components/terminator/tests/xpcshell/test_terminator_record.js +++ b/toolkit/components/terminator/tests/xpcshell/test_terminator_advance_phases.js @@ -4,14 +4,13 @@ "use strict"; -// Test that the Shutdown Terminator records durations correctly +// Test that the Shutdown Terminator advances through the shutdown phases +// correctly. const { setTimeout } = ChromeUtils.importESModule( "resource://gre/modules/Timer.sys.mjs" ); -var PATH; -var PATH_TMP; var terminator; var HEARTBEAT_MS = 100; @@ -34,8 +33,6 @@ let MeasuredDurations = []; add_task(async function init() { do_get_profile(); - PATH = PathUtils.join(PathUtils.localProfileDir, "ShutdownDuration.json"); - PATH_TMP = PATH + ".tmp"; // Initialize the terminator // (normally, this is done through the manifest file, but xpcshell @@ -44,23 +41,12 @@ add_task(async function init() { terminator = Cc["@mozilla.org/toolkit/shutdown-terminator;1"].createInstance( Ci.nsIObserver ); -}); - -var promiseShutdownDurationData = async function () { - // Wait until PATH exists. - // Timeout if it is never created. - while (true) { - if (await IOUtils.exists(PATH)) { - break; - } - - // Wait just a very short period to not increase measured values. - // Usually the file should appear almost immediately. - await new Promise(resolve => setTimeout(resolve, 50)); - } - return IOUtils.readJSON(PATH); -}; + Assert.ok( + terminator instanceof Ci.nsITerminatorTest, + "Terminator should implement nsITerminatorTest" + ); +}); var currentPhase = 0; @@ -72,13 +58,16 @@ var advancePhase = async function () { terminator.observe(null, key, null); await new Promise(resolve => setTimeout(resolve, msDuration)); - let data = await promiseShutdownDurationData(); + let data = terminator.getTicksForShutdownPhases(); - Assert.ok(KEYS[currentPhase] in data, "The file contains the expected key"); + Assert.ok( + KEYS[currentPhase] in data, + "The KEYS object contains the expected key" + ); Assert.equal( Object.keys(data).length, currentPhase + 1, - "File does not contain more durations than expected" + "KEYS object does not contain more durations than expected" ); DATA[currentPhase] = data; @@ -108,9 +97,6 @@ add_task(async function test_record() { morePhases = await advancePhase(); - await IOUtils.remove(PATH); - await IOUtils.remove(PATH_TMP); - // We measure the effective time that passed as wall-clock and include all // file IO overhead as the terminator will do so in its measurement, too. MeasuredDurations[currentPhase - 1] = Math.floor( @@ -166,6 +152,6 @@ add_task(async function test_record() { .sort() .join(", "), KEYS.sort().join(", "), - "The last file contains all expected keys" + "The KEYS object contains all expected keys" ); }); diff --git a/toolkit/components/terminator/tests/xpcshell/test_terminator_reload.js b/toolkit/components/terminator/tests/xpcshell/test_terminator_reload.js deleted file mode 100644 index 79aecd818b..0000000000 --- a/toolkit/components/terminator/tests/xpcshell/test_terminator_reload.js +++ /dev/null @@ -1,88 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -// Test that the Shutdown Terminator reloads durations correctly - -const HISTOGRAMS = { - "quit-application": "SHUTDOWN_PHASE_DURATION_TICKS_QUIT_APPLICATION", - "profile-change-net-teardown": - "SHUTDOWN_PHASE_DURATION_TICKS_PROFILE_CHANGE_NET_TEARDOWN", - "profile-change-teardown": - "SHUTDOWN_PHASE_DURATION_TICKS_PROFILE_CHANGE_TEARDOWN", - "profile-before-change": - "SHUTDOWN_PHASE_DURATION_TICKS_PROFILE_BEFORE_CHANGE", - "profile-before-change-qm": - "SHUTDOWN_PHASE_DURATION_TICKS_PROFILE_BEFORE_CHANGE_QM", - "xpcom-will-shutdown": "SHUTDOWN_PHASE_DURATION_TICKS_XPCOM_WILL_SHUTDOWN", - "xpcom-shutdown": "SHUTDOWN_PHASE_DURATION_TICKS_XPCOM_SHUTDOWN", -}; - -let PATH; - -add_setup(async function init() { - do_get_profile(); - PATH = PathUtils.join(PathUtils.localProfileDir, "ShutdownDuration.json"); -}); - -add_task(async function test_reload() { - info("Forging data"); - let data = {}; - let telemetrySnapshots = Services.telemetry.getSnapshotForHistograms( - "main", - false /* clear */ - ).parent; - let i = 0; - for (let k of Object.keys(HISTOGRAMS)) { - let id = HISTOGRAMS[k]; - data[k] = i++; - Assert.equal( - telemetrySnapshots[id] || undefined, - undefined, - "Histogram " + id + " is empty" - ); - } - - // Extra fields that nsTerminator reports that we do not have histograms for. - data["xpcom-shutdown-threads"] = 123; - data.XPCOMShutdownFinal = 456; - data.CCPostLastCycleCollection = 789; - - await IOUtils.writeJSON(PATH, data); - - const TOPIC = "shutdown-terminator-telemetry-updated"; - - let wait = new Promise(resolve => - Services.obs.addObserver(function observer() { - info("Telemetry has been updated"); - Services.obs.removeObserver(observer, TOPIC); - resolve(); - }, TOPIC) - ); - - info("Starting nsTerminatorTelemetry"); - let tt = Cc[ - "@mozilla.org/toolkit/shutdown-terminator-telemetry;1" - ].createInstance(Ci.nsIObserver); - tt.observe(null, "profile-after-change", ""); - - info("Waiting until telemetry is updated"); - // Now wait until Telemetry is updated - await wait; - - telemetrySnapshots = Services.telemetry.getSnapshotForHistograms( - "main", - false /* clear */ - ).parent; - for (let k of Object.keys(HISTOGRAMS)) { - let id = HISTOGRAMS[k]; - info("Testing histogram " + id); - let snapshot = telemetrySnapshots[id]; - let count = 0; - for (let x of Object.values(snapshot.values)) { - count += x; - } - Assert.equal(count, 1, "We have added one item"); - } -}); diff --git a/toolkit/components/terminator/tests/xpcshell/xpcshell.toml b/toolkit/components/terminator/tests/xpcshell/xpcshell.toml index cb328ca2c8..67cc36f265 100644 --- a/toolkit/components/terminator/tests/xpcshell/xpcshell.toml +++ b/toolkit/components/terminator/tests/xpcshell/xpcshell.toml @@ -1,15 +1,10 @@ [DEFAULT] head = "" -["test_terminator_record.js"] +["test_terminator_advance_phases.js"] skip-if = [ "debug", "asan", # Disabled by bug 1242084, bug 1255484 will enable it again "ccov", # Bug 1607583 tracks the ccov failure "tsan", # Bug 1683730 made this timeout for tsan. ] -run-sequentially = "very high failure rate in parallel" - -["test_terminator_reload.js"] -skip-if = ["os == 'android'"] -run-sequentially = "very high failure rate in parallel" diff --git a/toolkit/components/thumbnails/BackgroundPageThumbs.sys.mjs b/toolkit/components/thumbnails/BackgroundPageThumbs.sys.mjs index 8467653acb..3040adc6a1 100644 --- a/toolkit/components/thumbnails/BackgroundPageThumbs.sys.mjs +++ b/toolkit/components/thumbnails/BackgroundPageThumbs.sys.mjs @@ -219,7 +219,7 @@ export const BackgroundPageThumbs = { "nsISupportsWeakReference", ]), }; - this._listener.onStateChange = (wbp, request, stateFlags, status) => { + this._listener.onStateChange = (wbp, request, stateFlags) => { if (!request) { return; } @@ -591,7 +591,7 @@ Capture.prototype = { this._done(browser, null, TEL_CAPTURE_DONE_BAD_URI); } }, - failure => { + () => { // The query can fail when a crash occurs while loading. The error causes // thumbnail crash tests to fail with an uninteresting error message. } diff --git a/toolkit/components/thumbnails/PageThumbs.sys.mjs b/toolkit/components/thumbnails/PageThumbs.sys.mjs index 4d103c8bd8..da7e187d85 100644 --- a/toolkit/components/thumbnails/PageThumbs.sys.mjs +++ b/toolkit/components/thumbnails/PageThumbs.sys.mjs @@ -262,7 +262,7 @@ export var PageThumbs = { aBrowser.browsingContext.currentWindowGlobal.getActor("Thumbnails"); return thumbnailsActor .sendQuery("Browser:Thumbnail:CheckState") - .catch(err => { + .catch(() => { return false; }); } @@ -835,7 +835,7 @@ export var PageThumbsExpiration = { } }, - notify: function Expiration_notify(aTimer) { + notify: function Expiration_notify() { let urls = []; let filtersToWaitFor = this._filters.length; diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_bg_captureIfMissing.js b/toolkit/components/thumbnails/test/browser_thumbnails_bg_captureIfMissing.js index 15cf4d7067..b3b4c1f305 100644 --- a/toolkit/components/thumbnails/test/browser_thumbnails_bg_captureIfMissing.js +++ b/toolkit/components/thumbnails/test/browser_thumbnails_bg_captureIfMissing.js @@ -3,7 +3,7 @@ add_task(async function thumbnails_bg_captureIfMissing() { let numNotifications = 0; - function observe(subject, topic, data) { + function observe(subject, topic) { is(topic, "page-thumbnail:create", "got expected topic"); numNotifications += 1; } diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_bug727765.js b/toolkit/components/thumbnails/test/browser_thumbnails_bug727765.js index 8e953e0ffa..d4dd7caca7 100644 --- a/toolkit/components/thumbnails/test/browser_thumbnails_bug727765.js +++ b/toolkit/components/thumbnails/test/browser_thumbnails_bug727765.js @@ -21,7 +21,7 @@ add_task(async function thumbnails_bg_bug727765() { gBrowser, url: URL, }, - async browser => { + async () => { await captureAndCheckColor(255, 0, 0, "we have a red thumbnail"); // Check the thumbnail color of the bottom right pixel. diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_bug818225.js b/toolkit/components/thumbnails/test/browser_thumbnails_bug818225.js index 5f557c40a4..37f9b2f7d7 100644 --- a/toolkit/components/thumbnails/test/browser_thumbnails_bug818225.js +++ b/toolkit/components/thumbnails/test/browser_thumbnails_bug818225.js @@ -17,7 +17,7 @@ add_task(async function thumbnails_bg_bug818225() { gBrowser, url: URL, }, - async browser => { + async () => { gBrowserThumbnails.clearTopSiteURLCache(); await whenFileExists(URL); } diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_redirect.js b/toolkit/components/thumbnails/test/browser_thumbnails_redirect.js index b77b4011c3..ba06b9e779 100644 --- a/toolkit/components/thumbnails/test/browser_thumbnails_redirect.js +++ b/toolkit/components/thumbnails/test/browser_thumbnails_redirect.js @@ -21,7 +21,7 @@ add_task(async function thumbnails_redirect() { gBrowser, url: URL, }, - browser => {} + () => {} ); // Create a tab, redirecting to a page with a red background. @@ -30,7 +30,7 @@ add_task(async function thumbnails_redirect() { gBrowser, url: URL, }, - async browser => { + async () => { await captureAndCheckColor(255, 0, 0, "we have a red thumbnail"); // Wait until the referrer's thumbnail's file has been written. diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_storage.js b/toolkit/components/thumbnails/test/browser_thumbnails_storage.js index 6a9f1ed3f6..39d26897a2 100644 --- a/toolkit/components/thumbnails/test/browser_thumbnails_storage.js +++ b/toolkit/components/thumbnails/test/browser_thumbnails_storage.js @@ -89,7 +89,7 @@ async function promiseCreateThumbnail() { gBrowser, url: URL, }, - async browser => { + async () => { gBrowserThumbnails.clearTopSiteURLCache(); await whenFileExists(URL); } diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_update.js b/toolkit/components/thumbnails/test/browser_thumbnails_update.js index 004df9a01f..7a00717e2f 100644 --- a/toolkit/components/thumbnails/test/browser_thumbnails_update.js +++ b/toolkit/components/thumbnails/test/browser_thumbnails_update.js @@ -163,7 +163,7 @@ add_task(async function thumbnails_captureAndStore_error_response() { gBrowser, url: URL, }, - async browser => { + async () => { await captureAndCheckColor(0, 255, 0, "we have a green thumbnail"); } ); @@ -176,7 +176,7 @@ add_task(async function thumbnails_captureAndStore_error_response() { gBrowser, url: URL, }, - async browser => { + async () => { await captureAndCheckColor(0, 255, 0, "we still have a green thumbnail"); } ); @@ -195,7 +195,7 @@ add_task(async function thumbnails_captureAndStore_ok_response() { gBrowser, url: URL, }, - async browser => { + async () => { await captureAndCheckColor(0, 255, 0, "we have a green thumbnail"); } ); @@ -208,7 +208,7 @@ add_task(async function thumbnails_captureAndStore_ok_response() { gBrowser, url: URL, }, - async browser => { + async () => { await captureAndCheckColor(255, 0, 0, "we now have a red thumbnail"); } ); diff --git a/toolkit/components/timermanager/UpdateTimerManager.sys.mjs b/toolkit/components/timermanager/UpdateTimerManager.sys.mjs index 607a3b0e71..14c34e8847 100644 --- a/toolkit/components/timermanager/UpdateTimerManager.sys.mjs +++ b/toolkit/components/timermanager/UpdateTimerManager.sys.mjs @@ -69,7 +69,7 @@ TimerManager.prototype = { /** * See nsIObserver.idl */ - observe: function TM_observe(aSubject, aTopic, aData) { + observe: function TM_observe(aSubject, aTopic) { // Prevent setting the timer interval to a value of less than 30 seconds. var minInterval = 30000; // Prevent setting the first timer interval to a value of less than 10 diff --git a/toolkit/components/timermanager/tests/unit/consumerNotifications.js b/toolkit/components/timermanager/tests/unit/consumerNotifications.js index 1e09207043..0f20e282e4 100644 --- a/toolkit/components/timermanager/tests/unit/consumerNotifications.js +++ b/toolkit/components/timermanager/tests/unit/consumerNotifications.js @@ -142,7 +142,7 @@ ChromeUtils.defineLazyGetter(this, "gCompReg", function () { }); const gTest0TimerCallback = { - notify: function T0CB_notify(aTimer) { + notify: function T0CB_notify() { // This can happen when another notification fails and this timer having // time to fire so check other timers are successful. do_throw("gTest0TimerCallback notify method should not have been called"); @@ -157,7 +157,7 @@ const gTest0Factory = { }; const gTest1TimerCallback = { - notify: function T1CB_notify(aTimer) { + notify: function T1CB_notify() { // This can happen when another notification fails and this timer having // time to fire so check other timers are successful. do_throw("gTest1TimerCallback notify method should not have been called"); @@ -172,7 +172,7 @@ const gTest1Factory = { }; const gTest2TimerCallback = { - notify: function T2CB_notify(aTimer) { + notify: function T2CB_notify() { // This can happen when another notification fails and this timer having // time to fire so check other timers are successful. do_throw("gTest2TimerCallback notify method should not have been called"); @@ -197,7 +197,7 @@ const gTest3Factory = { }; const gTest4TimerCallback = { - notify: function T4CB_notify(aTimer) { + notify: function T4CB_notify() { Services.catMan.deleteCategoryEntry( CATEGORY_UPDATE_TIMER, TESTS[4].desc, @@ -216,7 +216,7 @@ const gTest4Factory = { }; const gTest5TimerCallback = { - notify: function T5CB_notify(aTimer) { + notify: function T5CB_notify() { Services.catMan.deleteCategoryEntry( CATEGORY_UPDATE_TIMER, TESTS[5].desc, @@ -235,7 +235,7 @@ const gTest5Factory = { }; const gTest6TimerCallback = { - notify: function T6CB_notify(aTimer) { + notify: function T6CB_notify() { Services.catMan.deleteCategoryEntry( CATEGORY_UPDATE_TIMER, TESTS[6].desc, @@ -254,7 +254,7 @@ const gTest6Factory = { }; const gTest7TimerCallback = { - notify: function T7CB_notify(aTimer) { + notify: function T7CB_notify() { Services.catMan.deleteCategoryEntry( CATEGORY_UPDATE_TIMER, TESTS[7].desc, @@ -273,7 +273,7 @@ const gTest7Factory = { }; const gTest8TimerCallback = { - notify: function T8CB_notify(aTimer) { + notify: function T8CB_notify() { TESTS[8].notified = true; TESTS[8].notifyTime = Date.now(); executeSoon(function () { @@ -290,7 +290,7 @@ const gTest8Factory = { }; const gTest9TimerCallback = { - notify: function T9CB_notify(aTimer) { + notify: function T9CB_notify() { TESTS[9].notified = true; TESTS[9].notifyTime = Date.now(); executeSoon(function () { @@ -301,7 +301,7 @@ const gTest9TimerCallback = { }; const gTest10TimerCallback = { - notify: function T9CB_notify(aTimer) { + notify: function T9CB_notify() { // The timer should have been unregistered before this could // be called. do_throw("gTest10TimerCallback notify method should not have been called"); diff --git a/toolkit/components/tooltiptext/TooltipTextProvider.sys.mjs b/toolkit/components/tooltiptext/TooltipTextProvider.sys.mjs index 96a3e8dd5f..a231f5b4cc 100644 --- a/toolkit/components/tooltiptext/TooltipTextProvider.sys.mjs +++ b/toolkit/components/tooltiptext/TooltipTextProvider.sys.mjs @@ -69,11 +69,7 @@ TooltipTextProvider.prototype = { } if (tipElement.namespaceURI == XUL_NS) { lookingForSVGTitle = false; - // NOTE: getAttribute behaves differently for XUL so we can't rely on - // it returning null, see bug 232598. - titleText = tipElement.hasAttribute("tooltiptext") - ? tipElement.getAttribute("tooltiptext") - : null; + titleText = tipElement.getAttribute("tooltiptext"); } else if (!defView.SVGElement.isInstance(tipElement)) { lookingForSVGTitle = false; titleText = tipElement.getAttribute("title"); diff --git a/toolkit/components/tooltiptext/tests/browser.toml b/toolkit/components/tooltiptext/tests/browser.toml index d03716e683..42aca4a7a6 100644 --- a/toolkit/components/tooltiptext/tests/browser.toml +++ b/toolkit/components/tooltiptext/tests/browser.toml @@ -11,7 +11,6 @@ support-files = ["xul_tooltiptext.xhtml"] ["browser_bug581947.js"] ["browser_input_file_tooltips.js"] -skip-if = ["os == 'win' && os_version == '10.0'"] # Permafail on Win 10 (bug 1400368) ["browser_nac_tooltip.js"] diff --git a/toolkit/components/tooltiptext/tests/browser_bug581947.js b/toolkit/components/tooltiptext/tests/browser_bug581947.js index 6e5eb9ea14..0971735470 100644 --- a/toolkit/components/tooltiptext/tests/browser_bug581947.js +++ b/toolkit/components/tooltiptext/tests/browser_bug581947.js @@ -55,7 +55,7 @@ function todo_check(aBrowser, aElementName, aBarred) { return SpecialPowers.spawn( aBrowser, [[aElementName, aBarred]], - async function ([aElementName, aBarred]) { + async function ([aElementName]) { let e = content.document.createElement(aElementName); let contentElement = content.document.getElementById("content"); contentElement.appendChild(e); diff --git a/toolkit/components/translation/moz.build b/toolkit/components/translation/moz.build index 75dfd04bc3..38214a2dc1 100644 --- a/toolkit/components/translation/moz.build +++ b/toolkit/components/translation/moz.build @@ -3,7 +3,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. with Files("**"): - BUG_COMPONENT = ("Firefox", "Translation") + BUG_COMPONENT = ("Firefox", "Translations") EXTRA_JS_MODULES.translation = [ "cld2/cld-worker.js", diff --git a/toolkit/components/translations/actors/AboutTranslationsChild.sys.mjs b/toolkit/components/translations/actors/AboutTranslationsChild.sys.mjs index 9d0b27a6a1..0b600bb03c 100644 --- a/toolkit/components/translations/actors/AboutTranslationsChild.sys.mjs +++ b/toolkit/components/translations/actors/AboutTranslationsChild.sys.mjs @@ -53,7 +53,7 @@ export class AboutTranslationsChild extends JSWindowActorChild { receiveMessage({ name, data }) { switch (name) { - case "AboutTranslations:SendTranslationsPort": + case "AboutTranslations:SendTranslationsPort": { const { fromLanguage, toLanguage, port } = data; const transferables = [port]; this.contentWindow.postMessage( @@ -67,6 +67,7 @@ export class AboutTranslationsChild extends JSWindowActorChild { transferables ); break; + } default: throw new Error("Unknown AboutTranslations message: " + name); } diff --git a/toolkit/components/translations/actors/TranslationsParent.sys.mjs b/toolkit/components/translations/actors/TranslationsParent.sys.mjs index 70754d95c4..f262cbeab2 100644 --- a/toolkit/components/translations/actors/TranslationsParent.sys.mjs +++ b/toolkit/components/translations/actors/TranslationsParent.sys.mjs @@ -150,10 +150,110 @@ const VERIFY_SIGNATURES_FROM_FS = false; */ /** - * The translations parent is used to orchestrate translations in Firefox. It can - * download the wasm translation engines, and the machine learning language models. + * The state that is stored per a "top" ChromeWindow. This "top" ChromeWindow is the JS + * global associated with a browser window. Some state is unique to a browser window, and + * using the top ChromeWindow is a unique key that ensures the state will be unique to + * that browser window. * - * See Bug 971044 for more details of planned work. + * See BrowsingContext.webidl for information on the "top" + * See the TranslationsParent JSDoc for more information on the state management. + */ +class StatePerTopChromeWindow { + /** + * The storage backing for the states. + * + * @type {WeakMap<ChromeWindow, StatePerTopChromeWindow>} + */ + static #states = new WeakMap(); + + /** + * When reloading the page, store the translation pair that needs translating. + * + * @type {null | TranslationPair} + */ + translateOnPageReload = null; + + /** + * The page may auto-translate due to user settings. On a page restore, always + * skip the page restore logic. + * + * @type {boolean} + */ + isPageRestored = false; + + /** + * Remember the detected languages on a page reload. This will keep the translations + * button from disappearing and reappearing, which causes the button to lose focus. + * + * @type {LangTags | null} previousDetectedLanguages + */ + previousDetectedLanguages = null; + + static #id = 0; + /** + * @param {ChromeWindow} topChromeWindow + */ + constructor(topChromeWindow) { + this.id = StatePerTopChromeWindow.#id++; + StatePerTopChromeWindow.#states.set(topChromeWindow, this); + } + + /** + * @param {ChromeWindow} topChromeWindow + * @returns {StatePerTopChromeWindow} + */ + static getOrCreate(topChromeWindow) { + let state = StatePerTopChromeWindow.#states.get(topChromeWindow); + if (state) { + return state; + } + state = new StatePerTopChromeWindow(topChromeWindow); + StatePerTopChromeWindow.#states.set(topChromeWindow, state); + return state; + } +} + +/** + * The TranslationsParent is used to orchestrate translations in Firefox. It can + * download the Wasm translation engine, and the language models. It manages the life + * cycle for offering and performing translations. + * + * Care must be taken for the life cycle of the state management and data caching. The + * following examples use a fictitious `myState` property to show how state can be stored. + * + * There is only 1 TranslationsParent static class in the parent process. At this + * layer it is safe to store things like translation models and general browser + * configuration as these don't change across browser windows. This is accessed like + * `TranslationsParent.myState` + * + * The next layer down are the top ChromeWindows. These map to the UI and user's conception + * of a browser window, such as what you would get by hitting cmd+n or ctrl+n to get a new + * browser window. State such as whether a page is reloaded or general navigation events + * must be unique per ChromeWindow. State here is stored in the `StatePerTopChromeWindow` + * abstraction, like `this.getWindowState().myState`. This layer also consists of a + * `FullPageTranslationsPanel` instance per top ChromeWindow (at least on Desktop). + * + * The final layer consists of the multiple tabs and navigation history inside of a + * ChromeWindow. Data for this layer is safe to store on the TranslationsParent instance, + * like `this.myState`. + * + * Below is an ascii diagram of this relationship. + * + * ┌─────────────────────────────────────────────────────────────────────────────┐ + * │ static TranslationsParent │ + * └─────────────────────────────────────────────────────────────────────────────┘ + * | | + * v v + * ┌──────────────────────────────────────┐ ┌──────────────────────────────────────┐ + * │ top ChromeWindow │ │ top ChromeWindow │ + * │ (FullPageTranslationsPanel instance) │ │ (FullPageTranslationsPanel instance) │ + * └──────────────────────────────────────┘ └──────────────────────────────────────┘ + * | | | | | | + * v v v v v v + * ┌────────────────────┐ ┌─────┐ ┌─────┐ ┌────────────────────┐ ┌─────┐ ┌─────┐ + * │ TranslationsParent │ │ ... │ │ ... │ │ TranslationsParent │ │ ... │ │ ... │ + * │ (actor instance) │ │ │ │ │ │ (actor instance) │ │ │ │ │ + * └────────────────────┘ └─────┘ └─────┘ └────────────────────┘ └─────┘ └─────┘ */ export class TranslationsParent extends JSWindowActorParent { /** @@ -205,26 +305,32 @@ export class TranslationsParent extends JSWindowActorParent { #isDestroyed = false; /** - * Remember the detected languages on a page reload. This will keep the translations - * button from disappearing and reappearing, which causes the button to lose focus. + * There is only one static TranslationsParent for all of the top ChromeWindows. + * The top ChromeWindow maps to the user's conception of a window such as when you hit + * cmd+n or ctrl+n. * - * @type {LangTags | null} previousDetectedLanguages + * @returns {StatePerTopChromeWindow} */ - static #previousDetectedLanguages = null; + getWindowState() { + const state = StatePerTopChromeWindow.getOrCreate( + this.browsingContext.top.embedderWindowGlobal + ); + return state; + } actorCreated() { this.innerWindowId = this.browsingContext.top.embedderElement.innerWindowID; + const windowState = this.getWindowState(); this.languageState = new TranslationsLanguageState( this, - TranslationsParent.#previousDetectedLanguages + windowState.previousDetectedLanguages ); - TranslationsParent.#previousDetectedLanguages = null; + windowState.previousDetectedLanguages = null; - if (TranslationsParent.#translateOnPageReload) { + if (windowState.translateOnPageReload) { // The actor was recreated after a page reload, start the translation. - const { fromLanguage, toLanguage } = - TranslationsParent.#translateOnPageReload; - TranslationsParent.#translateOnPageReload = null; + const { fromLanguage, toLanguage } = windowState.translateOnPageReload; + windowState.translateOnPageReload = null; lazy.console.log( `Translating on a page reload from "${fromLanguage}" to "${toLanguage}".` @@ -261,12 +367,6 @@ export class TranslationsParent extends JSWindowActorParent { static #translationsWasmRemoteClient = null; /** - * The page may auto-translate due to user settings. On a page restore, always - * skip the page restore logic. - */ - static #isPageRestored = false; - - /** * Allows the actor's behavior to be changed when the translations engine is mocked via * a dummy RemoteSettingsClient. * @@ -280,13 +380,6 @@ export class TranslationsParent extends JSWindowActorParent { static #isTranslationsEngineSupported = null; /** - * When reloading the page, store the translation pair that needs translating. - * - * @type {null | TranslationPair} - */ - static #translateOnPageReload = null; - - /** * An ordered list of preferred languages based on: * 1. App languages * 2. Web requested languages @@ -477,6 +570,24 @@ export class TranslationsParent extends JSWindowActorParent { } /** + * Retrieves the Translations actor from the current browser context. + * + * @param {object} browser - The browser object from which to get the context. + * + * @returns {object} The Translations actor for handling translation actions. + * @throws {Error} Throws an error if the TranslationsParent actor cannot be found. + */ + static getTranslationsActor(browser) { + const actor = + browser.browsingContext.currentWindowGlobal.getActor("Translations"); + + if (!actor) { + throw new Error("Unable to get the TranslationsParent actor."); + } + return actor; + } + + /** * Detect if Wasm SIMD is supported, and cache the value. It's better to check * for support before downloading large binary blobs to a user who can't even * use the feature. This function also respects mocks and simulating unsupported @@ -670,6 +781,42 @@ export class TranslationsParent extends JSWindowActorParent { return TranslationsParent.#preferredLanguages; } + /** + * Requests a new translations port. + * + * @param {number} innerWindowId - The id of the current window. + * @param {string} fromLanguage - The BCP-47 from-language tag. + * @param {string} toLanguage - The BCP-47 to-language tag. + * + * @returns {Promise<MessagePort | undefined>} The port for communication with the translation engine, or undefined on failure. + */ + static async requestTranslationsPort( + innerWindowId, + fromLanguage, + toLanguage + ) { + let translationsEngineParent; + try { + translationsEngineParent = + await lazy.EngineProcess.getTranslationsEngineParent(); + } catch (error) { + console.error("Failed to get the translation engine process", error); + return undefined; + } + + // The MessageChannel will be used for communicating directly between the content + // process and the engine's process. + const { port1, port2 } = new MessageChannel(); + translationsEngineParent.startTranslation( + fromLanguage, + toLanguage, + port1, + innerWindowId + ); + + return port2; + } + async receiveMessage({ name, data }) { switch (name) { case "Translations:ReportLangTags": { @@ -826,10 +973,11 @@ export class TranslationsParent extends JSWindowActorParent { * @param {LangTags} langTags * @returns {boolean} */ - static #maybeAutoTranslate(langTags) { - if (TranslationsParent.#isPageRestored) { + #maybeAutoTranslate(langTags) { + const windowState = this.getWindowState(); + if (windowState.isPageRestored) { // The user clicked the restore button. Respect it for one page load. - TranslationsParent.#isPageRestored = false; + windowState.isPageRestored = false; // Skip this auto-translation. return false; @@ -875,6 +1023,9 @@ export class TranslationsParent extends JSWindowActorParent { } return Array.from(languagePairMap.values()); }); + TranslationsParent.#languagePairs.catch(() => { + TranslationsParent.#languagePairs = null; + }); } return TranslationsParent.#languagePairs; } @@ -1671,7 +1822,8 @@ export class TranslationsParent extends JSWindowActorParent { `Translation model fetched in ${duration / 1000} seconds:`, record.fromLang, record.toLang, - record.fileType + record.fileType, + record.version ); }) ); @@ -1901,7 +2053,8 @@ export class TranslationsParent extends JSWindowActorParent { if (this.languageState.requestedTranslationPair) { // This page has already been translated, restore it and translate it // again once the actor has been recreated. - TranslationsParent.#translateOnPageReload = { fromLanguage, toLanguage }; + const windowState = this.getWindowState(); + windowState.translateOnPageReload = { fromLanguage, toLanguage }; this.restorePage(fromLanguage); } else { const { docLangTag } = this.languageState.detectedLanguages; @@ -1970,47 +2123,29 @@ export class TranslationsParent extends JSWindowActorParent { restorePage() { TranslationsParent.telemetry().onRestorePage(); // Skip auto-translate for one page load. - TranslationsParent.#isPageRestored = true; + const windowState = this.getWindowState(); + windowState.isPageRestored = true; this.languageState.requestedTranslationPair = null; - TranslationsParent.#previousDetectedLanguages = + windowState.previousDetectedLanguages = this.languageState.detectedLanguages; const browser = this.browsingContext.embedderElement; browser.reload(); } - /** - * Keep track of when the location changes. - */ - static #locationChangeId = 0; - static onLocationChange(browser) { if (!lazy.translationsEnabledPref) { // The pref isn't enabled, so don't attempt to get the actor. return; } - let windowGlobal = browser.browsingContext.currentWindowGlobal; - TranslationsParent.#locationChangeId++; let actor; try { - actor = windowGlobal.getActor("Translations"); - } catch (_) { - // The actor may not be supported on this page. - } - if (actor) { - actor.languageState.locationChangeId = - TranslationsParent.#locationChangeId; + actor = + browser.browsingContext.currentWindowGlobal.getActor("Translations"); + } catch { + // The actor may not be supported on this page, which throws an error. } - } - - /** - * Is this actor active for the current location change? - * - * @param {number} locationChangeId - The id sent by the "TranslationsParent:LanguageState" event. - * @returns {boolean} - */ - static isActiveLocation(locationChangeId) { - return locationChangeId === TranslationsParent.#locationChangeId; + actor?.languageState.locationChanged(); } async queryIdentifyLanguage() { @@ -2046,7 +2181,7 @@ export class TranslationsParent extends JSWindowActorParent { langTags.docLangTag && langTags.userLangTag && langTags.isDocLangTagSupported && - TranslationsParent.#maybeAutoTranslate(langTags) && + this.#maybeAutoTranslate(langTags) && !TranslationsParent.shouldNeverTranslateLanguage(langTags.docLangTag) && !this.shouldNeverTranslateSite() ) { @@ -2599,7 +2734,6 @@ class TranslationsLanguageState { constructor(actor, previousDetectedLanguages = null) { this.#actor = actor; this.#detectedLanguages = previousDetectedLanguages; - this.dispatch(); } /** @@ -2616,9 +2750,6 @@ class TranslationsLanguageState { /** @type {LangTags | null} */ #detectedLanguages = null; - /** @type {number} */ - #locationChangeId = -1; - /** @type {null | TranslationErrors} */ #error = null; @@ -2628,11 +2759,6 @@ class TranslationsLanguageState { * Dispatch anytime the language details change, so that any UI can react to it. */ dispatch() { - if (!TranslationsParent.isActiveLocation(this.#locationChangeId)) { - // Do not dispatch as this location is not active. - return; - } - const browser = this.#actor.browsingContext.top.embedderElement; if (!browser) { return; @@ -2690,26 +2816,11 @@ class TranslationsLanguageState { } /** - * This id represents the last location change that happened for this actor. This - * allows the UI to disambiguate when there are races and out of order events that - * are dispatched. Only the most up to date `locationChangeId` is used. - * - * @returns {number} + * When the location changes remove the previous error and dispatch a change event + * so that any browser chrome UI that needs to be updated can get the latest state. */ - get locationChangeId() { - return this.#locationChangeId; - } - - set locationChangeId(locationChangeId) { - if (this.#locationChangeId === locationChangeId) { - return; - } - - this.#locationChangeId = locationChangeId; - - // When the location changes remove the previous error. + locationChanged() { this.#error = null; - this.dispatch(); } diff --git a/toolkit/components/translations/content/Translator.mjs b/toolkit/components/translations/content/Translator.mjs new file mode 100644 index 0000000000..9a0de6a2c2 --- /dev/null +++ b/toolkit/components/translations/content/Translator.mjs @@ -0,0 +1,227 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This class manages the communications to the translations engine via MessagePort. + */ +export class Translator { + /** + * The port through with to communicate with the Translations Engine. + * + * @type {MessagePort} + */ + #port; + + /** + * True if the current #port is closed, otherwise false. + * + * @type {boolean} + */ + #portClosed = true; + + /** + * A promise that resolves when the Translator has successfully established communication with + * the translations engine, or rejects if communication was not successfully established. + * + * @type {Promise<void>} + */ + #ready = Promise.reject; + + /** + * The BCP-47 language tag for the from-language. + * + * @type {string} + */ + #fromLanguage; + + /** + * The BCP-47 language tag for the to-language. + * + * @type {string} + */ + #toLanguage; + + /** + * The callback function to request a new port, provided at construction time + * by the caller. + * + * @type {Function} + */ + #requestTranslationsPort; + + /** + * An id for each message sent. This is used to match up the request and response. + * + * @type {number} + */ + #nextMessageId = 0; + + /** + * Tie together a message id to a resolved response. + * + * @type {Map<number, TranslationRequest>} + */ + #requests = new Map(); + + /** + * Initializes a new Translator. + * + * Prefer using the Translator.create() function. + * + * @see Translator.create + * + * @param {string} fromLanguage - The BCP-47 from-language tag. + * @param {string} toLanguage - The BCP-47 to-language tag. + * @param {Function} requestTranslationsPort - A callback function to request a new MessagePort. + */ + constructor(fromLanguage, toLanguage, requestTranslationsPort) { + this.#fromLanguage = fromLanguage; + this.#toLanguage = toLanguage; + this.#requestTranslationsPort = requestTranslationsPort; + this.#createNewPortIfClosed(); + } + + /** + * @returns {Promise<void>} A promise that indicates if the Translator is ready to translate. + */ + get ready() { + return this.#ready; + } + + /** + * @returns {boolean} True if the translation port is closed, false otherwise. + */ + get portClosed() { + return this.#portClosed; + } + + /** + * @returns {string} The BCP-47 language tag of the from-language. + */ + get fromLanguage() { + return this.#fromLanguage; + } + + /** + * @returns {string} The BCP-47 language tag of the to-language. + */ + get toLanguage() { + return this.#toLanguage; + } + + /** + * Opens up a port and creates a new translator. + * + * @param {string} fromLanguage + * @param {string} toLanguage + * @returns {Promise<Translator>} + */ + static async create(fromLanguage, toLanguage, requestTranslationsPort) { + if (!fromLanguage || !toLanguage || !requestTranslationsPort) { + return undefined; + } + + const translator = new Translator( + fromLanguage, + toLanguage, + requestTranslationsPort + ); + await translator.ready; + + return translator; + } + + /** + * Creates a new translation port if the current one is closed. + * + * @returns {Promise<void>} - Whether the Translator is ready to translate. + */ + async #createNewPortIfClosed() { + if (!this.#portClosed) { + return this.#ready; + } + + this.#port = await this.#requestTranslationsPort( + this.#fromLanguage, + this.#toLanguage + ); + + // Create a promise that will be resolved when the engine is ready. + const { promise, resolve, reject } = Promise.withResolvers(); + + // Match up a response on the port to message that was sent. + this.#port.onmessage = ({ data }) => { + switch (data.type) { + case "TranslationsPort:TranslationResponse": { + const { targetText, messageId } = data; + // A request may not match match a messageId if there is a race during the pausing + // and discarding of the queue. + this.#requests.get(messageId)?.resolve(targetText); + break; + } + case "TranslationsPort:GetEngineStatusResponse": { + if (data.status === "ready") { + this.#portClosed = false; + resolve(); + } else { + this.#portClosed = true; + reject(); + } + break; + } + case "TranslationsPort:EngineTerminated": { + this.#portClosed = true; + break; + } + default: + break; + } + }; + + this.#ready = promise; + this.#port.postMessage({ type: "TranslationsPort:GetEngineStatusRequest" }); + + return this.#ready; + } + + /** + * Send a request to translate text to the Translations Engine. If it returns `null` + * then the request is stale. A rejection means there was an error in the translation. + * This request may be queued. + * + * @param {string} sourceText + * @returns {Promise<string>} + */ + async translate(sourceText, isHTML = false) { + await this.#createNewPortIfClosed(); + const { promise, resolve, reject } = Promise.withResolvers(); + const messageId = this.#nextMessageId++; + + // Store the "resolve" for the promise. It will be matched back up with the + // `messageId` in #handlePortMessage. + this.#requests.set(messageId, { + sourceText, + isHTML, + resolve, + reject, + }); + this.#port.postMessage({ + type: "TranslationsPort:TranslationRequest", + messageId, + sourceText, + isHTML, + }); + + return promise; + } + + /** + * Close the port and remove any pending or queued requests. + */ + destroy() { + this.#port.close(); + this.#portClosed = true; + this.#ready = Promise.reject; + } +} diff --git a/toolkit/components/translations/content/translations.mjs b/toolkit/components/translations/content/translations.mjs index 478f854bb5..d5541408d4 100644 --- a/toolkit/components/translations/content/translations.mjs +++ b/toolkit/components/translations/content/translations.mjs @@ -10,6 +10,8 @@ AT_logError, AT_createTranslationsPort, AT_isHtmlTranslation, AT_isTranslationEngineSupported, AT_identifyLanguage */ +import { Translator } from "chrome://global/content/translations/Translator.mjs"; + // Allow tests to override this value so that they can run faster. // This is the delay in milliseconds. window.DEBOUNCE_DELAY = 200; @@ -66,7 +68,7 @@ class TranslationsState { * The translator is only valid for a single language pair, and needs * to be recreated if the language pair changes. * - * @type {null | Promise<Translator>} + * @type {null | Translator} */ translator = null; @@ -133,42 +135,28 @@ class TranslationsState { onDebounce: async () => { // The contents of "this" can change between async steps, store a local variable // binding of these values. - const { - fromLanguage, - toLanguage, - messageToTranslate, - translator: translatorPromise, - } = this; + const { fromLanguage, toLanguage, messageToTranslate, translator } = this; if (!this.isTranslationEngineSupported) { // Never translate when the engine isn't supported. return; } - if ( - !fromLanguage || - !toLanguage || - !messageToTranslate || - !translatorPromise - ) { + if (!fromLanguage || !toLanguage || !messageToTranslate || !translator) { // Not everything is set for translation. this.ui.updateTranslation(""); return; } - const [translator] = await Promise.all([ - // Ensure the engine is ready to go. - translatorPromise, - // Ensure the previous translation has finished so that only the latest - // translation goes through. - this.translationRequest, - ]); + // Ensure the previous translation has finished so that only the latest + // translation goes through. + await this.translationRequest; if ( // Check if the current configuration has changed and if this is stale. If so // then skip this request, as there is already a newer request with more up to // date information. - this.translator !== translatorPromise || + this.translator !== translator || this.fromLanguage !== fromLanguage || this.toLanguage !== toLanguage || this.messageToTranslate !== messageToTranslate @@ -177,8 +165,10 @@ class TranslationsState { } const start = performance.now(); - - this.translationRequest = translator.translate(messageToTranslate); + this.translationRequest = this.translator.translate( + messageToTranslate, + AT_isHtmlTranslation() + ); const translation = await this.translationRequest; // The measure events will show up in the Firefox Profiler. @@ -236,15 +226,40 @@ class TranslationsState { `Creating a new translator for "${this.fromLanguage}" to "${this.toLanguage}"` ); - this.translator = Translator.create(this.fromLanguage, this.toLanguage); - this.maybeRequestTranslation(); + const translationPortPromise = (fromLanguage, toLanguage) => { + const { promise, resolve } = Promise.withResolvers(); + + const getResponse = ({ data }) => { + if ( + data.type == "GetTranslationsPort" && + data.fromLanguage === fromLanguage && + data.toLanguage === toLanguage + ) { + window.removeEventListener("message", getResponse); + resolve(data.port); + } + }; + + window.addEventListener("message", getResponse); + AT_createTranslationsPort(fromLanguage, toLanguage); + + return promise; + }; try { - await this.translator; + const translatorPromise = Translator.create( + this.fromLanguage, + this.toLanguage, + translationPortPromise + ); const duration = performance.now() - start; + // Signal to tests that the translator was created so they can exit. window.postMessage("translator-ready"); AT_log(`Created a new Translator in ${duration / 1000} seconds`); + + this.translator = await translatorPromise; + this.maybeRequestTranslation(); } catch (error) { this.ui.showInfo("about-translations-engine-error"); AT_logError("Failed to get the Translations worker", error); @@ -655,137 +670,3 @@ function debounce({ onDebounce, doEveryTime }) { }, timeLeft); }; } - -/** - * Perform transalations over a `MessagePort`. This class manages the communications to - * the translations engine. - */ -class Translator { - /** - * @type {MessagePort} - */ - #port; - - /** - * An id for each message sent. This is used to match up the request and response. - */ - #nextMessageId = 0; - - /** - * Tie together a message id to a resolved response. - * - * @type {Map<number, TranslationRequest>} - */ - #requests = new Map(); - - engineStatus = "initializing"; - - /** - * @param {MessagePort} port - */ - constructor(port) { - this.#port = port; - - // Create a promise that will be resolved when the engine is ready. - let engineLoaded; - let engineFailed; - this.ready = new Promise((resolve, reject) => { - engineLoaded = resolve; - engineFailed = reject; - }); - - // Match up a response on the port to message that was sent. - port.onmessage = ({ data }) => { - switch (data.type) { - case "TranslationsPort:TranslationResponse": { - const { targetText, messageId } = data; - // A request may not match match a messageId if there is a race during the pausing - // and discarding of the queue. - this.#requests.get(messageId)?.resolve(targetText); - break; - } - case "TranslationsPort:GetEngineStatusResponse": { - if (data.status === "ready") { - engineLoaded(); - } else { - engineFailed(); - } - break; - } - default: - AT_logError("Unknown translations port message: " + data.type); - break; - } - }; - - port.postMessage({ type: "TranslationsPort:GetEngineStatusRequest" }); - } - - /** - * Opens up a port and creates a new translator. - * - * @param {string} fromLanguage - * @param {string} toLanguage - * @returns {Promise<Translator>} - */ - static create(fromLanguage, toLanguage) { - return new Promise((resolve, reject) => { - AT_createTranslationsPort(fromLanguage, toLanguage); - - function getResponse({ data }) { - if ( - data.type == "GetTranslationsPort" && - fromLanguage === data.fromLanguage && - toLanguage === data.toLanguage - ) { - // The response matches, resolve the port. - const translator = new Translator(data.port); - - // Resolve the translator once it is ready, or propagate the rejection - // if it failed. - translator.ready.then(() => resolve(translator), reject); - window.removeEventListener("message", getResponse); - } - } - - // Listen for a response for the message port. - window.addEventListener("message", getResponse); - }); - } - - /** - * Send a request to translate text to the Translations Engine. If it returns `null` - * then the request is stale. A rejection means there was an error in the translation. - * This request may be queued. - * - * @param {string} sourceText - * @returns {Promise<string>} - */ - translate(sourceText) { - return new Promise((resolve, reject) => { - const messageId = this.#nextMessageId++; - // Store the "resolve" for the promise. It will be matched back up with the - // `messageId` in #handlePortMessage. - const isHTML = AT_isHtmlTranslation(); - this.#requests.set(messageId, { - sourceText, - isHTML, - resolve, - reject, - }); - this.#port.postMessage({ - type: "TranslationsPort:TranslationRequest", - messageId, - sourceText, - isHTML, - }); - }); - } - - /** - * Close the port and remove any pending or queued requests. - */ - destroy() { - this.#port.close(); - } -} diff --git a/toolkit/components/translations/jar.mn b/toolkit/components/translations/jar.mn index b31f17718f..1dde76d3ff 100644 --- a/toolkit/components/translations/jar.mn +++ b/toolkit/components/translations/jar.mn @@ -11,6 +11,7 @@ toolkit.jar: content/global/translations/translations.html (content/translations.html) content/global/translations/translations.css (content/translations.css) content/global/translations/translations.mjs (content/translations.mjs) + content/global/translations/Translator.mjs (content/Translator.mjs) content/global/translations/TranslationsTelemetry.sys.mjs (TranslationsTelemetry.sys.mjs) # Uncomment this line to test a local build of Bergamot. It will automatically be loaded in. diff --git a/toolkit/components/translations/moz.build b/toolkit/components/translations/moz.build index cdb0ca4e75..0e6c222126 100644 --- a/toolkit/components/translations/moz.build +++ b/toolkit/components/translations/moz.build @@ -7,7 +7,7 @@ SPHINX_TREES["/toolkit/components/translations"] = "docs" JAR_MANIFESTS += ["jar.mn"] with Files("**"): - BUG_COMPONENT = ("Firefox", "Translation") + BUG_COMPONENT = ("Firefox", "Translations") DIRS += ["actors"] diff --git a/toolkit/components/translations/tests/browser/browser.toml b/toolkit/components/translations/tests/browser/browser.toml index 50a1be7150..c940bea9a0 100644 --- a/toolkit/components/translations/tests/browser/browser.toml +++ b/toolkit/components/translations/tests/browser/browser.toml @@ -9,6 +9,7 @@ support-files = [ "translations-tester-es-2.html", "translations-tester-fr.html", "translations-tester-no-tag.html", + "translations-tester-select.html", "translations-tester-shadow-dom-es.html", "translations-tester-shadow-dom-mutation-es.html", "translations-tester-shadow-dom-mutation-es-2.html", diff --git a/toolkit/components/translations/tests/browser/browser_about_translations_dropdowns.js b/toolkit/components/translations/tests/browser/browser_about_translations_dropdowns.js index 6aed512952..381903e5b1 100644 --- a/toolkit/components/translations/tests/browser/browser_about_translations_dropdowns.js +++ b/toolkit/components/translations/tests/browser/browser_about_translations_dropdowns.js @@ -13,7 +13,7 @@ add_task(async function test_about_translations_dropdowns() { await openAboutTranslations({ languagePairs, dataForContent: languagePairs, - runInPage: async ({ dataForContent: languagePairs, selectors }) => { + runInPage: async ({ selectors }) => { const { document } = content; await ContentTaskUtils.waitForCondition( diff --git a/toolkit/components/translations/tests/browser/shared-head.js b/toolkit/components/translations/tests/browser/shared-head.js index 82b3e783a7..afa060c8a3 100644 --- a/toolkit/components/translations/tests/browser/shared-head.js +++ b/toolkit/components/translations/tests/browser/shared-head.js @@ -9,6 +9,9 @@ const { EngineProcess } = ChromeUtils.importESModule( "chrome://global/content/ml/EngineProcess.sys.mjs" ); +const { TranslationsPanelShared } = ChromeUtils.importESModule( + "chrome://browser/content/translations/TranslationsPanelShared.sys.mjs" +); // Avoid about:blank's non-standard behavior. const BLANK_PAGE = @@ -32,6 +35,8 @@ const NO_LANGUAGE_URL = URL_COM_PREFIX + DIR_PATH + "translations-tester-no-tag.html"; const EMPTY_PDF_URL = URL_COM_PREFIX + DIR_PATH + "translations-tester-empty-pdf-file.pdf"; +const SELECT_TEST_PAGE_URL = + URL_COM_PREFIX + DIR_PATH + "translations-tester-select.html"; const PIVOT_LANGUAGE = "en"; const LANGUAGE_PAIRS = [ @@ -349,33 +354,37 @@ function getTranslationsParent() { /** * Closes all open panels and menu popups related to Translations. + * + * @param {ChromeWindow} [win] */ -async function closeAllOpenPanelsAndMenus() { - await closeSettingsMenuIfOpen(); - await closeFullPageTranslationsPanelIfOpen(); - await closeSelectTranslationsPanelIfOpen(); - await closeContextMenuIfOpen(); +async function closeAllOpenPanelsAndMenus(win) { + await closeSettingsMenuIfOpen(win); + await closeFullPageTranslationsPanelIfOpen(win); + await closeSelectTranslationsPanelIfOpen(win); + await closeContextMenuIfOpen(win); } /** * Closes the popup element with the given Id if it is open. * * @param {string} popupElementId + * @param {ChromeWindow} [win] */ -async function closePopupIfOpen(popupElementId) { +async function closePopupIfOpen(popupElementId, win = window) { await waitForCondition(async () => { - const contextMenu = document.getElementById(popupElementId); - if (!contextMenu) { + const popupElement = win.document.getElementById(popupElementId); + if (!popupElement) { return true; } - if (contextMenu.state === "closed") { + if (popupElement.state === "closed") { return true; } let popuphiddenPromise = BrowserTestUtils.waitForEvent( - contextMenu, + popupElement, "popuphidden" ); - PanelMultiView.hidePopup(contextMenu); + popupElement.hidePopup(); + PanelMultiView.hidePopup(popupElement); await popuphiddenPromise; return false; }); @@ -383,30 +392,41 @@ async function closePopupIfOpen(popupElementId) { /** * Closes the context menu if it is open. + * + * @param {ChromeWindow} [win] */ -async function closeContextMenuIfOpen() { - await closePopupIfOpen("contentAreaContextMenu"); +async function closeContextMenuIfOpen(win) { + await closePopupIfOpen("contentAreaContextMenu", win); } /** * Closes the translations panel settings menu if it is open. + * + * @param {ChromeWindow} [win] */ -async function closeSettingsMenuIfOpen() { - await closePopupIfOpen("full-page-translations-panel-settings-menupopup"); +async function closeSettingsMenuIfOpen(win) { + await closePopupIfOpen( + "full-page-translations-panel-settings-menupopup", + win + ); } /** * Closes the translations panel if it is open. + * + * @param {ChromeWindow} [win] */ -async function closeFullPageTranslationsPanelIfOpen() { - await closePopupIfOpen("full-page-translations-panel"); +async function closeFullPageTranslationsPanelIfOpen(win) { + await closePopupIfOpen("full-page-translations-panel", win); } /** * Closes the translations panel if it is open. + * + * @param {ChromeWindow} [win] */ -async function closeSelectTranslationsPanelIfOpen() { - await closePopupIfOpen("select-translations-panel"); +async function closeSelectTranslationsPanelIfOpen(win) { + await closePopupIfOpen("select-translations-panel", win); } /** @@ -471,6 +491,7 @@ async function createAndMockRemoteSettings({ // The TranslationsParent will pull the language pair values from the JSON dump // of Remote Settings. Clear these before mocking the translations engine. TranslationsParent.clearCache(); + TranslationsPanelShared.clearCache(); TranslationsParent.mockTranslationsEngine( remoteClients.translationModels.client, @@ -485,6 +506,7 @@ async function createAndMockRemoteSettings({ TranslationsParent.unmockTranslationsEngine(); TranslationsParent.clearCache(); + TranslationsPanelShared.clearCache(); }, remoteClients, }; @@ -497,38 +519,56 @@ async function loadTestPage({ prefs, autoOffer, permissionsUrls, + win = window, }) { info(`Loading test page starting at url: ${page}`); - // Ensure no engine is being carried over from a previous test. - await EngineProcess.destroyTranslationsEngine(); - Services.fog.testResetFOG(); - await SpecialPowers.pushPrefEnv({ - set: [ - // Enabled by default. - ["browser.translations.enable", true], - ["browser.translations.logLevel", "All"], - ["browser.translations.panelShown", true], - ["browser.translations.automaticallyPopup", true], - ["browser.translations.alwaysTranslateLanguages", ""], - ["browser.translations.neverTranslateLanguages", ""], - ...(prefs ?? []), - ], - }); - await SpecialPowers.pushPermissions( - [ - ENGLISH_PAGE_URL, - FRENCH_PAGE_URL, - NO_LANGUAGE_URL, - SPANISH_PAGE_URL, - SPANISH_PAGE_URL_2, - SPANISH_PAGE_URL_DOT_ORG, - ...(permissionsUrls || []), - ].map(url => ({ - type: TRANSLATIONS_PERMISSION, - allow: true, - context: url, - })) - ); + + // If there are multiple windows, only do the first time setup on the main window. + const isFirstTimeSetup = win === window; + + let remoteClients = null; + let removeMocks = () => {}; + + if (isFirstTimeSetup) { + // Ensure no engine is being carried over from a previous test. + await EngineProcess.destroyTranslationsEngine(); + + Services.fog.testResetFOG(); + await SpecialPowers.pushPrefEnv({ + set: [ + // Enabled by default. + ["browser.translations.enable", true], + ["browser.translations.logLevel", "All"], + ["browser.translations.panelShown", true], + ["browser.translations.automaticallyPopup", true], + ["browser.translations.alwaysTranslateLanguages", ""], + ["browser.translations.neverTranslateLanguages", ""], + ...(prefs ?? []), + ], + }); + await SpecialPowers.pushPermissions( + [ + ENGLISH_PAGE_URL, + FRENCH_PAGE_URL, + NO_LANGUAGE_URL, + SPANISH_PAGE_URL, + SPANISH_PAGE_URL_2, + SPANISH_PAGE_URL_DOT_ORG, + ...(permissionsUrls || []), + ].map(url => ({ + type: TRANSLATIONS_PERMISSION, + allow: true, + context: url, + })) + ); + + const result = await createAndMockRemoteSettings({ + languagePairs, + autoDownloadFromRemoteSettings, + }); + remoteClients = result.remoteClients; + removeMocks = result.removeMocks; + } if (autoOffer) { TranslationsParent.testAutomaticPopup = true; @@ -536,16 +576,11 @@ async function loadTestPage({ // Start the tab at a blank page. const tab = await BrowserTestUtils.openNewForegroundTab( - gBrowser, + win.gBrowser, BLANK_PAGE, true // waitForLoad ); - const { remoteClients, removeMocks } = await createAndMockRemoteSettings({ - languagePairs, - autoDownloadFromRemoteSettings, - }); - BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, page); await BrowserTestUtils.browserLoaded(tab.linkedBrowser); @@ -1377,7 +1412,7 @@ async function waitForCloseDialogWindow(dialogWindow) { // Extracted from https://searchfox.org/mozilla-central/rev/40ef22080910c2e2c27d9e2120642376b1d8b8b2/browser/components/preferences/in-content/tests/head.js#41 function promiseLoadSubDialog(aURL) { - return new Promise((resolve, reject) => { + return new Promise(resolve => { content.gSubDialog._dialogStack.addEventListener( "dialogopen", function dialogopen(aEvent) { @@ -1444,3 +1479,10 @@ async function loadBlankPage() { BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, BLANK_PAGE); await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); } + +/** + * Destroys the Translations Engine process. + */ +async function destroyTranslationsEngine() { + await EngineProcess.destroyTranslationsEngine(); +} diff --git a/toolkit/components/translations/tests/browser/translations-test.mjs b/toolkit/components/translations/tests/browser/translations-test.mjs index a740a2d1cc..3ff107a699 100644 --- a/toolkit/components/translations/tests/browser/translations-test.mjs +++ b/toolkit/components/translations/tests/browser/translations-test.mjs @@ -38,21 +38,36 @@ export function getSelectors() { getHeader() { return content.document.querySelector("header"); }, - getFirstParagraph() { - return content.document.querySelector("p:first-of-type"); - }, getLastParagraph() { return content.document.querySelector("p:last-of-type"); }, - getSpanishParagraph() { - return content.document.getElementById("spanish-paragraph"); + getFrenchSection() { + return content.document.getElementById("french-section"); }, - getSpanishHyperlink() { - return content.document.getElementById("spanish-hyperlink"); + getEnglishSection() { + return content.document.getElementById("english-section"); + }, + getSpanishSection() { + return content.document.getElementById("spanish-section"); + }, + getFrenchSentence() { + return content.document.getElementById("french-sentence"); + }, + getEnglishSentence() { + return content.document.getElementById("english-sentence"); + }, + getSpanishSentence() { + return content.document.getElementById("spanish-sentence"); }, getEnglishHyperlink() { return content.document.getElementById("english-hyperlink"); }, + getFrenchHyperlink() { + return content.document.getElementById("french-hyperlink"); + }, + getSpanishHyperlink() { + return content.document.getElementById("spanish-hyperlink"); + }, }; } diff --git a/toolkit/components/translations/tests/browser/translations-tester-es.html b/toolkit/components/translations/tests/browser/translations-tester-es.html index f589df0649..abf2d42c62 100644 --- a/toolkit/components/translations/tests/browser/translations-tester-es.html +++ b/toolkit/components/translations/tests/browser/translations-tester-es.html @@ -20,7 +20,7 @@ <header lang="en">The following is an excerpt from Don Quijote de la Mancha, which is in the public domain</header> <h1>Don Quijote de La Mancha</h1> <h2>Capítulo VIII.</h2> - <p id="spanish-paragraph">Del buen suceso que el valeroso don Quijote tuvo en la espantable y jamás imaginada aventura de los molinos de viento, con otros sucesos dignos de felice recordación</p> + <p>Del buen suceso que el valeroso don Quijote tuvo en la espantable y jamás imaginada aventura de los molinos de viento, con otros sucesos dignos de felice recordación</p> <p>En esto, descubrieron treinta o cuarenta molinos de viento que hay en aquel campo; y, así como don Quijote los vio, dijo a su escudero:</p> <p>— La ventura va guiando nuestras cosas mejor de lo que acertáramos a desear, porque ves allí, amigo Sancho Panza, donde se descubren treinta, o pocos más, desaforados gigantes, con quien pienso hacer batalla y quitarles a todos las vidas, con cuyos despojos comenzaremos a enriquecer; que ésta es buena guerra, y es gran servicio de Dios quitar tan mala simiente de sobre la faz de la tierra.</p> <p>— ¿Qué gigantes? —dijo Sancho Panza.</p> @@ -32,13 +32,5 @@ <p>Levantóse en esto un poco de viento y las grandes aspas comenzaron a moverse, lo cual visto por don Quijote, dijo:</p> <p>— Pues, aunque mováis más brazos que los del gigante Briareo, me lo habéis de pagar.</p> </div> - <div> - <header lang="en">The following is a link to another test page in Spanish.</header> - <p><a id="spanish-hyperlink" href="https://example.org/browser/translations-tester-es.html">Otra pagina en español.</a></p> - </div> - <div> - <header lang="en">The following is a link to another test page in English.</header> - <p lang="en"><a id="english-hyperlink" href="https://example.org/browser/translations-tester-en.html">Another page in English.</a></p> - </div> </body> </html> diff --git a/toolkit/components/translations/tests/browser/translations-tester-select.html b/toolkit/components/translations/tests/browser/translations-tester-select.html new file mode 100644 index 0000000000..034d89a95b --- /dev/null +++ b/toolkit/components/translations/tests/browser/translations-tester-select.html @@ -0,0 +1,76 @@ +<!DOCTYPE html> +<html lang="es"> +<head> + <meta charset="utf-8" /> + <title>Select Translations Test</title> + <style> + div { + margin: 10px auto; + width: 600px + } + p { + margin: 47px 0; + font-size: 21px; + line-height: 2; + } + </style> +</head> +<body> + <div> + <header lang="en">The following is an excerpt from Don Quijote de la Mancha, which is in the public domain</header> + <div id="spanish-section"> + <h1>Don Quijote de La Mancha</h1> + <h2>Capítulo VIII.</h2> + <p id="spanish-sentence">Del buen suceso que el valeroso don Quijote tuvo en la espantable y jamás imaginada aventura de los molinos de viento, con otros sucesos dignos de felice recordación</p> + <p>En esto, descubrieron treinta o cuarenta molinos de viento que hay en aquel campo; y, así como don Quijote los vio, dijo a su escudero:</p> + <p>— La ventura va guiando nuestras cosas mejor de lo que acertáramos a desear, porque ves allí, amigo Sancho Panza, donde se descubren treinta, o pocos más, desaforados gigantes, con quien pienso hacer batalla y quitarles a todos las vidas, con cuyos despojos comenzaremos a enriquecer; que ésta es buena guerra, y es gran servicio de Dios quitar tan mala simiente de sobre la faz de la tierra.</p> + <p>— ¿Qué gigantes? —dijo Sancho Panza.</p> + <p>— Aquellos que allí ves —respondió su amo— de los brazos largos, que los suelen tener algunos de casi dos leguas.</p> + <p>— Mire vuestra merced —respondió Sancho— que aquellos que allí se parecen no son gigantes, sino molinos de viento, y lo que en ellos parecen brazos son las aspas, que, volteadas del viento, hacen andar la piedra del molino.</p> + <p>— Bien parece —respondió don Quijote— que no estás cursado en esto de las aventuras: ellos son gigantes; y si tienes miedo, quítate de ahí, y ponte en oración en el espacio que yo voy a entrar con ellos en fiera y desigual batalla.</p> + <p>Y, diciendo esto, dio de espuelas a su caballo Rocinante, sin atender a las voces que su escudero Sancho le daba, advirtiéndole que, sin duda alguna, eran molinos de viento, y no gigantes, aquellos que iba a acometer. Pero él iba tan puesto en que eran gigantes, que ni oía las voces de su escudero Sancho ni echaba de ver, aunque estaba ya bien cerca, lo que eran; antes, iba diciendo en voces altas:</p> + <p>— Non fuyades, cobardes y viles criaturas, que un solo caballero es el que os acomete.</p> + <p>Levantóse en esto un poco de viento y las grandes aspas comenzaron a moverse, lo cual visto por don Quijote, dijo:</p> + <p>— Pues, aunque mováis más brazos que los del gigante Briareo, me lo habéis de pagar.</p> + </div> + </div> + <div> + <header lang="en">The following is an excerpt from Frankenstein, which is in the public domain</header> + <div id="english-section"> + <h1>Frankenstein</h1> + <h2>Letter 3</h2> + <p>To Mrs. Saville, England.</p> + <p>July 7th, 17—.</p> + <p>My dear Sister,</p> + <p>I write a few lines in haste to say that I am safe—and well advanced on my voyage. This letter will reach England by a merchantman now on its homeward voyage from Archangel; more fortunate than I, who may not see my native land, perhaps, for many years. I am, however, in good spirits: my men are bold and apparently firm of purpose, nor do the floating sheets of ice that continually pass us, indicating the dangers of the region towards which we are advancing, appear to dismay them. We have already reached a very high latitude; but it is the height of summer, and although not so warm as in England, the southern gales, which blow us speedily towards those shores which I so ardently desire to attain, breathe a degree of renovating warmth which I had not expected.</p> + <p>No incidents have hitherto befallen us that would make a figure in a letter. One or two stiff gales and the springing of a leak are accidents which experienced navigators scarcely remember to record, and I shall be well content if nothing worse happen to us during our voyage.</p> + <p>Adieu, my dear Margaret. Be assured that for my own sake, as well as yours, I will not rashly encounter danger. I will be cool, persevering, and prudent.</p> + <p>But success shall crown my endeavours. Wherefore not? Thus far I have gone, tracing a secure way over the pathless seas, the very stars themselves being witnesses and testimonies of my triumph. Why not still proceed over the untamed yet obedient element? What can stop the determined heart and resolved will of man?</p> + <p id="english-sentence">My swelling heart involuntarily pours itself out thus. But I must finish. Heaven bless my beloved sister!</p> + </div> + </div> + <div> + <header lang="en">The following is an excerpt from Les Misérables, which is in the public domain</header> + <div id="french-section"> + <h1>Les Misérables</h1> + <h2>Chapitre II</h2> + <p>Monsieur Myriel devient monseigneur Bienvenu</p> + <p id="french-sentence">Le palais épiscopal de Digne était attenant à l'hôpital.</p> + <p>Le palais épiscopal était un vaste et bel hôtel bâti en pierre au commencement du siècle dernier par monseigneur Henri Puget, docteur en théologie de la faculté de Paris, abbé de Simore, lequel était évêque de Digne en 1712. Ce palais était un vrai logis seigneurial. Tout y avait grand air, les appartements de l'évêque, les salons, les chambres, la cour d'honneur, fort large, avec promenoirs à arcades, selon l'ancienne mode florentine, les jardins plantés de magnifiques arbres. Dans la salle à manger, longue et superbe galerie qui était au rez-de-chaussée et s'ouvrait sur les jardins, monseigneur Henri Puget avait donné à manger en cérémonie le 29 juillet 1714 à messeigneurs Charles Brûlart de Genlis, archevêque-prince d'Embrun, Antoine de Mesgrigny, capucin, évêque de Grasse, Philippe de Vendôme, grand prieur de France, abbé de Saint-Honoré de Lérins, François de Berton de Grillon, évêque-baron de Vence, César de Sabran de Forcalquier, évêque-seigneur de Glandève, et Jean Soanen, prêtre de l'oratoire, prédicateur ordinaire du roi, évêque-seigneur de Senez. Les portraits de ces sept révérends personnages décoraient cette salle, et cette date mémorable, 29 juillet 1714, y était gravée en lettres d'or sur une table de marbre blanc.</p> + <p>L'hôpital était une maison étroite et basse à un seul étage avec un petit jardin. Trois jours après son arrivée, l'évêque visita l'hôpital. La visite terminée, il fit prier le directeur de vouloir bien venir jusque chez lui.</p> + </div> + </div> + <div> + <header lang="en">The following is a link to another test page in Spanish.</header> + <p lang="es"><a id="spanish-hyperlink" href="https://example.org/browser/translations-tester-es.html">Otra pagina en español.</a></p> + </div> + <div> + <header lang="en">The following is a link to another test page in French.</header> + <p lang="fr"><a id="french-hyperlink" href="https://example.org/browser/translations-tester-fr.html">Une autre page en français.</a></p> + </div> + <div> + <header lang="en">The following is a link to another test page in English.</header> + <p lang="en"><a id="english-hyperlink" href="https://example.org/browser/translations-tester-en.html">Another page in English.</a></p> + </div> +</body> +</html> diff --git a/toolkit/components/translations/translations.d.ts b/toolkit/components/translations/translations.d.ts index cc8d462a9c..9823f6a845 100644 --- a/toolkit/components/translations/translations.d.ts +++ b/toolkit/components/translations/translations.d.ts @@ -269,3 +269,10 @@ export interface SupportedLanguages { } export type TranslationErrors = "engine-load-error"; + +export type SelectTranslationsPanelState = + | { phase: "closed"; } + | { phase: "idle"; fromLanguage: string; toLanguage: string, sourceText: string, } + | { phase: "translatable"; fromLanguage: string; toLanguage: string, sourceText: string, } + | { phase: "translating"; fromLanguage: string; toLanguage: string, sourceText: string, } + | { phase: "translated"; fromLanguage: string; toLanguage: string, sourceText: string, translatedText: string, } diff --git a/toolkit/components/uniffi-bindgen-gecko-js/Cargo.toml b/toolkit/components/uniffi-bindgen-gecko-js/Cargo.toml index 32232c64b9..5ef165dc73 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/Cargo.toml +++ b/toolkit/components/uniffi-bindgen-gecko-js/Cargo.toml @@ -15,7 +15,7 @@ clap = { version = "4", default-features = false, features = ["std", "derive", " extend = "1.1" heck = "0.4" uniffi = { workspace = true } -uniffi_bindgen = "0.25" +uniffi_bindgen = { workspace = true } serde = "1" toml = "0.5" camino = "1.0.8" diff --git a/toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustRelevancy.sys.mjs b/toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustRelevancy.sys.mjs new file mode 100644 index 0000000000..2a4663e1cb --- /dev/null +++ b/toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustRelevancy.sys.mjs @@ -0,0 +1,1249 @@ +// This file was autogenerated by the `uniffi-bindgen-gecko-js` crate. +// Trust me, you don't want to mess with it! + +import { UniFFITypeError } from "resource://gre/modules/UniFFI.sys.mjs"; + + + +// Objects intended to be used in the unit tests +export var UnitTestObjs = {}; + +// Write/Read data to/from an ArrayBuffer +class ArrayBufferDataStream { + constructor(arrayBuffer) { + this.dataView = new DataView(arrayBuffer); + this.pos = 0; + } + + readUint8() { + let rv = this.dataView.getUint8(this.pos); + this.pos += 1; + return rv; + } + + writeUint8(value) { + this.dataView.setUint8(this.pos, value); + this.pos += 1; + } + + readUint16() { + let rv = this.dataView.getUint16(this.pos); + this.pos += 2; + return rv; + } + + writeUint16(value) { + this.dataView.setUint16(this.pos, value); + this.pos += 2; + } + + readUint32() { + let rv = this.dataView.getUint32(this.pos); + this.pos += 4; + return rv; + } + + writeUint32(value) { + this.dataView.setUint32(this.pos, value); + this.pos += 4; + } + + readUint64() { + let rv = this.dataView.getBigUint64(this.pos); + this.pos += 8; + return Number(rv); + } + + writeUint64(value) { + this.dataView.setBigUint64(this.pos, BigInt(value)); + this.pos += 8; + } + + + readInt8() { + let rv = this.dataView.getInt8(this.pos); + this.pos += 1; + return rv; + } + + writeInt8(value) { + this.dataView.setInt8(this.pos, value); + this.pos += 1; + } + + readInt16() { + let rv = this.dataView.getInt16(this.pos); + this.pos += 2; + return rv; + } + + writeInt16(value) { + this.dataView.setInt16(this.pos, value); + this.pos += 2; + } + + readInt32() { + let rv = this.dataView.getInt32(this.pos); + this.pos += 4; + return rv; + } + + writeInt32(value) { + this.dataView.setInt32(this.pos, value); + this.pos += 4; + } + + readInt64() { + let rv = this.dataView.getBigInt64(this.pos); + this.pos += 8; + return Number(rv); + } + + writeInt64(value) { + this.dataView.setBigInt64(this.pos, BigInt(value)); + this.pos += 8; + } + + readFloat32() { + let rv = this.dataView.getFloat32(this.pos); + this.pos += 4; + return rv; + } + + writeFloat32(value) { + this.dataView.setFloat32(this.pos, value); + this.pos += 4; + } + + readFloat64() { + let rv = this.dataView.getFloat64(this.pos); + this.pos += 8; + return rv; + } + + writeFloat64(value) { + this.dataView.setFloat64(this.pos, value); + this.pos += 8; + } + + + writeString(value) { + const encoder = new TextEncoder(); + // Note: in order to efficiently write this data, we first write the + // string data, reserving 4 bytes for the size. + const dest = new Uint8Array(this.dataView.buffer, this.pos + 4); + const encodeResult = encoder.encodeInto(value, dest); + if (encodeResult.read != value.length) { + throw new UniFFIError( + "writeString: out of space when writing to ArrayBuffer. Did the computeSize() method returned the wrong result?" + ); + } + const size = encodeResult.written; + // Next, go back and write the size before the string data + this.dataView.setUint32(this.pos, size); + // Finally, advance our position past both the size and string data + this.pos += size + 4; + } + + readString() { + const decoder = new TextDecoder(); + const size = this.readUint32(); + const source = new Uint8Array(this.dataView.buffer, this.pos, size) + const value = decoder.decode(source); + this.pos += size; + return value; + } + + // Reads a RelevancyStore pointer from the data stream + // UniFFI Pointers are **always** 8 bytes long. That is enforced + // by the C++ and Rust Scaffolding code. + readPointerRelevancyStore() { + const pointerId = 0; // relevancy:RelevancyStore + const res = UniFFIScaffolding.readPointer(pointerId, this.dataView.buffer, this.pos); + this.pos += 8; + return res; + } + + // Writes a RelevancyStore pointer into the data stream + // UniFFI Pointers are **always** 8 bytes long. That is enforced + // by the C++ and Rust Scaffolding code. + writePointerRelevancyStore(value) { + const pointerId = 0; // relevancy:RelevancyStore + UniFFIScaffolding.writePointer(pointerId, value, this.dataView.buffer, this.pos); + this.pos += 8; + } + +} + +function handleRustResult(result, liftCallback, liftErrCallback) { + switch (result.code) { + case "success": + return liftCallback(result.data); + + case "error": + throw liftErrCallback(result.data); + + case "internal-error": + let message = result.internalErrorMessage; + if (message) { + throw new UniFFIInternalError(message); + } else { + throw new UniFFIInternalError("Unknown error"); + } + + default: + throw new UniFFIError(`Unexpected status code: ${result.code}`); + } +} + +class UniFFIError { + constructor(message) { + this.message = message; + } + + toString() { + return `UniFFIError: ${this.message}` + } +} + +class UniFFIInternalError extends UniFFIError {} + +// Base class for FFI converters +class FfiConverter { + // throw `UniFFITypeError` if a value to be converted has an invalid type + static checkType(value) { + if (value === undefined ) { + throw new UniFFITypeError(`undefined`); + } + if (value === null ) { + throw new UniFFITypeError(`null`); + } + } +} + +// Base class for FFI converters that lift/lower by reading/writing to an ArrayBuffer +class FfiConverterArrayBuffer extends FfiConverter { + static lift(buf) { + return this.read(new ArrayBufferDataStream(buf)); + } + + static lower(value) { + const buf = new ArrayBuffer(this.computeSize(value)); + const dataStream = new ArrayBufferDataStream(buf); + this.write(dataStream, value); + return buf; + } +} + +// Symbols that are used to ensure that Object constructors +// can only be used with a proper UniFFI pointer +const uniffiObjectPtr = Symbol("uniffiObjectPtr"); +const constructUniffiObject = Symbol("constructUniffiObject"); +UnitTestObjs.uniffiObjectPtr = uniffiObjectPtr; + +// Export the FFIConverter object to make external types work. +export class FfiConverterU32 extends FfiConverter { + static checkType(value) { + super.checkType(value); + if (!Number.isInteger(value)) { + throw new UniFFITypeError(`${value} is not an integer`); + } + if (value < 0 || value > 4294967295) { + throw new UniFFITypeError(`${value} exceeds the U32 bounds`); + } + } + static computeSize() { + return 4; + } + static lift(value) { + return value; + } + static lower(value) { + return value; + } + static write(dataStream, value) { + dataStream.writeUint32(value) + } + static read(dataStream) { + return dataStream.readUint32() + } +} + +// Export the FFIConverter object to make external types work. +export class FfiConverterString extends FfiConverter { + static checkType(value) { + super.checkType(value); + if (typeof value !== "string") { + throw new UniFFITypeError(`${value} is not a string`); + } + } + + static lift(buf) { + const decoder = new TextDecoder(); + const utf8Arr = new Uint8Array(buf); + return decoder.decode(utf8Arr); + } + static lower(value) { + const encoder = new TextEncoder(); + return encoder.encode(value).buffer; + } + + static write(dataStream, value) { + dataStream.writeString(value); + } + + static read(dataStream) { + return dataStream.readString(); + } + + static computeSize(value) { + const encoder = new TextEncoder(); + return 4 + encoder.encode(value).length + } +} + +export class RelevancyStore { + // Use `init` to instantiate this class. + // DO NOT USE THIS CONSTRUCTOR DIRECTLY + constructor(opts) { + if (!Object.prototype.hasOwnProperty.call(opts, constructUniffiObject)) { + throw new UniFFIError("Attempting to construct an object using the JavaScript constructor directly" + + "Please use a UDL defined constructor, or the init function for the primary constructor") + } + if (!opts[constructUniffiObject] instanceof UniFFIPointer) { + throw new UniFFIError("Attempting to create a UniFFI object with a pointer that is not an instance of UniFFIPointer") + } + this[uniffiObjectPtr] = opts[constructUniffiObject]; + } + /** + * An async constructor for RelevancyStore. + * + * @returns {Promise<RelevancyStore>}: A promise that resolves + * to a newly constructed RelevancyStore + */ + static init(dbpath) { + const liftResult = (result) => FfiConverterTypeRelevancyStore.lift(result); + const liftError = (data) => FfiConverterTypeRelevancyApiError.lift(data); + const functionCall = () => { + try { + FfiConverterString.checkType(dbpath) + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart("dbpath"); + } + throw e; + } + return UniFFIScaffolding.callAsync( + 1, // relevancy:uniffi_relevancy_fn_constructor_relevancystore_new + FfiConverterString.lower(dbpath), + ) + } + try { + return functionCall().then((result) => handleRustResult(result, liftResult, liftError)); + } catch (error) { + return Promise.reject(error) + }} + + calculateMetrics() { + const liftResult = (result) => FfiConverterTypeInterestMetrics.lift(result); + const liftError = (data) => FfiConverterTypeRelevancyApiError.lift(data); + const functionCall = () => { + return UniFFIScaffolding.callAsync( + 2, // relevancy:uniffi_relevancy_fn_method_relevancystore_calculate_metrics + FfiConverterTypeRelevancyStore.lower(this), + ) + } + try { + return functionCall().then((result) => handleRustResult(result, liftResult, liftError)); + } catch (error) { + return Promise.reject(error) + } + } + + ingest(topUrls) { + const liftResult = (result) => undefined; + const liftError = (data) => FfiConverterTypeRelevancyApiError.lift(data); + const functionCall = () => { + try { + FfiConverterSequencestring.checkType(topUrls) + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart("topUrls"); + } + throw e; + } + return UniFFIScaffolding.callAsync( + 3, // relevancy:uniffi_relevancy_fn_method_relevancystore_ingest + FfiConverterTypeRelevancyStore.lower(this), + FfiConverterSequencestring.lower(topUrls), + ) + } + try { + return functionCall().then((result) => handleRustResult(result, liftResult, liftError)); + } catch (error) { + return Promise.reject(error) + } + } + + userInterestVector() { + const liftResult = (result) => FfiConverterTypeInterestVector.lift(result); + const liftError = (data) => FfiConverterTypeRelevancyApiError.lift(data); + const functionCall = () => { + return UniFFIScaffolding.callAsync( + 4, // relevancy:uniffi_relevancy_fn_method_relevancystore_user_interest_vector + FfiConverterTypeRelevancyStore.lower(this), + ) + } + try { + return functionCall().then((result) => handleRustResult(result, liftResult, liftError)); + } catch (error) { + return Promise.reject(error) + } + } + +} + +// Export the FFIConverter object to make external types work. +export class FfiConverterTypeRelevancyStore extends FfiConverter { + static lift(value) { + const opts = {}; + opts[constructUniffiObject] = value; + return new RelevancyStore(opts); + } + + static lower(value) { + const ptr = value[uniffiObjectPtr]; + if (!(ptr instanceof UniFFIPointer)) { + throw new UniFFITypeError("Object is not a 'RelevancyStore' instance"); + } + return ptr; + } + + static read(dataStream) { + return this.lift(dataStream.readPointerRelevancyStore()); + } + + static write(dataStream, value) { + dataStream.writePointerRelevancyStore(value[uniffiObjectPtr]); + } + + static computeSize(value) { + return 8; + } +} + +export class InterestMetrics { + constructor({ topSingleInterestSimilarity, top2interestSimilarity, top3interestSimilarity } = {}) { + try { + FfiConverterU32.checkType(topSingleInterestSimilarity) + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart("topSingleInterestSimilarity"); + } + throw e; + } + try { + FfiConverterU32.checkType(top2interestSimilarity) + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart("top2interestSimilarity"); + } + throw e; + } + try { + FfiConverterU32.checkType(top3interestSimilarity) + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart("top3interestSimilarity"); + } + throw e; + } + this.topSingleInterestSimilarity = topSingleInterestSimilarity; + this.top2interestSimilarity = top2interestSimilarity; + this.top3interestSimilarity = top3interestSimilarity; + } + equals(other) { + return ( + this.topSingleInterestSimilarity == other.topSingleInterestSimilarity && + this.top2interestSimilarity == other.top2interestSimilarity && + this.top3interestSimilarity == other.top3interestSimilarity + ) + } +} + +// Export the FFIConverter object to make external types work. +export class FfiConverterTypeInterestMetrics extends FfiConverterArrayBuffer { + static read(dataStream) { + return new InterestMetrics({ + topSingleInterestSimilarity: FfiConverterU32.read(dataStream), + top2interestSimilarity: FfiConverterU32.read(dataStream), + top3interestSimilarity: FfiConverterU32.read(dataStream), + }); + } + static write(dataStream, value) { + FfiConverterU32.write(dataStream, value.topSingleInterestSimilarity); + FfiConverterU32.write(dataStream, value.top2interestSimilarity); + FfiConverterU32.write(dataStream, value.top3interestSimilarity); + } + + static computeSize(value) { + let totalSize = 0; + totalSize += FfiConverterU32.computeSize(value.topSingleInterestSimilarity); + totalSize += FfiConverterU32.computeSize(value.top2interestSimilarity); + totalSize += FfiConverterU32.computeSize(value.top3interestSimilarity); + return totalSize + } + + static checkType(value) { + super.checkType(value); + if (!(value instanceof InterestMetrics)) { + throw new UniFFITypeError(`Expected 'InterestMetrics', found '${typeof value}'`); + } + try { + FfiConverterU32.checkType(value.topSingleInterestSimilarity); + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart(".topSingleInterestSimilarity"); + } + throw e; + } + try { + FfiConverterU32.checkType(value.top2interestSimilarity); + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart(".top2interestSimilarity"); + } + throw e; + } + try { + FfiConverterU32.checkType(value.top3interestSimilarity); + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart(".top3interestSimilarity"); + } + throw e; + } + } +} + +export class InterestVector { + constructor({ animals, arts, autos, business, career, education, fashion, finance, food, government, health, hobbies, home, news, realEstate, society, sports, tech, travel, inconclusive } = {}) { + try { + FfiConverterU32.checkType(animals) + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart("animals"); + } + throw e; + } + try { + FfiConverterU32.checkType(arts) + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart("arts"); + } + throw e; + } + try { + FfiConverterU32.checkType(autos) + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart("autos"); + } + throw e; + } + try { + FfiConverterU32.checkType(business) + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart("business"); + } + throw e; + } + try { + FfiConverterU32.checkType(career) + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart("career"); + } + throw e; + } + try { + FfiConverterU32.checkType(education) + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart("education"); + } + throw e; + } + try { + FfiConverterU32.checkType(fashion) + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart("fashion"); + } + throw e; + } + try { + FfiConverterU32.checkType(finance) + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart("finance"); + } + throw e; + } + try { + FfiConverterU32.checkType(food) + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart("food"); + } + throw e; + } + try { + FfiConverterU32.checkType(government) + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart("government"); + } + throw e; + } + try { + FfiConverterU32.checkType(health) + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart("health"); + } + throw e; + } + try { + FfiConverterU32.checkType(hobbies) + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart("hobbies"); + } + throw e; + } + try { + FfiConverterU32.checkType(home) + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart("home"); + } + throw e; + } + try { + FfiConverterU32.checkType(news) + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart("news"); + } + throw e; + } + try { + FfiConverterU32.checkType(realEstate) + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart("realEstate"); + } + throw e; + } + try { + FfiConverterU32.checkType(society) + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart("society"); + } + throw e; + } + try { + FfiConverterU32.checkType(sports) + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart("sports"); + } + throw e; + } + try { + FfiConverterU32.checkType(tech) + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart("tech"); + } + throw e; + } + try { + FfiConverterU32.checkType(travel) + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart("travel"); + } + throw e; + } + try { + FfiConverterU32.checkType(inconclusive) + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart("inconclusive"); + } + throw e; + } + this.animals = animals; + this.arts = arts; + this.autos = autos; + this.business = business; + this.career = career; + this.education = education; + this.fashion = fashion; + this.finance = finance; + this.food = food; + this.government = government; + this.health = health; + this.hobbies = hobbies; + this.home = home; + this.news = news; + this.realEstate = realEstate; + this.society = society; + this.sports = sports; + this.tech = tech; + this.travel = travel; + this.inconclusive = inconclusive; + } + equals(other) { + return ( + this.animals == other.animals && + this.arts == other.arts && + this.autos == other.autos && + this.business == other.business && + this.career == other.career && + this.education == other.education && + this.fashion == other.fashion && + this.finance == other.finance && + this.food == other.food && + this.government == other.government && + this.health == other.health && + this.hobbies == other.hobbies && + this.home == other.home && + this.news == other.news && + this.realEstate == other.realEstate && + this.society == other.society && + this.sports == other.sports && + this.tech == other.tech && + this.travel == other.travel && + this.inconclusive == other.inconclusive + ) + } +} + +// Export the FFIConverter object to make external types work. +export class FfiConverterTypeInterestVector extends FfiConverterArrayBuffer { + static read(dataStream) { + return new InterestVector({ + animals: FfiConverterU32.read(dataStream), + arts: FfiConverterU32.read(dataStream), + autos: FfiConverterU32.read(dataStream), + business: FfiConverterU32.read(dataStream), + career: FfiConverterU32.read(dataStream), + education: FfiConverterU32.read(dataStream), + fashion: FfiConverterU32.read(dataStream), + finance: FfiConverterU32.read(dataStream), + food: FfiConverterU32.read(dataStream), + government: FfiConverterU32.read(dataStream), + health: FfiConverterU32.read(dataStream), + hobbies: FfiConverterU32.read(dataStream), + home: FfiConverterU32.read(dataStream), + news: FfiConverterU32.read(dataStream), + realEstate: FfiConverterU32.read(dataStream), + society: FfiConverterU32.read(dataStream), + sports: FfiConverterU32.read(dataStream), + tech: FfiConverterU32.read(dataStream), + travel: FfiConverterU32.read(dataStream), + inconclusive: FfiConverterU32.read(dataStream), + }); + } + static write(dataStream, value) { + FfiConverterU32.write(dataStream, value.animals); + FfiConverterU32.write(dataStream, value.arts); + FfiConverterU32.write(dataStream, value.autos); + FfiConverterU32.write(dataStream, value.business); + FfiConverterU32.write(dataStream, value.career); + FfiConverterU32.write(dataStream, value.education); + FfiConverterU32.write(dataStream, value.fashion); + FfiConverterU32.write(dataStream, value.finance); + FfiConverterU32.write(dataStream, value.food); + FfiConverterU32.write(dataStream, value.government); + FfiConverterU32.write(dataStream, value.health); + FfiConverterU32.write(dataStream, value.hobbies); + FfiConverterU32.write(dataStream, value.home); + FfiConverterU32.write(dataStream, value.news); + FfiConverterU32.write(dataStream, value.realEstate); + FfiConverterU32.write(dataStream, value.society); + FfiConverterU32.write(dataStream, value.sports); + FfiConverterU32.write(dataStream, value.tech); + FfiConverterU32.write(dataStream, value.travel); + FfiConverterU32.write(dataStream, value.inconclusive); + } + + static computeSize(value) { + let totalSize = 0; + totalSize += FfiConverterU32.computeSize(value.animals); + totalSize += FfiConverterU32.computeSize(value.arts); + totalSize += FfiConverterU32.computeSize(value.autos); + totalSize += FfiConverterU32.computeSize(value.business); + totalSize += FfiConverterU32.computeSize(value.career); + totalSize += FfiConverterU32.computeSize(value.education); + totalSize += FfiConverterU32.computeSize(value.fashion); + totalSize += FfiConverterU32.computeSize(value.finance); + totalSize += FfiConverterU32.computeSize(value.food); + totalSize += FfiConverterU32.computeSize(value.government); + totalSize += FfiConverterU32.computeSize(value.health); + totalSize += FfiConverterU32.computeSize(value.hobbies); + totalSize += FfiConverterU32.computeSize(value.home); + totalSize += FfiConverterU32.computeSize(value.news); + totalSize += FfiConverterU32.computeSize(value.realEstate); + totalSize += FfiConverterU32.computeSize(value.society); + totalSize += FfiConverterU32.computeSize(value.sports); + totalSize += FfiConverterU32.computeSize(value.tech); + totalSize += FfiConverterU32.computeSize(value.travel); + totalSize += FfiConverterU32.computeSize(value.inconclusive); + return totalSize + } + + static checkType(value) { + super.checkType(value); + if (!(value instanceof InterestVector)) { + throw new UniFFITypeError(`Expected 'InterestVector', found '${typeof value}'`); + } + try { + FfiConverterU32.checkType(value.animals); + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart(".animals"); + } + throw e; + } + try { + FfiConverterU32.checkType(value.arts); + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart(".arts"); + } + throw e; + } + try { + FfiConverterU32.checkType(value.autos); + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart(".autos"); + } + throw e; + } + try { + FfiConverterU32.checkType(value.business); + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart(".business"); + } + throw e; + } + try { + FfiConverterU32.checkType(value.career); + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart(".career"); + } + throw e; + } + try { + FfiConverterU32.checkType(value.education); + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart(".education"); + } + throw e; + } + try { + FfiConverterU32.checkType(value.fashion); + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart(".fashion"); + } + throw e; + } + try { + FfiConverterU32.checkType(value.finance); + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart(".finance"); + } + throw e; + } + try { + FfiConverterU32.checkType(value.food); + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart(".food"); + } + throw e; + } + try { + FfiConverterU32.checkType(value.government); + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart(".government"); + } + throw e; + } + try { + FfiConverterU32.checkType(value.health); + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart(".health"); + } + throw e; + } + try { + FfiConverterU32.checkType(value.hobbies); + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart(".hobbies"); + } + throw e; + } + try { + FfiConverterU32.checkType(value.home); + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart(".home"); + } + throw e; + } + try { + FfiConverterU32.checkType(value.news); + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart(".news"); + } + throw e; + } + try { + FfiConverterU32.checkType(value.realEstate); + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart(".realEstate"); + } + throw e; + } + try { + FfiConverterU32.checkType(value.society); + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart(".society"); + } + throw e; + } + try { + FfiConverterU32.checkType(value.sports); + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart(".sports"); + } + throw e; + } + try { + FfiConverterU32.checkType(value.tech); + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart(".tech"); + } + throw e; + } + try { + FfiConverterU32.checkType(value.travel); + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart(".travel"); + } + throw e; + } + try { + FfiConverterU32.checkType(value.inconclusive); + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart(".inconclusive"); + } + throw e; + } + } +} + + +export const Interest = { + ANIMALS: 1, + ARTS: 2, + AUTOS: 3, + BUSINESS: 4, + CAREER: 5, + EDUCATION: 6, + FASHION: 7, + FINANCE: 8, + FOOD: 9, + GOVERNMENT: 10, + HEALTH: 11, + HOBBIES: 12, + HOME: 13, + NEWS: 14, + REAL_ESTATE: 15, + SOCIETY: 16, + SPORTS: 17, + TECH: 18, + TRAVEL: 19, + INCONCLUSIVE: 20, +}; + +Object.freeze(Interest); +// Export the FFIConverter object to make external types work. +export class FfiConverterTypeInterest extends FfiConverterArrayBuffer { + static read(dataStream) { + switch (dataStream.readInt32()) { + case 1: + return Interest.ANIMALS + case 2: + return Interest.ARTS + case 3: + return Interest.AUTOS + case 4: + return Interest.BUSINESS + case 5: + return Interest.CAREER + case 6: + return Interest.EDUCATION + case 7: + return Interest.FASHION + case 8: + return Interest.FINANCE + case 9: + return Interest.FOOD + case 10: + return Interest.GOVERNMENT + case 11: + return Interest.HEALTH + case 12: + return Interest.HOBBIES + case 13: + return Interest.HOME + case 14: + return Interest.NEWS + case 15: + return Interest.REAL_ESTATE + case 16: + return Interest.SOCIETY + case 17: + return Interest.SPORTS + case 18: + return Interest.TECH + case 19: + return Interest.TRAVEL + case 20: + return Interest.INCONCLUSIVE + default: + throw new UniFFITypeError("Unknown Interest variant"); + } + } + + static write(dataStream, value) { + if (value === Interest.ANIMALS) { + dataStream.writeInt32(1); + return; + } + if (value === Interest.ARTS) { + dataStream.writeInt32(2); + return; + } + if (value === Interest.AUTOS) { + dataStream.writeInt32(3); + return; + } + if (value === Interest.BUSINESS) { + dataStream.writeInt32(4); + return; + } + if (value === Interest.CAREER) { + dataStream.writeInt32(5); + return; + } + if (value === Interest.EDUCATION) { + dataStream.writeInt32(6); + return; + } + if (value === Interest.FASHION) { + dataStream.writeInt32(7); + return; + } + if (value === Interest.FINANCE) { + dataStream.writeInt32(8); + return; + } + if (value === Interest.FOOD) { + dataStream.writeInt32(9); + return; + } + if (value === Interest.GOVERNMENT) { + dataStream.writeInt32(10); + return; + } + if (value === Interest.HEALTH) { + dataStream.writeInt32(11); + return; + } + if (value === Interest.HOBBIES) { + dataStream.writeInt32(12); + return; + } + if (value === Interest.HOME) { + dataStream.writeInt32(13); + return; + } + if (value === Interest.NEWS) { + dataStream.writeInt32(14); + return; + } + if (value === Interest.REAL_ESTATE) { + dataStream.writeInt32(15); + return; + } + if (value === Interest.SOCIETY) { + dataStream.writeInt32(16); + return; + } + if (value === Interest.SPORTS) { + dataStream.writeInt32(17); + return; + } + if (value === Interest.TECH) { + dataStream.writeInt32(18); + return; + } + if (value === Interest.TRAVEL) { + dataStream.writeInt32(19); + return; + } + if (value === Interest.INCONCLUSIVE) { + dataStream.writeInt32(20); + return; + } + throw new UniFFITypeError("Unknown Interest variant"); + } + + static computeSize(value) { + return 4; + } + + static checkType(value) { + if (!Number.isInteger(value) || value < 1 || value > 20) { + throw new UniFFITypeError(`${value} is not a valid value for Interest`); + } + } +} + + + + + +export class RelevancyApiError extends Error {} + + +export class Unexpected extends RelevancyApiError { + + constructor( + reason, + ...params + ) { + super(...params); + this.reason = reason; + } + toString() { + return `Unexpected: ${super.toString()}` + } +} + +// Export the FFIConverter object to make external types work. +export class FfiConverterTypeRelevancyApiError extends FfiConverterArrayBuffer { + static read(dataStream) { + switch (dataStream.readInt32()) { + case 1: + return new Unexpected( + FfiConverterString.read(dataStream) + ); + default: + throw new UniFFITypeError("Unknown RelevancyApiError variant"); + } + } + static computeSize(value) { + // Size of the Int indicating the variant + let totalSize = 4; + if (value instanceof Unexpected) { + totalSize += FfiConverterString.computeSize(value.reason); + return totalSize; + } + throw new UniFFITypeError("Unknown RelevancyApiError variant"); + } + static write(dataStream, value) { + if (value instanceof Unexpected) { + dataStream.writeInt32(1); + FfiConverterString.write(dataStream, value.reason); + return; + } + throw new UniFFITypeError("Unknown RelevancyApiError variant"); + } + + static errorClass = RelevancyApiError; +} + +// Export the FFIConverter object to make external types work. +export class FfiConverterSequencestring extends FfiConverterArrayBuffer { + static read(dataStream) { + const len = dataStream.readInt32(); + const arr = []; + for (let i = 0; i < len; i++) { + arr.push(FfiConverterString.read(dataStream)); + } + return arr; + } + + static write(dataStream, value) { + dataStream.writeInt32(value.length); + value.forEach((innerValue) => { + FfiConverterString.write(dataStream, innerValue); + }) + } + + static computeSize(value) { + // The size of the length + let size = 4; + for (const innerValue of value) { + size += FfiConverterString.computeSize(innerValue); + } + return size; + } + + static checkType(value) { + if (!Array.isArray(value)) { + throw new UniFFITypeError(`${value} is not an array`); + } + value.forEach((innerValue, idx) => { + try { + FfiConverterString.checkType(innerValue); + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart(`[${idx}]`); + } + throw e; + } + }) + } +} + + + + diff --git a/toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustRemoteSettings.sys.mjs b/toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustRemoteSettings.sys.mjs index ab35fbb1e7..e839b6f17d 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustRemoteSettings.sys.mjs +++ b/toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustRemoteSettings.sys.mjs @@ -158,7 +158,7 @@ class ArrayBufferDataStream { // UniFFI Pointers are **always** 8 bytes long. That is enforced // by the C++ and Rust Scaffolding code. readPointerRemoteSettings() { - const pointerId = 0; // remote_settings:RemoteSettings + const pointerId = 1; // remote_settings:RemoteSettings const res = UniFFIScaffolding.readPointer(pointerId, this.dataView.buffer, this.pos); this.pos += 8; return res; @@ -168,7 +168,7 @@ class ArrayBufferDataStream { // UniFFI Pointers are **always** 8 bytes long. That is enforced // by the C++ and Rust Scaffolding code. writePointerRemoteSettings(value) { - const pointerId = 0; // remote_settings:RemoteSettings + const pointerId = 1; // remote_settings:RemoteSettings UniFFIScaffolding.writePointer(pointerId, value, this.dataView.buffer, this.pos); this.pos += 8; } @@ -356,7 +356,7 @@ export class RemoteSettings { throw e; } return UniFFIScaffolding.callSync( - 0, // remote_settings:uniffi_remote_settings_fn_constructor_remotesettings_new + 6, // remote_settings:uniffi_remote_settings_fn_constructor_remotesettings_new FfiConverterTypeRemoteSettingsConfig.lower(remoteSettingsConfig), ) } @@ -383,7 +383,7 @@ export class RemoteSettings { throw e; } return UniFFIScaffolding.callAsync( - 1, // remote_settings:uniffi_remote_settings_fn_method_remotesettings_download_attachment_to_path + 7, // remote_settings:uniffi_remote_settings_fn_method_remotesettings_download_attachment_to_path FfiConverterTypeRemoteSettings.lower(this), FfiConverterString.lower(attachmentId), FfiConverterString.lower(path), @@ -401,7 +401,7 @@ export class RemoteSettings { const liftError = (data) => FfiConverterTypeRemoteSettingsError.lift(data); const functionCall = () => { return UniFFIScaffolding.callAsync( - 2, // remote_settings:uniffi_remote_settings_fn_method_remotesettings_get_records + 8, // remote_settings:uniffi_remote_settings_fn_method_remotesettings_get_records FfiConverterTypeRemoteSettings.lower(this), ) } @@ -425,7 +425,7 @@ export class RemoteSettings { throw e; } return UniFFIScaffolding.callAsync( - 3, // remote_settings:uniffi_remote_settings_fn_method_remotesettings_get_records_since + 9, // remote_settings:uniffi_remote_settings_fn_method_remotesettings_get_records_since FfiConverterTypeRemoteSettings.lower(this), FfiConverterU64.lower(timestamp), ) @@ -448,7 +448,11 @@ export class FfiConverterTypeRemoteSettings extends FfiConverter { } static lower(value) { - return value[uniffiObjectPtr]; + const ptr = value[uniffiObjectPtr]; + if (!(ptr instanceof UniFFIPointer)) { + throw new UniFFITypeError("Object is not a 'RemoteSettings' instance"); + } + return ptr; } static read(dataStream) { @@ -555,7 +559,7 @@ export class FfiConverterTypeAttachment extends FfiConverterArrayBuffer { static checkType(value) { super.checkType(value); if (!(value instanceof Attachment)) { - throw new TypeError(`Expected 'Attachment', found '${typeof value}'`); + throw new UniFFITypeError(`Expected 'Attachment', found '${typeof value}'`); } try { FfiConverterString.checkType(value.filename); @@ -665,7 +669,7 @@ export class FfiConverterTypeRemoteSettingsConfig extends FfiConverterArrayBuffe static checkType(value) { super.checkType(value); if (!(value instanceof RemoteSettingsConfig)) { - throw new TypeError(`Expected 'RemoteSettingsConfig', found '${typeof value}'`); + throw new UniFFITypeError(`Expected 'RemoteSettingsConfig', found '${typeof value}'`); } try { FfiConverterString.checkType(value.collectionName); @@ -785,7 +789,7 @@ export class FfiConverterTypeRemoteSettingsRecord extends FfiConverterArrayBuffe static checkType(value) { super.checkType(value); if (!(value instanceof RemoteSettingsRecord)) { - throw new TypeError(`Expected 'RemoteSettingsRecord', found '${typeof value}'`); + throw new UniFFITypeError(`Expected 'RemoteSettingsRecord', found '${typeof value}'`); } try { FfiConverterString.checkType(value.id); @@ -882,7 +886,7 @@ export class FfiConverterTypeRemoteSettingsResponse extends FfiConverterArrayBuf static checkType(value) { super.checkType(value); if (!(value instanceof RemoteSettingsResponse)) { - throw new TypeError(`Expected 'RemoteSettingsResponse', found '${typeof value}'`); + throw new UniFFITypeError(`Expected 'RemoteSettingsResponse', found '${typeof value}'`); } try { FfiConverterSequenceTypeRemoteSettingsRecord.checkType(value.records); @@ -1005,7 +1009,7 @@ export class FfiConverterTypeRemoteSettingsError extends FfiConverterArrayBuffer case 7: return new AttachmentsUnsupportedError(FfiConverterString.read(dataStream)); default: - throw new Error("Unknown RemoteSettingsError variant"); + throw new UniFFITypeError("Unknown RemoteSettingsError variant"); } } static computeSize(value) { @@ -1032,7 +1036,7 @@ export class FfiConverterTypeRemoteSettingsError extends FfiConverterArrayBuffer if (value instanceof AttachmentsUnsupportedError) { return totalSize; } - throw new Error("Unknown RemoteSettingsError variant"); + throw new UniFFITypeError("Unknown RemoteSettingsError variant"); } static write(dataStream, value) { if (value instanceof JsonError) { @@ -1063,7 +1067,7 @@ export class FfiConverterTypeRemoteSettingsError extends FfiConverterArrayBuffer dataStream.writeInt32(7); return; } - throw new Error("Unknown RemoteSettingsError variant"); + throw new UniFFITypeError("Unknown RemoteSettingsError variant"); } static errorClass = RemoteSettingsError; diff --git a/toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustSuggest.sys.mjs b/toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustSuggest.sys.mjs index ff9507c520..7ab2e2d8e2 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustSuggest.sys.mjs +++ b/toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustSuggest.sys.mjs @@ -158,7 +158,7 @@ class ArrayBufferDataStream { // UniFFI Pointers are **always** 8 bytes long. That is enforced // by the C++ and Rust Scaffolding code. readPointerSuggestStore() { - const pointerId = 1; // suggest:SuggestStore + const pointerId = 2; // suggest:SuggestStore const res = UniFFIScaffolding.readPointer(pointerId, this.dataView.buffer, this.pos); this.pos += 8; return res; @@ -168,7 +168,7 @@ class ArrayBufferDataStream { // UniFFI Pointers are **always** 8 bytes long. That is enforced // by the C++ and Rust Scaffolding code. writePointerSuggestStore(value) { - const pointerId = 1; // suggest:SuggestStore + const pointerId = 2; // suggest:SuggestStore UniFFIScaffolding.writePointer(pointerId, value, this.dataView.buffer, this.pos); this.pos += 8; } @@ -178,7 +178,7 @@ class ArrayBufferDataStream { // UniFFI Pointers are **always** 8 bytes long. That is enforced // by the C++ and Rust Scaffolding code. readPointerSuggestStoreBuilder() { - const pointerId = 2; // suggest:SuggestStoreBuilder + const pointerId = 3; // suggest:SuggestStoreBuilder const res = UniFFIScaffolding.readPointer(pointerId, this.dataView.buffer, this.pos); this.pos += 8; return res; @@ -188,7 +188,7 @@ class ArrayBufferDataStream { // UniFFI Pointers are **always** 8 bytes long. That is enforced // by the C++ and Rust Scaffolding code. writePointerSuggestStoreBuilder(value) { - const pointerId = 2; // suggest:SuggestStoreBuilder + const pointerId = 3; // suggest:SuggestStoreBuilder UniFFIScaffolding.writePointer(pointerId, value, this.dataView.buffer, this.pos); this.pos += 8; } @@ -484,7 +484,7 @@ export class SuggestStore { throw e; } return UniFFIScaffolding.callSync( - 4, // suggest:uniffi_suggest_fn_constructor_suggeststore_new + 11, // suggest:uniffi_suggest_fn_constructor_suggeststore_new FfiConverterString.lower(path), FfiConverterOptionalTypeRemoteSettingsConfig.lower(settingsConfig), ) @@ -496,7 +496,7 @@ export class SuggestStore { const liftError = (data) => FfiConverterTypeSuggestApiError.lift(data); const functionCall = () => { return UniFFIScaffolding.callAsync( - 5, // suggest:uniffi_suggest_fn_method_suggeststore_clear + 12, // suggest:uniffi_suggest_fn_method_suggeststore_clear FfiConverterTypeSuggestStore.lower(this), ) } @@ -507,12 +507,53 @@ export class SuggestStore { } } + clearDismissedSuggestions() { + const liftResult = (result) => undefined; + const liftError = (data) => FfiConverterTypeSuggestApiError.lift(data); + const functionCall = () => { + return UniFFIScaffolding.callAsync( + 13, // suggest:uniffi_suggest_fn_method_suggeststore_clear_dismissed_suggestions + FfiConverterTypeSuggestStore.lower(this), + ) + } + try { + return functionCall().then((result) => handleRustResult(result, liftResult, liftError)); + } catch (error) { + return Promise.reject(error) + } + } + + dismissSuggestion(rawSuggestionUrl) { + const liftResult = (result) => undefined; + const liftError = (data) => FfiConverterTypeSuggestApiError.lift(data); + const functionCall = () => { + try { + FfiConverterString.checkType(rawSuggestionUrl) + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart("rawSuggestionUrl"); + } + throw e; + } + return UniFFIScaffolding.callAsync( + 14, // suggest:uniffi_suggest_fn_method_suggeststore_dismiss_suggestion + FfiConverterTypeSuggestStore.lower(this), + FfiConverterString.lower(rawSuggestionUrl), + ) + } + try { + return functionCall().then((result) => handleRustResult(result, liftResult, liftError)); + } catch (error) { + return Promise.reject(error) + } + } + fetchGlobalConfig() { const liftResult = (result) => FfiConverterTypeSuggestGlobalConfig.lift(result); const liftError = (data) => FfiConverterTypeSuggestApiError.lift(data); const functionCall = () => { return UniFFIScaffolding.callAsync( - 6, // suggest:uniffi_suggest_fn_method_suggeststore_fetch_global_config + 15, // suggest:uniffi_suggest_fn_method_suggeststore_fetch_global_config FfiConverterTypeSuggestStore.lower(this), ) } @@ -536,7 +577,7 @@ export class SuggestStore { throw e; } return UniFFIScaffolding.callAsync( - 7, // suggest:uniffi_suggest_fn_method_suggeststore_fetch_provider_config + 16, // suggest:uniffi_suggest_fn_method_suggeststore_fetch_provider_config FfiConverterTypeSuggestStore.lower(this), FfiConverterTypeSuggestionProvider.lower(provider), ) @@ -561,7 +602,7 @@ export class SuggestStore { throw e; } return UniFFIScaffolding.callAsync( - 8, // suggest:uniffi_suggest_fn_method_suggeststore_ingest + 17, // suggest:uniffi_suggest_fn_method_suggeststore_ingest FfiConverterTypeSuggestStore.lower(this), FfiConverterTypeSuggestIngestionConstraints.lower(constraints), ) @@ -578,7 +619,7 @@ export class SuggestStore { const liftError = null; const functionCall = () => { return UniFFIScaffolding.callSync( - 9, // suggest:uniffi_suggest_fn_method_suggeststore_interrupt + 18, // suggest:uniffi_suggest_fn_method_suggeststore_interrupt FfiConverterTypeSuggestStore.lower(this), ) } @@ -598,7 +639,7 @@ export class SuggestStore { throw e; } return UniFFIScaffolding.callAsync( - 10, // suggest:uniffi_suggest_fn_method_suggeststore_query + 19, // suggest:uniffi_suggest_fn_method_suggeststore_query FfiConverterTypeSuggestStore.lower(this), FfiConverterTypeSuggestionQuery.lower(query), ) @@ -621,7 +662,11 @@ export class FfiConverterTypeSuggestStore extends FfiConverter { } static lower(value) { - return value[uniffiObjectPtr]; + const ptr = value[uniffiObjectPtr]; + if (!(ptr instanceof UniFFIPointer)) { + throw new UniFFITypeError("Object is not a 'SuggestStore' instance"); + } + return ptr; } static read(dataStream) { @@ -661,7 +706,7 @@ export class SuggestStoreBuilder { const liftError = null; const functionCall = () => { return UniFFIScaffolding.callAsync( - 11, // suggest:uniffi_suggest_fn_constructor_suggeststorebuilder_new + 21, // suggest:uniffi_suggest_fn_constructor_suggeststorebuilder_new ) } try { @@ -675,7 +720,7 @@ export class SuggestStoreBuilder { const liftError = (data) => FfiConverterTypeSuggestApiError.lift(data); const functionCall = () => { return UniFFIScaffolding.callAsync( - 12, // suggest:uniffi_suggest_fn_method_suggeststorebuilder_build + 22, // suggest:uniffi_suggest_fn_method_suggeststorebuilder_build FfiConverterTypeSuggestStoreBuilder.lower(this), ) } @@ -699,7 +744,7 @@ export class SuggestStoreBuilder { throw e; } return UniFFIScaffolding.callAsync( - 13, // suggest:uniffi_suggest_fn_method_suggeststorebuilder_cache_path + 23, // suggest:uniffi_suggest_fn_method_suggeststorebuilder_cache_path FfiConverterTypeSuggestStoreBuilder.lower(this), FfiConverterString.lower(path), ) @@ -724,7 +769,7 @@ export class SuggestStoreBuilder { throw e; } return UniFFIScaffolding.callAsync( - 14, // suggest:uniffi_suggest_fn_method_suggeststorebuilder_data_path + 24, // suggest:uniffi_suggest_fn_method_suggeststorebuilder_data_path FfiConverterTypeSuggestStoreBuilder.lower(this), FfiConverterString.lower(path), ) @@ -749,7 +794,7 @@ export class SuggestStoreBuilder { throw e; } return UniFFIScaffolding.callAsync( - 15, // suggest:uniffi_suggest_fn_method_suggeststorebuilder_remote_settings_config + 25, // suggest:uniffi_suggest_fn_method_suggeststorebuilder_remote_settings_config FfiConverterTypeSuggestStoreBuilder.lower(this), FfiConverterTypeRemoteSettingsConfig.lower(config), ) @@ -772,7 +817,11 @@ export class FfiConverterTypeSuggestStoreBuilder extends FfiConverter { } static lower(value) { - return value[uniffiObjectPtr]; + const ptr = value[uniffiObjectPtr]; + if (!(ptr instanceof UniFFIPointer)) { + throw new UniFFITypeError("Object is not a 'SuggestStoreBuilder' instance"); + } + return ptr; } static read(dataStream) { @@ -827,7 +876,7 @@ export class FfiConverterTypeSuggestGlobalConfig extends FfiConverterArrayBuffer static checkType(value) { super.checkType(value); if (!(value instanceof SuggestGlobalConfig)) { - throw new TypeError(`Expected 'SuggestGlobalConfig', found '${typeof value}'`); + throw new UniFFITypeError(`Expected 'SuggestGlobalConfig', found '${typeof value}'`); } try { FfiConverterI32.checkType(value.showLessFrequentlyCap); @@ -841,7 +890,7 @@ export class FfiConverterTypeSuggestGlobalConfig extends FfiConverterArrayBuffer } export class SuggestIngestionConstraints { - constructor({ maxSuggestions = null } = {}) { + constructor({ maxSuggestions = null, providers = null } = {}) { try { FfiConverterOptionalu64.checkType(maxSuggestions) } catch (e) { @@ -850,11 +899,21 @@ export class SuggestIngestionConstraints { } throw e; } + try { + FfiConverterOptionalSequenceTypeSuggestionProvider.checkType(providers) + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart("providers"); + } + throw e; + } this.maxSuggestions = maxSuggestions; + this.providers = providers; } equals(other) { return ( - this.maxSuggestions == other.maxSuggestions + this.maxSuggestions == other.maxSuggestions && + this.providers == other.providers ) } } @@ -864,22 +923,25 @@ export class FfiConverterTypeSuggestIngestionConstraints extends FfiConverterArr static read(dataStream) { return new SuggestIngestionConstraints({ maxSuggestions: FfiConverterOptionalu64.read(dataStream), + providers: FfiConverterOptionalSequenceTypeSuggestionProvider.read(dataStream), }); } static write(dataStream, value) { FfiConverterOptionalu64.write(dataStream, value.maxSuggestions); + FfiConverterOptionalSequenceTypeSuggestionProvider.write(dataStream, value.providers); } static computeSize(value) { let totalSize = 0; totalSize += FfiConverterOptionalu64.computeSize(value.maxSuggestions); + totalSize += FfiConverterOptionalSequenceTypeSuggestionProvider.computeSize(value.providers); return totalSize } static checkType(value) { super.checkType(value); if (!(value instanceof SuggestIngestionConstraints)) { - throw new TypeError(`Expected 'SuggestIngestionConstraints', found '${typeof value}'`); + throw new UniFFITypeError(`Expected 'SuggestIngestionConstraints', found '${typeof value}'`); } try { FfiConverterOptionalu64.checkType(value.maxSuggestions); @@ -889,6 +951,14 @@ export class FfiConverterTypeSuggestIngestionConstraints extends FfiConverterArr } throw e; } + try { + FfiConverterOptionalSequenceTypeSuggestionProvider.checkType(value.providers); + } catch (e) { + if (e instanceof UniFFITypeError) { + e.addItemDescriptionPart(".providers"); + } + throw e; + } } } @@ -957,7 +1027,7 @@ export class FfiConverterTypeSuggestionQuery extends FfiConverterArrayBuffer { static checkType(value) { super.checkType(value); if (!(value instanceof SuggestionQuery)) { - throw new TypeError(`Expected 'SuggestionQuery', found '${typeof value}'`); + throw new UniFFITypeError(`Expected 'SuggestionQuery', found '${typeof value}'`); } try { FfiConverterString.checkType(value.keyword); @@ -1066,7 +1136,7 @@ export class FfiConverterTypeSuggestApiError extends FfiConverterArrayBuffer { FfiConverterString.read(dataStream) ); default: - throw new Error("Unknown SuggestApiError variant"); + throw new UniFFITypeError("Unknown SuggestApiError variant"); } } static computeSize(value) { @@ -1087,7 +1157,7 @@ export class FfiConverterTypeSuggestApiError extends FfiConverterArrayBuffer { totalSize += FfiConverterString.computeSize(value.reason); return totalSize; } - throw new Error("Unknown SuggestApiError variant"); + throw new UniFFITypeError("Unknown SuggestApiError variant"); } static write(dataStream, value) { if (value instanceof Interrupted) { @@ -1109,7 +1179,7 @@ export class FfiConverterTypeSuggestApiError extends FfiConverterArrayBuffer { FfiConverterString.write(dataStream, value.reason); return; } - throw new Error("Unknown SuggestApiError variant"); + throw new UniFFITypeError("Unknown SuggestApiError variant"); } static errorClass = SuggestApiError; @@ -1135,7 +1205,7 @@ export class FfiConverterTypeSuggestProviderConfig extends FfiConverterArrayBuff FfiConverterI32.read(dataStream) ); default: - return new Error("Unknown SuggestProviderConfig variant"); + throw new UniFFITypeError("Unknown SuggestProviderConfig variant"); } } @@ -1145,7 +1215,7 @@ export class FfiConverterTypeSuggestProviderConfig extends FfiConverterArrayBuff FfiConverterI32.write(dataStream, value.minKeywordLength); return; } - return new Error("Unknown SuggestProviderConfig variant"); + throw new UniFFITypeError("Unknown SuggestProviderConfig variant"); } static computeSize(value) { @@ -1155,7 +1225,7 @@ export class FfiConverterTypeSuggestProviderConfig extends FfiConverterArrayBuff totalSize += FfiConverterI32.computeSize(value.minKeywordLength); return totalSize; } - return new Error("Unknown SuggestProviderConfig variant"); + throw new UniFFITypeError("Unknown SuggestProviderConfig variant"); } static checkType(value) { @@ -1174,6 +1244,7 @@ Suggestion.Amp = class extends Suggestion{ url, rawUrl, icon, + iconMimetype, fullKeyword, blockId, advertiser, @@ -1188,6 +1259,7 @@ Suggestion.Amp = class extends Suggestion{ this.url = url; this.rawUrl = rawUrl; this.icon = icon; + this.iconMimetype = iconMimetype; this.fullKeyword = fullKeyword; this.blockId = blockId; this.advertiser = advertiser; @@ -1217,12 +1289,14 @@ Suggestion.Wikipedia = class extends Suggestion{ title, url, icon, + iconMimetype, fullKeyword ) { super(); this.title = title; this.url = url; this.icon = icon; + this.iconMimetype = iconMimetype; this.fullKeyword = fullKeyword; } } @@ -1253,6 +1327,7 @@ Suggestion.Yelp = class extends Suggestion{ url, title, icon, + iconMimetype, score, hasLocationSign, subjectExactMatch, @@ -1262,6 +1337,7 @@ Suggestion.Yelp = class extends Suggestion{ this.url = url; this.title = title; this.icon = icon; + this.iconMimetype = iconMimetype; this.score = score; this.hasLocationSign = hasLocationSign; this.subjectExactMatch = subjectExactMatch; @@ -1301,6 +1377,7 @@ export class FfiConverterTypeSuggestion extends FfiConverterArrayBuffer { FfiConverterString.read(dataStream), FfiConverterString.read(dataStream), FfiConverterOptionalSequenceu8.read(dataStream), + FfiConverterOptionalstring.read(dataStream), FfiConverterString.read(dataStream), FfiConverterI64.read(dataStream), FfiConverterString.read(dataStream), @@ -1322,6 +1399,7 @@ export class FfiConverterTypeSuggestion extends FfiConverterArrayBuffer { FfiConverterString.read(dataStream), FfiConverterString.read(dataStream), FfiConverterOptionalSequenceu8.read(dataStream), + FfiConverterOptionalstring.read(dataStream), FfiConverterString.read(dataStream) ); case 4: @@ -1340,6 +1418,7 @@ export class FfiConverterTypeSuggestion extends FfiConverterArrayBuffer { FfiConverterString.read(dataStream), FfiConverterString.read(dataStream), FfiConverterOptionalSequenceu8.read(dataStream), + FfiConverterOptionalstring.read(dataStream), FfiConverterF64.read(dataStream), FfiConverterBool.read(dataStream), FfiConverterBool.read(dataStream), @@ -1357,7 +1436,7 @@ export class FfiConverterTypeSuggestion extends FfiConverterArrayBuffer { FfiConverterF64.read(dataStream) ); default: - return new Error("Unknown Suggestion variant"); + throw new UniFFITypeError("Unknown Suggestion variant"); } } @@ -1368,6 +1447,7 @@ export class FfiConverterTypeSuggestion extends FfiConverterArrayBuffer { FfiConverterString.write(dataStream, value.url); FfiConverterString.write(dataStream, value.rawUrl); FfiConverterOptionalSequenceu8.write(dataStream, value.icon); + FfiConverterOptionalstring.write(dataStream, value.iconMimetype); FfiConverterString.write(dataStream, value.fullKeyword); FfiConverterI64.write(dataStream, value.blockId); FfiConverterString.write(dataStream, value.advertiser); @@ -1391,6 +1471,7 @@ export class FfiConverterTypeSuggestion extends FfiConverterArrayBuffer { FfiConverterString.write(dataStream, value.title); FfiConverterString.write(dataStream, value.url); FfiConverterOptionalSequenceu8.write(dataStream, value.icon); + FfiConverterOptionalstring.write(dataStream, value.iconMimetype); FfiConverterString.write(dataStream, value.fullKeyword); return; } @@ -1411,6 +1492,7 @@ export class FfiConverterTypeSuggestion extends FfiConverterArrayBuffer { FfiConverterString.write(dataStream, value.url); FfiConverterString.write(dataStream, value.title); FfiConverterOptionalSequenceu8.write(dataStream, value.icon); + FfiConverterOptionalstring.write(dataStream, value.iconMimetype); FfiConverterF64.write(dataStream, value.score); FfiConverterBool.write(dataStream, value.hasLocationSign); FfiConverterBool.write(dataStream, value.subjectExactMatch); @@ -1430,7 +1512,7 @@ export class FfiConverterTypeSuggestion extends FfiConverterArrayBuffer { FfiConverterF64.write(dataStream, value.score); return; } - return new Error("Unknown Suggestion variant"); + throw new UniFFITypeError("Unknown Suggestion variant"); } static computeSize(value) { @@ -1441,6 +1523,7 @@ export class FfiConverterTypeSuggestion extends FfiConverterArrayBuffer { totalSize += FfiConverterString.computeSize(value.url); totalSize += FfiConverterString.computeSize(value.rawUrl); totalSize += FfiConverterOptionalSequenceu8.computeSize(value.icon); + totalSize += FfiConverterOptionalstring.computeSize(value.iconMimetype); totalSize += FfiConverterString.computeSize(value.fullKeyword); totalSize += FfiConverterI64.computeSize(value.blockId); totalSize += FfiConverterString.computeSize(value.advertiser); @@ -1462,6 +1545,7 @@ export class FfiConverterTypeSuggestion extends FfiConverterArrayBuffer { totalSize += FfiConverterString.computeSize(value.title); totalSize += FfiConverterString.computeSize(value.url); totalSize += FfiConverterOptionalSequenceu8.computeSize(value.icon); + totalSize += FfiConverterOptionalstring.computeSize(value.iconMimetype); totalSize += FfiConverterString.computeSize(value.fullKeyword); return totalSize; } @@ -1480,6 +1564,7 @@ export class FfiConverterTypeSuggestion extends FfiConverterArrayBuffer { totalSize += FfiConverterString.computeSize(value.url); totalSize += FfiConverterString.computeSize(value.title); totalSize += FfiConverterOptionalSequenceu8.computeSize(value.icon); + totalSize += FfiConverterOptionalstring.computeSize(value.iconMimetype); totalSize += FfiConverterF64.computeSize(value.score); totalSize += FfiConverterBool.computeSize(value.hasLocationSign); totalSize += FfiConverterBool.computeSize(value.subjectExactMatch); @@ -1497,7 +1582,7 @@ export class FfiConverterTypeSuggestion extends FfiConverterArrayBuffer { totalSize += FfiConverterF64.computeSize(value.score); return totalSize; } - return new Error("Unknown Suggestion variant"); + throw new UniFFITypeError("Unknown Suggestion variant"); } static checkType(value) { @@ -1542,7 +1627,7 @@ export class FfiConverterTypeSuggestionProvider extends FfiConverterArrayBuffer case 8: return SuggestionProvider.AMP_MOBILE default: - return new Error("Unknown SuggestionProvider variant"); + throw new UniFFITypeError("Unknown SuggestionProvider variant"); } } @@ -1579,7 +1664,7 @@ export class FfiConverterTypeSuggestionProvider extends FfiConverterArrayBuffer dataStream.writeInt32(8); return; } - return new Error("Unknown SuggestionProvider variant"); + throw new UniFFITypeError("Unknown SuggestionProvider variant"); } static computeSize(value) { @@ -1780,6 +1865,43 @@ export class FfiConverterOptionalSequenceu8 extends FfiConverterArrayBuffer { } // Export the FFIConverter object to make external types work. +export class FfiConverterOptionalSequenceTypeSuggestionProvider extends FfiConverterArrayBuffer { + static checkType(value) { + if (value !== undefined && value !== null) { + FfiConverterSequenceTypeSuggestionProvider.checkType(value) + } + } + + static read(dataStream) { + const code = dataStream.readUint8(0); + switch (code) { + case 0: + return null + case 1: + return FfiConverterSequenceTypeSuggestionProvider.read(dataStream) + default: + throw UniFFIError(`Unexpected code: ${code}`); + } + } + + static write(dataStream, value) { + if (value === null || value === undefined) { + dataStream.writeUint8(0); + return; + } + dataStream.writeUint8(1); + FfiConverterSequenceTypeSuggestionProvider.write(dataStream, value) + } + + static computeSize(value) { + if (value === null || value === undefined) { + return 1; + } + return 1 + FfiConverterSequenceTypeSuggestionProvider.computeSize(value) + } +} + +// Export the FFIConverter object to make external types work. export class FfiConverterOptionalTypeRemoteSettingsConfig extends FfiConverterArrayBuffer { static checkType(value) { if (value !== undefined && value !== null) { @@ -1982,7 +2104,7 @@ export function rawSuggestionUrlMatches(rawUrl,url) { throw e; } return UniFFIScaffolding.callSync( - 16, // suggest:uniffi_suggest_fn_func_raw_suggestion_url_matches + 26, // suggest:uniffi_suggest_fn_func_raw_suggestion_url_matches FfiConverterString.lower(rawUrl), FfiConverterString.lower(url), ) diff --git a/toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustSync15.sys.mjs b/toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustSync15.sys.mjs index fb9b06bfa7..c02bf51235 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustSync15.sys.mjs +++ b/toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustSync15.sys.mjs @@ -282,7 +282,7 @@ export class FfiConverterTypeDeviceType extends FfiConverterArrayBuffer { case 6: return DeviceType.UNKNOWN default: - return new Error("Unknown DeviceType variant"); + throw new UniFFITypeError("Unknown DeviceType variant"); } } @@ -311,7 +311,7 @@ export class FfiConverterTypeDeviceType extends FfiConverterArrayBuffer { dataStream.writeInt32(6); return; } - return new Error("Unknown DeviceType variant"); + throw new UniFFITypeError("Unknown DeviceType variant"); } static computeSize(value) { diff --git a/toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustTabs.sys.mjs b/toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustTabs.sys.mjs index 614d7fb95b..4d34a46d9e 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustTabs.sys.mjs +++ b/toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustTabs.sys.mjs @@ -158,7 +158,7 @@ class ArrayBufferDataStream { // UniFFI Pointers are **always** 8 bytes long. That is enforced // by the C++ and Rust Scaffolding code. readPointerTabsBridgedEngine() { - const pointerId = 3; // tabs:TabsBridgedEngine + const pointerId = 4; // tabs:TabsBridgedEngine const res = UniFFIScaffolding.readPointer(pointerId, this.dataView.buffer, this.pos); this.pos += 8; return res; @@ -168,7 +168,7 @@ class ArrayBufferDataStream { // UniFFI Pointers are **always** 8 bytes long. That is enforced // by the C++ and Rust Scaffolding code. writePointerTabsBridgedEngine(value) { - const pointerId = 3; // tabs:TabsBridgedEngine + const pointerId = 4; // tabs:TabsBridgedEngine UniFFIScaffolding.writePointer(pointerId, value, this.dataView.buffer, this.pos); this.pos += 8; } @@ -178,7 +178,7 @@ class ArrayBufferDataStream { // UniFFI Pointers are **always** 8 bytes long. That is enforced // by the C++ and Rust Scaffolding code. readPointerTabsStore() { - const pointerId = 4; // tabs:TabsStore + const pointerId = 5; // tabs:TabsStore const res = UniFFIScaffolding.readPointer(pointerId, this.dataView.buffer, this.pos); this.pos += 8; return res; @@ -188,7 +188,7 @@ class ArrayBufferDataStream { // UniFFI Pointers are **always** 8 bytes long. That is enforced // by the C++ and Rust Scaffolding code. writePointerTabsStore(value) { - const pointerId = 4; // tabs:TabsStore + const pointerId = 5; // tabs:TabsStore UniFFIScaffolding.writePointer(pointerId, value, this.dataView.buffer, this.pos); this.pos += 8; } @@ -361,7 +361,7 @@ export class TabsBridgedEngine { const liftError = (data) => FfiConverterTypeTabsApiError.lift(data); const functionCall = () => { return UniFFIScaffolding.callAsync( - 17, // tabs:uniffi_tabs_fn_method_tabsbridgedengine_apply + 28, // tabs:uniffi_tabs_fn_method_tabsbridgedengine_apply FfiConverterTypeTabsBridgedEngine.lower(this), ) } @@ -385,7 +385,7 @@ export class TabsBridgedEngine { throw e; } return UniFFIScaffolding.callAsync( - 18, // tabs:uniffi_tabs_fn_method_tabsbridgedengine_ensure_current_sync_id + 29, // tabs:uniffi_tabs_fn_method_tabsbridgedengine_ensure_current_sync_id FfiConverterTypeTabsBridgedEngine.lower(this), FfiConverterString.lower(newSyncId), ) @@ -402,7 +402,7 @@ export class TabsBridgedEngine { const liftError = (data) => FfiConverterTypeTabsApiError.lift(data); const functionCall = () => { return UniFFIScaffolding.callAsync( - 19, // tabs:uniffi_tabs_fn_method_tabsbridgedengine_last_sync + 30, // tabs:uniffi_tabs_fn_method_tabsbridgedengine_last_sync FfiConverterTypeTabsBridgedEngine.lower(this), ) } @@ -426,7 +426,7 @@ export class TabsBridgedEngine { throw e; } return UniFFIScaffolding.callAsync( - 20, // tabs:uniffi_tabs_fn_method_tabsbridgedengine_prepare_for_sync + 31, // tabs:uniffi_tabs_fn_method_tabsbridgedengine_prepare_for_sync FfiConverterTypeTabsBridgedEngine.lower(this), FfiConverterString.lower(clientData), ) @@ -443,7 +443,7 @@ export class TabsBridgedEngine { const liftError = (data) => FfiConverterTypeTabsApiError.lift(data); const functionCall = () => { return UniFFIScaffolding.callAsync( - 21, // tabs:uniffi_tabs_fn_method_tabsbridgedengine_reset + 32, // tabs:uniffi_tabs_fn_method_tabsbridgedengine_reset FfiConverterTypeTabsBridgedEngine.lower(this), ) } @@ -459,7 +459,7 @@ export class TabsBridgedEngine { const liftError = (data) => FfiConverterTypeTabsApiError.lift(data); const functionCall = () => { return UniFFIScaffolding.callAsync( - 22, // tabs:uniffi_tabs_fn_method_tabsbridgedengine_reset_sync_id + 33, // tabs:uniffi_tabs_fn_method_tabsbridgedengine_reset_sync_id FfiConverterTypeTabsBridgedEngine.lower(this), ) } @@ -483,7 +483,7 @@ export class TabsBridgedEngine { throw e; } return UniFFIScaffolding.callAsync( - 23, // tabs:uniffi_tabs_fn_method_tabsbridgedengine_set_last_sync + 34, // tabs:uniffi_tabs_fn_method_tabsbridgedengine_set_last_sync FfiConverterTypeTabsBridgedEngine.lower(this), FfiConverterI64.lower(lastSync), ) @@ -516,7 +516,7 @@ export class TabsBridgedEngine { throw e; } return UniFFIScaffolding.callAsync( - 24, // tabs:uniffi_tabs_fn_method_tabsbridgedengine_set_uploaded + 35, // tabs:uniffi_tabs_fn_method_tabsbridgedengine_set_uploaded FfiConverterTypeTabsBridgedEngine.lower(this), FfiConverterI64.lower(newTimestamp), FfiConverterSequenceTypeTabsGuid.lower(uploadedIds), @@ -542,7 +542,7 @@ export class TabsBridgedEngine { throw e; } return UniFFIScaffolding.callAsync( - 25, // tabs:uniffi_tabs_fn_method_tabsbridgedengine_store_incoming + 36, // tabs:uniffi_tabs_fn_method_tabsbridgedengine_store_incoming FfiConverterTypeTabsBridgedEngine.lower(this), FfiConverterSequencestring.lower(incomingEnvelopesAsJson), ) @@ -559,7 +559,7 @@ export class TabsBridgedEngine { const liftError = (data) => FfiConverterTypeTabsApiError.lift(data); const functionCall = () => { return UniFFIScaffolding.callAsync( - 26, // tabs:uniffi_tabs_fn_method_tabsbridgedengine_sync_finished + 37, // tabs:uniffi_tabs_fn_method_tabsbridgedengine_sync_finished FfiConverterTypeTabsBridgedEngine.lower(this), ) } @@ -575,7 +575,7 @@ export class TabsBridgedEngine { const liftError = (data) => FfiConverterTypeTabsApiError.lift(data); const functionCall = () => { return UniFFIScaffolding.callAsync( - 27, // tabs:uniffi_tabs_fn_method_tabsbridgedengine_sync_id + 38, // tabs:uniffi_tabs_fn_method_tabsbridgedengine_sync_id FfiConverterTypeTabsBridgedEngine.lower(this), ) } @@ -591,7 +591,7 @@ export class TabsBridgedEngine { const liftError = (data) => FfiConverterTypeTabsApiError.lift(data); const functionCall = () => { return UniFFIScaffolding.callAsync( - 28, // tabs:uniffi_tabs_fn_method_tabsbridgedengine_sync_started + 39, // tabs:uniffi_tabs_fn_method_tabsbridgedengine_sync_started FfiConverterTypeTabsBridgedEngine.lower(this), ) } @@ -607,7 +607,7 @@ export class TabsBridgedEngine { const liftError = (data) => FfiConverterTypeTabsApiError.lift(data); const functionCall = () => { return UniFFIScaffolding.callAsync( - 29, // tabs:uniffi_tabs_fn_method_tabsbridgedengine_wipe + 40, // tabs:uniffi_tabs_fn_method_tabsbridgedengine_wipe FfiConverterTypeTabsBridgedEngine.lower(this), ) } @@ -629,7 +629,11 @@ export class FfiConverterTypeTabsBridgedEngine extends FfiConverter { } static lower(value) { - return value[uniffiObjectPtr]; + const ptr = value[uniffiObjectPtr]; + if (!(ptr instanceof UniFFIPointer)) { + throw new UniFFITypeError("Object is not a 'TabsBridgedEngine' instance"); + } + return ptr; } static read(dataStream) { @@ -677,7 +681,7 @@ export class TabsStore { throw e; } return UniFFIScaffolding.callAsync( - 30, // tabs:uniffi_tabs_fn_constructor_tabsstore_new + 42, // tabs:uniffi_tabs_fn_constructor_tabsstore_new FfiConverterString.lower(path), ) } @@ -692,7 +696,7 @@ export class TabsStore { const liftError = null; const functionCall = () => { return UniFFIScaffolding.callAsync( - 31, // tabs:uniffi_tabs_fn_method_tabsstore_bridged_engine + 43, // tabs:uniffi_tabs_fn_method_tabsstore_bridged_engine FfiConverterTypeTabsStore.lower(this), ) } @@ -708,7 +712,7 @@ export class TabsStore { const liftError = null; const functionCall = () => { return UniFFIScaffolding.callAsync( - 32, // tabs:uniffi_tabs_fn_method_tabsstore_get_all + 44, // tabs:uniffi_tabs_fn_method_tabsstore_get_all FfiConverterTypeTabsStore.lower(this), ) } @@ -724,7 +728,7 @@ export class TabsStore { const liftError = null; const functionCall = () => { return UniFFIScaffolding.callAsync( - 33, // tabs:uniffi_tabs_fn_method_tabsstore_register_with_sync_manager + 45, // tabs:uniffi_tabs_fn_method_tabsstore_register_with_sync_manager FfiConverterTypeTabsStore.lower(this), ) } @@ -748,7 +752,7 @@ export class TabsStore { throw e; } return UniFFIScaffolding.callAsync( - 34, // tabs:uniffi_tabs_fn_method_tabsstore_set_local_tabs + 46, // tabs:uniffi_tabs_fn_method_tabsstore_set_local_tabs FfiConverterTypeTabsStore.lower(this), FfiConverterSequenceTypeRemoteTabRecord.lower(remoteTabs), ) @@ -771,7 +775,11 @@ export class FfiConverterTypeTabsStore extends FfiConverter { } static lower(value) { - return value[uniffiObjectPtr]; + const ptr = value[uniffiObjectPtr]; + if (!(ptr instanceof UniFFIPointer)) { + throw new UniFFITypeError("Object is not a 'TabsStore' instance"); + } + return ptr; } static read(dataStream) { @@ -878,7 +886,7 @@ export class FfiConverterTypeClientRemoteTabs extends FfiConverterArrayBuffer { static checkType(value) { super.checkType(value); if (!(value instanceof ClientRemoteTabs)) { - throw new TypeError(`Expected 'ClientRemoteTabs', found '${typeof value}'`); + throw new UniFFITypeError(`Expected 'ClientRemoteTabs', found '${typeof value}'`); } try { FfiConverterString.checkType(value.clientId); @@ -1014,7 +1022,7 @@ export class FfiConverterTypeRemoteTabRecord extends FfiConverterArrayBuffer { static checkType(value) { super.checkType(value); if (!(value instanceof RemoteTabRecord)) { - throw new TypeError(`Expected 'RemoteTabRecord', found '${typeof value}'`); + throw new UniFFITypeError(`Expected 'RemoteTabRecord', found '${typeof value}'`); } try { FfiConverterString.checkType(value.title); @@ -1124,7 +1132,7 @@ export class FfiConverterTypeTabsApiError extends FfiConverterArrayBuffer { FfiConverterString.read(dataStream) ); default: - throw new Error("Unknown TabsApiError variant"); + throw new UniFFITypeError("Unknown TabsApiError variant"); } } static computeSize(value) { @@ -1142,7 +1150,7 @@ export class FfiConverterTypeTabsApiError extends FfiConverterArrayBuffer { totalSize += FfiConverterString.computeSize(value.reason); return totalSize; } - throw new Error("Unknown TabsApiError variant"); + throw new UniFFITypeError("Unknown TabsApiError variant"); } static write(dataStream, value) { if (value instanceof SyncError) { @@ -1160,7 +1168,7 @@ export class FfiConverterTypeTabsApiError extends FfiConverterArrayBuffer { FfiConverterString.write(dataStream, value.reason); return; } - throw new Error("Unknown TabsApiError variant"); + throw new UniFFITypeError("Unknown TabsApiError variant"); } static errorClass = TabsApiError; diff --git a/toolkit/components/uniffi-bindgen-gecko-js/components/moz.build b/toolkit/components/uniffi-bindgen-gecko-js/components/moz.build index 107745a4b8..405711db98 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/components/moz.build +++ b/toolkit/components/uniffi-bindgen-gecko-js/components/moz.build @@ -5,6 +5,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. EXTRA_JS_MODULES += [ + "generated/RustRelevancy.sys.mjs", "generated/RustRemoteSettings.sys.mjs", "generated/RustSuggest.sys.mjs", "generated/RustSync15.sys.mjs", diff --git a/toolkit/components/uniffi-bindgen-gecko-js/config.toml b/toolkit/components/uniffi-bindgen-gecko-js/config.toml index d59b2a69e1..d8404f5142 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/config.toml +++ b/toolkit/components/uniffi-bindgen-gecko-js/config.toml @@ -29,6 +29,14 @@ main = [ "SuggestStore.interrupt", ] +[relevancy] +crate_name = "relevancy" +udl_file = "third_party/rust/relevancy/src/relevancy.udl" + +[relevancy.receiver_thread] +default = "worker" +main = [] + [remote_settings] crate_name = "remote_settings" udl_file = "third_party/rust/remote_settings/src/remote_settings.udl" @@ -64,16 +72,27 @@ crate_name = "uniffi_todolist" udl_file = "third_party/rust/uniffi-example-todolist/src/todolist.udl" fixture = true -[fixture_callbacks] -crate_name = "uniffi_fixture_callbacks" -udl_file = "toolkit/components/uniffi-fixture-callbacks/src/callbacks.udl" +[fixture_refcounts] +crate_name = "uniffi_fixture_refcounts" +udl_file = "toolkit/components/uniffi-fixture-refcounts/src/refcounts.udl" fixture = true -[fixture_callbacks.receiver_thread] -default = "worker" -main = [ - "log_even_numbers_main_thread", -] +[fixture_refcounts.receiver_thread] +default = "main" + +# Temporarily disabled until we can re-enable callback support +# +# I think this can be done when we implement https://bugzilla.mozilla.org/show_bug.cgi?id=1888668 +# [fixture_callbacks] +# crate_name = "uniffi_fixture_callbacks" +# udl_file = "toolkit/components/uniffi-fixture-callbacks/src/callbacks.udl" +# fixture = true +# +# [fixture_callbacks.receiver_thread] +# default = "worker" +# main = [ +# "log_even_numbers_main_thread", +# ] [custom_types] crate_name = "uniffi_custom_types" diff --git a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustArithmetic.sys.mjs b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustArithmetic.sys.mjs index 1790f0effa..62766a45a4 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustArithmetic.sys.mjs +++ b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustArithmetic.sys.mjs @@ -329,7 +329,7 @@ export class FfiConverterTypeArithmeticError extends FfiConverterArrayBuffer { case 1: return new IntegerOverflow(FfiConverterString.read(dataStream)); default: - throw new Error("Unknown ArithmeticError variant"); + throw new UniFFITypeError("Unknown ArithmeticError variant"); } } static computeSize(value) { @@ -338,14 +338,14 @@ export class FfiConverterTypeArithmeticError extends FfiConverterArrayBuffer { if (value instanceof IntegerOverflow) { return totalSize; } - throw new Error("Unknown ArithmeticError variant"); + throw new UniFFITypeError("Unknown ArithmeticError variant"); } static write(dataStream, value) { if (value instanceof IntegerOverflow) { dataStream.writeInt32(1); return; } - throw new Error("Unknown ArithmeticError variant"); + throw new UniFFITypeError("Unknown ArithmeticError variant"); } static errorClass = ArithmeticError; @@ -377,7 +377,7 @@ export function add(a,b) { throw e; } return UniFFIScaffolding.callAsync( - 35, // arithmetic:uniffi_arithmetical_fn_func_add + 47, // arithmetic:uniffi_arithmetical_fn_func_add FfiConverterU64.lower(a), FfiConverterU64.lower(b), ) @@ -411,7 +411,7 @@ export function div(dividend,divisor) { throw e; } return UniFFIScaffolding.callAsync( - 36, // arithmetic:uniffi_arithmetical_fn_func_div + 48, // arithmetic:uniffi_arithmetical_fn_func_div FfiConverterU64.lower(dividend), FfiConverterU64.lower(divisor), ) @@ -445,7 +445,7 @@ export function equal(a,b) { throw e; } return UniFFIScaffolding.callAsync( - 37, // arithmetic:uniffi_arithmetical_fn_func_equal + 49, // arithmetic:uniffi_arithmetical_fn_func_equal FfiConverterU64.lower(a), FfiConverterU64.lower(b), ) @@ -479,7 +479,7 @@ export function sub(a,b) { throw e; } return UniFFIScaffolding.callAsync( - 38, // arithmetic:uniffi_arithmetical_fn_func_sub + 50, // arithmetic:uniffi_arithmetical_fn_func_sub FfiConverterU64.lower(a), FfiConverterU64.lower(b), ) diff --git a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustCustomTypes.sys.mjs b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustCustomTypes.sys.mjs index 286c046fa6..581478cc8a 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustCustomTypes.sys.mjs +++ b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustCustomTypes.sys.mjs @@ -331,7 +331,7 @@ export class FfiConverterTypeCustomTypesDemo extends FfiConverterArrayBuffer { static checkType(value) { super.checkType(value); if (!(value instanceof CustomTypesDemo)) { - throw new TypeError(`Expected 'CustomTypesDemo', found '${typeof value}'`); + throw new UniFFITypeError(`Expected 'CustomTypesDemo', found '${typeof value}'`); } try { FfiConverterTypeUrl.checkType(value.url); @@ -455,7 +455,7 @@ export function getCustomTypesDemo(demo) { throw e; } return UniFFIScaffolding.callAsync( - 39, // custom_types:uniffi_uniffi_custom_types_fn_func_get_custom_types_demo + 51, // custom_types:uniffi_uniffi_custom_types_fn_func_get_custom_types_demo FfiConverterOptionalTypeCustomTypesDemo.lower(demo), ) } diff --git a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustExternalTypes.sys.mjs b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustExternalTypes.sys.mjs index c01b1a58b3..88c9390225 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustExternalTypes.sys.mjs +++ b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustExternalTypes.sys.mjs @@ -381,7 +381,7 @@ export function gradient(value) { throw e; } return UniFFIScaffolding.callAsync( - 40, // external_types:uniffi_uniffi_fixture_external_types_fn_func_gradient + 52, // external_types:uniffi_uniffi_fixture_external_types_fn_func_gradient FfiConverterOptionalTypeLine.lower(value), ) } @@ -414,7 +414,7 @@ export function intersection(ln1,ln2) { throw e; } return UniFFIScaffolding.callAsync( - 41, // external_types:uniffi_uniffi_fixture_external_types_fn_func_intersection + 53, // external_types:uniffi_uniffi_fixture_external_types_fn_func_intersection FfiConverterTypeLine.lower(ln1), FfiConverterTypeLine.lower(ln2), ) diff --git a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustFixtureCallbacks.sys.mjs b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustFixtureCallbacks.sys.mjs index 75fe81a458..2b6b6b26ff 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustFixtureCallbacks.sys.mjs +++ b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustFixtureCallbacks.sys.mjs @@ -596,7 +596,7 @@ export function logEvenNumbers(logger,items) { throw e; } return UniFFIScaffolding.callAsync( - 42, // fixture_callbacks:uniffi_uniffi_fixture_callbacks_fn_func_log_even_numbers + 46, // fixture_callbacks:uniffi_uniffi_fixture_callbacks_fn_func_log_even_numbers FfiConverterTypeLogger.lower(logger), FfiConverterSequencei32.lower(items), ) @@ -630,7 +630,7 @@ export function logEvenNumbersMainThread(logger,items) { throw e; } return UniFFIScaffolding.callSync( - 43, // fixture_callbacks:uniffi_uniffi_fixture_callbacks_fn_func_log_even_numbers_main_thread + 47, // fixture_callbacks:uniffi_uniffi_fixture_callbacks_fn_func_log_even_numbers_main_thread FfiConverterTypeLogger.lower(logger), FfiConverterSequencei32.lower(items), ) diff --git a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustGeometry.sys.mjs b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustGeometry.sys.mjs index 1a7ebb287a..6ff9a25fa9 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustGeometry.sys.mjs +++ b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustGeometry.sys.mjs @@ -325,7 +325,7 @@ export class FfiConverterTypeLine extends FfiConverterArrayBuffer { static checkType(value) { super.checkType(value); if (!(value instanceof Line)) { - throw new TypeError(`Expected 'Line', found '${typeof value}'`); + throw new UniFFITypeError(`Expected 'Line', found '${typeof value}'`); } try { FfiConverterTypePoint.checkType(value.start); @@ -398,7 +398,7 @@ export class FfiConverterTypePoint extends FfiConverterArrayBuffer { static checkType(value) { super.checkType(value); if (!(value instanceof Point)) { - throw new TypeError(`Expected 'Point', found '${typeof value}'`); + throw new UniFFITypeError(`Expected 'Point', found '${typeof value}'`); } try { FfiConverterF64.checkType(value.coordX); @@ -474,7 +474,7 @@ export function gradient(ln) { throw e; } return UniFFIScaffolding.callAsync( - 44, // geometry:uniffi_uniffi_geometry_fn_func_gradient + 58, // geometry:uniffi_uniffi_geometry_fn_func_gradient FfiConverterTypeLine.lower(ln), ) } @@ -507,7 +507,7 @@ export function intersection(ln1,ln2) { throw e; } return UniFFIScaffolding.callAsync( - 45, // geometry:uniffi_uniffi_geometry_fn_func_intersection + 59, // geometry:uniffi_uniffi_geometry_fn_func_intersection FfiConverterTypeLine.lower(ln1), FfiConverterTypeLine.lower(ln2), ) diff --git a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustRefcounts.sys.mjs b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustRefcounts.sys.mjs new file mode 100644 index 0000000000..d8a598d150 --- /dev/null +++ b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustRefcounts.sys.mjs @@ -0,0 +1,388 @@ +// This file was autogenerated by the `uniffi-bindgen-gecko-js` crate. +// Trust me, you don't want to mess with it! + +import { UniFFITypeError } from "resource://gre/modules/UniFFI.sys.mjs"; + + + +// Objects intended to be used in the unit tests +export var UnitTestObjs = {}; + +// Write/Read data to/from an ArrayBuffer +class ArrayBufferDataStream { + constructor(arrayBuffer) { + this.dataView = new DataView(arrayBuffer); + this.pos = 0; + } + + readUint8() { + let rv = this.dataView.getUint8(this.pos); + this.pos += 1; + return rv; + } + + writeUint8(value) { + this.dataView.setUint8(this.pos, value); + this.pos += 1; + } + + readUint16() { + let rv = this.dataView.getUint16(this.pos); + this.pos += 2; + return rv; + } + + writeUint16(value) { + this.dataView.setUint16(this.pos, value); + this.pos += 2; + } + + readUint32() { + let rv = this.dataView.getUint32(this.pos); + this.pos += 4; + return rv; + } + + writeUint32(value) { + this.dataView.setUint32(this.pos, value); + this.pos += 4; + } + + readUint64() { + let rv = this.dataView.getBigUint64(this.pos); + this.pos += 8; + return Number(rv); + } + + writeUint64(value) { + this.dataView.setBigUint64(this.pos, BigInt(value)); + this.pos += 8; + } + + + readInt8() { + let rv = this.dataView.getInt8(this.pos); + this.pos += 1; + return rv; + } + + writeInt8(value) { + this.dataView.setInt8(this.pos, value); + this.pos += 1; + } + + readInt16() { + let rv = this.dataView.getInt16(this.pos); + this.pos += 2; + return rv; + } + + writeInt16(value) { + this.dataView.setInt16(this.pos, value); + this.pos += 2; + } + + readInt32() { + let rv = this.dataView.getInt32(this.pos); + this.pos += 4; + return rv; + } + + writeInt32(value) { + this.dataView.setInt32(this.pos, value); + this.pos += 4; + } + + readInt64() { + let rv = this.dataView.getBigInt64(this.pos); + this.pos += 8; + return Number(rv); + } + + writeInt64(value) { + this.dataView.setBigInt64(this.pos, BigInt(value)); + this.pos += 8; + } + + readFloat32() { + let rv = this.dataView.getFloat32(this.pos); + this.pos += 4; + return rv; + } + + writeFloat32(value) { + this.dataView.setFloat32(this.pos, value); + this.pos += 4; + } + + readFloat64() { + let rv = this.dataView.getFloat64(this.pos); + this.pos += 8; + return rv; + } + + writeFloat64(value) { + this.dataView.setFloat64(this.pos, value); + this.pos += 8; + } + + + writeString(value) { + const encoder = new TextEncoder(); + // Note: in order to efficiently write this data, we first write the + // string data, reserving 4 bytes for the size. + const dest = new Uint8Array(this.dataView.buffer, this.pos + 4); + const encodeResult = encoder.encodeInto(value, dest); + if (encodeResult.read != value.length) { + throw new UniFFIError( + "writeString: out of space when writing to ArrayBuffer. Did the computeSize() method returned the wrong result?" + ); + } + const size = encodeResult.written; + // Next, go back and write the size before the string data + this.dataView.setUint32(this.pos, size); + // Finally, advance our position past both the size and string data + this.pos += size + 4; + } + + readString() { + const decoder = new TextDecoder(); + const size = this.readUint32(); + const source = new Uint8Array(this.dataView.buffer, this.pos, size) + const value = decoder.decode(source); + this.pos += size; + return value; + } + + // Reads a SingletonObject pointer from the data stream + // UniFFI Pointers are **always** 8 bytes long. That is enforced + // by the C++ and Rust Scaffolding code. + readPointerSingletonObject() { + const pointerId = 6; // refcounts:SingletonObject + const res = UniFFIScaffolding.readPointer(pointerId, this.dataView.buffer, this.pos); + this.pos += 8; + return res; + } + + // Writes a SingletonObject pointer into the data stream + // UniFFI Pointers are **always** 8 bytes long. That is enforced + // by the C++ and Rust Scaffolding code. + writePointerSingletonObject(value) { + const pointerId = 6; // refcounts:SingletonObject + UniFFIScaffolding.writePointer(pointerId, value, this.dataView.buffer, this.pos); + this.pos += 8; + } + +} + +function handleRustResult(result, liftCallback, liftErrCallback) { + switch (result.code) { + case "success": + return liftCallback(result.data); + + case "error": + throw liftErrCallback(result.data); + + case "internal-error": + let message = result.internalErrorMessage; + if (message) { + throw new UniFFIInternalError(message); + } else { + throw new UniFFIInternalError("Unknown error"); + } + + default: + throw new UniFFIError(`Unexpected status code: ${result.code}`); + } +} + +class UniFFIError { + constructor(message) { + this.message = message; + } + + toString() { + return `UniFFIError: ${this.message}` + } +} + +class UniFFIInternalError extends UniFFIError {} + +// Base class for FFI converters +class FfiConverter { + // throw `UniFFITypeError` if a value to be converted has an invalid type + static checkType(value) { + if (value === undefined ) { + throw new UniFFITypeError(`undefined`); + } + if (value === null ) { + throw new UniFFITypeError(`null`); + } + } +} + +// Base class for FFI converters that lift/lower by reading/writing to an ArrayBuffer +class FfiConverterArrayBuffer extends FfiConverter { + static lift(buf) { + return this.read(new ArrayBufferDataStream(buf)); + } + + static lower(value) { + const buf = new ArrayBuffer(this.computeSize(value)); + const dataStream = new ArrayBufferDataStream(buf); + this.write(dataStream, value); + return buf; + } +} + +// Symbols that are used to ensure that Object constructors +// can only be used with a proper UniFFI pointer +const uniffiObjectPtr = Symbol("uniffiObjectPtr"); +const constructUniffiObject = Symbol("constructUniffiObject"); +UnitTestObjs.uniffiObjectPtr = uniffiObjectPtr; + +// Export the FFIConverter object to make external types work. +export class FfiConverterI32 extends FfiConverter { + static checkType(value) { + super.checkType(value); + if (!Number.isInteger(value)) { + throw new UniFFITypeError(`${value} is not an integer`); + } + if (value < -2147483648 || value > 2147483647) { + throw new UniFFITypeError(`${value} exceeds the I32 bounds`); + } + } + static computeSize() { + return 4; + } + static lift(value) { + return value; + } + static lower(value) { + return value; + } + static write(dataStream, value) { + dataStream.writeInt32(value) + } + static read(dataStream) { + return dataStream.readInt32() + } +} + +// Export the FFIConverter object to make external types work. +export class FfiConverterString extends FfiConverter { + static checkType(value) { + super.checkType(value); + if (typeof value !== "string") { + throw new UniFFITypeError(`${value} is not a string`); + } + } + + static lift(buf) { + const decoder = new TextDecoder(); + const utf8Arr = new Uint8Array(buf); + return decoder.decode(utf8Arr); + } + static lower(value) { + const encoder = new TextEncoder(); + return encoder.encode(value).buffer; + } + + static write(dataStream, value) { + dataStream.writeString(value); + } + + static read(dataStream) { + return dataStream.readString(); + } + + static computeSize(value) { + const encoder = new TextEncoder(); + return 4 + encoder.encode(value).length + } +} + +export class SingletonObject { + // Use `init` to instantiate this class. + // DO NOT USE THIS CONSTRUCTOR DIRECTLY + constructor(opts) { + if (!Object.prototype.hasOwnProperty.call(opts, constructUniffiObject)) { + throw new UniFFIError("Attempting to construct an object using the JavaScript constructor directly" + + "Please use a UDL defined constructor, or the init function for the primary constructor") + } + if (!opts[constructUniffiObject] instanceof UniFFIPointer) { + throw new UniFFIError("Attempting to create a UniFFI object with a pointer that is not an instance of UniFFIPointer") + } + this[uniffiObjectPtr] = opts[constructUniffiObject]; + } + + method() { + const liftResult = (result) => undefined; + const liftError = null; + const functionCall = () => { + return UniFFIScaffolding.callSync( + 55, // refcounts:uniffi_uniffi_fixture_refcounts_fn_method_singletonobject_method + FfiConverterTypeSingletonObject.lower(this), + ) + } + return handleRustResult(functionCall(), liftResult, liftError); + } + +} + +// Export the FFIConverter object to make external types work. +export class FfiConverterTypeSingletonObject extends FfiConverter { + static lift(value) { + const opts = {}; + opts[constructUniffiObject] = value; + return new SingletonObject(opts); + } + + static lower(value) { + const ptr = value[uniffiObjectPtr]; + if (!(ptr instanceof UniFFIPointer)) { + throw new UniFFITypeError("Object is not a 'SingletonObject' instance"); + } + return ptr; + } + + static read(dataStream) { + return this.lift(dataStream.readPointerSingletonObject()); + } + + static write(dataStream, value) { + dataStream.writePointerSingletonObject(value[uniffiObjectPtr]); + } + + static computeSize(value) { + return 8; + } +} + + + + + +export function getJsRefcount() { + + const liftResult = (result) => FfiConverterI32.lift(result); + const liftError = null; + const functionCall = () => { + return UniFFIScaffolding.callSync( + 56, // refcounts:uniffi_uniffi_fixture_refcounts_fn_func_get_js_refcount + ) + } + return handleRustResult(functionCall(), liftResult, liftError); +} + +export function getSingleton() { + + const liftResult = (result) => FfiConverterTypeSingletonObject.lift(result); + const liftError = null; + const functionCall = () => { + return UniFFIScaffolding.callSync( + 57, // refcounts:uniffi_uniffi_fixture_refcounts_fn_func_get_singleton + ) + } + return handleRustResult(functionCall(), liftResult, liftError); +} diff --git a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustRondpoint.sys.mjs b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustRondpoint.sys.mjs index 1edb7ec608..f6db36163e 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustRondpoint.sys.mjs +++ b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustRondpoint.sys.mjs @@ -158,7 +158,7 @@ class ArrayBufferDataStream { // UniFFI Pointers are **always** 8 bytes long. That is enforced // by the C++ and Rust Scaffolding code. readPointerOptionneur() { - const pointerId = 5; // rondpoint:Optionneur + const pointerId = 7; // rondpoint:Optionneur const res = UniFFIScaffolding.readPointer(pointerId, this.dataView.buffer, this.pos); this.pos += 8; return res; @@ -168,7 +168,7 @@ class ArrayBufferDataStream { // UniFFI Pointers are **always** 8 bytes long. That is enforced // by the C++ and Rust Scaffolding code. writePointerOptionneur(value) { - const pointerId = 5; // rondpoint:Optionneur + const pointerId = 7; // rondpoint:Optionneur UniFFIScaffolding.writePointer(pointerId, value, this.dataView.buffer, this.pos); this.pos += 8; } @@ -178,7 +178,7 @@ class ArrayBufferDataStream { // UniFFI Pointers are **always** 8 bytes long. That is enforced // by the C++ and Rust Scaffolding code. readPointerRetourneur() { - const pointerId = 6; // rondpoint:Retourneur + const pointerId = 8; // rondpoint:Retourneur const res = UniFFIScaffolding.readPointer(pointerId, this.dataView.buffer, this.pos); this.pos += 8; return res; @@ -188,7 +188,7 @@ class ArrayBufferDataStream { // UniFFI Pointers are **always** 8 bytes long. That is enforced // by the C++ and Rust Scaffolding code. writePointerRetourneur(value) { - const pointerId = 6; // rondpoint:Retourneur + const pointerId = 8; // rondpoint:Retourneur UniFFIScaffolding.writePointer(pointerId, value, this.dataView.buffer, this.pos); this.pos += 8; } @@ -198,7 +198,7 @@ class ArrayBufferDataStream { // UniFFI Pointers are **always** 8 bytes long. That is enforced // by the C++ and Rust Scaffolding code. readPointerStringifier() { - const pointerId = 7; // rondpoint:Stringifier + const pointerId = 9; // rondpoint:Stringifier const res = UniFFIScaffolding.readPointer(pointerId, this.dataView.buffer, this.pos); this.pos += 8; return res; @@ -208,7 +208,7 @@ class ArrayBufferDataStream { // UniFFI Pointers are **always** 8 bytes long. That is enforced // by the C++ and Rust Scaffolding code. writePointerStringifier(value) { - const pointerId = 7; // rondpoint:Stringifier + const pointerId = 9; // rondpoint:Stringifier UniFFIScaffolding.writePointer(pointerId, value, this.dataView.buffer, this.pos); this.pos += 8; } @@ -620,7 +620,7 @@ export class Optionneur { const liftError = null; const functionCall = () => { return UniFFIScaffolding.callAsync( - 46, // rondpoint:uniffi_uniffi_rondpoint_fn_constructor_optionneur_new + 61, // rondpoint:uniffi_uniffi_rondpoint_fn_constructor_optionneur_new ) } try { @@ -642,7 +642,7 @@ export class Optionneur { throw e; } return UniFFIScaffolding.callAsync( - 47, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_boolean + 62, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_boolean FfiConverterTypeOptionneur.lower(this), FfiConverterBool.lower(value), ) @@ -667,7 +667,7 @@ export class Optionneur { throw e; } return UniFFIScaffolding.callAsync( - 48, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_enum + 63, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_enum FfiConverterTypeOptionneur.lower(this), FfiConverterTypeEnumeration.lower(value), ) @@ -692,7 +692,7 @@ export class Optionneur { throw e; } return UniFFIScaffolding.callAsync( - 49, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_f32 + 64, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_f32 FfiConverterTypeOptionneur.lower(this), FfiConverterF32.lower(value), ) @@ -717,7 +717,7 @@ export class Optionneur { throw e; } return UniFFIScaffolding.callAsync( - 50, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_f64 + 65, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_f64 FfiConverterTypeOptionneur.lower(this), FfiConverterF64.lower(value), ) @@ -742,7 +742,7 @@ export class Optionneur { throw e; } return UniFFIScaffolding.callAsync( - 51, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i16_dec + 66, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i16_dec FfiConverterTypeOptionneur.lower(this), FfiConverterI16.lower(value), ) @@ -767,7 +767,7 @@ export class Optionneur { throw e; } return UniFFIScaffolding.callAsync( - 52, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i16_hex + 67, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i16_hex FfiConverterTypeOptionneur.lower(this), FfiConverterI16.lower(value), ) @@ -792,7 +792,7 @@ export class Optionneur { throw e; } return UniFFIScaffolding.callAsync( - 53, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i32_dec + 68, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i32_dec FfiConverterTypeOptionneur.lower(this), FfiConverterI32.lower(value), ) @@ -817,7 +817,7 @@ export class Optionneur { throw e; } return UniFFIScaffolding.callAsync( - 54, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i32_hex + 69, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i32_hex FfiConverterTypeOptionneur.lower(this), FfiConverterI32.lower(value), ) @@ -842,7 +842,7 @@ export class Optionneur { throw e; } return UniFFIScaffolding.callAsync( - 55, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i64_dec + 70, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i64_dec FfiConverterTypeOptionneur.lower(this), FfiConverterI64.lower(value), ) @@ -867,7 +867,7 @@ export class Optionneur { throw e; } return UniFFIScaffolding.callAsync( - 56, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i64_hex + 71, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i64_hex FfiConverterTypeOptionneur.lower(this), FfiConverterI64.lower(value), ) @@ -892,7 +892,7 @@ export class Optionneur { throw e; } return UniFFIScaffolding.callAsync( - 57, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i8_dec + 72, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i8_dec FfiConverterTypeOptionneur.lower(this), FfiConverterI8.lower(value), ) @@ -917,7 +917,7 @@ export class Optionneur { throw e; } return UniFFIScaffolding.callAsync( - 58, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i8_hex + 73, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i8_hex FfiConverterTypeOptionneur.lower(this), FfiConverterI8.lower(value), ) @@ -942,7 +942,7 @@ export class Optionneur { throw e; } return UniFFIScaffolding.callAsync( - 59, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_null + 74, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_null FfiConverterTypeOptionneur.lower(this), FfiConverterOptionalstring.lower(value), ) @@ -967,7 +967,7 @@ export class Optionneur { throw e; } return UniFFIScaffolding.callAsync( - 60, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_sequence + 75, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_sequence FfiConverterTypeOptionneur.lower(this), FfiConverterSequencestring.lower(value), ) @@ -992,7 +992,7 @@ export class Optionneur { throw e; } return UniFFIScaffolding.callAsync( - 61, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_string + 76, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_string FfiConverterTypeOptionneur.lower(this), FfiConverterString.lower(value), ) @@ -1017,7 +1017,7 @@ export class Optionneur { throw e; } return UniFFIScaffolding.callAsync( - 62, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u16_dec + 77, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u16_dec FfiConverterTypeOptionneur.lower(this), FfiConverterU16.lower(value), ) @@ -1042,7 +1042,7 @@ export class Optionneur { throw e; } return UniFFIScaffolding.callAsync( - 63, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u16_hex + 78, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u16_hex FfiConverterTypeOptionneur.lower(this), FfiConverterU16.lower(value), ) @@ -1067,7 +1067,7 @@ export class Optionneur { throw e; } return UniFFIScaffolding.callAsync( - 64, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_dec + 79, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_dec FfiConverterTypeOptionneur.lower(this), FfiConverterU32.lower(value), ) @@ -1092,7 +1092,7 @@ export class Optionneur { throw e; } return UniFFIScaffolding.callAsync( - 65, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_hex + 80, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_hex FfiConverterTypeOptionneur.lower(this), FfiConverterU32.lower(value), ) @@ -1117,7 +1117,7 @@ export class Optionneur { throw e; } return UniFFIScaffolding.callAsync( - 66, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_oct + 81, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_oct FfiConverterTypeOptionneur.lower(this), FfiConverterU32.lower(value), ) @@ -1142,7 +1142,7 @@ export class Optionneur { throw e; } return UniFFIScaffolding.callAsync( - 67, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u64_dec + 82, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u64_dec FfiConverterTypeOptionneur.lower(this), FfiConverterU64.lower(value), ) @@ -1167,7 +1167,7 @@ export class Optionneur { throw e; } return UniFFIScaffolding.callAsync( - 68, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u64_hex + 83, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u64_hex FfiConverterTypeOptionneur.lower(this), FfiConverterU64.lower(value), ) @@ -1192,7 +1192,7 @@ export class Optionneur { throw e; } return UniFFIScaffolding.callAsync( - 69, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u8_dec + 84, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u8_dec FfiConverterTypeOptionneur.lower(this), FfiConverterU8.lower(value), ) @@ -1217,7 +1217,7 @@ export class Optionneur { throw e; } return UniFFIScaffolding.callAsync( - 70, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u8_hex + 85, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u8_hex FfiConverterTypeOptionneur.lower(this), FfiConverterU8.lower(value), ) @@ -1242,7 +1242,7 @@ export class Optionneur { throw e; } return UniFFIScaffolding.callAsync( - 71, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_zero + 86, // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_zero FfiConverterTypeOptionneur.lower(this), FfiConverterOptionali32.lower(value), ) @@ -1265,7 +1265,11 @@ export class FfiConverterTypeOptionneur extends FfiConverter { } static lower(value) { - return value[uniffiObjectPtr]; + const ptr = value[uniffiObjectPtr]; + if (!(ptr instanceof UniFFIPointer)) { + throw new UniFFITypeError("Object is not a 'Optionneur' instance"); + } + return ptr; } static read(dataStream) { @@ -1305,7 +1309,7 @@ export class Retourneur { const liftError = null; const functionCall = () => { return UniFFIScaffolding.callAsync( - 72, // rondpoint:uniffi_uniffi_rondpoint_fn_constructor_retourneur_new + 88, // rondpoint:uniffi_uniffi_rondpoint_fn_constructor_retourneur_new ) } try { @@ -1327,7 +1331,7 @@ export class Retourneur { throw e; } return UniFFIScaffolding.callAsync( - 73, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_boolean + 89, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_boolean FfiConverterTypeRetourneur.lower(this), FfiConverterBool.lower(value), ) @@ -1352,7 +1356,7 @@ export class Retourneur { throw e; } return UniFFIScaffolding.callAsync( - 74, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_double + 90, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_double FfiConverterTypeRetourneur.lower(this), FfiConverterF64.lower(value), ) @@ -1377,7 +1381,7 @@ export class Retourneur { throw e; } return UniFFIScaffolding.callAsync( - 75, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_float + 91, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_float FfiConverterTypeRetourneur.lower(this), FfiConverterF32.lower(value), ) @@ -1402,7 +1406,7 @@ export class Retourneur { throw e; } return UniFFIScaffolding.callAsync( - 76, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i16 + 92, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i16 FfiConverterTypeRetourneur.lower(this), FfiConverterI16.lower(value), ) @@ -1427,7 +1431,7 @@ export class Retourneur { throw e; } return UniFFIScaffolding.callAsync( - 77, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i32 + 93, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i32 FfiConverterTypeRetourneur.lower(this), FfiConverterI32.lower(value), ) @@ -1452,7 +1456,7 @@ export class Retourneur { throw e; } return UniFFIScaffolding.callAsync( - 78, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i64 + 94, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i64 FfiConverterTypeRetourneur.lower(this), FfiConverterI64.lower(value), ) @@ -1477,7 +1481,7 @@ export class Retourneur { throw e; } return UniFFIScaffolding.callAsync( - 79, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i8 + 95, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i8 FfiConverterTypeRetourneur.lower(this), FfiConverterI8.lower(value), ) @@ -1502,7 +1506,7 @@ export class Retourneur { throw e; } return UniFFIScaffolding.callAsync( - 80, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_nombres + 96, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_nombres FfiConverterTypeRetourneur.lower(this), FfiConverterTypeDictionnaireNombres.lower(value), ) @@ -1527,7 +1531,7 @@ export class Retourneur { throw e; } return UniFFIScaffolding.callAsync( - 81, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_nombres_signes + 97, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_nombres_signes FfiConverterTypeRetourneur.lower(this), FfiConverterTypeDictionnaireNombresSignes.lower(value), ) @@ -1552,7 +1556,7 @@ export class Retourneur { throw e; } return UniFFIScaffolding.callAsync( - 82, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_optionneur_dictionnaire + 98, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_optionneur_dictionnaire FfiConverterTypeRetourneur.lower(this), FfiConverterTypeOptionneurDictionnaire.lower(value), ) @@ -1577,7 +1581,7 @@ export class Retourneur { throw e; } return UniFFIScaffolding.callAsync( - 83, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_string + 99, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_string FfiConverterTypeRetourneur.lower(this), FfiConverterString.lower(value), ) @@ -1602,7 +1606,7 @@ export class Retourneur { throw e; } return UniFFIScaffolding.callAsync( - 84, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u16 + 100, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u16 FfiConverterTypeRetourneur.lower(this), FfiConverterU16.lower(value), ) @@ -1627,7 +1631,7 @@ export class Retourneur { throw e; } return UniFFIScaffolding.callAsync( - 85, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u32 + 101, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u32 FfiConverterTypeRetourneur.lower(this), FfiConverterU32.lower(value), ) @@ -1652,7 +1656,7 @@ export class Retourneur { throw e; } return UniFFIScaffolding.callAsync( - 86, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u64 + 102, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u64 FfiConverterTypeRetourneur.lower(this), FfiConverterU64.lower(value), ) @@ -1677,7 +1681,7 @@ export class Retourneur { throw e; } return UniFFIScaffolding.callAsync( - 87, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u8 + 103, // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u8 FfiConverterTypeRetourneur.lower(this), FfiConverterU8.lower(value), ) @@ -1700,7 +1704,11 @@ export class FfiConverterTypeRetourneur extends FfiConverter { } static lower(value) { - return value[uniffiObjectPtr]; + const ptr = value[uniffiObjectPtr]; + if (!(ptr instanceof UniFFIPointer)) { + throw new UniFFITypeError("Object is not a 'Retourneur' instance"); + } + return ptr; } static read(dataStream) { @@ -1740,7 +1748,7 @@ export class Stringifier { const liftError = null; const functionCall = () => { return UniFFIScaffolding.callAsync( - 88, // rondpoint:uniffi_uniffi_rondpoint_fn_constructor_stringifier_new + 105, // rondpoint:uniffi_uniffi_rondpoint_fn_constructor_stringifier_new ) } try { @@ -1762,7 +1770,7 @@ export class Stringifier { throw e; } return UniFFIScaffolding.callAsync( - 89, // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_boolean + 106, // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_boolean FfiConverterTypeStringifier.lower(this), FfiConverterBool.lower(value), ) @@ -1787,7 +1795,7 @@ export class Stringifier { throw e; } return UniFFIScaffolding.callAsync( - 90, // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_double + 107, // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_double FfiConverterTypeStringifier.lower(this), FfiConverterF64.lower(value), ) @@ -1812,7 +1820,7 @@ export class Stringifier { throw e; } return UniFFIScaffolding.callAsync( - 91, // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_float + 108, // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_float FfiConverterTypeStringifier.lower(this), FfiConverterF32.lower(value), ) @@ -1837,7 +1845,7 @@ export class Stringifier { throw e; } return UniFFIScaffolding.callAsync( - 92, // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i16 + 109, // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i16 FfiConverterTypeStringifier.lower(this), FfiConverterI16.lower(value), ) @@ -1862,7 +1870,7 @@ export class Stringifier { throw e; } return UniFFIScaffolding.callAsync( - 93, // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i32 + 110, // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i32 FfiConverterTypeStringifier.lower(this), FfiConverterI32.lower(value), ) @@ -1887,7 +1895,7 @@ export class Stringifier { throw e; } return UniFFIScaffolding.callAsync( - 94, // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i64 + 111, // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i64 FfiConverterTypeStringifier.lower(this), FfiConverterI64.lower(value), ) @@ -1912,7 +1920,7 @@ export class Stringifier { throw e; } return UniFFIScaffolding.callAsync( - 95, // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i8 + 112, // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i8 FfiConverterTypeStringifier.lower(this), FfiConverterI8.lower(value), ) @@ -1937,7 +1945,7 @@ export class Stringifier { throw e; } return UniFFIScaffolding.callAsync( - 96, // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u16 + 113, // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u16 FfiConverterTypeStringifier.lower(this), FfiConverterU16.lower(value), ) @@ -1962,7 +1970,7 @@ export class Stringifier { throw e; } return UniFFIScaffolding.callAsync( - 97, // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u32 + 114, // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u32 FfiConverterTypeStringifier.lower(this), FfiConverterU32.lower(value), ) @@ -1987,7 +1995,7 @@ export class Stringifier { throw e; } return UniFFIScaffolding.callAsync( - 98, // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u64 + 115, // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u64 FfiConverterTypeStringifier.lower(this), FfiConverterU64.lower(value), ) @@ -2012,7 +2020,7 @@ export class Stringifier { throw e; } return UniFFIScaffolding.callAsync( - 99, // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u8 + 116, // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u8 FfiConverterTypeStringifier.lower(this), FfiConverterU8.lower(value), ) @@ -2037,7 +2045,7 @@ export class Stringifier { throw e; } return UniFFIScaffolding.callAsync( - 100, // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_well_known_string + 117, // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_well_known_string FfiConverterTypeStringifier.lower(this), FfiConverterString.lower(value), ) @@ -2060,7 +2068,11 @@ export class FfiConverterTypeStringifier extends FfiConverter { } static lower(value) { - return value[uniffiObjectPtr]; + const ptr = value[uniffiObjectPtr]; + if (!(ptr instanceof UniFFIPointer)) { + throw new UniFFITypeError("Object is not a 'Stringifier' instance"); + } + return ptr; } static read(dataStream) { @@ -2154,7 +2166,7 @@ export class FfiConverterTypeDictionnaire extends FfiConverterArrayBuffer { static checkType(value) { super.checkType(value); if (!(value instanceof Dictionnaire)) { - throw new TypeError(`Expected 'Dictionnaire', found '${typeof value}'`); + throw new UniFFITypeError(`Expected 'Dictionnaire', found '${typeof value}'`); } try { FfiConverterTypeEnumeration.checkType(value.un); @@ -2269,7 +2281,7 @@ export class FfiConverterTypeDictionnaireNombres extends FfiConverterArrayBuffer static checkType(value) { super.checkType(value); if (!(value instanceof DictionnaireNombres)) { - throw new TypeError(`Expected 'DictionnaireNombres', found '${typeof value}'`); + throw new UniFFITypeError(`Expected 'DictionnaireNombres', found '${typeof value}'`); } try { FfiConverterU8.checkType(value.petitNombre); @@ -2384,7 +2396,7 @@ export class FfiConverterTypeDictionnaireNombresSignes extends FfiConverterArray static checkType(value) { super.checkType(value); if (!(value instanceof DictionnaireNombresSignes)) { - throw new TypeError(`Expected 'DictionnaireNombresSignes', found '${typeof value}'`); + throw new UniFFITypeError(`Expected 'DictionnaireNombresSignes', found '${typeof value}'`); } try { FfiConverterI8.checkType(value.petitNombre); @@ -2642,7 +2654,7 @@ export class FfiConverterTypeOptionneurDictionnaire extends FfiConverterArrayBuf static checkType(value) { super.checkType(value); if (!(value instanceof OptionneurDictionnaire)) { - throw new TypeError(`Expected 'OptionneurDictionnaire', found '${typeof value}'`); + throw new UniFFITypeError(`Expected 'OptionneurDictionnaire', found '${typeof value}'`); } try { FfiConverterI8.checkType(value.i8Var); @@ -2806,7 +2818,7 @@ export class FfiConverterTypeminusculeMajusculeDict extends FfiConverterArrayBuf static checkType(value) { super.checkType(value); if (!(value instanceof MinusculeMajusculeDict)) { - throw new TypeError(`Expected 'MinusculeMajusculeDict', found '${typeof value}'`); + throw new UniFFITypeError(`Expected 'MinusculeMajusculeDict', found '${typeof value}'`); } try { FfiConverterBool.checkType(value.minusculeMajusculeField); @@ -2838,7 +2850,7 @@ export class FfiConverterTypeEnumeration extends FfiConverterArrayBuffer { case 3: return Enumeration.TROIS default: - return new Error("Unknown Enumeration variant"); + throw new UniFFITypeError("Unknown Enumeration variant"); } } @@ -2855,7 +2867,7 @@ export class FfiConverterTypeEnumeration extends FfiConverterArrayBuffer { dataStream.writeInt32(3); return; } - return new Error("Unknown Enumeration variant"); + throw new UniFFITypeError("Unknown Enumeration variant"); } static computeSize(value) { @@ -2914,7 +2926,7 @@ export class FfiConverterTypeEnumerationAvecDonnees extends FfiConverterArrayBuf FfiConverterString.read(dataStream) ); default: - return new Error("Unknown EnumerationAvecDonnees variant"); + throw new UniFFITypeError("Unknown EnumerationAvecDonnees variant"); } } @@ -2934,7 +2946,7 @@ export class FfiConverterTypeEnumerationAvecDonnees extends FfiConverterArrayBuf FfiConverterString.write(dataStream, value.second); return; } - return new Error("Unknown EnumerationAvecDonnees variant"); + throw new UniFFITypeError("Unknown EnumerationAvecDonnees variant"); } static computeSize(value) { @@ -2952,7 +2964,7 @@ export class FfiConverterTypeEnumerationAvecDonnees extends FfiConverterArrayBuf totalSize += FfiConverterString.computeSize(value.second); return totalSize; } - return new Error("Unknown EnumerationAvecDonnees variant"); + throw new UniFFITypeError("Unknown EnumerationAvecDonnees variant"); } static checkType(value) { @@ -2976,7 +2988,7 @@ export class FfiConverterTypeminusculeMajusculeEnum extends FfiConverterArrayBuf case 1: return MinusculeMajusculeEnum.MINUSCULE_MAJUSCULE_VARIANT default: - return new Error("Unknown MinusculeMajusculeEnum variant"); + throw new UniFFITypeError("Unknown MinusculeMajusculeEnum variant"); } } @@ -2985,7 +2997,7 @@ export class FfiConverterTypeminusculeMajusculeEnum extends FfiConverterArrayBuf dataStream.writeInt32(1); return; } - return new Error("Unknown MinusculeMajusculeEnum variant"); + throw new UniFFITypeError("Unknown MinusculeMajusculeEnum variant"); } static computeSize(value) { @@ -3272,7 +3284,7 @@ export function copieCarte(c) { throw e; } return UniFFIScaffolding.callAsync( - 101, // rondpoint:uniffi_uniffi_rondpoint_fn_func_copie_carte + 118, // rondpoint:uniffi_uniffi_rondpoint_fn_func_copie_carte FfiConverterMapStringTypeEnumerationAvecDonnees.lower(c), ) } @@ -3297,7 +3309,7 @@ export function copieDictionnaire(d) { throw e; } return UniFFIScaffolding.callAsync( - 102, // rondpoint:uniffi_uniffi_rondpoint_fn_func_copie_dictionnaire + 119, // rondpoint:uniffi_uniffi_rondpoint_fn_func_copie_dictionnaire FfiConverterTypeDictionnaire.lower(d), ) } @@ -3322,7 +3334,7 @@ export function copieEnumeration(e) { throw e; } return UniFFIScaffolding.callAsync( - 103, // rondpoint:uniffi_uniffi_rondpoint_fn_func_copie_enumeration + 120, // rondpoint:uniffi_uniffi_rondpoint_fn_func_copie_enumeration FfiConverterTypeEnumeration.lower(e), ) } @@ -3347,7 +3359,7 @@ export function copieEnumerations(e) { throw e; } return UniFFIScaffolding.callAsync( - 104, // rondpoint:uniffi_uniffi_rondpoint_fn_func_copie_enumerations + 121, // rondpoint:uniffi_uniffi_rondpoint_fn_func_copie_enumerations FfiConverterSequenceTypeEnumeration.lower(e), ) } @@ -3372,7 +3384,7 @@ export function switcheroo(b) { throw e; } return UniFFIScaffolding.callAsync( - 105, // rondpoint:uniffi_uniffi_rondpoint_fn_func_switcheroo + 122, // rondpoint:uniffi_uniffi_rondpoint_fn_func_switcheroo FfiConverterBool.lower(b), ) } diff --git a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustSprites.sys.mjs b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustSprites.sys.mjs index b404af06b1..c25a488009 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustSprites.sys.mjs +++ b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustSprites.sys.mjs @@ -158,7 +158,7 @@ class ArrayBufferDataStream { // UniFFI Pointers are **always** 8 bytes long. That is enforced // by the C++ and Rust Scaffolding code. readPointerSprite() { - const pointerId = 8; // sprites:Sprite + const pointerId = 10; // sprites:Sprite const res = UniFFIScaffolding.readPointer(pointerId, this.dataView.buffer, this.pos); this.pos += 8; return res; @@ -168,7 +168,7 @@ class ArrayBufferDataStream { // UniFFI Pointers are **always** 8 bytes long. That is enforced // by the C++ and Rust Scaffolding code. writePointerSprite(value) { - const pointerId = 8; // sprites:Sprite + const pointerId = 10; // sprites:Sprite UniFFIScaffolding.writePointer(pointerId, value, this.dataView.buffer, this.pos); this.pos += 8; } @@ -325,7 +325,7 @@ export class Sprite { throw e; } return UniFFIScaffolding.callAsync( - 106, // sprites:uniffi_uniffi_sprites_fn_constructor_sprite_new + 124, // sprites:uniffi_uniffi_sprites_fn_constructor_sprite_new FfiConverterOptionalTypePoint.lower(initialPosition), ) } @@ -361,7 +361,7 @@ export class Sprite { throw e; } return UniFFIScaffolding.callAsync( - 107, // sprites:uniffi_uniffi_sprites_fn_constructor_sprite_new_relative_to + 125, // sprites:uniffi_uniffi_sprites_fn_constructor_sprite_new_relative_to FfiConverterTypePoint.lower(reference), FfiConverterTypeVector.lower(direction), ) @@ -377,7 +377,7 @@ export class Sprite { const liftError = null; const functionCall = () => { return UniFFIScaffolding.callAsync( - 108, // sprites:uniffi_uniffi_sprites_fn_method_sprite_get_position + 126, // sprites:uniffi_uniffi_sprites_fn_method_sprite_get_position FfiConverterTypeSprite.lower(this), ) } @@ -401,7 +401,7 @@ export class Sprite { throw e; } return UniFFIScaffolding.callAsync( - 109, // sprites:uniffi_uniffi_sprites_fn_method_sprite_move_by + 127, // sprites:uniffi_uniffi_sprites_fn_method_sprite_move_by FfiConverterTypeSprite.lower(this), FfiConverterTypeVector.lower(direction), ) @@ -426,7 +426,7 @@ export class Sprite { throw e; } return UniFFIScaffolding.callAsync( - 110, // sprites:uniffi_uniffi_sprites_fn_method_sprite_move_to + 128, // sprites:uniffi_uniffi_sprites_fn_method_sprite_move_to FfiConverterTypeSprite.lower(this), FfiConverterTypePoint.lower(position), ) @@ -449,7 +449,11 @@ export class FfiConverterTypeSprite extends FfiConverter { } static lower(value) { - return value[uniffiObjectPtr]; + const ptr = value[uniffiObjectPtr]; + if (!(ptr instanceof UniFFIPointer)) { + throw new UniFFITypeError("Object is not a 'Sprite' instance"); + } + return ptr; } static read(dataStream) { @@ -517,7 +521,7 @@ export class FfiConverterTypePoint extends FfiConverterArrayBuffer { static checkType(value) { super.checkType(value); if (!(value instanceof Point)) { - throw new TypeError(`Expected 'Point', found '${typeof value}'`); + throw new UniFFITypeError(`Expected 'Point', found '${typeof value}'`); } try { FfiConverterF64.checkType(value.x); @@ -590,7 +594,7 @@ export class FfiConverterTypeVector extends FfiConverterArrayBuffer { static checkType(value) { super.checkType(value); if (!(value instanceof Vector)) { - throw new TypeError(`Expected 'Vector', found '${typeof value}'`); + throw new UniFFITypeError(`Expected 'Vector', found '${typeof value}'`); } try { FfiConverterF64.checkType(value.dx); @@ -674,7 +678,7 @@ export function translate(position,direction) { throw e; } return UniFFIScaffolding.callAsync( - 111, // sprites:uniffi_uniffi_sprites_fn_func_translate + 129, // sprites:uniffi_uniffi_sprites_fn_func_translate FfiConverterTypePoint.lower(position), FfiConverterTypeVector.lower(direction), ) diff --git a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustTodolist.sys.mjs b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustTodolist.sys.mjs index 59996b78fa..ddeccc9bf2 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustTodolist.sys.mjs +++ b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/generated/RustTodolist.sys.mjs @@ -158,7 +158,7 @@ class ArrayBufferDataStream { // UniFFI Pointers are **always** 8 bytes long. That is enforced // by the C++ and Rust Scaffolding code. readPointerTodoList() { - const pointerId = 9; // todolist:TodoList + const pointerId = 11; // todolist:TodoList const res = UniFFIScaffolding.readPointer(pointerId, this.dataView.buffer, this.pos); this.pos += 8; return res; @@ -168,7 +168,7 @@ class ArrayBufferDataStream { // UniFFI Pointers are **always** 8 bytes long. That is enforced // by the C++ and Rust Scaffolding code. writePointerTodoList(value) { - const pointerId = 9; // todolist:TodoList + const pointerId = 11; // todolist:TodoList UniFFIScaffolding.writePointer(pointerId, value, this.dataView.buffer, this.pos); this.pos += 8; } @@ -298,7 +298,7 @@ export class TodoList { const liftError = null; const functionCall = () => { return UniFFIScaffolding.callAsync( - 112, // todolist:uniffi_uniffi_todolist_fn_constructor_todolist_new + 131, // todolist:uniffi_uniffi_todolist_fn_constructor_todolist_new ) } try { @@ -320,7 +320,7 @@ export class TodoList { throw e; } return UniFFIScaffolding.callAsync( - 113, // todolist:uniffi_uniffi_todolist_fn_method_todolist_add_entries + 132, // todolist:uniffi_uniffi_todolist_fn_method_todolist_add_entries FfiConverterTypeTodoList.lower(this), FfiConverterSequenceTypeTodoEntry.lower(entries), ) @@ -345,7 +345,7 @@ export class TodoList { throw e; } return UniFFIScaffolding.callAsync( - 114, // todolist:uniffi_uniffi_todolist_fn_method_todolist_add_entry + 133, // todolist:uniffi_uniffi_todolist_fn_method_todolist_add_entry FfiConverterTypeTodoList.lower(this), FfiConverterTypeTodoEntry.lower(entry), ) @@ -370,7 +370,7 @@ export class TodoList { throw e; } return UniFFIScaffolding.callAsync( - 115, // todolist:uniffi_uniffi_todolist_fn_method_todolist_add_item + 134, // todolist:uniffi_uniffi_todolist_fn_method_todolist_add_item FfiConverterTypeTodoList.lower(this), FfiConverterString.lower(todo), ) @@ -395,7 +395,7 @@ export class TodoList { throw e; } return UniFFIScaffolding.callAsync( - 116, // todolist:uniffi_uniffi_todolist_fn_method_todolist_add_items + 135, // todolist:uniffi_uniffi_todolist_fn_method_todolist_add_items FfiConverterTypeTodoList.lower(this), FfiConverterSequencestring.lower(items), ) @@ -420,7 +420,7 @@ export class TodoList { throw e; } return UniFFIScaffolding.callAsync( - 117, // todolist:uniffi_uniffi_todolist_fn_method_todolist_clear_item + 136, // todolist:uniffi_uniffi_todolist_fn_method_todolist_clear_item FfiConverterTypeTodoList.lower(this), FfiConverterString.lower(todo), ) @@ -437,7 +437,7 @@ export class TodoList { const liftError = null; const functionCall = () => { return UniFFIScaffolding.callAsync( - 118, // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_entries + 137, // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_entries FfiConverterTypeTodoList.lower(this), ) } @@ -453,7 +453,7 @@ export class TodoList { const liftError = (data) => FfiConverterTypeTodoError.lift(data); const functionCall = () => { return UniFFIScaffolding.callAsync( - 119, // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_first + 138, // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_first FfiConverterTypeTodoList.lower(this), ) } @@ -469,7 +469,7 @@ export class TodoList { const liftError = null; const functionCall = () => { return UniFFIScaffolding.callAsync( - 120, // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_items + 139, // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_items FfiConverterTypeTodoList.lower(this), ) } @@ -485,7 +485,7 @@ export class TodoList { const liftError = (data) => FfiConverterTypeTodoError.lift(data); const functionCall = () => { return UniFFIScaffolding.callAsync( - 121, // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_last + 140, // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_last FfiConverterTypeTodoList.lower(this), ) } @@ -501,7 +501,7 @@ export class TodoList { const liftError = (data) => FfiConverterTypeTodoError.lift(data); const functionCall = () => { return UniFFIScaffolding.callAsync( - 122, // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_last_entry + 141, // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_last_entry FfiConverterTypeTodoList.lower(this), ) } @@ -517,7 +517,7 @@ export class TodoList { const liftError = null; const functionCall = () => { return UniFFIScaffolding.callAsync( - 123, // todolist:uniffi_uniffi_todolist_fn_method_todolist_make_default + 142, // todolist:uniffi_uniffi_todolist_fn_method_todolist_make_default FfiConverterTypeTodoList.lower(this), ) } @@ -539,7 +539,11 @@ export class FfiConverterTypeTodoList extends FfiConverter { } static lower(value) { - return value[uniffiObjectPtr]; + const ptr = value[uniffiObjectPtr]; + if (!(ptr instanceof UniFFIPointer)) { + throw new UniFFITypeError("Object is not a 'TodoList' instance"); + } + return ptr; } static read(dataStream) { @@ -594,7 +598,7 @@ export class FfiConverterTypeTodoEntry extends FfiConverterArrayBuffer { static checkType(value) { super.checkType(value); if (!(value instanceof TodoEntry)) { - throw new TypeError(`Expected 'TodoEntry', found '${typeof value}'`); + throw new UniFFITypeError(`Expected 'TodoEntry', found '${typeof value}'`); } try { FfiConverterString.checkType(value.text); @@ -683,7 +687,7 @@ export class FfiConverterTypeTodoError extends FfiConverterArrayBuffer { case 5: return new DeligatedError(FfiConverterString.read(dataStream)); default: - throw new Error("Unknown TodoError variant"); + throw new UniFFITypeError("Unknown TodoError variant"); } } static computeSize(value) { @@ -704,7 +708,7 @@ export class FfiConverterTypeTodoError extends FfiConverterArrayBuffer { if (value instanceof DeligatedError) { return totalSize; } - throw new Error("Unknown TodoError variant"); + throw new UniFFITypeError("Unknown TodoError variant"); } static write(dataStream, value) { if (value instanceof TodoDoesNotExist) { @@ -727,7 +731,7 @@ export class FfiConverterTypeTodoError extends FfiConverterArrayBuffer { dataStream.writeInt32(5); return; } - throw new Error("Unknown TodoError variant"); + throw new UniFFITypeError("Unknown TodoError variant"); } static errorClass = TodoError; @@ -876,7 +880,7 @@ export function createEntryWith(todo) { throw e; } return UniFFIScaffolding.callAsync( - 124, // todolist:uniffi_uniffi_todolist_fn_func_create_entry_with + 143, // todolist:uniffi_uniffi_todolist_fn_func_create_entry_with FfiConverterString.lower(todo), ) } @@ -893,7 +897,7 @@ export function getDefaultList() { const liftError = null; const functionCall = () => { return UniFFIScaffolding.callAsync( - 125, // todolist:uniffi_uniffi_todolist_fn_func_get_default_list + 144, // todolist:uniffi_uniffi_todolist_fn_func_get_default_list ) } try { @@ -917,7 +921,7 @@ export function setDefaultList(list) { throw e; } return UniFFIScaffolding.callAsync( - 126, // todolist:uniffi_uniffi_todolist_fn_func_set_default_list + 145, // todolist:uniffi_uniffi_todolist_fn_func_set_default_list FfiConverterTypeTodoList.lower(list), ) } diff --git a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/moz.build b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/moz.build index d5ff2d7cb6..eed04220cb 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/moz.build +++ b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/moz.build @@ -12,6 +12,7 @@ components = [ "ExternalTypes", "FixtureCallbacks", "Geometry", + "Refcounts", "Rondpoint", "Sprites", "Todolist", diff --git a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/test_external_types.js b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/test_external_types.js index e2f985cde7..699dcfe6f3 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/test_external_types.js +++ b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/test_external_types.js @@ -6,10 +6,10 @@ const ExternalTypes = ChromeUtils.importESModule( ); add_task(async function () { - const line = new ExternalTypes.Line( - new ExternalTypes.Point(0, 0, "p1"), - new ExternalTypes.Point(2, 1, "p2") - ); + const line = new ExternalTypes.Line({ + start: await new ExternalTypes.Point({ coordX: 0, coordY: 0 }), + end: await new ExternalTypes.Point({ coordX: 2, coordY: 1 }), + }); Assert.equal(await ExternalTypes.gradient(line), 0.5); Assert.equal(await ExternalTypes.gradient(null), 0.0); diff --git a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/test_geometry.js b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/test_geometry.js index cdb552e4ef..1028cd37c2 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/test_geometry.js +++ b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/test_geometry.js @@ -6,24 +6,16 @@ const Geometry = ChromeUtils.importESModule( ); add_task(async function () { - const ln1 = new Geometry.Line( - new Geometry.Point({ coord_x: 0, coord_y: 0 }), - new Geometry.Point({ coord_x: 1, coord_y: 2 }) - ); - const ln2 = new Geometry.Line( - new Geometry.Point({ coord_x: 1, coord_y: 1 }), - new Geometry.Point({ coord_x: 2, coord_y: 2 }) - ); - const origin = new Geometry.Point({ coord_x: 0, coord_y: 0 }); - Assert.ok( - (await Geometry.intersection({ start: ln1, end: ln2 })).equals(origin) - ); - Assert.deepEqual( - await Geometry.intersection({ start: ln1, end: ln2 }), - origin - ); - Assert.strictEqual( - await Geometry.intersection({ start: ln1, end: ln1 }), - null - ); + const ln1 = new Geometry.Line({ + start: new Geometry.Point({ coordX: 0, coordY: 0 }), + end: new Geometry.Point({ coordX: 1, coordY: 2 }), + }); + const ln2 = new Geometry.Line({ + start: new Geometry.Point({ coordX: 1, coordY: 1 }), + end: new Geometry.Point({ coordX: 2, coordY: 2 }), + }); + const origin = new Geometry.Point({ coordX: 0, coordY: 0 }); + Assert.ok((await Geometry.intersection(ln1, ln2)).equals(origin)); + Assert.deepEqual(await Geometry.intersection(ln1, ln2), origin); + Assert.strictEqual(await Geometry.intersection(ln1, ln1), null); }); diff --git a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/test_refcounts.js b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/test_refcounts.js new file mode 100644 index 0000000000..790d981104 --- /dev/null +++ b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/test_refcounts.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { getSingleton, getJsRefcount } = ChromeUtils.importESModule( + "resource://gre/modules/RustRefcounts.sys.mjs" +); + +// Test refcounts when we call methods. +// +// Each method call requires that we clone the Arc pointer on the JS side, then pass it to Rust +// which will consumer the reference. Make sure we get this right + +function createObjectAndCallMethods() { + const obj = getSingleton(); + obj.method(); +} + +add_test(() => { + // Create an object that we'll keep around. If the ref count ends up being low, we don't want + // to reduce it below 0, since the Rust code may catch that and clamp it + const obj = getSingleton(); + createObjectAndCallMethods(); + Cu.forceGC(); + Cu.forceCC(); + do_test_pending(); + do_timeout(500, () => { + Assert.equal(getJsRefcount(), 1); + // Use `obj` to avoid unused warnings and try to ensure that JS doesn't destroy it early + obj.method(); + do_test_finished(); + run_next_test(); + }); +}); + +// Test refcounts when creating/destroying objects +function createAndDeleteObjects() { + [getSingleton(), getSingleton(), getSingleton()]; +} + +add_test(() => { + const obj = getSingleton(); + createAndDeleteObjects(); + Cu.forceGC(); + Cu.forceCC(); + do_timeout(500, () => { + Assert.equal(getJsRefcount(), 1); + obj.method(); + do_test_finished(); + run_next_test(); + }); +}); + +// As we implement more UniFFI features we should probably add refcount tests for it. +// Some features that should probably have tests: +// - Async methods +// - UniFFI builtin trait methods like 'to_string' +// - Rust trait objects diff --git a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/test_rondpoint.js b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/test_rondpoint.js index 8c673b2e92..4ad76074f9 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/test_rondpoint.js +++ b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/test_rondpoint.js @@ -22,7 +22,12 @@ const { OptionneurDictionnaire, } = Rondpoint; add_task(async function () { - const dico = new Dictionnaire(Enumeration.DEUX, true, 0, 1235); + const dico = new Dictionnaire({ + un: Enumeration.DEUX, + deux: true, + petitNombre: 0, + grosNombre: 1235, + }); const copyDico = await copieDictionnaire(dico); Assert.deepEqual(dico, copyDico); @@ -127,13 +132,29 @@ add_task(async function () { ); await affirmAllerRetour( - [-1, 0, 1].map(n => new DictionnaireNombresSignes(n, n, n, n)), + [-1, 0, 1].map( + n => + new DictionnaireNombresSignes({ + petitNombre: n, + courtNombre: n, + nombreSimple: n, + grosNombre: n, + }) + ), rt.identiqueNombresSignes.bind(rt), (a, b) => Assert.deepEqual(a, b) ); await affirmAllerRetour( - [0, 1].map(n => new DictionnaireNombres(n, n, n, n)), + [0, 1].map( + n => + new DictionnaireNombres({ + petitNombre: n, + courtNombre: n, + nombreSimple: n, + grosNombre: n, + }) + ), rt.identiqueNombres.bind(rt), (a, b) => Assert.deepEqual(a, b) ); diff --git a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/test_sprites.js b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/test_sprites.js index 3feb2fd34d..e1f29cd9c8 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/test_sprites.js +++ b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/test_sprites.js @@ -9,20 +9,26 @@ add_task(async function () { Assert.ok(Sprites.Sprite); const sempty = await Sprites.Sprite.init(null); - Assert.deepEqual(await sempty.getPosition(), new Sprites.Point(0, 0)); + Assert.deepEqual( + await sempty.getPosition(), + new Sprites.Point({ x: 0, y: 0 }) + ); - const s = await Sprites.Sprite.init(new Sprites.Point(0, 1)); - Assert.deepEqual(await s.getPosition(), new Sprites.Point(0, 1)); + const s = await Sprites.Sprite.init(new Sprites.Point({ x: 0, y: 1 })); + Assert.deepEqual(await s.getPosition(), new Sprites.Point({ x: 0, y: 1 })); - s.moveTo(new Sprites.Point(1, 2)); - Assert.deepEqual(await s.getPosition(), new Sprites.Point(1, 2)); + s.moveTo(new Sprites.Point({ x: 1, y: 2 })); + Assert.deepEqual(await s.getPosition(), new Sprites.Point({ x: 1, y: 2 })); - s.moveBy(new Sprites.Vector(-4, 2)); - Assert.deepEqual(await s.getPosition(), new Sprites.Point(-3, 4)); + s.moveBy(new Sprites.Vector({ dx: -4, dy: 2 })); + Assert.deepEqual(await s.getPosition(), new Sprites.Point({ x: -3, y: 4 })); const srel = await Sprites.Sprite.newRelativeTo( - new Sprites.Point(0, 1), - new Sprites.Vector(1, 1.5) + new Sprites.Point({ x: 0, y: 1 }), + new Sprites.Vector({ dx: 1, dy: 1.5 }) + ); + Assert.deepEqual( + await srel.getPosition(), + new Sprites.Point({ x: 1, y: 2.5 }) ); - Assert.deepEqual(await srel.getPosition(), new Sprites.Point(1, 2.5)); }); diff --git a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/test_todolist.js b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/test_todolist.js index dac26d2be1..2cbd43304c 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/test_todolist.js +++ b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/test_todolist.js @@ -6,7 +6,9 @@ const { TodoList, TodoEntry, getDefaultList, setDefaultList } = add_task(async function () { const todo = await TodoList.init(); - const entry = new TodoEntry("Write bindings for strings in records"); + const entry = new TodoEntry({ + text: "Write bindings for strings in records", + }); await todo.addItem("Write JS bindings"); Assert.equal(await todo.getLast(), "Write JS bindings"); @@ -30,9 +32,9 @@ add_task(async function () { "Test Ünicode hàndling without an entry can't believe I didn't test this at first 🤣" ); - const entry2 = new TodoEntry( - "Test Ünicode hàndling in an entry can't believe I didn't test this at first 🤣" - ); + const entry2 = new TodoEntry({ + text: "Test Ünicode hàndling in an entry can't believe I didn't test this at first 🤣", + }); await todo.addEntry(entry2); Assert.equal( (await todo.getLastEntry()).text, diff --git a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/test_type_checking.js b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/test_type_checking.js index bfeb07c82b..7a5e04cea1 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/test_type_checking.js +++ b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/test_type_checking.js @@ -42,6 +42,12 @@ add_task(async function testObjectPointers() { /Bad pointer type/, "getEntries() with wrong pointer type" ); + + await Assert.rejects( + TodoList.setDefaultList(1), // expecting an object + /Object is not a 'TodoList' instance/, + "attempting to lift the wrong object type" + ); }); add_task(async function testEnumTypeCheck() { @@ -68,18 +74,6 @@ add_task(async function testRecordTypeCheck() { UniFFITypeError, "gradient with non-Line object should throw" ); - - await Assert.rejects( - Geometry.gradient({ - start: { - coordX: 0.0, - coordY: 0.0, - }, - // missing the end field - }), - /ln.end/, // Ensure exception message includes the argument name - "gradient with Line object with missing end field should throw" - ); }); add_task(async function testOptionTypeCheck() { diff --git a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/xpcshell.toml b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/xpcshell.toml index 76814ff199..801e95382f 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/xpcshell.toml +++ b/toolkit/components/uniffi-bindgen-gecko-js/fixtures/tests/xpcshell/xpcshell.toml @@ -2,18 +2,33 @@ ["test_arithmetic.js"] +# I think this can be re-enabled when we implement https://bugzilla.mozilla.org/show_bug.cgi?id=1888668 +lineno = "3" + ["test_callbacks.js"] +disabled = "Temporarily disabled until we can re-enable callback support" +lineno = "8" ["test_custom_types.js"] +lineno = "12" ["test_external_types.js"] +lineno = "15" ["test_geometry.js"] +lineno = "18" + +["test_refcounts.js"] +lineno = "21" ["test_rondpoint.js"] +lineno = "24" ["test_sprites.js"] +lineno = "27" ["test_todolist.js"] +lineno = "30" ["test_type_checking.js"] +lineno = "33" diff --git a/toolkit/components/uniffi-bindgen-gecko-js/src/render/cpp.rs b/toolkit/components/uniffi-bindgen-gecko-js/src/render/cpp.rs index 685c3c2bf3..7b63e8f3af 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/src/render/cpp.rs +++ b/toolkit/components/uniffi-bindgen-gecko-js/src/render/cpp.rs @@ -136,31 +136,25 @@ pub impl FfiType { // Type for the Rust scaffolding code fn rust_type(&self) -> String { match self { - FfiType::UInt8 => "uint8_t", - FfiType::Int8 => "int8_t", - FfiType::UInt16 => "uint16_t", - FfiType::Int16 => "int16_t", - FfiType::UInt32 => "uint32_t", - FfiType::Int32 => "int32_t", - FfiType::UInt64 => "uint64_t", - FfiType::Int64 => "int64_t", - FfiType::Float32 => "float", - FfiType::Float64 => "double", - FfiType::RustBuffer(_) => "RustBuffer", - FfiType::RustArcPtr(_) => "void *", - FfiType::ForeignCallback => "ForeignCallback", + FfiType::UInt8 => "uint8_t".to_owned(), + FfiType::Int8 => "int8_t".to_owned(), + FfiType::UInt16 => "uint16_t".to_owned(), + FfiType::Int16 => "int16_t".to_owned(), + FfiType::UInt32 => "uint32_t".to_owned(), + FfiType::Int32 => "int32_t".to_owned(), + FfiType::UInt64 => "uint64_t".to_owned(), + FfiType::Int64 => "int64_t".to_owned(), + FfiType::Float32 => "float".to_owned(), + FfiType::Float64 => "double".to_owned(), + FfiType::RustBuffer(_) => "RustBuffer".to_owned(), + FfiType::RustArcPtr(_) => "void *".to_owned(), FfiType::ForeignBytes => unimplemented!("ForeignBytes not supported"), - FfiType::ForeignExecutorHandle => unimplemented!("ForeignExecutorHandle not supported"), - FfiType::ForeignExecutorCallback => { - unimplemented!("ForeignExecutorCallback not supported") - } - FfiType::RustFutureHandle - | FfiType::RustFutureContinuationCallback - | FfiType::RustFutureContinuationData => { - unimplemented!("Rust async functions not supported") - } + FfiType::Handle => "uint64_t".to_owned(), + FfiType::RustCallStatus => "RustCallStatus".to_owned(), + FfiType::Callback(name) | FfiType::Struct(name) => name.to_owned(), + FfiType::VoidPointer => "void *".to_owned(), + FfiType::Reference(_) => unimplemented!("References not supported"), } - .to_owned() } } diff --git a/toolkit/components/uniffi-bindgen-gecko-js/src/render/js.rs b/toolkit/components/uniffi-bindgen-gecko-js/src/render/js.rs index efd7b42456..cd9af529a7 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/src/render/js.rs +++ b/toolkit/components/uniffi-bindgen-gecko-js/src/render/js.rs @@ -92,7 +92,8 @@ pub impl Literal { Literal::Enum(name, typ) => render_enum_literal(typ, name), Literal::EmptyMap => "{}".to_string(), Literal::EmptySequence => "[]".to_string(), - Literal::Null => "null".to_string(), + Literal::Some { inner } => inner.render(), + Literal::None => "null".to_string(), } } } @@ -258,7 +259,6 @@ pub impl Type { | Type::CallbackInterface { name, .. } => format!("Type{name}"), Type::Timestamp => "Timestamp".into(), Type::Duration => "Duration".into(), - Type::ForeignExecutor => "ForeignExecutor".into(), Type::Optional { inner_type } => format!("Optional{}", inner_type.canonical_name()), Type::Sequence { inner_type } => format!("Sequence{}", inner_type.canonical_name()), Type::Map { diff --git a/toolkit/components/uniffi-bindgen-gecko-js/src/templates/UniFFIScaffolding.cpp b/toolkit/components/uniffi-bindgen-gecko-js/src/templates/UniFFIScaffolding.cpp index 5c4ed8c2f5..83aeae8086 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/src/templates/UniFFIScaffolding.cpp +++ b/toolkit/components/uniffi-bindgen-gecko-js/src/templates/UniFFIScaffolding.cpp @@ -37,7 +37,8 @@ extern "C" { {%- let pointer_type = ci.pointer_type(object) %} const static mozilla::uniffi::UniFFIPointerType {{ pointer_type }} { "{{ "{}::{}"|format(ci.namespace(), object.name()) }}"_ns, - {{ object.ffi_object_free().rust_name() }} + {{ object.ffi_object_clone().rust_name() }}, + {{ object.ffi_object_free().rust_name() }}, }; {%- endfor %} {%- endfor %} diff --git a/toolkit/components/uniffi-bindgen-gecko-js/src/templates/js/Enum.sys.mjs b/toolkit/components/uniffi-bindgen-gecko-js/src/templates/js/Enum.sys.mjs index f7716ac6d8..cbcc256eb9 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/src/templates/js/Enum.sys.mjs +++ b/toolkit/components/uniffi-bindgen-gecko-js/src/templates/js/Enum.sys.mjs @@ -16,7 +16,7 @@ export class {{ ffi_converter }} extends FfiConverterArrayBuffer { return {{ enum_.nm() }}.{{ variant.name().to_shouty_snake_case() }} {%- endfor %} default: - return new Error("Unknown {{ enum_.nm() }} variant"); + throw new UniFFITypeError("Unknown {{ enum_.nm() }} variant"); } } @@ -27,7 +27,7 @@ export class {{ ffi_converter }} extends FfiConverterArrayBuffer { return; } {%- endfor %} - return new Error("Unknown {{ enum_.nm() }} variant"); + throw new UniFFITypeError("Unknown {{ enum_.nm() }} variant"); } static computeSize(value) { @@ -72,7 +72,7 @@ export class {{ ffi_converter }} extends FfiConverterArrayBuffer { ); {%- endfor %} default: - return new Error("Unknown {{ enum_.nm() }} variant"); + throw new UniFFITypeError("Unknown {{ enum_.nm() }} variant"); } } @@ -86,7 +86,7 @@ export class {{ ffi_converter }} extends FfiConverterArrayBuffer { return; } {%- endfor %} - return new Error("Unknown {{ enum_.nm() }} variant"); + throw new UniFFITypeError("Unknown {{ enum_.nm() }} variant"); } static computeSize(value) { @@ -100,7 +100,7 @@ export class {{ ffi_converter }} extends FfiConverterArrayBuffer { return totalSize; } {%- endfor %} - return new Error("Unknown {{ enum_.nm() }} variant"); + throw new UniFFITypeError("Unknown {{ enum_.nm() }} variant"); } static checkType(value) { diff --git a/toolkit/components/uniffi-bindgen-gecko-js/src/templates/js/Error.sys.mjs b/toolkit/components/uniffi-bindgen-gecko-js/src/templates/js/Error.sys.mjs index b140d908da..c472f1a27d 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/src/templates/js/Error.sys.mjs +++ b/toolkit/components/uniffi-bindgen-gecko-js/src/templates/js/Error.sys.mjs @@ -46,7 +46,7 @@ export class {{ ffi_converter }} extends FfiConverterArrayBuffer { {%- endif %} {%- endfor %} default: - throw new Error("Unknown {{ error.nm() }} variant"); + throw new UniFFITypeError("Unknown {{ error.nm() }} variant"); } } static computeSize(value) { @@ -60,7 +60,7 @@ export class {{ ffi_converter }} extends FfiConverterArrayBuffer { return totalSize; } {%- endfor %} - throw new Error("Unknown {{ error.nm() }} variant"); + throw new UniFFITypeError("Unknown {{ error.nm() }} variant"); } static write(dataStream, value) { {%- for variant in error.variants() %} @@ -72,7 +72,7 @@ export class {{ ffi_converter }} extends FfiConverterArrayBuffer { return; } {%- endfor %} - throw new Error("Unknown {{ error.nm() }} variant"); + throw new UniFFITypeError("Unknown {{ error.nm() }} variant"); } static errorClass = {{ error.nm() }}; diff --git a/toolkit/components/uniffi-bindgen-gecko-js/src/templates/js/Object.sys.mjs b/toolkit/components/uniffi-bindgen-gecko-js/src/templates/js/Object.sys.mjs index e03291089e..204bf752dd 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/src/templates/js/Object.sys.mjs +++ b/toolkit/components/uniffi-bindgen-gecko-js/src/templates/js/Object.sys.mjs @@ -51,7 +51,11 @@ export class {{ ffi_converter }} extends FfiConverter { } static lower(value) { - return value[uniffiObjectPtr]; + const ptr = value[uniffiObjectPtr]; + if (!(ptr instanceof UniFFIPointer)) { + throw new UniFFITypeError("Object is not a '{{ object.nm() }}' instance"); + } + return ptr; } static read(dataStream) { diff --git a/toolkit/components/uniffi-bindgen-gecko-js/src/templates/js/Record.sys.mjs b/toolkit/components/uniffi-bindgen-gecko-js/src/templates/js/Record.sys.mjs index 2f54160b9e..638be5bee8 100644 --- a/toolkit/components/uniffi-bindgen-gecko-js/src/templates/js/Record.sys.mjs +++ b/toolkit/components/uniffi-bindgen-gecko-js/src/templates/js/Record.sys.mjs @@ -51,7 +51,7 @@ export class {{ ffi_converter }} extends FfiConverterArrayBuffer { static checkType(value) { super.checkType(value); if (!(value instanceof {{ record.nm() }})) { - throw new TypeError(`Expected '{{ record.nm() }}', found '${typeof value}'`); + throw new UniFFITypeError(`Expected '{{ record.nm() }}', found '${typeof value}'`); } {%- for field in record.fields() %} try { diff --git a/toolkit/components/uniffi-fixture-external-types/Cargo.toml b/toolkit/components/uniffi-fixture-external-types/Cargo.toml index f9ac840c3f..46ee01662d 100644 --- a/toolkit/components/uniffi-fixture-external-types/Cargo.toml +++ b/toolkit/components/uniffi-fixture-external-types/Cargo.toml @@ -7,7 +7,7 @@ license = "MPL-2.0" publish = false [dependencies] -uniffi-example-geometry = { git = "https://github.com/mozilla/uniffi-rs.git", rev = "afb29ebdc1d9edf15021b1c5332fc9f285bbe13b" } +uniffi-example-geometry = { git = "https://github.com/mozilla/uniffi-rs.git", rev = "d52c5460ae42ecad1e73a5b394ac96d48f4769de" } uniffi = { workspace = true } thiserror = "1.0" diff --git a/toolkit/components/uniffi-fixture-refcounts/Cargo.toml b/toolkit/components/uniffi-fixture-refcounts/Cargo.toml new file mode 100644 index 0000000000..877e502711 --- /dev/null +++ b/toolkit/components/uniffi-fixture-refcounts/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "uniffi-fixture-refcounts" +edition = "2021" +version = "0.21.0" +license = "MPL-2.0" +publish = false + +[dependencies] +uniffi = { workspace = true } + +[build-dependencies] +uniffi = { workspace = true, features = ["build"] } diff --git a/toolkit/components/search/.eslintrc.js b/toolkit/components/uniffi-fixture-refcounts/build.rs index 9aafb4a214..9ea03e12de 100644 --- a/toolkit/components/search/.eslintrc.js +++ b/toolkit/components/uniffi-fixture-refcounts/build.rs @@ -2,8 +2,6 @@ * 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"; - -module.exports = { - extends: ["plugin:mozilla/require-jsdoc"], -}; +fn main() { + uniffi::generate_scaffolding("./src/refcounts.udl").unwrap(); +} diff --git a/toolkit/components/uniffi-fixture-refcounts/src/lib.rs b/toolkit/components/uniffi-fixture-refcounts/src/lib.rs new file mode 100644 index 0000000000..e453b22a4a --- /dev/null +++ b/toolkit/components/uniffi-fixture-refcounts/src/lib.rs @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/// This crate exists to test managing the Rust Arc strong counts as JS objects are +/// created/destroyed. See `test_refcounts.js` for how it's used. + +use std::sync::{Arc, Mutex}; + +pub struct SingletonObject; + +impl SingletonObject { + pub fn method(&self) { } +} + +static SINGLETON: Mutex<Option<Arc<SingletonObject>>> = Mutex::new(None); + +pub fn get_singleton() -> Arc<SingletonObject> { + Arc::clone(SINGLETON.lock().unwrap().get_or_insert_with(|| Arc::new(SingletonObject))) +} + +pub fn get_js_refcount() -> i32 { + // Subtract 2: one for the reference in the Mutex and one for the temporary reference that + // we're calling Arc::strong_count on. + (Arc::strong_count(&get_singleton()) as i32) - 2 +} + +include!(concat!(env!("OUT_DIR"), "/refcounts.uniffi.rs")); diff --git a/toolkit/components/uniffi-fixture-refcounts/src/refcounts.udl b/toolkit/components/uniffi-fixture-refcounts/src/refcounts.udl new file mode 100644 index 0000000000..25ec83cfcc --- /dev/null +++ b/toolkit/components/uniffi-fixture-refcounts/src/refcounts.udl @@ -0,0 +1,8 @@ +namespace refcounts { + SingletonObject get_singleton(); + i32 get_js_refcount(); +}; + +interface SingletonObject { + void method(); +}; diff --git a/toolkit/components/uniffi-js/OwnedRustBuffer.cpp b/toolkit/components/uniffi-js/OwnedRustBuffer.cpp index 4e334a966e..f14033a213 100644 --- a/toolkit/components/uniffi-js/OwnedRustBuffer.cpp +++ b/toolkit/components/uniffi-js/OwnedRustBuffer.cpp @@ -27,7 +27,7 @@ Result<OwnedRustBuffer, nsCString> OwnedRustBuffer::FromArrayBuffer( RustCallStatus status{}; RustBuffer buf = uniffi_rustbuffer_alloc( - static_cast<int32_t>(aData.Length()), &status); + static_cast<uint64_t>(aData.Length()), &status); buf.len = aData.Length(); if (status.code != 0) { if (status.error_buf.data) { @@ -84,7 +84,7 @@ RustBuffer OwnedRustBuffer::IntoRustBuffer() { JSObject* OwnedRustBuffer::IntoArrayBuffer(JSContext* cx) { JS::Rooted<JSObject*> obj(cx); { - int32_t len = mBuf.len; + auto len = mBuf.len; void* data = mBuf.data; auto userData = MakeUnique<OwnedRustBuffer>(std::move(*this)); UniquePtr<void, JS::BufferContentsDeleter> dataPtr{ diff --git a/toolkit/components/uniffi-js/ScaffoldingConverter.h b/toolkit/components/uniffi-js/ScaffoldingConverter.h index ae5629e2e4..d11fd0c314 100644 --- a/toolkit/components/uniffi-js/ScaffoldingConverter.h +++ b/toolkit/components/uniffi-js/ScaffoldingConverter.h @@ -173,7 +173,7 @@ class ScaffoldingObjectConverter { if (!value.IsSamePtrType(PointerType)) { return Err("Bad pointer type"_ns); } - return value.GetPtr(); + return value.ClonePtr(); } static void* IntoRust(void* aValue) { return aValue; } diff --git a/toolkit/components/uniffi-js/UniFFICallbacks.h b/toolkit/components/uniffi-js/UniFFICallbacks.h index b18ba14f9e..2104e8dff2 100644 --- a/toolkit/components/uniffi-js/UniFFICallbacks.h +++ b/toolkit/components/uniffi-js/UniFFICallbacks.h @@ -67,7 +67,7 @@ void DeregisterCallbackHandler(uint64_t aInterfaceId, ErrorResult& aError); * good use case for this is logging. */ MOZ_CAN_RUN_SCRIPT -void QueueCallback(size_t aInterfaceId, uint64_t aHandle, uint32_t aMethod, +void QueueCallback(uint64_t aInterfaceId, uint64_t aHandle, uint32_t aMethod, const uint8_t* aArgsData, int32_t aArgsLen); } // namespace mozilla::uniffi diff --git a/toolkit/components/uniffi-js/UniFFIFixtureScaffolding.cpp b/toolkit/components/uniffi-js/UniFFIFixtureScaffolding.cpp index c0695f227c..d8c4d0d8a0 100644 --- a/toolkit/components/uniffi-js/UniFFIFixtureScaffolding.cpp +++ b/toolkit/components/uniffi-js/UniFFIFixtureScaffolding.cpp @@ -31,11 +31,14 @@ extern "C" { RustBuffer uniffi_uniffi_custom_types_fn_func_get_custom_types_demo(RustBuffer, RustCallStatus*); double uniffi_uniffi_fixture_external_types_fn_func_gradient(RustBuffer, RustCallStatus*); RustBuffer uniffi_uniffi_fixture_external_types_fn_func_intersection(RustBuffer, RustBuffer, RustCallStatus*); - void uniffi_uniffi_fixture_callbacks_fn_init_callback_logger(ForeignCallback, RustCallStatus*); - void uniffi_uniffi_fixture_callbacks_fn_func_log_even_numbers(uint64_t, RustBuffer, RustCallStatus*); - void uniffi_uniffi_fixture_callbacks_fn_func_log_even_numbers_main_thread(uint64_t, RustBuffer, RustCallStatus*); + void * uniffi_uniffi_fixture_refcounts_fn_clone_singletonobject(void *, RustCallStatus*); + void uniffi_uniffi_fixture_refcounts_fn_free_singletonobject(void *, RustCallStatus*); + void uniffi_uniffi_fixture_refcounts_fn_method_singletonobject_method(void *, RustCallStatus*); + int32_t uniffi_uniffi_fixture_refcounts_fn_func_get_js_refcount(RustCallStatus*); + void * uniffi_uniffi_fixture_refcounts_fn_func_get_singleton(RustCallStatus*); double uniffi_uniffi_geometry_fn_func_gradient(RustBuffer, RustCallStatus*); RustBuffer uniffi_uniffi_geometry_fn_func_intersection(RustBuffer, RustBuffer, RustCallStatus*); + void * uniffi_uniffi_rondpoint_fn_clone_optionneur(void *, RustCallStatus*); void uniffi_uniffi_rondpoint_fn_free_optionneur(void *, RustCallStatus*); void * uniffi_uniffi_rondpoint_fn_constructor_optionneur_new(RustCallStatus*); int8_t uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_boolean(void *, int8_t, RustCallStatus*); @@ -63,6 +66,7 @@ extern "C" { uint8_t uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u8_dec(void *, uint8_t, RustCallStatus*); uint8_t uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u8_hex(void *, uint8_t, RustCallStatus*); RustBuffer uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_zero(void *, RustBuffer, RustCallStatus*); + void * uniffi_uniffi_rondpoint_fn_clone_retourneur(void *, RustCallStatus*); void uniffi_uniffi_rondpoint_fn_free_retourneur(void *, RustCallStatus*); void * uniffi_uniffi_rondpoint_fn_constructor_retourneur_new(RustCallStatus*); int8_t uniffi_uniffi_rondpoint_fn_method_retourneur_identique_boolean(void *, int8_t, RustCallStatus*); @@ -80,6 +84,7 @@ extern "C" { uint32_t uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u32(void *, uint32_t, RustCallStatus*); uint64_t uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u64(void *, uint64_t, RustCallStatus*); uint8_t uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u8(void *, uint8_t, RustCallStatus*); + void * uniffi_uniffi_rondpoint_fn_clone_stringifier(void *, RustCallStatus*); void uniffi_uniffi_rondpoint_fn_free_stringifier(void *, RustCallStatus*); void * uniffi_uniffi_rondpoint_fn_constructor_stringifier_new(RustCallStatus*); RustBuffer uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_boolean(void *, int8_t, RustCallStatus*); @@ -99,6 +104,7 @@ extern "C" { RustBuffer uniffi_uniffi_rondpoint_fn_func_copie_enumeration(RustBuffer, RustCallStatus*); RustBuffer uniffi_uniffi_rondpoint_fn_func_copie_enumerations(RustBuffer, RustCallStatus*); int8_t uniffi_uniffi_rondpoint_fn_func_switcheroo(int8_t, RustCallStatus*); + void * uniffi_uniffi_sprites_fn_clone_sprite(void *, RustCallStatus*); void uniffi_uniffi_sprites_fn_free_sprite(void *, RustCallStatus*); void * uniffi_uniffi_sprites_fn_constructor_sprite_new(RustBuffer, RustCallStatus*); void * uniffi_uniffi_sprites_fn_constructor_sprite_new_relative_to(RustBuffer, RustBuffer, RustCallStatus*); @@ -106,6 +112,7 @@ extern "C" { void uniffi_uniffi_sprites_fn_method_sprite_move_by(void *, RustBuffer, RustCallStatus*); void uniffi_uniffi_sprites_fn_method_sprite_move_to(void *, RustBuffer, RustCallStatus*); RustBuffer uniffi_uniffi_sprites_fn_func_translate(RustBuffer, RustBuffer, RustCallStatus*); + void * uniffi_uniffi_todolist_fn_clone_todolist(void *, RustCallStatus*); void uniffi_uniffi_todolist_fn_free_todolist(void *, RustCallStatus*); void * uniffi_uniffi_todolist_fn_constructor_todolist_new(RustCallStatus*); void uniffi_uniffi_todolist_fn_method_todolist_add_entries(void *, RustBuffer, RustCallStatus*); @@ -125,51 +132,42 @@ extern "C" { } // Define pointer types +const static mozilla::uniffi::UniFFIPointerType kRefcountsSingletonObjectPointerType { + "refcounts::SingletonObject"_ns, + uniffi_uniffi_fixture_refcounts_fn_clone_singletonobject, + uniffi_uniffi_fixture_refcounts_fn_free_singletonobject, +}; const static mozilla::uniffi::UniFFIPointerType kRondpointOptionneurPointerType { "rondpoint::Optionneur"_ns, - uniffi_uniffi_rondpoint_fn_free_optionneur + uniffi_uniffi_rondpoint_fn_clone_optionneur, + uniffi_uniffi_rondpoint_fn_free_optionneur, }; const static mozilla::uniffi::UniFFIPointerType kRondpointRetourneurPointerType { "rondpoint::Retourneur"_ns, - uniffi_uniffi_rondpoint_fn_free_retourneur + uniffi_uniffi_rondpoint_fn_clone_retourneur, + uniffi_uniffi_rondpoint_fn_free_retourneur, }; const static mozilla::uniffi::UniFFIPointerType kRondpointStringifierPointerType { "rondpoint::Stringifier"_ns, - uniffi_uniffi_rondpoint_fn_free_stringifier + uniffi_uniffi_rondpoint_fn_clone_stringifier, + uniffi_uniffi_rondpoint_fn_free_stringifier, }; const static mozilla::uniffi::UniFFIPointerType kSpritesSpritePointerType { "sprites::Sprite"_ns, - uniffi_uniffi_sprites_fn_free_sprite + uniffi_uniffi_sprites_fn_clone_sprite, + uniffi_uniffi_sprites_fn_free_sprite, }; const static mozilla::uniffi::UniFFIPointerType kTodolistTodoListPointerType { "todolist::TodoList"_ns, - uniffi_uniffi_todolist_fn_free_todolist + uniffi_uniffi_todolist_fn_clone_todolist, + uniffi_uniffi_todolist_fn_free_todolist, }; // Define the data we need per-callback interface -MOZ_CAN_RUN_SCRIPT -extern "C" int UniFFIFixturesCallbackHandlerLogger(uint64_t aHandle, uint32_t aMethod, const uint8_t* aArgsData, int32_t aArgsLen, RustBuffer* aOutBuffer) { - // Currently, we only support "fire-and-forget" async callbacks. These are - // callbacks that run asynchronously without returning anything. The main - // use case for callbacks is logging, which fits very well with this model. - // - // So, here we simple queue the callback and return immediately. - mozilla::uniffi::QueueCallback(0, aHandle, aMethod, aArgsData, aArgsLen); - return CALLBACK_INTERFACE_SUCCESS; -} -static StaticRefPtr<dom::UniFFICallbackHandler> JS_CALLBACK_HANDLER_LOGGER; // Define a lookup function for our callback interface info Maybe<CallbackInterfaceInfo> UniFFIFixturesGetCallbackInterfaceInfo(uint64_t aInterfaceId) { switch(aInterfaceId) { - case 0: { // fixture_callbacks:Logger - return Some(CallbackInterfaceInfo { - "Logger", - &JS_CALLBACK_HANDLER_LOGGER, - UniFFIFixturesCallbackHandlerLogger, - uniffi_uniffi_fixture_callbacks_fn_init_callback_logger, - }); - } default: return Nothing(); @@ -178,371 +176,399 @@ Maybe<CallbackInterfaceInfo> UniFFIFixturesGetCallbackInterfaceInfo(uint64_t aIn Maybe<already_AddRefed<Promise>> UniFFIFixturesCallAsync(const GlobalObject& aGlobal, uint64_t aId, const Sequence<ScaffoldingType>& aArgs, ErrorResult& aError) { switch (aId) { - case 35: { // arithmetic:uniffi_arithmetical_fn_func_add + case 47: { // arithmetic:uniffi_arithmetical_fn_func_add using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint64_t>, ScaffoldingConverter<uint64_t>, ScaffoldingConverter<uint64_t>>; return Some(CallHandler::CallAsync(uniffi_arithmetical_fn_func_add, aGlobal, aArgs, "uniffi_arithmetical_fn_func_add: "_ns, aError)); } - case 36: { // arithmetic:uniffi_arithmetical_fn_func_div + case 48: { // arithmetic:uniffi_arithmetical_fn_func_div using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint64_t>, ScaffoldingConverter<uint64_t>, ScaffoldingConverter<uint64_t>>; return Some(CallHandler::CallAsync(uniffi_arithmetical_fn_func_div, aGlobal, aArgs, "uniffi_arithmetical_fn_func_div: "_ns, aError)); } - case 37: { // arithmetic:uniffi_arithmetical_fn_func_equal + case 49: { // arithmetic:uniffi_arithmetical_fn_func_equal using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int8_t>, ScaffoldingConverter<uint64_t>, ScaffoldingConverter<uint64_t>>; return Some(CallHandler::CallAsync(uniffi_arithmetical_fn_func_equal, aGlobal, aArgs, "uniffi_arithmetical_fn_func_equal: "_ns, aError)); } - case 38: { // arithmetic:uniffi_arithmetical_fn_func_sub + case 50: { // arithmetic:uniffi_arithmetical_fn_func_sub using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint64_t>, ScaffoldingConverter<uint64_t>, ScaffoldingConverter<uint64_t>>; return Some(CallHandler::CallAsync(uniffi_arithmetical_fn_func_sub, aGlobal, aArgs, "uniffi_arithmetical_fn_func_sub: "_ns, aError)); } - case 39: { // custom_types:uniffi_uniffi_custom_types_fn_func_get_custom_types_demo + case 51: { // custom_types:uniffi_uniffi_custom_types_fn_func_get_custom_types_demo using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_custom_types_fn_func_get_custom_types_demo, aGlobal, aArgs, "uniffi_uniffi_custom_types_fn_func_get_custom_types_demo: "_ns, aError)); } - case 40: { // external_types:uniffi_uniffi_fixture_external_types_fn_func_gradient + case 52: { // external_types:uniffi_uniffi_fixture_external_types_fn_func_gradient using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<double>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_fixture_external_types_fn_func_gradient, aGlobal, aArgs, "uniffi_uniffi_fixture_external_types_fn_func_gradient: "_ns, aError)); } - case 41: { // external_types:uniffi_uniffi_fixture_external_types_fn_func_intersection + case 53: { // external_types:uniffi_uniffi_fixture_external_types_fn_func_intersection using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_fixture_external_types_fn_func_intersection, aGlobal, aArgs, "uniffi_uniffi_fixture_external_types_fn_func_intersection: "_ns, aError)); } - case 42: { // fixture_callbacks:uniffi_uniffi_fixture_callbacks_fn_func_log_even_numbers - using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingConverter<uint64_t>, ScaffoldingConverter<RustBuffer>>; - return Some(CallHandler::CallAsync(uniffi_uniffi_fixture_callbacks_fn_func_log_even_numbers, aGlobal, aArgs, "uniffi_uniffi_fixture_callbacks_fn_func_log_even_numbers: "_ns, aError)); + case 54: { // refcounts:uniffi_uniffi_fixture_refcounts_fn_clone_singletonobject + using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kRefcountsSingletonObjectPointerType>, ScaffoldingObjectConverter<&kRefcountsSingletonObjectPointerType>>; + return Some(CallHandler::CallAsync(uniffi_uniffi_fixture_refcounts_fn_clone_singletonobject, aGlobal, aArgs, "uniffi_uniffi_fixture_refcounts_fn_clone_singletonobject: "_ns, aError)); + } + case 55: { // refcounts:uniffi_uniffi_fixture_refcounts_fn_method_singletonobject_method + using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kRefcountsSingletonObjectPointerType>>; + return Some(CallHandler::CallAsync(uniffi_uniffi_fixture_refcounts_fn_method_singletonobject_method, aGlobal, aArgs, "uniffi_uniffi_fixture_refcounts_fn_method_singletonobject_method: "_ns, aError)); + } + case 56: { // refcounts:uniffi_uniffi_fixture_refcounts_fn_func_get_js_refcount + using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int32_t>>; + return Some(CallHandler::CallAsync(uniffi_uniffi_fixture_refcounts_fn_func_get_js_refcount, aGlobal, aArgs, "uniffi_uniffi_fixture_refcounts_fn_func_get_js_refcount: "_ns, aError)); } - case 43: { // fixture_callbacks:uniffi_uniffi_fixture_callbacks_fn_func_log_even_numbers_main_thread - using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingConverter<uint64_t>, ScaffoldingConverter<RustBuffer>>; - return Some(CallHandler::CallAsync(uniffi_uniffi_fixture_callbacks_fn_func_log_even_numbers_main_thread, aGlobal, aArgs, "uniffi_uniffi_fixture_callbacks_fn_func_log_even_numbers_main_thread: "_ns, aError)); + case 57: { // refcounts:uniffi_uniffi_fixture_refcounts_fn_func_get_singleton + using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kRefcountsSingletonObjectPointerType>>; + return Some(CallHandler::CallAsync(uniffi_uniffi_fixture_refcounts_fn_func_get_singleton, aGlobal, aArgs, "uniffi_uniffi_fixture_refcounts_fn_func_get_singleton: "_ns, aError)); } - case 44: { // geometry:uniffi_uniffi_geometry_fn_func_gradient + case 58: { // geometry:uniffi_uniffi_geometry_fn_func_gradient using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<double>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_geometry_fn_func_gradient, aGlobal, aArgs, "uniffi_uniffi_geometry_fn_func_gradient: "_ns, aError)); } - case 45: { // geometry:uniffi_uniffi_geometry_fn_func_intersection + case 59: { // geometry:uniffi_uniffi_geometry_fn_func_intersection using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_geometry_fn_func_intersection, aGlobal, aArgs, "uniffi_uniffi_geometry_fn_func_intersection: "_ns, aError)); } - case 46: { // rondpoint:uniffi_uniffi_rondpoint_fn_constructor_optionneur_new + case 60: { // rondpoint:uniffi_uniffi_rondpoint_fn_clone_optionneur + using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>>; + return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_clone_optionneur, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_clone_optionneur: "_ns, aError)); + } + case 61: { // rondpoint:uniffi_uniffi_rondpoint_fn_constructor_optionneur_new using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_constructor_optionneur_new, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_constructor_optionneur_new: "_ns, aError)); } - case 47: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_boolean + case 62: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_boolean using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int8_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<int8_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_boolean, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_boolean: "_ns, aError)); } - case 48: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_enum + case 63: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_enum using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_enum, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_enum: "_ns, aError)); } - case 49: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_f32 + case 64: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_f32 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<float>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<float>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_f32, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_f32: "_ns, aError)); } - case 50: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_f64 + case 65: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_f64 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<double>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<double>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_f64, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_f64: "_ns, aError)); } - case 51: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i16_dec + case 66: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i16_dec using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int16_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<int16_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i16_dec, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i16_dec: "_ns, aError)); } - case 52: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i16_hex + case 67: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i16_hex using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int16_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<int16_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i16_hex, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i16_hex: "_ns, aError)); } - case 53: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i32_dec + case 68: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i32_dec using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int32_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<int32_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i32_dec, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i32_dec: "_ns, aError)); } - case 54: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i32_hex + case 69: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i32_hex using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int32_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<int32_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i32_hex, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i32_hex: "_ns, aError)); } - case 55: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i64_dec + case 70: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i64_dec using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int64_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<int64_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i64_dec, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i64_dec: "_ns, aError)); } - case 56: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i64_hex + case 71: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i64_hex using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int64_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<int64_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i64_hex, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i64_hex: "_ns, aError)); } - case 57: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i8_dec + case 72: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i8_dec using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int8_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<int8_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i8_dec, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i8_dec: "_ns, aError)); } - case 58: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i8_hex + case 73: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i8_hex using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int8_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<int8_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i8_hex, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i8_hex: "_ns, aError)); } - case 59: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_null + case 74: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_null using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_null, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_null: "_ns, aError)); } - case 60: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_sequence + case 75: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_sequence using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_sequence, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_sequence: "_ns, aError)); } - case 61: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_string + case 76: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_string using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_string, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_string: "_ns, aError)); } - case 62: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u16_dec + case 77: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u16_dec using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint16_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<uint16_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u16_dec, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u16_dec: "_ns, aError)); } - case 63: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u16_hex + case 78: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u16_hex using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint16_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<uint16_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u16_hex, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u16_hex: "_ns, aError)); } - case 64: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_dec + case 79: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_dec using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint32_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<uint32_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_dec, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_dec: "_ns, aError)); } - case 65: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_hex + case 80: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_hex using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint32_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<uint32_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_hex, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_hex: "_ns, aError)); } - case 66: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_oct + case 81: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_oct using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint32_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<uint32_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_oct, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_oct: "_ns, aError)); } - case 67: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u64_dec + case 82: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u64_dec using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint64_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<uint64_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u64_dec, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u64_dec: "_ns, aError)); } - case 68: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u64_hex + case 83: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u64_hex using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint64_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<uint64_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u64_hex, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u64_hex: "_ns, aError)); } - case 69: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u8_dec + case 84: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u8_dec using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint8_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<uint8_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u8_dec, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u8_dec: "_ns, aError)); } - case 70: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u8_hex + case 85: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u8_hex using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint8_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<uint8_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u8_hex, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u8_hex: "_ns, aError)); } - case 71: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_zero + case 86: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_zero using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_zero, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_zero: "_ns, aError)); } - case 72: { // rondpoint:uniffi_uniffi_rondpoint_fn_constructor_retourneur_new + case 87: { // rondpoint:uniffi_uniffi_rondpoint_fn_clone_retourneur + using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>>; + return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_clone_retourneur, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_clone_retourneur: "_ns, aError)); + } + case 88: { // rondpoint:uniffi_uniffi_rondpoint_fn_constructor_retourneur_new using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_constructor_retourneur_new, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_constructor_retourneur_new: "_ns, aError)); } - case 73: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_boolean + case 89: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_boolean using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int8_t>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<int8_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_boolean, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_boolean: "_ns, aError)); } - case 74: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_double + case 90: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_double using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<double>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<double>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_double, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_double: "_ns, aError)); } - case 75: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_float + case 91: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_float using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<float>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<float>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_float, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_float: "_ns, aError)); } - case 76: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i16 + case 92: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i16 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int16_t>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<int16_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i16, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i16: "_ns, aError)); } - case 77: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i32 + case 93: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i32 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int32_t>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<int32_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i32, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i32: "_ns, aError)); } - case 78: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i64 + case 94: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i64 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int64_t>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<int64_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i64, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i64: "_ns, aError)); } - case 79: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i8 + case 95: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i8 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int8_t>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<int8_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i8, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i8: "_ns, aError)); } - case 80: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_nombres + case 96: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_nombres using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_nombres, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_nombres: "_ns, aError)); } - case 81: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_nombres_signes + case 97: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_nombres_signes using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_nombres_signes, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_nombres_signes: "_ns, aError)); } - case 82: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_optionneur_dictionnaire + case 98: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_optionneur_dictionnaire using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_optionneur_dictionnaire, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_optionneur_dictionnaire: "_ns, aError)); } - case 83: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_string + case 99: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_string using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_string, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_string: "_ns, aError)); } - case 84: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u16 + case 100: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u16 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint16_t>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<uint16_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u16, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u16: "_ns, aError)); } - case 85: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u32 + case 101: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u32 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint32_t>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<uint32_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u32, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u32: "_ns, aError)); } - case 86: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u64 + case 102: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u64 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint64_t>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<uint64_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u64, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u64: "_ns, aError)); } - case 87: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u8 + case 103: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u8 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint8_t>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<uint8_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u8, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u8: "_ns, aError)); } - case 88: { // rondpoint:uniffi_uniffi_rondpoint_fn_constructor_stringifier_new + case 104: { // rondpoint:uniffi_uniffi_rondpoint_fn_clone_stringifier + using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kRondpointStringifierPointerType>, ScaffoldingObjectConverter<&kRondpointStringifierPointerType>>; + return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_clone_stringifier, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_clone_stringifier: "_ns, aError)); + } + case 105: { // rondpoint:uniffi_uniffi_rondpoint_fn_constructor_stringifier_new using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kRondpointStringifierPointerType>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_constructor_stringifier_new, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_constructor_stringifier_new: "_ns, aError)); } - case 89: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_boolean + case 106: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_boolean using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointStringifierPointerType>, ScaffoldingConverter<int8_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_boolean, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_boolean: "_ns, aError)); } - case 90: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_double + case 107: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_double using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointStringifierPointerType>, ScaffoldingConverter<double>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_double, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_double: "_ns, aError)); } - case 91: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_float + case 108: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_float using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointStringifierPointerType>, ScaffoldingConverter<float>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_float, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_float: "_ns, aError)); } - case 92: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i16 + case 109: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i16 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointStringifierPointerType>, ScaffoldingConverter<int16_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i16, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i16: "_ns, aError)); } - case 93: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i32 + case 110: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i32 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointStringifierPointerType>, ScaffoldingConverter<int32_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i32, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i32: "_ns, aError)); } - case 94: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i64 + case 111: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i64 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointStringifierPointerType>, ScaffoldingConverter<int64_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i64, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i64: "_ns, aError)); } - case 95: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i8 + case 112: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i8 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointStringifierPointerType>, ScaffoldingConverter<int8_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i8, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i8: "_ns, aError)); } - case 96: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u16 + case 113: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u16 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointStringifierPointerType>, ScaffoldingConverter<uint16_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u16, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u16: "_ns, aError)); } - case 97: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u32 + case 114: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u32 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointStringifierPointerType>, ScaffoldingConverter<uint32_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u32, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u32: "_ns, aError)); } - case 98: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u64 + case 115: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u64 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointStringifierPointerType>, ScaffoldingConverter<uint64_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u64, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u64: "_ns, aError)); } - case 99: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u8 + case 116: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u8 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointStringifierPointerType>, ScaffoldingConverter<uint8_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u8, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u8: "_ns, aError)); } - case 100: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_well_known_string + case 117: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_well_known_string using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointStringifierPointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_method_stringifier_well_known_string, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_method_stringifier_well_known_string: "_ns, aError)); } - case 101: { // rondpoint:uniffi_uniffi_rondpoint_fn_func_copie_carte + case 118: { // rondpoint:uniffi_uniffi_rondpoint_fn_func_copie_carte using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_func_copie_carte, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_func_copie_carte: "_ns, aError)); } - case 102: { // rondpoint:uniffi_uniffi_rondpoint_fn_func_copie_dictionnaire + case 119: { // rondpoint:uniffi_uniffi_rondpoint_fn_func_copie_dictionnaire using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_func_copie_dictionnaire, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_func_copie_dictionnaire: "_ns, aError)); } - case 103: { // rondpoint:uniffi_uniffi_rondpoint_fn_func_copie_enumeration + case 120: { // rondpoint:uniffi_uniffi_rondpoint_fn_func_copie_enumeration using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_func_copie_enumeration, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_func_copie_enumeration: "_ns, aError)); } - case 104: { // rondpoint:uniffi_uniffi_rondpoint_fn_func_copie_enumerations + case 121: { // rondpoint:uniffi_uniffi_rondpoint_fn_func_copie_enumerations using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_func_copie_enumerations, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_func_copie_enumerations: "_ns, aError)); } - case 105: { // rondpoint:uniffi_uniffi_rondpoint_fn_func_switcheroo + case 122: { // rondpoint:uniffi_uniffi_rondpoint_fn_func_switcheroo using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int8_t>, ScaffoldingConverter<int8_t>>; return Some(CallHandler::CallAsync(uniffi_uniffi_rondpoint_fn_func_switcheroo, aGlobal, aArgs, "uniffi_uniffi_rondpoint_fn_func_switcheroo: "_ns, aError)); } - case 106: { // sprites:uniffi_uniffi_sprites_fn_constructor_sprite_new + case 123: { // sprites:uniffi_uniffi_sprites_fn_clone_sprite + using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kSpritesSpritePointerType>, ScaffoldingObjectConverter<&kSpritesSpritePointerType>>; + return Some(CallHandler::CallAsync(uniffi_uniffi_sprites_fn_clone_sprite, aGlobal, aArgs, "uniffi_uniffi_sprites_fn_clone_sprite: "_ns, aError)); + } + case 124: { // sprites:uniffi_uniffi_sprites_fn_constructor_sprite_new using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kSpritesSpritePointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_sprites_fn_constructor_sprite_new, aGlobal, aArgs, "uniffi_uniffi_sprites_fn_constructor_sprite_new: "_ns, aError)); } - case 107: { // sprites:uniffi_uniffi_sprites_fn_constructor_sprite_new_relative_to + case 125: { // sprites:uniffi_uniffi_sprites_fn_constructor_sprite_new_relative_to using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kSpritesSpritePointerType>, ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_sprites_fn_constructor_sprite_new_relative_to, aGlobal, aArgs, "uniffi_uniffi_sprites_fn_constructor_sprite_new_relative_to: "_ns, aError)); } - case 108: { // sprites:uniffi_uniffi_sprites_fn_method_sprite_get_position + case 126: { // sprites:uniffi_uniffi_sprites_fn_method_sprite_get_position using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kSpritesSpritePointerType>>; return Some(CallHandler::CallAsync(uniffi_uniffi_sprites_fn_method_sprite_get_position, aGlobal, aArgs, "uniffi_uniffi_sprites_fn_method_sprite_get_position: "_ns, aError)); } - case 109: { // sprites:uniffi_uniffi_sprites_fn_method_sprite_move_by + case 127: { // sprites:uniffi_uniffi_sprites_fn_method_sprite_move_by using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kSpritesSpritePointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_sprites_fn_method_sprite_move_by, aGlobal, aArgs, "uniffi_uniffi_sprites_fn_method_sprite_move_by: "_ns, aError)); } - case 110: { // sprites:uniffi_uniffi_sprites_fn_method_sprite_move_to + case 128: { // sprites:uniffi_uniffi_sprites_fn_method_sprite_move_to using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kSpritesSpritePointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_sprites_fn_method_sprite_move_to, aGlobal, aArgs, "uniffi_uniffi_sprites_fn_method_sprite_move_to: "_ns, aError)); } - case 111: { // sprites:uniffi_uniffi_sprites_fn_func_translate + case 129: { // sprites:uniffi_uniffi_sprites_fn_func_translate using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_sprites_fn_func_translate, aGlobal, aArgs, "uniffi_uniffi_sprites_fn_func_translate: "_ns, aError)); } - case 112: { // todolist:uniffi_uniffi_todolist_fn_constructor_todolist_new + case 130: { // todolist:uniffi_uniffi_todolist_fn_clone_todolist + using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kTodolistTodoListPointerType>, ScaffoldingObjectConverter<&kTodolistTodoListPointerType>>; + return Some(CallHandler::CallAsync(uniffi_uniffi_todolist_fn_clone_todolist, aGlobal, aArgs, "uniffi_uniffi_todolist_fn_clone_todolist: "_ns, aError)); + } + case 131: { // todolist:uniffi_uniffi_todolist_fn_constructor_todolist_new using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kTodolistTodoListPointerType>>; return Some(CallHandler::CallAsync(uniffi_uniffi_todolist_fn_constructor_todolist_new, aGlobal, aArgs, "uniffi_uniffi_todolist_fn_constructor_todolist_new: "_ns, aError)); } - case 113: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_add_entries + case 132: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_add_entries using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTodolistTodoListPointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_todolist_fn_method_todolist_add_entries, aGlobal, aArgs, "uniffi_uniffi_todolist_fn_method_todolist_add_entries: "_ns, aError)); } - case 114: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_add_entry + case 133: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_add_entry using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTodolistTodoListPointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_todolist_fn_method_todolist_add_entry, aGlobal, aArgs, "uniffi_uniffi_todolist_fn_method_todolist_add_entry: "_ns, aError)); } - case 115: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_add_item + case 134: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_add_item using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTodolistTodoListPointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_todolist_fn_method_todolist_add_item, aGlobal, aArgs, "uniffi_uniffi_todolist_fn_method_todolist_add_item: "_ns, aError)); } - case 116: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_add_items + case 135: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_add_items using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTodolistTodoListPointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_todolist_fn_method_todolist_add_items, aGlobal, aArgs, "uniffi_uniffi_todolist_fn_method_todolist_add_items: "_ns, aError)); } - case 117: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_clear_item + case 136: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_clear_item using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTodolistTodoListPointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_todolist_fn_method_todolist_clear_item, aGlobal, aArgs, "uniffi_uniffi_todolist_fn_method_todolist_clear_item: "_ns, aError)); } - case 118: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_entries + case 137: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_entries using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kTodolistTodoListPointerType>>; return Some(CallHandler::CallAsync(uniffi_uniffi_todolist_fn_method_todolist_get_entries, aGlobal, aArgs, "uniffi_uniffi_todolist_fn_method_todolist_get_entries: "_ns, aError)); } - case 119: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_first + case 138: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_first using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kTodolistTodoListPointerType>>; return Some(CallHandler::CallAsync(uniffi_uniffi_todolist_fn_method_todolist_get_first, aGlobal, aArgs, "uniffi_uniffi_todolist_fn_method_todolist_get_first: "_ns, aError)); } - case 120: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_items + case 139: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_items using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kTodolistTodoListPointerType>>; return Some(CallHandler::CallAsync(uniffi_uniffi_todolist_fn_method_todolist_get_items, aGlobal, aArgs, "uniffi_uniffi_todolist_fn_method_todolist_get_items: "_ns, aError)); } - case 121: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_last + case 140: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_last using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kTodolistTodoListPointerType>>; return Some(CallHandler::CallAsync(uniffi_uniffi_todolist_fn_method_todolist_get_last, aGlobal, aArgs, "uniffi_uniffi_todolist_fn_method_todolist_get_last: "_ns, aError)); } - case 122: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_last_entry + case 141: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_last_entry using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kTodolistTodoListPointerType>>; return Some(CallHandler::CallAsync(uniffi_uniffi_todolist_fn_method_todolist_get_last_entry, aGlobal, aArgs, "uniffi_uniffi_todolist_fn_method_todolist_get_last_entry: "_ns, aError)); } - case 123: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_make_default + case 142: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_make_default using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTodolistTodoListPointerType>>; return Some(CallHandler::CallAsync(uniffi_uniffi_todolist_fn_method_todolist_make_default, aGlobal, aArgs, "uniffi_uniffi_todolist_fn_method_todolist_make_default: "_ns, aError)); } - case 124: { // todolist:uniffi_uniffi_todolist_fn_func_create_entry_with + case 143: { // todolist:uniffi_uniffi_todolist_fn_func_create_entry_with using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_todolist_fn_func_create_entry_with, aGlobal, aArgs, "uniffi_uniffi_todolist_fn_func_create_entry_with: "_ns, aError)); } - case 125: { // todolist:uniffi_uniffi_todolist_fn_func_get_default_list + case 144: { // todolist:uniffi_uniffi_todolist_fn_func_get_default_list using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_uniffi_todolist_fn_func_get_default_list, aGlobal, aArgs, "uniffi_uniffi_todolist_fn_func_get_default_list: "_ns, aError)); } - case 126: { // todolist:uniffi_uniffi_todolist_fn_func_set_default_list + case 145: { // todolist:uniffi_uniffi_todolist_fn_func_set_default_list using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTodolistTodoListPointerType>>; return Some(CallHandler::CallAsync(uniffi_uniffi_todolist_fn_func_set_default_list, aGlobal, aArgs, "uniffi_uniffi_todolist_fn_func_set_default_list: "_ns, aError)); } @@ -552,462 +578,497 @@ Maybe<already_AddRefed<Promise>> UniFFIFixturesCallAsync(const GlobalObject& aGl bool UniFFIFixturesCallSync(const GlobalObject& aGlobal, uint64_t aId, const Sequence<ScaffoldingType>& aArgs, RootedDictionary<UniFFIScaffoldingCallResult>& aReturnValue, ErrorResult& aError) { switch (aId) { - case 35: { // arithmetic:uniffi_arithmetical_fn_func_add + case 47: { // arithmetic:uniffi_arithmetical_fn_func_add using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint64_t>, ScaffoldingConverter<uint64_t>, ScaffoldingConverter<uint64_t>>; CallHandler::CallSync(uniffi_arithmetical_fn_func_add, aGlobal, aArgs, aReturnValue, "uniffi_arithmetical_fn_func_add: "_ns, aError); return true; } - case 36: { // arithmetic:uniffi_arithmetical_fn_func_div + case 48: { // arithmetic:uniffi_arithmetical_fn_func_div using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint64_t>, ScaffoldingConverter<uint64_t>, ScaffoldingConverter<uint64_t>>; CallHandler::CallSync(uniffi_arithmetical_fn_func_div, aGlobal, aArgs, aReturnValue, "uniffi_arithmetical_fn_func_div: "_ns, aError); return true; } - case 37: { // arithmetic:uniffi_arithmetical_fn_func_equal + case 49: { // arithmetic:uniffi_arithmetical_fn_func_equal using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int8_t>, ScaffoldingConverter<uint64_t>, ScaffoldingConverter<uint64_t>>; CallHandler::CallSync(uniffi_arithmetical_fn_func_equal, aGlobal, aArgs, aReturnValue, "uniffi_arithmetical_fn_func_equal: "_ns, aError); return true; } - case 38: { // arithmetic:uniffi_arithmetical_fn_func_sub + case 50: { // arithmetic:uniffi_arithmetical_fn_func_sub using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint64_t>, ScaffoldingConverter<uint64_t>, ScaffoldingConverter<uint64_t>>; CallHandler::CallSync(uniffi_arithmetical_fn_func_sub, aGlobal, aArgs, aReturnValue, "uniffi_arithmetical_fn_func_sub: "_ns, aError); return true; } - case 39: { // custom_types:uniffi_uniffi_custom_types_fn_func_get_custom_types_demo + case 51: { // custom_types:uniffi_uniffi_custom_types_fn_func_get_custom_types_demo using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_custom_types_fn_func_get_custom_types_demo, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_custom_types_fn_func_get_custom_types_demo: "_ns, aError); return true; } - case 40: { // external_types:uniffi_uniffi_fixture_external_types_fn_func_gradient + case 52: { // external_types:uniffi_uniffi_fixture_external_types_fn_func_gradient using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<double>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_fixture_external_types_fn_func_gradient, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_fixture_external_types_fn_func_gradient: "_ns, aError); return true; } - case 41: { // external_types:uniffi_uniffi_fixture_external_types_fn_func_intersection + case 53: { // external_types:uniffi_uniffi_fixture_external_types_fn_func_intersection using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_fixture_external_types_fn_func_intersection, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_fixture_external_types_fn_func_intersection: "_ns, aError); return true; } - case 42: { // fixture_callbacks:uniffi_uniffi_fixture_callbacks_fn_func_log_even_numbers - using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingConverter<uint64_t>, ScaffoldingConverter<RustBuffer>>; - CallHandler::CallSync(uniffi_uniffi_fixture_callbacks_fn_func_log_even_numbers, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_fixture_callbacks_fn_func_log_even_numbers: "_ns, aError); + case 54: { // refcounts:uniffi_uniffi_fixture_refcounts_fn_clone_singletonobject + using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kRefcountsSingletonObjectPointerType>, ScaffoldingObjectConverter<&kRefcountsSingletonObjectPointerType>>; + CallHandler::CallSync(uniffi_uniffi_fixture_refcounts_fn_clone_singletonobject, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_fixture_refcounts_fn_clone_singletonobject: "_ns, aError); + return true; + } + case 55: { // refcounts:uniffi_uniffi_fixture_refcounts_fn_method_singletonobject_method + using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kRefcountsSingletonObjectPointerType>>; + CallHandler::CallSync(uniffi_uniffi_fixture_refcounts_fn_method_singletonobject_method, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_fixture_refcounts_fn_method_singletonobject_method: "_ns, aError); return true; } - case 43: { // fixture_callbacks:uniffi_uniffi_fixture_callbacks_fn_func_log_even_numbers_main_thread - using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingConverter<uint64_t>, ScaffoldingConverter<RustBuffer>>; - CallHandler::CallSync(uniffi_uniffi_fixture_callbacks_fn_func_log_even_numbers_main_thread, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_fixture_callbacks_fn_func_log_even_numbers_main_thread: "_ns, aError); + case 56: { // refcounts:uniffi_uniffi_fixture_refcounts_fn_func_get_js_refcount + using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int32_t>>; + CallHandler::CallSync(uniffi_uniffi_fixture_refcounts_fn_func_get_js_refcount, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_fixture_refcounts_fn_func_get_js_refcount: "_ns, aError); return true; } - case 44: { // geometry:uniffi_uniffi_geometry_fn_func_gradient + case 57: { // refcounts:uniffi_uniffi_fixture_refcounts_fn_func_get_singleton + using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kRefcountsSingletonObjectPointerType>>; + CallHandler::CallSync(uniffi_uniffi_fixture_refcounts_fn_func_get_singleton, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_fixture_refcounts_fn_func_get_singleton: "_ns, aError); + return true; + } + case 58: { // geometry:uniffi_uniffi_geometry_fn_func_gradient using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<double>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_geometry_fn_func_gradient, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_geometry_fn_func_gradient: "_ns, aError); return true; } - case 45: { // geometry:uniffi_uniffi_geometry_fn_func_intersection + case 59: { // geometry:uniffi_uniffi_geometry_fn_func_intersection using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_geometry_fn_func_intersection, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_geometry_fn_func_intersection: "_ns, aError); return true; } - case 46: { // rondpoint:uniffi_uniffi_rondpoint_fn_constructor_optionneur_new + case 60: { // rondpoint:uniffi_uniffi_rondpoint_fn_clone_optionneur + using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>>; + CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_clone_optionneur, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_clone_optionneur: "_ns, aError); + return true; + } + case 61: { // rondpoint:uniffi_uniffi_rondpoint_fn_constructor_optionneur_new using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_constructor_optionneur_new, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_constructor_optionneur_new: "_ns, aError); return true; } - case 47: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_boolean + case 62: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_boolean using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int8_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<int8_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_boolean, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_boolean: "_ns, aError); return true; } - case 48: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_enum + case 63: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_enum using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_enum, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_enum: "_ns, aError); return true; } - case 49: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_f32 + case 64: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_f32 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<float>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<float>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_f32, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_f32: "_ns, aError); return true; } - case 50: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_f64 + case 65: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_f64 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<double>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<double>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_f64, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_f64: "_ns, aError); return true; } - case 51: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i16_dec + case 66: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i16_dec using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int16_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<int16_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i16_dec, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i16_dec: "_ns, aError); return true; } - case 52: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i16_hex + case 67: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i16_hex using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int16_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<int16_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i16_hex, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i16_hex: "_ns, aError); return true; } - case 53: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i32_dec + case 68: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i32_dec using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int32_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<int32_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i32_dec, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i32_dec: "_ns, aError); return true; } - case 54: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i32_hex + case 69: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i32_hex using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int32_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<int32_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i32_hex, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i32_hex: "_ns, aError); return true; } - case 55: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i64_dec + case 70: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i64_dec using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int64_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<int64_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i64_dec, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i64_dec: "_ns, aError); return true; } - case 56: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i64_hex + case 71: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i64_hex using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int64_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<int64_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i64_hex, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i64_hex: "_ns, aError); return true; } - case 57: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i8_dec + case 72: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i8_dec using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int8_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<int8_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i8_dec, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i8_dec: "_ns, aError); return true; } - case 58: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i8_hex + case 73: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i8_hex using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int8_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<int8_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i8_hex, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_i8_hex: "_ns, aError); return true; } - case 59: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_null + case 74: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_null using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_null, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_null: "_ns, aError); return true; } - case 60: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_sequence + case 75: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_sequence using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_sequence, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_sequence: "_ns, aError); return true; } - case 61: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_string + case 76: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_string using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_string, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_string: "_ns, aError); return true; } - case 62: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u16_dec + case 77: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u16_dec using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint16_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<uint16_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u16_dec, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u16_dec: "_ns, aError); return true; } - case 63: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u16_hex + case 78: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u16_hex using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint16_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<uint16_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u16_hex, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u16_hex: "_ns, aError); return true; } - case 64: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_dec + case 79: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_dec using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint32_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<uint32_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_dec, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_dec: "_ns, aError); return true; } - case 65: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_hex + case 80: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_hex using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint32_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<uint32_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_hex, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_hex: "_ns, aError); return true; } - case 66: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_oct + case 81: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_oct using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint32_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<uint32_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_oct, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u32_oct: "_ns, aError); return true; } - case 67: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u64_dec + case 82: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u64_dec using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint64_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<uint64_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u64_dec, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u64_dec: "_ns, aError); return true; } - case 68: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u64_hex + case 83: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u64_hex using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint64_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<uint64_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u64_hex, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u64_hex: "_ns, aError); return true; } - case 69: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u8_dec + case 84: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u8_dec using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint8_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<uint8_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u8_dec, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u8_dec: "_ns, aError); return true; } - case 70: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u8_hex + case 85: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u8_hex using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint8_t>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<uint8_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u8_hex, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_u8_hex: "_ns, aError); return true; } - case 71: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_zero + case 86: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_zero using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointOptionneurPointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_zero, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_optionneur_sinon_zero: "_ns, aError); return true; } - case 72: { // rondpoint:uniffi_uniffi_rondpoint_fn_constructor_retourneur_new + case 87: { // rondpoint:uniffi_uniffi_rondpoint_fn_clone_retourneur + using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>>; + CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_clone_retourneur, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_clone_retourneur: "_ns, aError); + return true; + } + case 88: { // rondpoint:uniffi_uniffi_rondpoint_fn_constructor_retourneur_new using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_constructor_retourneur_new, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_constructor_retourneur_new: "_ns, aError); return true; } - case 73: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_boolean + case 89: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_boolean using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int8_t>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<int8_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_boolean, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_boolean: "_ns, aError); return true; } - case 74: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_double + case 90: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_double using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<double>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<double>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_double, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_double: "_ns, aError); return true; } - case 75: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_float + case 91: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_float using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<float>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<float>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_float, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_float: "_ns, aError); return true; } - case 76: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i16 + case 92: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i16 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int16_t>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<int16_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i16, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i16: "_ns, aError); return true; } - case 77: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i32 + case 93: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i32 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int32_t>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<int32_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i32, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i32: "_ns, aError); return true; } - case 78: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i64 + case 94: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i64 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int64_t>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<int64_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i64, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i64: "_ns, aError); return true; } - case 79: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i8 + case 95: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i8 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int8_t>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<int8_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i8, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_i8: "_ns, aError); return true; } - case 80: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_nombres + case 96: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_nombres using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_nombres, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_nombres: "_ns, aError); return true; } - case 81: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_nombres_signes + case 97: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_nombres_signes using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_nombres_signes, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_nombres_signes: "_ns, aError); return true; } - case 82: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_optionneur_dictionnaire + case 98: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_optionneur_dictionnaire using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_optionneur_dictionnaire, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_optionneur_dictionnaire: "_ns, aError); return true; } - case 83: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_string + case 99: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_string using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_string, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_string: "_ns, aError); return true; } - case 84: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u16 + case 100: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u16 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint16_t>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<uint16_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u16, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u16: "_ns, aError); return true; } - case 85: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u32 + case 101: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u32 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint32_t>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<uint32_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u32, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u32: "_ns, aError); return true; } - case 86: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u64 + case 102: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u64 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint64_t>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<uint64_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u64, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u64: "_ns, aError); return true; } - case 87: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u8 + case 103: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u8 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<uint8_t>, ScaffoldingObjectConverter<&kRondpointRetourneurPointerType>, ScaffoldingConverter<uint8_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u8, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_retourneur_identique_u8: "_ns, aError); return true; } - case 88: { // rondpoint:uniffi_uniffi_rondpoint_fn_constructor_stringifier_new + case 104: { // rondpoint:uniffi_uniffi_rondpoint_fn_clone_stringifier + using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kRondpointStringifierPointerType>, ScaffoldingObjectConverter<&kRondpointStringifierPointerType>>; + CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_clone_stringifier, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_clone_stringifier: "_ns, aError); + return true; + } + case 105: { // rondpoint:uniffi_uniffi_rondpoint_fn_constructor_stringifier_new using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kRondpointStringifierPointerType>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_constructor_stringifier_new, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_constructor_stringifier_new: "_ns, aError); return true; } - case 89: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_boolean + case 106: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_boolean using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointStringifierPointerType>, ScaffoldingConverter<int8_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_boolean, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_boolean: "_ns, aError); return true; } - case 90: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_double + case 107: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_double using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointStringifierPointerType>, ScaffoldingConverter<double>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_double, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_double: "_ns, aError); return true; } - case 91: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_float + case 108: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_float using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointStringifierPointerType>, ScaffoldingConverter<float>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_float, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_float: "_ns, aError); return true; } - case 92: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i16 + case 109: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i16 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointStringifierPointerType>, ScaffoldingConverter<int16_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i16, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i16: "_ns, aError); return true; } - case 93: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i32 + case 110: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i32 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointStringifierPointerType>, ScaffoldingConverter<int32_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i32, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i32: "_ns, aError); return true; } - case 94: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i64 + case 111: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i64 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointStringifierPointerType>, ScaffoldingConverter<int64_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i64, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i64: "_ns, aError); return true; } - case 95: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i8 + case 112: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i8 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointStringifierPointerType>, ScaffoldingConverter<int8_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i8, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_i8: "_ns, aError); return true; } - case 96: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u16 + case 113: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u16 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointStringifierPointerType>, ScaffoldingConverter<uint16_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u16, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u16: "_ns, aError); return true; } - case 97: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u32 + case 114: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u32 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointStringifierPointerType>, ScaffoldingConverter<uint32_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u32, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u32: "_ns, aError); return true; } - case 98: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u64 + case 115: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u64 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointStringifierPointerType>, ScaffoldingConverter<uint64_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u64, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u64: "_ns, aError); return true; } - case 99: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u8 + case 116: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u8 using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointStringifierPointerType>, ScaffoldingConverter<uint8_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u8, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_stringifier_to_string_u8: "_ns, aError); return true; } - case 100: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_well_known_string + case 117: { // rondpoint:uniffi_uniffi_rondpoint_fn_method_stringifier_well_known_string using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRondpointStringifierPointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_method_stringifier_well_known_string, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_method_stringifier_well_known_string: "_ns, aError); return true; } - case 101: { // rondpoint:uniffi_uniffi_rondpoint_fn_func_copie_carte + case 118: { // rondpoint:uniffi_uniffi_rondpoint_fn_func_copie_carte using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_func_copie_carte, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_func_copie_carte: "_ns, aError); return true; } - case 102: { // rondpoint:uniffi_uniffi_rondpoint_fn_func_copie_dictionnaire + case 119: { // rondpoint:uniffi_uniffi_rondpoint_fn_func_copie_dictionnaire using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_func_copie_dictionnaire, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_func_copie_dictionnaire: "_ns, aError); return true; } - case 103: { // rondpoint:uniffi_uniffi_rondpoint_fn_func_copie_enumeration + case 120: { // rondpoint:uniffi_uniffi_rondpoint_fn_func_copie_enumeration using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_func_copie_enumeration, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_func_copie_enumeration: "_ns, aError); return true; } - case 104: { // rondpoint:uniffi_uniffi_rondpoint_fn_func_copie_enumerations + case 121: { // rondpoint:uniffi_uniffi_rondpoint_fn_func_copie_enumerations using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_func_copie_enumerations, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_func_copie_enumerations: "_ns, aError); return true; } - case 105: { // rondpoint:uniffi_uniffi_rondpoint_fn_func_switcheroo + case 122: { // rondpoint:uniffi_uniffi_rondpoint_fn_func_switcheroo using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int8_t>, ScaffoldingConverter<int8_t>>; CallHandler::CallSync(uniffi_uniffi_rondpoint_fn_func_switcheroo, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_rondpoint_fn_func_switcheroo: "_ns, aError); return true; } - case 106: { // sprites:uniffi_uniffi_sprites_fn_constructor_sprite_new + case 123: { // sprites:uniffi_uniffi_sprites_fn_clone_sprite + using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kSpritesSpritePointerType>, ScaffoldingObjectConverter<&kSpritesSpritePointerType>>; + CallHandler::CallSync(uniffi_uniffi_sprites_fn_clone_sprite, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_sprites_fn_clone_sprite: "_ns, aError); + return true; + } + case 124: { // sprites:uniffi_uniffi_sprites_fn_constructor_sprite_new using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kSpritesSpritePointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_sprites_fn_constructor_sprite_new, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_sprites_fn_constructor_sprite_new: "_ns, aError); return true; } - case 107: { // sprites:uniffi_uniffi_sprites_fn_constructor_sprite_new_relative_to + case 125: { // sprites:uniffi_uniffi_sprites_fn_constructor_sprite_new_relative_to using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kSpritesSpritePointerType>, ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_sprites_fn_constructor_sprite_new_relative_to, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_sprites_fn_constructor_sprite_new_relative_to: "_ns, aError); return true; } - case 108: { // sprites:uniffi_uniffi_sprites_fn_method_sprite_get_position + case 126: { // sprites:uniffi_uniffi_sprites_fn_method_sprite_get_position using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kSpritesSpritePointerType>>; CallHandler::CallSync(uniffi_uniffi_sprites_fn_method_sprite_get_position, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_sprites_fn_method_sprite_get_position: "_ns, aError); return true; } - case 109: { // sprites:uniffi_uniffi_sprites_fn_method_sprite_move_by + case 127: { // sprites:uniffi_uniffi_sprites_fn_method_sprite_move_by using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kSpritesSpritePointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_sprites_fn_method_sprite_move_by, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_sprites_fn_method_sprite_move_by: "_ns, aError); return true; } - case 110: { // sprites:uniffi_uniffi_sprites_fn_method_sprite_move_to + case 128: { // sprites:uniffi_uniffi_sprites_fn_method_sprite_move_to using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kSpritesSpritePointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_sprites_fn_method_sprite_move_to, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_sprites_fn_method_sprite_move_to: "_ns, aError); return true; } - case 111: { // sprites:uniffi_uniffi_sprites_fn_func_translate + case 129: { // sprites:uniffi_uniffi_sprites_fn_func_translate using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_sprites_fn_func_translate, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_sprites_fn_func_translate: "_ns, aError); return true; } - case 112: { // todolist:uniffi_uniffi_todolist_fn_constructor_todolist_new + case 130: { // todolist:uniffi_uniffi_todolist_fn_clone_todolist + using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kTodolistTodoListPointerType>, ScaffoldingObjectConverter<&kTodolistTodoListPointerType>>; + CallHandler::CallSync(uniffi_uniffi_todolist_fn_clone_todolist, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_todolist_fn_clone_todolist: "_ns, aError); + return true; + } + case 131: { // todolist:uniffi_uniffi_todolist_fn_constructor_todolist_new using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kTodolistTodoListPointerType>>; CallHandler::CallSync(uniffi_uniffi_todolist_fn_constructor_todolist_new, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_todolist_fn_constructor_todolist_new: "_ns, aError); return true; } - case 113: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_add_entries + case 132: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_add_entries using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTodolistTodoListPointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_todolist_fn_method_todolist_add_entries, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_todolist_fn_method_todolist_add_entries: "_ns, aError); return true; } - case 114: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_add_entry + case 133: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_add_entry using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTodolistTodoListPointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_todolist_fn_method_todolist_add_entry, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_todolist_fn_method_todolist_add_entry: "_ns, aError); return true; } - case 115: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_add_item + case 134: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_add_item using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTodolistTodoListPointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_todolist_fn_method_todolist_add_item, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_todolist_fn_method_todolist_add_item: "_ns, aError); return true; } - case 116: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_add_items + case 135: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_add_items using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTodolistTodoListPointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_todolist_fn_method_todolist_add_items, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_todolist_fn_method_todolist_add_items: "_ns, aError); return true; } - case 117: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_clear_item + case 136: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_clear_item using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTodolistTodoListPointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_todolist_fn_method_todolist_clear_item, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_todolist_fn_method_todolist_clear_item: "_ns, aError); return true; } - case 118: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_entries + case 137: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_entries using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kTodolistTodoListPointerType>>; CallHandler::CallSync(uniffi_uniffi_todolist_fn_method_todolist_get_entries, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_todolist_fn_method_todolist_get_entries: "_ns, aError); return true; } - case 119: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_first + case 138: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_first using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kTodolistTodoListPointerType>>; CallHandler::CallSync(uniffi_uniffi_todolist_fn_method_todolist_get_first, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_todolist_fn_method_todolist_get_first: "_ns, aError); return true; } - case 120: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_items + case 139: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_items using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kTodolistTodoListPointerType>>; CallHandler::CallSync(uniffi_uniffi_todolist_fn_method_todolist_get_items, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_todolist_fn_method_todolist_get_items: "_ns, aError); return true; } - case 121: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_last + case 140: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_last using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kTodolistTodoListPointerType>>; CallHandler::CallSync(uniffi_uniffi_todolist_fn_method_todolist_get_last, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_todolist_fn_method_todolist_get_last: "_ns, aError); return true; } - case 122: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_last_entry + case 141: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_get_last_entry using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kTodolistTodoListPointerType>>; CallHandler::CallSync(uniffi_uniffi_todolist_fn_method_todolist_get_last_entry, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_todolist_fn_method_todolist_get_last_entry: "_ns, aError); return true; } - case 123: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_make_default + case 142: { // todolist:uniffi_uniffi_todolist_fn_method_todolist_make_default using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTodolistTodoListPointerType>>; CallHandler::CallSync(uniffi_uniffi_todolist_fn_method_todolist_make_default, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_todolist_fn_method_todolist_make_default: "_ns, aError); return true; } - case 124: { // todolist:uniffi_uniffi_todolist_fn_func_create_entry_with + case 143: { // todolist:uniffi_uniffi_todolist_fn_func_create_entry_with using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_todolist_fn_func_create_entry_with, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_todolist_fn_func_create_entry_with: "_ns, aError); return true; } - case 125: { // todolist:uniffi_uniffi_todolist_fn_func_get_default_list + case 144: { // todolist:uniffi_uniffi_todolist_fn_func_get_default_list using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_uniffi_todolist_fn_func_get_default_list, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_todolist_fn_func_get_default_list: "_ns, aError); return true; } - case 126: { // todolist:uniffi_uniffi_todolist_fn_func_set_default_list + case 145: { // todolist:uniffi_uniffi_todolist_fn_func_set_default_list using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTodolistTodoListPointerType>>; CallHandler::CallSync(uniffi_uniffi_todolist_fn_func_set_default_list, aGlobal, aArgs, aReturnValue, "uniffi_uniffi_todolist_fn_func_set_default_list: "_ns, aError); return true; @@ -1019,23 +1080,27 @@ bool UniFFIFixturesCallSync(const GlobalObject& aGlobal, uint64_t aId, const Seq Maybe<already_AddRefed<UniFFIPointer>> UniFFIFixturesReadPointer(const GlobalObject& aGlobal, uint64_t aId, const ArrayBuffer& aArrayBuff, long aPosition, ErrorResult& aError) { const UniFFIPointerType* type; switch (aId) { - case 5: { // rondpoint:Optionneur + case 6: { // refcounts:SingletonObject + type = &kRefcountsSingletonObjectPointerType; + break; + } + case 7: { // rondpoint:Optionneur type = &kRondpointOptionneurPointerType; break; } - case 6: { // rondpoint:Retourneur + case 8: { // rondpoint:Retourneur type = &kRondpointRetourneurPointerType; break; } - case 7: { // rondpoint:Stringifier + case 9: { // rondpoint:Stringifier type = &kRondpointStringifierPointerType; break; } - case 8: { // sprites:Sprite + case 10: { // sprites:Sprite type = &kSpritesSpritePointerType; break; } - case 9: { // todolist:TodoList + case 11: { // todolist:TodoList type = &kTodolistTodoListPointerType; break; } @@ -1048,23 +1113,27 @@ Maybe<already_AddRefed<UniFFIPointer>> UniFFIFixturesReadPointer(const GlobalObj bool UniFFIFixturesWritePointer(const GlobalObject& aGlobal, uint64_t aId, const UniFFIPointer& aPtr, const ArrayBuffer& aArrayBuff, long aPosition, ErrorResult& aError) { const UniFFIPointerType* type; switch (aId) { - case 5: { // rondpoint:Optionneur + case 6: { // refcounts:SingletonObject + type = &kRefcountsSingletonObjectPointerType; + break; + } + case 7: { // rondpoint:Optionneur type = &kRondpointOptionneurPointerType; break; } - case 6: { // rondpoint:Retourneur + case 8: { // rondpoint:Retourneur type = &kRondpointRetourneurPointerType; break; } - case 7: { // rondpoint:Stringifier + case 9: { // rondpoint:Stringifier type = &kRondpointStringifierPointerType; break; } - case 8: { // sprites:Sprite + case 10: { // sprites:Sprite type = &kSpritesSpritePointerType; break; } - case 9: { // todolist:TodoList + case 11: { // todolist:TodoList type = &kTodolistTodoListPointerType; break; } diff --git a/toolkit/components/uniffi-js/UniFFIGeneratedScaffolding.cpp b/toolkit/components/uniffi-js/UniFFIGeneratedScaffolding.cpp index c235d92986..20a581d5e2 100644 --- a/toolkit/components/uniffi-js/UniFFIGeneratedScaffolding.cpp +++ b/toolkit/components/uniffi-js/UniFFIGeneratedScaffolding.cpp @@ -24,19 +24,30 @@ using dom::UniFFIScaffoldingCallResult; // Define scaffolding functions from UniFFI extern "C" { + void * uniffi_relevancy_fn_clone_relevancystore(void *, RustCallStatus*); + void uniffi_relevancy_fn_free_relevancystore(void *, RustCallStatus*); + void * uniffi_relevancy_fn_constructor_relevancystore_new(RustBuffer, RustCallStatus*); + RustBuffer uniffi_relevancy_fn_method_relevancystore_calculate_metrics(void *, RustCallStatus*); + void uniffi_relevancy_fn_method_relevancystore_ingest(void *, RustBuffer, RustCallStatus*); + RustBuffer uniffi_relevancy_fn_method_relevancystore_user_interest_vector(void *, RustCallStatus*); + void * uniffi_remote_settings_fn_clone_remotesettings(void *, RustCallStatus*); void uniffi_remote_settings_fn_free_remotesettings(void *, RustCallStatus*); void * uniffi_remote_settings_fn_constructor_remotesettings_new(RustBuffer, RustCallStatus*); void uniffi_remote_settings_fn_method_remotesettings_download_attachment_to_path(void *, RustBuffer, RustBuffer, RustCallStatus*); RustBuffer uniffi_remote_settings_fn_method_remotesettings_get_records(void *, RustCallStatus*); RustBuffer uniffi_remote_settings_fn_method_remotesettings_get_records_since(void *, uint64_t, RustCallStatus*); + void * uniffi_suggest_fn_clone_suggeststore(void *, RustCallStatus*); void uniffi_suggest_fn_free_suggeststore(void *, RustCallStatus*); void * uniffi_suggest_fn_constructor_suggeststore_new(RustBuffer, RustBuffer, RustCallStatus*); void uniffi_suggest_fn_method_suggeststore_clear(void *, RustCallStatus*); + void uniffi_suggest_fn_method_suggeststore_clear_dismissed_suggestions(void *, RustCallStatus*); + void uniffi_suggest_fn_method_suggeststore_dismiss_suggestion(void *, RustBuffer, RustCallStatus*); RustBuffer uniffi_suggest_fn_method_suggeststore_fetch_global_config(void *, RustCallStatus*); RustBuffer uniffi_suggest_fn_method_suggeststore_fetch_provider_config(void *, RustBuffer, RustCallStatus*); void uniffi_suggest_fn_method_suggeststore_ingest(void *, RustBuffer, RustCallStatus*); void uniffi_suggest_fn_method_suggeststore_interrupt(void *, RustCallStatus*); RustBuffer uniffi_suggest_fn_method_suggeststore_query(void *, RustBuffer, RustCallStatus*); + void * uniffi_suggest_fn_clone_suggeststorebuilder(void *, RustCallStatus*); void uniffi_suggest_fn_free_suggeststorebuilder(void *, RustCallStatus*); void * uniffi_suggest_fn_constructor_suggeststorebuilder_new(RustCallStatus*); void * uniffi_suggest_fn_method_suggeststorebuilder_build(void *, RustCallStatus*); @@ -44,6 +55,7 @@ extern "C" { void * uniffi_suggest_fn_method_suggeststorebuilder_data_path(void *, RustBuffer, RustCallStatus*); void * uniffi_suggest_fn_method_suggeststorebuilder_remote_settings_config(void *, RustBuffer, RustCallStatus*); int8_t uniffi_suggest_fn_func_raw_suggestion_url_matches(RustBuffer, RustBuffer, RustCallStatus*); + void * uniffi_tabs_fn_clone_tabsbridgedengine(void *, RustCallStatus*); void uniffi_tabs_fn_free_tabsbridgedengine(void *, RustCallStatus*); RustBuffer uniffi_tabs_fn_method_tabsbridgedengine_apply(void *, RustCallStatus*); RustBuffer uniffi_tabs_fn_method_tabsbridgedengine_ensure_current_sync_id(void *, RustBuffer, RustCallStatus*); @@ -58,6 +70,7 @@ extern "C" { RustBuffer uniffi_tabs_fn_method_tabsbridgedengine_sync_id(void *, RustCallStatus*); void uniffi_tabs_fn_method_tabsbridgedengine_sync_started(void *, RustCallStatus*); void uniffi_tabs_fn_method_tabsbridgedengine_wipe(void *, RustCallStatus*); + void * uniffi_tabs_fn_clone_tabsstore(void *, RustCallStatus*); void uniffi_tabs_fn_free_tabsstore(void *, RustCallStatus*); void * uniffi_tabs_fn_constructor_tabsstore_new(RustBuffer, RustCallStatus*); void * uniffi_tabs_fn_method_tabsstore_bridged_engine(void *, RustCallStatus*); @@ -67,25 +80,35 @@ extern "C" { } // Define pointer types +const static mozilla::uniffi::UniFFIPointerType kRelevancyRelevancyStorePointerType { + "relevancy::RelevancyStore"_ns, + uniffi_relevancy_fn_clone_relevancystore, + uniffi_relevancy_fn_free_relevancystore, +}; const static mozilla::uniffi::UniFFIPointerType kRemoteSettingsRemoteSettingsPointerType { "remote_settings::RemoteSettings"_ns, - uniffi_remote_settings_fn_free_remotesettings + uniffi_remote_settings_fn_clone_remotesettings, + uniffi_remote_settings_fn_free_remotesettings, }; const static mozilla::uniffi::UniFFIPointerType kSuggestSuggestStorePointerType { "suggest::SuggestStore"_ns, - uniffi_suggest_fn_free_suggeststore + uniffi_suggest_fn_clone_suggeststore, + uniffi_suggest_fn_free_suggeststore, }; const static mozilla::uniffi::UniFFIPointerType kSuggestSuggestStoreBuilderPointerType { "suggest::SuggestStoreBuilder"_ns, - uniffi_suggest_fn_free_suggeststorebuilder + uniffi_suggest_fn_clone_suggeststorebuilder, + uniffi_suggest_fn_free_suggeststorebuilder, }; const static mozilla::uniffi::UniFFIPointerType kTabsTabsBridgedEnginePointerType { "tabs::TabsBridgedEngine"_ns, - uniffi_tabs_fn_free_tabsbridgedengine + uniffi_tabs_fn_clone_tabsbridgedengine, + uniffi_tabs_fn_free_tabsbridgedengine, }; const static mozilla::uniffi::UniFFIPointerType kTabsTabsStorePointerType { "tabs::TabsStore"_ns, - uniffi_tabs_fn_free_tabsstore + uniffi_tabs_fn_clone_tabsstore, + uniffi_tabs_fn_free_tabsstore, }; // Define the data we need per-callback interface @@ -101,143 +124,191 @@ Maybe<CallbackInterfaceInfo> UniFFIGetCallbackInterfaceInfo(uint64_t aInterfaceI Maybe<already_AddRefed<Promise>> UniFFICallAsync(const GlobalObject& aGlobal, uint64_t aId, const Sequence<ScaffoldingType>& aArgs, ErrorResult& aError) { switch (aId) { - case 0: { // remote_settings:uniffi_remote_settings_fn_constructor_remotesettings_new + case 0: { // relevancy:uniffi_relevancy_fn_clone_relevancystore + using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kRelevancyRelevancyStorePointerType>, ScaffoldingObjectConverter<&kRelevancyRelevancyStorePointerType>>; + return Some(CallHandler::CallAsync(uniffi_relevancy_fn_clone_relevancystore, aGlobal, aArgs, "uniffi_relevancy_fn_clone_relevancystore: "_ns, aError)); + } + case 1: { // relevancy:uniffi_relevancy_fn_constructor_relevancystore_new + using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kRelevancyRelevancyStorePointerType>, ScaffoldingConverter<RustBuffer>>; + return Some(CallHandler::CallAsync(uniffi_relevancy_fn_constructor_relevancystore_new, aGlobal, aArgs, "uniffi_relevancy_fn_constructor_relevancystore_new: "_ns, aError)); + } + case 2: { // relevancy:uniffi_relevancy_fn_method_relevancystore_calculate_metrics + using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRelevancyRelevancyStorePointerType>>; + return Some(CallHandler::CallAsync(uniffi_relevancy_fn_method_relevancystore_calculate_metrics, aGlobal, aArgs, "uniffi_relevancy_fn_method_relevancystore_calculate_metrics: "_ns, aError)); + } + case 3: { // relevancy:uniffi_relevancy_fn_method_relevancystore_ingest + using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kRelevancyRelevancyStorePointerType>, ScaffoldingConverter<RustBuffer>>; + return Some(CallHandler::CallAsync(uniffi_relevancy_fn_method_relevancystore_ingest, aGlobal, aArgs, "uniffi_relevancy_fn_method_relevancystore_ingest: "_ns, aError)); + } + case 4: { // relevancy:uniffi_relevancy_fn_method_relevancystore_user_interest_vector + using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRelevancyRelevancyStorePointerType>>; + return Some(CallHandler::CallAsync(uniffi_relevancy_fn_method_relevancystore_user_interest_vector, aGlobal, aArgs, "uniffi_relevancy_fn_method_relevancystore_user_interest_vector: "_ns, aError)); + } + case 5: { // remote_settings:uniffi_remote_settings_fn_clone_remotesettings + using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kRemoteSettingsRemoteSettingsPointerType>, ScaffoldingObjectConverter<&kRemoteSettingsRemoteSettingsPointerType>>; + return Some(CallHandler::CallAsync(uniffi_remote_settings_fn_clone_remotesettings, aGlobal, aArgs, "uniffi_remote_settings_fn_clone_remotesettings: "_ns, aError)); + } + case 6: { // remote_settings:uniffi_remote_settings_fn_constructor_remotesettings_new using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kRemoteSettingsRemoteSettingsPointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_remote_settings_fn_constructor_remotesettings_new, aGlobal, aArgs, "uniffi_remote_settings_fn_constructor_remotesettings_new: "_ns, aError)); } - case 1: { // remote_settings:uniffi_remote_settings_fn_method_remotesettings_download_attachment_to_path + case 7: { // remote_settings:uniffi_remote_settings_fn_method_remotesettings_download_attachment_to_path using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kRemoteSettingsRemoteSettingsPointerType>, ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_remote_settings_fn_method_remotesettings_download_attachment_to_path, aGlobal, aArgs, "uniffi_remote_settings_fn_method_remotesettings_download_attachment_to_path: "_ns, aError)); } - case 2: { // remote_settings:uniffi_remote_settings_fn_method_remotesettings_get_records + case 8: { // remote_settings:uniffi_remote_settings_fn_method_remotesettings_get_records using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRemoteSettingsRemoteSettingsPointerType>>; return Some(CallHandler::CallAsync(uniffi_remote_settings_fn_method_remotesettings_get_records, aGlobal, aArgs, "uniffi_remote_settings_fn_method_remotesettings_get_records: "_ns, aError)); } - case 3: { // remote_settings:uniffi_remote_settings_fn_method_remotesettings_get_records_since + case 9: { // remote_settings:uniffi_remote_settings_fn_method_remotesettings_get_records_since using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRemoteSettingsRemoteSettingsPointerType>, ScaffoldingConverter<uint64_t>>; return Some(CallHandler::CallAsync(uniffi_remote_settings_fn_method_remotesettings_get_records_since, aGlobal, aArgs, "uniffi_remote_settings_fn_method_remotesettings_get_records_since: "_ns, aError)); } - case 4: { // suggest:uniffi_suggest_fn_constructor_suggeststore_new + case 10: { // suggest:uniffi_suggest_fn_clone_suggeststore + using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kSuggestSuggestStorePointerType>, ScaffoldingObjectConverter<&kSuggestSuggestStorePointerType>>; + return Some(CallHandler::CallAsync(uniffi_suggest_fn_clone_suggeststore, aGlobal, aArgs, "uniffi_suggest_fn_clone_suggeststore: "_ns, aError)); + } + case 11: { // suggest:uniffi_suggest_fn_constructor_suggeststore_new using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kSuggestSuggestStorePointerType>, ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_suggest_fn_constructor_suggeststore_new, aGlobal, aArgs, "uniffi_suggest_fn_constructor_suggeststore_new: "_ns, aError)); } - case 5: { // suggest:uniffi_suggest_fn_method_suggeststore_clear + case 12: { // suggest:uniffi_suggest_fn_method_suggeststore_clear using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kSuggestSuggestStorePointerType>>; return Some(CallHandler::CallAsync(uniffi_suggest_fn_method_suggeststore_clear, aGlobal, aArgs, "uniffi_suggest_fn_method_suggeststore_clear: "_ns, aError)); } - case 6: { // suggest:uniffi_suggest_fn_method_suggeststore_fetch_global_config + case 13: { // suggest:uniffi_suggest_fn_method_suggeststore_clear_dismissed_suggestions + using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kSuggestSuggestStorePointerType>>; + return Some(CallHandler::CallAsync(uniffi_suggest_fn_method_suggeststore_clear_dismissed_suggestions, aGlobal, aArgs, "uniffi_suggest_fn_method_suggeststore_clear_dismissed_suggestions: "_ns, aError)); + } + case 14: { // suggest:uniffi_suggest_fn_method_suggeststore_dismiss_suggestion + using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kSuggestSuggestStorePointerType>, ScaffoldingConverter<RustBuffer>>; + return Some(CallHandler::CallAsync(uniffi_suggest_fn_method_suggeststore_dismiss_suggestion, aGlobal, aArgs, "uniffi_suggest_fn_method_suggeststore_dismiss_suggestion: "_ns, aError)); + } + case 15: { // suggest:uniffi_suggest_fn_method_suggeststore_fetch_global_config using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kSuggestSuggestStorePointerType>>; return Some(CallHandler::CallAsync(uniffi_suggest_fn_method_suggeststore_fetch_global_config, aGlobal, aArgs, "uniffi_suggest_fn_method_suggeststore_fetch_global_config: "_ns, aError)); } - case 7: { // suggest:uniffi_suggest_fn_method_suggeststore_fetch_provider_config + case 16: { // suggest:uniffi_suggest_fn_method_suggeststore_fetch_provider_config using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kSuggestSuggestStorePointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_suggest_fn_method_suggeststore_fetch_provider_config, aGlobal, aArgs, "uniffi_suggest_fn_method_suggeststore_fetch_provider_config: "_ns, aError)); } - case 8: { // suggest:uniffi_suggest_fn_method_suggeststore_ingest + case 17: { // suggest:uniffi_suggest_fn_method_suggeststore_ingest using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kSuggestSuggestStorePointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_suggest_fn_method_suggeststore_ingest, aGlobal, aArgs, "uniffi_suggest_fn_method_suggeststore_ingest: "_ns, aError)); } - case 9: { // suggest:uniffi_suggest_fn_method_suggeststore_interrupt + case 18: { // suggest:uniffi_suggest_fn_method_suggeststore_interrupt using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kSuggestSuggestStorePointerType>>; return Some(CallHandler::CallAsync(uniffi_suggest_fn_method_suggeststore_interrupt, aGlobal, aArgs, "uniffi_suggest_fn_method_suggeststore_interrupt: "_ns, aError)); } - case 10: { // suggest:uniffi_suggest_fn_method_suggeststore_query + case 19: { // suggest:uniffi_suggest_fn_method_suggeststore_query using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kSuggestSuggestStorePointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_suggest_fn_method_suggeststore_query, aGlobal, aArgs, "uniffi_suggest_fn_method_suggeststore_query: "_ns, aError)); } - case 11: { // suggest:uniffi_suggest_fn_constructor_suggeststorebuilder_new + case 20: { // suggest:uniffi_suggest_fn_clone_suggeststorebuilder + using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kSuggestSuggestStoreBuilderPointerType>, ScaffoldingObjectConverter<&kSuggestSuggestStoreBuilderPointerType>>; + return Some(CallHandler::CallAsync(uniffi_suggest_fn_clone_suggeststorebuilder, aGlobal, aArgs, "uniffi_suggest_fn_clone_suggeststorebuilder: "_ns, aError)); + } + case 21: { // suggest:uniffi_suggest_fn_constructor_suggeststorebuilder_new using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kSuggestSuggestStoreBuilderPointerType>>; return Some(CallHandler::CallAsync(uniffi_suggest_fn_constructor_suggeststorebuilder_new, aGlobal, aArgs, "uniffi_suggest_fn_constructor_suggeststorebuilder_new: "_ns, aError)); } - case 12: { // suggest:uniffi_suggest_fn_method_suggeststorebuilder_build + case 22: { // suggest:uniffi_suggest_fn_method_suggeststorebuilder_build using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kSuggestSuggestStorePointerType>, ScaffoldingObjectConverter<&kSuggestSuggestStoreBuilderPointerType>>; return Some(CallHandler::CallAsync(uniffi_suggest_fn_method_suggeststorebuilder_build, aGlobal, aArgs, "uniffi_suggest_fn_method_suggeststorebuilder_build: "_ns, aError)); } - case 13: { // suggest:uniffi_suggest_fn_method_suggeststorebuilder_cache_path + case 23: { // suggest:uniffi_suggest_fn_method_suggeststorebuilder_cache_path using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kSuggestSuggestStoreBuilderPointerType>, ScaffoldingObjectConverter<&kSuggestSuggestStoreBuilderPointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_suggest_fn_method_suggeststorebuilder_cache_path, aGlobal, aArgs, "uniffi_suggest_fn_method_suggeststorebuilder_cache_path: "_ns, aError)); } - case 14: { // suggest:uniffi_suggest_fn_method_suggeststorebuilder_data_path + case 24: { // suggest:uniffi_suggest_fn_method_suggeststorebuilder_data_path using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kSuggestSuggestStoreBuilderPointerType>, ScaffoldingObjectConverter<&kSuggestSuggestStoreBuilderPointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_suggest_fn_method_suggeststorebuilder_data_path, aGlobal, aArgs, "uniffi_suggest_fn_method_suggeststorebuilder_data_path: "_ns, aError)); } - case 15: { // suggest:uniffi_suggest_fn_method_suggeststorebuilder_remote_settings_config + case 25: { // suggest:uniffi_suggest_fn_method_suggeststorebuilder_remote_settings_config using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kSuggestSuggestStoreBuilderPointerType>, ScaffoldingObjectConverter<&kSuggestSuggestStoreBuilderPointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_suggest_fn_method_suggeststorebuilder_remote_settings_config, aGlobal, aArgs, "uniffi_suggest_fn_method_suggeststorebuilder_remote_settings_config: "_ns, aError)); } - case 16: { // suggest:uniffi_suggest_fn_func_raw_suggestion_url_matches + case 26: { // suggest:uniffi_suggest_fn_func_raw_suggestion_url_matches using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int8_t>, ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_suggest_fn_func_raw_suggestion_url_matches, aGlobal, aArgs, "uniffi_suggest_fn_func_raw_suggestion_url_matches: "_ns, aError)); } - case 17: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_apply + case 27: { // tabs:uniffi_tabs_fn_clone_tabsbridgedengine + using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>, ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>>; + return Some(CallHandler::CallAsync(uniffi_tabs_fn_clone_tabsbridgedengine, aGlobal, aArgs, "uniffi_tabs_fn_clone_tabsbridgedengine: "_ns, aError)); + } + case 28: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_apply using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>>; return Some(CallHandler::CallAsync(uniffi_tabs_fn_method_tabsbridgedengine_apply, aGlobal, aArgs, "uniffi_tabs_fn_method_tabsbridgedengine_apply: "_ns, aError)); } - case 18: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_ensure_current_sync_id + case 29: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_ensure_current_sync_id using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_tabs_fn_method_tabsbridgedengine_ensure_current_sync_id, aGlobal, aArgs, "uniffi_tabs_fn_method_tabsbridgedengine_ensure_current_sync_id: "_ns, aError)); } - case 19: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_last_sync + case 30: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_last_sync using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int64_t>, ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>>; return Some(CallHandler::CallAsync(uniffi_tabs_fn_method_tabsbridgedengine_last_sync, aGlobal, aArgs, "uniffi_tabs_fn_method_tabsbridgedengine_last_sync: "_ns, aError)); } - case 20: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_prepare_for_sync + case 31: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_prepare_for_sync using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_tabs_fn_method_tabsbridgedengine_prepare_for_sync, aGlobal, aArgs, "uniffi_tabs_fn_method_tabsbridgedengine_prepare_for_sync: "_ns, aError)); } - case 21: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_reset + case 32: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_reset using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>>; return Some(CallHandler::CallAsync(uniffi_tabs_fn_method_tabsbridgedengine_reset, aGlobal, aArgs, "uniffi_tabs_fn_method_tabsbridgedengine_reset: "_ns, aError)); } - case 22: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_reset_sync_id + case 33: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_reset_sync_id using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>>; return Some(CallHandler::CallAsync(uniffi_tabs_fn_method_tabsbridgedengine_reset_sync_id, aGlobal, aArgs, "uniffi_tabs_fn_method_tabsbridgedengine_reset_sync_id: "_ns, aError)); } - case 23: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_set_last_sync + case 34: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_set_last_sync using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>, ScaffoldingConverter<int64_t>>; return Some(CallHandler::CallAsync(uniffi_tabs_fn_method_tabsbridgedengine_set_last_sync, aGlobal, aArgs, "uniffi_tabs_fn_method_tabsbridgedengine_set_last_sync: "_ns, aError)); } - case 24: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_set_uploaded + case 35: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_set_uploaded using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>, ScaffoldingConverter<int64_t>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_tabs_fn_method_tabsbridgedengine_set_uploaded, aGlobal, aArgs, "uniffi_tabs_fn_method_tabsbridgedengine_set_uploaded: "_ns, aError)); } - case 25: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_store_incoming + case 36: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_store_incoming using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_tabs_fn_method_tabsbridgedengine_store_incoming, aGlobal, aArgs, "uniffi_tabs_fn_method_tabsbridgedengine_store_incoming: "_ns, aError)); } - case 26: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_sync_finished + case 37: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_sync_finished using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>>; return Some(CallHandler::CallAsync(uniffi_tabs_fn_method_tabsbridgedengine_sync_finished, aGlobal, aArgs, "uniffi_tabs_fn_method_tabsbridgedengine_sync_finished: "_ns, aError)); } - case 27: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_sync_id + case 38: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_sync_id using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>>; return Some(CallHandler::CallAsync(uniffi_tabs_fn_method_tabsbridgedengine_sync_id, aGlobal, aArgs, "uniffi_tabs_fn_method_tabsbridgedengine_sync_id: "_ns, aError)); } - case 28: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_sync_started + case 39: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_sync_started using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>>; return Some(CallHandler::CallAsync(uniffi_tabs_fn_method_tabsbridgedengine_sync_started, aGlobal, aArgs, "uniffi_tabs_fn_method_tabsbridgedengine_sync_started: "_ns, aError)); } - case 29: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_wipe + case 40: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_wipe using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>>; return Some(CallHandler::CallAsync(uniffi_tabs_fn_method_tabsbridgedengine_wipe, aGlobal, aArgs, "uniffi_tabs_fn_method_tabsbridgedengine_wipe: "_ns, aError)); } - case 30: { // tabs:uniffi_tabs_fn_constructor_tabsstore_new + case 41: { // tabs:uniffi_tabs_fn_clone_tabsstore + using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kTabsTabsStorePointerType>, ScaffoldingObjectConverter<&kTabsTabsStorePointerType>>; + return Some(CallHandler::CallAsync(uniffi_tabs_fn_clone_tabsstore, aGlobal, aArgs, "uniffi_tabs_fn_clone_tabsstore: "_ns, aError)); + } + case 42: { // tabs:uniffi_tabs_fn_constructor_tabsstore_new using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kTabsTabsStorePointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_tabs_fn_constructor_tabsstore_new, aGlobal, aArgs, "uniffi_tabs_fn_constructor_tabsstore_new: "_ns, aError)); } - case 31: { // tabs:uniffi_tabs_fn_method_tabsstore_bridged_engine + case 43: { // tabs:uniffi_tabs_fn_method_tabsstore_bridged_engine using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>, ScaffoldingObjectConverter<&kTabsTabsStorePointerType>>; return Some(CallHandler::CallAsync(uniffi_tabs_fn_method_tabsstore_bridged_engine, aGlobal, aArgs, "uniffi_tabs_fn_method_tabsstore_bridged_engine: "_ns, aError)); } - case 32: { // tabs:uniffi_tabs_fn_method_tabsstore_get_all + case 44: { // tabs:uniffi_tabs_fn_method_tabsstore_get_all using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kTabsTabsStorePointerType>>; return Some(CallHandler::CallAsync(uniffi_tabs_fn_method_tabsstore_get_all, aGlobal, aArgs, "uniffi_tabs_fn_method_tabsstore_get_all: "_ns, aError)); } - case 33: { // tabs:uniffi_tabs_fn_method_tabsstore_register_with_sync_manager + case 45: { // tabs:uniffi_tabs_fn_method_tabsstore_register_with_sync_manager using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTabsTabsStorePointerType>>; return Some(CallHandler::CallAsync(uniffi_tabs_fn_method_tabsstore_register_with_sync_manager, aGlobal, aArgs, "uniffi_tabs_fn_method_tabsstore_register_with_sync_manager: "_ns, aError)); } - case 34: { // tabs:uniffi_tabs_fn_method_tabsstore_set_local_tabs + case 46: { // tabs:uniffi_tabs_fn_method_tabsstore_set_local_tabs using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTabsTabsStorePointerType>, ScaffoldingConverter<RustBuffer>>; return Some(CallHandler::CallAsync(uniffi_tabs_fn_method_tabsstore_set_local_tabs, aGlobal, aArgs, "uniffi_tabs_fn_method_tabsstore_set_local_tabs: "_ns, aError)); } @@ -247,177 +318,237 @@ Maybe<already_AddRefed<Promise>> UniFFICallAsync(const GlobalObject& aGlobal, ui bool UniFFICallSync(const GlobalObject& aGlobal, uint64_t aId, const Sequence<ScaffoldingType>& aArgs, RootedDictionary<UniFFIScaffoldingCallResult>& aReturnValue, ErrorResult& aError) { switch (aId) { - case 0: { // remote_settings:uniffi_remote_settings_fn_constructor_remotesettings_new + case 0: { // relevancy:uniffi_relevancy_fn_clone_relevancystore + using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kRelevancyRelevancyStorePointerType>, ScaffoldingObjectConverter<&kRelevancyRelevancyStorePointerType>>; + CallHandler::CallSync(uniffi_relevancy_fn_clone_relevancystore, aGlobal, aArgs, aReturnValue, "uniffi_relevancy_fn_clone_relevancystore: "_ns, aError); + return true; + } + case 1: { // relevancy:uniffi_relevancy_fn_constructor_relevancystore_new + using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kRelevancyRelevancyStorePointerType>, ScaffoldingConverter<RustBuffer>>; + CallHandler::CallSync(uniffi_relevancy_fn_constructor_relevancystore_new, aGlobal, aArgs, aReturnValue, "uniffi_relevancy_fn_constructor_relevancystore_new: "_ns, aError); + return true; + } + case 2: { // relevancy:uniffi_relevancy_fn_method_relevancystore_calculate_metrics + using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRelevancyRelevancyStorePointerType>>; + CallHandler::CallSync(uniffi_relevancy_fn_method_relevancystore_calculate_metrics, aGlobal, aArgs, aReturnValue, "uniffi_relevancy_fn_method_relevancystore_calculate_metrics: "_ns, aError); + return true; + } + case 3: { // relevancy:uniffi_relevancy_fn_method_relevancystore_ingest + using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kRelevancyRelevancyStorePointerType>, ScaffoldingConverter<RustBuffer>>; + CallHandler::CallSync(uniffi_relevancy_fn_method_relevancystore_ingest, aGlobal, aArgs, aReturnValue, "uniffi_relevancy_fn_method_relevancystore_ingest: "_ns, aError); + return true; + } + case 4: { // relevancy:uniffi_relevancy_fn_method_relevancystore_user_interest_vector + using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRelevancyRelevancyStorePointerType>>; + CallHandler::CallSync(uniffi_relevancy_fn_method_relevancystore_user_interest_vector, aGlobal, aArgs, aReturnValue, "uniffi_relevancy_fn_method_relevancystore_user_interest_vector: "_ns, aError); + return true; + } + case 5: { // remote_settings:uniffi_remote_settings_fn_clone_remotesettings + using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kRemoteSettingsRemoteSettingsPointerType>, ScaffoldingObjectConverter<&kRemoteSettingsRemoteSettingsPointerType>>; + CallHandler::CallSync(uniffi_remote_settings_fn_clone_remotesettings, aGlobal, aArgs, aReturnValue, "uniffi_remote_settings_fn_clone_remotesettings: "_ns, aError); + return true; + } + case 6: { // remote_settings:uniffi_remote_settings_fn_constructor_remotesettings_new using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kRemoteSettingsRemoteSettingsPointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_remote_settings_fn_constructor_remotesettings_new, aGlobal, aArgs, aReturnValue, "uniffi_remote_settings_fn_constructor_remotesettings_new: "_ns, aError); return true; } - case 1: { // remote_settings:uniffi_remote_settings_fn_method_remotesettings_download_attachment_to_path + case 7: { // remote_settings:uniffi_remote_settings_fn_method_remotesettings_download_attachment_to_path using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kRemoteSettingsRemoteSettingsPointerType>, ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_remote_settings_fn_method_remotesettings_download_attachment_to_path, aGlobal, aArgs, aReturnValue, "uniffi_remote_settings_fn_method_remotesettings_download_attachment_to_path: "_ns, aError); return true; } - case 2: { // remote_settings:uniffi_remote_settings_fn_method_remotesettings_get_records + case 8: { // remote_settings:uniffi_remote_settings_fn_method_remotesettings_get_records using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRemoteSettingsRemoteSettingsPointerType>>; CallHandler::CallSync(uniffi_remote_settings_fn_method_remotesettings_get_records, aGlobal, aArgs, aReturnValue, "uniffi_remote_settings_fn_method_remotesettings_get_records: "_ns, aError); return true; } - case 3: { // remote_settings:uniffi_remote_settings_fn_method_remotesettings_get_records_since + case 9: { // remote_settings:uniffi_remote_settings_fn_method_remotesettings_get_records_since using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kRemoteSettingsRemoteSettingsPointerType>, ScaffoldingConverter<uint64_t>>; CallHandler::CallSync(uniffi_remote_settings_fn_method_remotesettings_get_records_since, aGlobal, aArgs, aReturnValue, "uniffi_remote_settings_fn_method_remotesettings_get_records_since: "_ns, aError); return true; } - case 4: { // suggest:uniffi_suggest_fn_constructor_suggeststore_new + case 10: { // suggest:uniffi_suggest_fn_clone_suggeststore + using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kSuggestSuggestStorePointerType>, ScaffoldingObjectConverter<&kSuggestSuggestStorePointerType>>; + CallHandler::CallSync(uniffi_suggest_fn_clone_suggeststore, aGlobal, aArgs, aReturnValue, "uniffi_suggest_fn_clone_suggeststore: "_ns, aError); + return true; + } + case 11: { // suggest:uniffi_suggest_fn_constructor_suggeststore_new using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kSuggestSuggestStorePointerType>, ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_suggest_fn_constructor_suggeststore_new, aGlobal, aArgs, aReturnValue, "uniffi_suggest_fn_constructor_suggeststore_new: "_ns, aError); return true; } - case 5: { // suggest:uniffi_suggest_fn_method_suggeststore_clear + case 12: { // suggest:uniffi_suggest_fn_method_suggeststore_clear using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kSuggestSuggestStorePointerType>>; CallHandler::CallSync(uniffi_suggest_fn_method_suggeststore_clear, aGlobal, aArgs, aReturnValue, "uniffi_suggest_fn_method_suggeststore_clear: "_ns, aError); return true; } - case 6: { // suggest:uniffi_suggest_fn_method_suggeststore_fetch_global_config + case 13: { // suggest:uniffi_suggest_fn_method_suggeststore_clear_dismissed_suggestions + using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kSuggestSuggestStorePointerType>>; + CallHandler::CallSync(uniffi_suggest_fn_method_suggeststore_clear_dismissed_suggestions, aGlobal, aArgs, aReturnValue, "uniffi_suggest_fn_method_suggeststore_clear_dismissed_suggestions: "_ns, aError); + return true; + } + case 14: { // suggest:uniffi_suggest_fn_method_suggeststore_dismiss_suggestion + using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kSuggestSuggestStorePointerType>, ScaffoldingConverter<RustBuffer>>; + CallHandler::CallSync(uniffi_suggest_fn_method_suggeststore_dismiss_suggestion, aGlobal, aArgs, aReturnValue, "uniffi_suggest_fn_method_suggeststore_dismiss_suggestion: "_ns, aError); + return true; + } + case 15: { // suggest:uniffi_suggest_fn_method_suggeststore_fetch_global_config using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kSuggestSuggestStorePointerType>>; CallHandler::CallSync(uniffi_suggest_fn_method_suggeststore_fetch_global_config, aGlobal, aArgs, aReturnValue, "uniffi_suggest_fn_method_suggeststore_fetch_global_config: "_ns, aError); return true; } - case 7: { // suggest:uniffi_suggest_fn_method_suggeststore_fetch_provider_config + case 16: { // suggest:uniffi_suggest_fn_method_suggeststore_fetch_provider_config using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kSuggestSuggestStorePointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_suggest_fn_method_suggeststore_fetch_provider_config, aGlobal, aArgs, aReturnValue, "uniffi_suggest_fn_method_suggeststore_fetch_provider_config: "_ns, aError); return true; } - case 8: { // suggest:uniffi_suggest_fn_method_suggeststore_ingest + case 17: { // suggest:uniffi_suggest_fn_method_suggeststore_ingest using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kSuggestSuggestStorePointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_suggest_fn_method_suggeststore_ingest, aGlobal, aArgs, aReturnValue, "uniffi_suggest_fn_method_suggeststore_ingest: "_ns, aError); return true; } - case 9: { // suggest:uniffi_suggest_fn_method_suggeststore_interrupt + case 18: { // suggest:uniffi_suggest_fn_method_suggeststore_interrupt using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kSuggestSuggestStorePointerType>>; CallHandler::CallSync(uniffi_suggest_fn_method_suggeststore_interrupt, aGlobal, aArgs, aReturnValue, "uniffi_suggest_fn_method_suggeststore_interrupt: "_ns, aError); return true; } - case 10: { // suggest:uniffi_suggest_fn_method_suggeststore_query + case 19: { // suggest:uniffi_suggest_fn_method_suggeststore_query using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kSuggestSuggestStorePointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_suggest_fn_method_suggeststore_query, aGlobal, aArgs, aReturnValue, "uniffi_suggest_fn_method_suggeststore_query: "_ns, aError); return true; } - case 11: { // suggest:uniffi_suggest_fn_constructor_suggeststorebuilder_new + case 20: { // suggest:uniffi_suggest_fn_clone_suggeststorebuilder + using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kSuggestSuggestStoreBuilderPointerType>, ScaffoldingObjectConverter<&kSuggestSuggestStoreBuilderPointerType>>; + CallHandler::CallSync(uniffi_suggest_fn_clone_suggeststorebuilder, aGlobal, aArgs, aReturnValue, "uniffi_suggest_fn_clone_suggeststorebuilder: "_ns, aError); + return true; + } + case 21: { // suggest:uniffi_suggest_fn_constructor_suggeststorebuilder_new using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kSuggestSuggestStoreBuilderPointerType>>; CallHandler::CallSync(uniffi_suggest_fn_constructor_suggeststorebuilder_new, aGlobal, aArgs, aReturnValue, "uniffi_suggest_fn_constructor_suggeststorebuilder_new: "_ns, aError); return true; } - case 12: { // suggest:uniffi_suggest_fn_method_suggeststorebuilder_build + case 22: { // suggest:uniffi_suggest_fn_method_suggeststorebuilder_build using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kSuggestSuggestStorePointerType>, ScaffoldingObjectConverter<&kSuggestSuggestStoreBuilderPointerType>>; CallHandler::CallSync(uniffi_suggest_fn_method_suggeststorebuilder_build, aGlobal, aArgs, aReturnValue, "uniffi_suggest_fn_method_suggeststorebuilder_build: "_ns, aError); return true; } - case 13: { // suggest:uniffi_suggest_fn_method_suggeststorebuilder_cache_path + case 23: { // suggest:uniffi_suggest_fn_method_suggeststorebuilder_cache_path using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kSuggestSuggestStoreBuilderPointerType>, ScaffoldingObjectConverter<&kSuggestSuggestStoreBuilderPointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_suggest_fn_method_suggeststorebuilder_cache_path, aGlobal, aArgs, aReturnValue, "uniffi_suggest_fn_method_suggeststorebuilder_cache_path: "_ns, aError); return true; } - case 14: { // suggest:uniffi_suggest_fn_method_suggeststorebuilder_data_path + case 24: { // suggest:uniffi_suggest_fn_method_suggeststorebuilder_data_path using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kSuggestSuggestStoreBuilderPointerType>, ScaffoldingObjectConverter<&kSuggestSuggestStoreBuilderPointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_suggest_fn_method_suggeststorebuilder_data_path, aGlobal, aArgs, aReturnValue, "uniffi_suggest_fn_method_suggeststorebuilder_data_path: "_ns, aError); return true; } - case 15: { // suggest:uniffi_suggest_fn_method_suggeststorebuilder_remote_settings_config + case 25: { // suggest:uniffi_suggest_fn_method_suggeststorebuilder_remote_settings_config using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kSuggestSuggestStoreBuilderPointerType>, ScaffoldingObjectConverter<&kSuggestSuggestStoreBuilderPointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_suggest_fn_method_suggeststorebuilder_remote_settings_config, aGlobal, aArgs, aReturnValue, "uniffi_suggest_fn_method_suggeststorebuilder_remote_settings_config: "_ns, aError); return true; } - case 16: { // suggest:uniffi_suggest_fn_func_raw_suggestion_url_matches + case 26: { // suggest:uniffi_suggest_fn_func_raw_suggestion_url_matches using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int8_t>, ScaffoldingConverter<RustBuffer>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_suggest_fn_func_raw_suggestion_url_matches, aGlobal, aArgs, aReturnValue, "uniffi_suggest_fn_func_raw_suggestion_url_matches: "_ns, aError); return true; } - case 17: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_apply + case 27: { // tabs:uniffi_tabs_fn_clone_tabsbridgedengine + using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>, ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>>; + CallHandler::CallSync(uniffi_tabs_fn_clone_tabsbridgedengine, aGlobal, aArgs, aReturnValue, "uniffi_tabs_fn_clone_tabsbridgedengine: "_ns, aError); + return true; + } + case 28: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_apply using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>>; CallHandler::CallSync(uniffi_tabs_fn_method_tabsbridgedengine_apply, aGlobal, aArgs, aReturnValue, "uniffi_tabs_fn_method_tabsbridgedengine_apply: "_ns, aError); return true; } - case 18: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_ensure_current_sync_id + case 29: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_ensure_current_sync_id using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_tabs_fn_method_tabsbridgedengine_ensure_current_sync_id, aGlobal, aArgs, aReturnValue, "uniffi_tabs_fn_method_tabsbridgedengine_ensure_current_sync_id: "_ns, aError); return true; } - case 19: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_last_sync + case 30: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_last_sync using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<int64_t>, ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>>; CallHandler::CallSync(uniffi_tabs_fn_method_tabsbridgedengine_last_sync, aGlobal, aArgs, aReturnValue, "uniffi_tabs_fn_method_tabsbridgedengine_last_sync: "_ns, aError); return true; } - case 20: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_prepare_for_sync + case 31: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_prepare_for_sync using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_tabs_fn_method_tabsbridgedengine_prepare_for_sync, aGlobal, aArgs, aReturnValue, "uniffi_tabs_fn_method_tabsbridgedengine_prepare_for_sync: "_ns, aError); return true; } - case 21: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_reset + case 32: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_reset using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>>; CallHandler::CallSync(uniffi_tabs_fn_method_tabsbridgedengine_reset, aGlobal, aArgs, aReturnValue, "uniffi_tabs_fn_method_tabsbridgedengine_reset: "_ns, aError); return true; } - case 22: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_reset_sync_id + case 33: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_reset_sync_id using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>>; CallHandler::CallSync(uniffi_tabs_fn_method_tabsbridgedengine_reset_sync_id, aGlobal, aArgs, aReturnValue, "uniffi_tabs_fn_method_tabsbridgedengine_reset_sync_id: "_ns, aError); return true; } - case 23: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_set_last_sync + case 34: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_set_last_sync using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>, ScaffoldingConverter<int64_t>>; CallHandler::CallSync(uniffi_tabs_fn_method_tabsbridgedengine_set_last_sync, aGlobal, aArgs, aReturnValue, "uniffi_tabs_fn_method_tabsbridgedengine_set_last_sync: "_ns, aError); return true; } - case 24: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_set_uploaded + case 35: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_set_uploaded using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>, ScaffoldingConverter<int64_t>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_tabs_fn_method_tabsbridgedengine_set_uploaded, aGlobal, aArgs, aReturnValue, "uniffi_tabs_fn_method_tabsbridgedengine_set_uploaded: "_ns, aError); return true; } - case 25: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_store_incoming + case 36: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_store_incoming using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_tabs_fn_method_tabsbridgedengine_store_incoming, aGlobal, aArgs, aReturnValue, "uniffi_tabs_fn_method_tabsbridgedengine_store_incoming: "_ns, aError); return true; } - case 26: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_sync_finished + case 37: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_sync_finished using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>>; CallHandler::CallSync(uniffi_tabs_fn_method_tabsbridgedengine_sync_finished, aGlobal, aArgs, aReturnValue, "uniffi_tabs_fn_method_tabsbridgedengine_sync_finished: "_ns, aError); return true; } - case 27: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_sync_id + case 38: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_sync_id using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>>; CallHandler::CallSync(uniffi_tabs_fn_method_tabsbridgedengine_sync_id, aGlobal, aArgs, aReturnValue, "uniffi_tabs_fn_method_tabsbridgedengine_sync_id: "_ns, aError); return true; } - case 28: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_sync_started + case 39: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_sync_started using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>>; CallHandler::CallSync(uniffi_tabs_fn_method_tabsbridgedengine_sync_started, aGlobal, aArgs, aReturnValue, "uniffi_tabs_fn_method_tabsbridgedengine_sync_started: "_ns, aError); return true; } - case 29: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_wipe + case 40: { // tabs:uniffi_tabs_fn_method_tabsbridgedengine_wipe using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>>; CallHandler::CallSync(uniffi_tabs_fn_method_tabsbridgedengine_wipe, aGlobal, aArgs, aReturnValue, "uniffi_tabs_fn_method_tabsbridgedengine_wipe: "_ns, aError); return true; } - case 30: { // tabs:uniffi_tabs_fn_constructor_tabsstore_new + case 41: { // tabs:uniffi_tabs_fn_clone_tabsstore + using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kTabsTabsStorePointerType>, ScaffoldingObjectConverter<&kTabsTabsStorePointerType>>; + CallHandler::CallSync(uniffi_tabs_fn_clone_tabsstore, aGlobal, aArgs, aReturnValue, "uniffi_tabs_fn_clone_tabsstore: "_ns, aError); + return true; + } + case 42: { // tabs:uniffi_tabs_fn_constructor_tabsstore_new using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kTabsTabsStorePointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_tabs_fn_constructor_tabsstore_new, aGlobal, aArgs, aReturnValue, "uniffi_tabs_fn_constructor_tabsstore_new: "_ns, aError); return true; } - case 31: { // tabs:uniffi_tabs_fn_method_tabsstore_bridged_engine + case 43: { // tabs:uniffi_tabs_fn_method_tabsstore_bridged_engine using CallHandler = ScaffoldingCallHandler<ScaffoldingObjectConverter<&kTabsTabsBridgedEnginePointerType>, ScaffoldingObjectConverter<&kTabsTabsStorePointerType>>; CallHandler::CallSync(uniffi_tabs_fn_method_tabsstore_bridged_engine, aGlobal, aArgs, aReturnValue, "uniffi_tabs_fn_method_tabsstore_bridged_engine: "_ns, aError); return true; } - case 32: { // tabs:uniffi_tabs_fn_method_tabsstore_get_all + case 44: { // tabs:uniffi_tabs_fn_method_tabsstore_get_all using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<RustBuffer>, ScaffoldingObjectConverter<&kTabsTabsStorePointerType>>; CallHandler::CallSync(uniffi_tabs_fn_method_tabsstore_get_all, aGlobal, aArgs, aReturnValue, "uniffi_tabs_fn_method_tabsstore_get_all: "_ns, aError); return true; } - case 33: { // tabs:uniffi_tabs_fn_method_tabsstore_register_with_sync_manager + case 45: { // tabs:uniffi_tabs_fn_method_tabsstore_register_with_sync_manager using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTabsTabsStorePointerType>>; CallHandler::CallSync(uniffi_tabs_fn_method_tabsstore_register_with_sync_manager, aGlobal, aArgs, aReturnValue, "uniffi_tabs_fn_method_tabsstore_register_with_sync_manager: "_ns, aError); return true; } - case 34: { // tabs:uniffi_tabs_fn_method_tabsstore_set_local_tabs + case 46: { // tabs:uniffi_tabs_fn_method_tabsstore_set_local_tabs using CallHandler = ScaffoldingCallHandler<ScaffoldingConverter<void>, ScaffoldingObjectConverter<&kTabsTabsStorePointerType>, ScaffoldingConverter<RustBuffer>>; CallHandler::CallSync(uniffi_tabs_fn_method_tabsstore_set_local_tabs, aGlobal, aArgs, aReturnValue, "uniffi_tabs_fn_method_tabsstore_set_local_tabs: "_ns, aError); return true; @@ -429,23 +560,27 @@ bool UniFFICallSync(const GlobalObject& aGlobal, uint64_t aId, const Sequence<Sc Maybe<already_AddRefed<UniFFIPointer>> UniFFIReadPointer(const GlobalObject& aGlobal, uint64_t aId, const ArrayBuffer& aArrayBuff, long aPosition, ErrorResult& aError) { const UniFFIPointerType* type; switch (aId) { - case 0: { // remote_settings:RemoteSettings + case 0: { // relevancy:RelevancyStore + type = &kRelevancyRelevancyStorePointerType; + break; + } + case 1: { // remote_settings:RemoteSettings type = &kRemoteSettingsRemoteSettingsPointerType; break; } - case 1: { // suggest:SuggestStore + case 2: { // suggest:SuggestStore type = &kSuggestSuggestStorePointerType; break; } - case 2: { // suggest:SuggestStoreBuilder + case 3: { // suggest:SuggestStoreBuilder type = &kSuggestSuggestStoreBuilderPointerType; break; } - case 3: { // tabs:TabsBridgedEngine + case 4: { // tabs:TabsBridgedEngine type = &kTabsTabsBridgedEnginePointerType; break; } - case 4: { // tabs:TabsStore + case 5: { // tabs:TabsStore type = &kTabsTabsStorePointerType; break; } @@ -458,23 +593,27 @@ Maybe<already_AddRefed<UniFFIPointer>> UniFFIReadPointer(const GlobalObject& aGl bool UniFFIWritePointer(const GlobalObject& aGlobal, uint64_t aId, const UniFFIPointer& aPtr, const ArrayBuffer& aArrayBuff, long aPosition, ErrorResult& aError) { const UniFFIPointerType* type; switch (aId) { - case 0: { // remote_settings:RemoteSettings + case 0: { // relevancy:RelevancyStore + type = &kRelevancyRelevancyStorePointerType; + break; + } + case 1: { // remote_settings:RemoteSettings type = &kRemoteSettingsRemoteSettingsPointerType; break; } - case 1: { // suggest:SuggestStore + case 2: { // suggest:SuggestStore type = &kSuggestSuggestStorePointerType; break; } - case 2: { // suggest:SuggestStoreBuilder + case 3: { // suggest:SuggestStoreBuilder type = &kSuggestSuggestStoreBuilderPointerType; break; } - case 3: { // tabs:TabsBridgedEngine + case 4: { // tabs:TabsBridgedEngine type = &kTabsTabsBridgedEnginePointerType; break; } - case 4: { // tabs:TabsStore + case 5: { // tabs:TabsStore type = &kTabsTabsStorePointerType; break; } diff --git a/toolkit/components/uniffi-js/UniFFIPointer.cpp b/toolkit/components/uniffi-js/UniFFIPointer.cpp index c3a1eba93d..8e79bac0db 100644 --- a/toolkit/components/uniffi-js/UniFFIPointer.cpp +++ b/toolkit/components/uniffi-js/UniFFIPointer.cpp @@ -5,6 +5,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "nsPrintfCString.h" +#include "js/GCAPI.h" #include "mozilla/EndianUtils.h" #include "mozilla/dom/UniFFIPointer.h" #include "mozilla/dom/UniFFIBinding.h" @@ -81,8 +82,14 @@ void UniFFIPointer::Write(const ArrayBuffer& aArrayBuff, uint32_t aPosition, // in Rust and Read(), a u64 is read as BigEndian and then converted to // a pointer we do the reverse here const auto& data_ptr = aData.Subspan(aPosition, 8); + // The hazard checker assumes all calls to a function pointer may result + // in a GC call and `ClonePtr` calls mType->clone. However, we know that + // mtype->clone won't make a GC call since it's essentially just a call + // to Rust's `Arc::clone()`. Use AutoSuppressGCAnalysis to tell the + // hazard checker to ignore the call. + JS::AutoSuppressGCAnalysis suppress; mozilla::BigEndian::writeUint64(data_ptr.Elements(), - (uint64_t)GetPtr()); + (uint64_t)ClonePtr()); return true; })) { aError.ThrowRangeError("position is out of range"); @@ -100,10 +107,14 @@ JSObject* UniFFIPointer::WrapObject(JSContext* aCx, return dom::UniFFIPointer_Binding::Wrap(aCx, this, aGivenProto); } -void* UniFFIPointer::GetPtr() const { +void* UniFFIPointer::ClonePtr() const { MOZ_LOG(sUniFFIPointerLogger, LogLevel::Info, - ("[UniFFI] Getting raw pointer")); - return this->mPtr; + ("[UniFFI] Cloning raw pointer")); + RustCallStatus status{}; + auto cloned = this->mType->clone(this->mPtr, &status); + MOZ_DIAGNOSTIC_ASSERT(status.code == RUST_CALL_SUCCESS, + "UniFFI clone call returned a non-success result"); + return cloned; } bool UniFFIPointer::IsSamePtrType(const UniFFIPointerType* aType) const { diff --git a/toolkit/components/uniffi-js/UniFFIPointer.h b/toolkit/components/uniffi-js/UniFFIPointer.h index 59acc0ca66..f9a95c309e 100644 --- a/toolkit/components/uniffi-js/UniFFIPointer.h +++ b/toolkit/components/uniffi-js/UniFFIPointer.h @@ -36,15 +36,13 @@ class UniFFIPointer final : public nsISupports, public nsWrapperCache { nsISupports* GetParentObject() { return nullptr; } /** - * returns the raw pointer `UniFFIPointer` holds - * This is safe because: - * - The pointer was allocated in Rust as a reference counted `Arc<T>` - * - Rust cloned the pointer without destructing it when passed into C++ - * - Eventually, when the destructor of `UniFFIPointer` runs, we return - * ownership to Rust, which then decrements the count and deallocates the - * memory the pointer points to. + * Clone the raw pointer that `UniFFIPointer` holds + * + * Use this when lowering the pointer to pass it across the FFI, for example: + * - When calling a method + * - When passing the object as an argument to a function */ - void* GetPtr() const; + void* ClonePtr() const; /** * Returns true if the pointer type `this` holds is the same as the argument diff --git a/toolkit/components/uniffi-js/UniFFIPointerType.h b/toolkit/components/uniffi-js/UniFFIPointerType.h index 7236e50cb7..25294cfc9d 100644 --- a/toolkit/components/uniffi-js/UniFFIPointerType.h +++ b/toolkit/components/uniffi-js/UniFFIPointerType.h @@ -21,7 +21,9 @@ namespace mozilla::uniffi { **/ struct UniFFIPointerType { nsLiteralCString typeName; - // The Rust destructor for the pointer, this gives back ownership to Rust + // Rust FFI function to clone for the pointer + void* (*clone)(void*, RustCallStatus*); + // Rust FFI function to destroy for the pointer void (*destructor)(void*, RustCallStatus*); }; } // namespace mozilla::uniffi diff --git a/toolkit/components/uniffi-js/UniFFIRust.h b/toolkit/components/uniffi-js/UniFFIRust.h index 2907af8f51..608b8063ae 100644 --- a/toolkit/components/uniffi-js/UniFFIRust.h +++ b/toolkit/components/uniffi-js/UniFFIRust.h @@ -28,8 +28,8 @@ constexpr int8_t CALLBACK_INTERFACE_UNEXPECTED_ERROR = 2; // structs/functions from UniFFI extern "C" { struct RustBuffer { - int32_t capacity; - int32_t len; + uint64_t capacity; + uint64_t len; uint8_t* data; }; @@ -42,7 +42,7 @@ typedef int (*ForeignCallback)(uint64_t handle, uint32_t method, const uint8_t* argsData, int32_t argsLen, RustBuffer* buf_ptr); -RustBuffer uniffi_rustbuffer_alloc(int32_t size, RustCallStatus* call_status); +RustBuffer uniffi_rustbuffer_alloc(uint64_t size, RustCallStatus* call_status); void uniffi_rustbuffer_free(RustBuffer buf, RustCallStatus* call_status); } diff --git a/toolkit/components/url-classifier/nsUrlClassifierUtils.cpp b/toolkit/components/url-classifier/nsUrlClassifierUtils.cpp index da5e9cd451..6a329830d6 100644 --- a/toolkit/components/url-classifier/nsUrlClassifierUtils.cpp +++ b/toolkit/components/url-classifier/nsUrlClassifierUtils.cpp @@ -293,7 +293,12 @@ nsUrlClassifierUtils::GetKeyForURI(nsIURI* uri, nsACString& _retval) { rv = innerURI->GetQuery(query); NS_ENSURE_SUCCESS(rv, rv); - _retval.AppendPrintf("?%s", query.get()); + // We have to canonicalize the query too based on + // https://developers.google.com/safe-browsing/v4/urls-hashing?hl=en#canonicalization + rv = CanonicalizeQuery(query, temp); + NS_ENSURE_SUCCESS(rv, rv); + + _retval.Append(temp); } return NS_OK; @@ -917,6 +922,25 @@ nsresult nsUrlClassifierUtils::CanonicalizePath(const nsACString& path, return NS_OK; } +nsresult nsUrlClassifierUtils::CanonicalizeQuery(const nsACString& query, + nsACString& _retval) { + _retval.Truncate(); + _retval.Append('?'); + + // Unescape the query + nsAutoCString unescaped; + if (!NS_UnescapeURL(PromiseFlatCString(query).get(), + PromiseFlatCString(query).Length(), 0, unescaped)) { + unescaped.Assign(query); + } + + // slash folding does not apply to the query parameters, but we need to + // percent-escape all characters that are <= ASCII 32, >= 127, "#", or "%" + SpecialEncode(unescaped, false, _retval); + + return NS_OK; +} + void nsUrlClassifierUtils::CleanupHostname(const nsACString& hostname, nsACString& _retval) { _retval.Truncate(); diff --git a/toolkit/components/url-classifier/nsUrlClassifierUtils.h b/toolkit/components/url-classifier/nsUrlClassifierUtils.h index 5ff9d97fdc..bcced1a76a 100644 --- a/toolkit/components/url-classifier/nsUrlClassifierUtils.h +++ b/toolkit/components/url-classifier/nsUrlClassifierUtils.h @@ -29,6 +29,8 @@ class nsUrlClassifierUtils final : public nsIUrlClassifierUtils, nsACString& _retval); nsresult CanonicalizePath(const nsACString& url, nsACString& _retval); + nsresult CanonicalizeQuery(const nsACString& query, nsACString& _retval); + // This function will encode all "special" characters in typical url encoding, // that is %hh where h is a valid hex digit. The characters which are encoded // by this function are any ascii characters under 32(control characters and diff --git a/toolkit/components/url-classifier/tests/mochitest/classifierCommon.js b/toolkit/components/url-classifier/tests/mochitest/classifierCommon.js index 00a4e8d08b..bf3d35047f 100644 --- a/toolkit/components/url-classifier/tests/mochitest/classifierCommon.js +++ b/toolkit/components/url-classifier/tests/mochitest/classifierCommon.js @@ -63,7 +63,7 @@ function doReload() { } } -// SafeBrowsing.jsm is initialized after mozEntries are added. Add observer +// SafeBrowsing.sys.mjs is initialized after mozEntries are added. Add observer // to receive "finished" event. For the case when this function is called // after the event had already been notified, we lookup entries to see if // they are already added to database. diff --git a/toolkit/components/url-classifier/tests/mochitest/classifierHelper.js b/toolkit/components/url-classifier/tests/mochitest/classifierHelper.js index b07bfc7fc5..55870b462c 100644 --- a/toolkit/components/url-classifier/tests/mochitest/classifierHelper.js +++ b/toolkit/components/url-classifier/tests/mochitest/classifierHelper.js @@ -24,7 +24,7 @@ classifierHelper._updatesToCleanup = []; classifierHelper._initsCB = []; -// This function return a Promise, promise is resolved when SafeBrowsing.jsm +// This function return a Promise, promise is resolved when SafeBrowsing.sys.mjs // is initialized. classifierHelper.waitForInit = function () { return new Promise(function (resolve) { diff --git a/toolkit/components/url-classifier/tests/unit/test_canonicalization.js b/toolkit/components/url-classifier/tests/unit/test_canonicalization.js index e26bb5d84a..a9fdf71315 100644 --- a/toolkit/components/url-classifier/tests/unit/test_canonicalization.js +++ b/toolkit/components/url-classifier/tests/unit/test_canonicalization.js @@ -80,4 +80,14 @@ function run_test() { canonicalize("http://host.com//twoslashes?more//slashes"), "http://host.com/twoslashes?more//slashes" ); + equal( + canonicalize("http://host.com/path?query%3Awith%3Acolons"), + "http://host.com/path?query:with:colons" + ); + equal( + canonicalize( + "https://wiki.mozilla.org/index.php?title=MozillaWiki%3AHelp&action=history" + ), + "https://wiki.mozilla.org/index.php?title=MozillaWiki:Help&action=history" + ); } diff --git a/toolkit/components/utils/ClientEnvironment.sys.mjs b/toolkit/components/utils/ClientEnvironment.sys.mjs index 1c6bbfbaba..64fb6da9e1 100644 --- a/toolkit/components/utils/ClientEnvironment.sys.mjs +++ b/toolkit/components/utils/ClientEnvironment.sys.mjs @@ -76,7 +76,7 @@ export class ClientEnvironmentBase { } return new Proxy(target, { - get(target, prop, receiver) { + get(target, prop) { if (prop == "main") { return target.main; } diff --git a/toolkit/components/utils/SimpleServices.sys.mjs b/toolkit/components/utils/SimpleServices.sys.mjs index a69388435c..7c872cc7a6 100644 --- a/toolkit/components/utils/SimpleServices.sys.mjs +++ b/toolkit/components/utils/SimpleServices.sys.mjs @@ -103,7 +103,7 @@ AddonLocalizationConverter.prototype = { this.listener = aListener; }, - onStartRequest(aRequest) { + onStartRequest() { this.parts = []; this.decoder = new TextDecoder(); }, diff --git a/toolkit/components/viewsource/content/viewSourceUtils.js b/toolkit/components/viewsource/content/viewSourceUtils.js index 69d3eff287..bc85bb3c1b 100644 --- a/toolkit/components/viewsource/content/viewSourceUtils.js +++ b/toolkit/components/viewsource/content/viewSourceUtils.js @@ -62,8 +62,8 @@ var gViewSourceUtils = { } // Try existing browsers first. let browserWin = Services.wm.getMostRecentWindow("navigator:browser"); - if (browserWin && browserWin.BrowserViewSourceOfDocument) { - browserWin.BrowserViewSourceOfDocument(aArgs); + if (browserWin && browserWin.BrowserCommands.viewSourceOfDocument) { + browserWin.BrowserCommands.viewSourceOfDocument(aArgs); return; } // No browser window created yet, try to create one. diff --git a/toolkit/components/viewsource/test/browser/browser_contextmenu.js b/toolkit/components/viewsource/test/browser/browser_contextmenu.js index afc8ab8c84..12244f7589 100644 --- a/toolkit/components/viewsource/test/browser/browser_contextmenu.js +++ b/toolkit/components/viewsource/test/browser/browser_contextmenu.js @@ -42,7 +42,7 @@ async function onViewSourceWindowOpen(aWindow) { gCopyEmailMenuItem = aWindow.document.getElementById("context-copyemail"); let browser = gBrowser.selectedBrowser; - await SpecialPowers.spawn(browser, [], async function (arg) { + await SpecialPowers.spawn(browser, [], async function () { let tags = content.document.querySelectorAll("a[href]"); Assert.equal( tags[0].href, diff --git a/toolkit/components/viewsource/test/browser/browser_viewsource_newwindow.js b/toolkit/components/viewsource/test/browser/browser_viewsource_newwindow.js index 8a38d94719..e141e94150 100644 --- a/toolkit/components/viewsource/test/browser/browser_viewsource_newwindow.js +++ b/toolkit/components/viewsource/test/browser/browser_viewsource_newwindow.js @@ -33,7 +33,7 @@ add_task(async function () { }, async browser => { let winPromise = waitForNewViewSourceWindow("view-source:" + PAGE); - BrowserViewSource(browser); + BrowserCommands.viewSource(browser); let win = await winPromise; ok(win, "View Source opened up in a new window."); @@ -58,14 +58,14 @@ add_task(async function () { url: "data:text/html," + source, gBrowser, }, - async browser => { + async () => { let winPromise = waitForNewViewSourceWindow( "view-source:data:text/html;charset=utf-8,%3Cp%3E%EF%B7%90test%EF%B7%AF%3C%2Fp%3E" ); await SpecialPowers.spawn( gBrowser.selectedBrowser, [], - async function (arg) { + async function () { let element = content.document.querySelector("p"); content.getSelection().selectAllChildren(element); } diff --git a/toolkit/components/viewsource/test/browser/head.js b/toolkit/components/viewsource/test/browser/head.js index a75c6a8658..083c169c0a 100644 --- a/toolkit/components/viewsource/test/browser/head.js +++ b/toolkit/components/viewsource/test/browser/head.js @@ -43,7 +43,7 @@ async function waitForViewSourceTab(open) { */ function openViewSourceForBrowser(browser) { return waitForViewSourceTab(() => { - window.BrowserViewSource(browser); + window.BrowserCommands.viewSource(browser); }); } diff --git a/toolkit/components/windowcreator/test/320x240.ogv b/toolkit/components/windowcreator/test/320x240.ogv Binary files differdeleted file mode 100644 index 093158432a..0000000000 --- a/toolkit/components/windowcreator/test/320x240.ogv +++ /dev/null diff --git a/toolkit/components/windowcreator/test/320x240.webm b/toolkit/components/windowcreator/test/320x240.webm Binary files differnew file mode 100644 index 0000000000..16ecdbf688 --- /dev/null +++ b/toolkit/components/windowcreator/test/320x240.webm diff --git a/toolkit/components/windowcreator/test/bug449141_page.html b/toolkit/components/windowcreator/test/bug449141_page.html index 3b8e3f550a..4c8e46dd10 100644 --- a/toolkit/components/windowcreator/test/bug449141_page.html +++ b/toolkit/components/windowcreator/test/bug449141_page.html @@ -6,6 +6,6 @@ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> </head> <body> - <video src='320x240.ogv'></video> + <video src='320x240.webm'></video> </body> </html> diff --git a/toolkit/components/windowcreator/test/chrome.toml b/toolkit/components/windowcreator/test/chrome.toml index 0fdc8250e0..8afd9acbe5 100644 --- a/toolkit/components/windowcreator/test/chrome.toml +++ b/toolkit/components/windowcreator/test/chrome.toml @@ -1,6 +1,6 @@ [DEFAULT] support-files = [ - "320x240.ogv", + "320x240.webm", "bug449141_page.html", "bug1170334_iframe.xml", "bug1170334_style.css", diff --git a/toolkit/components/windowcreator/test/test_bug449141.html b/toolkit/components/windowcreator/test/test_bug449141.html index f178689599..24963295a0 100644 --- a/toolkit/components/windowcreator/test/test_bug449141.html +++ b/toolkit/components/windowcreator/test/test_bug449141.html @@ -35,7 +35,7 @@ var progressListener = { onProgressChange() { /* Ignore progress callback */ }, - onStateChange(aProgress, aRequest, aStateFlag, aStatus) { + onStateChange(aProgress, aRequest, aStateFlag) { if (aStateFlag & STATE_STOP) { var dirExists = false; var videoExists = false; @@ -43,7 +43,7 @@ var progressListener = { var videoFile = getTempDir(); videoFile.append(this.dirName); dirExists = videoFile.exists(); - videoFile.append("320x240.ogv"); + videoFile.append("320x240.webm"); videoExists = videoFile.exists(); this.folder.remove(true); this.file.remove(false); diff --git a/toolkit/components/windowwatcher/nsWindowWatcher.cpp b/toolkit/components/windowwatcher/nsWindowWatcher.cpp index 209af157a3..b482214d31 100644 --- a/toolkit/components/windowwatcher/nsWindowWatcher.cpp +++ b/toolkit/components/windowwatcher/nsWindowWatcher.cpp @@ -1431,7 +1431,7 @@ nsresult nsWindowWatcher::OpenWindowInternal( } if (parentWidget && ((!newWindowShouldBeModal && parentIsModal) || isAppModal)) { - parentWidget->SetFakeModal(true); + parentWidget->SetModal(true); } else { // Reset popup state while opening a modal dialog, and firing // events about the dialog, to prevent the current state from diff --git a/toolkit/components/windowwatcher/test/browser_new_content_window_chromeflags.js b/toolkit/components/windowwatcher/test/browser_new_content_window_chromeflags.js index 4eff3b46f0..d14175c088 100644 --- a/toolkit/components/windowwatcher/test/browser_new_content_window_chromeflags.js +++ b/toolkit/components/windowwatcher/test/browser_new_content_window_chromeflags.js @@ -134,7 +134,7 @@ function getContentChromeFlags(win) { * @param chromeFlags (int) * Some chromeFlags to check. */ -function assertContentFlags(chromeFlags, isPopup) { +function assertContentFlags(chromeFlags) { for (let feature in DISALLOWED) { let flag = DISALLOWED[feature].flag; Assert.ok(flag, "Expected flag to be a non-zeroish value"); @@ -183,7 +183,7 @@ add_task(async function test_disallowed_flags() { gBrowser, url: SCRIPT_PAGE, }, - async function (browser) { + async function () { let win = await newWinPromise; let parentChromeFlags = getParentChromeFlags(win); assertContentFlags(parentChromeFlags); @@ -230,7 +230,7 @@ add_task(async function test_disallowed_flags() { gBrowser, url: SCRIPT_PAGE_FOR_CHROME_ALL, }, - async function (browser) { + async function () { let win = await newWinPromise; let parentChromeFlags = getParentChromeFlags(win); Assert.notEqual( @@ -257,7 +257,7 @@ add_task(async function test_scrollbars_flag() { gBrowser, url: SCRIPT_PAGE, }, - async function (browser) { + async function () { let win = await newWinPromise; let parentChromeFlags = getParentChromeFlags(win); diff --git a/toolkit/components/windowwatcher/test/browser_new_remote_window_flags.js b/toolkit/components/windowwatcher/test/browser_new_remote_window_flags.js index 06a15d123c..ae2bdc41b3 100644 --- a/toolkit/components/windowwatcher/test/browser_new_remote_window_flags.js +++ b/toolkit/components/windowwatcher/test/browser_new_remote_window_flags.js @@ -61,7 +61,7 @@ add_task(async function test_new_remote_window_flags_window_open() { gBrowser, url: SCRIPT_PAGE, }, - async function (browser) { + async function () { let win = await newWinPromise; assertFlags(win); await BrowserTestUtils.closeWindow(win); diff --git a/toolkit/components/windowwatcher/test/browser_new_sized_window.js b/toolkit/components/windowwatcher/test/browser_new_sized_window.js index 0a1e63a8c8..acb73f027e 100644 --- a/toolkit/components/windowwatcher/test/browser_new_sized_window.js +++ b/toolkit/components/windowwatcher/test/browser_new_sized_window.js @@ -33,7 +33,7 @@ function test_dimensions({ width, height }) { gBrowser, url: SCRIPT_PAGE, }, - async function (browser) { + async function () { let win = await newWinPromise; let rect = win.gBrowser.selectedBrowser.getBoundingClientRect(); diff --git a/toolkit/components/windowwatcher/test/browser_non_popup_from_popup.js b/toolkit/components/windowwatcher/test/browser_non_popup_from_popup.js index 9c905bacc1..b6e7f2b131 100644 --- a/toolkit/components/windowwatcher/test/browser_non_popup_from_popup.js +++ b/toolkit/components/windowwatcher/test/browser_non_popup_from_popup.js @@ -28,7 +28,7 @@ add_task(async function test_non_popup_from_popup() { gBrowser, url: BLANK_PAGE, }, - async function (browser) { + async function () { // Wait for a popup opened by POPUP_OPENER. const newPopupPromise = BrowserTestUtils.waitForNewWindow(); diff --git a/toolkit/components/windowwatcher/test/head.js b/toolkit/components/windowwatcher/test/head.js index f72344bcd0..d126856244 100644 --- a/toolkit/components/windowwatcher/test/head.js +++ b/toolkit/components/windowwatcher/test/head.js @@ -125,7 +125,7 @@ async function testPopupPatterns(nonPopup) { gBrowser, url: BLANK_PAGE, }, - async function (browser) { + async function () { const newWinPromise = BrowserTestUtils.waitForNewWindow(); BrowserTestUtils.startLoadingURIString(gBrowser, SCRIPT_PAGE); @@ -159,7 +159,7 @@ async function testPopupPatterns(nonPopup) { gBrowser, url: BLANK_PAGE, }, - async function (browser) { + async function () { const newTabPromise = BrowserTestUtils.waitForNewTab( gBrowser, OPEN_PAGE diff --git a/toolkit/components/workerloader/tests/worker_test_loading.js b/toolkit/components/workerloader/tests/worker_test_loading.js index 551c56d7d3..d13fbed684 100644 --- a/toolkit/components/workerloader/tests/worker_test_loading.js +++ b/toolkit/components/workerloader/tests/worker_test_loading.js @@ -152,7 +152,7 @@ add_test(function test_load() { is(J.foo, "foo", "Module J exported value foo"); }); -self.onmessage = function (message) { +self.onmessage = function () { for (let test of tests) { info("Entering " + test.name); try { diff --git a/toolkit/components/xulstore/XULStore.sys.mjs b/toolkit/components/xulstore/XULStore.sys.mjs index eb1965ec6d..f84cd57712 100644 --- a/toolkit/components/xulstore/XULStore.sys.mjs +++ b/toolkit/components/xulstore/XULStore.sys.mjs @@ -89,7 +89,7 @@ XULStore.prototype = { this.readFile(); }, - observe(subject, topic, data) { + observe(subject, topic) { this.writeFile(); if (topic == "profile-before-change") { this._saveAllowed = false; @@ -153,7 +153,7 @@ XULStore.prototype = { } const uri = node.ownerDocument.documentURI; - const value = node.getAttribute(attr); + const value = node.getAttribute(attr) || ""; if (node.localName == "window") { this.log("Persisting attributes to windows is handled by AppWindow."); diff --git a/toolkit/components/xulstore/nsIXULStore.idl b/toolkit/components/xulstore/nsIXULStore.idl index 832c07dc90..488a99c50b 100644 --- a/toolkit/components/xulstore/nsIXULStore.idl +++ b/toolkit/components/xulstore/nsIXULStore.idl @@ -46,7 +46,7 @@ interface nsIXULStore: nsISupports * @param id - identifier of the node * @param attr - attribute */ - bool hasValue(in AString doc, in AString id, in AString attr); + boolean hasValue(in AString doc, in AString id, in AString attr); /** * Retrieves a value in the store, or an empty string if it does not exist. diff --git a/toolkit/content/aboutLogging.js b/toolkit/content/aboutLogging.js index daff10fbec..360ab366e1 100644 --- a/toolkit/content/aboutLogging.js +++ b/toolkit/content/aboutLogging.js @@ -234,7 +234,7 @@ function populatePresets() { $("#log-modules").value = gLoggingPresets[dropdown.value].modules; setPresetAndDescription(dropdown.value); // When changing the list switch to custom. - $("#log-modules").oninput = e => { + $("#log-modules").oninput = () => { dropdown.value = "custom"; }; } @@ -534,7 +534,7 @@ function updateLogFile(file) { if (file.exists()) { openLogFileButton.disabled = false; - openLogFileButton.onclick = function (e) { + openLogFileButton.onclick = function () { file.reveal(); }; } diff --git a/toolkit/content/aboutNetError.mjs b/toolkit/content/aboutNetError.mjs index 554553fd62..1c733d5dbb 100644 --- a/toolkit/content/aboutNetError.mjs +++ b/toolkit/content/aboutNetError.mjs @@ -498,7 +498,7 @@ function initPage() { trrExceptionButton.addEventListener("click", () => { RPMSendQuery("Browser:AddTRRExcludedDomain", { hostname: HOST_NAME, - }).then(msg => { + }).then(() => { retryThis(trrExceptionButton); }); }); @@ -1061,15 +1061,15 @@ function addCertException() { () => { location.reload(); }, - err => {} + () => {} ); } -function onReturnButtonClick(e) { +function onReturnButtonClick() { RPMSendAsyncMessage("Browser:SSLErrorGoBack"); } -function copyPEMToClipboard(e) { +function copyPEMToClipboard() { const errorText = document.getElementById("certificateErrorText"); navigator.clipboard.writeText(errorText.textContent); } diff --git a/toolkit/content/aboutProfiles.js b/toolkit/content/aboutProfiles.js index 15c0419a11..38dc86c31f 100644 --- a/toolkit/content/aboutProfiles.js +++ b/toolkit/content/aboutProfiles.js @@ -139,7 +139,7 @@ function display(profileData) { td.appendChild(button); - button.addEventListener("click", function (e) { + button.addEventListener("click", function () { value.reveal(); }); } diff --git a/toolkit/content/aboutSupport.js b/toolkit/content/aboutSupport.js index f9f35e7e76..6b75776bd6 100644 --- a/toolkit/content/aboutSupport.js +++ b/toolkit/content/aboutSupport.js @@ -20,7 +20,7 @@ ChromeUtils.defineESModuleGetters(this, { ProcessType: "resource://gre/modules/ProcessType.sys.mjs", }); -window.addEventListener("load", function onload(event) { +window.addEventListener("load", function onload() { try { window.removeEventListener("load", onload); Troubleshoot.snapshot().then(async snapshot => { @@ -1051,7 +1051,7 @@ var snapshotFormatters = { } let button = $("enumerate-database-button"); if (button) { - button.addEventListener("click", function (event) { + button.addEventListener("click", function () { let { KeyValueService } = ChromeUtils.importESModule( "resource://gre/modules/kvstore.sys.mjs" ); @@ -1074,7 +1074,7 @@ var snapshotFormatters = { $("enumerate-database-result").textContent += logs.join("\n") + "\n"; }) - .catch(err => { + .catch(() => { $("enumerate-database-result").textContent += `${name}:\n`; }); } @@ -1105,7 +1105,7 @@ var snapshotFormatters = { 'th[data-l10n-id="roundtrip-latency"]' ).nextSibling.textContent = latencyString; }) - .catch(e => {}); + .catch(() => {}); } function createCDMInfoRow(cdmInfo) { @@ -1369,6 +1369,17 @@ var snapshotFormatters = { $("remote-debugging-url").textContent = data.url; }, + contentAnalysis(data) { + $("content-analysis-active").textContent = data.active; + if (data.active) { + $("content-analysis-connected-to-agent").textContent = data.connected; + $("content-analysis-agent-path").textContent = data.agentPath; + $("content-analysis-agent-failed-signature-verification").textContent = + data.failedSignatureVerification; + $("content-analysis-request-count").textContent = data.requestCount; + } + }, + accessibility(data) { $("a11y-activated").textContent = data.isActive; $("a11y-force-disabled").textContent = data.forceDisabled || 0; @@ -1606,7 +1617,7 @@ function sortedArrayFromObject(obj) { for (let prop in obj) { tuples.push([prop, obj[prop]]); } - tuples.sort(([prop1, v1], [prop2, v2]) => prop1.localeCompare(prop2)); + tuples.sort(([prop1], [prop2]) => prop1.localeCompare(prop2)); return tuples; } @@ -1757,7 +1768,7 @@ Serializer.prototype = { } }, - _startNewLine(lines) { + _startNewLine() { let currLine = this._currentLine; if (currLine) { // The current line is not empty. Trim it. @@ -1770,7 +1781,7 @@ Serializer.prototype = { this._lines.push(""); }, - _appendText(text, lines) { + _appendText(text) { this._currentLine += text; }, @@ -1926,13 +1937,13 @@ function safeModeRestart() { function setupEventListeners() { let button = $("reset-box-button"); if (button) { - button.addEventListener("click", function (event) { + button.addEventListener("click", function () { ResetProfile.openConfirmationDialog(window); }); } button = $("clear-startup-cache-button"); if (button) { - button.addEventListener("click", async function (event) { + button.addEventListener("click", async function () { const [promptTitle, promptBody, restartButtonLabel] = await document.l10n.formatValues([ { id: "startup-cache-dialog-title2" }, @@ -1965,7 +1976,7 @@ function setupEventListeners() { } button = $("restart-in-safe-mode-button"); if (button) { - button.addEventListener("click", function (event) { + button.addEventListener("click", function () { if ( Services.obs .enumerateObservers("restart-in-safe-mode") @@ -1983,7 +1994,7 @@ function setupEventListeners() { if (AppConstants.MOZ_UPDATER) { button = $("update-dir-button"); if (button) { - button.addEventListener("click", function (event) { + button.addEventListener("click", function () { // Get the update directory. let updateDir = Services.dirsvc.get("UpdRootD", Ci.nsIFile); if (!updateDir.exists()) { @@ -2001,7 +2012,7 @@ function setupEventListeners() { } button = $("show-update-history-button"); if (button) { - button.addEventListener("click", function (event) { + button.addEventListener("click", function () { window.browsingContext.topChromeWindow.openDialog( "chrome://mozapps/content/update/history.xhtml", "Update:History", @@ -2012,7 +2023,7 @@ function setupEventListeners() { } button = $("verify-place-integrity-button"); if (button) { - button.addEventListener("click", function (event) { + button.addEventListener("click", function () { PlacesDBUtils.checkAndFixDatabase().then(tasksStatusMap => { let logs = []; for (let [key, value] of tasksStatusMap) { @@ -2027,13 +2038,13 @@ function setupEventListeners() { }); } - $("copy-raw-data-to-clipboard").addEventListener("click", function (event) { + $("copy-raw-data-to-clipboard").addEventListener("click", function () { copyRawDataToClipboard(this); }); - $("copy-to-clipboard").addEventListener("click", function (event) { + $("copy-to-clipboard").addEventListener("click", function () { copyContentsToClipboard(); }); - $("profile-dir-button").addEventListener("click", function (event) { + $("profile-dir-button").addEventListener("click", function () { openProfileDirectory(); }); } diff --git a/toolkit/content/aboutSupport.xhtml b/toolkit/content/aboutSupport.xhtml index d19fb64d56..f815ab77f4 100644 --- a/toolkit/content/aboutSupport.xhtml +++ b/toolkit/content/aboutSupport.xhtml @@ -902,6 +902,38 @@ </table> #endif +#ifndef ANDROID + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section" id="content-analysis" data-l10n-id="content-analysis-title"/> + + <table> + <tbody> + <tr> + <th class="column" data-l10n-id="content-analysis-active"/> + <td id="content-analysis-active"/> + </tr> + <tr> + <th class="column" data-l10n-id="content-analysis-connected-to-agent"/> + <td id="content-analysis-connected-to-agent"/> + </tr> + <tr> + <th class="column" data-l10n-id="content-analysis-agent-path"/> + <td id="content-analysis-agent-path"/> + </tr> + <tr> + <th class="column" data-l10n-id="content-analysis-agent-failed-signature-verification"/> + <td id="content-analysis-agent-failed-signature-verification"/> + </tr> + <tr> + <th class="column" data-l10n-id="content-analysis-request-count"/> + <td id="content-analysis-request-count"/> + </tr> + </tbody> + </table> + +#endif + </div> </body> diff --git a/toolkit/content/aboutTelemetry.js b/toolkit/content/aboutTelemetry.js index b25c2687d9..45a8488820 100644 --- a/toolkit/content/aboutTelemetry.js +++ b/toolkit/content/aboutTelemetry.js @@ -257,7 +257,7 @@ var PingPicker = { "mouseleave", () => (pingPickerNeedHide = true) ); - document.addEventListener("click", ev => { + document.addEventListener("click", () => { if (pingPickerNeedHide) { pingPicker.classList.add("hidden"); } @@ -966,7 +966,7 @@ var RawPayloadData = { attachObservers() { document .getElementById("payload-json-viewer") - .addEventListener("click", e => { + .addEventListener("click", () => { openJsonInFirefoxJsonViewer(JSON.stringify(gPingData.payload, null, 2)); }); }, @@ -1121,7 +1121,7 @@ var Histogram = { return outerDiv; }, - processHistogram(aHgram, aName) { + processHistogram(aHgram) { const values = Object.keys(aHgram.values).map(k => aHgram.values[k]); if (!values.length) { // If we have no values collected for this histogram, just return diff --git a/toolkit/content/aboutUrlClassifier.js b/toolkit/content/aboutUrlClassifier.js index 14b34c7e89..cc88e3bf73 100644 --- a/toolkit/content/aboutUrlClassifier.js +++ b/toolkit/content/aboutUrlClassifier.js @@ -429,7 +429,7 @@ var Cache = { createCacheEntries() { function createRow(tds, body, cols) { let tr = document.createElement("tr"); - tds.forEach(function (v, i, a) { + tds.forEach(function (v, i) { let td = document.createElement("td"); if (i == 0 && tds.length != cols) { td.setAttribute("colspan", cols - tds.length + 1); diff --git a/toolkit/content/aboutwebrtc/aboutWebrtc.mjs b/toolkit/content/aboutwebrtc/aboutWebrtc.mjs index 7e2c92a4bd..5aa8119c5a 100644 --- a/toolkit/content/aboutwebrtc/aboutWebrtc.mjs +++ b/toolkit/content/aboutwebrtc/aboutWebrtc.mjs @@ -53,7 +53,7 @@ class Renderer { // adding elements more readable, e.g. elemRenderer.elem_h4(...) instead of // elemRenderer.elem("h4", ...). const elemRenderer = new Proxy(new Renderer(), { - get(target, prop, receiver) { + get(target, prop) { // Function prefixes to proxy. const proxied = { elem_: (...args) => target.elem(...args), @@ -502,7 +502,7 @@ async function renderPeerConnectionSection() { return body; }, // Creates the filling for the disclosure - updateFn: async section => { + updateFn: async () => { let statsReports = await getStats(needsFullUpdate); needsFullUpdate = REQUEST_UPDATE_ONLY_REFRESH; @@ -1828,7 +1828,7 @@ class PrimarySection { disclosureHideL10nId, autoRefreshPref, renderFn = async () => {}, // Creates the filling for the disclosure - updateFn = async section => {}, // Updates the contents. + updateFn = async () => {}, // Updates the contents. headerElementsFn = async () => [], // Accessory elements for the heading }) { const newSect = new PrimarySection(); @@ -1937,7 +1937,7 @@ async function renderMediaCtx(rndr) { hasH264Hardware.dataset.value = ctx.hasH264Hardware; const renderFn = async () => rndr.elems_div({}, [hasH264Hardware, rndr.elem_hr(), confList.view()]); - const updateFn = async section => { + const updateFn = async () => { const newCtx = WGI.getMediaContext(); if (hasH264Hardware.dataset.value != newCtx.hasH264Hardware) { hasH264Hardware.dataset.value = newCtx.hasH264Hardware; diff --git a/toolkit/content/aboutwebrtc/graph.mjs b/toolkit/content/aboutwebrtc/graph.mjs index f2c93f0709..002b3c9d28 100644 --- a/toolkit/content/aboutwebrtc/graph.mjs +++ b/toolkit/content/aboutwebrtc/graph.mjs @@ -35,14 +35,14 @@ class GraphImpl { // The color to use for average graph lines and text averageLineColor = () => "green"; // The color to use for the max value - maxColor = ({ time, value }) => "grey"; + maxColor = () => "grey"; // The color to use for the min value - minColor = ({ time, value }) => "grey"; + minColor = () => "grey"; // Title color - titleColor = title => compStyle("--in-content-page-color"); + titleColor = () => compStyle("--in-content-page-color"); // The color to use for a data point at a time. // The destination x coordinate and graph width are also provided. - datumColor = ({ time, value, x, width }) => "red"; + datumColor = () => "red"; // Returns an SVG element that needs to be inserted into the DOM for display drawSparseValues = (dataSet, title, config) => { diff --git a/toolkit/content/contentAreaUtils.js b/toolkit/content/contentAreaUtils.js index 983fd9890d..0e7d072e6b 100644 --- a/toolkit/content/contentAreaUtils.js +++ b/toolkit/content/contentAreaUtils.js @@ -1180,10 +1180,10 @@ function openURL(aURL) { var appstartup = Services.startup; var loadListener = { - onStartRequest: function ll_start(aRequest) { + onStartRequest: function ll_start() { appstartup.enterLastWindowClosingSurvivalArea(); }, - onStopRequest: function ll_stop(aRequest, aStatusCode) { + onStopRequest: function ll_stop() { appstartup.exitLastWindowClosingSurvivalArea(); }, QueryInterface: ChromeUtils.generateQI([ @@ -1194,13 +1194,13 @@ function openURL(aURL) { loadgroup.groupObserver = loadListener; var uriListener = { - doContent(ctype, preferred, request, handler) { + doContent() { return false; }, - isPreferred(ctype, desired) { + isPreferred() { return false; }, - canHandleContent(ctype, preferred, desired) { + canHandleContent() { return false; }, loadCookie: null, diff --git a/toolkit/content/customElements.js b/toolkit/content/customElements.js index b0a8f33fe6..ef58963a02 100644 --- a/toolkit/content/customElements.js +++ b/toolkit/content/customElements.js @@ -732,7 +732,7 @@ } get label() { - return this.getAttribute("label"); + return this.getAttribute("label") || ""; } set image(val) { @@ -762,9 +762,7 @@ } get accessKey() { - return this.labelElement - ? this.labelElement.accessKey - : this.getAttribute("accesskey"); + return this.labelElement?.accessKey || this.getAttribute("accesskey"); } }; MozElements.BaseTextMixin = BaseTextMixin; diff --git a/toolkit/content/gmp-sources/openh264.json b/toolkit/content/gmp-sources/openh264.json index 9e6e9b875c..0f857866d7 100644 --- a/toolkit/content/gmp-sources/openh264.json +++ b/toolkit/content/gmp-sources/openh264.json @@ -1,54 +1,47 @@ { + "hashFunction": "sha512", + "name": "OpenH264-2.3.2", + "schema_version": 1000, "vendors": { "gmp-gmpopenh264": { "platforms": { - "Android_aarch64-gcc3": { - "fileUrl": "http://ciscobinary.openh264.org/openh264-android-aarch64-42954cf0fe8a2bdc97fdc180462a3eaefceb035f.zip", - "filesize": 557884, - "hashValue": "307d188876f3612a9168c0b4ed191db2132f2e3193bdd3024ce50adcb9c1e085ab43008531a25e93d570a377283336cda9bcd7609ee6b702c5292f12d20b616b" - }, - "Android_arm-eabi-gcc3": { - "fileUrl": "http://ciscobinary.openh264.org/openh264-android-arm-42954cf0fe8a2bdc97fdc180462a3eaefceb035f.zip", - "filesize": 539311, - "hashValue": "f4f0bfe333b7e0cd0453e787dc3c15bebe9cc771cb3e57540d53f0ac9a37eee4ea8559a45a51824ee4d706ee0b3d80b2d331468a0aa533cd958081f23ee0aaae" - }, - "Android_x86-gcc3": { - "fileUrl": "http://ciscobinary.openh264.org/openh264-android-x86-42954cf0fe8a2bdc97fdc180462a3eaefceb035f.zip", - "filesize": 589947, - "hashValue": "eb7a1c9c2d29a2fd12dfe82d0f575f1d855478640816a7fb9402ce82c65878ffc5aa3d5f8bb46cd01231005c37d86984d7a631cfe45c7d56a6d4dabc427b15a0" - }, "Darwin_aarch64-gcc3": { - "fileUrl": "http://ciscobinary.openh264.org/openh264-macosx64-aarch64-2e1774ab6dc6c43debb0b5b628bdf122a391d521-2.zip", - "filesize": 395414, - "hashValue": "d0905cd3c23541f67f9ff29ce392afdb7a5dd111906e1b5fa8fad4743b227acd7bd83d50e13ed029118a764983cf9e0a24c5321ecb9e5b88740dfbefcf34864c" + "fileUrl": "http://ciscobinary.openh264.org/openh264-macosx64-aarch64-31c4d2e4a037526fd30d4e5c39f60885986cf865.zip", + "filesize": 477938, + "hashValue": "391efb184373d533713a9e99a9e63c3bbaf614e8d8bdfdd84d4d5e53b9a737e75032187309dd00e58b58bb1033ab68d199f994744f6add57dd08f5fbb654d2f3" }, "Darwin_x86_64-gcc3": { - "fileUrl": "http://ciscobinary.openh264.org/openh264-macosx64-2e1774ab6dc6c43debb0b5b628bdf122a391d521-2.zip", - "filesize": 488639, - "hashValue": "d847b153f8ef2b4b095fbaf9f64b6d08658720ca1e4dc7288622c56d00858d038fe0fd07bc1efb0afc8f02dbd07818416ecc2555db8ed1199872b0d165f4eb62" + "fileUrl": "http://ciscobinary.openh264.org/openh264-macosx64-31c4d2e4a037526fd30d4e5c39f60885986cf865.zip", + "filesize": 552074, + "hashValue": "c649bfa20c48406ccbae1917d7478773cd5250ef995828b58cc56cc4db0a3c7ce3f89eb187bd1b3d26cda0e5e65322b710cfd66a2953adeea0e4361c51488add" + }, + "Linux_aarch64-gcc3": { + "fileUrl": "http://ciscobinary.openh264.org/openh264-linux64-aarch64-31c4d2e4a037526fd30d4e5c39f60885986cf865.zip", + "filesize": 537296, + "hashValue": "eab3fca253c10739c4930bdaf83ca7a9a0a3580937e0e945dde3a45bcdb39b6240ae0d7133ebfb10029442322f4ce6e8647917d15afea601351dc8a5f0dbf3ec" }, "Linux_x86-gcc3": { - "fileUrl": "http://ciscobinary.openh264.org/openh264-linux32-2e1774ab6dc6c43debb0b5b628bdf122a391d521.zip", - "filesize": 527704, - "hashValue": "903aecd631624db3047fc477363ac076794517bbc72b33a88a73627066b5997d9c1194975729ef2acbabba19e93574333b54e32763a5a834b8d9431b99181fd1" + "fileUrl": "http://ciscobinary.openh264.org/openh264-linux32-31c4d2e4a037526fd30d4e5c39f60885986cf865.zip", + "filesize": 623151, + "hashValue": "d80508260c6419acc09c9fde539b18e9231899786df549bc8c86e564d1fb8c500327adb96bba204a386326dd5213ae907108c9ca4fc64ae385fbb2291e0e8cc5" }, "Linux_x86_64-gcc3": { - "fileUrl": "http://ciscobinary.openh264.org/openh264-linux64-2e1774ab6dc6c43debb0b5b628bdf122a391d521.zip", - "filesize": 511815, - "hashValue": "94531e267314de661b2205c606283fb066d781e5c11027578f2a3c3aa353437c2289544074a28101b6b6f0179f0fe6bd890a0ae2bb6e1cf9053650472576366c" + "fileUrl": "http://ciscobinary.openh264.org/openh264-linux64-31c4d2e4a037526fd30d4e5c39f60885986cf865.zip", + "filesize": 583674, + "hashValue": "53a58bfb4c8124ad4f7655b99bfdea290033a085e0796b19245b33b91c0948fdac9f0c3e817130b352493a65d9a7a0fc8a7c1eedc618cdaa2b4580734a11cd9c" }, "Linux_x86_64-gcc3-asan": { "alias": "Linux_x86_64-gcc3" }, "WINNT_aarch64-msvc-aarch64": { - "fileUrl": "http://ciscobinary.openh264.org/openh264-win64-aarch64-2e1774ab6dc6c43debb0b5b628bdf122a391d521.zip", - "filesize": 558607, - "hashValue": "8d936bca08dcf3538c5c118c0f468d672c556ac2ac828a4b9d1fcbb4339885d17ebcc748a918457abbea87d21c5cab2c007ca5b4ef87f04a52d44f42ee5fdbb9" + "fileUrl": "http://ciscobinary.openh264.org/openh264-win64-aarch64-31c4d2e4a037526fd30d4e5c39f60885986cf865.zip", + "filesize": 412806, + "hashValue": "cc3306873d6ad8ea6a27096c8ce75831463633a530d8f1794ae3eba3efd4a572459b6b0f3ac73f46552a7d6c7a29b152e299d862a2f76420eee07d1d39979182" }, "WINNT_x86-msvc": { - "fileUrl": "http://ciscobinary.openh264.org/openh264-win32-2e1774ab6dc6c43debb0b5b628bdf122a391d521.zip", - "filesize": 491261, - "hashValue": "9ed5b4c27c2c159b83a1b887a1215d0472171cff422d2bc1962312f90e62d1b212955fe68bc88f826d613c9fb58b86f6fa16ebc1533e863f6a5648dcb1319bcb" + "fileUrl": "http://ciscobinary.openh264.org/openh264-win32-31c4d2e4a037526fd30d4e5c39f60885986cf865.zip", + "filesize": 472465, + "hashValue": "b5c8290bbee9503b6f9d2fcc3cbcf94f9b2d4e8ee143e37cfc8c68de593c8189aecfe447b811accecdb564c8788bd05e116f4eda62ac29b3c6b8a6fa5f564ee0" }, "WINNT_x86-msvc-x64": { "alias": "WINNT_x86-msvc" @@ -57,26 +50,18 @@ "alias": "WINNT_x86-msvc" }, "WINNT_x86_64-msvc": { - "fileUrl": "http://ciscobinary.openh264.org/openh264-win64-2e1774ab6dc6c43debb0b5b628bdf122a391d521.zip", - "filesize": 453023, - "hashValue": "06511f1f6c6d44d076b3c593528c26a602348d9c41689dbf5ff716b671c3ca5756b12cb2e5869f836dedce27b1a5cfe79b93c707fd01f8e84b620923bb61b5f1" + "fileUrl": "http://ciscobinary.openh264.org/openh264-win64-31c4d2e4a037526fd30d4e5c39f60885986cf865.zip", + "filesize": 491284, + "hashValue": "b667086ed49579592d435df2b486fe30ba1b62ddd169f19e700cd079239747dd3e20058c285fa9c10a533e34f22b5198ed9b1f92ae560a3067f3e3feacc724f1" }, "WINNT_x86_64-msvc-x64": { "alias": "WINNT_x86_64-msvc" }, "WINNT_x86_64-msvc-x64-asan": { "alias": "WINNT_x86_64-msvc" - }, - "android-x86_64": { - "fileUrl": "http://ciscobinary.openh264.org/openh264-android-x86_64-42954cf0fe8a2bdc97fdc180462a3eaefceb035f.zip", - "filesize": 539311, - "hashValue": "2c80df83c84841477cf5489e4109a0913cf3ca801063d788e100a511c9226d46059e4d28ea76496c3208c046cc44c5ce0b5263b1bfda5b731f8461ce8ce7d1b7" } }, - "version": "1.8.1.2" + "version": "2.3.2" } - }, - "hashFunction": "sha512", - "name": "OpenH264-1.8.1.2", - "schema_version": 1000 + } } diff --git a/toolkit/content/license.html b/toolkit/content/license.html index e9d2642354..fe489f74d1 100644 --- a/toolkit/content/license.html +++ b/toolkit/content/license.html @@ -1682,7 +1682,15 @@ licences. <h1><a id="apple"></a>Apple License</h1> - <p>This license applies to certain files in the directories <code>dom/media/webaudio/blink</code>, and <code>widget/cocoa</code>.</p> + <p>This license applies to parts of the code in:</p> + <ul> + <li><code>xpcom/base/DarwinObjectPtr.h</code></li> + </ul> + <p>and also some files in these directories:</p> + <ul> + <li><code>dom/media/webaudio/blink</code></li> + <li><code>widget/cocoa</code></li> + </ul> <pre> Copyright (C) 2008, 2009 Apple Inc. All rights reserved. @@ -1897,6 +1905,7 @@ into source code and to files in the following directories: <li><code>third_party/jpeg-xl/</code></li> #endif <li><code>third_party/xsimd/</code></li> + <li><code>third_party/zstd/</code></li> </ul> See the individual LICENSE files for copyright owners.</p> @@ -5147,7 +5156,6 @@ ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation a <ul> <li><code>security/nss/lib/dbm</code></li> - <li><code>xpcom/ds/nsQuickSort.cpp</code></li> <li><code>nsprpub/pr/src/misc/praton.c</code></li> <li><code>dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs.c</code></li> </ul> diff --git a/toolkit/content/tests/browser/browser_about_logging.js b/toolkit/content/tests/browser/browser_about_logging.js index fdf8eab57b..d9a2459c12 100644 --- a/toolkit/content/tests/browser/browser_about_logging.js +++ b/toolkit/content/tests/browser/browser_about_logging.js @@ -201,7 +201,7 @@ add_task(async function testURLParameters() { url: PAGE + "?invalid-param", }, async browser => { - await SpecialPowers.spawn(browser, [profilerPresetInURL], async inURL => { + await SpecialPowers.spawn(browser, [profilerPresetInURL], async () => { let $ = content.document.querySelector.bind(content.document); Assert.ok( !$("#error").hidden, @@ -368,7 +368,7 @@ add_task(async function testProfilerOpens() { "https://example.com/", false ); - SpecialPowers.spawn(browser, [], async savedLogModules => { + SpecialPowers.spawn(browser, [], async () => { let $ = content.document.querySelector.bind(content.document); // Override the URL the profiler uses to avoid hitting external // resources (and crash). diff --git a/toolkit/content/tests/browser/browser_bug982298.js b/toolkit/content/tests/browser/browser_bug982298.js index ffbc916a5e..9b6f636db3 100644 --- a/toolkit/content/tests/browser/browser_bug982298.js +++ b/toolkit/content/tests/browser/browser_bug982298.js @@ -50,7 +50,7 @@ add_task(async function () { let awaitFindResult2 = new Promise(resolve => { let listener = { - onFindResult(aData) { + onFindResult() { info("got find result #2"); browser.finder.removeResultListener(listener); resolve(); diff --git a/toolkit/content/tests/browser/browser_f7_caret_browsing.js b/toolkit/content/tests/browser/browser_f7_caret_browsing.js index be6ae7d1f7..c0413c459b 100644 --- a/toolkit/content/tests/browser/browser_f7_caret_browsing.js +++ b/toolkit/content/tests/browser/browser_f7_caret_browsing.js @@ -37,7 +37,7 @@ registerCleanupFunction(function () { let gCaretPromptOpeningObserver; function promiseCaretPromptOpened() { return new Promise(resolve => { - function observer(subject, topic, data) { + function observer(subject) { info("Dialog opened."); resolve(subject); gCaretPromptOpeningObserver(); diff --git a/toolkit/content/tests/browser/browser_isSynthetic.js b/toolkit/content/tests/browser/browser_isSynthetic.js index 21b22fe171..3473dfc915 100644 --- a/toolkit/content/tests/browser/browser_isSynthetic.js +++ b/toolkit/content/tests/browser/browser_isSynthetic.js @@ -11,7 +11,7 @@ LocationChangeListener.prototype = { this.browser.removeProgressListener(this); }, - onLocationChange(webProgress, request, location, flags) { + onLocationChange() { this.wasSynthetic = this.browser.isSyntheticDocument; }, diff --git a/toolkit/content/tests/browser/browser_media_wakelock_webaudio.js b/toolkit/content/tests/browser/browser_media_wakelock_webaudio.js index 7c40b5fe1a..9e18cc89f3 100644 --- a/toolkit/content/tests/browser/browser_media_wakelock_webaudio.js +++ b/toolkit/content/tests/browser/browser_media_wakelock_webaudio.js @@ -56,7 +56,6 @@ async function checkWakelockWhenChangeTabVisibility({ description, additionalParams, needLock, - elementIdForEnteringPIPMode, }) { const originalTab = gBrowser.selectedTab; info(`start a new tab for '${description}'`); diff --git a/toolkit/content/tests/browser/browser_save_folder_standalone_image.js b/toolkit/content/tests/browser/browser_save_folder_standalone_image.js index 073e71a88b..6bcc95ae65 100644 --- a/toolkit/content/tests/browser/browser_save_folder_standalone_image.js +++ b/toolkit/content/tests/browser/browser_save_folder_standalone_image.js @@ -49,7 +49,7 @@ add_task(async function () { const IMAGE_URL = "http://mochi.test:8888/browser/toolkit/content/tests/browser/doggy.png"; - await BrowserTestUtils.withNewTab(IMAGE_URL, async function (browser) { + await BrowserTestUtils.withNewTab(IMAGE_URL, async function () { let tmpDir = FileUtils.getDir("TmpD", []); let dir = newDirectory(); let downloadLastDir = new DownloadLastDir(null); diff --git a/toolkit/content/tests/browser/browser_save_resend_postdata.js b/toolkit/content/tests/browser/browser_save_resend_postdata.js index 3f3e729dab..b028cac4ae 100644 --- a/toolkit/content/tests/browser/browser_save_resend_postdata.js +++ b/toolkit/content/tests/browser/browser_save_resend_postdata.js @@ -59,7 +59,7 @@ function test() { var file = destDir.clone(); file.append("no_default_file_name"); MockFilePicker.setFiles([file]); - MockFilePicker.showCallback = function (fp) { + MockFilePicker.showCallback = function () { MockFilePicker.filterIndex = 1; // kSaveAsType_URL }; diff --git a/toolkit/content/tests/browser/common/mockTransfer.js b/toolkit/content/tests/browser/common/mockTransfer.js index f4afa44903..0240d9c14b 100644 --- a/toolkit/content/tests/browser/common/mockTransfer.js +++ b/toolkit/content/tests/browser/common/mockTransfer.js @@ -51,8 +51,7 @@ MockTransfer.prototype = { onStatusChange: function MTFC_onStatusChange( aWebProgress, aRequest, - aStatus, - aMessage + aStatus ) { // If at least one notification reported an error, the download failed. if (!Components.isSuccessCode(aStatus)) { diff --git a/toolkit/content/tests/browser/datetime/head.js b/toolkit/content/tests/browser/datetime/head.js index 46e2c78af5..532f7743b2 100644 --- a/toolkit/content/tests/browser/datetime/head.js +++ b/toolkit/content/tests/browser/datetime/head.js @@ -69,7 +69,7 @@ class DateTimeTestHelper { [selector], async selector => { let input = content.document.querySelector(selector); - await ContentTaskUtils.waitForEvent(input, "change", false, e => { + await ContentTaskUtils.waitForEvent(input, "change", false, () => { ok( content.window.windowUtils.isHandlingUserInput, "isHandlingUserInput should be true" diff --git a/toolkit/content/tests/browser/head.js b/toolkit/content/tests/browser/head.js index be15cd9684..03a239463d 100644 --- a/toolkit/content/tests/browser/head.js +++ b/toolkit/content/tests/browser/head.js @@ -177,7 +177,7 @@ function leave_icon(icon) { * Used to listen events if you just need it once */ function once(target, name) { - var p = new Promise(function (resolve, reject) { + var p = new Promise(function (resolve) { target.addEventListener( name, function () { diff --git a/toolkit/content/tests/chrome/bug263683_window.xhtml b/toolkit/content/tests/chrome/bug263683_window.xhtml index b124361fd7..74846e3c27 100644 --- a/toolkit/content/tests/chrome/bug263683_window.xhtml +++ b/toolkit/content/tests/chrome/bug263683_window.xhtml @@ -176,7 +176,7 @@ const matches = JSON.parse(gFindBar._foundMatches.dataset.l10nArgs); is(matches.total, 2, "Found correct amount of matches") - await SpecialPowers.spawn(gBrowser, [], async function(args) { + await SpecialPowers.spawn(gBrowser, [], async function() { function getSelection(docShell) { let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsISelectionDisplay) diff --git a/toolkit/content/tests/chrome/bug366992_window.xhtml b/toolkit/content/tests/chrome/bug366992_window.xhtml index 698d26b43a..ee595706f5 100644 --- a/toolkit/content/tests/chrome/bug366992_window.xhtml +++ b/toolkit/content/tests/chrome/bug366992_window.xhtml @@ -46,7 +46,7 @@ isCommandEnabled(aCommand) { return aCommand == "cmd_delete"; }, - doCommand(aCommand) { } + doCommand() { } } function ok(condition, message) { diff --git a/toolkit/content/tests/chrome/chrome.toml b/toolkit/content/tests/chrome/chrome.toml index 3391a2923d..986822ac48 100644 --- a/toolkit/content/tests/chrome/chrome.toml +++ b/toolkit/content/tests/chrome/chrome.toml @@ -226,6 +226,8 @@ support-files = [ ["test_menulist_initial_selection.xhtml"] +["test_menulist_initial_selection_with_connected_callback.xhtml"] + ["test_menulist_keynav.xhtml"] ["test_menulist_null_value.xhtml"] diff --git a/toolkit/content/tests/chrome/file_editor_with_autocomplete.js b/toolkit/content/tests/chrome/file_editor_with_autocomplete.js index 78611efc70..acf2c9e9df 100644 --- a/toolkit/content/tests/chrome/file_editor_with_autocomplete.js +++ b/toolkit/content/tests/chrome/file_editor_with_autocomplete.js @@ -195,7 +195,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text exactly matches the case: type 'Mo'", completeDefaultIndex: false, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("M", { shiftKey: true }, aWindow); synthesizeKey("o", {}, aWindow); return true; @@ -212,7 +212,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text exactly matches the case: select 'Mozilla' to complete the word", completeDefaultIndex: false, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("KEY_ArrowDown", {}, aWindow); synthesizeKey("KEY_Enter", {}, aWindow); return true; @@ -226,7 +226,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text exactly matches the case: undo the word, but typed text shouldn't be canceled", completeDefaultIndex: false, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("z", { accelKey: true }, aWindow); return true; }, @@ -239,7 +239,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text exactly matches the case: undo the typed text", completeDefaultIndex: false, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("z", { accelKey: true }, aWindow); return true; }, @@ -252,7 +252,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text exactly matches the case: redo the typed text", completeDefaultIndex: false, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow); return true; }, @@ -265,7 +265,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text exactly matches the case: redo the word", completeDefaultIndex: false, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow); return true; }, @@ -278,7 +278,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text exactly matches the case: removing all text for next test...", completeDefaultIndex: false, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("a", { accelKey: true }, aWindow); synthesizeKey("KEY_Backspace", {}, aWindow); return true; @@ -293,7 +293,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text does not match the case: type 'mo'", completeDefaultIndex: false, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("m", {}, aWindow); synthesizeKey("o", {}, aWindow); return true; @@ -310,7 +310,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text does not match the case: select 'Mozilla' to complete the word", completeDefaultIndex: false, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("KEY_ArrowDown", {}, aWindow); synthesizeKey("KEY_Enter", {}, aWindow); return true; @@ -324,7 +324,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text does not match the case: undo the word, but typed text shouldn't be canceled", completeDefaultIndex: false, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("z", { accelKey: true }, aWindow); return true; }, @@ -337,7 +337,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text does not match the case: undo the typed text", completeDefaultIndex: false, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("z", { accelKey: true }, aWindow); return true; }, @@ -350,7 +350,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text does not match the case: redo the typed text", completeDefaultIndex: false, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow); return true; }, @@ -363,7 +363,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text does not match the case: redo the word", completeDefaultIndex: false, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow); return true; }, @@ -376,7 +376,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text does not match the case: removing all text for next test...", completeDefaultIndex: false, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("a", { accelKey: true }, aWindow); synthesizeKey("KEY_Backspace", {}, aWindow); return true; @@ -392,7 +392,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): type 'Mo'", completeDefaultIndex: true, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("M", { shiftKey: true }, aWindow); synthesizeKey("o", {}, aWindow); return true; @@ -410,7 +410,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): select 'Mozilla' to complete the word", completeDefaultIndex: true, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("KEY_ArrowDown", {}, aWindow); synthesizeKey("KEY_Enter", {}, aWindow); return true; @@ -424,7 +424,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): undo the word, but typed text shouldn't be canceled", completeDefaultIndex: true, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("z", { accelKey: true }, aWindow); return true; }, @@ -437,7 +437,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): undo the typed text", completeDefaultIndex: true, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("z", { accelKey: true }, aWindow); return true; }, @@ -450,7 +450,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): redo the typed text", completeDefaultIndex: true, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow); return true; }, @@ -466,7 +466,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): redo the word", completeDefaultIndex: true, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow); return true; }, @@ -479,7 +479,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): removing all text for next test...", completeDefaultIndex: true, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("a", { accelKey: true }, aWindow); synthesizeKey("KEY_Backspace", {}, aWindow); return true; @@ -494,7 +494,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): type 'mo'", completeDefaultIndex: true, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("m", {}, aWindow); synthesizeKey("o", {}, aWindow); return true; @@ -512,7 +512,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): select 'Mozilla' to complete the word", completeDefaultIndex: true, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("KEY_ArrowDown", {}, aWindow); synthesizeKey("KEY_Enter", {}, aWindow); return true; @@ -528,7 +528,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): undo the selected word, but typed text shouldn't be canceled", completeDefaultIndex: true, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("z", { accelKey: true }, aWindow); return true; }, @@ -541,7 +541,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): undo the word, but typed text shouldn't be canceled", completeDefaultIndex: true, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("z", { accelKey: true }, aWindow); return true; }, @@ -554,7 +554,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): undo the typed text", completeDefaultIndex: true, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("z", { accelKey: true }, aWindow); return true; }, @@ -571,7 +571,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): redo the typed text", completeDefaultIndex: true, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow); return true; }, @@ -587,7 +587,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): redo the default index word", completeDefaultIndex: true, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow); return true; }, @@ -600,7 +600,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): redo the word", completeDefaultIndex: true, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow); return true; }, @@ -613,7 +613,7 @@ nsDoTestsForEditorWithAutoComplete.prototype = { description: "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): removing all text for next test...", completeDefaultIndex: true, - execute(aWindow, aTarget) { + execute(aWindow) { synthesizeKey("a", { accelKey: true }, aWindow); synthesizeKey("KEY_Backspace", {}, aWindow); return true; diff --git a/toolkit/content/tests/chrome/popup_trigger.js b/toolkit/content/tests/chrome/popup_trigger.js index 003af044e5..71846a01ea 100644 --- a/toolkit/content/tests/chrome/popup_trigger.js +++ b/toolkit/content/tests/chrome/popup_trigger.js @@ -238,7 +238,7 @@ var popupTests = [ // rollup this way. // synthesizeMouse(gTrigger, 0, -12, { }); }, - result(testname, step) { + result(testname) { is(gMenuPopup.anchorNode, null, testname + " anchorNode"); is(gMenuPopup.triggerNode, null, testname + " triggerNode"); checkClosed("trigger", testname); @@ -532,7 +532,7 @@ var popupTests = [ // event to openPopup to check the trigger node. testname: "open popup anchored with override", events: ["popupshowing thepopup 0010", "popupshown thepopup"], - test(testname, step) { + test() { // attribute overrides the position passed in gMenuPopup.setAttribute("position", "end_after"); gExpectedTriggerNode = gCachedEvent.target; @@ -546,7 +546,7 @@ var popupTests = [ gCachedEvent ); }, - result(testname, step) { + result(testname) { gExpectedTriggerNode = null; is(gMenuPopup.anchorNode, gTrigger, testname + " anchorNode"); is( @@ -564,7 +564,7 @@ var popupTests = [ "popuphidden thepopup", "DOMMenuInactive thepopup", ], - test(testname, step) { + test(testname) { synthesizeKey("KEY_Escape"); checkClosed("trigger", testname); }, @@ -574,12 +574,12 @@ var popupTests = [ testname: "open popup anchored with offsets", events: ["popupshowing thepopup", "popupshown thepopup"], autohide: "thepopup", - test(testname, step) { + test() { // attribute is empty so does not override gMenuPopup.setAttribute("position", ""); gMenuPopup.openPopup(gTrigger, "before_start", 5, 10, true, true); }, - result(testname, step) { + result(testname) { compareEdge(gTrigger, gMenuPopup, "before_start", 5, 10, testname); }, }, @@ -588,10 +588,10 @@ var popupTests = [ // to the viewport. testname: "open popup unanchored", events: ["popupshowing thepopup", "popupshown thepopup"], - test(testname, step) { + test() { gMenuPopup.openPopup(null, "after_start", 6, 8, false); }, - result(testname, step) { + result(testname) { var rect = gMenuPopup.getBoundingClientRect(); ok( rect.left == 6 && rect.top == 8 && rect.right && rect.bottom, @@ -607,11 +607,11 @@ var popupTests = [ "popuphiding thepopup", "popuphidden thepopup", ], - test(testname, step) { + test() { var item3 = document.getElementById("item3"); synthesizeMouse(item3, 4, 4, {}); }, - result(testname, step) { + result(testname) { checkClosed("trigger", testname); }, }, @@ -625,18 +625,18 @@ var popupTests = [ "popuphidden thepopup", "DOMMenuInactive thepopup", ], - test(testname, step) { + test() { gMenuPopup.hidePopup(); }, }, { testname: "open popup at screen", events: ["popupshowing thepopup", "popupshown thepopup"], - test(testname, step) { + test() { gExpectedTriggerNode = "notset"; gMenuPopup.openPopupAtScreen(gScreenX + 24, gScreenY + 20, false); }, - result(testname, step) { + result(testname) { gExpectedTriggerNode = null; is(gMenuPopup.anchorNode, null, testname + " anchorNode"); is(gMenuPopup.triggerNode, null, testname + " triggerNode"); @@ -670,7 +670,7 @@ var popupTests = [ { testname: "open context popup at screen", events: ["popupshowing thepopup 0010", "popupshown thepopup"], - test(testname, step) { + test() { gExpectedTriggerNode = gCachedEvent.target; gMenuPopup.openPopupAtScreen( gScreenX + 8, @@ -679,7 +679,7 @@ var popupTests = [ gCachedEvent ); }, - result(testname, step) { + result(testname) { gExpectedTriggerNode = null; is(gMenuPopup.anchorNode, null, testname + " anchorNode"); is( @@ -873,7 +873,7 @@ var popupTests = [ testname: "open context popup at screen with all modifiers set", events: ["popupshowing thepopup 1111", "popupshown thepopup"], autohide: "thepopup", - test(testname, step) { + test() { gMenuPopup.openPopupAtScreen( gScreenX + 8, gScreenY + 16, @@ -885,10 +885,10 @@ var popupTests = [ { testname: "open popup with open property", events: ["popupshowing thepopup", "popupshown thepopup"], - test(testname, step) { + test() { openMenu(gTrigger); }, - result(testname, step) { + result(testname) { checkOpen("trigger", testname); if (gIsMenu) { compareEdge(gTrigger, gMenuPopup, "after_start", 0, 0, testname); @@ -902,10 +902,10 @@ var popupTests = [ "DOMMenuItemActive submenu", "popupshown submenupopup", ], - test(testname, step) { + test() { openMenu(document.getElementById("submenu")); }, - result(testname, step) { + result(testname) { checkOpen("trigger", testname); checkOpen("submenu", testname); // XXXndeakin @@ -929,17 +929,17 @@ var popupTests = [ test() { gMenuPopup.hidePopup(); }, - result(testname, step) { + result(testname) { checkClosed("trigger", testname); checkClosed("submenu", testname); }, }, { testname: "open submenu with open property without parent open", - test(testname, step) { + test() { openMenu(document.getElementById("submenu")); }, - result(testname, step) { + result(testname) { checkClosed("trigger", testname); checkClosed("submenu", testname); }, @@ -950,11 +950,11 @@ var popupTests = [ return gIsMenu; }, events: ["popupshowing thepopup", "popupshown thepopup"], - test(testname, step) { + test() { gMenuPopup.setAttribute("position", "before_start"); openMenu(gTrigger); }, - result(testname, step) { + result(testname) { compareEdge(gTrigger, gMenuPopup, "before_start", 0, 0, testname); }, }, @@ -968,10 +968,10 @@ var popupTests = [ "popuphidden thepopup", "DOMMenuInactive thepopup", ], - test(testname, step) { + test() { closeMenu(gTrigger, gMenuPopup); }, - result(testname, step) { + result(testname) { checkClosed("trigger", testname); }, }, @@ -982,13 +982,13 @@ var popupTests = [ }, events: ["popupshowing thepopup", "popupshown thepopup"], autohide: "thepopup", - test(testname, step) { + test() { gMenuPopup.setAttribute("position", "start_after"); gMenuPopup.setAttribute("popupanchor", "topright"); gMenuPopup.setAttribute("popupalign", "bottomright"); openMenu(gTrigger); }, - result(testname, step) { + result(testname) { compareEdge(gTrigger, gMenuPopup, "start_after", 0, 0, testname); }, }, @@ -999,13 +999,13 @@ var popupTests = [ }, events: ["popupshowing thepopup", "popupshown thepopup"], autohide: "thepopup", - test(testname, step) { + test() { gMenuPopup.removeAttribute("position"); gMenuPopup.setAttribute("popupanchor", "bottomright"); gMenuPopup.setAttribute("popupalign", "topright"); openMenu(gTrigger); }, - result(testname, step) { + result(testname) { compareEdge(gTrigger, gMenuPopup, "after_end", 0, 0, testname); gMenuPopup.removeAttribute("popupanchor"); gMenuPopup.removeAttribute("popupalign"); @@ -1018,11 +1018,11 @@ var popupTests = [ }, events: ["popupshowing thepopup", "popupshown thepopup"], autohide: "thepopup", - test(testname, step) { + test() { gTrigger.focus(); synthesizeKey("KEY_ArrowDown", { altKey: !platformIsMac() }); }, - result(testname, step) { + result(testname) { checkOpen("trigger", testname); checkActive(gMenuPopup, "", testname); }, @@ -1033,11 +1033,11 @@ var popupTests = [ return gIsMenu; }, events: ["popupshowing thepopup", "popupshown thepopup"], - test(testname, step) { + test() { gTrigger.focus(); synthesizeKey("KEY_ArrowUp", { altKey: !platformIsMac() }); }, - result(testname, step) { + result(testname) { checkOpen("trigger", testname); checkActive(gMenuPopup, "", testname); }, @@ -1055,11 +1055,11 @@ var popupTests = [ "popuphiding thepopup", "popuphidden thepopup", ], - test(testname, step) { + test() { synthesizeKey("KEY_ArrowDown"); synthesizeKey("KEY_Enter"); }, - result(testname, step) { + result(testname) { checkClosed("trigger", testname); }, }, @@ -1070,11 +1070,11 @@ var popupTests = [ }, events: ["popupshowing thepopup", "popupshown thepopup"], autohide: "thepopup", - test(testname, step) { + test() { gTrigger.focus(); synthesizeKey(platformIsMac() ? " " : "KEY_F4"); }, - result(testname, step) { + result(testname) { checkOpen("trigger", testname); checkActive(gMenuPopup, "", testname); }, @@ -1085,7 +1085,7 @@ var popupTests = [ condition() { return gIsMenu; }, - test(testname, step) { + test() { gTrigger.focus(); if (platformIsMac()) { synthesizeKey("KEY_F4", { altKey: true }); @@ -1093,7 +1093,7 @@ var popupTests = [ synthesizeKey("", { metaKey: true }); } }, - result(testname, step) { + result(testname) { checkClosed("trigger", testname); }, }, @@ -1102,11 +1102,11 @@ var popupTests = [ condition() { return gIsMenu; }, - test(testname, step) { + test() { gTrigger.setAttribute("disabled", "true"); synthesizeMouse(gTrigger, 4, 4, {}); }, - result(testname, step) { + result(testname) { checkClosed("trigger", testname); gTrigger.removeAttribute("disabled"); }, @@ -1116,11 +1116,11 @@ var popupTests = [ testname: "openPopup with object argument", events: ["popupshowing thepopup 0000", "popupshown thepopup"], autohide: "thepopup", - test(testname, step) { + test(testname) { gMenuPopup.openPopup(gTrigger, { position: "before_start", x: 5, y: 7 }); checkOpen("trigger", testname); }, - result(testname, step) { + result(testname) { var triggerrect = gTrigger.getBoundingClientRect(); var popuprect = gMenuPopup.getBoundingClientRect(); is( @@ -1139,7 +1139,7 @@ var popupTests = [ testname: "openPopup with object argument with event", events: ["popupshowing thepopup 1000", "popupshown thepopup"], autohide: "thepopup", - test(testname, step) { + test(testname) { gMenuPopup.openPopup(gTrigger, { position: "after_start", x: 0, @@ -1153,10 +1153,10 @@ var popupTests = [ testname: "openPopup with no arguments", events: ["popupshowing thepopup", "popupshown thepopup"], autohide: "thepopup", - test(testname, step) { + test() { gMenuPopup.openPopup(); }, - result(testname, step) { + result(testname) { let isMenu = gTrigger.type == "menu"; // With no arguments, open in default menu position var triggerrect = gTrigger.getBoundingClientRect(); @@ -1184,7 +1184,7 @@ var popupTests = [ "DOMMenuItemActive submenu", "popupshown submenupopup", ], - test(testname, step) { + test(testname) { gMenuPopup.openPopup(gTrigger, "after_start", 0, 0, false, true); document .getElementById("submenupopup") @@ -1196,7 +1196,7 @@ var popupTests = [ { // remove the content nodes for the popup testname: "remove content", - test(testname, step) { + test() { var submenupopup = document.getElementById("submenupopup"); submenupopup.remove(); var popup = document.getElementById("thepopup"); diff --git a/toolkit/content/tests/chrome/test_arrowpanel.xhtml b/toolkit/content/tests/chrome/test_arrowpanel.xhtml index cd8d312e1d..f9f8a0eaf5 100644 --- a/toolkit/content/tests/chrome/test_arrowpanel.xhtml +++ b/toolkit/content/tests/chrome/test_arrowpanel.xhtml @@ -201,7 +201,7 @@ function* nextTest() // Test that a transition occurs when opening or closing the popup. if (matchMedia("(-moz-panel-animations").matches) { - function transitionEnded(event) { + function transitionEnded() { if ($("animatepanel").state != "open") { is($("animatepanel").state, "showing", "state is showing during transitionend"); ok(!animatedPopupShown, "popupshown not fired yet") diff --git a/toolkit/content/tests/chrome/test_labelcontrol.xhtml b/toolkit/content/tests/chrome/test_labelcontrol.xhtml index 06e7f96105..937d3e7403 100644 --- a/toolkit/content/tests/chrome/test_labelcontrol.xhtml +++ b/toolkit/content/tests/chrome/test_labelcontrol.xhtml @@ -40,7 +40,7 @@ function runTests() let checkboxLabel = $("checkbox-label"); is(checkboxLabel.control, "checkbox", "checkbox control"); is(checkboxLabel.labeledControlElement, checkbox, "checkbox labeledControlElement"); - is(checkbox.accessKey, "", "checkbox accessKey not set"); + is(checkbox.accessKey, null, "checkbox accessKey not set"); checkboxLabel.accessKey = "C"; is(checkbox.accessKey, "C", "checkbox accessKey set"); diff --git a/toolkit/content/tests/chrome/test_menuitem_blink.xhtml b/toolkit/content/tests/chrome/test_menuitem_blink.xhtml index 700a8a7465..ad76eee8bf 100644 --- a/toolkit/content/tests/chrome/test_menuitem_blink.xhtml +++ b/toolkit/content/tests/chrome/test_menuitem_blink.xhtml @@ -32,7 +32,7 @@ function test_crash() { var menupopup = document.getElementById("menupopup"); var menuitem = document.getElementById("menuitem"); menupopup.addEventListener("popupshown", function () { - menuitem.addEventListener("mouseup", function (e) { + menuitem.addEventListener("mouseup", function () { const observer = new MutationObserver((aMutationList, aObserver) => { for (const mutation of aMutationList) { if (mutation.attributeName != "_moz-menuactive") { diff --git a/toolkit/content/tests/chrome/test_menuitem_commands.xhtml b/toolkit/content/tests/chrome/test_menuitem_commands.xhtml index 60a35b36c5..a342b9d1d5 100644 --- a/toolkit/content/tests/chrome/test_menuitem_commands.xhtml +++ b/toolkit/content/tests/chrome/test_menuitem_commands.xhtml @@ -49,10 +49,10 @@ function runTestSet(suffix) var three = $("three" + suffix); var four = $("four" + suffix); - checkAttributes(one, "One", "", "", true, false); - checkAttributes(two, "", "", "false", false, false); + checkAttributes(one, "One", null, null, true, false); + checkAttributes(two, null, null, "false", false, false); checkAttributes(three, "Three", "T", "true", false, false); - checkAttributes(four, "Four", "F", "", false, false); + checkAttributes(four, "Four", "F", null, false, false); if (isMac && suffix) { var utils = window.windowUtils; @@ -62,8 +62,8 @@ function runTestSet(suffix) $("menu" + suffix).open = true; } - checkAttributes(one, "One", "", "", false, true); - checkAttributes(two, "Cat", "C", "", false, true); + checkAttributes(one, "One", null, null, false, true); + checkAttributes(two, "Cat", "C", null, false, true); checkAttributes(three, "Dog", "D", "false", true, true); checkAttributes(four, "Four", "F", "true", false, true); diff --git a/toolkit/content/tests/chrome/test_menulist_in_popup.xhtml b/toolkit/content/tests/chrome/test_menulist_in_popup.xhtml index 971fe90322..75fb19adcf 100644 --- a/toolkit/content/tests/chrome/test_menulist_in_popup.xhtml +++ b/toolkit/content/tests/chrome/test_menulist_in_popup.xhtml @@ -17,7 +17,7 @@ async function runTest() { let menulist = document.getElementById("menulist"); let menulistPopup = document.getElementById("menulistpopup"); - menulistPopup.addEventListener("popupshown", function(e) { + menulistPopup.addEventListener("popupshown", function() { ok(false, "Menulist popup shown"); }); diff --git a/toolkit/content/tests/chrome/test_menulist_initial_selection.xhtml b/toolkit/content/tests/chrome/test_menulist_initial_selection.xhtml index 19e9beae67..255318f5a7 100644 --- a/toolkit/content/tests/chrome/test_menulist_initial_selection.xhtml +++ b/toolkit/content/tests/chrome/test_menulist_initial_selection.xhtml @@ -29,6 +29,9 @@ async function runTest() { is(menulist2.value, "", "menulist2 should not be selected to the first item's value"); is(menulist2.label, "None", "menulist2 should not be selected to the first item's value"); + is(menulist1.menupopup.querySelectorAll('menuitem[selected="true"]').length, 1, "menulist1 should have only one selected item."); + is(menulist2.menupopup.querySelectorAll('menuitem[selected="true"]').length, 0, "menulist2 should have no selected items."); + SimpleTest.finish(); } @@ -37,17 +40,17 @@ async function runTest() { <panel> <menulist id="menulist1" value="" label="None"> - <menupopup id="menulistpopup"> + <menupopup id="menulistpopup1"> <menuitem value="1" label="One"/> <menuitem value="2" label="Two"/> <menuitem value="3" label="Three"/> </menupopup> </menulist> <menulist id="menulist2" value="" label="None" noinitialselection="true"> - <menupopup id="menulistpopup"> - <menuitem value="1" label="One"/> - <menuitem value="2" label="Two"/> - <menuitem value="3" label="Three"/> + <menupopup id="menulistpopup2"> + <menuitem value="4" label="Four"/> + <menuitem value="5" label="Five"/> + <menuitem value="6" label="Six"/> </menupopup> </menulist> </panel> diff --git a/toolkit/content/tests/chrome/test_menulist_initial_selection_with_connected_callback.xhtml b/toolkit/content/tests/chrome/test_menulist_initial_selection_with_connected_callback.xhtml new file mode 100644 index 0000000000..2c9b462948 --- /dev/null +++ b/toolkit/content/tests/chrome/test_menulist_initial_selection_with_connected_callback.xhtml @@ -0,0 +1,80 @@ +<window title="Menulist Initial Selection Connected Callback Test" + onload="setTimeout(runTest, 0)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> +<script> +<![CDATA[ +SimpleTest.waitForExplicitFinish(); + +async function runTest() { + const panel = document.querySelector("panel"); + + // Dynamically initialize two menulists, one with the noinitialselection + // attribute and one without, but ensure that their delayConnectedCallback + // functions always return true to ensure that connectedCallback will not run yet. + const menulist1 = document.createXULElement("menulist"); + menulist1.delayConnectedCallback = () => true; + menulist1.setAttribute("id", "menulist1"); + menulist1.appendItem(/* label */ "One", /* id */ "1"); + menulist1.appendItem(/* label */ "Two", /* id */ "2"); + menulist1.appendItem(/* label */ "Three", /* id */ "3"); + is(menulist1.menupopup.querySelectorAll('menuitem[selected="true"]').length, 0, "menulist1 should have no selected items yet."); + + const menulist2 = document.createXULElement("menulist"); + menulist2.delayConnectedCallback = () => true; + menulist2.setAttribute("id", "menulist2"); + menulist2.setAttribute("noinitialselection", "true"); + menulist2.appendItem(/* label */ "Four", /* id */ "4"); + menulist2.appendItem(/* label */ "Five", /* id */ "5"); + menulist2.appendItem(/* label */ "Six", /* id */ "6"); + is(menulist2.menupopup.querySelectorAll('menuitem[selected="true"]').length, 0, "menulist2 should have no selected items yet."); + + // Set their values before allowing connectedCallback to run, simulating + // a race condition where an initial value is manually set before the + // connectedCallback is invoked. + menulist1.value = "2"; + panel.appendChild(menulist1); + menulist1.delayConnectedCallback = () => false; + menulist1.connectedCallback(); + + menulist2.value = "5"; + panel.appendChild(menulist2); + menulist2.delayConnectedCallback = () => false; + menulist2.connectedCallback(); + + const panelShown = new Promise(r => panel.addEventListener("popupshown", r, { once: true })); + info("opening panel"); + panel.openPopup(null, { x: 0, y: 0 }); + await panelShown; + info("panel opened"); + + is(menulist1.value, "2", "menulist1 should have the second menuitem's value"); + is(menulist1.label, "Two", "menulist1 should have the second menuitem's label"); + is(menulist1.menupopup.querySelectorAll('menuitem[selected="true"]').length, 1, "menulist1 should have only one selected item."); + + is(menulist2.value, "5", "menulist2 should not be selected to the second item's value"); + is(menulist2.label, "Five", "menulist2 should not be selected to the second item's value"); + is(menulist2.menupopup.querySelectorAll('menuitem[selected="true"]').length, 1, "menulist2 should have only one selected item."); + + menulist1.value = "3"; + menulist2.value = "6"; + + is(menulist1.value, "3", "menulist1 should have the third menuitem's value"); + is(menulist1.label, "Three", "menulist1 should have the third menuitem's label"); + is(menulist1.menupopup.querySelectorAll('menuitem[selected="true"]').length, 1, "menulist1 should have only one selected item."); + + is(menulist2.value, "6", "menulist2 should not be selected to the third item's value"); + is(menulist2.label, "Six", "menulist2 should not be selected to the third item's value"); + is(menulist2.menupopup.querySelectorAll('menuitem[selected="true"]').length, 1, "menulist2 should have only one selected item."); + + SimpleTest.finish(); +} + +]]> +</script> +<panel></panel> +</window> + + diff --git a/toolkit/content/tests/chrome/test_menulist_null_value.xhtml b/toolkit/content/tests/chrome/test_menulist_null_value.xhtml index 9312c236dc..e0ef42f5bd 100644 --- a/toolkit/content/tests/chrome/test_menulist_null_value.xhtml +++ b/toolkit/content/tests/chrome/test_menulist_null_value.xhtml @@ -34,7 +34,7 @@ function runTests() list.selectedItem = null; is(list.value, "", "Check list value after setting selectedItem to null"); - is(list.getAttribute("label"), "", "Check list label after setting selectedItem to null"); + is(list.getAttribute("label"), null, "Check list label after setting selectedItem to null"); // select something again to make sure the label is not already empty list.selectedIndex = 1; @@ -61,14 +61,14 @@ function runTests() // set the value to null and test it (bug 408940) list.value = null; is(list.value, "", "Check list value after setting value to null"); - is(list.getAttribute("label"), "", "Check list label after setting value to null"); + is(list.getAttribute("label"), null, "Check list label after setting value to null"); // select something again to make sure the label is not already empty list.selectedIndex = 1; // set the value to undefined and test it (bug 408940) list.value = undefined; is(list.value, "", "Check list value after setting value to undefined"); - is(list.getAttribute("label"), "", "Check list label after setting value to undefined"); + is(list.getAttribute("label"), null, "Check list label after setting value to undefined"); // select something again to make sure the label is not already empty list.selectedIndex = 1; @@ -76,7 +76,7 @@ function runTests() // and make sure the previous label is removed list.value = "this does not exist"; is(list.value, "this does not exist", "Check the list value after setting it to something not associated witn an existing menuitem"); - is(list.getAttribute("label"), "", "Check that the list label is empty after selecting a nonexistent item"); + is(list.getAttribute("label"), null, "Check that the list label is empty after selecting a nonexistent item"); SimpleTest.finish(); } diff --git a/toolkit/content/tests/chrome/test_notificationbox.xhtml b/toolkit/content/tests/chrome/test_notificationbox.xhtml index 8de985175a..9a2bc9d888 100644 --- a/toolkit/content/tests/chrome/test_notificationbox.xhtml +++ b/toolkit/content/tests/chrome/test_notificationbox.xhtml @@ -89,8 +89,8 @@ function testtag_notificationbox_buttonpressed(notification, button) } let buttonsPressedLog = ""; -function testtag_notificationbox_button1pressed(notification, button) { buttonsPressedLog += "button1"; return true; } -function testtag_notificationbox_button2pressed(notification, button) { buttonsPressedLog += "button2"; return true; } +function testtag_notificationbox_button1pressed() { buttonsPressedLog += "button1"; return true; } +function testtag_notificationbox_button2pressed() { buttonsPressedLog += "button2"; return true; } function testtag_notificationbox(nb) { @@ -227,7 +227,7 @@ var tests = nb.removeNotification(ntf); return ntf; }, - result(nb, ntf) { + result(nb) { testtag_notificationbox_State(nb, "removeNotification", null, 0); } }, @@ -394,7 +394,7 @@ var tests = nb.removeCurrentNotification(); return ntf; }, - result(nb, ntf) { + result(nb) { testtag_notificationbox_State(nb, "removeCurrentNotification", null, 0); } }, @@ -451,7 +451,7 @@ var tests = }, testtag_notificationbox_buttons_nopopup); return ntf; }, - result(nb, ntf) { + result(nb) { let buttons = nb.currentNotification.buttonContainer.querySelectorAll("* button"); buttons[0].focus(); @@ -510,7 +510,7 @@ var tests = } }, { - async test(nb, unused) { + async test(nb) { // add a number of notifications and check that they are added in order await nb.appendNotification("4", { label: "Four", priority: nb.PRIORITY_INFO_HIGH }, testtag_notificationbox_buttons); @@ -537,11 +537,11 @@ var tests = { // test closing notifications to make sure that the current notification is still set properly repeat: true, - test(nb, testidx) { + test() { this.repeat = false; return undefined; }, - result(nb, arr) { + result(nb) { let notificationOrder = [4, 7, 2, 8, 5, 6, 1, 9, 10, 3]; let allNotificationValues = [...nb.stack.children].map(n => n.getAttribute("value")); is(allNotificationValues.length, notificationOrder.length, "Expected number of notifications"); @@ -556,7 +556,7 @@ var tests = } }, { - async test(nb, ntf) { + async test(nb) { var exh = false; try { await nb.appendNotification("no", { label: "no", priority: -1 }); diff --git a/toolkit/content/tests/chrome/test_popup_keys.xhtml b/toolkit/content/tests/chrome/test_popup_keys.xhtml index 6b8dd31143..e72c496a59 100644 --- a/toolkit/content/tests/chrome/test_popup_keys.xhtml +++ b/toolkit/content/tests/chrome/test_popup_keys.xhtml @@ -37,7 +37,7 @@ let gLastFirstMenuActiveValue = null; function waitForEvent(target, eventName) { return new Promise(resolve => { - target.addEventListener(eventName, function eventOccurred(event) { + target.addEventListener(eventName, function eventOccurred() { resolve(); }, { once: true}); }); diff --git a/toolkit/content/tests/chrome/test_popup_moveToAnchor.xhtml b/toolkit/content/tests/chrome/test_popup_moveToAnchor.xhtml index a7800caaad..628ff2a851 100644 --- a/toolkit/content/tests/chrome/test_popup_moveToAnchor.xhtml +++ b/toolkit/content/tests/chrome/test_popup_moveToAnchor.xhtml @@ -20,7 +20,7 @@ <script> SimpleTest.waitForExplicitFinish(); -function runTest(id) +function runTest() { $("popup").openPopup($("button1"), "after_start"); } diff --git a/toolkit/content/tests/chrome/test_richlistbox.xhtml b/toolkit/content/tests/chrome/test_richlistbox.xhtml index 48303e0172..65f3626a8c 100644 --- a/toolkit/content/tests/chrome/test_richlistbox.xhtml +++ b/toolkit/content/tests/chrome/test_richlistbox.xhtml @@ -5,7 +5,6 @@ XUL Widget Test for listbox direction --> <window title="Listbox direction test" - onload="test_richlistbox()" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> @@ -26,8 +25,7 @@ function getScrollIndexAmount(aDirection) { return (4 * aDirection + richListBox.currentIndex); } -function test_richlistbox() -{ +add_task(function test_richlistbox() { var height = richListBox.clientHeight; var item; do { @@ -93,9 +91,22 @@ function test_richlistbox() richListBox.selectedItem = richListBox.firstChild; richListBox.firstChild.nextSibling.setAttribute("disabled", true); richListBox.focus(); + synthesizeKey("KEY_ArrowDown", {}, window); is(richListBox.selectedItems.length, 1, "one item selected"); - is(richListBox.selectedItems[0], richListBox.firstChild, "first item selected"); + is(richListBox.selectedItems[0], richListBox.getItemAtIndex(2), "skipped over disabled item on keydown"); + + synthesizeKey("KEY_ArrowUp", {}, window); + is(richListBox.selectedItems.length, 1, "one item selected"); + is(richListBox.selectedItems[0], richListBox.firstChild, "skipped over disabled item on keyup"); + + synthesizeMouseAtCenter(richListBox.getItemAtIndex(1), {}); + is(richListBox.selectedItems.length, 1, "one item selected"); + is(richListBox.selectedItems[0], richListBox.firstChild, "cannot select disabled item"); + + synthesizeMouseAtCenter(richListBox.getItemAtIndex(2), {}); + is(richListBox.selectedItems.length, 1, "one item selected"); + is(richListBox.selectedItems[0], richListBox.getItemAtIndex(2), "can select enabled item"); // Selected item re-insertion should keep the item selected. richListBox.clearSelection(); @@ -107,9 +118,7 @@ function test_richlistbox() richListBox.append(item); is(richListBox.selectedItems.length, 1, "one item selected"); is(richListBox.selectedItems[0], item, "last (previosly first) item selected"); - - SimpleTest.finish(); -} +}); ]]> </script> diff --git a/toolkit/content/tests/chrome/test_tree_view.xhtml b/toolkit/content/tests/chrome/test_tree_view.xhtml index 5e45161c6c..c9ec889995 100644 --- a/toolkit/content/tests/chrome/test_tree_view.xhtml +++ b/toolkit/content/tests/chrome/test_tree_view.xhtml @@ -24,23 +24,23 @@ var view = value: "", rowCount: 8, getCellText(row, column) { return this.treeData[row % 4][column.index]; }, - getCellValue(row, column) { return this.value; }, + getCellValue() { return this.value; }, setCellText(row, column, val) { this.treeData[row % 4][column.index] = val; }, setCellValue(row, column, val) { this.value = val; }, setTree(tree) { this.tree = tree; }, - isContainer(row) { return false; }, - isContainerOpen(row) { return false; }, - isContainerEmpty(row) { return false; }, - isSeparator(row) { return false; }, - isSorted(row) { return false; }, + isContainer() { return false; }, + isContainerOpen() { return false; }, + isContainerEmpty() { return false; }, + isSeparator() { return false; }, + isSorted() { return false; }, isEditable(row, column) { return row != 2 || column.index != 1; }, - getParentIndex(row, column) { return -1; }, - getLevel(row) { return 0; }, - hasNextSibling(row, column) { return row != this.rowCount - 1; }, - getImageSrc(row, column) { return ""; }, - cycleHeader(column) { }, - getRowProperties(row) { return ""; }, - getCellProperties(row, column) { return ""; }, + getParentIndex() { return -1; }, + getLevel() { return 0; }, + hasNextSibling(row) { return row != this.rowCount - 1; }, + getImageSrc() { return ""; }, + cycleHeader() { }, + getRowProperties() { return ""; }, + getCellProperties() { return ""; }, getColumnProperties(column) { if (!column.index) { diff --git a/toolkit/content/tests/chrome/window_chromemargin.xhtml b/toolkit/content/tests/chrome/window_chromemargin.xhtml index 81bcba62fe..8c7cf5c0d3 100644 --- a/toolkit/content/tests/chrome/window_chromemargin.xhtml +++ b/toolkit/content/tests/chrome/window_chromemargin.xhtml @@ -16,15 +16,19 @@ function ok(condition, message) { window.arguments[0].SimpleTest.ok(condition, message); } +function is(a, b, message) { + window.arguments[0].SimpleTest.is(a, b, message); +} + function doSingleTest(param) { var exception = null; try { document.documentElement.removeAttribute("chromemargin"); document.documentElement.setAttribute("chromemargin", param); - ok(document. + is(document. documentElement. - getAttribute("chromemargin") == param, "couldn't set/get chromemargin?"); + getAttribute("chromemargin"), param, "couldn't set/get chromemargin?"); } catch (ex) { exception = ex; } @@ -44,7 +48,7 @@ function runTests() // test remove doc.removeAttribute("chromemargin"); - ok(doc.getAttribute("chromemargin") == "", "couldn't remove chromemargin?"); + is(doc.getAttribute("chromemargin"), null, "couldn't remove chromemargin?"); // we already test these really well in a c++ test in widget doSingleTest("1,2,3,4"); diff --git a/toolkit/content/tests/chrome/window_cursorsnap_dialog.xhtml b/toolkit/content/tests/chrome/window_cursorsnap_dialog.xhtml index d5c0e2753e..fa239a87af 100644 --- a/toolkit/content/tests/chrome/window_cursorsnap_dialog.xhtml +++ b/toolkit/content/tests/chrome/window_cursorsnap_dialog.xhtml @@ -60,7 +60,7 @@ function finish() window.close(); } -function onMouseMove(aEvent) +function onMouseMove() { var button = document.getElementById("dialog").getButton("accept"); if (button.disabled) diff --git a/toolkit/content/tests/chrome/window_keys.xhtml b/toolkit/content/tests/chrome/window_keys.xhtml index 77a098ef0b..51f3be0a33 100644 --- a/toolkit/content/tests/chrome/window_keys.xhtml +++ b/toolkit/content/tests/chrome/window_keys.xhtml @@ -82,7 +82,7 @@ function runTest() document.documentElement.appendChild(keyset); iterateKeys(true, "appended"); - var accelText = menuitem => menuitem.getAttribute("acceltext").toLowerCase(); + var accelText = menuitem => (menuitem.getAttribute("acceltext") || "").toLowerCase(); $("menubutton").open = true; diff --git a/toolkit/content/tests/chrome/window_largemenu.xhtml b/toolkit/content/tests/chrome/window_largemenu.xhtml index d84b045e78..8e6b6718b4 100644 --- a/toolkit/content/tests/chrome/window_largemenu.xhtml +++ b/toolkit/content/tests/chrome/window_largemenu.xhtml @@ -363,8 +363,8 @@ function testPopupMovement() is(screenX, expectedx, gTests[gTestIndex] + " (6000, 100) x"); is(screenY, 100, gTests[gTestIndex] + " (6000, 100) y"); - is(popup.getAttribute("left"), "", gTests[gTestIndex] + " left is empty after moving"); - is(popup.getAttribute("top"), "", gTests[gTestIndex] + " top is empty after moving"); + is(popup.getAttribute("left"), null, gTests[gTestIndex] + " left is empty after moving"); + is(popup.getAttribute("top"), null, gTests[gTestIndex] + " top is empty after moving"); popup.setAttribute("left", "80"); popup.setAttribute("top", "82"); [screenX, screenY] = getScreenXY(popup); @@ -387,8 +387,8 @@ function testPopupMovement() is(screenX, expectedx, gTests[gTestIndex] + " move after set left and top x to -1"); is(screenY, expectedy, gTests[gTestIndex] + " move after set left and top y to -1"); - is(popup.getAttribute("left"), "", gTests[gTestIndex] + " left is not set after moving to -1"); - is(popup.getAttribute("top"), "", gTests[gTestIndex] + " top is not set after moving to -1"); + is(popup.getAttribute("left"), null, gTests[gTestIndex] + " left is not set after moving to -1"); + is(popup.getAttribute("top"), null, gTests[gTestIndex] + " top is not set after moving to -1"); popup.hidePopup(); } diff --git a/toolkit/content/tests/chrome/window_maximized_persist.xhtml b/toolkit/content/tests/chrome/window_maximized_persist.xhtml index f7eb695f0f..10f7d36d07 100644 --- a/toolkit/content/tests/chrome/window_maximized_persist.xhtml +++ b/toolkit/content/tests/chrome/window_maximized_persist.xhtml @@ -8,7 +8,7 @@ id="window" persist="height width sizemode"> <script type="application/javascript"><![CDATA[ - window.addEventListener("sizemodechange", evt => { + window.addEventListener("sizemodechange", () => { window.arguments[0].postMessage("sizemodechange", "*"); }); ]]></script> diff --git a/toolkit/content/tests/chrome/window_maximized_persist_with_no_titlebar.xhtml b/toolkit/content/tests/chrome/window_maximized_persist_with_no_titlebar.xhtml index 83fede7fae..e104dfa652 100644 --- a/toolkit/content/tests/chrome/window_maximized_persist_with_no_titlebar.xhtml +++ b/toolkit/content/tests/chrome/window_maximized_persist_with_no_titlebar.xhtml @@ -9,7 +9,7 @@ id="window" persist="height width sizemode"> <script type="application/javascript"><![CDATA[ - window.addEventListener("sizemodechange", evt => { + window.addEventListener("sizemodechange", () => { window.arguments[0].postMessage("sizemodechange", "*"); }); ]]></script> diff --git a/toolkit/content/tests/chrome/window_popup_anchoratrect.xhtml b/toolkit/content/tests/chrome/window_popup_anchoratrect.xhtml index 524a95b643..2262ba08e9 100644 --- a/toolkit/content/tests/chrome/window_popup_anchoratrect.xhtml +++ b/toolkit/content/tests/chrome/window_popup_anchoratrect.xhtml @@ -79,7 +79,7 @@ let tests = [ }, ]; -function runTest(id) +function runTest() { menupopup = $("popup"); nextTest(); diff --git a/toolkit/content/tests/chrome/window_tooltip.xhtml b/toolkit/content/tests/chrome/window_tooltip.xhtml index b78075de45..e6ee5c6b42 100644 --- a/toolkit/content/tests/chrome/window_tooltip.xhtml +++ b/toolkit/content/tests/chrome/window_tooltip.xhtml @@ -30,6 +30,8 @@ var gOriginalWidth = -1; var gOriginalHeight = -1; var gButton = null; +const kTooltipOffsetVertical = 10; + function runTest() { startPopupTests(popupTests); @@ -112,7 +114,7 @@ var popupTests = [ Math.round(buttonrect.left + parseFloat(popupstyle.marginLeft) + 6), testname + " left position of tooltip"); is(Math.round(rect.top), - Math.round(buttonrect.top + parseFloat(popupstyle.marginTop) + 6), + Math.round(buttonrect.top + parseFloat(popupstyle.marginTop) + 6 + kTooltipOffsetVertical), testname + " top position of tooltip"); var labelrect = document.getElementById("label").getBoundingClientRect(); @@ -159,7 +161,7 @@ var popupTests = [ Math.round(buttonrect.left + parseFloat(popupstyle.marginLeft) + 4), testname + " left position of tooltip"); is(Math.round(rect.top), - Math.round(buttonrect.top + parseFloat(popupstyle.marginTop) + 4), + Math.round(buttonrect.top + parseFloat(popupstyle.marginTop) + 4 + kTooltipOffsetVertical), testname + " top position of tooltip"); var labelrect = document.getElementById("label").getBoundingClientRect(); @@ -204,7 +206,7 @@ var popupTests = [ Math.round(buttonrect.left + parseFloat(popupstyle.marginLeft) + 6), testname + " left position of tooltip"); is(Math.round(rect.top), - Math.round(buttonrect.top + parseFloat(popupstyle.marginTop) + 6), + Math.round(buttonrect.top + parseFloat(popupstyle.marginTop) + 6 + kTooltipOffsetVertical), testname + " top position of tooltip"); var labelrect = document.getElementById("label").getBoundingClientRect(); @@ -327,7 +329,43 @@ var popupTests = [ // that the original height has increased by at least 10 pixels ok(gOriginalHeight + 10 < rect.bottom - rect.top, testname + " tooltip is wrapped"); } -} +}, +{ + testname: "anchored tooltip with no offset", + events: ["popupshowing thetooltip", "popupshown thetooltip"], + autohide: "thetooltip", + test() { + gButton = document.getElementById("withtooltip"); + document + .getElementById("thetooltip") + .setAttribute( + "onpopupshowing", + "this.moveToAnchor(this.triggerNode, 'after_start');" + ); + disableNonTestMouse(true); + synthesizeMouse(gButton, 2, 2, { type: "mouseover" }); + synthesizeMouse(gButton, 4, 4, { type: "mousemove" }); + synthesizeMouse(gButton, 6, 6, { type: "mousemove" }); + disableNonTestMouse(false); + }, + result(testname) { + var buttonrect = document + .getElementById("withtooltip") + .getBoundingClientRect(); + var tooltip = document.getElementById("thetooltip"); + var rect = tooltip.getBoundingClientRect(); + var popupstyle = window.getComputedStyle(tooltip); + + // Offset does not apply to anchored tooltips + is( + Math.round(rect.top), + Math.round(buttonrect.bottom + parseFloat(popupstyle.marginTop)), + testname + " top position of tooltip" + ); + + tooltip.removeAttribute("onpopupshowing"); + }, +}, ]; var waitSteps = 0; @@ -359,7 +397,14 @@ function moveWindowTo(x, y, callback, arg) } } -window.arguments[0].SimpleTest.waitForFocus(runTest, window); +(async function() { + let parent = window.arguments[0]; + await Promise.all([ + parent.SimpleTest.promiseFocus(window), + parent.SpecialPowers.pushPrefEnv({set: [["ui.tooltipOffsetVertical", kTooltipOffsetVertical]]}), + ]); + runTest(); +})(); ]]> </script> diff --git a/toolkit/content/tests/widgets/chrome.toml b/toolkit/content/tests/widgets/chrome.toml index 18fe0d153a..fa10249e89 100644 --- a/toolkit/content/tests/widgets/chrome.toml +++ b/toolkit/content/tests/widgets/chrome.toml @@ -5,7 +5,7 @@ support-files = [ "popup_shared.js", "window_label_checkbox.xhtml", "window_menubar.xhtml", - "seek_with_sound.ogg", + "seek_with_sound.webm", ] prefs = ["app.support.baseURL='https://support.mozilla.org/'"] @@ -63,7 +63,7 @@ skip-if = [ ["test_videocontrols_focus.html"] support-files = [ "head.js", - "video.ogg", + "video.webm", ] skip-if = [ "os == 'android'", diff --git a/toolkit/content/tests/widgets/file_videocontrols_jsdisabled.html b/toolkit/content/tests/widgets/file_videocontrols_jsdisabled.html index 56917b69ac..95dba91eea 100644 --- a/toolkit/content/tests/widgets/file_videocontrols_jsdisabled.html +++ b/toolkit/content/tests/widgets/file_videocontrols_jsdisabled.html @@ -1,2 +1,2 @@ -<video src="seek_with_sound.ogg" controls autoplay=true></video> +<video src="seek_with_sound.webm" controls autoplay=true></video> <script>window.testExpando = true;</script> diff --git a/toolkit/content/tests/widgets/head.js b/toolkit/content/tests/widgets/head.js index d7473fa92d..2a72845a27 100644 --- a/toolkit/content/tests/widgets/head.js +++ b/toolkit/content/tests/widgets/head.js @@ -51,7 +51,7 @@ function executeTests() { } function once(target, name, cb) { - let p = new Promise(function (resolve, reject) { + let p = new Promise(function (resolve) { target.addEventListener( name, function () { diff --git a/toolkit/content/tests/widgets/mochitest.toml b/toolkit/content/tests/widgets/mochitest.toml index 7e20352256..efd86f1208 100644 --- a/toolkit/content/tests/widgets/mochitest.toml +++ b/toolkit/content/tests/widgets/mochitest.toml @@ -5,8 +5,8 @@ support-files = [ "file_videocontrols_jsdisabled.html", "image.png", "image-zh.png", - "seek_with_sound.ogg", - "video.ogg", + "seek_with_sound.webm", + "video.webm", "head.js", "tree_shared.js", "test-webvtt-1.vtt", @@ -97,7 +97,10 @@ skip-if = [ ["test_videocontrols_size.html"] ["test_videocontrols_standalone.html"] -skip-if = ["os == 'android'"] # bug 1075573 +skip-if = [ + "os == 'linux'", # bug 1804621 + "os == 'android'", # bug 1075573 +] ["test_videocontrols_video_direction.html"] skip-if = [ diff --git a/toolkit/content/tests/widgets/seek_with_sound.ogg b/toolkit/content/tests/widgets/seek_with_sound.ogg Binary files differdeleted file mode 100644 index c86d9946bd..0000000000 --- a/toolkit/content/tests/widgets/seek_with_sound.ogg +++ /dev/null diff --git a/toolkit/content/tests/widgets/seek_with_sound.webm b/toolkit/content/tests/widgets/seek_with_sound.webm Binary files differnew file mode 100644 index 0000000000..dd60cc5a0a --- /dev/null +++ b/toolkit/content/tests/widgets/seek_with_sound.webm diff --git a/toolkit/content/tests/widgets/test_moz_button.html b/toolkit/content/tests/widgets/test_moz_button.html index 473b2d1a1c..a849ccc956 100644 --- a/toolkit/content/tests/widgets/test_moz_button.html +++ b/toolkit/content/tests/widgets/test_moz_button.html @@ -75,24 +75,23 @@ color: "--button-text-color-primary", height: "--button-min-height", }); - assertButtonPropertiesMatch(four, { width: "--button-size-icon", height: "--button-size-icon", backgroundColor: "--button-background-color", - fill: "--button-text-color", + fill: "--icon-color", }); assertButtonPropertiesMatch(five, { width: "--button-size-icon", height: "--button-size-icon", backgroundColor: "transparent", - fill: "--button-text-color", + fill: "--icon-color", }); assertButtonPropertiesMatch(six, { width: "--button-size-icon", height: "--button-size-icon", backgroundColor: "transparent", - fill: "--button-text-color", + fill: "--icon-color", }); buttons.forEach(btn => (btn.size = "small")); diff --git a/toolkit/content/tests/widgets/test_moz_card.html b/toolkit/content/tests/widgets/test_moz_card.html index ef4e67d0fa..aa9ecfde31 100644 --- a/toolkit/content/tests/widgets/test_moz_card.html +++ b/toolkit/content/tests/widgets/test_moz_card.html @@ -13,6 +13,11 @@ <body> <p id="display"></p> + <style> + moz-card.withHeadingIcon::part(icon) { + background-image: url("chrome://browser/skin/preferences/category-general.svg"); + } + </style> <div id="content"> <moz-card id="default-card" data-l10n-id="test-id-1" data-l10n-attrs="heading"> <div>TEST</div> @@ -25,6 +30,12 @@ </moz-card> <hr /> + <moz-card id="heading-icon-card" data-l10n-id="test-id-3" data-l10n-attrs="heading" heading="heading with icon" + type="accordion" icon class="withHeadingIcon"> + <div>heading icon test content</div> + </moz-card> + <hr /> + </div> <pre id="test"></pre> <script> @@ -58,12 +69,16 @@ } - function assertAccordionCardProperties(card, expectedValues) { + function assertAccordionCardProperties(card) { ok(card.detailsEl, "The details element should exist"); ok(card.detailsEl.querySelector("summary"), "There should be a summary element within the details element"); ok(card.detailsEl.querySelector("summary").querySelector(".chevron-icon"), "There should be a chevron icon div within the summary element"); } + function assertHeadingIconCardProperties(card) { + ok(card.shadowRoot.querySelector("#heading-wrapper").querySelector("#heading-icon"), "The heading icon element should exist"); + } + async function generateCard(values) { let card = document.createElement("moz-card"); for (let [key, value] of Object.entries(values)) { @@ -152,6 +167,54 @@ ); }); + add_task(async function testHeadingIconCard() { + assertBasicProperties(document.getElementById("heading-icon-card"), + { + "data-l10n-id": "test-id-3", + "data-l10n-attrs": "heading", + contentText: "heading icon test content", + headingText: "heading with icon", + } + ); + assertHeadingIconCardProperties(document.getElementById("heading-icon-card"), + { + "data-l10n-id": "test-id-3", + "data-l10n-attrs": "heading", + contentText: "heading icon test content", + headingText: "heading with icon", + } + ); + + let headingIconCard = await generateCard( + { + class: "heading-icon-class", + type: "accordion", + icon: "", + id: "generated-heading-icon-card", + "data-l10n-id": "generated-id-3", + "data-l10n-attrs": "heading", + heading: testHeading + } + ); + + assertBasicProperties(headingIconCard, + { + "data-l10n-id": "generated-id-3", + "data-l10n-attrs": "heading", + headingText: testHeading, + contentText: generatedSlotText, + } + ); + assertHeadingIconCardProperties(headingIconCard, + { + "data-l10n-id": "generated-id-3", + "data-l10n-attrs": "heading", + headingText: testHeading, + contentText: generatedSlotText, + } + ); + }); + </script> </body> diff --git a/toolkit/content/tests/widgets/test_popupreflows.xhtml b/toolkit/content/tests/widgets/test_popupreflows.xhtml index c3f8068779..c014d6bee4 100644 --- a/toolkit/content/tests/widgets/test_popupreflows.xhtml +++ b/toolkit/content/tests/widgets/test_popupreflows.xhtml @@ -20,7 +20,7 @@ let panel, anchor; // done by the panel. let observer = { reflows: [], - reflow (start, end) { + reflow () { // Ignore reflows triggered by native code // (Reflows from native code only have an empty stack after the first frame) var path = (new Error().stack).split("\n").slice(1).join(""); @@ -31,7 +31,7 @@ let observer = { this.reflows.push(new Error().stack); }, - reflowInterruptible (start, end) { + reflowInterruptible () { // We're not interested in interruptible reflows. Why, you ask? Because // we've simply cargo-culted this test from browser_tabopen_reflows.js! }, diff --git a/toolkit/content/tests/widgets/test_videocontrols.html b/toolkit/content/tests/widgets/test_videocontrols.html index 076b4350fd..f2fc53a1cc 100644 --- a/toolkit/content/tests/widgets/test_videocontrols.html +++ b/toolkit/content/tests/widgets/test_videocontrols.html @@ -184,7 +184,7 @@ add_task(async function setup() { ]}); await new Promise(resolve => { video.addEventListener("canplaythrough", resolve, {once: true}); - video.src = "seek_with_sound.ogg"; + video.src = "seek_with_sound.webm"; }); video.addEventListener("play", captureEventThenCheck); @@ -437,7 +437,7 @@ add_task(async function click_and_hold_slider() { * Bug 1402877: Don't let click event dispatch through media controls to video element. */ add_task(async function click_event_dispatch() { - const clientScriptClickHandler = (e) => { + const clientScriptClickHandler = () => { ok(false, "Should not receive the event"); }; video.addEventListener("click", clientScriptClickHandler); diff --git a/toolkit/content/tests/widgets/test_videocontrols_audio.html b/toolkit/content/tests/widgets/test_videocontrols_audio.html index ad528f4c27..0a7a2dcbc3 100644 --- a/toolkit/content/tests/widgets/test_videocontrols_audio.html +++ b/toolkit/content/tests/widgets/test_videocontrols_audio.html @@ -18,7 +18,7 @@ <script> const video = document.getElementById("video"); - function loadedmetadata(event) { + function loadedmetadata() { SimpleTest.executeSoon(function() { const controlBar = SpecialPowers.wrap(video).openOrClosedShadowRoot.querySelector(".controlBar"); is(controlBar.getAttribute("fullscreen-unavailable"), "true", "Fullscreen button is hidden"); diff --git a/toolkit/content/tests/widgets/test_videocontrols_closed_caption_menu.html b/toolkit/content/tests/widgets/test_videocontrols_closed_caption_menu.html index 39d6ff494f..5a2602d204 100644 --- a/toolkit/content/tests/widgets/test_videocontrols_closed_caption_menu.html +++ b/toolkit/content/tests/widgets/test_videocontrols_closed_caption_menu.html @@ -49,7 +49,7 @@ ["media.videocontrols.keyboard-tab-to-all-controls", true], ]}, done); }, done => { - video.src = "seek_with_sound.ogg"; + video.src = "seek_with_sound.webm"; video.addEventListener("loadedmetadata", done); }, cleanup); diff --git a/toolkit/content/tests/widgets/test_videocontrols_error.html b/toolkit/content/tests/widgets/test_videocontrols_error.html index af90a4672a..922726d5be 100644 --- a/toolkit/content/tests/widgets/test_videocontrols_error.html +++ b/toolkit/content/tests/widgets/test_videocontrols_error.html @@ -27,7 +27,7 @@ add_task(async function check_normal_status() { await new Promise(resolve => { - video.src = "seek_with_sound.ogg"; + video.src = "seek_with_sound.webm"; video.addEventListener("loadedmetadata", () => SimpleTest.executeSoon(resolve)); }); @@ -44,7 +44,7 @@ const errorType = "errorNoSource"; await new Promise(resolve => { - video.src = "invalid_source.ogg"; + video.src = "invalid_source.webm"; video.addEventListener("error", () => SimpleTest.executeSoon(resolve)); }); diff --git a/toolkit/content/tests/widgets/test_videocontrols_focus.html b/toolkit/content/tests/widgets/test_videocontrols_focus.html index 0982947ffe..e19262bf6e 100644 --- a/toolkit/content/tests/widgets/test_videocontrols_focus.html +++ b/toolkit/content/tests/widgets/test_videocontrols_focus.html @@ -36,7 +36,7 @@ add_task(async function setup() { video.controls = true; video.preload = "auto"; video.loop = true; - video.src = "video.ogg"; + video.src = "video.webm"; const caption = video.addTextTrack("captions", "English", "en"); caption.mode = "showing"; const content = document.getElementById("content"); diff --git a/toolkit/content/tests/widgets/test_videocontrols_iframe_fullscreen.html b/toolkit/content/tests/widgets/test_videocontrols_iframe_fullscreen.html index 0a74b25609..d30d10b8bc 100644 --- a/toolkit/content/tests/widgets/test_videocontrols_iframe_fullscreen.html +++ b/toolkit/content/tests/widgets/test_videocontrols_iframe_fullscreen.html @@ -30,7 +30,7 @@ ifr.addEventListener("load", resolve); }).then(() => new Promise(resolve => { video = ifr.contentDocument.getElementById("video"); - video.src = "seek_with_sound.ogg"; + video.src = "seek_with_sound.webm"; video.addEventListener("loadedmetadata", resolve); })).then(() => new Promise(resolve => { const available = video.ownerDocument.fullscreenEnabled; diff --git a/toolkit/content/tests/widgets/test_videocontrols_jsdisabled.html b/toolkit/content/tests/widgets/test_videocontrols_jsdisabled.html index f3fdecc47f..16cf2b92e0 100644 --- a/toolkit/content/tests/widgets/test_videocontrols_jsdisabled.html +++ b/toolkit/content/tests/widgets/test_videocontrols_jsdisabled.html @@ -45,7 +45,7 @@ SpecialPowers.pushPrefEnv({"set": [["javascript.enabled", false]]}, startTest); var testnum = 1; var video; -function loadevent(event) { +function loadevent() { is(win.testExpando, undefined, "expando shouldn't exist because js is disabled"); video = win.document.querySelector("video"); // Other events expected by the test. diff --git a/toolkit/content/tests/widgets/test_videocontrols_keyhandler.html b/toolkit/content/tests/widgets/test_videocontrols_keyhandler.html index 5b771fc745..9737a9fff2 100644 --- a/toolkit/content/tests/widgets/test_videocontrols_keyhandler.html +++ b/toolkit/content/tests/widgets/test_videocontrols_keyhandler.html @@ -31,7 +31,7 @@ ["media.videocontrols.keyboard-tab-to-all-controls", true], ]}, done); }, done => { - video.src = "seek_with_sound.ogg"; + video.src = "seek_with_sound.webm"; video.addEventListener("loadedmetadata", done); }); diff --git a/toolkit/content/tests/widgets/test_videocontrols_onclickplay.html b/toolkit/content/tests/widgets/test_videocontrols_onclickplay.html index 9023512ab7..3b1bd4653c 100644 --- a/toolkit/content/tests/widgets/test_videocontrols_onclickplay.html +++ b/toolkit/content/tests/widgets/test_videocontrols_onclickplay.html @@ -22,11 +22,11 @@ var video = document.getElementById("video"); function startMediaLoad() { // Kick off test once video has loaded, in its canplaythrough event handler. - video.src = "seek_with_sound.ogg"; + video.src = "seek_with_sound.webm"; video.addEventListener("canplaythrough", runTest); } -function loadevent(event) { +function loadevent() { SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}, startMediaLoad); } diff --git a/toolkit/content/tests/widgets/test_videocontrols_scrubber_position.html b/toolkit/content/tests/widgets/test_videocontrols_scrubber_position.html index b1d2ab9e74..51117e7955 100644 --- a/toolkit/content/tests/widgets/test_videocontrols_scrubber_position.html +++ b/toolkit/content/tests/widgets/test_videocontrols_scrubber_position.html @@ -26,7 +26,7 @@ const video = document.getElementById("video"); add_task(async function setup() { await new Promise(resolve => { video.addEventListener("canplaythrough", resolve, {once: true}); - video.src = "seek_with_sound.ogg"; + video.src = "seek_with_sound.webm"; }); // Check initial state upon load diff --git a/toolkit/content/tests/widgets/test_videocontrols_scrubber_position_nopreload.html b/toolkit/content/tests/widgets/test_videocontrols_scrubber_position_nopreload.html index 9fbb6fbcb5..968ba5db96 100644 --- a/toolkit/content/tests/widgets/test_videocontrols_scrubber_position_nopreload.html +++ b/toolkit/content/tests/widgets/test_videocontrols_scrubber_position_nopreload.html @@ -13,7 +13,7 @@ <p id="display"></p> <div id="content"> - <video width="320" height="240" id="video" mozNoDynamicControls controls="true" preload="none" src="seek_with_sound.ogg"></video> + <video width="320" height="240" id="video" mozNoDynamicControls controls="true" preload="none" src="seek_with_sound.webm"></video> </div> <div id="host"></div> diff --git a/toolkit/content/tests/widgets/test_videocontrols_size.html b/toolkit/content/tests/widgets/test_videocontrols_size.html index 559cc66e86..d940b1d295 100644 --- a/toolkit/content/tests/widgets/test_videocontrols_size.html +++ b/toolkit/content/tests/widgets/test_videocontrols_size.html @@ -148,7 +148,7 @@ testCases.push(() => Promise.all(videoElems.map(video => new Promise(resolve => { video.addEventListener("loadedmetadata", resolve); - video.src = "seek_with_sound.ogg"; + video.src = "seek_with_sound.webm"; })))); videoElems.forEach(video => { diff --git a/toolkit/content/tests/widgets/test_videocontrols_standalone.html b/toolkit/content/tests/widgets/test_videocontrols_standalone.html index 14208923dd..1eaa026cdc 100644 --- a/toolkit/content/tests/widgets/test_videocontrols_standalone.html +++ b/toolkit/content/tests/widgets/test_videocontrols_standalone.html @@ -22,7 +22,7 @@ function getMediaElement(aWindow) { return aWindow.document.getElementsByTagName("video")[0]; } -var popup = window.open("seek_with_sound.ogg"); +var popup = window.open("seek_with_sound.webm"); popup.addEventListener("load", function() { var video = getMediaElement(popup); diff --git a/toolkit/content/tests/widgets/test_videocontrols_video_noaudio.html b/toolkit/content/tests/widgets/test_videocontrols_video_noaudio.html index bfc8018466..99d5bdad01 100644 --- a/toolkit/content/tests/widgets/test_videocontrols_video_noaudio.html +++ b/toolkit/content/tests/widgets/test_videocontrols_video_noaudio.html @@ -23,7 +23,7 @@ add_task(async function setup() { await SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}); await new Promise(resolve => { - video.src = "video.ogg"; + video.src = "video.webm"; video.addEventListener("loadedmetadata", () => SimpleTest.executeSoon(resolve)); }); }); diff --git a/toolkit/content/tests/widgets/test_videocontrols_vtt.html b/toolkit/content/tests/widgets/test_videocontrols_vtt.html index 2f8d70f35a..33b8bcf24e 100644 --- a/toolkit/content/tests/widgets/test_videocontrols_vtt.html +++ b/toolkit/content/tests/widgets/test_videocontrols_vtt.html @@ -26,7 +26,7 @@ add_task(async function wait_for_media_ready() { await SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}); await new Promise(resolve => { - video.src = "seek_with_sound.ogg"; + video.src = "seek_with_sound.webm"; video.addEventListener("loadedmetadata", resolve); }); }); diff --git a/toolkit/content/tests/widgets/tree_shared.js b/toolkit/content/tests/widgets/tree_shared.js index ba52bf828e..2cc40686ec 100644 --- a/toolkit/content/tests/widgets/tree_shared.js +++ b/toolkit/content/tests/widgets/tree_shared.js @@ -321,11 +321,7 @@ function testtag_tree_columns(tree, expectedColumns, testid) { // check the view's getColumnProperties method var properties = tree.view.getColumnProperties(column); var expectedProperties = expectedColumn.properties; - is( - properties, - expectedProperties ? expectedProperties : "", - adjtestid + "getColumnProperties" - ); + is(properties, expectedProperties || "", adjtestid + "getColumnProperties"); } is(columns.getFirstColumn(), columns[0], testid + "getFirstColumn"); @@ -574,10 +570,10 @@ function testtag_tree_TreeSelection_UI(tree, testid, multiple) { var keydownFired = 0; var keypressFired = 0; - function keydownListener(event) { + function keydownListener() { keydownFired++; } - function keypressListener(event) { + function keypressListener() { keypressFired++; } @@ -1307,7 +1303,7 @@ function testtag_tree_TreeView_rows(tree, testid, rowInfo, startRow) { isContainer(row) { return row.container; }, - isContainerOpen(row) { + isContainerOpen() { return false; }, isContainerEmpty(row) { @@ -1325,7 +1321,7 @@ function testtag_tree_TreeView_rows(tree, testid, rowInfo, startRow) { getParentIndex(row) { return row.parent; }, - hasNextSibling(row) { + hasNextSibling() { return r < startRow + length - 1; }, }; @@ -1433,7 +1429,7 @@ function testtag_tree_TreeView_rows(tree, testid, rowInfo, startRow) { } } -function testtag_tree_TreeView_rows_sort(tree, testid, rowInfo) { +function testtag_tree_TreeView_rows_sort(tree) { // check if cycleHeader sorts the columns var columnIndex = 0; var view = tree.view; @@ -1734,7 +1730,7 @@ function testtag_tree_wheel(aTree) { var defaultPrevented = 0; - function wheelListener(event) { + function wheelListener() { defaultPrevented++; } window.addEventListener("wheel", wheelListener); @@ -2109,7 +2105,7 @@ function mouseClickOnColumnHeader( } } -function mouseDblClickOnCell(tree, row, column, testname) { +function mouseDblClickOnCell(tree, row, column) { // select the row we will edit var selection = tree.view.selection; selection.select(row); @@ -2151,12 +2147,12 @@ function convertDOMtoTreeRowInfo(treechildren, level, rowidx) { for (var c = 0; c < treerow.childNodes.length; c++) { var cell = treerow.childNodes[c]; cellInfo.push({ - label: "" + cell.getAttribute("label"), - value: cell.getAttribute("value"), - properties: cell.getAttribute("properties"), + label: cell.getAttribute("label") || "", + value: cell.getAttribute("value") || "", + properties: cell.getAttribute("properties") || "", editable: cell.getAttribute("editable") != "false", selectable: cell.getAttribute("selectable") != "false", - image: cell.getAttribute("src"), + image: cell.getAttribute("src") || "", mode: cell.hasAttribute("mode") ? parseInt(cell.getAttribute("mode")) : 3, @@ -2170,7 +2166,7 @@ function convertDOMtoTreeRowInfo(treechildren, level, rowidx) { : convertDOMtoTreeRowInfo(descendants, level + 1, rowidx); obj.rows.push({ cells: cellInfo, - properties: treerow.getAttribute("properties"), + properties: treerow.getAttribute("properties") || "", container: treeitem.getAttribute("container") == "true", separator: treeitem.localName == "treeseparator", children, diff --git a/toolkit/content/tests/widgets/video.ogg b/toolkit/content/tests/widgets/video.ogg Binary files differdeleted file mode 100644 index ac7ece3519..0000000000 --- a/toolkit/content/tests/widgets/video.ogg +++ /dev/null diff --git a/toolkit/content/tests/widgets/video.webm b/toolkit/content/tests/widgets/video.webm Binary files differnew file mode 100644 index 0000000000..87a8d837a8 --- /dev/null +++ b/toolkit/content/tests/widgets/video.webm diff --git a/toolkit/content/tests/widgets/videocontrols_direction_test.js b/toolkit/content/tests/widgets/videocontrols_direction_test.js index e937f06b3f..608a39cc5b 100644 --- a/toolkit/content/tests/widgets/videocontrols_direction_test.js +++ b/toolkit/content/tests/widgets/videocontrols_direction_test.js @@ -26,7 +26,7 @@ RemoteCanvas.prototype.load = function (callback) { var m = iframe.contentDocument.getElementById("av"); m.addEventListener( "suspend", - function (aEvent) { + function () { setTimeout(function () { let mediaElement = iframe.contentDocument.querySelector("audio, video"); diff --git a/toolkit/content/tests/widgets/window_menubar.xhtml b/toolkit/content/tests/widgets/window_menubar.xhtml index c4ced844ad..ded1361365 100644 --- a/toolkit/content/tests/widgets/window_menubar.xhtml +++ b/toolkit/content/tests/widgets/window_menubar.xhtml @@ -426,7 +426,7 @@ var popupTests = [ test() { synthesizeKey("KEY_Escape"); }, - result(testname) { + result() { }, }, { @@ -504,14 +504,14 @@ var popupTests = [ testname: "cursor up wrap", events: [ "DOMMenuItemInactive contents", "DOMMenuItemActive about" ], test() { synthesizeKey("KEY_ArrowUp"); }, - result(testname) { } + result() { } }, { // check that pressing cursor down skips non menuitems testname: "cursor down wrap", events: [ "DOMMenuItemInactive about", "DOMMenuItemActive contents" ], test() { synthesizeKey("KEY_ArrowDown"); }, - result(testname) { } + result() { } }, { // check that pressing a menuitem's accelerator selects it @@ -617,7 +617,7 @@ var popupTests = [ condition() { return kIsWindows; }, events: [ "DOMMenuItemInactive only", "DOMMenuItemActive other" ], test() { sendChar("o"); }, - result(testname) { } + result() { } }, { // when only one menuitem starting with that letter exists, it should be diff --git a/toolkit/content/widgets/autocomplete-popup.js b/toolkit/content/widgets/autocomplete-popup.js index a13ba1bc62..8bbd012f31 100644 --- a/toolkit/content/widgets/autocomplete-popup.js +++ b/toolkit/content/widgets/autocomplete-popup.js @@ -211,24 +211,32 @@ return -1; } - var newIdx = aIndex + (aReverse ? -1 : 1) * aAmount; - if ( - (aReverse && aIndex == -1) || - (newIdx > aMaxRow && aIndex != aMaxRow) - ) { - newIdx = aMaxRow; - } else if ((!aReverse && aIndex == -1) || (newIdx < 0 && aIndex != 0)) { - newIdx = 0; - } + do { + var newIdx = aIndex + (aReverse ? -1 : 1) * aAmount; + if ( + (aReverse && aIndex == -1) || + (newIdx > aMaxRow && aIndex != aMaxRow) + ) { + newIdx = aMaxRow; + } else if ((!aReverse && aIndex == -1) || (newIdx < 0 && aIndex != 0)) { + newIdx = 0; + } - if ( - (newIdx < 0 && aIndex == 0) || - (newIdx > aMaxRow && aIndex == aMaxRow) - ) { - aIndex = -1; - } else { - aIndex = newIdx; - } + if ( + (newIdx < 0 && aIndex == 0) || + (newIdx > aMaxRow && aIndex == aMaxRow) + ) { + aIndex = -1; + } else { + aIndex = newIdx; + } + + if (aIndex == -1) { + return -1; + } + } while ( + !this.richlistbox.canUserSelect(this.richlistbox.getItemAtIndex(aIndex)) + ); return aIndex; } @@ -313,12 +321,7 @@ _collapseUnusedItems() { let existingItemsCount = this.richlistbox.children.length; for (let i = this.matchCount; i < existingItemsCount; ++i) { - let item = this.richlistbox.children[i]; - - item.collapsed = true; - if (typeof item._onCollapse == "function") { - item._onCollapse(); - } + this.richlistbox.children[i].collapsed = true; } } @@ -408,10 +411,9 @@ // The styles on the list which have different <content> structure and overrided // _adjustAcItem() are unreusable. const UNREUSEABLE_STYLES = [ - "autofill-profile", - "autofill-footer", - "autofill-clear-button", - "autofill-insecureWarning", + "autofill", + "action", + "status", "generatedPassword", "generic", "importableLearnMore", @@ -434,17 +436,14 @@ if (!reusable) { let options = null; switch (style) { - case "autofill-profile": - options = { is: "autocomplete-profile-listitem" }; - break; - case "autofill-footer": - options = { is: "autocomplete-profile-listitem-footer" }; + case "autofill": + options = { is: "autocomplete-autofill-richlistitem" }; break; - case "autofill-clear-button": - options = { is: "autocomplete-profile-listitem-clear-button" }; + case "action": + options = { is: "autocomplete-action-richlistitem" }; break; - case "autofill-insecureWarning": - options = { is: "autocomplete-creditcard-insecure-field" }; + case "status": + options = { is: "autocomplete-status-richlistitem" }; break; case "generic": options = { is: "autocomplete-two-line-richlistitem" }; diff --git a/toolkit/content/widgets/autocomplete-richlistitem.js b/toolkit/content/widgets/autocomplete-richlistitem.js index fddd5b4029..b339ab1e27 100644 --- a/toolkit/content/widgets/autocomplete-richlistitem.js +++ b/toolkit/content/widgets/autocomplete-richlistitem.js @@ -522,6 +522,11 @@ return; } + let label = this.getAttribute("ac-label"); + if (label && JSON.parse(label)?.noLearnMore) { + return; + } + let baseURL = Services.urlFormatter.formatURLPref( "app.support.baseURL" ); @@ -715,6 +720,129 @@ } } + // This type has an action that is triggered when activated. The comment + // for that result should contain a fillMessageName -- the message to send -- + // and, optionally a secondary label, for example: + // { "fillMessageName": "Fill:Clear", secondary: "Second Label" } + class MozAutocompleteActionRichlistitem extends MozAutocompleteTwoLineRichlistitem { + constructor() { + super(); + this.selectedByMouseOver = true; + } + + _adjustAcItem() { + super._adjustAcItem(); + + let comment = JSON.parse(this.getAttribute("ac-label")); + this.querySelector(".line2-label").textContent = comment?.secondary || ""; + this.querySelector(".ac-site-icon").collapsed = + this.getAttribute("ac-image") == ""; + } + } + + // A row that conveys status information assigned from the status field + // within the comment associated with the selected item in the list. + class MozAutocompleteStatusRichlistitem extends MozAutocompleteTwoLineRichlistitem { + static get markup() { + return `<div class="ac-status" xmlns="http://www.w3.org/1999/xhtml"></div>`; + } + + connectedCallback() { + super.connectedCallback(); + this.parentNode.addEventListener("select", this); + this.eventListenerParentNode = this.parentNode; + } + + disconnectedCallback() { + this.eventListenerParentNode?.removeEventListener("select", this); + this.eventListenerParentNode = null; + } + + handleEvent(event) { + if (event.type == "select") { + let selectedItem = event.target.selectedItem; + if (selectedItem) { + this.#setStatus(selectedItem); + } + } + } + + #setStatus(item) { + // For normal rows, use that row's comment, otherwise use the status's + // comment which serves as the default label. + let target = + !item || item instanceof MozAutocompleteActionRichlistitem + ? this + : item; + + let comment = JSON.parse(target.getAttribute("ac-comment")); + let statusBox = this.querySelector(".ac-status"); + statusBox.textContent = comment?.status || ""; + } + + _adjustAcItem() { + super._adjustAcItem(); + this.#setStatus(this); + this.setAttribute("disabled", "true"); + } + } + + class MozAutocompleteAutoFillRichlistitem extends MozAutocompleteTwoLineRichlistitem { + constructor() { + super(); + this.selectedByMouseOver = true; + } + + _adjustAcItem() { + let { primary, secondary, ariaLabel } = JSON.parse( + this.getAttribute("ac-value") + ); + + let line1Label = this.querySelector(".line1-label"); + line1Label.textContent = primary.toString(); + + let line2Label = this.querySelector(".line2-label"); + line2Label.textContent = secondary.toString(); + + if (ariaLabel) { + this.setAttribute("aria-label", ariaLabel); + } + + this.querySelector(".ac-site-icon").collapsed = + this.getAttribute("ac-image") == ""; + } + + set selected(val) { + if (val) { + this.setAttribute("selected", "true"); + } else { + this.removeAttribute("selected"); + } + + let { AutoCompleteParent } = ChromeUtils.importESModule( + "resource://gre/actors/AutoCompleteParent.sys.mjs" + ); + + let actor = AutoCompleteParent.getCurrentActor(); + if (!actor) { + return; + } + + let popup = actor.openedPopup; + + setTimeout(() => { + let selectedIndex = popup ? popup.selectedIndex : -1; + actor.manager + .getActor("FormAutofill") + .sendAsyncMessage("FormAutofill:PreviewProfile", { selectedIndex }); + }, 0); + } + + get selected() { + return this.getAttribute("selected") == "true"; + } + } + class MozAutocompleteGeneratedPasswordRichlistitem extends MozAutocompleteTwoLineRichlistitem { constructor() { super(); @@ -840,6 +968,14 @@ ); customElements.define( + "autocomplete-autofill-richlistitem", + MozAutocompleteAutoFillRichlistitem, + { + extends: "richlistitem", + } + ); + + customElements.define( "autocomplete-login-richlistitem", MozAutocompleteLoginRichlistitem, { @@ -848,6 +984,22 @@ ); customElements.define( + "autocomplete-action-richlistitem", + MozAutocompleteActionRichlistitem, + { + extends: "richlistitem", + } + ); + + customElements.define( + "autocomplete-status-richlistitem", + MozAutocompleteStatusRichlistitem, + { + extends: "richlistitem", + } + ); + + customElements.define( "autocomplete-generated-password-richlistitem", MozAutocompleteGeneratedPasswordRichlistitem, { diff --git a/toolkit/content/widgets/button.js b/toolkit/content/widgets/button.js index ce48fac1e9..7ca2eddbed 100644 --- a/toolkit/content/widgets/button.js +++ b/toolkit/content/widgets/button.js @@ -84,7 +84,7 @@ ).toLowerCase(); // If the accesskey of the current button is pressed, just activate it - if (this.accessKey.toLowerCase() == charPressedLower) { + if (this.accessKey?.toLowerCase() == charPressedLower) { this.click(); return; } @@ -201,7 +201,7 @@ while (iterator.nextNode()) { var test = iterator.currentNode; if ( - test.accessKey.toLowerCase() == aAccessKeyLower && + test.accessKey?.toLowerCase() == aAccessKeyLower && !test.disabled && !test.collapsed && !test.hidden diff --git a/toolkit/content/widgets/infobar.css b/toolkit/content/widgets/infobar.css index ee811818b5..7818f1ef1d 100644 --- a/toolkit/content/widgets/infobar.css +++ b/toolkit/content/widgets/infobar.css @@ -32,11 +32,8 @@ } .close { - margin-block: 4px; + margin-block: 2px; margin-inline-start: 8px; - background-size: 12px; - height: 24px; - width: 24px; align-self: flex-start; } } diff --git a/toolkit/content/widgets/mach_commands.py b/toolkit/content/widgets/mach_commands.py index 58a8b8fcda..79398ab11a 100644 --- a/toolkit/content/widgets/mach_commands.py +++ b/toolkit/content/widgets/mach_commands.py @@ -206,3 +206,16 @@ def addstory(command_context, name, project_name, path): html_lit_import=html_lit_import, ) ) + + +@Command( + "buildtokens", + category="misc", + description="Build the design tokens CSS files", +) +def buildtokens(command_context): + run_mach( + command_context, + "npm", + args=["run", "build", "--prefix=toolkit/themes/shared/design-system"], + ) diff --git a/toolkit/content/widgets/marquee.css b/toolkit/content/widgets/marquee.css index b898cd0dce..6cdb52ca02 100644 --- a/toolkit/content/widgets/marquee.css +++ b/toolkit/content/widgets/marquee.css @@ -2,25 +2,19 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -.outerDiv { - overflow: hidden; - width: -moz-available; +slot { + display: block; + will-change: translate; } -.horizontal > .innerDiv { - width: max-content; - /* We want to create overflow of twice our available space. */ - padding: 0 100%; -} - -/* disable scrolling in contenteditable */ -:host(:read-write) .innerDiv { - padding: 0 !important; +/* Disable the animation on contenteditable */ +:host(:read-write) > slot { + translate: none !important; } /* When printing or when the user doesn't want movement, we disable scrolling */ @media print, (prefers-reduced-motion) { - .innerDiv { - padding: 0 !important; + slot { + translate: none !important; } } diff --git a/toolkit/content/widgets/marquee.js b/toolkit/content/widgets/marquee.js index 8b18703b92..a694ffdca1 100644 --- a/toolkit/content/widgets/marquee.js +++ b/toolkit/content/widgets/marquee.js @@ -4,87 +4,25 @@ "use strict"; -/* - * This is the class of entry. It will construct the actual implementation - * according to the value of the "direction" property. - */ this.MarqueeWidget = class { constructor(shadowRoot) { this.shadowRoot = shadowRoot; this.element = shadowRoot.host; - } - - /* - * Callback called by UAWidgets right after constructor. - */ - onsetup() { - this.switchImpl(); - } - - /* - * Callback called by UAWidgetsChild wheen the direction property - * changes. - */ - onchange() { - this.switchImpl(); - } - - switchImpl() { - let newImpl; - switch (this.element.direction) { - case "up": - case "down": - newImpl = MarqueeVerticalImplWidget; - break; - case "left": - case "right": - newImpl = MarqueeHorizontalImplWidget; - break; - } - - // Skip if we are asked to load the same implementation. - // This can happen if the property is set again w/o value change. - if (this.impl && this.impl.constructor == newImpl) { - return; - } - this.teardown(); - if (newImpl) { - this.impl = new newImpl(this.shadowRoot); - this.impl.onsetup(); - } - } - - teardown() { - if (!this.impl) { - return; - } - this.impl.teardown(); - this.shadowRoot.firstChild.remove(); - delete this.impl; - } -}; - -this.MarqueeBaseImplWidget = class { - constructor(shadowRoot) { - this.shadowRoot = shadowRoot; - this.element = shadowRoot.host; this.document = this.element.ownerDocument; this.window = this.document.defaultView; + // This needed for behavior=alternate, in order to know in which of the two + // directions we're going. + this.dirsign = 1; + this._currentLoop = this.element.loop; + this.animation = null; + this._restartScheduled = null; } onsetup() { - this.generateContent(); - - // Set up state. - this._currentDirection = this.element.direction || "left"; - this._currentLoop = this.element.loop; - this.dirsign = 1; - this.startAt = 0; - this.stopAt = 0; - this.newPosition = 0; - this.runId = 0; - this.originalHeight = 0; - this.invalidateCache = true; + // White-space isn't allowed because a marquee could be + // inside 'white-space: pre' + this.shadowRoot.innerHTML = `<link rel="stylesheet" href="chrome://global/content/elements/marquee.css" + /><slot></slot>`; this._mutationObserver = new this.window.MutationObserver(aMutations => this._mutationActor(aMutations) @@ -92,7 +30,7 @@ this.MarqueeBaseImplWidget = class { this._mutationObserver.observe(this.element, { attributes: true, attributeOldValue: true, - attributeFilter: ["loop", "", "behavior", "direction", "width", "height"], + attributeFilter: ["loop", "direction", "behavior"], }); // init needs to be run after the page has loaded in order to calculate @@ -108,12 +46,13 @@ this.MarqueeBaseImplWidget = class { } teardown() { + this.doStop(); this._mutationObserver.disconnect(); - this.window.clearTimeout(this.runId); this.window.removeEventListener("load", this); this.shadowRoot.removeEventListener("marquee-start", this); this.shadowRoot.removeEventListener("marquee-stop", this); + this.shadowRoot.replaceChildren(); } handleEvent(aEvent) { @@ -131,15 +70,26 @@ this.MarqueeBaseImplWidget = class { case "marquee-stop": this.doStop(); break; + case "finish": + this._animationFinished(); + break; } } - get outerDiv() { - return this.shadowRoot.firstChild; - } - - get innerDiv() { - return this.shadowRoot.getElementById("innerDiv"); + _animationFinished() { + let behavior = this.element.behavior; + let shouldLoop = + this._currentLoop > 1 || (this._currentLoop == -1 && behavior != "slide"); + if (shouldLoop) { + if (this._currentLoop > 0) { + this._currentLoop--; + } + if (behavior == "alternate") { + this.dirsign = -this.dirsign; + } + this.doStop(); + this.doStart(); + } } get scrollDelayWithTruespeed() { @@ -149,269 +99,249 @@ this.MarqueeBaseImplWidget = class { return this.element.scrollDelay; } - doStart() { - if (this.runId == 0) { - var lambda = () => this._doMove(false); - this.runId = this.window.setTimeout( - lambda, - this.scrollDelayWithTruespeed - this._deltaStartStop - ); - this._deltaStartStop = 0; - } - } - - doStop() { - if (this.runId != 0) { - this._deltaStartStop = Date.now() - this._lastMoveDate; - this.window.clearTimeout(this.runId); - } - - this.runId = 0; + get slot() { + return this.shadowRoot.lastChild; } - _fireEvent(aName, aBubbles, aCancelable) { - var e = this.document.createEvent("Events"); - e.initEvent(aName, aBubbles, aCancelable); - this.element.dispatchEvent(e); + /** + * Computes CSS-derived values needed to compute the transform of the + * contents. + * + * In particular, it measures the auto width and height of the contents, + * and the effective width and height of the marquee itself, along with its + * css directionality (which affects the effective direction). + */ + getMetrics() { + let slot = this.slot; + slot.style.width = "max-content"; + let slotCS = this.window.getComputedStyle(slot); + let marqueeCS = this.window.getComputedStyle(this.element); + let contentWidth = parseFloat(slotCS.width) || 0; + let contentHeight = parseFloat(slotCS.height) || 0; + let marqueeWidth = parseFloat(marqueeCS.width) || 0; + let marqueeHeight = parseFloat(marqueeCS.height) || 0; + slot.style.width = ""; + return { + contentWidth, + contentHeight, + marqueeWidth, + marqueeHeight, + cssDirection: marqueeCS.direction, + }; } - _doMove(aResetPosition) { - this._lastMoveDate = Date.now(); - - // invalidateCache is true at first load and whenever an attribute - // is changed - if (this.invalidateCache) { - this.invalidateCache = false; // we only want this to run once every scroll direction change - - var corrvalue = 0; - - switch (this._currentDirection) { - case "up": - case "down": { - let height = this.window.getComputedStyle(this.element).height; - this.outerDiv.style.height = height; - if (this.originalHeight > this.outerDiv.offsetHeight) { - corrvalue = this.originalHeight - this.outerDiv.offsetHeight; + /** + * Gets the layout metrics from getMetrics(), and returns an object + * describing the start, end, and axis of the animation for the given marquee + * behavior and direction. + */ + getTransformParameters({ + contentWidth, + contentHeight, + marqueeWidth, + marqueeHeight, + cssDirection, + }) { + const innerWidth = marqueeWidth - contentWidth; + const innerHeight = marqueeHeight - contentHeight; + const dir = this.element.direction; + + let start = 0; + let end = 0; + const axis = dir == "up" || dir == "down" ? "y" : "x"; + switch (this.element.behavior) { + case "alternate": + switch (dir) { + case "up": + case "down": { + if (innerHeight >= 0) { + start = innerHeight; + end = 0; + } else { + start = 0; + end = innerHeight; + } + if (dir == "down") { + [start, end] = [end, start]; + } + if (this.dirsign == -1) { + [start, end] = [end, start]; + } + break; } - this.innerDiv.style.padding = height + " 0"; - let isUp = this._currentDirection == "up"; - if (isUp) { - this.dirsign = 1; - this.startAt = - this.element.behavior == "alternate" - ? this.originalHeight - corrvalue - : 0; - this.stopAt = - this.element.behavior == "alternate" || - this.element.behavior == "slide" - ? parseInt(height) + corrvalue - : this.originalHeight + parseInt(height); - } else { - this.dirsign = -1; - this.startAt = - this.element.behavior == "alternate" - ? parseInt(height) + corrvalue - : this.originalHeight + parseInt(height); - this.stopAt = - this.element.behavior == "alternate" || - this.element.behavior == "slide" - ? this.originalHeight - corrvalue - : 0; + case "right": + case "left": + default: { + if (innerWidth >= 0) { + start = innerWidth; + end = 0; + } else { + start = 0; + end = innerWidth; + } + if (dir == "right") { + [start, end] = [end, start]; + } + if (cssDirection == "rtl") { + [start, end] = [end, start]; + } + if (this.dirsign == -1) { + [start, end] = [end, start]; + } + break; } - break; } - case "left": - case "right": - default: { - let isRight = this._currentDirection == "right"; - // NOTE: It's important to use getComputedStyle() to not account for the padding. - let innerWidth = parseInt( - this.window.getComputedStyle(this.innerDiv).width - ); - if (innerWidth > this.outerDiv.offsetWidth) { - corrvalue = innerWidth - this.outerDiv.offsetWidth; + break; + case "slide": + switch (dir) { + case "up": { + start = marqueeHeight; + end = 0; + break; } - let rtl = - this.window.getComputedStyle(this.element).direction == "rtl"; - if (isRight != rtl) { - this.dirsign = -1; - this.stopAt = - this.element.behavior == "alternate" || - this.element.behavior == "slide" - ? innerWidth - corrvalue - : 0; - this.startAt = - this.outerDiv.offsetWidth + - (this.element.behavior == "alternate" - ? corrvalue - : innerWidth + this.stopAt); - } else { - this.dirsign = 1; - this.startAt = - this.element.behavior == "alternate" ? innerWidth - corrvalue : 0; - this.stopAt = - this.outerDiv.offsetWidth + - (this.element.behavior == "alternate" || - this.element.behavior == "slide" - ? corrvalue - : innerWidth + this.startAt); + case "down": { + start = -contentHeight; + end = innerHeight; + break; } - if (rtl) { - this.startAt = -this.startAt; - this.stopAt = -this.stopAt; - this.dirsign = -this.dirsign; + case "right": + default: { + let isRight = dir == "right"; + if (cssDirection == "rtl") { + isRight = !isRight; + } + if (isRight) { + start = -contentWidth; + end = innerWidth; + } else { + start = marqueeWidth; + end = 0; + } + break; } - break; } - } - - if (aResetPosition) { - this.newPosition = this.startAt; - this._fireEvent("start", false, false); - } - } // end if - - this.newPosition = - this.newPosition + this.dirsign * this.element.scrollAmount; - - if ( - (this.dirsign == 1 && this.newPosition > this.stopAt) || - (this.dirsign == -1 && this.newPosition < this.stopAt) - ) { - switch (this.element.behavior) { - case "alternate": - // lets start afresh - this.invalidateCache = true; - - // swap direction - const swap = { left: "right", down: "up", up: "down", right: "left" }; - this._currentDirection = swap[this._currentDirection] || "left"; - this.newPosition = this.stopAt; - - if ( - this._currentDirection == "up" || - this._currentDirection == "down" - ) { - this.outerDiv.scrollTop = this.newPosition; - } else { - this.outerDiv.scrollLeft = this.newPosition; - } - - if (this._currentLoop != 1) { - this._fireEvent("bounce", false, true); - } - break; - - case "slide": - if (this._currentLoop > 1) { - this.newPosition = this.startAt; + break; + case "scroll": + default: + switch (dir) { + case "up": + case "down": { + start = marqueeHeight; + end = -contentHeight; + if (dir == "down") { + [start, end] = [end, start]; + } + break; } - break; - - default: - this.newPosition = this.startAt; - - if ( - this._currentDirection == "up" || - this._currentDirection == "down" - ) { - this.outerDiv.scrollTop = this.newPosition; - } else { - this.outerDiv.scrollLeft = this.newPosition; + case "right": + case "left": + default: { + start = marqueeWidth; + end = -contentWidth; + if (dir == "right") { + [start, end] = [end, start]; + } + if (cssDirection == "rtl") { + [start, end] = [end, start]; + } + break; } + } + break; + } + return { start, end, axis }; + } - // dispatch start event, even when this._currentLoop == 1, comp. with IE6 - this._fireEvent("start", false, false); + /** + * Measures the marquee contents, and starts the marquee animation if needed. + * The translate animation is applied to the <slot> element. + * Bouncing and looping is implemented in the finish event handler for the + * given animation (see _animationFinished()). + */ + doStart() { + if (this.animation) { + return; + } + let scrollAmount = this.element.scrollAmount; + if (!scrollAmount) { + return; + } + let metrics = this.getMetrics(); + let { axis, start, end } = this.getTransformParameters(metrics); + let duration = + (Math.abs(end - start) * this.scrollDelayWithTruespeed) / scrollAmount; + let startValue = start + "px"; + let endValue = end + "px"; + if (axis == "y") { + startValue = "0 " + startValue; + endValue = "0 " + endValue; + } + // NOTE(emilio): It seems tempting to use `iterations` here, but doing so + // wouldn't be great because this uses current layout values (via + // getMetrics()), so sizes wouldn't update. This way we update once per + // animation iteration. + // + // fill: forwards is needed so that behavior=slide doesn't jump back to the + // start after the animation finishes. + this.animation = this.slot.animate( + { + translate: [startValue, endValue], + }, + { + duration, + easing: "linear", + fill: "forwards", } + ); + this.animation.addEventListener("finish", this, { once: true }); + } - if (this._currentLoop > 1) { - this._currentLoop--; - } else if (this._currentLoop == 1) { - if ( - this._currentDirection == "up" || - this._currentDirection == "down" - ) { - this.outerDiv.scrollTop = this.stopAt; - } else { - this.outerDiv.scrollLeft = this.stopAt; - } - this.element.stop(); - this._fireEvent("finish", false, true); - return; - } - } else if ( - this._currentDirection == "up" || - this._currentDirection == "down" - ) { - this.outerDiv.scrollTop = this.newPosition; - } else { - this.outerDiv.scrollLeft = this.newPosition; + doStop() { + if (!this.animation) { + return; } - - var myThis = this; - var lambda = function myTimeOutFunction() { - myThis._doMove(false); - }; - this.runId = this.window.setTimeout(lambda, this.scrollDelayWithTruespeed); + if (this._restartScheduled) { + this.window.cancelAnimationFrame(this._restartScheduled); + this._restartScheduled = null; + } + this.animation.removeEventListener("finish", this); + this.animation.cancel(); + this.animation = null; } init() { this.element.stop(); - - if (this._currentDirection == "up" || this._currentDirection == "down") { - // store the original height before we add padding - this.innerDiv.style.padding = 0; - this.originalHeight = this.innerDiv.offsetHeight; - } - - this._doMove(true); + this.doStart(); } _mutationActor(aMutations) { while (aMutations.length) { - var mutation = aMutations.shift(); - var attrName = mutation.attributeName.toLowerCase(); - var oldValue = mutation.oldValue; - var target = mutation.target; - var newValue = target.getAttribute(attrName); - - if (oldValue != newValue) { - this.invalidateCache = true; - switch (attrName) { - case "loop": - this._currentLoop = target.loop; - break; - case "direction": - this._currentDirection = target.direction; - break; - } + let mutation = aMutations.shift(); + let attrName = mutation.attributeName.toLowerCase(); + let oldValue = mutation.oldValue; + let newValue = this.element.getAttribute(attrName); + if (oldValue == newValue) { + continue; + } + if (attrName == "loop") { + this._currentLoop = this.element.loop; + } + if (attrName == "direction" || attrName == "behavior") { + this._scheduleRestartIfNeeded(); } } } -}; - -this.MarqueeHorizontalImplWidget = class extends MarqueeBaseImplWidget { - generateContent() { - // White-space isn't allowed because a marquee could be - // inside 'white-space: pre' - this.shadowRoot.innerHTML = `<div class="outerDiv horizontal" - ><link rel="stylesheet" href="chrome://global/content/elements/marquee.css" - /><div class="innerDiv" id="innerDiv" - ><slot - /></div - ></div>`; - } -}; -this.MarqueeVerticalImplWidget = class extends MarqueeBaseImplWidget { - generateContent() { - // White-space isn't allowed because a marquee could be - // inside 'white-space: pre' - this.shadowRoot.innerHTML = `<div class="outerDiv vertical" - ><link rel="stylesheet" href="chrome://global/content/elements/marquee.css" - /><div class="innerDiv" id="innerDiv" - ><slot - /></div - ></div>`; + // Schedule a restart with the new parameters if we're running. + _scheduleRestartIfNeeded() { + if (!this.animation || this._restartScheduled != null) { + return; + } + this._restartScheduled = this.window.requestAnimationFrame(() => { + if (this.animation) { + this.doStop(); + this.doStart(); + } + }); } }; diff --git a/toolkit/content/widgets/menu.js b/toolkit/content/widgets/menu.js index 1a55d799b6..70b9521a27 100644 --- a/toolkit/content/widgets/menu.js +++ b/toolkit/content/widgets/menu.js @@ -19,7 +19,7 @@ this.setAttribute("value", val); } get value() { - return this.getAttribute("value"); + return this.getAttribute("value") || ""; } // nsIDOMXULSelectControlItemElement diff --git a/toolkit/content/widgets/menulist.js b/toolkit/content/widgets/menulist.js index 3672d4ccf1..c7bff9c9d0 100644 --- a/toolkit/content/widgets/menulist.js +++ b/toolkit/content/widgets/menulist.js @@ -124,8 +124,6 @@ this.shadowRoot.appendChild(document.createElement("slot")); } - this.mSelectedInternal = null; - this.mAttributeObserver = null; this.setInitialSelection(); } @@ -153,7 +151,7 @@ // nsIDOMXULSelectControlElement get value() { - return this.getAttribute("value"); + return this.getAttribute("value") || ""; } // nsIDOMXULMenuListElement @@ -163,12 +161,12 @@ // nsIDOMXULMenuListElement get image() { - return this.getAttribute("image"); + return this.getAttribute("image") || ""; } // nsIDOMXULMenuListElement get label() { - return this.getAttribute("label"); + return this.getAttribute("label") || ""; } set description(val) { @@ -176,7 +174,7 @@ } get description() { - return this.getAttribute("description"); + return this.getAttribute("description") || ""; } // nsIDOMXULMenuListElement @@ -292,6 +290,10 @@ if (this.getAttribute("noinitialselection") === "true") { return; } + + this.mSelectedInternal = null; + this.mAttributeObserver = null; + var popup = this.menupopup; if (popup) { var arr = popup.getElementsByAttribute("selected", "true"); diff --git a/toolkit/content/widgets/moz-button/moz-button.css b/toolkit/content/widgets/moz-button/moz-button.css index 47567df41d..71d57ea93a 100644 --- a/toolkit/content/widgets/moz-button/moz-button.css +++ b/toolkit/content/widgets/moz-button/moz-button.css @@ -4,6 +4,8 @@ :host { display: inline-block; + height: fit-content; + width: fit-content; } button { @@ -133,6 +135,7 @@ button { width: var(--button-size-icon); height: var(--button-size-icon); padding: var(--button-padding-icon); + color: var(--icon-color); &[size=small] { width: var(--button-size-icon-small); diff --git a/toolkit/content/widgets/moz-button/moz-button.mjs b/toolkit/content/widgets/moz-button/moz-button.mjs index 3e7c151e61..a951dbf5d8 100644 --- a/toolkit/content/widgets/moz-button/moz-button.mjs +++ b/toolkit/content/widgets/moz-button/moz-button.mjs @@ -68,6 +68,11 @@ export default class MozButton extends MozLitElement { } } + // Delegate clicks on host to the button element. + click() { + this.buttonEl.click(); + } + render() { return html` <link diff --git a/toolkit/content/widgets/moz-card/moz-card.css b/toolkit/content/widgets/moz-card/moz-card.css index 52c0ac0980..da5656e53e 100644 --- a/toolkit/content/widgets/moz-card/moz-card.css +++ b/toolkit/content/widgets/moz-card/moz-card.css @@ -7,7 +7,7 @@ --card-border-radius: var(--border-radius-medium); --card-border-width: var(--border-width); --card-border: var(--card-border-width) solid var(--card-border-color); - --card-background-color: var(--box-background-color); + --card-background-color: var(--background-color-box); --card-focus-outline: var(--focus-outline); --card-box-shadow: var(--box-shadow-10); /* Bug 1791816, 1839523: replace with spacing tokens */ @@ -17,9 +17,9 @@ /* Bug 1791816: replace with button tokens */ @media (prefers-contrast) { - --button-border-color: var(--border-interactive-color); - --button-border-color-hover: var(--border-interactive-color-hover); - --button-border-color-active: var(--border-interactive-color-active); + --button-border-color: var(--border-color-interactive); + --button-border-color-hover: var(--border-color-interactive-hover); + --button-border-color-active: var(--border-color-interactive-active); --card-border-color: color-mix(in srgb, currentColor 41%, transparent); } /* Bug 1791816: replace with button tokens */ @@ -27,9 +27,9 @@ --button-background-color: ButtonFace; --button-background-color-hover: SelectedItemText; --button-background-color-active: SelectedItemText; - --button-border-color: var(--border-interactive-color); - --button-border-color-hover: var(--border-interactive-color-hover); - --button-border-color-active: var(--border-interactive-color-active); + --button-border-color: var(--border-color-interactive); + --button-border-color-hover: var(--border-color-interactive-hover); + --button-border-color-active: var(--border-color-interactive-active); --button-text-color: ButtonText; --button-text-color-hover: SelectedItem; --button-text-color-active: SelectedItem; @@ -81,6 +81,29 @@ summary { gap: var(--card-gap); padding-inline: var(--card-padding); border-radius: var(--card-border-radius); + + .chevron-icon { + background-image: url("chrome://global/skin/icons/arrow-down.svg"); + + details[open] & { + background-image: url("chrome://global/skin/icons/arrow-up.svg"); + } + } + + .chevron-icon, + #heading-icon { + background-position: center; + background-repeat: no-repeat; + -moz-context-properties: fill; + fill: currentColor; + width: 24px; + height: 24px; + min-width: 24px; + min-height: 24px; + padding: 0; + flex-shrink: 0; + align-self: flex-start; + } } #heading { @@ -159,22 +182,3 @@ details { outline: var(--card-focus-outline); } } - -.chevron-icon { - background-image: url("chrome://global/skin/icons/arrow-down.svg"); - background-position: center; - background-repeat: no-repeat; - -moz-context-properties: fill; - fill: currentColor; - width: 24px; - height: 24px; - min-width: 24px; - min-height: 24px; - padding: 0; - flex-shrink: 0; - align-self: flex-start; - - details[open] & { - background-image: url("chrome://global/skin/icons/arrow-up.svg"); - } -} diff --git a/toolkit/content/widgets/moz-card/moz-card.mjs b/toolkit/content/widgets/moz-card/moz-card.mjs index 2bd6e0f987..5687608975 100644 --- a/toolkit/content/widgets/moz-card/moz-card.mjs +++ b/toolkit/content/widgets/moz-card/moz-card.mjs @@ -3,7 +3,11 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { html, ifDefined } from "chrome://global/content/vendor/lit.all.mjs"; +import { + html, + ifDefined, + when, +} from "chrome://global/content/vendor/lit.all.mjs"; import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; /** @@ -18,6 +22,7 @@ import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; * * * @property {string} heading - The heading text that will be used for the card. + * @property {string} icon - (optional) A flag to indicate the header should include an icon * @property {string} type - (optional) The type of card. No type specified * will be the default card. The other available type is "accordion" * @slot content - The content to show inside of the card. @@ -31,6 +36,7 @@ export default class MozCard extends MozLitElement { static properties = { heading: { type: String }, + icon: { type: Boolean }, type: { type: String, reflect: true }, expanded: { type: Boolean }, }; @@ -47,9 +53,15 @@ export default class MozCard extends MozLitElement { } return html` <div id="heading-wrapper"> - ${this.type == "accordion" - ? html` <div class="chevron-icon"></div>` - : ""} + ${when( + this.type == "accordion", + () => html`<div class="chevron-icon"></div>` + )} + ${when( + this.icon, + () => + html`<div part="icon" id="heading-icon" role="presentation"></div>` + )} <span id="heading">${this.heading}</span> </div> `; diff --git a/toolkit/content/widgets/moz-card/moz-card.stories.mjs b/toolkit/content/widgets/moz-card/moz-card.stories.mjs index da3279b2a4..0430956a52 100644 --- a/toolkit/content/widgets/moz-card/moz-card.stories.mjs +++ b/toolkit/content/widgets/moz-card/moz-card.stories.mjs @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // eslint-disable-next-line import/no-unresolved -import { html, ifDefined } from "lit.all.mjs"; +import { classMap, html, ifDefined } from "lit.all.mjs"; // eslint-disable-next-line import/no-unassigned-import import "./moz-card.mjs"; @@ -15,6 +15,8 @@ export default { fluent: ` moz-card-heading = .heading = This is the label +moz-card-heading-with-icon = + .heading = This is a card with a heading icon `, }, argTypes: { @@ -22,13 +24,27 @@ moz-card-heading = options: ["default", "accordion"], control: { type: "select" }, }, + hasHeadingIcon: { + options: [true, false], + control: { type: "select" }, + }, }, }; -const Template = ({ l10nId, content, type }) => html` - <main style="max-width: 400px"> +const Template = ({ l10nId, content, type, hasHeadingIcon }) => html` + <style> + main { + max-width: 400px; + } + moz-card.headingWithIcon::part(icon) { + background-image: url("chrome://browser/skin/preferences/category-general.svg"); + } + </style> + <main> <moz-card type=${ifDefined(type)} + ?icon=${hasHeadingIcon} + class=${classMap({ headingWithIcon: hasHeadingIcon })} data-l10n-id=${ifDefined(l10nId)} data-l10n-attrs="heading" > @@ -86,3 +102,13 @@ CardTypeAccordion.parameters = { }, }, }; + +export const CardWithHeadingIcon = Template.bind({}); +CardWithHeadingIcon.args = { + l10nId: "moz-card-heading-with-icon", + content: `Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Nunc velit turpis, mollis a ultricies vitae, accumsan ut augue. + In a eros ac dolor hendrerit varius et at mauris.`, + type: "default", + hasHeadingIcon: true, +}; diff --git a/toolkit/content/widgets/moz-message-bar/moz-message-bar.css b/toolkit/content/widgets/moz-message-bar/moz-message-bar.css index 6d35009982..fa7b08612d 100644 --- a/toolkit/content/widgets/moz-message-bar/moz-message-bar.css +++ b/toolkit/content/widgets/moz-message-bar/moz-message-bar.css @@ -6,14 +6,7 @@ /* Icon */ --message-bar-icon-color: var(--icon-color-information); --message-bar-icon-size: var(--size-item-small); - --message-bar-icon-close-color: var(--icon-color); - --message-bar-icon-close-url: url("chrome://global/skin/icons/close-12.svg"); - - /* Button */ - --message-bar-button-size-ghost: var(--button-min-height); - --message-bar-button-border-radius-ghost: var(--button-border-radius); - --message-bar-button-background-color-ghost-hover: var(--button-background-color-hover); - --message-bar-button-background-color-ghost-active: var(--button-background-color-active); + --message-bar-icon-close-url: url("chrome://global/skin/icons/close.svg"); /* Container */ --message-bar-container-min-height: var(--size-item-large); @@ -28,12 +21,13 @@ --message-bar-text-line-height: 1.5em; /* Background */ - --message-bar-background-color: var(--color-background-information); + --message-bar-background-color: var(--background-color-information); background-color: var(--message-bar-background-color); border: var(--message-bar-border-width) solid var(--message-bar-border-color); border-radius: var(--message-bar-border-radius); color: var(--message-bar-text-color); + text-align: start; } @media (prefers-contrast) { @@ -144,31 +138,8 @@ /* Close icon styles */ -.close { +moz-button::part(button) { background-image: var(--message-bar-icon-close-url); - background-repeat: no-repeat; - background-position: center center; - -moz-context-properties: fill; - fill: currentColor; - min-width: auto; - min-height: auto; - width: var(--message-bar-button-size-ghost); - height: var(--message-bar-button-size-ghost); - margin: 0; - padding: 0; - flex-shrink: 0; -} - -.ghost-button { - border-radius: var(--message-bar-button-border-radius-ghost); -} - -.ghost-button:enabled:hover { - background-color: var(--message-bar-button-background-color-ghost-hover); -} - -.ghost-button:enabled:hover:active { - background-color: var(--message-bar-button-background-color-ghost-active); } @media not (prefers-contrast) { @@ -176,7 +147,7 @@ /* Colors from: https://www.figma.com/file/zd3B9UyknB2XNZNdrYLm2W/Outreachy?type=design&node-id=59-1921&mode=design&t=ZYS4e6pAbAlXGvun-4 */ :host([type=warning]) { - --message-bar-background-color: var(--color-background-warning); + --message-bar-background-color: var(--background-color-warning); .icon { --message-bar-icon-color: var(--icon-color-warning); @@ -184,7 +155,7 @@ } :host([type=success]) { - --message-bar-background-color: var(--color-background-success); + --message-bar-background-color: var(--background-color-success); .icon { --message-bar-icon-color: var(--icon-color-success); @@ -193,19 +164,10 @@ :host([type=error]), :host([type=critical]) { - --message-bar-background-color: var(--color-background-critical); + --message-bar-background-color: var(--background-color-critical); .icon { --message-bar-icon-color: var(--icon-color-critical); } } - - .close { - fill: var(--message-bar-icon-close-color); - } - - .ghost-button { - border: none; - background-color: transparent; - } } diff --git a/toolkit/content/widgets/moz-message-bar/moz-message-bar.mjs b/toolkit/content/widgets/moz-message-bar/moz-message-bar.mjs index d83de5d29f..8f0c997149 100644 --- a/toolkit/content/widgets/moz-message-bar/moz-message-bar.mjs +++ b/toolkit/content/widgets/moz-message-bar/moz-message-bar.mjs @@ -4,6 +4,8 @@ import { html, ifDefined } from "../vendor/lit.all.mjs"; import { MozLitElement } from "../lit-utils.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://global/content/elements/moz-button.mjs"; const messageTypeToIconData = { info: { @@ -41,7 +43,7 @@ const messageTypeToIconData = { * @property {string} messageL10nArgs - Any args needed for the message l10n ID. * @fires message-bar:close * Custom event indicating that message bar was closed. - * @fires message-bar:user-dismissed + * @fires message-bar:user-dismissed * Custom event indicating that message bar was dismissed by the user. */ @@ -49,7 +51,7 @@ export default class MozMessageBar extends MozLitElement { static queries = { actionsSlotEl: "slot[name=actions]", actionsEl: ".actions", - closeButtonEl: "button.close", + closeButtonEl: "moz-button.close", supportLinkSlotEl: "slot[name=support-link]", }; @@ -113,14 +115,16 @@ export default class MozMessageBar extends MozLitElement { return ""; } - closeButtonTemplate() { + closeButtonTemplate({ size } = {}) { if (this.dismissable) { return html` - <button - class="close ghost-button" + <moz-button + type="icon ghost" + class="close" + size=${ifDefined(size)} data-l10n-id="moz-message-bar-close-button" @click=${this.dismiss} - ></button> + ></moz-button> `; } return ""; diff --git a/toolkit/content/widgets/moz-page-nav/README.stories.md b/toolkit/content/widgets/moz-page-nav/README.stories.md new file mode 100644 index 0000000000..800d446478 --- /dev/null +++ b/toolkit/content/widgets/moz-page-nav/README.stories.md @@ -0,0 +1,98 @@ +# MozPageNav + +`moz-page-nav` is a grouping of navigation buttons that is displayed at the page level, +intended to change the selected view, provide a heading, and have links to external resources. + +```html story +<moz-page-nav heading="This is a nav" style={{ '--page-nav-margin-top': 0, '--page-nav-margin-bottom': 0, height: '200px' }}> + <moz-page-nav-button + view="view-one" + iconSrc="chrome://browser/skin/preferences/category-general.svg" + > + <p style={{ margin: 0 }}>Test 1</p> + </moz-page-nav-button> + <moz-page-nav-button + view="view-two" + iconSrc="chrome://browser/skin/preferences/category-general.svg" + > + <p style={{ margin: 0 }}>Test 2</p> + </moz-page-nav-button> + <moz-page-nav-button + view="view-three" + iconSrc="chrome://browser/skin/preferences/category-general.svg" + > + <p style={{ margin: 0 }}>Test 3</p> + </moz-page-nav-button> +</moz-page-nav> +``` + +## When to use + +* Use moz-page-nav for single-page navigation to switch between different views. +* moz-page-nav will also support footer buttons for external support links in the future (See [bug 1877826](https://bugzilla.mozilla.org/show_bug.cgi?id=1877826)) +* This component will be used in about: pages such as about:firefoxview, about:preferences, about:addons, about:debugging, etc. + +## When not to use + +* If you need a navigation menu that does not switch between views within a single page + +## Code + +The source for `moz-page-nav` and `moz-page-nav-button` can be found under +[toolkit/content/widgets/moz-page-nav](https://searchfox.org/mozilla-central/source/toolkit/content/widgets/moz-page-nav). +You can find an examples of `moz-page-nav` in use in the Firefox codebase in +[about:firefoxview](https://searchfox.org/mozilla-central/source/browser/components/firefoxview/firefoxview.html#52-87). + +`moz-page-nav` can be imported into `.html`/`.xhtml` files: + +```html +<script type="module" src="chrome://global/content/elements/moz-page-nav.mjs"></script> +``` + +And used as follows: + +```html +<moz-page-nav> + <moz-page-nav-button + view="A name for the first view" + iconSrc="A url for the icon for the first navigation button"> + </moz-page-nav-button> + <moz-page-nav-button + view="A name for the second view" + iconSrc="A url for the icon for the second navigation button"> + </moz-page-nav-button> + <moz-page-nav-button + view="A name for the third view" + iconSrc="A url for the icon for the third navigation button"> + </moz-page-nav-button> +</moz-page-nav> +``` + +### Fluent usage + +Generally the `heading` property of +`moz-page-nav` will be provided via [Fluent attributes](https://mozilla-l10n.github.io/localizer-documentation/tools/fluent/basic_syntax.html#attributes). +To get this working you will need to specify a `data-l10n-id` as well as +`data-l10n-attrs` if you're providing a heading: + +```html +<moz-page-nav data-l10n-id="with-heading" + data-l10n-attrs="heading"></moz-page-nav> +``` + +In which case your Fluent messages will look something like this: + +``` +with-heading = + .heading = Heading text goes here +``` + +You also need to specify a `data-l10n-id` for each `moz-page-nav-button`: + +```html +<moz-page-nav-button data-l10n-id="with-button-text"></moz-page-nav-button> +``` + +``` +with-button-text = button text goes here +``` diff --git a/toolkit/content/widgets/moz-page-nav/moz-page-nav-button.css b/toolkit/content/widgets/moz-page-nav/moz-page-nav-button.css index 2975bb1a7c..5d00198d65 100644 --- a/toolkit/content/widgets/moz-page-nav/moz-page-nav-button.css +++ b/toolkit/content/widgets/moz-page-nav/moz-page-nav-button.css @@ -13,7 +13,7 @@ button { background-color: var(--page-nav-button-background-color); border: 1px solid var(--page-nav-border-color); -moz-context-properties: fill, fill-opacity; - fill: currentColor; + fill: var(--icon-color); display: grid; grid-template-columns: min-content 1fr; gap: 12px; @@ -34,34 +34,35 @@ button:hover { @media not (prefers-contrast) { button { - border-inline-start: 2px solid transparent; - border-inline-end: none; - border-block: none; + position: relative; } - button:hover, - button[selected]:hover { - background-color: var(--page-nav-button-background-color-hover); + button::before { + content: ""; + display: block; + position: absolute; + inset-block: 0; + inset-inline-start: 0; + width: 2px; + background-color: transparent; } - button[selected]:hover { - border-inline-start-color: inherit; + button[selected]::before { + background-color: var(--color-accent-primary); } - button[selected], - button[selected]:hover { - border-inline-start: 2px solid; + button[selected]:hover::before { + background-color: var(--icon-color); } - button[selected]:not(:focus-visible) { - border-start-start-radius: 0; - border-end-start-radius: 0; + button:hover, + button[selected]:hover { + background-color: var(--page-nav-button-background-color-hover); } button[selected]:not(:hover) { color: var(--color-accent-primary); background-color: color-mix(in srgb, var(--page-nav-button-background-color-selected) 5%, transparent); - border-inline-start-color: var(--color-accent-primary); } } @@ -74,7 +75,7 @@ button:hover { button:focus-visible, button[selected]:focus-visible { outline: var(--focus-outline); - outline-offset: var(--focus-outline-offset); + outline-offset: 0; border-radius: var(--border-radius-small); } diff --git a/toolkit/content/widgets/moz-page-nav/moz-page-nav.css b/toolkit/content/widgets/moz-page-nav/moz-page-nav.css index 49000f622d..8e6724b2f5 100644 --- a/toolkit/content/widgets/moz-page-nav/moz-page-nav.css +++ b/toolkit/content/widgets/moz-page-nav/moz-page-nav.css @@ -20,6 +20,7 @@ position: sticky; top: 0; height: 100vh; + display: block; width: var(--page-nav-width); @media (prefers-reduced-motion) { @@ -38,7 +39,7 @@ nav { grid-template-rows: min-content 1fr auto; gap: var(--page-nav-gap); margin-block: var(--page-nav-margin-top) var(--page-nav-margin-bottom); - height: calc(100vh - var(--page-nav-margin-top) - var(--page-nav-margin-bottom)); + height: calc(100% - var(--page-nav-margin-top) - var(--page-nav-margin-bottom)); @media (max-width: 52rem) { grid-template-rows: 1fr auto; } diff --git a/toolkit/content/widgets/moz-toggle/moz-toggle.css b/toolkit/content/widgets/moz-toggle/moz-toggle.css index 2005544181..ba98e80021 100644 --- a/toolkit/content/widgets/moz-toggle/moz-toggle.css +++ b/toolkit/content/widgets/moz-toggle/moz-toggle.css @@ -37,7 +37,7 @@ --toggle-background-color-pressed: var(--color-accent-primary); --toggle-background-color-pressed-hover: var(--color-accent-primary-hover); --toggle-background-color-pressed-active: var(--color-accent-primary-active); - --toggle-border-color: var(--border-interactive-color); + --toggle-border-color: var(--border-color-interactive); --toggle-border-color-hover: var(--toggle-border-color); --toggle-border-color-active: var(--toggle-border-color); --toggle-border-radius: var(--border-radius-circle); @@ -47,7 +47,7 @@ --toggle-dot-background-color: var(--toggle-border-color); --toggle-dot-background-color-hover: var(--toggle-dot-background-color); --toggle-dot-background-color-active: var(--toggle-dot-background-color); - --toggle-dot-background-color-on-pressed: var(--color-canvas); + --toggle-dot-background-color-on-pressed: var(--background-color-canvas); --toggle-dot-margin: 1px; --toggle-dot-height: calc(var(--toggle-height) - 2 * var(--toggle-dot-margin) - 2 * var(--toggle-border-width)); --toggle-dot-width: var(--toggle-dot-height); @@ -77,7 +77,7 @@ border-color: var(--toggle-border-color); } -.toggle-button:enabled:active { +.toggle-button:enabled:hover:active { background: var(--toggle-background-color-active); border-color: var(--toggle-border-color); } @@ -92,7 +92,7 @@ border-color: transparent; } -.toggle-button[aria-pressed="true"]:enabled:active { +.toggle-button[aria-pressed="true"]:enabled:hover:active { background: var(--toggle-background-color-pressed-active); border-color: transparent; } @@ -114,7 +114,7 @@ } .toggle-button[aria-pressed="true"]:enabled:hover::before, -.toggle-button[aria-pressed="true"]:enabled:active::before { +.toggle-button[aria-pressed="true"]:enabled:hover:active::before { background-color: var(--toggle-dot-background-color-on-pressed); } @@ -149,7 +149,7 @@ border-color: var(--toggle-border-color-hover); } - .toggle-button:enabled:active { + .toggle-button:enabled:hover:active { border-color: var(--toggle-border-color-active); } @@ -163,13 +163,13 @@ border-color: var(--toggle-border-color-hover); } - .toggle-button[aria-pressed="true"]:enabled:active { + .toggle-button[aria-pressed="true"]:enabled:hover:active { background-color: var(--toggle-dot-background-color-active); border-color: var(--toggle-dot-background-color-hover); } .toggle-button:hover::before, - .toggle-button:active::before { + .toggle-button:hover:active::before { background-color: var(--toggle-dot-background-color-hover); } } @@ -181,9 +181,9 @@ --toggle-dot-background-color-active: var(--color-accent-primary-active); --toggle-dot-background-color-on-pressed: var(--button-background-color); --toggle-background-color-disabled: var(--button-background-color-disabled); - --toggle-border-color-hover: var(--border-interactive-color-hover); - --toggle-border-color-active: var(--border-interactive-color-active); - --toggle-border-color-disabled: var(--border-interactive-color-disabled); + --toggle-border-color-hover: var(--border-color-interactive-hover); + --toggle-border-color-active: var(--border-color-interactive-active); + --toggle-border-color-disabled: var(--border-color-interactive-disabled); } .toggle-button[aria-pressed="true"]:enabled::after { @@ -197,7 +197,7 @@ inset: -2px; } - .toggle-button[aria-pressed="true"]:enabled:active::after { + .toggle-button[aria-pressed="true"]:enabled:hover:active::after { border-color: var(--toggle-border-color-active); } } diff --git a/toolkit/content/widgets/notificationbox.js b/toolkit/content/widgets/notificationbox.js index 0161588853..8cfc7b865c 100644 --- a/toolkit/content/widgets/notificationbox.js +++ b/toolkit/content/widgets/notificationbox.js @@ -663,6 +663,10 @@ } } + closeButtonTemplate() { + return super.closeButtonTemplate({ size: "small" }); + } + #setStyles() { let style = document.createElement("link"); style.rel = "stylesheet"; diff --git a/toolkit/content/widgets/radio.js b/toolkit/content/widgets/radio.js index 41e8a945ba..52ace9bf6b 100644 --- a/toolkit/content/widgets/radio.js +++ b/toolkit/content/widgets/radio.js @@ -214,7 +214,7 @@ } get value() { - return this.getAttribute("value"); + return this.getAttribute("value") || ""; } set disabled(val) { @@ -526,7 +526,7 @@ } get value() { - return this.getAttribute("value"); + return this.getAttribute("value") || ""; } get selected() { diff --git a/toolkit/content/widgets/richlistbox.js b/toolkit/content/widgets/richlistbox.js index 01d970e6ed..0d669c4c5e 100644 --- a/toolkit/content/widgets/richlistbox.js +++ b/toolkit/content/widgets/richlistbox.js @@ -109,14 +109,14 @@ for (var i = 0; i < rowCount; i++) { var k = (start + i) % rowCount; var listitem = this.getItemAtIndex(k); - if (!this._canUserSelect(listitem)) { + if (!this.canUserSelect(listitem)) { continue; } // allow richlistitems to specify the string being searched for var searchText = "searchLabel" in listitem ? listitem.searchLabel - : listitem.getAttribute("label"); // (see also bug 250123) + : listitem.getAttribute("label") || ""; // (see also bug 250123) searchText = searchText.substring(0, length).toLowerCase(); if (searchText == incrementalString) { this.ensureIndexIsVisible(k); @@ -227,7 +227,7 @@ this.setAttribute("seltype", val); } get selType() { - return this.getAttribute("seltype"); + return this.getAttribute("seltype") || ""; } // nsIDOMXULSelectControlElement @@ -337,7 +337,7 @@ if ( aStartItem && aStartItem.localName == "richlistitem" && - (!this._userSelecting || this._canUserSelect(aStartItem)) + (!this._userSelecting || this.canUserSelect(aStartItem)) ) { --aDelta; if (aDelta == 0) { @@ -354,7 +354,7 @@ if ( aStartItem && aStartItem.localName == "richlistitem" && - (!this._userSelecting || this._canUserSelect(aStartItem)) + (!this._userSelecting || this.canUserSelect(aStartItem)) ) { --aDelta; if (aDelta == 0) { @@ -771,7 +771,7 @@ var newItem = this.getItemAtIndex(newIndex); // make sure that the item is actually visible/selectable - if (this._userSelecting && newItem && !this._canUserSelect(newItem)) { + if (this._userSelecting && newItem && !this.canUserSelect(newItem)) { newItem = aOffset > 0 ? this.getNextItem(newItem, 1) || this.getPreviousItem(newItem, 1) @@ -811,7 +811,11 @@ } } - _canUserSelect(aItem) { + canUserSelect(aItem) { + if (aItem.disabled) { + return false; + } + var style = document.defaultView.getComputedStyle(aItem); return ( style.display != "none" && @@ -886,7 +890,7 @@ */ this.addEventListener("mousedown", event => { var control = this.control; - if (!control || control.disabled) { + if (!control || this.disabled || control.disabled) { return; } if ( @@ -912,7 +916,7 @@ } var control = this.control; - if (!control || control.disabled) { + if (!control || this.disabled || control.disabled) { return; } control._userSelecting = true; @@ -977,7 +981,7 @@ } get value() { - return this.getAttribute("value"); + return this.getAttribute("value") || ""; } /** diff --git a/toolkit/content/widgets/tabbox.js b/toolkit/content/widgets/tabbox.js index b1b2ddecce..f4003d002c 100644 --- a/toolkit/content/widgets/tabbox.js +++ b/toolkit/content/widgets/tabbox.js @@ -459,7 +459,7 @@ } get value() { - return this.getAttribute("value"); + return this.getAttribute("value") || ""; } get control() { @@ -563,7 +563,7 @@ } get value() { - return this.getAttribute("value"); + return this.getAttribute("value") || ""; } get tabbox() { diff --git a/toolkit/content/widgets/tree.js b/toolkit/content/widgets/tree.js index 4993bef0c2..a871d3396f 100644 --- a/toolkit/content/widgets/tree.js +++ b/toolkit/content/widgets/tree.js @@ -1078,7 +1078,7 @@ } get selType() { - return this.getAttribute("seltype"); + return this.getAttribute("seltype") || ""; } set currentIndex(val) { diff --git a/toolkit/content/widgets/wizard.js b/toolkit/content/widgets/wizard.js index c4285fada5..8a3b9c164d 100644 --- a/toolkit/content/widgets/wizard.js +++ b/toolkit/content/widgets/wizard.js @@ -222,7 +222,7 @@ cp && ((this._accessMethod == "sequential" && cp.pageIndex == this.pageCount - 1) || - (this._accessMethod == "random" && cp.next == "")) + (this._accessMethod == "random" && !cp.next)) ); } @@ -381,7 +381,7 @@ aPage.pageIndex = this.pageCount; this.pageCount += 1; if (!this._accessMethod) { - this._accessMethod = aPage.next == "" ? "sequential" : "random"; + this._accessMethod = aPage.next ? "random" : "sequential"; } if (!this._maybeStartWizard() && this._hasStarted) { // If the wizard has already started, adding a page might require diff --git a/toolkit/content/xul.css b/toolkit/content/xul.css index ead9997295..908132d598 100644 --- a/toolkit/content/xul.css +++ b/toolkit/content/xul.css @@ -389,10 +389,6 @@ tooltip { pointer-events: none; } -tooltip:not([position]) { - margin-top: 21px; -} - /** * It's important that these styles are in a UA sheet, because the default * tooltip is native anonymous content diff --git a/toolkit/crashreporter/CrashAnnotations.yaml b/toolkit/crashreporter/CrashAnnotations.yaml index 8f6bb2e8c2..61da061768 100644 --- a/toolkit/crashreporter/CrashAnnotations.yaml +++ b/toolkit/crashreporter/CrashAnnotations.yaml @@ -753,6 +753,12 @@ QuotaManagerShutdownTimeout: type: string ping: true +QuotaManagerStorageIsNetworkResource: + description: > + On Windows, this indicates if QM's base dir lives on a network resource. + It is the direct result of the Win32 API function PathIsNetworkPath. + type: boolean + RDDProcessStatus: description: > Status of the RDD process, can be set to "Running" or "Destroyed" diff --git a/toolkit/crashreporter/CrashSubmit.sys.mjs b/toolkit/crashreporter/CrashSubmit.sys.mjs index 28647c5a83..72ff5dc30f 100644 --- a/toolkit/crashreporter/CrashSubmit.sys.mjs +++ b/toolkit/crashreporter/CrashSubmit.sys.mjs @@ -2,8 +2,6 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { FileUtils } from "resource://gre/modules/FileUtils.sys.mjs"; - const SUCCESS = "success"; const FAILED = "failed"; const SUBMITTING = "submitting"; @@ -13,81 +11,6 @@ const UUID_REGEX = const SUBMISSION_REGEX = /^bp-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; -// TODO: this is still synchronous; need an async INI parser to make it async -function parseINIStrings(path) { - let file = new FileUtils.File(path); - let factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].getService( - Ci.nsIINIParserFactory - ); - let parser = factory.createINIParser(file); - let obj = {}; - for (let key of parser.getKeys("Strings")) { - obj[key] = parser.getString("Strings", key); - } - return obj; -} - -// Since we're basically re-implementing (with async) part of the crashreporter -// client here, we'll just steal the strings we need from crashreporter.ini -async function getL10nStrings() { - let path = PathUtils.join( - Services.dirsvc.get("GreD", Ci.nsIFile).path, - "crashreporter.ini" - ); - let pathExists = await IOUtils.exists(path); - - if (!pathExists) { - // we if we're on a mac - let parentDir = PathUtils.parent(path); - path = PathUtils.join( - parentDir, - "MacOS", - "crashreporter.app", - "Contents", - "Resources", - "crashreporter.ini" - ); - - let pathExists = await IOUtils.exists(path); - - if (!pathExists) { - // This happens on Android where everything is in an APK. - // Android users can't see the contents of the submitted files - // anyway, so just hardcode some fallback strings. - return { - crashid: "Crash ID: %s", - reporturl: "You can view details of this crash at %s", - }; - } - } - - let crstrings = parseINIStrings(path); - let strings = { - crashid: crstrings.CrashID, - reporturl: crstrings.CrashDetailsURL, - }; - - path = PathUtils.join( - Services.dirsvc.get("XCurProcD", Ci.nsIFile).path, - "crashreporter-override.ini" - ); - pathExists = await IOUtils.exists(path); - - if (pathExists) { - crstrings = parseINIStrings(path); - - if ("CrashID" in crstrings) { - strings.crashid = crstrings.CrashID; - } - - if ("CrashDetailsURL" in crstrings) { - strings.reporturl = crstrings.CrashDetailsURL; - } - } - - return strings; -} - function getDir(name) { let uAppDataPath = Services.dirsvc.get("UAppData", Ci.nsIFile).path; return PathUtils.join(uAppDataPath, "Crash Reports", name); @@ -109,11 +32,17 @@ function getPendingMinidump(id) { } async function writeSubmittedReportAsync(crashID, viewURL) { - let strings = await getL10nStrings(); - let data = strings.crashid.replace("%s", crashID); - + // Since we're basically re-implementing (with async) part of the + // crashreporter client here, we'll use the strings we need from the + // crashreporter fluent file. + const l10n = new Localization(["crashreporter/crashreporter.ftl"]); + let data = await l10n.formatValue("crashreporter-crash-identifier", { + id: crashID, + }); if (viewURL) { - data += "\n" + strings.reporturl.replace("%s", viewURL); + data += + "\n" + + (await l10n.formatValue("crashreporter-crash-details", { url: viewURL })); } await writeFileAsync("submitted", `${crashID}.txt`, data); @@ -259,7 +188,7 @@ Submitter.prototype = { let manager = Services.crashmanager; let submissionID = manager.generateSubmissionID(); - xhr.addEventListener("readystatechange", evt => { + xhr.addEventListener("readystatechange", () => { if (xhr.readyState == 4) { let ret = xhr.status === 200 ? this.parseResponse(xhr.responseText) : {}; diff --git a/toolkit/crashreporter/client/Throbber-small.avi b/toolkit/crashreporter/client/Throbber-small.avi Binary files differdeleted file mode 100644 index 640ea62c0e..0000000000 --- a/toolkit/crashreporter/client/Throbber-small.avi +++ /dev/null diff --git a/toolkit/crashreporter/client/Throbber-small.gif b/toolkit/crashreporter/client/Throbber-small.gif Binary files differdeleted file mode 100644 index cce32f20f4..0000000000 --- a/toolkit/crashreporter/client/Throbber-small.gif +++ /dev/null diff --git a/toolkit/crashreporter/client/app/Cargo.toml b/toolkit/crashreporter/client/app/Cargo.toml new file mode 100644 index 0000000000..381ed32f97 --- /dev/null +++ b/toolkit/crashreporter/client/app/Cargo.toml @@ -0,0 +1,66 @@ +[package] +name = "crashreporter" +version = "1.0.0" +edition = "2021" + +[[bin]] +name = "crashreporter" + +[dependencies] +anyhow = "1.0" +cfg-if = "1.0" +env_logger = { version = "0.10", default-features = false } +fluent = "0.16.0" +intl-memoizer = "0.5" +libloading = "0.7" +log = "0.4.17" +mozbuild = "0.1" +mozilla-central-workspace-hack = { version = "0.1", features = ["crashreporter"], optional = true } +once_cell = "1" +phf = "0.11" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +sha2 = "0.10" +time = { version = "0.3", features = ["formatting", "serde"] } +unic-langid = { version = "0.9.1" } +uuid = { version = "1", features = ["v4", "serde"] } +zip = { version = "0.6", default-features = false } + +[target."cfg(target_os = \"macos\")".dependencies] +block = "0.1" +cocoa = { package = "cocoabind", path = "../cocoabind" } +objc = "0.2" + +[target."cfg(target_os = \"linux\")".dependencies] +gtk = { package = "gtkbind", path = "../gtkbind" } + +[target."cfg(target_os = \"windows\")".dependencies.windows-sys] +version = "0.52" +features = [ + "Win32_Foundation", + "Win32_Globalization", + "Win32_Graphics_Gdi", + "Win32_System_Com", + "Win32_System_LibraryLoader", + "Win32_System_SystemServices", + "Win32_System_Threading", + "Win32_UI_Controls", + "Win32_UI_Input_KeyboardAndMouse", + "Win32_UI_Shell", + "Win32_UI_WindowsAndMessaging" +] + +[features] +# Required for tests +mock = [] + +[build-dependencies] +embed-manifest = "1.4" +mozbuild = "0.1" +phf_codegen = "0.11" +yaml-rust = "0.4" + +[dev-dependencies] +bytes = "1.4" +tokio = { version = "1.29", features = ["rt", "net", "time", "sync"] } +warp = { version = "0.3", default-features = false } diff --git a/toolkit/crashreporter/client/Makefile.in b/toolkit/crashreporter/client/app/Makefile.in index 2d3ef1bee9..39fa3c8fbb 100644 --- a/toolkit/crashreporter/client/Makefile.in +++ b/toolkit/crashreporter/client/app/Makefile.in @@ -3,17 +3,15 @@ # 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/. -ifeq ($(OS_ARCH),WINNT) -MOZ_WINCONSOLE = 0 -endif +ifeq ($(OS_ARCH),Darwin) include $(topsrcdir)/config/rules.mk -ifeq ($(OS_ARCH),Darwin) libs:: $(NSINSTALL) -D $(DIST)/bin/crashreporter.app - rsync -a -C --exclude '*.in' $(srcdir)/macbuild/Contents $(DIST)/bin/crashreporter.app - $(call py_action,preprocessor crashreporter.app/Contents/Resources/English.lproj/InfoPlist.strings,-Fsubstitution --output-encoding utf-16 -DAPP_NAME='$(MOZ_APP_DISPLAYNAME)' $(srcdir)/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in -o $(DIST)/bin/crashreporter.app/Contents/Resources/English.lproj/InfoPlist.strings) + rsync --archive --cvs-exclude --exclude '*.in' $(srcdir)/macos_app_bundle/ $(DIST)/bin/crashreporter.app/Contents/ + $(call py_action,preprocessor crashreporter.app/Contents/Resources/English.lproj/InfoPlist.strings,-Fsubstitution --output-encoding utf-16 -DAPP_NAME='$(MOZ_APP_DISPLAYNAME)' $(srcdir)/macos_app_bundle/Resources/English.lproj/InfoPlist.strings.in -o $(DIST)/bin/crashreporter.app/Contents/Resources/English.lproj/InfoPlist.strings) $(NSINSTALL) -D $(DIST)/bin/crashreporter.app/Contents/MacOS $(NSINSTALL) $(DIST)/bin/crashreporter $(DIST)/bin/crashreporter.app/Contents/MacOS + endif diff --git a/toolkit/crashreporter/client/app/build.rs b/toolkit/crashreporter/client/app/build.rs new file mode 100644 index 0000000000..98f20dc375 --- /dev/null +++ b/toolkit/crashreporter/client/app/build.rs @@ -0,0 +1,89 @@ +/* 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/. */ + +use std::{env, path::Path}; + +fn main() { + windows_manifest(); + crash_ping_annotations(); + set_mock_cfg(); +} + +fn windows_manifest() { + use embed_manifest::{embed_manifest, manifest, new_manifest}; + + if std::env::var_os("CARGO_CFG_WINDOWS").is_none() { + return; + } + + // See https://docs.rs/embed-manifest/1.4.0/embed_manifest/fn.new_manifest.html for what the + // default manifest includes. The defaults include almost all of the settings used in the old + // crash reporter. + let manifest = new_manifest("CrashReporter") + // Use legacy active code page because GDI doesn't support per-process UTF8 (and older + // win10 may not support this setting anyway). + .active_code_page(manifest::ActiveCodePage::Legacy) + // GDI scaling is not enabled by default but we need it to make the GDI-drawn text look + // nice on high-DPI displays. + .gdi_scaling(manifest::Setting::Enabled); + + embed_manifest(manifest).expect("unable to embed windows manifest file"); + + println!("cargo:rerun-if-changed=build.rs"); +} + +/// Generate crash ping annotation information from the yaml definition file. +fn crash_ping_annotations() { + use std::fs::File; + use std::io::{BufWriter, Write}; + use yaml_rust::{Yaml, YamlLoader}; + + let crash_annotations = Path::new("../../CrashAnnotations.yaml") + .canonicalize() + .unwrap(); + println!("cargo:rerun-if-changed={}", crash_annotations.display()); + + let crash_ping_file = Path::new(&env::var("OUT_DIR").unwrap()).join("ping_annotations.rs"); + + let yaml = std::fs::read_to_string(crash_annotations).unwrap(); + let Yaml::Hash(entries) = YamlLoader::load_from_str(&yaml) + .unwrap() + .into_iter() + .next() + .unwrap() + else { + panic!("unexpected crash annotations root type"); + }; + + let ping_annotations = entries.into_iter().filter_map(|(k, v)| { + v["ping"] + .as_bool() + .unwrap_or(false) + .then(|| k.into_string().unwrap()) + }); + + let mut phf_set = phf_codegen::Set::new(); + for annotation in ping_annotations { + phf_set.entry(annotation); + } + + let mut file = BufWriter::new(File::create(&crash_ping_file).unwrap()); + writeln!( + &mut file, + "static PING_ANNOTATIONS: phf::Set<&'static str> = {};", + phf_set.build() + ) + .unwrap(); +} + +/// Set the mock configuration option when tests are enabled or when the mock feature is enabled. +fn set_mock_cfg() { + // Very inconveniently, there's no way to detect `cfg(test)` from build scripts. See + // https://github.com/rust-lang/cargo/issues/4789. This seems like an arbitrary and pointless + // limitation, and only complicates the evaluation of mock behavior. Because of this, we have a + // `mock` feature which is activated by `toolkit/library/rust/moz.build`. + if env::var_os("CARGO_FEATURE_MOCK").is_some() || mozbuild::config::MOZ_CRASHREPORTER_MOCK { + println!("cargo:rustc-cfg=mock"); + } +} diff --git a/toolkit/crashreporter/client/macbuild/Contents/Info.plist b/toolkit/crashreporter/client/app/macos_app_bundle/Info.plist index 51d6c4de37..f1679a922e 100644 --- a/toolkit/crashreporter/client/macbuild/Contents/Info.plist +++ b/toolkit/crashreporter/client/app/macos_app_bundle/Info.plist @@ -24,8 +24,6 @@ <string>1.0</string> <key>LSHasLocalizedDisplayName</key> <true/> - <key>NSMainNibFile</key> - <string>MainMenu</string> <key>NSRequiresAquaSystemAppearance</key> <false/> <key>NSPrincipalClass</key> diff --git a/toolkit/crashreporter/client/macbuild/Contents/PkgInfo b/toolkit/crashreporter/client/app/macos_app_bundle/PkgInfo index cae6d0a58f..cae6d0a58f 100644 --- a/toolkit/crashreporter/client/macbuild/Contents/PkgInfo +++ b/toolkit/crashreporter/client/app/macos_app_bundle/PkgInfo diff --git a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in b/toolkit/crashreporter/client/app/macos_app_bundle/Resources/English.lproj/InfoPlist.strings.in index e08ce59eb6..e08ce59eb6 100644 --- a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in +++ b/toolkit/crashreporter/client/app/macos_app_bundle/Resources/English.lproj/InfoPlist.strings.in diff --git a/toolkit/crashreporter/client/macbuild/Contents/Resources/crashreporter.icns b/toolkit/crashreporter/client/app/macos_app_bundle/Resources/crashreporter.icns Binary files differindex 341cd05a4d..341cd05a4d 100644 --- a/toolkit/crashreporter/client/macbuild/Contents/Resources/crashreporter.icns +++ b/toolkit/crashreporter/client/app/macos_app_bundle/Resources/crashreporter.icns diff --git a/toolkit/locales/en-US/toolkit/global/tabprompts.ftl b/toolkit/crashreporter/client/app/moz.build index 26eefa368b..6e5c19173f 100644 --- a/toolkit/locales/en-US/toolkit/global/tabprompts.ftl +++ b/toolkit/crashreporter/client/app/moz.build @@ -1,13 +1,7 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: # 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/. -tabmodalprompt-username = - .value = User Name: -tabmodalprompt-password = - .value = Password: - -tabmodalprompt-ok-button = - .label = OK -tabmodalprompt-cancel-button = - .label = Cancel +RUST_PROGRAMS = ["crashreporter"] diff --git a/toolkit/crashreporter/client/app/src/async_task.rs b/toolkit/crashreporter/client/app/src/async_task.rs new file mode 100644 index 0000000000..839db5e562 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/async_task.rs @@ -0,0 +1,31 @@ +/* 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/. */ + +//! Manage work across multiple threads. +//! +//! Each thread has thread-bound data which can be accessed in queued task functions. + +pub type TaskFn<T> = Box<dyn FnOnce(&T) + Send + 'static>; + +pub struct AsyncTask<T> { + send: Box<dyn Fn(TaskFn<T>) + Send + Sync>, +} + +impl<T> AsyncTask<T> { + pub fn new<F: Fn(TaskFn<T>) + Send + Sync + 'static>(send: F) -> Self { + AsyncTask { + send: Box::new(send), + } + } + + pub fn push<F: FnOnce(&T) + Send + 'static>(&self, f: F) { + (self.send)(Box::new(f)); + } + + pub fn wait<R: Send + 'static, F: FnOnce(&T) -> R + Send + 'static>(&self, f: F) -> R { + let (tx, rx) = std::sync::mpsc::sync_channel(0); + self.push(move |v| tx.send(f(v)).unwrap()); + rx.recv().unwrap() + } +} diff --git a/toolkit/crashreporter/client/app/src/config.rs b/toolkit/crashreporter/client/app/src/config.rs new file mode 100644 index 0000000000..4e919395e2 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/config.rs @@ -0,0 +1,527 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +//! Application configuration. + +use crate::std::borrow::Cow; +use crate::std::ffi::{OsStr, OsString}; +use crate::std::path::{Path, PathBuf}; +use crate::{lang, logging::LogTarget, std}; +use anyhow::Context; + +/// The number of the most recent minidump files to retain when pruning. +const MINIDUMP_PRUNE_SAVE_COUNT: usize = 10; + +#[cfg(test)] +pub mod test { + pub const MINIDUMP_PRUNE_SAVE_COUNT: usize = super::MINIDUMP_PRUNE_SAVE_COUNT; +} + +const VENDOR_KEY: &str = "Vendor"; +const PRODUCT_KEY: &str = "ProductName"; +const DEFAULT_VENDOR: &str = "Mozilla"; +const DEFAULT_PRODUCT: &str = "Firefox"; + +#[derive(Default)] +pub struct Config { + /// Whether reports should be automatically submitted. + pub auto_submit: bool, + /// Whether all threads of the process should be dumped (versus just the crashing thread). + pub dump_all_threads: bool, + /// Whether to delete the dump files after submission. + pub delete_dump: bool, + /// The data directory. + pub data_dir: Option<PathBuf>, + /// The events directory. + pub events_dir: Option<PathBuf>, + /// The ping directory. + pub ping_dir: Option<PathBuf>, + /// The dump file. + /// + /// If missing, an error dialog is displayed. + pub dump_file: Option<PathBuf>, + /// The XUL_APP_FILE to define if restarting the application. + pub app_file: Option<OsString>, + /// The path to the application to use when restarting the crashed process. + pub restart_command: Option<OsString>, + /// The arguments to pass if restarting the application. + pub restart_args: Vec<OsString>, + /// The URL to which to send reports. + pub report_url: Option<OsString>, + /// The localized strings to use. + pub strings: Option<lang::LangStrings>, + /// The log target. + pub log_target: Option<LogTarget>, +} + +pub struct ConfigStringBuilder<'a>(lang::LangStringBuilder<'a>); + +impl<'a> ConfigStringBuilder<'a> { + /// Set an argument for the string. + pub fn arg<V: Into<Cow<'a, str>>>(self, key: &'a str, value: V) -> Self { + ConfigStringBuilder(self.0.arg(key, value)) + } + + /// Get the localized string. + pub fn get(self) -> String { + self.0 + .get() + .context("failed to get localized string") + .unwrap() + } +} + +impl Config { + /// Return a configuration with no values set, and all bool values false. + pub fn new() -> Self { + Self::default() + } + + /// Load a configuration from the application environment. + #[cfg_attr(mock, allow(unused))] + pub fn read_from_environment(&mut self) -> anyhow::Result<()> { + /// Most environment variables are prefixed with `MOZ_CRASHREPORTER_`. + macro_rules! ekey { + ( $name:literal ) => { + concat!("MOZ_CRASHREPORTER_", $name) + }; + } + + self.auto_submit = env_bool(ekey!("AUTO_SUBMIT")); + self.dump_all_threads = env_bool(ekey!("DUMP_ALL_THREADS")); + self.delete_dump = !env_bool(ekey!("NO_DELETE_DUMP")); + self.data_dir = env_path(ekey!("DATA_DIRECTORY")); + self.events_dir = env_path(ekey!("EVENTS_DIRECTORY")); + self.ping_dir = env_path(ekey!("PING_DIRECTORY")); + self.app_file = std::env::var_os(ekey!("RESTART_XUL_APP_FILE")); + + // Only support `MOZ_APP_LAUNCHER` on linux and macos. + if cfg!(not(target_os = "windows")) { + self.restart_command = std::env::var_os("MOZ_APP_LAUNCHER"); + } + + if self.restart_command.is_none() { + self.restart_command = Some( + self.sibling_program_path(mozbuild::config::MOZ_APP_NAME) + .into(), + ) + } + + // We no longer use don't use `MOZ_CRASHREPORTER_RESTART_ARG_0`, see bug 1872920. + self.restart_args = (1..) + .into_iter() + .map_while(|arg_num| std::env::var_os(format!("{}_{}", ekey!("RESTART_ARG"), arg_num))) + // Sometimes these are empty, in which case they should be ignored. + .filter(|s| !s.is_empty()) + .collect(); + + self.report_url = std::env::var_os(ekey!("URL")); + + let mut args = std::env::args_os() + // skip program name + .skip(1); + self.dump_file = args.next().map(|p| p.into()); + while let Some(arg) = args.next() { + log::warn!("ignoring extraneous argument: {}", arg.to_string_lossy()); + } + + self.strings = Some(lang::load().context("failed to load localized strings")?); + + Ok(()) + } + + /// Get the localized string for the given index. + pub fn string(&self, index: &str) -> String { + self.build_string(index).get() + } + + /// Build the localized string for the given index. + pub fn build_string<'a>(&'a self, index: &'a str) -> ConfigStringBuilder<'a> { + ConfigStringBuilder( + self.strings + .as_ref() + .expect("strings not set") + .builder(index), + ) + } + + /// Whether the configured language has right-to-left text flow. + pub fn is_rtl(&self) -> bool { + self.strings + .as_ref() + .map(|s| s.is_rtl()) + .unwrap_or_default() + } + + /// Load the extra file, updating configuration. + pub fn load_extra_file(&mut self) -> anyhow::Result<serde_json::Value> { + let extra_file = self.extra_file().unwrap(); + + // Load the extra file (which minidump-analyzer just updated). + let extra: serde_json::Value = + serde_json::from_reader(std::fs::File::open(&extra_file).with_context(|| { + self.build_string("crashreporter-error-opening-file") + .arg("path", extra_file.display().to_string()) + .get() + })?) + .with_context(|| { + self.build_string("crashreporter-error-loading-file") + .arg("path", extra_file.display().to_string()) + .get() + })?; + + // Set report url if not already set. + if self.report_url.is_none() { + if let Some(url) = extra["ServerURL"].as_str() { + self.report_url = Some(url.into()); + } + } + + // Set the data dir if not already set. + if self.data_dir.is_none() { + let vendor = extra[VENDOR_KEY].as_str().unwrap_or(DEFAULT_VENDOR); + let product = extra[PRODUCT_KEY].as_str().unwrap_or(DEFAULT_PRODUCT); + self.data_dir = Some(self.get_data_dir(vendor, product)?); + } + + // Clear the restart command if WER handled the crash. This prevents restarting the + // program. See bug 1872920. + if extra.get("WindowsErrorReporting").is_some() { + self.restart_command = None; + } + + Ok(extra) + } + + /// Get the path to the extra file. + /// + /// Returns None if no dump_file is set. + pub fn extra_file(&self) -> Option<PathBuf> { + self.dump_file.clone().map(extra_file_for_dump_file) + } + + /// Get the path to the memory file. + /// + /// Returns None if no dump_file is set or if the memory file does not exist. + pub fn memory_file(&self) -> Option<PathBuf> { + self.dump_file.clone().and_then(|p| { + let p = memory_file_for_dump_file(p); + p.exists().then_some(p) + }) + } + + /// The path to the data directory. + /// + /// Panics if no data directory is set. + pub fn data_dir(&self) -> &Path { + self.data_dir.as_deref().unwrap() + } + + /// The path to the dump file. + /// + /// Panics if no dump file is set. + pub fn dump_file(&self) -> &Path { + self.dump_file.as_deref().unwrap() + } + + /// The id of the local dump file (the base filename without extension). + /// + /// Panics if no dump file is set. + pub fn local_dump_id(&self) -> Cow<str> { + self.dump_file().file_stem().unwrap().to_string_lossy() + } + + /// Move crash data to the pending folder. + pub fn move_crash_data_to_pending(&mut self) -> anyhow::Result<()> { + let pending_crashes_dir = self.data_dir().join("pending"); + std::fs::create_dir_all(&pending_crashes_dir).with_context(|| { + self.build_string("crashreporter-error-creating-dir") + .arg("path", pending_crashes_dir.display().to_string()) + .get() + })?; + + let move_file = |from: &Path| -> anyhow::Result<PathBuf> { + let to = pending_crashes_dir.join(from.file_name().unwrap()); + std::fs::rename(from, &to).with_context(|| { + self.build_string("crashreporter-error-moving-path") + .arg("from", from.display().to_string()) + .arg("to", to.display().to_string()) + .get() + })?; + Ok(to) + }; + + let new_dump_file = move_file(self.dump_file())?; + move_file(self.extra_file().unwrap().as_ref())?; + // Failing to move the memory file is recoverable. + if let Some(memory_file) = self.memory_file() { + if let Err(e) = move_file(memory_file.as_ref()) { + log::warn!("failed to move memory file: {e}"); + if let Err(e) = std::fs::remove_file(&memory_file) { + log::warn!("failed to remove {}: {e}", memory_file.display()); + } + } + } + + self.dump_file = Some(new_dump_file); + + Ok(()) + } + + /// Form the path which signals EOL for a particular version. + pub fn version_eol_file(&self, version: &str) -> PathBuf { + self.data_dir().join(format!("EndOfLife{version}")) + } + + /// Return the path used to store submitted crash ids. + pub fn submitted_crash_dir(&self) -> PathBuf { + self.data_dir().join("submitted") + } + + /// Delete files related to the crash report. + pub fn delete_files(&self) { + if !self.delete_dump { + return; + } + + for file in [&self.dump_file, &self.extra_file(), &self.memory_file()] + .into_iter() + .flatten() + { + if let Err(e) = std::fs::remove_file(file) { + log::warn!("failed to remove {}: {e}", file.display()); + } + } + } + + /// Prune old minidump files adjacent to the dump file. + pub fn prune_files(&self) -> anyhow::Result<()> { + log::info!("pruning minidump files to the {MINIDUMP_PRUNE_SAVE_COUNT} most recent"); + let Some(file) = &self.dump_file else { + anyhow::bail!("no dump file") + }; + let Some(dir) = file.parent() else { + anyhow::bail!("no parent directory for dump file") + }; + log::debug!("pruning {} directory", dir.display()); + let read_dir = dir.read_dir().with_context(|| { + format!( + "failed to read dump file parent directory {}", + dir.display() + ) + })?; + + let mut minidump_files = Vec::new(); + for entry in read_dir { + match entry { + Err(e) => log::error!( + "error while iterating over {} directory entry: {e}", + dir.display() + ), + Ok(e) if e.path().extension() == Some("dmp".as_ref()) => { + // Return if the metadata can't be read, since not being able to get metadata + // for any file could make the selection of minidumps to delete incorrect. + let meta = e.metadata().with_context(|| { + format!("failed to read metadata for {}", e.path().display()) + })?; + if meta.is_file() { + let modified_time = + meta.modified().expect( + "file modification time should be available on all crashreporter platforms", + ); + minidump_files.push((modified_time, e.path())); + } + } + _ => (), + } + } + + // Sort by modification time first, then path (just to have a defined behavior in the case + // of identical times). The reverse leaves the files in order from newest to oldest. + minidump_files.sort_unstable_by(|a, b| a.cmp(b).reverse()); + + // Delete files, skipping the most recent MINIDUMP_PRUNE_SAVE_COUNT. + for dump_file in minidump_files + .into_iter() + .skip(MINIDUMP_PRUNE_SAVE_COUNT) + .map(|v| v.1) + { + log::debug!("pruning {} and related files", dump_file.display()); + if let Err(e) = std::fs::remove_file(&dump_file) { + log::warn!("failed to delete {}: {e}", dump_file.display()); + } + + // Ignore errors for the extra file and the memory file: they may not exist. + let _ = std::fs::remove_file(extra_file_for_dump_file(dump_file.clone())); + let _ = std::fs::remove_file(memory_file_for_dump_file(dump_file)); + } + Ok(()) + } + + /// Get the path of a program that is a sibling of the crashreporter. + /// + /// On MacOS, this assumes that the crashreporter is its own application bundle within the main + /// program bundle. On other platforms this assumes siblings reside in the same directory as + /// the crashreporter. + /// + /// The returned path isn't guaranteed to exist. + // This method could be standalone rather than living in `Config`; it's here because it makes + // sense that if it were to rely on anything, it would be the `Config` (and that may change in + // the future). + pub fn sibling_program_path<N: AsRef<OsStr>>(&self, program: N) -> PathBuf { + // Expect shouldn't ever panic here because we need more than one argument to run + // the program in the first place (we've already previously iterated args). + // + // We use argv[0] rather than `std::env::current_exe` because `current_exe` doesn't define + // how symlinks are treated, and we want to support running directly from the local build + // directory (which uses symlinks on linux and macos). + let self_path = PathBuf::from(std::env::args_os().next().expect("failed to get argv[0]")); + let exe_extension = self_path.extension().unwrap_or_default(); + + let mut program_path = self_path.clone(); + // Pop the executable off to get the parent directory. + program_path.pop(); + program_path.push(program.as_ref()); + program_path.set_extension(exe_extension); + + if !program_path.exists() && cfg!(all(not(mock), target_os = "macos")) { + // On macOS the crash reporter client is shipped as an application bundle contained + // within Firefox's main application bundle. So when it's invoked its current working + // directory looks like: + // Firefox.app/Contents/MacOS/crashreporter.app/Contents/MacOS/ + // The other applications we ship with Firefox are stored in the main bundle + // (Firefox.app/Contents/MacOS/) so we we need to go back three directories + // to reach them. + + // 4 pops: 1 for the path that was just pushed, and 3 more for + // `crashreporter.app/Contents/MacOS`. + for _ in 0..4 { + program_path.pop(); + } + program_path.push(program.as_ref()); + program_path.set_extension(exe_extension); + } + + program_path + } + + cfg_if::cfg_if! { + if #[cfg(mock)] { + fn get_data_dir(&self, vendor: &str, product: &str) -> anyhow::Result<PathBuf> { + let mut path = PathBuf::from("data_dir"); + path.push(vendor); + path.push(product); + path.push("Crash Reports"); + Ok(path) + } + } else if #[cfg(target_os = "linux")] { + fn get_data_dir(&self, vendor: &str, product: &str) -> anyhow::Result<PathBuf> { + // home_dir is deprecated due to incorrect behavior on windows, but we only use it on linux + #[allow(deprecated)] + let mut data_path = + std::env::home_dir().with_context(|| self.string("crashreporter-error-no-home-dir"))?; + data_path.push(format!(".{}", vendor.to_lowercase())); + data_path.push(product.to_lowercase()); + data_path.push("Crash Reports"); + Ok(data_path) + } + } else if #[cfg(target_os = "macos")] { + fn get_data_dir(&self, _vendor: &str, product: &str) -> anyhow::Result<PathBuf> { + use objc::{ + rc::autoreleasepool, + runtime::{Object, BOOL, YES}, + *, + }; + #[link(name = "Foundation", kind = "framework")] + extern "system" { + fn NSSearchPathForDirectoriesInDomains( + directory: usize, + domain_mask: usize, + expand_tilde: BOOL, + ) -> *mut Object /* NSArray<NSString*>* */; + } + #[allow(non_upper_case_globals)] + const NSApplicationSupportDirectory: usize = 14; + #[allow(non_upper_case_globals)] + const NSUserDomainMask: usize = 1; + + let mut data_path = autoreleasepool(|| { + let paths /* NSArray<NSString*>* */ = unsafe { + NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) + }; + if paths.is_null() { + anyhow::bail!("NSSearchPathForDirectoriesInDomains returned nil"); + } + let path: *mut Object /* NSString* */ = unsafe { msg_send![paths, firstObject] }; + if path.is_null() { + anyhow::bail!("NSSearchPathForDirectoriesInDomains returned no paths"); + } + + let str_pointer: *const i8 = unsafe { msg_send![path, UTF8String] }; + // # Safety + // The pointer is a readable C string with a null terminator. + let Ok(s) = unsafe { std::ffi::CStr::from_ptr(str_pointer) }.to_str() else { + anyhow::bail!("NSString wasn't valid UTF8"); + }; + Ok(PathBuf::from(s)) + })?; + data_path.push(product); + std::fs::create_dir_all(&data_path).with_context(|| { + self.build_string("crashreporter-error-creating-dir") + .arg("path", data_path.display().to_string()) + .get() + })?; + data_path.push("Crash Reports"); + Ok(data_path) + } + } else if #[cfg(target_os = "windows")] { + fn get_data_dir(&self, vendor: &str, product: &str) -> anyhow::Result<PathBuf> { + use crate::std::os::windows::ffi::OsStringExt; + use windows_sys::{ + core::PWSTR, + Win32::{ + Globalization::lstrlenW, + System::Com::CoTaskMemFree, + UI::Shell::{FOLDERID_RoamingAppData, SHGetKnownFolderPath}, + }, + }; + + let mut path: PWSTR = std::ptr::null_mut(); + let result = unsafe { SHGetKnownFolderPath(&FOLDERID_RoamingAppData, 0, 0, &mut path) }; + if result != 0 { + unsafe { CoTaskMemFree(path as _) }; + anyhow::bail!("failed to get known path for roaming appdata"); + } + + let length = unsafe { lstrlenW(path) }; + let slice = unsafe { std::slice::from_raw_parts(path, length as usize) }; + let osstr = OsString::from_wide(slice); + unsafe { CoTaskMemFree(path as _) }; + let mut path = PathBuf::from(osstr); + path.push(vendor); + path.push(product); + path.push("Crash Reports"); + Ok(path) + } + } + } +} + +fn env_bool<K: AsRef<OsStr>>(name: K) -> bool { + std::env::var(name).map(|s| !s.is_empty()).unwrap_or(false) +} + +fn env_path<K: AsRef<OsStr>>(name: K) -> Option<PathBuf> { + std::env::var_os(name).map(PathBuf::from) +} + +fn extra_file_for_dump_file(mut dump_file: PathBuf) -> PathBuf { + dump_file.set_extension("extra"); + dump_file +} + +fn memory_file_for_dump_file(mut dump_file: PathBuf) -> PathBuf { + dump_file.set_extension("memory.json.gz"); + dump_file +} diff --git a/toolkit/crashreporter/client/app/src/data.rs b/toolkit/crashreporter/client/app/src/data.rs new file mode 100644 index 0000000000..474da8966a --- /dev/null +++ b/toolkit/crashreporter/client/app/src/data.rs @@ -0,0 +1,400 @@ +/* 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/. */ + +//! Data binding types are used to implement dynamic behaviors in UIs. [`Event`] is the primitive +//! type underlying most others. [Properties](Property) are what should usually be used in UI +//! models, since they have `From` impls allowing different binding behaviors to be set. + +use std::cell::RefCell; +use std::rc::Rc; + +/// An event which can have multiple subscribers. +/// +/// The type parameter is the payload of the event. +pub struct Event<T> { + subscribers: Rc<RefCell<Vec<Box<dyn Fn(&T)>>>>, +} + +impl<T> Clone for Event<T> { + fn clone(&self) -> Self { + Event { + subscribers: self.subscribers.clone(), + } + } +} + +impl<T> Default for Event<T> { + fn default() -> Self { + Event { + subscribers: Default::default(), + } + } +} + +impl<T> std::fmt::Debug for Event<T> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "{} {{ {} subscribers }}", + std::any::type_name::<Self>(), + self.subscribers.borrow().len() + ) + } +} + +impl<T> Event<T> { + /// Add a callback for when the event is fired. + pub fn subscribe<F>(&self, f: F) + where + F: Fn(&T) + 'static, + { + self.subscribers.borrow_mut().push(Box::new(f)); + } + + /// Fire the event with the given payload. + pub fn fire(&self, payload: &T) { + for f in self.subscribers.borrow().iter() { + f(payload); + } + } +} + +/// A synchronized runtime value. +/// +/// Consumers can subscribe to change events on the value. Change events are fired when +/// `borrow_mut()` references are dropped. +#[derive(Default)] +pub struct Synchronized<T> { + inner: Rc<SynchronizedInner<T>>, +} + +impl<T> Clone for Synchronized<T> { + fn clone(&self) -> Self { + Synchronized { + inner: self.inner.clone(), + } + } +} + +impl<T: std::fmt::Debug> std::fmt::Debug for Synchronized<T> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.debug_struct(std::any::type_name::<Self>()) + .field("current", &*self.inner.current.borrow()) + .field("change", &self.inner.change) + .finish() + } +} + +#[derive(Default)] +struct SynchronizedInner<T> { + current: RefCell<T>, + change: Event<T>, +} + +impl<T> Synchronized<T> { + /// Create a new value with the given inner data. + pub fn new(initial: T) -> Self { + Synchronized { + inner: Rc::new(SynchronizedInner { + current: RefCell::new(initial), + change: Default::default(), + }), + } + } + + /// Borrow a value's data. + pub fn borrow(&self) -> std::cell::Ref<T> { + self.inner.current.borrow() + } + + /// Mutably borrow a value's data. + /// + /// When the mutable reference is dropped, a change event is fired. + pub fn borrow_mut(&self) -> ValueRefMut<'_, T> { + ValueRefMut { + value: std::mem::ManuallyDrop::new(self.inner.current.borrow_mut()), + inner: &self.inner, + } + } + + /// Subscribe to change events in the value. + pub fn on_change<F: Fn(&T) + 'static>(&self, f: F) { + self.inner.change.subscribe(f); + } + + /// Update another synchronized value when this one changes. + pub fn update_on_change<U: 'static, F: Fn(&T) -> U + 'static>( + &self, + other: &Synchronized<U>, + f: F, + ) { + let other = other.clone(); + self.on_change(move |val| { + *other.borrow_mut() = f(val); + }); + } + + /// Create a new synchronized value which will update when this one changes. + pub fn mapped<U: 'static, F: Fn(&T) -> U + 'static>(&self, f: F) -> Synchronized<U> { + let s = Synchronized::new(f(&*self.borrow())); + self.update_on_change(&s, f); + s + } + + pub fn join<A: 'static, B: 'static, F: Fn(&A, &B) -> T + Clone + 'static>( + a: &Synchronized<A>, + b: &Synchronized<B>, + f: F, + ) -> Self + where + T: 'static, + { + let s = Synchronized::new(f(&*a.borrow(), &*b.borrow())); + let update = cc! { (a,b,s) move || { + *s.borrow_mut() = f(&*a.borrow(), &*b.borrow()); + }}; + a.on_change(cc! { (update) move |_| update()}); + b.on_change(move |_| update()); + s + } +} + +/// A runtime value that can be fetched on-demand (read-only). +/// +/// Consumers call [`read`] or [`get`] to retrieve the value, while producers call [`register`] to +/// set the function which is called to retrieve the value. This is of most use for things like +/// editable text strings, where it would be unnecessarily expensive to e.g. update a +/// `Synchronized` property as the text string is changed (debouncing could be used, but if change +/// notification isn't needed then it's still unnecessary). +pub struct OnDemand<T> { + get: Rc<RefCell<Option<Box<dyn Fn(&mut T) + 'static>>>>, +} + +impl<T> Default for OnDemand<T> { + fn default() -> Self { + OnDemand { + get: Default::default(), + } + } +} + +impl<T> Clone for OnDemand<T> { + fn clone(&self) -> Self { + OnDemand { + get: self.get.clone(), + } + } +} + +impl<T> std::fmt::Debug for OnDemand<T> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "{} {{ {} }}", + std::any::type_name::<Self>(), + if self.get.borrow().is_none() { + "not registered" + } else { + "registered" + } + ) + } +} + +impl<T> OnDemand<T> { + /// Reads the current value. + pub fn read(&self, value: &mut T) { + match &*self.get.borrow() { + None => { + // The test UI doesn't always register OnDemand getters (only on a per-test basis), + // so don't panic otherwise the tests will fail unnecessarily. + #[cfg(not(test))] + panic!("OnDemand not registered by renderer") + } + Some(f) => f(value), + } + } + + /// Get a copy of the current value. + pub fn get(&self) -> T + where + T: Default, + { + let mut r = T::default(); + self.read(&mut r); + r + } + + /// Register the function to use when getting the value. + pub fn register(&self, f: impl Fn(&mut T) + 'static) { + *self.get.borrow_mut() = Some(Box::new(f)); + } +} + +/// A UI element property. +/// +/// Properties support static and dynamic value bindings. +/// * `T` can be converted to static bindings. +/// * `Synchronized<T>` can be converted to dynamic bindings which will be updated +/// bidirectionally. +/// * `OnDemand<T>` can be converted to dynamic bindings which can be queried on an as-needed +/// basis. +#[derive(Clone, Debug)] +pub enum Property<T> { + Static(T), + Binding(Synchronized<T>), + ReadOnly(OnDemand<T>), +} + +#[cfg(test)] +impl<T: Clone + Default + 'static> Property<T> { + pub fn set(&self, value: T) { + match self { + Property::Static(_) => panic!("cannot set static property"), + Property::Binding(s) => *s.borrow_mut() = value, + Property::ReadOnly(o) => o.register(move |v| *v = value.clone()), + } + } + + pub fn get(&self) -> T { + match self { + Property::Static(v) => v.clone(), + Property::Binding(s) => s.borrow().clone(), + Property::ReadOnly(o) => o.get(), + } + } +} + +impl<T: Default> Default for Property<T> { + fn default() -> Self { + Property::Static(Default::default()) + } +} + +impl<T> From<T> for Property<T> { + fn from(value: T) -> Self { + Property::Static(value) + } +} + +impl<T> From<&Synchronized<T>> for Property<T> { + fn from(value: &Synchronized<T>) -> Self { + Property::Binding(value.clone()) + } +} + +impl<T> From<&OnDemand<T>> for Property<T> { + fn from(value: &OnDemand<T>) -> Self { + Property::ReadOnly(value.clone()) + } +} + +/// A mutable Value reference. +/// +/// When dropped, the Value's change event will fire (_after_ demoting the RefMut to a Ref). +pub struct ValueRefMut<'a, T> { + value: std::mem::ManuallyDrop<std::cell::RefMut<'a, T>>, + inner: &'a SynchronizedInner<T>, +} + +impl<T> std::ops::Deref for ValueRefMut<'_, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &*self.value + } +} + +impl<T> std::ops::DerefMut for ValueRefMut<'_, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut *self.value + } +} + +impl<T> Drop for ValueRefMut<'_, T> { + fn drop(&mut self) { + unsafe { std::mem::ManuallyDrop::drop(&mut self.value) }; + self.inner.change.fire(&*self.inner.current.borrow()); + } +} + +#[cfg(test)] +mod test { + use super::*; + use std::sync::atomic::{AtomicUsize, Ordering::Relaxed}; + + #[derive(Default, Clone)] + struct Trace { + count: Rc<AtomicUsize>, + } + + impl Trace { + fn inc(&self) { + self.count.fetch_add(1, Relaxed); + } + + fn set(&self, v: usize) { + self.count.store(v, Relaxed); + } + + fn count(&self) -> usize { + self.count.load(Relaxed) + } + } + + #[test] + fn event() { + let t1 = Trace::default(); + let t2 = Trace::default(); + let evt = Event::default(); + evt.subscribe(cc! { (t1) move |x| { + assert!(x == &42); + t1.inc() + }}); + evt.fire(&42); + assert_eq!(t1.count(), 1); + evt.subscribe(cc! { (t2) move |_| t2.inc() }); + evt.fire(&42); + assert_eq!(t1.count(), 2); + assert_eq!(t2.count(), 1); + } + + #[test] + fn synchronized() { + let t1 = Trace::default(); + let s = Synchronized::<usize>::default(); + assert_eq!(*s.borrow(), 0); + + s.on_change(cc! { (t1) move |v| t1.set(*v) }); + { + let mut s_ref = s.borrow_mut(); + *s_ref = 41; + // Changes should only occur when the ref is dropped + assert_eq!(t1.count(), 0); + *s_ref = 42; + } + assert_eq!(t1.count(), 42); + assert_eq!(*s.borrow(), 42); + } + + #[test] + fn ondemand() { + let t1 = Trace::default(); + let d = OnDemand::<usize>::default(); + d.register(|v| *v = 42); + { + let mut v = 0; + d.read(&mut v); + assert_eq!(v, 42); + } + d.register(|v| *v = 10); + assert_eq!(d.get(), 10); + + t1.inc(); + d.register(cc! { (t1) move |v| *v = t1.count() }); + assert_eq!(d.get(), 1); + t1.set(42); + assert_eq!(d.get(), 42); + } +} diff --git a/toolkit/crashreporter/client/app/src/lang/language_info.rs b/toolkit/crashreporter/client/app/src/lang/language_info.rs new file mode 100644 index 0000000000..b05953e2b3 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/lang/language_info.rs @@ -0,0 +1,74 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +use super::LangStrings; +use anyhow::Context; +use fluent::{bundle::FluentBundle, FluentResource}; +use unic_langid::LanguageIdentifier; + +const FALLBACK_FTL_FILE: &str = include_str!(mozbuild::srcdir_path!( + "/toolkit/locales/en-US/crashreporter/crashreporter.ftl" +)); +const FALLBACK_BRANDING_FILE: &str = include_str!(mozbuild::srcdir_path!( + "/browser/branding/official/locales/en-US/brand.ftl" +)); + +/// Localization language information. +#[derive(Debug, Clone)] +pub struct LanguageInfo { + pub identifier: String, + pub ftl_definitions: String, + pub ftl_branding: String, +} + +impl Default for LanguageInfo { + fn default() -> Self { + Self::fallback() + } +} + +impl LanguageInfo { + /// Get the fallback bundled language information (en-US). + pub fn fallback() -> Self { + LanguageInfo { + identifier: "en-US".to_owned(), + ftl_definitions: FALLBACK_FTL_FILE.to_owned(), + ftl_branding: FALLBACK_BRANDING_FILE.to_owned(), + } + } + + /// Load strings from the language info. + pub fn load_strings(self) -> anyhow::Result<LangStrings> { + let Self { + identifier: lang, + ftl_definitions: definitions, + ftl_branding: branding, + } = self; + + let langid = lang + .parse::<LanguageIdentifier>() + .with_context(|| format!("failed to parse language identifier ({lang})"))?; + let rtl = langid.character_direction() == unic_langid::CharacterDirection::RTL; + let mut bundle = FluentBundle::new_concurrent(vec![langid]); + + fn add_ftl<M>( + bundle: &mut FluentBundle<FluentResource, M>, + ftl: String, + ) -> anyhow::Result<()> { + let resource = FluentResource::try_new(ftl) + .ok() + .context("failed to create fluent resource")?; + bundle + .add_resource(resource) + .ok() + .context("failed to add fluent resource to bundle")?; + Ok(()) + } + + add_ftl(&mut bundle, branding).context("failed to add branding")?; + add_ftl(&mut bundle, definitions).context("failed to add localization")?; + + Ok(LangStrings::new(bundle, rtl)) + } +} diff --git a/toolkit/crashreporter/client/app/src/lang/mod.rs b/toolkit/crashreporter/client/app/src/lang/mod.rs new file mode 100644 index 0000000000..9b7495f92a --- /dev/null +++ b/toolkit/crashreporter/client/app/src/lang/mod.rs @@ -0,0 +1,89 @@ +/* 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/. */ + +mod language_info; +mod omnijar; + +use fluent::{bundle::FluentBundle, FluentArgs, FluentResource}; +use intl_memoizer::concurrent::IntlLangMemoizer; +#[cfg(test)] +pub use language_info::LanguageInfo; +use std::borrow::Cow; +use std::collections::BTreeMap; + +/// Get the localized string bundle. +pub fn load() -> anyhow::Result<LangStrings> { + // TODO support langpacks, bug 1873210 + omnijar::read().unwrap_or_else(|e| { + log::warn!("failed to read localization data from the omnijar ({e}), falling back to bundled content"); + Default::default() + }).load_strings() +} + +/// A bundle of localized strings. +pub struct LangStrings { + bundle: FluentBundle<FluentResource, IntlLangMemoizer>, + rtl: bool, +} + +/// Arguments to build a localized string. +pub type LangStringsArgs<'a> = BTreeMap<&'a str, Cow<'a, str>>; + +impl LangStrings { + pub fn new(bundle: FluentBundle<FluentResource, IntlLangMemoizer>, rtl: bool) -> Self { + LangStrings { bundle, rtl } + } + + /// Return whether the localized language has right-to-left text flow. + pub fn is_rtl(&self) -> bool { + self.rtl + } + + pub fn get(&self, index: &str, args: LangStringsArgs) -> anyhow::Result<String> { + let mut fluent_args = FluentArgs::with_capacity(args.len()); + for (k, v) in args { + fluent_args.set(k, v); + } + + let Some(pattern) = self.bundle.get_message(index).and_then(|m| m.value()) else { + anyhow::bail!("failed to get fluent message for {index}"); + }; + let mut errs = Vec::new(); + let ret = self + .bundle + .format_pattern(pattern, Some(&fluent_args), &mut errs); + if !errs.is_empty() { + anyhow::bail!("errors while formatting pattern: {errs:?}"); + } + Ok(ret.into_owned()) + } + + pub fn builder<'a>(&'a self, index: &'a str) -> LangStringBuilder<'a> { + LangStringBuilder { + strings: self, + index, + args: Default::default(), + } + } +} + +/// A localized string builder. +pub struct LangStringBuilder<'a> { + strings: &'a LangStrings, + index: &'a str, + args: LangStringsArgs<'a>, +} + +impl<'a> LangStringBuilder<'a> { + /// Set an argument for the string. + pub fn arg<V: Into<Cow<'a, str>>>(mut self, key: &'a str, value: V) -> Self { + self.args.insert(key, value.into()); + self + } + + /// Get the localized string. + pub fn get(self) -> anyhow::Result<String> { + self.strings.get(self.index, self.args) + } +} diff --git a/toolkit/crashreporter/client/app/src/lang/omnijar.rs b/toolkit/crashreporter/client/app/src/lang/omnijar.rs new file mode 100644 index 0000000000..2d2c34dd8d --- /dev/null +++ b/toolkit/crashreporter/client/app/src/lang/omnijar.rs @@ -0,0 +1,103 @@ +/* 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/. */ + +use super::language_info::LanguageInfo; +use crate::std::{ + env::current_exe, + fs::File, + io::{BufRead, BufReader, Read}, + path::Path, +}; +use anyhow::Context; +use zip::read::ZipArchive; + +/// Read the appropriate localization fluent definitions from the omnijar files. +/// +/// Returns (locale name, fluent definitions). +pub fn read() -> anyhow::Result<LanguageInfo> { + let mut path = current_exe().context("failed to get current executable")?; + path.pop(); + path.push("omni.ja"); + + let mut zip = read_omnijar_file(&path)?; + let locales = { + let buf = BufReader::new( + zip.by_name("res/multilocale.txt") + .context("failed to read multilocale file in zip archive")?, + ); + let line = buf + .lines() + .next() + .ok_or(anyhow::anyhow!("multilocale file was empty"))? + .context("failed to read first line of multilocale file")?; + line.split(",") + .map(|s| s.trim().to_owned()) + .collect::<Vec<_>>() + }; + + let (locale, ftl_definitions) = 'defs: { + for locale in &locales { + match read_strings(locale, &mut zip) { + Ok(v) => break 'defs (locale.to_string(), v), + Err(e) => log::warn!("{e:#}"), + } + } + anyhow::bail!("failed to find any usable localized strings in the omnijar") + }; + + // The brand ftl is in the browser omnijar. + path.pop(); + path.push("browser"); + path.push("omni.ja"); + + let ftl_branding = 'branding: { + for locale in &locales { + match read_branding(&locale, &mut zip) { + Ok(v) => break 'branding v, + Err(e) => log::warn!("failed to read branding from omnijar: {e:#}"), + } + } + log::info!("using fallback branding info"); + LanguageInfo::default().ftl_branding + }; + + Ok(LanguageInfo { + identifier: locale, + ftl_definitions, + ftl_branding, + }) +} + +/// Read the localized strings from the given zip archive (omnijar). +fn read_strings(locale: &str, archive: &mut ZipArchive<File>) -> anyhow::Result<String> { + let mut file = archive + .by_name(&format!( + "localization/{locale}/crashreporter/crashreporter.ftl" + )) + .with_context(|| format!("failed to locate localization file for {locale}"))?; + + let mut ftl_definitions = String::new(); + file.read_to_string(&mut ftl_definitions) + .with_context(|| format!("failed to read localization file for {locale}"))?; + + Ok(ftl_definitions) +} + +/// Read the branding information from the given zip archive (omnijar). +fn read_branding(locale: &str, archive: &mut ZipArchive<File>) -> anyhow::Result<String> { + let mut file = archive + .by_name(&format!("localization/{locale}/branding/brand.ftl")) + .with_context(|| format!("failed to locate branding localization file for {locale}"))?; + let mut s = String::new(); + file.read_to_string(&mut s) + .with_context(|| format!("failed to read branding localization file for {locale}"))?; + Ok(s) +} + +fn read_omnijar_file(path: &Path) -> anyhow::Result<ZipArchive<File>> { + ZipArchive::new( + File::open(&path).with_context(|| format!("failed to open {}", path.display()))?, + ) + .with_context(|| format!("failed to read zip archive in {}", path.display())) +} diff --git a/toolkit/crashreporter/client/app/src/logging.rs b/toolkit/crashreporter/client/app/src/logging.rs new file mode 100644 index 0000000000..c3f85312f8 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/logging.rs @@ -0,0 +1,86 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +//! Application logging facilities. + +use crate::std::{ + self, + path::Path, + sync::{Arc, Mutex}, +}; + +/// Initialize logging and return a log target which can be used to change the destination of log +/// statements. +#[cfg_attr(mock, allow(unused))] +pub fn init() -> LogTarget { + let log_target_inner = LogTargetInner::default(); + + env_logger::builder() + .parse_env( + env_logger::Env::new() + .filter("MOZ_CRASHEREPORTER") + .write_style("MOZ_CRASHREPORTER_STYLE"), + ) + .target(env_logger::fmt::Target::Pipe(Box::new( + log_target_inner.clone(), + ))) + .init(); + + LogTarget { + inner: log_target_inner, + } +} + +/// Controls the target of logging. +#[derive(Clone)] +pub struct LogTarget { + inner: LogTargetInner, +} + +impl LogTarget { + /// Set the file to which log statements will be written. + pub fn set_file(&self, path: &Path) { + match std::fs::File::create(path) { + Ok(file) => { + if let Ok(mut guard) = self.inner.target.lock() { + *guard = Box::new(file); + } + } + Err(e) => log::error!("failed to retarget log to {}: {e}", path.display()), + } + } +} + +/// A private inner class implements Write, allows creation, etc. Externally the `LogTarget` only +/// supports changing the target and nothing else. +#[derive(Clone)] +struct LogTargetInner { + target: Arc<Mutex<Box<dyn std::io::Write + Send + 'static>>>, +} + +impl Default for LogTargetInner { + fn default() -> Self { + LogTargetInner { + target: Arc::new(Mutex::new(Box::new(std::io::stderr()))), + } + } +} + +impl std::io::Write for LogTargetInner { + fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { + let Ok(mut guard) = self.target.lock() else { + // Pretend we wrote successfully. + return Ok(buf.len()); + }; + guard.write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + let Ok(mut guard) = self.target.lock() else { + // Pretend we flushed successfully. + return Ok(()); + }; + guard.flush() + } +} diff --git a/toolkit/crashreporter/client/app/src/logic.rs b/toolkit/crashreporter/client/app/src/logic.rs new file mode 100644 index 0000000000..4ad1baa9c6 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/logic.rs @@ -0,0 +1,660 @@ +/* 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/. */ + +//! Business logic for the crash reporter. + +use crate::std::{ + cell::RefCell, + path::PathBuf, + process::Command, + sync::{ + atomic::{AtomicBool, Ordering::Relaxed}, + Arc, Mutex, + }, +}; +use crate::{ + async_task::AsyncTask, + config::Config, + net, + settings::Settings, + std, + ui::{ReportCrashUI, ReportCrashUIState, SubmitState}, +}; +use anyhow::Context; +use uuid::Uuid; + +/// The main crash reporting logic. +pub struct ReportCrash { + pub settings: RefCell<Settings>, + config: Arc<Config>, + extra: serde_json::Value, + settings_file: PathBuf, + attempted_to_send: AtomicBool, + ui: Option<AsyncTask<ReportCrashUIState>>, +} + +impl ReportCrash { + pub fn new(config: Arc<Config>, extra: serde_json::Value) -> anyhow::Result<Self> { + let settings_file = config.data_dir().join("crashreporter_settings.json"); + let settings: Settings = match std::fs::File::open(&settings_file) { + Err(e) if e.kind() != std::io::ErrorKind::NotFound => { + anyhow::bail!( + "failed to open settings file ({}): {e}", + settings_file.display() + ); + } + Err(_) => Default::default(), + Ok(f) => Settings::from_reader(f)?, + }; + Ok(ReportCrash { + config, + extra, + settings_file, + settings: settings.into(), + attempted_to_send: Default::default(), + ui: None, + }) + } + + /// Returns whether an attempt was made to send the report. + pub fn run(mut self) -> anyhow::Result<bool> { + self.set_log_file(); + let hash = self.compute_minidump_hash().map(Some).unwrap_or_else(|e| { + log::warn!("failed to compute minidump hash: {e}"); + None + }); + let ping_uuid = self.send_crash_ping(hash.as_deref()).unwrap_or_else(|e| { + log::warn!("failed to send crash ping: {e}"); + None + }); + if let Err(e) = self.update_events_file(hash.as_deref(), ping_uuid) { + log::warn!("failed to update events file: {e}"); + } + self.sanitize_extra(); + self.check_eol_version()?; + if !self.config.auto_submit { + self.run_ui(); + } else { + anyhow::ensure!(self.try_send().unwrap_or(false), "failed to send report"); + } + + Ok(self.attempted_to_send.load(Relaxed)) + } + + /// Set the log file based on the current configured paths. + /// + /// This is the earliest that this can occur as the configuration data dir may be set based on + /// fields in the extra file. + fn set_log_file(&self) { + if let Some(log_target) = &self.config.log_target { + log_target.set_file(&self.config.data_dir().join("submit.log")); + } + } + + /// Compute the SHA256 hash of the minidump file contents, and return it as a hex string. + fn compute_minidump_hash(&self) -> anyhow::Result<String> { + let hash = { + use sha2::{Digest, Sha256}; + let mut dump_file = std::fs::File::open(self.config.dump_file())?; + let mut hasher = Sha256::new(); + std::io::copy(&mut dump_file, &mut hasher)?; + hasher.finalize() + }; + + let mut s = String::with_capacity(hash.len() * 2); + for byte in hash { + use crate::std::fmt::Write; + write!(s, "{:02x}", byte).unwrap(); + } + + Ok(s) + } + + /// Send a crash ping to telemetry. + /// + /// Returns the crash ping uuid. + fn send_crash_ping(&self, minidump_hash: Option<&str>) -> anyhow::Result<Option<Uuid>> { + if self.config.ping_dir.is_none() { + log::warn!("not sending crash ping because no ping directory configured"); + return Ok(None); + } + + //TODO support glean crash pings (or change pingsender to do so) + + let dump_id = self.config.local_dump_id(); + let ping = net::legacy_telemetry::Ping::crash(&self.extra, dump_id.as_ref(), minidump_hash) + .context("failed to create telemetry crash ping")?; + + let submission_url = ping + .submission_url(&self.extra) + .context("failed to generate ping submission URL")?; + + let target_file = self + .config + .ping_dir + .as_ref() + .unwrap() + .join(format!("{}.json", ping.id())); + + let file = std::fs::File::create(&target_file).with_context(|| { + format!( + "failed to open ping file {} for writing", + target_file.display() + ) + })?; + + serde_json::to_writer(file, &ping).context("failed to serialize telemetry crash ping")?; + + let pingsender_path = self.config.sibling_program_path("pingsender"); + + crate::process::background_command(&pingsender_path) + .arg(submission_url) + .arg(target_file) + .spawn() + .with_context(|| { + format!( + "failed to launch pingsender process at {}", + pingsender_path.display() + ) + })?; + + // TODO asynchronously get pingsender result and log it? + + Ok(Some(ping.id().clone())) + } + + /// Remove unneeded entries from the extra file, and add some that indicate from where the data + /// is being sent. + fn sanitize_extra(&mut self) { + if let Some(map) = self.extra.as_object_mut() { + // Remove these entries, they don't need to be sent. + map.remove("ServerURL"); + map.remove("StackTraces"); + } + + self.extra["SubmittedFrom"] = "Client".into(); + self.extra["Throttleable"] = "1".into(); + } + + /// Update the events file with information about the crash ping, minidump hash, and + /// stacktraces. + fn update_events_file( + &self, + minidump_hash: Option<&str>, + ping_uuid: Option<Uuid>, + ) -> anyhow::Result<()> { + use crate::std::io::{BufRead, Error, ErrorKind, Write}; + struct EventsFile { + event_version: String, + time: String, + uuid: String, + pub data: serde_json::Value, + } + + impl EventsFile { + pub fn parse(mut reader: impl BufRead) -> std::io::Result<Self> { + let mut lines = (&mut reader).lines(); + + let mut read_field = move |name: &str| -> std::io::Result<String> { + lines.next().transpose()?.ok_or_else(|| { + Error::new(ErrorKind::InvalidData, format!("missing {name} field")) + }) + }; + + let event_version = read_field("event version")?; + let time = read_field("time")?; + let uuid = read_field("uuid")?; + let data = serde_json::from_reader(reader)?; + Ok(EventsFile { + event_version, + time, + uuid, + data, + }) + } + + pub fn write(&self, mut writer: impl Write) -> std::io::Result<()> { + writeln!(writer, "{}", self.event_version)?; + writeln!(writer, "{}", self.time)?; + writeln!(writer, "{}", self.uuid)?; + serde_json::to_writer(writer, &self.data)?; + Ok(()) + } + } + + let Some(events_dir) = &self.config.events_dir else { + log::warn!("not updating the events file; no events directory configured"); + return Ok(()); + }; + + let event_path = events_dir.join(self.config.local_dump_id().as_ref()); + + // Read events file. + let file = std::fs::File::open(&event_path) + .with_context(|| format!("failed to open event file at {}", event_path.display()))?; + + let mut events_file = + EventsFile::parse(std::io::BufReader::new(file)).with_context(|| { + format!( + "failed to parse events file contents in {}", + event_path.display() + ) + })?; + + // Update events file fields. + if let Some(hash) = minidump_hash { + events_file.data["MinidumpSha256Hash"] = hash.into(); + } + if let Some(uuid) = ping_uuid { + events_file.data["CrashPingUUID"] = uuid.to_string().into(); + } + events_file.data["StackTraces"] = self.extra["StackTraces"].clone(); + + // Write altered events file. + let file = std::fs::File::create(&event_path).with_context(|| { + format!("failed to truncate event file at {}", event_path.display()) + })?; + + events_file + .write(file) + .with_context(|| format!("failed to write event file at {}", event_path.display())) + } + + /// Check whether the version of the software that generated the crash is EOL. + fn check_eol_version(&self) -> anyhow::Result<()> { + if let Some(version) = self.extra["Version"].as_str() { + if self.config.version_eol_file(version).exists() { + self.config.delete_files(); + anyhow::bail!(self.config.string("crashreporter-error-version-eol")); + } + } + Ok(()) + } + + /// Save the current settings. + fn save_settings(&self) { + let result: anyhow::Result<()> = (|| { + Ok(self + .settings + .borrow() + .to_writer(std::fs::File::create(&self.settings_file)?)?) + })(); + if let Err(e) = result { + log::error!("error while saving settings: {e}"); + } + } + + /// Handle a response from submitting a crash report. + /// + /// Returns the crash ID to use for the recorded submission event. Errors in this function may + /// result in None being returned to consider the crash report submission as a failure even + /// though the server did provide a response. + fn handle_crash_report_response( + &self, + response: net::report::Response, + ) -> anyhow::Result<Option<String>> { + if let Some(version) = response.stop_sending_reports_for { + // Create the EOL version file. The content seemingly doesn't matter, but we mimic what + // was written by the old crash reporter. + if let Err(e) = std::fs::write(self.config.version_eol_file(&version), "1\n") { + log::warn!("failed to write EOL file: {e}"); + } + } + + if response.discarded { + log::debug!("response indicated that the report was discarded"); + return Ok(None); + } + + let Some(crash_id) = response.crash_id else { + log::debug!("response did not provide a crash id"); + return Ok(None); + }; + + // Write the id to the `submitted` directory + let submitted_dir = self.config.submitted_crash_dir(); + std::fs::create_dir_all(&submitted_dir).with_context(|| { + format!( + "failed to create submitted crash directory {}", + submitted_dir.display() + ) + })?; + + let crash_id_file = submitted_dir.join(format!("{crash_id}.txt")); + + let mut file = std::fs::File::create(&crash_id_file).with_context(|| { + format!( + "failed to create submitted crash file for {crash_id} ({})", + crash_id_file.display() + ) + })?; + + // Shadow `std::fmt::Write` to use the correct trait below. + use crate::std::io::Write; + + if let Err(e) = writeln!( + &mut file, + "{}", + self.config + .build_string("crashreporter-crash-identifier") + .arg("id", &crash_id) + .get() + ) { + log::warn!( + "failed to write to submitted crash file ({}) for {crash_id}: {e}", + crash_id_file.display() + ); + } + + if let Some(url) = response.view_url { + if let Err(e) = writeln!( + &mut file, + "{}", + self.config + .build_string("crashreporter-crash-details") + .arg("url", url) + .get() + ) { + log::warn!( + "failed to write view url to submitted crash file ({}) for {crash_id}: {e}", + crash_id_file.display() + ); + } + } + + Ok(Some(crash_id)) + } + + /// Write the submission event. + /// + /// A `None` crash_id indicates that the submission failed. + fn write_submission_event(&self, crash_id: Option<String>) -> anyhow::Result<()> { + let Some(events_dir) = &self.config.events_dir else { + // If there's no events dir, don't do anything. + return Ok(()); + }; + + let local_id = self.config.local_dump_id(); + let event_path = events_dir.join(format!("{local_id}-submission")); + + let unix_epoch_seconds = std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .expect("system time is before the unix epoch") + .as_secs(); + std::fs::write( + &event_path, + format!( + "crash.submission.1\n{unix_epoch_seconds}\n{local_id}\n{}\n{}", + crash_id.is_some(), + crash_id.as_deref().unwrap_or("") + ), + ) + .with_context(|| format!("failed to write event to {}", event_path.display())) + } + + /// Restart the program. + fn restart_process(&self) { + if self.config.restart_command.is_none() { + // The restart button should be hidden in this case, so this error should not occur. + log::error!("no process configured for restart"); + return; + } + + let mut cmd = Command::new(self.config.restart_command.as_ref().unwrap()); + cmd.args(&self.config.restart_args) + .stdin(std::process::Stdio::null()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()); + if let Some(xul_app_file) = &self.config.app_file { + cmd.env("XUL_APP_FILE", xul_app_file); + } + log::debug!("restarting process: {:?}", cmd); + if let Err(e) = cmd.spawn() { + log::error!("failed to restart process: {e}"); + } + } + + /// Run the crash reporting UI. + fn run_ui(&mut self) { + use crate::std::{sync::mpsc, thread}; + + let (logic_send, logic_recv) = mpsc::channel(); + // Wrap work_send in an Arc so that it can be captured weakly by the work queue and + // drop when the UI finishes, including panics (allowing the logic thread to exit). + // + // We need to wrap in a Mutex because std::mpsc::Sender isn't Sync (until rust 1.72). + let logic_send = Arc::new(Mutex::new(logic_send)); + + let weak_logic_send = Arc::downgrade(&logic_send); + let logic_remote_queue = AsyncTask::new(move |f| { + if let Some(logic_send) = weak_logic_send.upgrade() { + // This is best-effort: ignore errors. + let _ = logic_send.lock().unwrap().send(f); + } + }); + + let crash_ui = ReportCrashUI::new( + &*self.settings.borrow(), + self.config.clone(), + logic_remote_queue, + ); + + // Set the UI remote queue. + self.ui = Some(crash_ui.async_task()); + + // Spawn a separate thread to handle all interactions with `self`. This prevents blocking + // the UI for any reason. + + // Use a barrier to ensure both threads are live before either starts (ensuring they + // can immediately queue work for each other). + let barrier = std::sync::Barrier::new(2); + let barrier = &barrier; + thread::scope(move |s| { + // Move `logic_send` into this scope so that it will drop when the scope completes + // (which will drop the `mpsc::Sender` and cause the logic thread to complete and join + // when the UI finishes so the scope can exit). + let _logic_send = logic_send; + s.spawn(move || { + barrier.wait(); + while let Ok(f) = logic_recv.recv() { + f(self); + } + // Clear the UI remote queue, using it after this point is an error. + // + // NOTE we do this here because the compiler can't reason about `self` being safely + // accessible after `thread::scope` returns. This is effectively the same result + // since the above loop will only exit when `logic_send` is dropped at the end of + // the scope. + self.ui = None; + }); + + barrier.wait(); + crash_ui.run() + }); + } +} + +// These methods may interact with `self.ui`. +impl ReportCrash { + /// Update the submission details shown in the UI. + pub fn update_details(&self) { + use crate::std::fmt::Write; + + let extra = self.current_extra_data(); + + let mut details = String::new(); + let mut entries: Vec<_> = extra.as_object().unwrap().into_iter().collect(); + entries.sort_unstable_by_key(|(k, _)| *k); + for (key, value) in entries { + let _ = write!(details, "{key}: "); + if let Some(v) = value.as_str() { + details.push_str(v); + } else { + match serde_json::to_string(value) { + Ok(s) => details.push_str(&s), + Err(e) => { + let _ = write!(details, "<serialization error: {e}>"); + } + } + } + let _ = writeln!(details); + } + let _ = writeln!( + details, + "{}", + self.config.string("crashreporter-report-info") + ); + + self.ui().push(move |ui| *ui.details.borrow_mut() = details); + } + + /// Restart the application and send the crash report. + pub fn restart(&self) { + self.save_settings(); + // Get the program restarted before sending the report. + self.restart_process(); + let result = self.try_send(); + self.close_window(result.is_some()); + } + + /// Quit and send the crash report. + pub fn quit(&self) { + self.save_settings(); + let result = self.try_send(); + self.close_window(result.is_some()); + } + + fn close_window(&self, report_sent: bool) { + if report_sent && !self.config.auto_submit && !cfg!(test) { + // Add a delay to allow the user to see the result. + std::thread::sleep(std::time::Duration::from_secs(5)); + } + + self.ui().push(|r| r.close_window.fire(&())); + } + + /// Try to send the report. + /// + /// This function may be called without a UI active (if auto_submit is true), so it will not + /// panic if `self.ui` is unset. + /// + /// Returns whether the report was received (regardless of whether the response was processed + /// successfully), if a report could be sent at all (based on the configuration). + fn try_send(&self) -> Option<bool> { + self.attempted_to_send.store(true, Relaxed); + let send_report = self.settings.borrow().submit_report; + + if !send_report { + log::trace!("not sending report due to user setting"); + return None; + } + + // TODO? load proxy info from libgconf on linux + + let Some(url) = &self.config.report_url else { + log::warn!("not sending report due to missing report url"); + return None; + }; + + if let Some(ui) = &self.ui { + ui.push(|r| *r.submit_state.borrow_mut() = SubmitState::InProgress); + } + + // Send the report to the server. + let extra = self.current_extra_data(); + let memory_file = self.config.memory_file(); + let report = net::report::CrashReport { + extra: &extra, + dump_file: self.config.dump_file(), + memory_file: memory_file.as_deref(), + url, + }; + + let report_response = report + .send() + .map(Some) + .unwrap_or_else(|e| { + log::error!("failed to initialize report transmission: {e}"); + None + }) + .and_then(|sender| { + // Normally we might want to do the following asynchronously since it will block, + // however we don't really need the Logic thread to do anything else (the UI + // becomes disabled from this point onward), so we just do it here. Same goes for + // the `std::thread::sleep` in close_window() later on. + sender.finish().map(Some).unwrap_or_else(|e| { + log::error!("failed to send report: {e}"); + None + }) + }); + + let report_received = report_response.is_some(); + let crash_id = report_response.and_then(|response| { + self.handle_crash_report_response(response) + .unwrap_or_else(|e| { + log::error!("failed to handle crash report response: {e}"); + None + }) + }); + + if report_received { + // If the response could be handled (indicated by the returned crash id), clean up by + // deleting the minidump files. Otherwise, prune old minidump files. + if crash_id.is_some() { + self.config.delete_files(); + } else { + if let Err(e) = self.config.prune_files() { + log::warn!("failed to prune files: {e}"); + } + } + } + + if let Err(e) = self.write_submission_event(crash_id) { + log::warn!("failed to write submission event: {e}"); + } + + // Indicate whether the report was sent successfully, regardless of whether the response + // was processed successfully. + // + // FIXME: this is how the old crash reporter worked, but we might want to change this + // behavior. + if let Some(ui) = &self.ui { + ui.push(move |r| { + *r.submit_state.borrow_mut() = if report_received { + SubmitState::Success + } else { + SubmitState::Failure + } + }); + } + + Some(report_received) + } + + /// Form the extra data, taking into account user input. + fn current_extra_data(&self) -> serde_json::Value { + let include_address = self.settings.borrow().include_url; + let comment = if !self.config.auto_submit { + self.ui().wait(|r| r.comment.get()) + } else { + Default::default() + }; + + let mut extra = self.extra.clone(); + + if !comment.is_empty() { + extra["Comments"] = comment.into(); + } + + if !include_address { + extra.as_object_mut().unwrap().remove("URL"); + } + + extra + } + + fn ui(&self) -> &AsyncTask<ReportCrashUIState> { + self.ui.as_ref().expect("UI remote queue missing") + } +} diff --git a/toolkit/crashreporter/client/app/src/main.rs b/toolkit/crashreporter/client/app/src/main.rs new file mode 100644 index 0000000000..07e1b04cb8 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/main.rs @@ -0,0 +1,229 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +//! The crash reporter application. +//! +//! # Architecture +//! The application uses a simple declarative [UI model](ui::model) to define the UI. This model +//! contains [data bindings](data) which provide the dynamic behaviors of the UI. Separate UI +//! implementations for linux (gtk), macos (cocoa), and windows (win32) exist, as well as a test UI +//! which is virtual (no actual interface is presented) but allows runtime introspection. +//! +//! # Mocking +//! This application contains mock interfaces for all the `std` functions it uses which interact +//! with the host system. You can see their implementation in [`crate::std`]. To enable mocking, +//! use the `mock` feature or build with `MOZ_CRASHREPORTER_MOCK` set (which, in `build.rs`, is +//! translated to a `cfg` option). *Note* that this cfg _must_ be enabled when running tests. +//! Unfortunately it is not possible to detect whether tests are being built in `build.rs, which +//! is why a feature needed to be made in the first place (it is enabled automatically when running +//! `mach rusttests`). +//! +//! Currently the input program configuration which is mocked when running the application is fixed +//! (see the [`main`] implementation in this file). If needed in the future, it would be nice to +//! extend this to allow runtime tweaking. +//! +//! # Development +//! Because of the mocking support previously mentioned, in generally any `std` imports should +//! actually use `crate::std`. If mocked functions/types are missing, they should be added with +//! appropriate mocking hooks. + +// Use the WINDOWS windows subsystem. This prevents a console window from opening with the +// application. +#![cfg_attr(windows, windows_subsystem = "windows")] + +use crate::std::sync::Arc; +use anyhow::Context; +use config::Config; + +/// cc is short for Clone Capture, a shorthand way to clone a bunch of values before an expression +/// (particularly useful for closures). +/// +/// It is defined here to allow it to be used in all submodules (textual scope lookup). +macro_rules! cc { + ( ($($c:ident),*) $e:expr ) => { + { + $(let $c = $c.clone();)* + $e + } + } +} + +mod async_task; +mod config; +mod data; +mod lang; +mod logging; +mod logic; +mod net; +mod process; +mod settings; +mod std; +mod thread_bound; +mod ui; + +#[cfg(test)] +mod test; + +#[cfg(not(mock))] +fn main() { + let log_target = logging::init(); + + let mut config = Config::new(); + let config_result = config.read_from_environment(); + config.log_target = Some(log_target); + + let mut config = Arc::new(config); + + let result = config_result.and_then(|()| { + let attempted_send = try_run(&mut config)?; + if !attempted_send { + // Exited without attempting to send the crash report; delete files. + config.delete_files(); + } + Ok(()) + }); + + if let Err(message) = result { + // TODO maybe errors should also delete files? + log::error!("exiting with error: {message}"); + if !config.auto_submit { + // Only show a dialog if auto_submit is disabled. + ui::error_dialog(&config, message); + } + std::process::exit(1); + } +} + +#[cfg(mock)] +fn main() { + // TODO it'd be nice to be able to set these values at runtime in some way when running the + // mock application. + + use crate::std::{ + fs::{MockFS, MockFiles}, + mock, + process::Command, + }; + const MOCK_MINIDUMP_EXTRA: &str = r#"{ + "Vendor": "FooCorp", + "ProductName": "Bar", + "ReleaseChannel": "release", + "BuildID": "1234", + "StackTraces": { + "status": "OK" + }, + "Version": "100.0", + "ServerURL": "https://reports.example", + "TelemetryServerURL": "https://telemetry.example", + "TelemetryClientId": "telemetry_client", + "TelemetrySessionId": "telemetry_session", + "URL": "https://url.example" + }"#; + + // Actual content doesn't matter, aside from the hash that is generated. + const MOCK_MINIDUMP_FILE: &[u8] = &[1, 2, 3, 4]; + const MOCK_CURRENT_TIME: &str = "2004-11-09T12:34:56Z"; + const MOCK_PING_UUID: uuid::Uuid = uuid::Uuid::nil(); + const MOCK_REMOTE_CRASH_ID: &str = "8cbb847c-def2-4f68-be9e-000000000000"; + + // Create a default set of files which allow successful operation. + let mock_files = MockFiles::new(); + mock_files + .add_file("minidump.dmp", MOCK_MINIDUMP_FILE) + .add_file("minidump.extra", MOCK_MINIDUMP_EXTRA); + + // Create a default mock environment which allows successful operation. + let mut mock = mock::builder(); + mock.set( + Command::mock("work_dir/minidump-analyzer"), + Box::new(|_| Ok(crate::std::process::success_output())), + ) + .set( + Command::mock("work_dir/pingsender"), + Box::new(|_| Ok(crate::std::process::success_output())), + ) + .set( + Command::mock("curl"), + Box::new(|_| { + let mut output = crate::std::process::success_output(); + output.stdout = format!("CrashID={MOCK_REMOTE_CRASH_ID}").into(); + // Network latency. + std::thread::sleep(std::time::Duration::from_secs(2)); + Ok(output) + }), + ) + .set(MockFS, mock_files.clone()) + .set( + crate::std::env::MockCurrentExe, + "work_dir/crashreporter".into(), + ) + .set( + crate::std::time::MockCurrentTime, + time::OffsetDateTime::parse( + MOCK_CURRENT_TIME, + &time::format_description::well_known::Rfc3339, + ) + .unwrap() + .into(), + ) + .set(mock::MockHook::new("ping_uuid"), MOCK_PING_UUID); + + let result = mock.run(|| { + let mut cfg = Config::new(); + cfg.data_dir = Some("data_dir".into()); + cfg.events_dir = Some("events_dir".into()); + cfg.ping_dir = Some("ping_dir".into()); + cfg.dump_file = Some("minidump.dmp".into()); + cfg.restart_command = Some("mockfox".into()); + cfg.strings = Some(lang::load().unwrap()); + let mut cfg = Arc::new(cfg); + try_run(&mut cfg) + }); + + if let Err(e) = result { + log::error!("exiting with error: {e}"); + std::process::exit(1); + } +} + +fn try_run(config: &mut Arc<Config>) -> anyhow::Result<bool> { + if config.dump_file.is_none() { + if !config.auto_submit { + Err(anyhow::anyhow!(config.string("crashreporter-information"))) + } else { + Ok(false) + } + } else { + // Run minidump-analyzer to gather stack traces. + { + let analyzer_path = config.sibling_program_path("minidump-analyzer"); + let mut cmd = crate::process::background_command(&analyzer_path); + if config.dump_all_threads { + cmd.arg("--full"); + } + cmd.arg(config.dump_file()); + let output = cmd + .output() + .with_context(|| config.string("crashreporter-error-minidump-analyzer"))?; + if !output.status.success() { + log::warn!( + "minidump-analyzer failed to run ({});\n\nstderr: {}\n\nstdout: {}", + output.status, + String::from_utf8_lossy(&output.stderr), + String::from_utf8_lossy(&output.stdout), + ); + } + } + + let extra = { + // Perform a few things which may change the config, then treat is as immutable. + let config = Arc::get_mut(config).expect("unexpected config references"); + let extra = config.load_extra_file()?; + config.move_crash_data_to_pending()?; + extra + }; + + logic::ReportCrash::new(config.clone(), extra)?.run() + } +} diff --git a/toolkit/crashreporter/client/app/src/net/legacy_telemetry.rs b/toolkit/crashreporter/client/app/src/net/legacy_telemetry.rs new file mode 100644 index 0000000000..680f1614b0 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/net/legacy_telemetry.rs @@ -0,0 +1,177 @@ +/* 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/. */ + +//! Support for legacy telemetry ping creation. The ping support serialization which should be used +//! when submitting. + +use anyhow::Context; +use serde::Serialize; +use std::collections::BTreeMap; +use uuid::Uuid; + +const TELEMETRY_VERSION: u64 = 4; +const PAYLOAD_VERSION: u64 = 1; + +// Generated by `build.rs`. +// static PING_ANNOTATIONS: phf::Set<&'static str>; +include!(concat!(env!("OUT_DIR"), "/ping_annotations.rs")); + +#[derive(Serialize)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum Ping<'a> { + Crash { + id: Uuid, + version: u64, + #[serde(with = "time::serde::rfc3339")] + creation_date: time::OffsetDateTime, + client_id: &'a str, + #[serde(skip_serializing_if = "serde_json::Value::is_null")] + environment: serde_json::Value, + payload: Payload<'a>, + application: Application<'a>, + }, +} + +time::serde::format_description!(date_format, Date, "[year]-[month]-[day]"); + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Payload<'a> { + session_id: &'a str, + version: u64, + #[serde(with = "date_format")] + crash_date: time::Date, + #[serde(with = "time::serde::rfc3339")] + crash_time: time::OffsetDateTime, + has_crash_environment: bool, + crash_id: &'a str, + minidump_sha256_hash: Option<&'a str>, + process_type: &'a str, + #[serde(skip_serializing_if = "serde_json::Value::is_null")] + stack_traces: serde_json::Value, + metadata: BTreeMap<&'a str, &'a str>, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Application<'a> { + vendor: &'a str, + name: &'a str, + build_id: &'a str, + display_version: String, + platform_version: String, + version: &'a str, + channel: &'a str, + #[serde(skip_serializing_if = "Option::is_none")] + architecture: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + xpcom_abi: Option<String>, +} + +impl<'a> Ping<'a> { + pub fn crash( + extra: &'a serde_json::Value, + crash_id: &'a str, + minidump_sha256_hash: Option<&'a str>, + ) -> anyhow::Result<Self> { + let now: time::OffsetDateTime = crate::std::time::SystemTime::now().into(); + let environment: serde_json::Value = extra["TelemetryEnvironment"] + .as_str() + .and_then(|estr| serde_json::from_str(estr).ok()) + .unwrap_or_default(); + + // The subset of extra file entries (crash annotations) which are allowed in pings. + let metadata = extra + .as_object() + .map(|map| { + map.iter() + .filter_map(|(k, v)| { + PING_ANNOTATIONS + .contains(k) + .then(|| k.as_str()) + .zip(v.as_str()) + }) + .collect() + }) + .unwrap_or_default(); + + let display_version = environment + .pointer("/build/displayVersion") + .and_then(|s| s.as_str()) + .unwrap_or_default() + .to_owned(); + let platform_version = environment + .pointer("/build/platformVersion") + .and_then(|s| s.as_str()) + .unwrap_or_default() + .to_owned(); + let architecture = environment + .pointer("/build/architecture") + .and_then(|s| s.as_str()) + .map(ToOwned::to_owned); + let xpcom_abi = environment + .pointer("/build/xpcomAbi") + .and_then(|s| s.as_str()) + .map(ToOwned::to_owned); + + Ok(Ping::Crash { + id: crate::std::mock::hook(Uuid::new_v4(), "ping_uuid"), + version: TELEMETRY_VERSION, + creation_date: now, + client_id: extra["TelemetryClientId"] + .as_str() + .context("missing TelemetryClientId")?, + environment, + payload: Payload { + session_id: extra["TelemetrySessionId"] + .as_str() + .context("missing TelemetrySessionId")?, + version: PAYLOAD_VERSION, + crash_date: now.date(), + crash_time: now, + has_crash_environment: true, + crash_id, + minidump_sha256_hash, + process_type: "main", + stack_traces: extra["StackTraces"].clone(), + metadata, + }, + application: Application { + vendor: extra["Vendor"].as_str().unwrap_or_default(), + name: extra["ProductName"].as_str().unwrap_or_default(), + build_id: extra["BuildID"].as_str().unwrap_or_default(), + display_version, + platform_version, + version: extra["Version"].as_str().unwrap_or_default(), + channel: extra["ReleaseChannel"].as_str().unwrap_or_default(), + architecture, + xpcom_abi, + }, + }) + } + + /// Generate the telemetry URL for submitting this ping. + pub fn submission_url(&self, extra: &serde_json::Value) -> anyhow::Result<String> { + let url = extra["TelemetryServerURL"] + .as_str() + .context("missing TelemetryServerURL")?; + let id = self.id(); + let name = extra["ProductName"] + .as_str() + .context("missing ProductName")?; + let version = extra["Version"].as_str().context("missing Version")?; + let channel = extra["ReleaseChannel"] + .as_str() + .context("missing ReleaseChannel")?; + let buildid = extra["BuildID"].as_str().context("missing BuildID")?; + Ok(format!("{url}/submit/telemetry/{id}/crash/{name}/{version}/{channel}/{buildid}?v={TELEMETRY_VERSION}")) + } + + /// Get the ping identifier. + pub fn id(&self) -> &Uuid { + match self { + Ping::Crash { id, .. } => id, + } + } +} diff --git a/toolkit/crashreporter/client/app/src/net/libcurl.rs b/toolkit/crashreporter/client/app/src/net/libcurl.rs new file mode 100644 index 0000000000..0adfd7d4b4 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/net/libcurl.rs @@ -0,0 +1,406 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +//! Partial libcurl bindings with some wrappers for safe cleanup. + +use crate::std::path::Path; +use libloading::{Library, Symbol}; +use once_cell::sync::Lazy; +use std::ffi::{c_char, c_long, c_uint, CStr, CString}; + +// Constants lifted from `curl.h` +const CURLE_OK: CurlCode = 0; +const CURL_ERROR_SIZE: usize = 256; + +const CURLOPTTYPE_LONG: CurlOption = 0; +const CURLOPTTYPE_OBJECTPOINT: CurlOption = 10000; +const CURLOPTTYPE_FUNCTIONPOINT: CurlOption = 20000; +const CURLOPTTYPE_STRINGPOINT: CurlOption = CURLOPTTYPE_OBJECTPOINT; +const CURLOPTTYPE_CBPOINT: CurlOption = CURLOPTTYPE_OBJECTPOINT; + +const CURLOPT_WRITEDATA: CurlOption = CURLOPTTYPE_CBPOINT + 1; +const CURLOPT_URL: CurlOption = CURLOPTTYPE_STRINGPOINT + 2; +const CURLOPT_ERRORBUFFER: CurlOption = CURLOPTTYPE_OBJECTPOINT + 10; +const CURLOPT_WRITEFUNCTION: CurlOption = CURLOPTTYPE_FUNCTIONPOINT + 11; +const CURLOPT_USERAGENT: CurlOption = CURLOPTTYPE_STRINGPOINT + 18; +const CURLOPT_MIMEPOST: CurlOption = CURLOPTTYPE_OBJECTPOINT + 269; +const CURLOPT_MAXREDIRS: CurlOption = CURLOPTTYPE_LONG + 68; + +const CURLINFO_LONG: CurlInfo = 0x200000; +const CURLINFO_RESPONSE_CODE: CurlInfo = CURLINFO_LONG + 2; + +const CURL_LIB_NAMES: &[&str] = if cfg!(target_os = "linux") { + &[ + "libcurl.so", + "libcurl.so.4", + // Debian gives libcurl a different name when it is built against GnuTLS + "libcurl-gnutls.so", + "libcurl-gnutls.so.4", + // Older versions in case we find nothing better + "libcurl.so.3", + "libcurl-gnutls.so.3", // See above for Debian + ] +} else if cfg!(target_os = "macos") { + &[ + "/usr/lib/libcurl.dylib", + "/usr/lib/libcurl.4.dylib", + "/usr/lib/libcurl.3.dylib", + ] +} else if cfg!(target_os = "windows") { + &["libcurl.dll", "curl.dll"] +} else { + &[] +}; + +// Shim until min rust version 1.74 which allows std::io::Error::other +fn error_other<E>(error: E) -> std::io::Error +where + E: Into<Box<dyn std::error::Error + Send + Sync>>, +{ + std::io::Error::new(std::io::ErrorKind::Other, error) +} + +#[repr(transparent)] +#[derive(Clone, Copy)] +struct CurlHandle(*mut ()); +type CurlCode = c_uint; +type CurlOption = c_uint; +type CurlInfo = c_uint; +#[repr(transparent)] +#[derive(Clone, Copy)] +struct CurlMime(*mut ()); +#[repr(transparent)] +#[derive(Clone, Copy)] +struct CurlMimePart(*mut ()); + +macro_rules! library_binding { + ( $localname:ident members[$($members:tt)*] load[$($load:tt)*] fn $name:ident $args:tt $( -> $ret:ty )? ; $($rest:tt)* ) => { + library_binding! { + $localname + members[ + $($members)* + $name: Symbol<'static, unsafe extern fn $args $(->$ret)?>, + ] + load[ + $($load)* + $name: unsafe { + let symbol = $localname.get::<unsafe extern fn $args $(->$ret)?>(stringify!($name).as_bytes()) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?; + // All symbols refer to library, so `'static` lifetimes are safe (`library` + // will outlive them). + std::mem::transmute(symbol) + }, + ] + $($rest)* + } + }; + ( $localname:ident members[$($members:tt)*] load[$($load:tt)*] ) => { + pub struct Curl { + $($members)* + _library: Library + } + + impl Curl { + fn load() -> std::io::Result<Self> { + // Try each of the libraries, debug-logging load failures. + let library = CURL_LIB_NAMES.iter().find_map(|name| { + log::debug!("attempting to load {name}"); + match unsafe { Library::new(name) } { + Ok(lib) => { + log::info!("loaded {name}"); + Some(lib) + } + Err(e) => { + log::debug!("error when loading {name}: {e}"); + None + } + } + }); + + let $localname = library.ok_or_else(|| { + std::io::Error::new(std::io::ErrorKind::NotFound, "failed to find curl library") + })?; + + Ok(Curl { $($load)* _library: $localname }) + } + } + }; + ( $($rest:tt)* ) => { + library_binding! { + library members[] load[] $($rest)* + } + } +} + +library_binding! { + fn curl_easy_init() -> CurlHandle; + fn curl_easy_setopt(CurlHandle, CurlOption, ...) -> CurlCode; + fn curl_easy_perform(CurlHandle) -> CurlCode; + fn curl_easy_getinfo(CurlHandle, CurlInfo, ...) -> CurlCode; + fn curl_easy_cleanup(CurlHandle); + fn curl_mime_init(CurlHandle) -> CurlMime; + fn curl_mime_addpart(CurlMime) -> CurlMimePart; + fn curl_mime_name(CurlMimePart, *const c_char) -> CurlCode; + fn curl_mime_filename(CurlMimePart, *const c_char) -> CurlCode; + fn curl_mime_type(CurlMimePart, *const c_char) -> CurlCode; + fn curl_mime_data(CurlMimePart, *const c_char, usize) -> CurlCode; + fn curl_mime_filedata(CurlMimePart, *const c_char) -> CurlCode; + fn curl_mime_free(CurlMime); +} + +/// Load libcurl if possible. +pub fn load() -> std::io::Result<&'static Curl> { + static CURL: Lazy<std::io::Result<Curl>> = Lazy::new(Curl::load); + CURL.as_ref().map_err(error_other) +} + +#[derive(Debug)] +pub struct Error { + code: CurlCode, + error: Option<String>, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "curl error code {}", self.code)?; + if let Some(e) = &self.error { + write!(f, ": {e}")?; + } + Ok(()) + } +} + +impl std::error::Error for Error {} + +impl From<Error> for std::io::Error { + fn from(e: Error) -> Self { + error_other(e) + } +} + +pub type Result<T> = std::result::Result<T, Error>; + +fn to_result(code: CurlCode) -> Result<()> { + if code == CURLE_OK { + Ok(()) + } else { + Err(Error { code, error: None }) + } +} + +impl Curl { + pub fn easy(&self) -> std::io::Result<Easy> { + let handle = unsafe { (self.curl_easy_init)() }; + if handle.0.is_null() { + Err(error_other("curl_easy_init failed")) + } else { + Ok(Easy { + lib: self, + handle, + mime: Default::default(), + }) + } + } +} + +struct ErrorBuffer([u8; CURL_ERROR_SIZE]); + +impl Default for ErrorBuffer { + fn default() -> Self { + ErrorBuffer([0; CURL_ERROR_SIZE]) + } +} + +pub struct Easy<'a> { + lib: &'a Curl, + handle: CurlHandle, + mime: Option<Mime<'a>>, +} + +impl<'a> Easy<'a> { + pub fn set_url(&mut self, url: &str) -> Result<()> { + let url = CString::new(url.to_string()).unwrap(); + to_result(unsafe { (self.lib.curl_easy_setopt)(self.handle, CURLOPT_URL, url.as_ptr()) }) + } + + pub fn set_user_agent(&mut self, user_agent: &str) -> Result<()> { + let ua = CString::new(user_agent.to_string()).unwrap(); + to_result(unsafe { + (self.lib.curl_easy_setopt)(self.handle, CURLOPT_USERAGENT, ua.as_ptr()) + }) + } + + pub fn mime(&self) -> std::io::Result<Mime<'a>> { + let handle = unsafe { (self.lib.curl_mime_init)(self.handle) }; + if handle.0.is_null() { + Err(error_other("curl_mime_init failed")) + } else { + Ok(Mime { + lib: self.lib, + handle, + }) + } + } + + pub fn set_mime_post(&mut self, mime: Mime<'a>) -> Result<()> { + let result = to_result(unsafe { + (self.lib.curl_easy_setopt)(self.handle, CURLOPT_MIMEPOST, mime.handle) + }); + if result.is_ok() { + self.mime = Some(mime); + } + result + } + + pub fn set_max_redirs(&mut self, redirs: c_long) -> Result<()> { + to_result(unsafe { (self.lib.curl_easy_setopt)(self.handle, CURLOPT_MAXREDIRS, redirs) }) + } + + /// Returns the response data on success. + pub fn perform(&self) -> Result<Vec<u8>> { + // Set error buffer, but degrade service if it doesn't work. + let mut error_buffer = ErrorBuffer::default(); + let error_buffer_set = unsafe { + (self.lib.curl_easy_setopt)( + self.handle, + CURLOPT_ERRORBUFFER, + error_buffer.0.as_mut_ptr() as *mut c_char, + ) + } == CURLE_OK; + + // Set the write function to fill a Vec. If there is a panic, this might leave stale + // pointers in the curl options, but they won't be used without another perform, at which + // point they'll be overwritten. + let mut data: Vec<u8> = Vec::new(); + extern "C" fn write_callback( + data: *const u8, + size: usize, + nmemb: usize, + dest: &mut Vec<u8>, + ) -> usize { + let total = size * nmemb; + dest.extend(unsafe { std::slice::from_raw_parts(data, total) }); + total + } + unsafe { + to_result((self.lib.curl_easy_setopt)( + self.handle, + CURLOPT_WRITEFUNCTION, + write_callback as extern "C" fn(*const u8, usize, usize, &mut Vec<u8>) -> usize, + ))?; + to_result((self.lib.curl_easy_setopt)( + self.handle, + CURLOPT_WRITEDATA, + &mut data as *mut _, + ))?; + }; + + let mut result = to_result(unsafe { (self.lib.curl_easy_perform)(self.handle) }); + + // Clean up a bit by unsetting the write function and write data, though they won't be used + // anywhere else. Ignore return values. + unsafe { + (self.lib.curl_easy_setopt)( + self.handle, + CURLOPT_WRITEFUNCTION, + std::ptr::null_mut::<()>(), + ); + (self.lib.curl_easy_setopt)(self.handle, CURLOPT_WRITEDATA, std::ptr::null_mut::<()>()); + } + + if error_buffer_set { + unsafe { + (self.lib.curl_easy_setopt)( + self.handle, + CURLOPT_ERRORBUFFER, + std::ptr::null_mut::<()>(), + ) + }; + if let Err(e) = &mut result { + if let Ok(cstr) = CStr::from_bytes_until_nul(error_buffer.0.as_slice()) { + e.error = Some(cstr.to_string_lossy().into_owned()); + } + } + } + + result.map(move |()| data) + } + + pub fn get_response_code(&self) -> Result<u64> { + let mut code = c_long::default(); + to_result(unsafe { + (self.lib.curl_easy_getinfo)( + self.handle, + CURLINFO_RESPONSE_CODE, + &mut code as *mut c_long, + ) + })?; + Ok(code.try_into().expect("negative http response code")) + } +} + +impl Drop for Easy<'_> { + fn drop(&mut self) { + self.mime.take(); + unsafe { (self.lib.curl_easy_cleanup)(self.handle) }; + } +} + +pub struct Mime<'a> { + lib: &'a Curl, + handle: CurlMime, +} + +impl<'a> Mime<'a> { + pub fn add_part(&mut self) -> std::io::Result<MimePart<'a>> { + let handle = unsafe { (self.lib.curl_mime_addpart)(self.handle) }; + if handle.0.is_null() { + Err(error_other("curl_mime_addpart failed")) + } else { + Ok(MimePart { + lib: self.lib, + handle, + }) + } + } +} + +impl Drop for Mime<'_> { + fn drop(&mut self) { + unsafe { (self.lib.curl_mime_free)(self.handle) }; + } +} + +pub struct MimePart<'a> { + lib: &'a Curl, + handle: CurlMimePart, +} + +impl MimePart<'_> { + pub fn set_name(&mut self, name: &str) -> Result<()> { + let name = CString::new(name.to_string()).unwrap(); + to_result(unsafe { (self.lib.curl_mime_name)(self.handle, name.as_ptr()) }) + } + + pub fn set_filename(&mut self, filename: &str) -> Result<()> { + let filename = CString::new(filename.to_string()).unwrap(); + to_result(unsafe { (self.lib.curl_mime_filename)(self.handle, filename.as_ptr()) }) + } + + pub fn set_type(&mut self, mime_type: &str) -> Result<()> { + let mime_type = CString::new(mime_type.to_string()).unwrap(); + to_result(unsafe { (self.lib.curl_mime_type)(self.handle, mime_type.as_ptr()) }) + } + + pub fn set_filedata(&mut self, file: &Path) -> Result<()> { + let file = CString::new(file.display().to_string()).unwrap(); + to_result(unsafe { (self.lib.curl_mime_filedata)(self.handle, file.as_ptr()) }) + } + + pub fn set_data(&mut self, data: &[u8]) -> Result<()> { + to_result(unsafe { + (self.lib.curl_mime_data)(self.handle, data.as_ptr() as *const c_char, data.len()) + }) + } +} diff --git a/toolkit/crashreporter/client/app/src/net/mod.rs b/toolkit/crashreporter/client/app/src/net/mod.rs new file mode 100644 index 0000000000..d9951d7fc3 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/net/mod.rs @@ -0,0 +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/. */ + +pub mod legacy_telemetry; +mod libcurl; +pub mod report; + +#[cfg(test)] +pub fn can_load_libcurl() -> bool { + libcurl::load().is_ok() +} diff --git a/toolkit/crashreporter/client/app/src/net/report.rs b/toolkit/crashreporter/client/app/src/net/report.rs new file mode 100644 index 0000000000..46be952547 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/net/report.rs @@ -0,0 +1,276 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +//! Support for crash report creation and upload. +//! +//! Upload currently uses the system libcurl or curl binary rather than a rust network stack (as +//! curl is more mature, albeit the code to interact with it must be a bit more careful). + +use crate::std::{ffi::OsStr, path::Path, process::Child}; +use anyhow::Context; + +#[cfg(mock)] +use crate::std::mock::{mock_key, MockKey}; + +#[cfg(mock)] +mock_key! { + pub struct MockLibCurl => Box<dyn Fn(&CrashReport) -> std::io::Result<std::io::Result<String>> + Send + Sync> +} + +pub const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); + +/// A crash report to upload. +/// +/// Post a multipart form payload to the report URL. +/// +/// The form data contains: +/// | name | filename | content | mime | +/// ==================================== +/// | `extra` | `extra.json` | extra json object | `application/json`| +/// | `upload_file_minidump` | dump file name | dump file contents | derived (probably application/binary) | +/// if present: +/// | `memory_report` | memory file name | memory file contents | derived (probably gzipped json) | +pub struct CrashReport<'a> { + pub extra: &'a serde_json::Value, + pub dump_file: &'a Path, + pub memory_file: Option<&'a Path>, + pub url: &'a OsStr, +} + +impl CrashReport<'_> { + /// Send the crash report. + pub fn send(&self) -> std::io::Result<CrashReportSender> { + // Windows 10+ and macOS 10.15+ contain `curl` 7.64.1+ as a system-provided binary, so + // `send_with_curl_binary` should not fail. + // + // Linux distros generally do not contain `curl`, but `libcurl` is very likely to be + // incidentally installed (if not outright part of the distro base packages). Based on a + // cursory look at the debian repositories as an examplar, the curl binary is much less + // likely to be incidentally installed. + // + // For uniformity, we always will try the curl binary first, then try libcurl if that + // fails. + + let extra_json_data = serde_json::to_string(self.extra)?; + + self.send_with_curl_binary(extra_json_data.clone()) + .or_else(|e| { + log::info!("failed to invoke curl ({e}), trying libcurl"); + self.send_with_libcurl(extra_json_data.clone()) + }) + } + + /// Send the crash report using the `curl` binary. + fn send_with_curl_binary(&self, extra_json_data: String) -> std::io::Result<CrashReportSender> { + let mut cmd = crate::process::background_command("curl"); + + cmd.args(["--user-agent", USER_AGENT]); + + cmd.arg("--form"); + // `@-` causes the data to be read from stdin, which is desirable to not have to worry + // about process argument string length limitations (though they are generally pretty high + // limits). + cmd.arg("extra=@-;filename=extra.json;type=application/json"); + + cmd.arg("--form"); + cmd.arg(format!( + "upload_file_minidump=@{}", + CurlQuote(&self.dump_file.display().to_string()) + )); + + if let Some(path) = self.memory_file { + cmd.arg("--form"); + cmd.arg(format!( + "memory_report=@{}", + CurlQuote(&path.display().to_string()) + )); + } + + cmd.arg(self.url); + + cmd.stdin(std::process::Stdio::piped()); + cmd.stdout(std::process::Stdio::piped()); + cmd.stderr(std::process::Stdio::piped()); + + cmd.spawn().map(move |child| CrashReportSender::CurlChild { + child, + extra_json_data, + }) + } + + /// Send the crash report using the `curl` library. + fn send_with_libcurl(&self, extra_json_data: String) -> std::io::Result<CrashReportSender> { + #[cfg(mock)] + if !crate::std::mock::try_hook(false, "use_system_libcurl") { + return self.send_with_mock_libcurl(extra_json_data); + } + + let curl = super::libcurl::load()?; + let mut easy = curl.easy()?; + + easy.set_url(&self.url.to_string_lossy())?; + easy.set_user_agent(USER_AGENT)?; + easy.set_max_redirs(30)?; + + let mut mime = easy.mime()?; + { + let mut part = mime.add_part()?; + part.set_name("extra")?; + part.set_filename("extra.json")?; + part.set_type("application/json")?; + part.set_data(extra_json_data.as_bytes())?; + } + { + let mut part = mime.add_part()?; + part.set_name("upload_file_minidump")?; + part.set_filename(&self.dump_file.display().to_string())?; + part.set_filedata(self.dump_file)?; + } + if let Some(path) = self.memory_file { + let mut part = mime.add_part()?; + part.set_name("memory_report")?; + part.set_filename(&path.display().to_string())?; + part.set_filedata(path)?; + } + easy.set_mime_post(mime)?; + + Ok(CrashReportSender::LibCurl { easy }) + } + + #[cfg(mock)] + fn send_with_mock_libcurl( + &self, + _extra_json_data: String, + ) -> std::io::Result<CrashReportSender> { + MockLibCurl + .get(|f| f(&self)) + .map(|response| CrashReportSender::MockLibCurl { response }) + } +} + +pub enum CrashReportSender { + CurlChild { + child: Child, + extra_json_data: String, + }, + LibCurl { + easy: super::libcurl::Easy<'static>, + }, + #[cfg(mock)] + MockLibCurl { + response: std::io::Result<String>, + }, +} + +impl CrashReportSender { + pub fn finish(self) -> anyhow::Result<Response> { + let response = match self { + Self::CurlChild { + mut child, + extra_json_data, + } => { + { + let mut stdin = child + .stdin + .take() + .context("failed to get curl process stdin")?; + std::io::copy(&mut std::io::Cursor::new(extra_json_data), &mut stdin) + .context("failed to write extra file data to stdin of curl process")?; + // stdin is dropped at the end of this scope so that the stream gets an EOF, + // otherwise curl will wait for more input. + } + let output = child + .wait_with_output() + .context("failed to wait on curl process")?; + anyhow::ensure!( + output.status.success(), + "process failed (exit status {}) with stderr: {}", + output.status, + String::from_utf8_lossy(&output.stderr) + ); + String::from_utf8_lossy(&output.stdout).into_owned() + } + Self::LibCurl { easy } => { + let response = easy.perform()?; + let response_code = easy.get_response_code()?; + + let response = String::from_utf8_lossy(&response).into_owned(); + dbg!(&response, &response_code); + + anyhow::ensure!( + response_code == 200, + "unexpected response code ({response_code}): {response}" + ); + + response + } + #[cfg(mock)] + Self::MockLibCurl { response } => response?.into(), + }; + + log::debug!("received response from sending report: {:?}", &*response); + Ok(Response::parse(response)) + } +} + +/// A parsed response from submitting a crash report. +#[derive(Default, Debug)] +pub struct Response { + pub crash_id: Option<String>, + pub stop_sending_reports_for: Option<String>, + pub view_url: Option<String>, + pub discarded: bool, +} + +impl Response { + /// Parse a server response. + /// + /// The response should be newline-separated `<key>=<value>` pairs. + fn parse<S: AsRef<str>>(response: S) -> Self { + let mut ret = Self::default(); + // Fields may be omitted, and parsing is best-effort but will not produce any errors (just + // a default Response struct). + for line in response.as_ref().lines() { + if let Some((key, value)) = line.split_once('=') { + match key { + "StopSendingReportsFor" => { + ret.stop_sending_reports_for = Some(value.to_owned()) + } + "Discarded" => ret.discarded = true, + "CrashID" => ret.crash_id = Some(value.to_owned()), + "ViewURL" => ret.view_url = Some(value.to_owned()), + _ => (), + } + } + } + ret + } +} + +/// Quote a string per https://curl.se/docs/manpage.html#-F. +/// That is, add quote characters and escape " and \ with backslashes. +struct CurlQuote<'a>(&'a str); +impl std::fmt::Display for CurlQuote<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use std::fmt::Write; + + f.write_char('"')?; + const ESCAPE_CHARS: [char; 2] = ['"', '\\']; + for substr in self.0.split_inclusive(ESCAPE_CHARS) { + // The last string returned by `split_inclusive` may or may not contain the + // search character, unfortunately. + if substr.ends_with(ESCAPE_CHARS) { + // Safe to use a byte offset rather than a character offset because the + // ESCAPE_CHARS are each 1 byte in utf8. + let (s, escape) = substr.split_at(substr.len() - 1); + f.write_str(s)?; + f.write_char('\\')?; + f.write_str(escape)?; + } else { + f.write_str(substr)?; + } + } + f.write_char('"') + } +} diff --git a/toolkit/crashreporter/client/app/src/process.rs b/toolkit/crashreporter/client/app/src/process.rs new file mode 100644 index 0000000000..126e5b533b --- /dev/null +++ b/toolkit/crashreporter/client/app/src/process.rs @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! Process utility functions. + +use crate::std::{ffi::OsStr, process::Command}; + +/// Return a command configured to run in the background. +/// +/// This means that no associated console will be opened, when applicable. +pub fn background_command<S: AsRef<OsStr>>(program: S) -> Command { + #[allow(unused_mut)] + let mut cmd = Command::new(program); + #[cfg(windows)] + { + #[cfg_attr(mock, allow(unused))] + use std::os::windows::process::CommandExt; + use windows_sys::Win32::System::Threading::CREATE_NO_WINDOW; + cmd.creation_flags(CREATE_NO_WINDOW); + } + cmd +} diff --git a/toolkit/crashreporter/client/app/src/settings.rs b/toolkit/crashreporter/client/app/src/settings.rs new file mode 100644 index 0000000000..7ea0e4fe26 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/settings.rs @@ -0,0 +1,39 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +//! Persistent settings of the application. + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct Settings { + /// Whether a crash report should be sent. + pub submit_report: bool, + /// Whether the URL that was open should be included in a sent report. + pub include_url: bool, +} + +impl Default for Settings { + fn default() -> Self { + Settings { + submit_report: true, + include_url: false, + } + } +} + +impl Settings { + /// Write the settings to the given writer. + pub fn to_writer<W: std::io::Write>(&self, writer: W) -> anyhow::Result<()> { + Ok(serde_json::to_writer_pretty(writer, self)?) + } + + /// Read the settings from the given reader. + pub fn from_reader<R: std::io::Read>(reader: R) -> anyhow::Result<Self> { + Ok(serde_json::from_reader(reader)?) + } + + #[cfg(test)] + pub fn to_string(&self) -> String { + serde_json::to_string_pretty(self).unwrap() + } +} diff --git a/toolkit/crashreporter/client/app/src/std/env.rs b/toolkit/crashreporter/client/app/src/std/env.rs new file mode 100644 index 0000000000..edc22ded8d --- /dev/null +++ b/toolkit/crashreporter/client/app/src/std/env.rs @@ -0,0 +1,45 @@ +/* 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/. */ + +use super::mock::{mock_key, MockKey}; +pub use std::env::VarError; +use std::ffi::{OsStr, OsString}; + +mock_key! { + pub struct MockCurrentExe => std::path::PathBuf +} + +pub struct ArgsOs { + argv0: Option<OsString>, +} + +impl Iterator for ArgsOs { + type Item = OsString; + + fn next(&mut self) -> Option<Self::Item> { + Some( + self.argv0 + .take() + .expect("only argv[0] is available when mocked"), + ) + } +} + +pub fn var<K: AsRef<OsStr>>(_key: K) -> Result<String, VarError> { + unimplemented!("no var access in tests") +} + +pub fn var_os<K: AsRef<OsStr>>(_key: K) -> Option<OsString> { + unimplemented!("no var access in tests") +} + +pub fn args_os() -> ArgsOs { + MockCurrentExe.get(|r| ArgsOs { + argv0: Some(r.clone().into()), + }) +} + +pub fn current_exe() -> std::io::Result<super::path::PathBuf> { + Ok(MockCurrentExe.get(|r| r.clone().into())) +} diff --git a/toolkit/crashreporter/client/app/src/std/fs.rs b/toolkit/crashreporter/client/app/src/std/fs.rs new file mode 100644 index 0000000000..8ba2c572d5 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/std/fs.rs @@ -0,0 +1,559 @@ +/* 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/. */ + +use crate::std::mock::{mock_key, MockKey}; +use std::collections::HashMap; +use std::ffi::OsString; +use std::io::{ErrorKind, Read, Result, Seek, SeekFrom, Write}; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; +use std::time::SystemTime; + +/// Mock filesystem file content. +#[derive(Debug, Default, Clone)] +pub struct MockFileContent(Arc<Mutex<Vec<u8>>>); + +impl MockFileContent { + pub fn empty() -> Self { + Self::default() + } + + pub fn new(data: String) -> Self { + Self::new_bytes(data.into()) + } + + pub fn new_bytes(data: Vec<u8>) -> Self { + MockFileContent(Arc::new(Mutex::new(data))) + } +} + +impl From<()> for MockFileContent { + fn from(_: ()) -> Self { + Self::empty() + } +} + +impl From<String> for MockFileContent { + fn from(s: String) -> Self { + Self::new(s) + } +} + +impl From<&str> for MockFileContent { + fn from(s: &str) -> Self { + Self::new(s.to_owned()) + } +} + +impl From<Vec<u8>> for MockFileContent { + fn from(bytes: Vec<u8>) -> Self { + Self::new_bytes(bytes) + } +} + +impl From<&[u8]> for MockFileContent { + fn from(bytes: &[u8]) -> Self { + Self::new_bytes(bytes.to_owned()) + } +} + +/// Mocked filesystem directory entries. +pub type MockDirEntries = HashMap<OsString, MockFSItem>; + +/// The content of a mock filesystem item. +pub enum MockFSContent { + /// File content. + File(Result<MockFileContent>), + /// A directory with the given entries. + Dir(MockDirEntries), +} + +impl std::fmt::Debug for MockFSContent { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::File(_) => f.debug_tuple("File").finish(), + Self::Dir(e) => f.debug_tuple("Dir").field(e).finish(), + } + } +} + +/// A mock filesystem item. +#[derive(Debug)] +pub struct MockFSItem { + /// The content of the item (file/dir). + pub content: MockFSContent, + /// The modification time of the item. + pub modified: SystemTime, +} + +impl From<MockFSContent> for MockFSItem { + fn from(content: MockFSContent) -> Self { + MockFSItem { + content, + modified: SystemTime::UNIX_EPOCH, + } + } +} + +/// A mock filesystem. +#[derive(Debug, Clone)] +pub struct MockFiles { + root: Arc<Mutex<MockFSItem>>, +} + +impl Default for MockFiles { + fn default() -> Self { + MockFiles { + root: Arc::new(Mutex::new(MockFSContent::Dir(Default::default()).into())), + } + } +} + +impl MockFiles { + /// Create a new, empty filesystem. + pub fn new() -> Self { + Self::default() + } + + /// Add a mocked file with the given content. The modification time will be the unix epoch. + /// + /// Pancis if the parent directory is not already mocked. + pub fn add_file<P: AsRef<Path>, C: Into<MockFileContent>>(&self, path: P, content: C) -> &Self { + self.add_file_result(path, Ok(content.into()), SystemTime::UNIX_EPOCH) + } + + /// Add a mocked directory. + pub fn add_dir<P: AsRef<Path>>(&self, path: P) -> &Self { + self.path(path, true, |_| ()).unwrap(); + self + } + + /// Add a mocked file that returns the given result and has the given modification time. + /// + /// Pancis if the parent directory is not already mocked. + pub fn add_file_result<P: AsRef<Path>>( + &self, + path: P, + result: Result<MockFileContent>, + modified: SystemTime, + ) -> &Self { + let name = path.as_ref().file_name().expect("invalid path"); + self.parent_dir(path.as_ref(), move |dir| { + if dir.contains_key(name) { + Err(ErrorKind::AlreadyExists.into()) + } else { + dir.insert( + name.to_owned(), + MockFSItem { + content: MockFSContent::File(result), + modified, + }, + ); + Ok(()) + } + }) + .and_then(|r| r) + .unwrap(); + self + } + + /// If create_dirs is true, all missing path components (_including the final component_) are + /// created as directories. In this case `Err` is only returned if a file conflicts with + /// a directory component. + pub fn path<P: AsRef<Path>, F, R>(&self, path: P, create_dirs: bool, f: F) -> Result<R> + where + F: FnOnce(&mut MockFSItem) -> R, + { + let mut guard = self.root.lock().unwrap(); + let mut cur_entry = &mut *guard; + for component in path.as_ref().components() { + use std::path::Component::*; + match component { + CurDir | RootDir | Prefix(_) => continue, + ParentDir => panic!("unsupported path: {}", path.as_ref().display()), + Normal(name) => { + let cur_dir = match &mut cur_entry.content { + MockFSContent::File(_) => return Err(ErrorKind::NotFound.into()), + MockFSContent::Dir(d) => d, + }; + cur_entry = if create_dirs { + cur_dir + .entry(name.to_owned()) + .or_insert_with(|| MockFSContent::Dir(Default::default()).into()) + } else { + cur_dir.get_mut(name).ok_or(ErrorKind::NotFound)? + }; + } + } + } + Ok(f(cur_entry)) + } + + /// Get the mocked parent directory of the given path and call a callback on the mocked + /// directory's entries. + pub fn parent_dir<P: AsRef<Path>, F, R>(&self, path: P, f: F) -> Result<R> + where + F: FnOnce(&mut MockDirEntries) -> R, + { + self.path( + path.as_ref().parent().unwrap_or(&Path::new("")), + false, + move |item| match &mut item.content { + MockFSContent::File(_) => Err(ErrorKind::NotFound.into()), + MockFSContent::Dir(d) => Ok(f(d)), + }, + ) + .and_then(|r| r) + } + + /// Return a file assertion helper for the mocked filesystem. + pub fn assert_files(&self) -> AssertFiles { + let mut files = HashMap::new(); + let root = self.root.lock().unwrap(); + + fn dir(files: &mut HashMap<PathBuf, MockFileContent>, path: &Path, item: &MockFSItem) { + match &item.content { + MockFSContent::File(Ok(c)) => { + files.insert(path.to_owned(), c.clone()); + } + MockFSContent::Dir(d) => { + for (component, item) in d { + dir(files, &path.join(component), item); + } + } + _ => (), + } + } + dir(&mut files, Path::new(""), &*root); + AssertFiles { files } + } +} + +/// A utility for asserting the state of the mocked filesystem. +/// +/// All files must be accounted for; when dropped, a panic will occur if some files remain which +/// weren't checked. +#[derive(Debug)] +pub struct AssertFiles { + files: HashMap<PathBuf, MockFileContent>, +} + +// On windows we ignore drive prefixes. This is only relevant for real paths, which are only +// present for edge case situations in tests (where AssertFiles is used). +fn remove_prefix(p: &Path) -> &Path { + let mut iter = p.components(); + if let Some(std::path::Component::Prefix(_)) = iter.next() { + iter.next(); // Prefix is followed by RootDir + iter.as_path() + } else { + p + } +} + +impl AssertFiles { + /// Assert that the given path contains the given content (as a utf8 string). + pub fn check<P: AsRef<Path>, S: AsRef<str>>(&mut self, path: P, content: S) -> &mut Self { + let p = remove_prefix(path.as_ref()); + let Some(mfc) = self.files.remove(p) else { + panic!("missing file: {}", p.display()); + }; + let guard = mfc.0.lock().unwrap(); + assert_eq!( + std::str::from_utf8(&*guard).unwrap(), + content.as_ref(), + "file content mismatch: {}", + p.display() + ); + self + } + + /// Assert that the given path contains the given byte content. + pub fn check_bytes<P: AsRef<Path>, B: AsRef<[u8]>>( + &mut self, + path: P, + content: B, + ) -> &mut Self { + let p = remove_prefix(path.as_ref()); + let Some(mfc) = self.files.remove(p) else { + panic!("missing file: {}", p.display()); + }; + let guard = mfc.0.lock().unwrap(); + assert_eq!( + &*guard, + content.as_ref(), + "file content mismatch: {}", + p.display() + ); + self + } + + /// Ignore the given file (whether it exists or not). + pub fn ignore<P: AsRef<Path>>(&mut self, path: P) -> &mut Self { + self.files.remove(remove_prefix(path.as_ref())); + self + } + + /// Assert that the given path exists without checking its content. + pub fn check_exists<P: AsRef<Path>>(&mut self, path: P) -> &mut Self { + let p = remove_prefix(path.as_ref()); + if self.files.remove(p).is_none() { + panic!("missing file: {}", p.display()); + } + self + } + + /// Finish checking files. + /// + /// This panics if all files were not checked. + /// + /// This is also called when the value is dropped. + pub fn finish(&mut self) { + let files = std::mem::take(&mut self.files); + if !files.is_empty() { + panic!("additional files not expected: {:?}", files.keys()); + } + } +} + +impl Drop for AssertFiles { + fn drop(&mut self) { + if !std::thread::panicking() { + self.finish(); + } + } +} + +mock_key! { + pub struct MockFS => MockFiles +} + +pub struct File { + content: MockFileContent, + pos: usize, +} + +impl File { + pub fn open<P: AsRef<Path>>(path: P) -> Result<File> { + MockFS.get(move |files| { + files + .path(path, false, |item| match &item.content { + MockFSContent::File(result) => result + .as_ref() + .map(|b| File { + content: b.clone(), + pos: 0, + }) + .map_err(|e| e.kind().into()), + MockFSContent::Dir(_) => Err(ErrorKind::NotFound.into()), + }) + .and_then(|r| r) + }) + } + + pub fn create<P: AsRef<Path>>(path: P) -> Result<File> { + let path = path.as_ref(); + MockFS.get(|files| { + let name = path.file_name().expect("invalid path"); + files.parent_dir(path, move |d| { + if !d.contains_key(name) { + d.insert( + name.to_owned(), + MockFSItem { + content: MockFSContent::File(Ok(Default::default())), + modified: super::time::SystemTime::now().0, + }, + ); + } + }) + })?; + Self::open(path) + } +} + +impl Read for File { + fn read(&mut self, buf: &mut [u8]) -> Result<usize> { + let guard = self.content.0.lock().unwrap(); + if self.pos >= guard.len() { + return Ok(0); + } + let to_read = std::cmp::min(buf.len(), guard.len() - self.pos); + buf[..to_read].copy_from_slice(&guard[self.pos..self.pos + to_read]); + self.pos += to_read; + Ok(to_read) + } +} + +impl Seek for File { + fn seek(&mut self, pos: SeekFrom) -> Result<u64> { + let len = self.content.0.lock().unwrap().len(); + match pos { + SeekFrom::Start(n) => self.pos = n as usize, + SeekFrom::End(n) => { + if n < 0 { + let offset = -n as usize; + if offset > len { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "out of bounds", + )); + } + self.pos = len - offset; + } else { + self.pos = len + n as usize + } + } + SeekFrom::Current(n) => { + if n < 0 { + let offset = -n as usize; + if offset > self.pos { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "out of bounds", + )); + } + self.pos -= offset; + } else { + self.pos += n as usize; + } + } + } + Ok(self.pos as u64) + } +} + +impl Write for File { + fn write(&mut self, buf: &[u8]) -> Result<usize> { + let mut guard = self.content.0.lock().unwrap(); + let end = self.pos + buf.len(); + if end > guard.len() { + guard.resize(end, 0); + } + (&mut guard[self.pos..end]).copy_from_slice(buf); + self.pos = end; + Ok(buf.len()) + } + + fn flush(&mut self) -> Result<()> { + Ok(()) + } +} + +pub fn create_dir_all<P: AsRef<Path>>(path: P) -> Result<()> { + MockFS.get(move |files| files.path(path, true, |_| ())) +} + +pub fn rename<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> { + MockFS.get(move |files| { + let from_name = from.as_ref().file_name().expect("invalid path"); + let item = files + .parent_dir(from.as_ref(), move |d| { + d.remove(from_name).ok_or(ErrorKind::NotFound.into()) + }) + .and_then(|r| r)?; + + let to_name = to.as_ref().file_name().expect("invalid path"); + files + .parent_dir(to.as_ref(), move |d| { + // Just error if `to` exists, which doesn't quite follow `std::fs::rename` behavior. + if d.contains_key(to_name) { + Err(ErrorKind::AlreadyExists.into()) + } else { + d.insert(to_name.to_owned(), item); + Ok(()) + } + }) + .and_then(|r| r) + }) +} + +pub fn remove_file<P: AsRef<Path>>(path: P) -> Result<()> { + MockFS.get(move |files| { + let name = path.as_ref().file_name().expect("invalid path"); + files + .parent_dir(path.as_ref(), |d| { + if let Some(MockFSItem { + content: MockFSContent::Dir(_), + .. + }) = d.get(name) + { + Err(ErrorKind::NotFound.into()) + } else { + d.remove(name).ok_or(ErrorKind::NotFound.into()).map(|_| ()) + } + }) + .and_then(|r| r) + }) +} + +pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> { + File::create(path.as_ref())?.write_all(contents.as_ref()) +} + +pub struct ReadDir { + base: PathBuf, + children: Vec<OsString>, +} + +impl ReadDir { + pub fn new(path: &Path) -> Result<Self> { + MockFS.get(move |files| { + files + .path(path, false, |item| match &item.content { + MockFSContent::Dir(d) => Ok(ReadDir { + base: path.to_owned(), + children: d.keys().cloned().collect(), + }), + MockFSContent::File(_) => Err(ErrorKind::NotFound.into()), + }) + .and_then(|r| r) + }) + } +} + +impl Iterator for ReadDir { + type Item = Result<DirEntry>; + fn next(&mut self) -> Option<Self::Item> { + let child = self.children.pop()?; + Some(Ok(DirEntry(self.base.join(child)))) + } +} + +pub struct DirEntry(PathBuf); + +impl DirEntry { + pub fn path(&self) -> super::path::PathBuf { + super::path::PathBuf(self.0.clone()) + } + + pub fn metadata(&self) -> Result<Metadata> { + MockFS.get(|files| { + files.path(&self.0, false, |item| { + let is_dir = matches!(&item.content, MockFSContent::Dir(_)); + Metadata { + is_dir, + modified: item.modified, + } + }) + }) + } +} + +pub struct Metadata { + is_dir: bool, + modified: SystemTime, +} + +impl Metadata { + pub fn is_file(&self) -> bool { + !self.is_dir + } + + pub fn is_dir(&self) -> bool { + self.is_dir + } + + pub fn modified(&self) -> Result<super::time::SystemTime> { + Ok(super::time::SystemTime(self.modified)) + } +} diff --git a/toolkit/crashreporter/client/app/src/std/mock.rs b/toolkit/crashreporter/client/app/src/std/mock.rs new file mode 100644 index 0000000000..ed942a09bd --- /dev/null +++ b/toolkit/crashreporter/client/app/src/std/mock.rs @@ -0,0 +1,254 @@ +/* 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/. */ + +//! Mocking utilities. +//! +//! Mock data is set on a per-thread basis. [`crate::std::thread`] handles this automatically for +//! scoped threads, and warns about creating threads otherwise (which won't be able to +//! automatically share mocked data, but it can be easily done with [`SharedMockData`] when +//! appropriate). +//! +//! Mock data is stored using type erasure, with a [`MockKey`] indexing arbitrary values. Use +//! [`mock_key!`] to define keys and the values to which they map. This approach was taken as a +//! matter of covenience for programmers, and the resulting creation and consumption APIs are +//! succinct yet extensible. +//! +//! Consumers should define keys (and expose them for mockers), and at runtime create a mock key +//! instance and call [`MockKey::get`] or [`MockKey::try_get`] to retrieve mocked values to use. +//! +//! Mockers should call [`builder`] to create a builder, [`set`](Builder::set) key/value mappings, +//! and call [`run`](Builder::run) to execute code with the mock data set. + +use std::any::{Any, TypeId}; +use std::collections::{hash_map::DefaultHasher, HashMap}; +use std::hash::{Hash, Hasher}; +use std::sync::atomic::{AtomicPtr, Ordering::Relaxed}; + +type MockDataMap = HashMap<Box<dyn MockKeyStored>, Box<dyn Any + Send + Sync>>; + +thread_local! { + static MOCK_DATA: AtomicPtr<MockDataMap> = Default::default(); +} + +/// A trait intended to be used as a trait object interface for mock keys. +pub trait MockKeyStored: Any + std::fmt::Debug + Sync { + fn eq(&self, other: &dyn MockKeyStored) -> bool; + fn hash(&self, state: &mut DefaultHasher); +} + +impl PartialEq for dyn MockKeyStored { + fn eq(&self, other: &Self) -> bool { + MockKeyStored::eq(self, other) + } +} + +impl Eq for dyn MockKeyStored {} + +impl Hash for dyn MockKeyStored { + fn hash<H: Hasher>(&self, state: &mut H) { + self.type_id().hash(state); + let mut hasher = DefaultHasher::new(); + MockKeyStored::hash(self, &mut hasher); + state.write_u64(hasher.finish()); + } +} + +impl dyn MockKeyStored { + pub fn downcast_ref<T: Any>(&self) -> Option<&T> { + if self.type_id() == TypeId::of::<T>() { + Some(unsafe { &*(self as *const _ as *const T) }) + } else { + None + } + } +} + +/// A type which can be used as a mock key. +pub trait MockKey: MockKeyStored + Sized { + /// The value to which the key maps. + type Value: Any + Send + Sync; + + /// Get the value set for this key, returning `None` if no data is set. + fn try_get<F, R>(&self, f: F) -> Option<R> + where + F: FnOnce(&Self::Value) -> R, + { + MOCK_DATA.with(move |ptr| { + let ptr = ptr.load(Relaxed); + if ptr.is_null() { + panic!("no mock data set"); + } + unsafe { &*ptr } + .get(self as &dyn MockKeyStored) + .and_then(move |b| b.downcast_ref()) + .map(f) + }) + } + + /// Get the value set for this key. + /// + /// Panics if no mock data is set for the key. + fn get<F, R>(&self, f: F) -> R + where + F: FnOnce(&Self::Value) -> R, + { + match self.try_get(f) { + Some(v) => v, + None => panic!("mock data for {self:?} not set"), + } + } +} + +/// Mock data which can be shared amongst threads. +pub struct SharedMockData(AtomicPtr<MockDataMap>); + +impl Clone for SharedMockData { + fn clone(&self) -> Self { + SharedMockData(AtomicPtr::new(self.0.load(Relaxed))) + } +} + +impl SharedMockData { + /// Create a `SharedMockData` which stores the mock data from the current thread. + pub fn new() -> Self { + MOCK_DATA.with(|ptr| SharedMockData(AtomicPtr::new(ptr.load(Relaxed)))) + } + + /// Set the mock data on the current thread. + /// + /// # Safety + /// Callers must ensure that the mock data outlives the lifetime of the thread. + pub unsafe fn set(self) { + MOCK_DATA.with(|ptr| ptr.store(self.0.into_inner(), Relaxed)); + } +} + +/// Create a mock builder, which allows adding mock data and running functions under that mock +/// environment. +pub fn builder() -> Builder { + Builder::new() +} + +/// A mock data builder. +#[derive(Default)] +pub struct Builder { + data: MockDataMap, +} + +impl Builder { + /// Create a new, empty builder. + pub fn new() -> Self { + Default::default() + } + + /// Set a mock data key/value mapping. + pub fn set<K: MockKey>(&mut self, key: K, value: K::Value) -> &mut Self { + self.data.insert(Box::new(key), Box::new(value)); + self + } + + /// Run the given function with mock data set. + pub fn run<F, R>(&mut self, f: F) -> R + where + F: FnOnce() -> R, + { + MOCK_DATA.with(|ptr| ptr.store(&mut self.data, Relaxed)); + let ret = f(); + MOCK_DATA.with(|ptr| ptr.store(std::ptr::null_mut(), Relaxed)); + ret + } +} + +/// A general-purpose [`MockKey`] keyed by an identifier string and the stored type. +/// +/// Use [`hook`] or [`try_hook`] in code accessing the values. +pub struct MockHook<T> { + name: &'static str, + _p: std::marker::PhantomData<fn() -> T>, +} + +impl<T> std::fmt::Debug for MockHook<T> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.debug_struct(&format!("MockHook<{}>", std::any::type_name::<T>())) + .field("name", &self.name) + .finish() + } +} + +impl<T: 'static> MockKeyStored for MockHook<T> { + fn eq(&self, other: &dyn MockKeyStored) -> bool { + std::any::TypeId::of::<Self>() == other.type_id() + && self.name == other.downcast_ref::<Self>().unwrap().name + } + fn hash(&self, state: &mut DefaultHasher) { + self.name.hash(state) + } +} + +impl<T: Any + Send + Sync + 'static> MockKey for MockHook<T> { + type Value = T; +} + +impl<T> MockHook<T> { + /// Create a new mock hook key with the given name. + pub fn new(name: &'static str) -> Self { + MockHook { + name, + _p: Default::default(), + } + } +} + +/// Create a mock hook with the given name. When mocking isn't enabled, the given value will be +/// used instead. Panics if the hook isn't set. +pub fn hook<T: Any + Send + Sync + Clone>(_normally: T, name: &'static str) -> T { + MockHook::new(name).get(|v: &T| v.clone()) +} + +/// Create a mock hook with the given name. When mocking isn't enabled or the hook hasn't been set, +/// the given value will be used instead. +pub fn try_hook<T: Any + Send + Sync + Clone>(fallback: T, name: &'static str) -> T { + MockHook::new(name) + .try_get(|v: &T| v.clone()) + .unwrap_or(fallback) +} + +/// Create a mock key with an associated value type. +/// +/// Supports the following syntaxes: +/// * Unit struct: `<visibility> struct NAME => VALUE_TYPE` +/// * Tuple struct: `<visibility> struct NAME(ITEMS) => VALUE_TYPE` +/// * Normal struct: `<visibility> struct NAME { FIELDS } => VALUE_TYPE` +macro_rules! mock_key { + ( $vis:vis struct $name:ident => $value:ty ) => { + $crate::std::mock::mock_key! { @structdef[$vis struct $name;] $name $value } + }; + ( $vis:vis struct $name:ident ($($tuple:tt)*) => $value:ty ) => { + $crate::std::mock::mock_key! { @structdef[$vis struct $name($($tuple)*);] $name $value } + }; + ( $vis:vis struct $name:ident {$($full:tt)*} => $value:ty ) => { + $crate::std::mock::mock_key! { @structdef[$vis struct $name{$($full)*}] $name $value } + }; + ( @structdef [$($def:tt)+] $name:ident $value:ty ) => { + #[derive(Debug, PartialEq, Eq, Hash)] + $($def)+ + + impl crate::std::mock::MockKeyStored for $name { + fn eq(&self, other: &dyn crate::std::mock::MockKeyStored) -> bool { + std::any::TypeId::of::<Self>() == other.type_id() + && PartialEq::eq(self, other.downcast_ref::<Self>().unwrap()) + } + + fn hash(&self, state: &mut std::collections::hash_map::DefaultHasher) { + std::hash::Hash::hash(self, state) + } + } + + impl crate::std::mock::MockKey for $name { + type Value = $value; + } + } +} + +pub(crate) use mock_key; diff --git a/toolkit/crashreporter/client/app/src/std/mock_stub.rs b/toolkit/crashreporter/client/app/src/std/mock_stub.rs new file mode 100644 index 0000000000..a02ac3dea1 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/std/mock_stub.rs @@ -0,0 +1,20 @@ +/* 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/. */ +#![allow(dead_code)] + +//! Stubs used when mocking isn't enabled. + +/// Create a mock hook with the given name. When mocking isn't enabled, the given value will be +/// used instead. Panics if the hook isn't set. +#[inline(always)] +pub fn hook<T: std::any::Any + Send + Sync + Clone>(normally: T, _name: &'static str) -> T { + normally +} + +/// Create a mock hook with the given name. When mocking isn't enabled or the hook hasn't been set, +/// the given value will be used instead. +#[inline(always)] +pub fn try_hook<T: std::any::Any + Send + Sync + Clone>(fallback: T, _name: &'static str) -> T { + fallback +} diff --git a/toolkit/crashreporter/client/app/src/std/mod.rs b/toolkit/crashreporter/client/app/src/std/mod.rs new file mode 100644 index 0000000000..467d6b0c14 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/std/mod.rs @@ -0,0 +1,33 @@ +/* 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/. */ + +//! Standard library wrapper (for mocking in tests). +//! +//! In general this should always be used rather than `std` directly, and _especially_ when using +//! `std` functions and types which interact with the runtime host environment. +//! +//! Note that, in some cases, this wrapper extends the `std` library. Notably, the [`mock`] module +//! adds mocking functions. + +#![cfg_attr(mock, allow(unused))] + +pub use std::*; + +#[cfg_attr(not(mock), path = "mock_stub.rs")] +pub mod mock; + +#[cfg(mock)] +pub mod env; +#[cfg(mock)] +pub mod fs; +#[cfg(mock)] +pub mod net; +#[cfg(mock)] +pub mod path; +#[cfg(mock)] +pub mod process; +#[cfg(mock)] +pub mod thread; +#[cfg(mock)] +pub mod time; diff --git a/toolkit/crashreporter/client/app/src/std/net.rs b/toolkit/crashreporter/client/app/src/std/net.rs new file mode 100644 index 0000000000..dd51a44756 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/std/net.rs @@ -0,0 +1,5 @@ +/* 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/. */ + +//! Stub to avoid net use, if any. diff --git a/toolkit/crashreporter/client/app/src/std/path.rs b/toolkit/crashreporter/client/app/src/std/path.rs new file mode 100644 index 0000000000..c565615514 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/std/path.rs @@ -0,0 +1,157 @@ +/* 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/. */ + +//! We unfortunately have to mock `Path` because of `exists`, `try_exists`, and `metadata`. + +pub use std::path::*; + +use super::mock::MockKey; +use std::ffi::OsStr; + +macro_rules! delegate { + ( fn $name:ident (&self $(, $arg:ident : $argty:ty )* ) -> $ret:ty ) => { + pub fn $name (&self, $($arg : $ty)*) -> $ret { + self.0.$name($($arg),*) + } + } +} + +#[repr(transparent)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Path(std::path::Path); + +impl AsRef<std::path::Path> for Path { + fn as_ref(&self) -> &std::path::Path { + &self.0 + } +} + +impl AsRef<OsStr> for Path { + fn as_ref(&self) -> &OsStr { + self.0.as_ref() + } +} + +impl AsRef<Path> for &str { + fn as_ref(&self) -> &Path { + Path::from_path(self.as_ref()) + } +} + +impl AsRef<Path> for String { + fn as_ref(&self) -> &Path { + Path::from_path(self.as_ref()) + } +} + +impl AsRef<Path> for &OsStr { + fn as_ref(&self) -> &Path { + Path::from_path(self.as_ref()) + } +} + +impl Path { + fn from_path(path: &std::path::Path) -> &Self { + // # Safety + // Transparent wrapper is safe to transmute. + unsafe { std::mem::transmute(path) } + } + + pub fn exists(&self) -> bool { + super::fs::MockFS + .try_get(|files| { + files + .path(self, false, |item| match &item.content { + super::fs::MockFSContent::File(r) => r.is_ok(), + _ => true, + }) + .unwrap_or(false) + }) + .unwrap_or(false) + } + + pub fn read_dir(&self) -> super::io::Result<super::fs::ReadDir> { + super::fs::ReadDir::new(&self.0) + } + + delegate!(fn display(&self) -> Display); + delegate!(fn file_stem(&self) -> Option<&OsStr>); + delegate!(fn file_name(&self) -> Option<&OsStr>); + delegate!(fn extension(&self) -> Option<&OsStr>); + + pub fn join<P: AsRef<Path>>(&self, path: P) -> PathBuf { + PathBuf(self.0.join(&path.as_ref().0)) + } + + pub fn parent(&self) -> Option<&Path> { + self.0.parent().map(Path::from_path) + } +} + +#[repr(transparent)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PathBuf(pub(super) std::path::PathBuf); + +impl PathBuf { + pub fn set_extension<S: AsRef<OsStr>>(&mut self, extension: S) -> bool { + self.0.set_extension(extension) + } + + pub fn push<P: AsRef<Path>>(&mut self, path: P) { + self.0.push(path.as_ref()) + } + + pub fn pop(&mut self) -> bool { + self.0.pop() + } +} + +impl std::ops::Deref for PathBuf { + type Target = Path; + fn deref(&self) -> &Self::Target { + Path::from_path(self.0.as_ref()) + } +} + +impl AsRef<Path> for PathBuf { + fn as_ref(&self) -> &Path { + Path::from_path(self.0.as_ref()) + } +} + +impl AsRef<std::path::Path> for PathBuf { + fn as_ref(&self) -> &std::path::Path { + self.0.as_ref() + } +} + +impl AsRef<OsStr> for PathBuf { + fn as_ref(&self) -> &OsStr { + self.0.as_ref() + } +} + +impl From<std::ffi::OsString> for PathBuf { + fn from(os_str: std::ffi::OsString) -> Self { + PathBuf(os_str.into()) + } +} + +impl From<std::path::PathBuf> for PathBuf { + fn from(pathbuf: std::path::PathBuf) -> Self { + PathBuf(pathbuf) + } +} + +impl From<PathBuf> for std::ffi::OsString { + fn from(pathbuf: PathBuf) -> Self { + pathbuf.0.into() + } +} + +impl From<&str> for PathBuf { + fn from(s: &str) -> Self { + PathBuf(s.into()) + } +} diff --git a/toolkit/crashreporter/client/app/src/std/process.rs b/toolkit/crashreporter/client/app/src/std/process.rs new file mode 100644 index 0000000000..49dd028447 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/std/process.rs @@ -0,0 +1,201 @@ +/* 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/. */ + +pub use std::process::*; + +use crate::std::mock::{mock_key, MockKey}; + +use std::ffi::{OsStr, OsString}; +use std::io::Result; +use std::sync::{Arc, Mutex}; + +mock_key! { + // Uses PathBuf rather than OsString to avoid path separator differences. + pub struct MockCommand(::std::path::PathBuf) => Box<dyn Fn(&Command) -> Result<Output> + Send + Sync> +} + +#[derive(Debug)] +pub struct Command { + pub program: OsString, + pub args: Vec<OsString>, + pub env: std::collections::HashMap<OsString, OsString>, + pub stdin: Vec<u8>, + // XXX The spawn stuff is hacky, but for now there's only one case where we really need to + // interact with `spawn` so we live with it for testing. + pub spawning: bool, + pub spawned_child: Mutex<Option<::std::process::Child>>, +} + +impl Command { + pub fn mock<S: AsRef<OsStr>>(program: S) -> MockCommand { + MockCommand(program.as_ref().into()) + } + + pub fn new<S: AsRef<OsStr>>(program: S) -> Self { + Command { + program: program.as_ref().into(), + args: vec![], + env: Default::default(), + stdin: Default::default(), + spawning: false, + spawned_child: Mutex::new(None), + } + } + + pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self { + self.args.push(arg.as_ref().into()); + self + } + + pub fn args<I, S>(&mut self, args: I) -> &mut Self + where + I: IntoIterator<Item = S>, + S: AsRef<OsStr>, + { + for arg in args.into_iter() { + self.arg(arg); + } + self + } + + pub fn env<K, V>(&mut self, key: K, val: V) -> &mut Self + where + K: AsRef<OsStr>, + V: AsRef<OsStr>, + { + self.env.insert(key.as_ref().into(), val.as_ref().into()); + self + } + + pub fn stdin<T: Into<Stdio>>(&mut self, _cfg: T) -> &mut Self { + self + } + + pub fn stdout<T: Into<Stdio>>(&mut self, _cfg: T) -> &mut Self { + self + } + + pub fn stderr<T: Into<Stdio>>(&mut self, _cfg: T) -> &mut Self { + self + } + + pub fn output(&mut self) -> std::io::Result<Output> { + MockCommand(self.program.as_os_str().into()).get(|f| f(self)) + } + + pub fn spawn(&mut self) -> std::io::Result<Child> { + self.spawning = true; + self.output()?; + self.spawning = false; + let stdin = Arc::new(Mutex::new(vec![])); + Ok(Child { + stdin: Some(ChildStdin { + data: stdin.clone(), + }), + cmd: self.clone_for_child(), + stdin_data: Some(stdin), + }) + } + + #[cfg(windows)] + pub fn creation_flags(&mut self, _flags: u32) -> &mut Self { + self + } + + pub fn output_from_real_command(&self) -> std::io::Result<Output> { + let mut spawned_child = self.spawned_child.lock().unwrap(); + if spawned_child.is_none() { + *spawned_child = Some( + ::std::process::Command::new(self.program.clone()) + .args(self.args.clone()) + .envs(self.env.clone()) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?, + ); + } + + if self.spawning { + return Ok(success_output()); + } + + let mut child = spawned_child.take().unwrap(); + { + let mut input = child.stdin.take().unwrap(); + std::io::copy(&mut std::io::Cursor::new(&self.stdin), &mut input)?; + } + child.wait_with_output() + } + + fn clone_for_child(&self) -> Self { + Command { + program: self.program.clone(), + args: self.args.clone(), + env: self.env.clone(), + stdin: self.stdin.clone(), + spawning: false, + spawned_child: Mutex::new(self.spawned_child.lock().unwrap().take()), + } + } +} + +pub struct Child { + pub stdin: Option<ChildStdin>, + cmd: Command, + stdin_data: Option<Arc<Mutex<Vec<u8>>>>, +} + +impl Child { + pub fn wait_with_output(mut self) -> std::io::Result<Output> { + self.ref_wait_with_output().unwrap() + } + + fn ref_wait_with_output(&mut self) -> Option<std::io::Result<Output>> { + drop(self.stdin.take()); + if let Some(stdin) = self.stdin_data.take() { + self.cmd.stdin = Arc::try_unwrap(stdin) + .expect("stdin not dropped, wait_with_output may block") + .into_inner() + .unwrap(); + Some(MockCommand(self.cmd.program.as_os_str().into()).get(|f| f(&self.cmd))) + } else { + None + } + } +} + +pub struct ChildStdin { + data: Arc<Mutex<Vec<u8>>>, +} + +impl std::io::Write for ChildStdin { + fn write(&mut self, buf: &[u8]) -> Result<usize> { + self.data.lock().unwrap().write(buf) + } + + fn flush(&mut self) -> Result<()> { + Ok(()) + } +} + +#[cfg(unix)] +pub fn success_exit_status() -> ExitStatus { + use std::os::unix::process::ExitStatusExt; + ExitStatus::from_raw(0) +} + +#[cfg(windows)] +pub fn success_exit_status() -> ExitStatus { + use std::os::windows::process::ExitStatusExt; + ExitStatus::from_raw(0) +} + +pub fn success_output() -> Output { + Output { + status: success_exit_status(), + stdout: vec![], + stderr: vec![], + } +} diff --git a/toolkit/crashreporter/client/app/src/std/thread.rs b/toolkit/crashreporter/client/app/src/std/thread.rs new file mode 100644 index 0000000000..d2dc74702a --- /dev/null +++ b/toolkit/crashreporter/client/app/src/std/thread.rs @@ -0,0 +1,45 @@ +/* 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/. */ + +pub use std::thread::*; + +// Mock `spawn` just to issue a warning that mocking within the thread won't work without manual +// intervention. +pub fn spawn<F, T>(f: F) -> JoinHandle<T> +where + F: FnOnce() -> T + Send + 'static, + T: Send + 'static, +{ + eprintln!("warning: mocking won't work in `std::thread::spawn`ed threads by default. Use `std::mock::SharedMockData` if mocking is needed and it's safe to do so."); + std::thread::spawn(f) +} + +pub struct Scope<'scope, 'env: 'scope> { + mock_data: super::mock::SharedMockData, + scope: &'scope std::thread::Scope<'scope, 'env>, +} + +impl<'scope, 'env> Scope<'scope, 'env> { + pub fn spawn<F, T>(&self, f: F) -> ScopedJoinHandle<'scope, T> + where + F: FnOnce() -> T + Send + 'scope, + T: Send + 'scope, + { + let mock_data = self.mock_data.clone(); + self.scope.spawn(move || { + // # Safety + // `thread::scope` guarantees that the mock data will outlive the thread. + unsafe { mock_data.set() }; + f() + }) + } +} + +pub fn scope<'env, F, T>(f: F) -> T +where + F: for<'scope> FnOnce(Scope<'scope, 'env>) -> T, +{ + let mock_data = super::mock::SharedMockData::new(); + std::thread::scope(|scope| f(Scope { mock_data, scope })) +} diff --git a/toolkit/crashreporter/client/app/src/std/time.rs b/toolkit/crashreporter/client/app/src/std/time.rs new file mode 100644 index 0000000000..5c351a7bcf --- /dev/null +++ b/toolkit/crashreporter/client/app/src/std/time.rs @@ -0,0 +1,32 @@ +/* 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/. */ + +use super::mock::{mock_key, MockKey}; +pub use std::time::{Duration, SystemTimeError}; + +mock_key! { + pub struct MockCurrentTime => std::time::SystemTime +} + +#[repr(transparent)] +#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)] +pub struct SystemTime(pub(super) std::time::SystemTime); + +impl From<SystemTime> for ::time::OffsetDateTime { + fn from(t: SystemTime) -> Self { + t.0.into() + } +} + +impl SystemTime { + pub const UNIX_EPOCH: SystemTime = SystemTime(std::time::SystemTime::UNIX_EPOCH); + + pub fn now() -> Self { + MockCurrentTime.get(|t| SystemTime(*t)) + } + + pub fn duration_since(&self, earlier: Self) -> Result<Duration, SystemTimeError> { + self.0.duration_since(earlier.0) + } +} diff --git a/toolkit/crashreporter/client/app/src/test.rs b/toolkit/crashreporter/client/app/src/test.rs new file mode 100644 index 0000000000..42d2334bca --- /dev/null +++ b/toolkit/crashreporter/client/app/src/test.rs @@ -0,0 +1,1289 @@ +/* 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/. */ + +//! Tests here mostly interact with the [test UI](crate::ui::test). As such, most tests read a bit +//! more like integration tests than unit tests, testing the behavior of the application as a +//! whole. + +use super::*; +use crate::config::{test::MINIDUMP_PRUNE_SAVE_COUNT, Config}; +use crate::settings::Settings; +use crate::std::{ + ffi::OsString, + fs::{MockFS, MockFiles}, + io::ErrorKind, + mock, + process::Command, + sync::{ + atomic::{AtomicUsize, Ordering::Relaxed}, + Arc, + }, +}; +use crate::ui::{self, test::model, ui_impl::Interact}; + +/// A simple thread-safe counter which can be used in tests to mark that certain code paths were +/// hit. +#[derive(Clone, Default)] +struct Counter(Arc<AtomicUsize>); + +impl Counter { + /// Create a new zero counter. + pub fn new() -> Self { + Self::default() + } + + /// Increment the counter. + pub fn inc(&self) { + self.0.fetch_add(1, Relaxed); + } + + /// Get the current count. + pub fn count(&self) -> usize { + self.0.load(Relaxed) + } + + /// Assert that the current count is 1. + pub fn assert_one(&self) { + assert_eq!(self.count(), 1); + } +} + +/// Fluent wraps arguments with the unicode BiDi characters. +struct FluentArg<T>(T); + +impl<T: std::fmt::Display> std::fmt::Display for FluentArg<T> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + use crate::std::fmt::Write; + f.write_char('\u{2068}')?; + self.0.fmt(f)?; + f.write_char('\u{2069}') + } +} + +/// Run a gui and interaction on separate threads. +fn gui_interact<G, I, R>(gui: G, interact: I) -> R +where + G: FnOnce() -> R, + I: FnOnce(Interact) + Send + 'static, +{ + let i = Interact::hook(); + let handle = { + let i = i.clone(); + ::std::thread::spawn(move || { + i.wait_for_ready(); + interact(i); + }) + }; + let ret = gui(); + // In case the gui failed before launching. + i.cancel(); + handle.join().unwrap(); + ret +} + +const MOCK_MINIDUMP_EXTRA: &str = r#"{ + "Vendor": "FooCorp", + "ProductName": "Bar", + "ReleaseChannel": "release", + "BuildID": "1234", + "StackTraces": { + "status": "OK" + }, + "Version": "100.0", + "ServerURL": "https://reports.example.com", + "TelemetryServerURL": "https://telemetry.example.com", + "TelemetryClientId": "telemetry_client", + "TelemetrySessionId": "telemetry_session", + "SomeNestedJson": { "foo": "bar" }, + "URL": "https://url.example.com" + }"#; + +// Actual content doesn't matter, aside from the hash that is generated. +const MOCK_MINIDUMP_FILE: &[u8] = &[1, 2, 3, 4]; +const MOCK_MINIDUMP_SHA256: &str = + "9f64a747e1b97f131fabb6b447296c9b6f0201e79fb3c5356e6c77e89b6a806a"; +macro_rules! current_date { + () => { + "2004-11-09" + }; +} +const MOCK_CURRENT_DATE: &str = current_date!(); +const MOCK_CURRENT_TIME: &str = concat!(current_date!(), "T12:34:56Z"); +const MOCK_PING_UUID: uuid::Uuid = uuid::Uuid::nil(); +const MOCK_REMOTE_CRASH_ID: &str = "8cbb847c-def2-4f68-be9e-000000000000"; + +fn current_datetime() -> time::OffsetDateTime { + time::OffsetDateTime::parse( + MOCK_CURRENT_TIME, + &time::format_description::well_known::Rfc3339, + ) + .unwrap() +} + +fn current_unix_time() -> i64 { + current_datetime().unix_timestamp() +} + +fn current_system_time() -> ::std::time::SystemTime { + current_datetime().into() +} + +/// A basic configuration which populates some necessary/useful fields. +fn test_config() -> Config { + let mut cfg = Config::default(); + cfg.data_dir = Some("data_dir".into()); + cfg.events_dir = Some("events_dir".into()); + cfg.ping_dir = Some("ping_dir".into()); + cfg.dump_file = Some("minidump.dmp".into()); + cfg.strings = lang::LanguageInfo::default().load_strings().ok(); + cfg +} + +/// A test fixture to make configuration, mocking, and assertions easier. +struct GuiTest { + /// The configuration used in the test. Initialized to [`test_config`]. + pub config: Config, + /// The mock builder used in the test, initialized with a basic set of mocked values to ensure + /// most things will work out of the box. + pub mock: mock::Builder, + /// The mocked filesystem, which can be used for mock setup and assertions after completion. + pub files: MockFiles, +} + +impl GuiTest { + /// Create a new GuiTest with enough configured for the application to run + pub fn new() -> Self { + // Create a default set of files which allow successful operation. + let mock_files = MockFiles::new(); + mock_files + .add_file_result( + "minidump.dmp", + Ok(MOCK_MINIDUMP_FILE.into()), + current_system_time(), + ) + .add_file_result( + "minidump.extra", + Ok(MOCK_MINIDUMP_EXTRA.into()), + current_system_time(), + ); + + // Create a default mock environment which allows successful operation. + let mut mock = mock::builder(); + mock.set( + Command::mock("work_dir/minidump-analyzer"), + Box::new(|_| Ok(crate::std::process::success_output())), + ) + .set( + Command::mock("work_dir/pingsender"), + Box::new(|_| Ok(crate::std::process::success_output())), + ) + .set( + Command::mock("curl"), + Box::new(|_| { + let mut output = crate::std::process::success_output(); + output.stdout = format!("CrashID={MOCK_REMOTE_CRASH_ID}").into(); + Ok(output) + }), + ) + .set(MockFS, mock_files.clone()) + .set( + crate::std::env::MockCurrentExe, + "work_dir/crashreporter".into(), + ) + .set(crate::std::time::MockCurrentTime, current_system_time()) + .set(mock::MockHook::new("ping_uuid"), MOCK_PING_UUID); + + GuiTest { + config: test_config(), + mock, + files: mock_files, + } + } + + /// Run the test as configured, using the given function to interact with the GUI. + /// + /// Returns the final result of the application logic. + pub fn try_run<F: FnOnce(Interact) + Send + 'static>( + &mut self, + interact: F, + ) -> anyhow::Result<bool> { + let GuiTest { + ref mut config, + ref mut mock, + .. + } = self; + let mut config = Arc::new(std::mem::take(config)); + + // Run the mock environment. + mock.run(move || gui_interact(move || try_run(&mut config), interact)) + } + + /// Run the test as configured, using the given function to interact with the GUI. + /// + /// Panics if the application logic returns an error (which would normally be displayed to the + /// user). + pub fn run<F: FnOnce(Interact) + Send + 'static>(&mut self, interact: F) { + if let Err(e) = self.try_run(interact) { + panic!( + "gui failure:{}", + e.chain().map(|e| format!("\n {e}")).collect::<String>() + ); + } + } + + /// Get the file assertion helper. + pub fn assert_files(&self) -> AssertFiles { + AssertFiles { + data_dir: "data_dir".into(), + events_dir: "events_dir".into(), + inner: self.files.assert_files(), + } + } +} + +/// A wrapper around the mock [`AssertFiles`](crate::std::fs::AssertFiles). +/// +/// This implements higher-level assertions common across tests, but also supports the lower-level +/// assertions (though those return the [`AssertFiles`](crate::std::fs::AssertFiles) reference so +/// higher-level assertions must be chained first). +struct AssertFiles { + data_dir: String, + events_dir: String, + inner: std::fs::AssertFiles, +} + +impl AssertFiles { + fn data(&self, rest: &str) -> String { + format!("{}/{rest}", &self.data_dir) + } + + fn events(&self, rest: &str) -> String { + format!("{}/{rest}", &self.events_dir) + } + + /// Set the data dir if not the default. + pub fn set_data_dir<S: ToString>(&mut self, data_dir: S) -> &mut Self { + let data_dir = data_dir.to_string(); + // Data dir should be relative to root. + self.data_dir = data_dir.trim_start_matches('/').to_string(); + self + } + + /// Ignore the generated log file. + pub fn ignore_log(&mut self) -> &mut Self { + self.inner.ignore(self.data("submit.log")); + self + } + + /// Assert that the crash report was submitted according to the filesystem. + pub fn submitted(&mut self) -> &mut Self { + self.inner.check( + self.data(&format!("submitted/{MOCK_REMOTE_CRASH_ID}.txt")), + format!("Crash ID: {}\n", FluentArg(MOCK_REMOTE_CRASH_ID)), + ); + self + } + + /// Assert that the given settings where saved. + pub fn saved_settings(&mut self, settings: Settings) -> &mut Self { + self.inner.check( + self.data("crashreporter_settings.json"), + settings.to_string(), + ); + self + } + + /// Assert that a crash is pending according to the filesystem. + pub fn pending(&mut self) -> &mut Self { + let dmp = self.data("pending/minidump.dmp"); + self.inner + .check(self.data("pending/minidump.extra"), MOCK_MINIDUMP_EXTRA) + .check_bytes(dmp, MOCK_MINIDUMP_FILE); + self + } + + /// Assert that a crash ping was sent according to the filesystem. + pub fn ping(&mut self) -> &mut Self { + self.inner.check( + format!("ping_dir/{MOCK_PING_UUID}.json"), + serde_json::json! {{ + "type": "crash", + "id": MOCK_PING_UUID, + "version": 4, + "creation_date": MOCK_CURRENT_TIME, + "client_id": "telemetry_client", + "payload": { + "sessionId": "telemetry_session", + "version": 1, + "crashDate": MOCK_CURRENT_DATE, + "crashTime": MOCK_CURRENT_TIME, + "hasCrashEnvironment": true, + "crashId": "minidump", + "minidumpSha256Hash": MOCK_MINIDUMP_SHA256, + "processType": "main", + "stackTraces": { + "status": "OK" + }, + "metadata": { + "BuildID": "1234", + "ProductName": "Bar", + "ReleaseChannel": "release", + } + }, + "application": { + "vendor": "FooCorp", + "name": "Bar", + "buildId": "1234", + "displayVersion": "", + "platformVersion": "", + "version": "100.0", + "channel": "release" + } + }} + .to_string(), + ); + self + } + + /// Assert that a crash submission event was written with the given submission status. + pub fn submission_event(&mut self, success: bool) -> &mut Self { + self.inner.check( + self.events("minidump-submission"), + format!( + "crash.submission.1\n\ + {}\n\ + minidump\n\ + {success}\n\ + {}", + current_unix_time(), + if success { MOCK_REMOTE_CRASH_ID } else { "" } + ), + ); + self + } +} + +impl std::ops::Deref for AssertFiles { + type Target = std::fs::AssertFiles; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl std::ops::DerefMut for AssertFiles { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +#[test] +fn error_dialog() { + gui_interact( + || { + let cfg = Config::default(); + ui::error_dialog(&cfg, "an error occurred") + }, + |interact| { + interact.element("close", |_style, b: &model::Button| b.click.fire(&())); + }, + ); +} + +#[test] +fn no_dump_file() { + let mut cfg = Arc::new(Config::default()); + { + let cfg = Arc::get_mut(&mut cfg).unwrap(); + cfg.strings = lang::LanguageInfo::default().load_strings().ok(); + } + assert!(try_run(&mut cfg).is_err()); + Arc::get_mut(&mut cfg).unwrap().auto_submit = true; + assert!(try_run(&mut cfg).is_ok()); +} + +#[test] +fn minidump_analyzer_error() { + mock::builder() + .set( + Command::mock("work_dir/minidump-analyzer"), + Box::new(|_| Err(ErrorKind::NotFound.into())), + ) + .set( + crate::std::env::MockCurrentExe, + "work_dir/crashreporter".into(), + ) + .run(|| { + let cfg = test_config(); + assert!(try_run(&mut Arc::new(cfg)).is_err()); + }); +} + +#[test] +fn no_extra_file() { + mock::builder() + .set( + Command::mock("work_dir/minidump-analyzer"), + Box::new(|_| Ok(crate::std::process::success_output())), + ) + .set( + crate::std::env::MockCurrentExe, + "work_dir/crashreporter".into(), + ) + .set(MockFS, { + let files = MockFiles::new(); + files.add_file_result( + "minidump.extra", + Err(ErrorKind::NotFound.into()), + ::std::time::SystemTime::UNIX_EPOCH, + ); + files + }) + .run(|| { + let cfg = test_config(); + assert!(try_run(&mut Arc::new(cfg)).is_err()); + }); +} + +#[test] +fn auto_submit() { + let mut test = GuiTest::new(); + test.config.auto_submit = true; + // auto_submit should not do any GUI things, including creating the crashreporter_settings.json + // file. + test.mock.run(|| { + assert!(try_run(&mut Arc::new(std::mem::take(&mut test.config))).is_ok()); + }); + test.assert_files().ignore_log().submitted().pending(); +} + +#[test] +fn restart() { + let mut test = GuiTest::new(); + test.config.restart_command = Some("my_process".into()); + test.config.restart_args = vec!["a".into(), "b".into()]; + let ran_process = Counter::new(); + let mock_ran_process = ran_process.clone(); + test.mock.set( + Command::mock("my_process"), + Box::new(move |cmd| { + assert_eq!(cmd.args, &["a", "b"]); + mock_ran_process.inc(); + Ok(crate::std::process::success_output()) + }), + ); + test.run(|interact| { + interact.element("restart", |_style, b: &model::Button| b.click.fire(&())); + }); + test.assert_files() + .ignore_log() + .saved_settings(Settings::default()) + .submitted() + .pending(); + ran_process.assert_one(); +} + +#[test] +fn no_restart_with_windows_error_reporting() { + let mut test = GuiTest::new(); + test.config.restart_command = Some("my_process".into()); + test.config.restart_args = vec!["a".into(), "b".into()]; + // Add the "WindowsErrorReporting" key to the extra file + const MINIDUMP_EXTRA_CONTENTS: &str = r#"{ + "Vendor": "FooCorp", + "ProductName": "Bar", + "ReleaseChannel": "release", + "BuildID": "1234", + "StackTraces": { + "status": "OK" + }, + "Version": "100.0", + "ServerURL": "https://reports.example.com", + "TelemetryServerURL": "https://telemetry.example.com", + "TelemetryClientId": "telemetry_client", + "TelemetrySessionId": "telemetry_session", + "SomeNestedJson": { "foo": "bar" }, + "URL": "https://url.example.com", + "WindowsErrorReporting": 1 + }"#; + test.files = { + let mock_files = MockFiles::new(); + mock_files + .add_file_result( + "minidump.dmp", + Ok(MOCK_MINIDUMP_FILE.into()), + current_system_time(), + ) + .add_file_result( + "minidump.extra", + Ok(MINIDUMP_EXTRA_CONTENTS.into()), + current_system_time(), + ); + test.mock.set(MockFS, mock_files.clone()); + mock_files + }; + let ran_process = Counter::new(); + let mock_ran_process = ran_process.clone(); + test.mock.set( + Command::mock("my_process"), + Box::new(move |cmd| { + assert_eq!(cmd.args, &["a", "b"]); + mock_ran_process.inc(); + Ok(crate::std::process::success_output()) + }), + ); + test.run(|interact| { + interact.element("restart", |style, b: &model::Button| { + // Check that the button is hidden, and invoke the click anyway to ensure the process + // isn't restarted (the window will still be closed). + assert_eq!(style.visible.get(), false); + b.click.fire(&()) + }); + }); + let mut assert_files = test.assert_files(); + assert_files + .ignore_log() + .saved_settings(Settings::default()) + .submitted(); + { + let dmp = assert_files.data("pending/minidump.dmp"); + let extra = assert_files.data("pending/minidump.extra"); + assert_files + .check(extra, MINIDUMP_EXTRA_CONTENTS) + .check_bytes(dmp, MOCK_MINIDUMP_FILE); + } + + assert_eq!(ran_process.count(), 0); +} + +#[test] +fn quit() { + let mut test = GuiTest::new(); + test.run(|interact| { + interact.element("quit", |_style, b: &model::Button| b.click.fire(&())); + }); + test.assert_files() + .ignore_log() + .saved_settings(Settings::default()) + .submitted() + .pending(); +} + +#[test] +fn delete_dump() { + let mut test = GuiTest::new(); + test.config.delete_dump = true; + test.run(|interact| { + interact.element("quit", |_style, b: &model::Button| b.click.fire(&())); + }); + test.assert_files() + .ignore_log() + .saved_settings(Settings::default()) + .submitted(); +} + +#[test] +fn no_submit() { + let mut test = GuiTest::new(); + test.files.add_dir("data_dir").add_file( + "data_dir/crashreporter_settings.json", + Settings { + submit_report: true, + include_url: true, + } + .to_string(), + ); + test.run(|interact| { + interact.element("send", |_style, c: &model::Checkbox| { + assert!(c.checked.get()) + }); + interact.element("include-url", |_style, c: &model::Checkbox| { + assert!(c.checked.get()) + }); + interact.element("send", |_style, c: &model::Checkbox| c.checked.set(false)); + interact.element("include-url", |_style, c: &model::Checkbox| { + c.checked.set(false) + }); + + // When submission is unchecked, the following elements should be disabled. + interact.element("details", |style, _: &model::Button| { + assert!(!style.enabled.get()); + }); + interact.element("comment", |style, _: &model::TextBox| { + assert!(!style.enabled.get()); + }); + interact.element("include-url", |style, _: &model::Checkbox| { + assert!(!style.enabled.get()); + }); + + interact.element("quit", |_style, b: &model::Button| b.click.fire(&())); + }); + test.assert_files() + .ignore_log() + .saved_settings(Settings { + submit_report: false, + include_url: false, + }) + .pending(); +} + +#[test] +fn ping_and_event_files() { + let mut test = GuiTest::new(); + test.files + .add_dir("ping_dir") + .add_dir("events_dir") + .add_file( + "events_dir/minidump", + "1\n\ + 12:34:56\n\ + e0423878-8d59-4452-b82e-cad9c846836e\n\ + {\"foo\":\"bar\"}", + ); + test.run(|interact| { + interact.element("quit", |_style, b: &model::Button| b.click.fire(&())); + }); + test.assert_files() + .ignore_log() + .saved_settings(Settings::default()) + .submitted() + .pending() + .submission_event(true) + .ping() + .check( + "events_dir/minidump", + format!( + "1\n\ + 12:34:56\n\ + e0423878-8d59-4452-b82e-cad9c846836e\n\ + {}", + serde_json::json! {{ + "foo": "bar", + "MinidumpSha256Hash": MOCK_MINIDUMP_SHA256, + "CrashPingUUID": MOCK_PING_UUID, + "StackTraces": { "status": "OK" } + }} + ), + ); +} + +#[test] +fn pingsender_failure() { + let mut test = GuiTest::new(); + test.mock.set( + Command::mock("work_dir/pingsender"), + Box::new(|_| Err(ErrorKind::NotFound.into())), + ); + test.files + .add_dir("ping_dir") + .add_dir("events_dir") + .add_file( + "events_dir/minidump", + "1\n\ + 12:34:56\n\ + e0423878-8d59-4452-b82e-cad9c846836e\n\ + {\"foo\":\"bar\"}", + ); + test.run(|interact| { + interact.element("quit", |_style, b: &model::Button| b.click.fire(&())); + }); + test.assert_files() + .ignore_log() + .saved_settings(Settings::default()) + .submitted() + .pending() + .submission_event(true) + .ping() + .check( + "events_dir/minidump", + format!( + "1\n\ + 12:34:56\n\ + e0423878-8d59-4452-b82e-cad9c846836e\n\ + {}", + serde_json::json! {{ + "foo": "bar", + "MinidumpSha256Hash": MOCK_MINIDUMP_SHA256, + // No crash ping UUID since pingsender fails + "StackTraces": { "status": "OK" } + }} + ), + ); +} + +#[test] +fn eol_version() { + let mut test = GuiTest::new(); + test.files + .add_dir("data_dir") + .add_file("data_dir/EndOfLife100.0", ""); + // Should fail before opening the gui + let result = test.try_run(|_| ()); + assert_eq!( + result.expect_err("should fail on EOL version").to_string(), + "Version end of life: crash reports are no longer accepted." + ); + test.assert_files() + .ignore_log() + .pending() + .ignore("data_dir/EndOfLife100.0"); +} + +#[test] +fn details_window() { + let mut test = GuiTest::new(); + test.run(|interact| { + let details_visible = || { + interact.window("crash-details-window", |style, _w: &model::Window| { + style.visible.get() + }) + }; + assert_eq!(details_visible(), false); + interact.element("details", |_style, b: &model::Button| b.click.fire(&())); + assert_eq!(details_visible(), true); + let details_text = loop { + let v = interact.element("details-text", |_style, t: &model::TextBox| t.content.get()); + if v == "Loading…" { + // Wait for the details to be populated. + std::thread::sleep(std::time::Duration::from_millis(50)); + continue; + } else { + break v; + } + }; + interact.element("close-details", |_style, b: &model::Button| b.click.fire(&())); + assert_eq!(details_visible(), false); + interact.element("quit", |_style, b: &model::Button| b.click.fire(&())); + assert_eq!(details_text, + "BuildID: 1234\n\ + ProductName: Bar\n\ + ReleaseChannel: release\n\ + SomeNestedJson: {\"foo\":\"bar\"}\n\ + SubmittedFrom: Client\n\ + TelemetryClientId: telemetry_client\n\ + TelemetryServerURL: https://telemetry.example.com\n\ + TelemetrySessionId: telemetry_session\n\ + Throttleable: 1\n\ + Vendor: FooCorp\n\ + Version: 100.0\n\ + This report also contains technical information about the state of the application when it crashed.\n" + ); + }); +} + +#[test] +fn data_dir_default() { + let mut test = GuiTest::new(); + test.config.data_dir = None; + test.run(|interact| { + interact.element("quit", |_style, b: &model::Button| b.click.fire(&())); + }); + test.assert_files() + .set_data_dir("data_dir/FooCorp/Bar/Crash Reports") + .ignore_log() + .saved_settings(Settings::default()) + .submitted() + .pending(); +} + +#[test] +fn include_url() { + for setting in [false, true] { + let mut test = GuiTest::new(); + test.files.add_dir("data_dir").add_file( + "data_dir/crashreporter_settings.json", + Settings { + submit_report: true, + include_url: setting, + } + .to_string(), + ); + test.mock + .set( + Command::mock("curl"), + Box::new(|_| Err(std::io::ErrorKind::NotFound.into())), + ) + .set( + net::report::MockLibCurl, + Box::new(move |report| { + assert_eq!( + report.extra.get("URL").and_then(|v| v.as_str()), + setting.then_some("https://url.example.com") + ); + Ok(Ok(format!("CrashID={MOCK_REMOTE_CRASH_ID}"))) + }), + ); + test.run(|interact| { + interact.element("quit", |_style, b: &model::Button| b.click.fire(&())); + }); + } +} + +#[test] +fn comment() { + const COMMENT: &str = "My program crashed"; + + for set_comment in [false, true] { + let invoked = Counter::new(); + let mock_invoked = invoked.clone(); + let mut test = GuiTest::new(); + test.mock + .set( + Command::mock("curl"), + Box::new(|_| Err(std::io::ErrorKind::NotFound.into())), + ) + .set( + net::report::MockLibCurl, + Box::new(move |report| { + mock_invoked.inc(); + assert_eq!( + report.extra.get("Comments").and_then(|v| v.as_str()), + set_comment.then_some(COMMENT) + ); + Ok(Ok(format!("CrashID={MOCK_REMOTE_CRASH_ID}"))) + }), + ); + test.run(move |interact| { + if set_comment { + interact.element("comment", |_style, c: &model::TextBox| { + c.content.set(COMMENT.into()) + }); + } + interact.element("quit", |_style, b: &model::Button| b.click.fire(&())); + }); + + invoked.assert_one(); + } +} + +#[test] +fn curl_binary() { + let mut test = GuiTest::new(); + test.files.add_file("minidump.memory.json.gz", ""); + let ran_process = Counter::new(); + let mock_ran_process = ran_process.clone(); + test.mock.set( + Command::mock("curl"), + Box::new(move |cmd| { + if cmd.spawning { + return Ok(crate::std::process::success_output()); + } + + // Curl strings need backslashes escaped. + let curl_escaped_separator = if std::path::MAIN_SEPARATOR == '\\' { + "\\\\" + } else { + std::path::MAIN_SEPARATOR_STR + }; + + let expected_args: Vec<OsString> = [ + "--user-agent", + net::report::USER_AGENT, + "--form", + "extra=@-;filename=extra.json;type=application/json", + "--form", + &format!( + "upload_file_minidump=@\"data_dir{0}pending{0}minidump.dmp\"", + curl_escaped_separator + ), + "--form", + &format!( + "memory_report=@\"data_dir{0}pending{0}minidump.memory.json.gz\"", + curl_escaped_separator + ), + "https://reports.example.com", + ] + .into_iter() + .map(Into::into) + .collect(); + assert_eq!(cmd.args, expected_args); + let mut output = crate::std::process::success_output(); + output.stdout = format!("CrashID={MOCK_REMOTE_CRASH_ID}").into(); + mock_ran_process.inc(); + Ok(output) + }), + ); + test.run(|interact| { + interact.element("quit", |_style, b: &model::Button| b.click.fire(&())); + }); + + ran_process.assert_one(); +} + +#[test] +fn curl_library() { + let invoked = Counter::new(); + let mock_invoked = invoked.clone(); + let mut test = GuiTest::new(); + test.mock + .set( + Command::mock("curl"), + Box::new(|_| Err(std::io::ErrorKind::NotFound.into())), + ) + .set( + net::report::MockLibCurl, + Box::new(move |_| { + mock_invoked.inc(); + Ok(Ok(format!("CrashID={MOCK_REMOTE_CRASH_ID}"))) + }), + ); + test.run(|interact| { + interact.element("quit", |_style, b: &model::Button| b.click.fire(&())); + }); + invoked.assert_one(); +} + +#[test] +fn report_not_sent() { + let mut test = GuiTest::new(); + test.files.add_dir("events_dir"); + test.mock + .set( + Command::mock("curl"), + Box::new(|_| Err(std::io::ErrorKind::NotFound.into())), + ) + .set( + net::report::MockLibCurl, + Box::new(move |_| Err(std::io::ErrorKind::NotFound.into())), + ); + test.run(|interact| { + interact.element("quit", |_style, b: &model::Button| b.click.fire(&())); + }); + + test.assert_files() + .ignore_log() + .saved_settings(Settings::default()) + .submission_event(false) + .pending(); +} + +#[test] +fn report_response_failed() { + let mut test = GuiTest::new(); + test.files.add_dir("events_dir"); + test.mock + .set( + Command::mock("curl"), + Box::new(|_| Err(std::io::ErrorKind::NotFound.into())), + ) + .set( + net::report::MockLibCurl, + Box::new(move |_| Ok(Err(std::io::ErrorKind::NotFound.into()))), + ); + test.run(|interact| { + interact.element("quit", |_style, b: &model::Button| b.click.fire(&())); + }); + + test.assert_files() + .ignore_log() + .saved_settings(Settings::default()) + .submission_event(false) + .pending(); +} + +#[test] +fn response_indicates_discarded() { + let mut test = GuiTest::new(); + // A response indicating discarded triggers a prune of the directory containing the minidump. + // Since there is one more minidump (the main one, minidump.dmp), pruning should keep all but + // the first 3, which will be the oldest. + const SHOULD_BE_PRUNED: usize = 3; + + for i in 0..MINIDUMP_PRUNE_SAVE_COUNT + SHOULD_BE_PRUNED - 1 { + test.files.add_dir("data_dir/pending").add_file_result( + format!("data_dir/pending/minidump{i}.dmp"), + Ok("contents".into()), + ::std::time::SystemTime::UNIX_EPOCH + ::std::time::Duration::from_secs(1234 + i as u64), + ); + if i % 2 == 0 { + test.files + .add_file(format!("data_dir/pending/minidump{i}.extra"), "{}"); + } + if i % 5 == 0 { + test.files + .add_file(format!("data_dir/pending/minidump{i}.memory.json.gz"), "{}"); + } + } + test.mock.set( + Command::mock("curl"), + Box::new(|_| { + let mut output = crate::std::process::success_output(); + output.stdout = format!("CrashID={MOCK_REMOTE_CRASH_ID}\nDiscarded=1").into(); + Ok(output) + }), + ); + test.run(|interact| { + interact.element("quit", |_style, b: &model::Button| b.click.fire(&())); + }); + + let mut assert_files = test.assert_files(); + assert_files + .ignore_log() + .saved_settings(Settings::default()) + .pending(); + for i in SHOULD_BE_PRUNED..MINIDUMP_PRUNE_SAVE_COUNT + SHOULD_BE_PRUNED - 1 { + assert_files.check_exists(format!("data_dir/pending/minidump{i}.dmp")); + if i % 2 == 0 { + assert_files.check_exists(format!("data_dir/pending/minidump{i}.extra")); + } + if i % 5 == 0 { + assert_files.check_exists(format!("data_dir/pending/minidump{i}.memory.json.gz")); + } + } +} + +#[test] +fn response_view_url() { + let mut test = GuiTest::new(); + test.mock.set( + Command::mock("curl"), + Box::new(|_| { + let mut output = crate::std::process::success_output(); + output.stdout = + format!("CrashID={MOCK_REMOTE_CRASH_ID}\nViewURL=https://foo.bar.example").into(); + Ok(output) + }), + ); + test.run(|interact| { + interact.element("quit", |_style, b: &model::Button| b.click.fire(&())); + }); + + test.assert_files() + .ignore_log() + .saved_settings(Settings::default()) + .pending() + .check( + format!("data_dir/submitted/{MOCK_REMOTE_CRASH_ID}.txt"), + format!( + "\ + Crash ID: {}\n\ + You can view details of this crash at {}.\n", + FluentArg(MOCK_REMOTE_CRASH_ID), + FluentArg("https://foo.bar.example") + ), + ); +} + +#[test] +fn response_stop_sending_reports() { + let mut test = GuiTest::new(); + test.mock.set( + Command::mock("curl"), + Box::new(|_| { + let mut output = crate::std::process::success_output(); + output.stdout = + format!("CrashID={MOCK_REMOTE_CRASH_ID}\nStopSendingReportsFor=100.0").into(); + Ok(output) + }), + ); + test.run(|interact| { + interact.element("quit", |_style, b: &model::Button| b.click.fire(&())); + }); + + test.assert_files() + .ignore_log() + .saved_settings(Settings::default()) + .submitted() + .pending() + .check_exists("data_dir/EndOfLife100.0"); +} + +/// A real temporary directory in the host filesystem. +/// +/// The directory is guaranteed to be unique to the test suite process (in case of crash, it can be +/// inspected). +/// +/// When dropped, the directory is deleted. +struct TempDir { + path: ::std::path::PathBuf, +} + +impl TempDir { + /// Create a new directory with the given identifying name. + /// + /// The name should be unique to deconflict amongst concurrent tests. + pub fn new(name: &str) -> Self { + let path = ::std::env::temp_dir().join(format!( + "{}-test-{}-{name}", + env!("CARGO_PKG_NAME"), + std::process::id() + )); + ::std::fs::create_dir_all(&path).unwrap(); + TempDir { path } + } + + /// Get the temporary directory path. + pub fn path(&self) -> &::std::path::Path { + &self.path + } +} + +impl Drop for TempDir { + fn drop(&mut self) { + // Best-effort removal, ignore errors. + let _ = ::std::fs::remove_dir_all(&self.path); + } +} + +/// A mock crash report server. +/// +/// When dropped, the server is shutdown. +struct TestCrashReportServer { + addr: ::std::net::SocketAddr, + shutdown_and_thread: Option<( + tokio::sync::oneshot::Sender<()>, + std::thread::JoinHandle<()>, + )>, +} + +impl TestCrashReportServer { + /// Create and start a mock crash report server on an ephemeral port, returning a handle to the + /// server. + pub fn run() -> Self { + let (shutdown, rx) = tokio::sync::oneshot::channel(); + + use warp::Filter; + + let submit = warp::path("submit") + .and(warp::filters::method::post()) + .and(warp::filters::header::header("content-type")) + .and(warp::filters::body::bytes()) + .and_then(|content_type: String, body: bytes::Bytes| async move { + let Some(boundary) = content_type.strip_prefix("multipart/form-data; boundary=") + else { + return Err(warp::reject()); + }; + + let body = String::from_utf8_lossy(&*body).to_owned(); + + for part in body.split(&format!("--{boundary}")).skip(1) { + if part == "--\r\n" { + break; + } + + let (_headers, _data) = part.split_once("\r\n\r\n").unwrap_or(("", part)); + // TODO validate parts + } + Ok(format!("CrashID={MOCK_REMOTE_CRASH_ID}")) + }); + + let (addr_channel_tx, addr_channel_rx) = std::sync::mpsc::sync_channel(0); + + let thread = ::std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("failed to create tokio runtime"); + let _guard = rt.enter(); + + let (addr, server) = + warp::serve(submit).bind_with_graceful_shutdown(([127, 0, 0, 1], 0), async move { + rx.await.ok(); + }); + + addr_channel_tx.send(addr).unwrap(); + + rt.block_on(server) + }); + + let addr = addr_channel_rx.recv().unwrap(); + + TestCrashReportServer { + addr, + shutdown_and_thread: Some((shutdown, thread)), + } + } + + /// Get the url to which to submit crash reports for this mocked server. + pub fn submit_url(&self) -> String { + format!("http://{}/submit", self.addr) + } +} + +impl Drop for TestCrashReportServer { + fn drop(&mut self) { + let (shutdown, thread) = self.shutdown_and_thread.take().unwrap(); + let _ = shutdown.send(()); + thread.join().unwrap(); + } +} + +#[test] +fn real_curl_binary() { + if ::std::process::Command::new("curl").output().is_err() { + eprintln!("no curl binary; skipping real_curl_binary test"); + return; + } + + let server = TestCrashReportServer::run(); + + let mut test = GuiTest::new(); + test.mock.set( + Command::mock("curl"), + Box::new(|cmd| cmd.output_from_real_command()), + ); + test.config.report_url = Some(server.submit_url().into()); + test.config.delete_dump = true; + + // We need the dump file to actually exist since the curl binary is passed the file path. + // The dump file needs to exist at the pending dir location. + + let tempdir = TempDir::new("real_curl_binary"); + let data_dir = tempdir.path().to_owned(); + let pending_dir = data_dir.join("pending"); + test.config.data_dir = Some(data_dir.clone().into()); + ::std::fs::create_dir_all(&pending_dir).unwrap(); + let dump_file = pending_dir.join("minidump.dmp"); + ::std::fs::write(&dump_file, MOCK_MINIDUMP_FILE).unwrap(); + + test.run(|interact| { + interact.element("quit", |_style, b: &model::Button| b.click.fire(&())); + }); + + test.assert_files() + .set_data_dir(data_dir.display()) + .ignore_log() + .saved_settings(Settings::default()) + .submitted(); +} + +#[test] +fn real_curl_library() { + if !crate::net::can_load_libcurl() { + eprintln!("no libcurl; skipping real_libcurl test"); + return; + } + + let server = TestCrashReportServer::run(); + + let mut test = GuiTest::new(); + test.mock + .set( + Command::mock("curl"), + Box::new(|_| Err(std::io::ErrorKind::NotFound.into())), + ) + .set(mock::MockHook::new("use_system_libcurl"), true); + test.config.report_url = Some(server.submit_url().into()); + test.config.delete_dump = true; + + // We need the dump file to actually exist since libcurl is passed the file path. + // The dump file needs to exist at the pending dir location. + + let tempdir = TempDir::new("real_libcurl"); + let data_dir = tempdir.path().to_owned(); + let pending_dir = data_dir.join("pending"); + test.config.data_dir = Some(data_dir.clone().into()); + ::std::fs::create_dir_all(&pending_dir).unwrap(); + let dump_file = pending_dir.join("minidump.dmp"); + ::std::fs::write(&dump_file, MOCK_MINIDUMP_FILE).unwrap(); + + test.run(|interact| { + interact.element("quit", |_style, b: &model::Button| b.click.fire(&())); + }); + + test.assert_files() + .set_data_dir(data_dir.display()) + .ignore_log() + .saved_settings(Settings::default()) + .submitted(); +} diff --git a/toolkit/crashreporter/client/app/src/thread_bound.rs b/toolkit/crashreporter/client/app/src/thread_bound.rs new file mode 100644 index 0000000000..28095f42f4 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/thread_bound.rs @@ -0,0 +1,41 @@ +/* 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/. */ + +//! Encapsulate thread-bound values in a safe manner. +//! +//! This allows non-`Send`/`Sync` values to be transferred across thread boundaries, checking at +//! runtime at access sites whether it is safe to use them. + +pub struct ThreadBound<T> { + data: T, + origin: std::thread::ThreadId, +} + +impl<T: Default> Default for ThreadBound<T> { + fn default() -> Self { + ThreadBound::new(Default::default()) + } +} + +impl<T> ThreadBound<T> { + pub fn new(data: T) -> Self { + ThreadBound { + data, + origin: std::thread::current().id(), + } + } + + pub fn borrow(&self) -> &T { + assert!( + std::thread::current().id() == self.origin, + "unsafe access to thread-bound value" + ); + &self.data + } +} + +// # Safety +// Access to the inner value is only permitted on the originating thread. +unsafe impl<T> Send for ThreadBound<T> {} +unsafe impl<T> Sync for ThreadBound<T> {} diff --git a/toolkit/crashreporter/client/app/src/ui/crashreporter.png b/toolkit/crashreporter/client/app/src/ui/crashreporter.png Binary files differnew file mode 100644 index 0000000000..5e68bac17c --- /dev/null +++ b/toolkit/crashreporter/client/app/src/ui/crashreporter.png diff --git a/toolkit/crashreporter/client/app/src/ui/gtk.rs b/toolkit/crashreporter/client/app/src/ui/gtk.rs new file mode 100644 index 0000000000..a76f99b0bd --- /dev/null +++ b/toolkit/crashreporter/client/app/src/ui/gtk.rs @@ -0,0 +1,841 @@ +/* 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/. */ + +use super::model::{self, Alignment, Application, Element}; +use crate::std::{ + cell::RefCell, + ffi::{c_char, CString}, + rc::Rc, + sync::atomic::{AtomicBool, Ordering::Relaxed}, +}; +use crate::{ + data::{Event, Property, Synchronized}, + std, +}; + +/// Create a `std::ffi::CStr` directly from a literal string. +/// +/// The argument is an `expr` rather than `literal` so that other macros can be used (such as +/// `stringify!`). +macro_rules! cstr { + ( $str:expr ) => { + #[allow(unused_unsafe)] + unsafe { std::ffi::CStr::from_bytes_with_nul_unchecked(concat!($str, "\0").as_bytes()) } + .as_ptr() + }; +} + +/// A GTK+ UI implementation. +#[derive(Default)] +pub struct UI { + running: AtomicBool, +} + +impl UI { + pub fn run_loop(&self, app: Application) { + unsafe { + let stream = gtk::g_memory_input_stream_new_from_data( + super::icon::PNG_DATA.as_ptr() as _, + // unwrap() because the PNG_DATA length will be well within gssize limits (32-bit + // at the smallest). + super::icon::PNG_DATA.len().try_into().unwrap(), + None, + ); + let icon_pixbuf = + gtk::gdk_pixbuf_new_from_stream(stream, std::ptr::null_mut(), std::ptr::null_mut()); + gtk::g_object_unref(stream as _); + + gtk::gtk_window_set_default_icon(icon_pixbuf); + + let app_ptr = gtk::gtk_application_new( + std::ptr::null(), + gtk::GApplicationFlags_G_APPLICATION_FLAGS_NONE, + ); + gtk::g_signal_connect_data( + app_ptr as *mut _, + cstr!("activate"), + Some(std::mem::transmute( + render_application + as for<'a> unsafe extern "C" fn(*mut gtk::GtkApplication, &'a Application), + )), + &app as *const Application as *mut Application as _, + None, + 0, + ); + self.running.store(true, Relaxed); + gtk::g_application_run(app_ptr as *mut gtk::GApplication, 0, std::ptr::null_mut()); + self.running.store(false, Relaxed); + gtk::g_object_unref(app_ptr as *mut _); + gtk::g_object_unref(icon_pixbuf as _); + } + } + + pub fn invoke(&self, f: model::InvokeFn) { + if !self.running.load(Relaxed) { + log::debug!("ignoring `invoke` as main loop isn't running"); + return; + } + type BoxedData = Option<model::InvokeFn>; + + unsafe extern "C" fn call(ptr: gtk::gpointer) -> gtk::gboolean { + let f: &mut BoxedData = ToPointer::from_ptr(ptr as _); + f.take().unwrap()(); + false.into() + } + + unsafe extern "C" fn drop(ptr: gtk::gpointer) { + let _: Box<BoxedData> = ToPointer::from_ptr(ptr as _); + } + + let data: Box<BoxedData> = Box::new(Some(f)); + + unsafe { + let main_context = gtk::g_main_context_default(); + gtk::g_main_context_invoke_full( + main_context, + 0, // G_PRIORITY_DEFAULT + Some(call as unsafe extern "C" fn(gtk::gpointer) -> gtk::gboolean), + data.to_ptr() as _, + Some(drop as unsafe extern "C" fn(gtk::gpointer)), + ); + } + } +} + +/// Types that can be converted to and from a pointer. +/// +/// These types must be sized to avoid fat pointers (i.e., the pointers must be FFI-compatible, the +/// same size as usize). +trait ToPointer: Sized { + fn to_ptr(self) -> *mut (); + /// # Safety + /// The caller must ensure that the pointer was created as the result of `to_ptr` on the same + /// or a compatible type, and that the data is still valid. + unsafe fn from_ptr(ptr: *mut ()) -> Self; +} + +/// Types that can be attached to a GLib object to be dropped when the widget is dropped. +trait DropWithObject: Sized { + fn drop_with_object(self, object: *mut gtk::GObject); + fn drop_with_widget(self, widget: *mut gtk::GtkWidget) { + self.drop_with_object(widget as *mut _); + } + + fn set_data(self, object: *mut gtk::GObject, key: *const c_char); +} + +impl<T: ToPointer> DropWithObject for T { + fn drop_with_object(self, object: *mut gtk::GObject) { + unsafe extern "C" fn free_ptr<T: ToPointer>( + ptr: gtk::gpointer, + _object: *mut gtk::GObject, + ) { + drop(T::from_ptr(ptr as *mut ())); + } + unsafe { gtk::g_object_weak_ref(object, Some(free_ptr::<T>), self.to_ptr() as *mut _) }; + } + + fn set_data(self, object: *mut gtk::GObject, key: *const c_char) { + unsafe extern "C" fn free_ptr<T: ToPointer>(ptr: gtk::gpointer) { + drop(T::from_ptr(ptr as *mut ())); + } + unsafe { + gtk::g_object_set_data_full(object, key, self.to_ptr() as *mut _, Some(free_ptr::<T>)) + }; + } +} + +impl ToPointer for CString { + fn to_ptr(self) -> *mut () { + self.into_raw() as _ + } + + unsafe fn from_ptr(ptr: *mut ()) -> Self { + CString::from_raw(ptr as *mut c_char) + } +} + +impl<T> ToPointer for Rc<T> { + fn to_ptr(self) -> *mut () { + Rc::into_raw(self) as *mut T as *mut () + } + + unsafe fn from_ptr(ptr: *mut ()) -> Self { + Rc::from_raw(ptr as *mut T as *const T) + } +} + +impl<T> ToPointer for Box<T> { + fn to_ptr(self) -> *mut () { + Box::into_raw(self) as _ + } + + unsafe fn from_ptr(ptr: *mut ()) -> Self { + Box::from_raw(ptr as _) + } +} + +impl<T: Sized> ToPointer for &mut T { + fn to_ptr(self) -> *mut () { + self as *mut T as _ + } + + unsafe fn from_ptr(ptr: *mut ()) -> Self { + &mut *(ptr as *mut T) + } +} + +/// Connect a GTK+ object signal to a function, providing an additional context value (by +/// reference). +macro_rules! connect_signal { + ( object $object:expr ; with $with:expr ; + signal $name:ident ($target:ident : &$type:ty $(, $argname:ident : $argtype:ty )* ) $( -> $ret:ty )? $body:block + ) => {{ + unsafe extern "C" fn $name($($argname : $argtype ,)* $target: &$type) $( -> $ret )? $body + #[allow(unused_unsafe)] + unsafe { + gtk::g_signal_connect_data( + $object as *mut _, + cstr!(stringify!($name)), + Some(std::mem::transmute( + $name + as for<'a> unsafe extern "C" fn( + $($argtype,)* + &'a $type, + ) $( -> $ret )?, + )), + $with as *const $type as *mut $type as _, + None, + 0, + ); + } + }}; +} + +/// Bind a read only (from the renderer perspective) property to a widget. +/// +/// The `set` function is called initially and when the property value changes. +macro_rules! property_read_only { + ( property $property:expr ; + fn set( $name:ident : & $type:ty ) $setbody:block + ) => {{ + let prop: &Property<$type> = $property; + match prop { + Property::Static($name) => $setbody, + Property::Binding(v) => { + { + let $name = v.borrow(); + $setbody + } + v.on_change(move |$name| $setbody); + } + Property::ReadOnly(_) => (), + } + }}; +} + +/// Bind a read/write property to a widget signal. +/// +/// This currently only allows signals which are of the form +/// `void(*)(SomeGtkObject*, gpointer user_data)`. +macro_rules! property_with_signal { + ( object $object:expr ; property $property:expr ; signal $signame:ident ; + fn set( $name:ident : & $type:ty ) $setbody:block + fn get( $getobj:ident : $gettype:ty ) -> $result:ty $getbody:block + ) => {{ + let prop: &Property<$type> = $property; + match prop { + Property::Static($name) => $setbody, + Property::Binding(v) => { + { + let $name = v.borrow(); + $setbody + } + let changing = Rc::new(RefCell::new(false)); + struct SignalData { + changing: Rc<RefCell<bool>>, + value: Synchronized<$result>, + } + let signal_data = Box::new(SignalData { + changing: changing.clone(), + value: v.clone(), + }); + v.on_change(move |$name| { + if !*changing.borrow() { + *changing.borrow_mut() = true; + $setbody; + *changing.borrow_mut() = false; + } + }); + connect_signal! { + object $object; + with signal_data.as_ref(); + signal $signame(signal_data: &SignalData, $getobj: $gettype) { + let new_value = (|| $getbody)(); + if !*signal_data.changing.borrow() { + *signal_data.changing.borrow_mut() = true; + *signal_data.value.borrow_mut() = new_value; + *signal_data.changing.borrow_mut() = false; + } + } + } + signal_data.drop_with_object($object as _); + } + Property::ReadOnly(v) => { + v.register(move |target: &mut $result| { + let $getobj: $gettype = $object as _; + *target = (|| $getbody)(); + }); + } + } + }}; +} + +unsafe extern "C" fn render_application(app_ptr: *mut gtk::GtkApplication, app: &Application) { + unsafe { + gtk::gtk_widget_set_default_direction(if app.rtl { + gtk::GtkTextDirection_GTK_TEXT_DIR_RTL + } else { + gtk::GtkTextDirection_GTK_TEXT_DIR_LTR + }); + } + for window in &app.windows { + let window_ptr = render_window(&window.element_type); + let style = &window.style; + + // Set size before applying style (since the style will set the visibility and show the + // window). Note that we take the size request as an initial size here. + // + // `gtk_window_set_default_size` doesn't work; it resizes to the size request of the inner + // labels (instead of wrapping them) since it doesn't know how small they should be (that's + // dictated by the window size!). + unsafe { + gtk::gtk_window_resize( + window_ptr as _, + style + .horizontal_size_request + .map(|v| v as i32) + .unwrap_or(-1), + style.vertical_size_request.map(|v| v as i32).unwrap_or(-1), + ); + } + + apply_style(window_ptr, style); + unsafe { + gtk::gtk_application_add_window(app_ptr, window_ptr as *mut _); + } + } +} + +fn render(element: &Element) -> Option<*mut gtk::GtkWidget> { + let widget = render_element_type(&element.element_type)?; + apply_style(widget, &element.style); + Some(widget) +} + +fn apply_style(widget: *mut gtk::GtkWidget, style: &model::ElementStyle) { + unsafe { + gtk::gtk_widget_set_halign(widget, alignment(&style.horizontal_alignment)); + if style.horizontal_alignment == Alignment::Fill { + gtk::gtk_widget_set_hexpand(widget, true.into()); + } + gtk::gtk_widget_set_valign(widget, alignment(&style.vertical_alignment)); + if style.vertical_alignment == Alignment::Fill { + gtk::gtk_widget_set_vexpand(widget, true.into()); + } + gtk::gtk_widget_set_size_request( + widget, + style + .horizontal_size_request + .map(|v| v as i32) + .unwrap_or(-1), + style.vertical_size_request.map(|v| v as i32).unwrap_or(-1), + ); + + gtk::gtk_widget_set_margin_start(widget, style.margin.start as i32); + gtk::gtk_widget_set_margin_end(widget, style.margin.end as i32); + gtk::gtk_widget_set_margin_top(widget, style.margin.top as i32); + gtk::gtk_widget_set_margin_bottom(widget, style.margin.bottom as i32); + } + property_read_only! { + property &style.visible; + fn set(new_value: &bool) { + unsafe { + gtk::gtk_widget_set_visible(widget, new_value.clone().into()); + }; + } + } + property_read_only! { + property &style.enabled; + fn set(new_value: &bool) { + unsafe { + gtk::gtk_widget_set_sensitive(widget, new_value.clone().into()); + } + } + } +} + +fn alignment(align: &Alignment) -> gtk::GtkAlign { + use Alignment::*; + match align { + Start => gtk::GtkAlign_GTK_ALIGN_START, + Center => gtk::GtkAlign_GTK_ALIGN_CENTER, + End => gtk::GtkAlign_GTK_ALIGN_END, + Fill => gtk::GtkAlign_GTK_ALIGN_FILL, + } +} + +struct PangoAttrList { + list: *mut gtk::PangoAttrList, +} + +impl PangoAttrList { + pub fn new() -> Self { + PangoAttrList { + list: unsafe { gtk::pango_attr_list_new() }, + } + } + + pub fn bold(&mut self) -> &mut Self { + unsafe { + gtk::pango_attr_list_insert( + self.list, + gtk::pango_attr_weight_new(gtk::PangoWeight_PANGO_WEIGHT_BOLD), + ) + }; + self + } +} + +impl std::ops::Deref for PangoAttrList { + type Target = *mut gtk::PangoAttrList; + + fn deref(&self) -> &Self::Target { + &self.list + } +} + +impl std::ops::DerefMut for PangoAttrList { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.list + } +} + +impl Drop for PangoAttrList { + fn drop(&mut self) { + unsafe { gtk::pango_attr_list_unref(self.list) }; + } +} + +fn render_element_type(element_type: &model::ElementType) -> Option<*mut gtk::GtkWidget> { + use model::ElementType::*; + Some(match element_type { + Label(model::Label { text, bold }) => { + let label_ptr = unsafe { gtk::gtk_label_new(std::ptr::null()) }; + match text { + Property::Static(text) => { + let text = CString::new(text.clone()).ok()?; + unsafe { gtk::gtk_label_set_text(label_ptr as _, text.as_ptr()) }; + text.drop_with_widget(label_ptr); + } + Property::Binding(b) => { + let label_text = Rc::new(RefCell::new(CString::new(b.borrow().clone()).ok()?)); + unsafe { + gtk::gtk_label_set_text(label_ptr as _, label_text.borrow().as_ptr()) + }; + let lt = label_text.clone(); + label_text.drop_with_widget(label_ptr); + b.on_change(move |t| { + let Some(cstr) = CString::new(t.clone()).ok() else { + return; + }; + unsafe { gtk::gtk_label_set_text(label_ptr as _, cstr.as_ptr()) }; + *lt.borrow_mut() = cstr; + }); + } + Property::ReadOnly(_) => unimplemented!("ReadOnly not supported for Label::text"), + } + unsafe { gtk::gtk_label_set_line_wrap(label_ptr as _, true.into()) }; + // This is gtk_label_set_{xalign,yalign} in gtk 3.16+ + unsafe { gtk::gtk_misc_set_alignment(label_ptr as _, 0.0, 0.5) }; + if *bold { + unsafe { + gtk::gtk_label_set_attributes(label_ptr as _, **PangoAttrList::new().bold()) + }; + } + label_ptr + } + HBox(model::HBox { + items, + spacing, + affirmative_order, + }) => { + let box_ptr = + unsafe { gtk::gtk_box_new(gtk::GtkOrientation_GTK_ORIENTATION_HORIZONTAL, 0) }; + unsafe { gtk::gtk_box_set_spacing(box_ptr as *mut _, *spacing as i32) }; + let items_iter: Box<dyn Iterator<Item = &Element>> = if *affirmative_order { + Box::new(items.iter().rev()) + } else { + Box::new(items.iter()) + }; + for item in items_iter { + if let Some(widget) = render(item) { + unsafe { + gtk::gtk_container_add(box_ptr as *mut gtk::GtkContainer, widget); + } + // Special case horizontal alignment to pack into the end if appropriate + if item.style.horizontal_alignment == Alignment::End { + unsafe { + gtk::gtk_box_set_child_packing( + box_ptr as _, + widget, + false.into(), + false.into(), + 0, + gtk::GtkPackType_GTK_PACK_END, + ); + } + } + } + } + box_ptr + } + VBox(model::VBox { items, spacing }) => { + let box_ptr = + unsafe { gtk::gtk_box_new(gtk::GtkOrientation_GTK_ORIENTATION_VERTICAL, 0) }; + unsafe { gtk::gtk_box_set_spacing(box_ptr as *mut _, *spacing as i32) }; + for item in items { + if let Some(widget) = render(item) { + unsafe { + gtk::gtk_container_add(box_ptr as *mut gtk::GtkContainer, widget); + } + // Special case vertical alignment to pack into the end if appropriate + if item.style.vertical_alignment == Alignment::End { + unsafe { + gtk::gtk_box_set_child_packing( + box_ptr as _, + widget, + false.into(), + false.into(), + 0, + gtk::GtkPackType_GTK_PACK_END, + ); + } + } + } + } + box_ptr + } + Button(model::Button { content, click }) => { + let button_ptr = unsafe { gtk::gtk_button_new() }; + if let Some(widget) = content.as_deref().and_then(render) { + unsafe { + // Always center widgets in buttons. + gtk::gtk_widget_set_valign(widget, alignment(&Alignment::Center)); + gtk::gtk_widget_set_halign(widget, alignment(&Alignment::Center)); + gtk::gtk_container_add(button_ptr as *mut gtk::GtkContainer, widget); + } + } + connect_signal! { + object button_ptr; + with click; + signal clicked(event: &Event<()>, _button: *mut gtk::GtkButton) { + event.fire(&()); + } + } + button_ptr + } + Checkbox(model::Checkbox { checked, label }) => { + let cb_ptr = match label { + None => unsafe { gtk::gtk_check_button_new() }, + Some(s) => { + let label = CString::new(s.clone()).ok()?; + let cb = unsafe { gtk::gtk_check_button_new_with_label(label.as_ptr()) }; + label.drop_with_widget(cb); + cb + } + }; + property_with_signal! { + object cb_ptr; + property checked; + signal toggled; + fn set(new_value: &bool) { + unsafe { + gtk::gtk_toggle_button_set_active(cb_ptr as *mut _, new_value.clone().into()) + }; + } + fn get(button: *mut gtk::GtkToggleButton) -> bool { + unsafe { + gtk::gtk_toggle_button_get_active(button) == 1 + } + } + } + cb_ptr + } + TextBox(model::TextBox { + placeholder, + content, + editable, + }) => { + let text_ptr = unsafe { gtk::gtk_text_view_new() }; + unsafe { + const GTK_WRAP_WORD_CHAR: u32 = 3; + gtk::gtk_text_view_set_wrap_mode(text_ptr as *mut _, GTK_WRAP_WORD_CHAR); + gtk::gtk_text_view_set_editable(text_ptr as *mut _, editable.clone().into()); + gtk::gtk_text_view_set_accepts_tab(text_ptr as *mut _, false.into()); + } + let buffer = unsafe { gtk::gtk_text_view_get_buffer(text_ptr as *mut _) }; + + struct State { + placeholder: Option<Placeholder>, + } + + struct Placeholder { + string: CString, + visible: RefCell<bool>, + } + + impl Placeholder { + fn focus(&self, widget: *mut gtk::GtkWidget) { + if *self.visible.borrow() { + unsafe { + let buffer = gtk::gtk_text_view_get_buffer(widget as *mut _); + gtk::gtk_text_buffer_set_text(buffer, self.string.as_ptr(), 0); + gtk::gtk_widget_override_color( + widget, + gtk::GtkStateFlags_GTK_STATE_FLAG_NORMAL, + std::ptr::null(), + ); + } + *self.visible.borrow_mut() = false; + } + } + + fn unfocus(&self, widget: *mut gtk::GtkWidget) { + unsafe { + let buffer = gtk::gtk_text_view_get_buffer(widget as *mut _); + + let mut end_iter = gtk::GtkTextIter::default(); + gtk::gtk_text_buffer_get_end_iter(buffer, &mut end_iter); + let is_empty = gtk::gtk_text_iter_get_offset(&end_iter) == 0; + + if is_empty && !*self.visible.borrow() { + gtk::gtk_text_buffer_set_text(buffer, self.string.as_ptr(), -1); + let context = gtk::gtk_widget_get_style_context(widget); + let mut color = gtk::GdkRGBA::default(); + gtk::gtk_style_context_get_color( + context, + gtk::GtkStateFlags_GTK_STATE_FLAG_INSENSITIVE, + &mut color, + ); + gtk::gtk_widget_override_color( + widget, + gtk::GtkStateFlags_GTK_STATE_FLAG_NORMAL, + &color, + ); + *self.visible.borrow_mut() = true; + } + } + } + } + + let mut state = Box::new(State { placeholder: None }); + + if let Some(placeholder) = placeholder { + state.placeholder = Some(Placeholder { + string: CString::new(placeholder.clone()).ok()?, + visible: RefCell::new(false), + }); + + let placeholder = state.placeholder.as_ref().unwrap(); + + placeholder.unfocus(text_ptr); + + connect_signal! { + object text_ptr; + with placeholder; + signal focus_in_event(placeholder: &Placeholder, widget: *mut gtk::GtkWidget, + _event: *mut gtk::GdkEventFocus) -> gtk::gboolean { + placeholder.focus(widget); + false.into() + } + } + connect_signal! { + object text_ptr; + with placeholder; + signal focus_out_event(placeholder: &Placeholder, widget: *mut gtk::GtkWidget, + _event: *mut gtk::GdkEventFocus) -> gtk::gboolean { + placeholder.unfocus(widget); + false.into() + } + } + } + + // Attach the state so that we can access it in the changed signal. + // This is kind of ugly; in the future it might be a nicer developer interface to simply + // always use a closure as the user data (which itself can capture arbitrary things). This + // would move the data management from GTK to rust. + state.set_data(buffer as *mut _, cstr!("textview-state")); + + property_with_signal! { + object buffer; + property content; + signal changed; + fn set(new_value: &String) { + unsafe { + gtk::gtk_text_buffer_set_text(buffer, new_value.as_ptr() as *const c_char, new_value.len().try_into().unwrap()); + } + } + fn get(buffer: *mut gtk::GtkTextBuffer) -> String { + let state = unsafe { + gtk::g_object_get_data(buffer as *mut _, cstr!("textview-state")) + }; + if !state.is_null() { + let s: &State = unsafe { &*(state as *mut State as *const State) }; + if let Some(placeholder) = &s.placeholder { + if *placeholder.visible.borrow() { + return "".to_owned(); + } + } + } + let mut start_iter = gtk::GtkTextIter::default(); + unsafe { + gtk::gtk_text_buffer_get_start_iter(buffer, &mut start_iter); + } + + let mut s = String::new(); + loop { + let c = unsafe { gtk::gtk_text_iter_get_char(&start_iter) }; + if c == 0 { + break; + } + // Safety: + // gunichar is guaranteed to be a valid unicode codepoint (if nonzero). + s.push(unsafe { char::from_u32_unchecked(c) }); + unsafe { + gtk::gtk_text_iter_forward_char(&mut start_iter); + } + } + s + } + } + text_ptr + } + Scroll(model::Scroll { content }) => { + let scroll_ptr = + unsafe { gtk::gtk_scrolled_window_new(std::ptr::null_mut(), std::ptr::null_mut()) }; + unsafe { + gtk::gtk_scrolled_window_set_policy( + scroll_ptr as *mut _, + gtk::GtkPolicyType_GTK_POLICY_NEVER, + gtk::GtkPolicyType_GTK_POLICY_ALWAYS, + ); + gtk::gtk_scrolled_window_set_shadow_type( + scroll_ptr as *mut _, + gtk::GtkShadowType_GTK_SHADOW_IN, + ); + }; + if let Some(widget) = content.as_deref().and_then(render) { + unsafe { + gtk::gtk_container_add(scroll_ptr as *mut gtk::GtkContainer, widget); + } + } + scroll_ptr + } + Progress(model::Progress { amount }) => { + let progress_ptr = unsafe { gtk::gtk_progress_bar_new() }; + property_read_only! { + property amount; + fn set(value: &Option<f32>) { + match &*value { + Some(v) => unsafe { + gtk::gtk_progress_bar_set_fraction( + progress_ptr as *mut _, + v.clamp(0f32,1f32) as f64, + ); + } + None => unsafe { + gtk::gtk_progress_bar_pulse(progress_ptr as *mut _); + + fn auto_pulse_progress_bar(progress: *mut gtk::GtkProgressBar) { + unsafe extern fn pulse(progress: *mut std::ffi::c_void) -> gtk::gboolean { + if gtk::gtk_widget_is_visible(progress as _) == 0 { + false.into() + } else { + gtk::gtk_progress_bar_pulse(progress as _); + true.into() + } + } + unsafe { + gtk::g_timeout_add(100, Some(pulse as unsafe extern fn(*mut std::ffi::c_void) -> gtk::gboolean), progress as _); + } + + } + + connect_signal! { + object progress_ptr; + with std::ptr::null_mut(); + signal show(_user_data: &(), progress: *mut gtk::GtkWidget) { + auto_pulse_progress_bar(progress as *mut _); + } + } + auto_pulse_progress_bar(progress_ptr as *mut _); + } + } + } + } + progress_ptr + } + }) +} + +fn render_window( + model::Window { + title, + content, + children, + modal, + close, + }: &model::Window, +) -> *mut gtk::GtkWidget { + unsafe { + let window_ptr = gtk::gtk_window_new(gtk::GtkWindowType_GTK_WINDOW_TOPLEVEL); + if !title.is_empty() { + if let Some(title) = CString::new(title.clone()).ok() { + gtk::gtk_window_set_title(window_ptr as *mut _, title.as_ptr()); + title.drop_with_widget(window_ptr); + } + } + if let Some(content) = content { + if let Some(widget) = render(content) { + gtk::gtk_container_add(window_ptr as *mut gtk::GtkContainer, widget); + } + } + for child in children { + let widget = render_window(&child.element_type); + apply_style(widget, &child.style); + gtk::gtk_window_set_transient_for(widget as *mut _, window_ptr as *mut _); + // Delete should hide the window. + gtk::g_signal_connect_data( + widget as *mut _, + cstr!("delete-event"), + Some(std::mem::transmute( + gtk::gtk_widget_hide_on_delete + as unsafe extern "C" fn(*mut gtk::GtkWidget) -> i32, + )), + std::ptr::null_mut(), + None, + 0, + ); + } + if *modal { + gtk::gtk_window_set_modal(window_ptr as *mut _, true.into()); + } + if let Some(close) = close { + close.subscribe(move |&()| gtk::gtk_window_close(window_ptr as *mut _)); + } + + window_ptr + } +} diff --git a/toolkit/crashreporter/client/app/src/ui/macos/mod.rs b/toolkit/crashreporter/client/app/src/ui/macos/mod.rs new file mode 100644 index 0000000000..6520d16472 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/ui/macos/mod.rs @@ -0,0 +1,1122 @@ +/* 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/. */ + +//! A UI using the macos cocoa API. +//! +//! This UI contains some edge cases that aren't implemented, for instance: +//! * there are a few cases where specific hierarchies are handled differently (e.g. a Button +//! containing Label), etc. +//! * not all controls handle all Property variants (e.g. Checkbox doesn't handle ReadOnly, Text +//! doesn't handle Binding, etc). +//! +//! The rendering ignores `ElementStyle::margin` entirely, because +//! * `NSView` doesn't support margins (so working them into constraints would be a bit annoying), +//! and +//! * `NSView.layoutMarginsGuide` results in a layout almost identical to what the margins (at the +//! time of this writing) in the UI layouts are achieving. +//! +//! In a few cases, init or creation functions are called which _could_ return nil and are wrapped +//! in their type wrapper (as those functions return `instancetype`/`id`). We consider this safe +//! enough because it won't cause unsoundness (they are only passed to objc functions which can +//! take nil arguments) and the failure case is very unlikely. + +#![allow(non_upper_case_globals)] + +use self::objc::*; +use super::model::{self, Alignment, Application, Element, TypedElement}; +use crate::data::Property; +use cocoa::{ + INSApplication, INSBox, INSButton, INSColor, INSControl, INSFont, INSLayoutAnchor, + INSLayoutConstraint, INSLayoutDimension, INSLayoutGuide, INSMenu, INSMenuItem, + INSMutableParagraphStyle, INSObject, INSProcessInfo, INSProgressIndicator, INSRunLoop, + INSScrollView, INSStackView, INSText, INSTextContainer, INSTextField, INSTextView, INSView, + INSWindow, NSArray_NSArrayCreation, NSAttributedString_NSExtendedAttributedString, + NSDictionary_NSDictionaryCreation, NSRunLoop_NSRunLoopConveniences, + NSStackView_NSStackViewGravityAreas, NSString_NSStringExtensionMethods, + NSTextField_NSTextFieldConvenience, NSView_NSConstraintBasedLayoutInstallingConstraints, + NSView_NSConstraintBasedLayoutLayering, NSView_NSSafeAreas, PNSObject, +}; +use once_cell::sync::Lazy; + +/// https://developer.apple.com/documentation/foundation/1497293-string_encodings/nsutf8stringencoding?language=objc +const NSUTF8StringEncoding: cocoa::NSStringEncoding = 4; + +/// Constant from NSCell.h +const NSControlStateValueOn: cocoa::NSControlStateValue = 1; + +/// Constant from NSLayoutConstraint.h +const NSLayoutPriorityDefaultHigh: cocoa::NSLayoutPriority = 750.0; + +mod objc; + +/// A MacOS Cocoa UI implementation. +#[derive(Default)] +pub struct UI; + +impl UI { + pub fn run_loop(&self, app: Application) { + let nsapp = unsafe { cocoa::NSApplication::sharedApplication() }; + + Objc::<AppDelegate>::register(); + Objc::<Button>::register(); + Objc::<Checkbox>::register(); + Objc::<TextView>::register(); + Objc::<Window>::register(); + + rc::autoreleasepool(|| { + let delegate = AppDelegate::new(app).into_object(); + // Set delegate + unsafe { nsapp.setDelegate_(delegate.instance as *mut _) }; + + // Set up the main menu + unsafe { + let appname = read_nsstring(cocoa::NSProcessInfo::processInfo().processName()); + let mainmenu = StrongRef::new(cocoa::NSMenu::alloc()); + mainmenu.init(); + + { + // We don't need a title for the app menu item nor menu; it will always come from + // the process name regardless of what we set. + let appmenuitem = StrongRef::new(cocoa::NSMenuItem::alloc()).autorelease(); + appmenuitem.init(); + mainmenu.addItem_(appmenuitem); + + let appmenu = StrongRef::new(cocoa::NSMenu::alloc()); + appmenu.init(); + + { + let quit = StrongRef::new(cocoa::NSMenuItem::alloc()); + quit.initWithTitle_action_keyEquivalent_( + nsstring(&format!("Quit {appname}")), + sel!(terminate:), + nsstring("q"), + ); + appmenu.addItem_(quit.autorelease()); + } + appmenuitem.setSubmenu_(appmenu.autorelease()); + } + { + let editmenuitem = StrongRef::new(cocoa::NSMenuItem::alloc()).autorelease(); + editmenuitem.initWithTitle_action_keyEquivalent_( + nsstring("Edit"), + runtime::Sel::from_ptr(std::ptr::null()), + nsstring(""), + ); + mainmenu.addItem_(editmenuitem); + + let editmenu = StrongRef::new(cocoa::NSMenu::alloc()); + editmenu.initWithTitle_(nsstring("Edit")); + + let add_item = |name, selector, shortcut| { + let item = StrongRef::new(cocoa::NSMenuItem::alloc()); + item.initWithTitle_action_keyEquivalent_( + nsstring(name), + selector, + nsstring(shortcut), + ); + editmenu.addItem_(item.autorelease()); + }; + + add_item("Undo", sel!(undo:), "z"); + add_item("Redo", sel!(redo:), "Z"); + editmenu.addItem_(cocoa::NSMenuItem::separatorItem()); + add_item("Cut", sel!(cut:), "x"); + add_item("Copy", sel!(copy:), "c"); + add_item("Paste", sel!(paste:), "v"); + add_item("Delete", sel!(delete:), ""); + add_item("Select All", sel!(selectAll:), "a"); + + editmenuitem.setSubmenu_(editmenu.autorelease()); + } + + nsapp.setMainMenu_(mainmenu.autorelease()); + } + + // Run the main application loop + unsafe { nsapp.run() }; + }); + } + + pub fn invoke(&self, f: model::InvokeFn) { + // Blocks only take `Fn`, so we have to wrap the boxed function. + let f = std::cell::RefCell::new(Some(f)); + enqueue(move || { + if let Some(f) = f.borrow_mut().take() { + f(); + } + }); + } +} + +fn enqueue<F: Fn() + 'static>(f: F) { + let block = block::ConcreteBlock::new(f); + // The block must be an RcBlock so addOperationWithBlock can retain it. + // https://docs.rs/block/latest/block/#creating-blocks + let block = block.copy(); + + // We need to explicitly signal that the enqueued blocks can run in both the default mode (the + // main loop) and modal mode, otherwise when modal windows are opened things get stuck. + struct RunloopModes(cocoa::NSArray); + + impl RunloopModes { + pub fn new() -> Self { + unsafe { + let objects: [cocoa::id; 2] = [ + cocoa::NSDefaultRunLoopMode.0, + cocoa::NSModalPanelRunLoopMode.0, + ]; + RunloopModes( + cocoa::NSArray(<cocoa::NSArray as NSArray_NSArrayCreation< + cocoa::NSRunLoopMode, + >>::arrayWithObjects_count_( + objects.as_slice().as_ptr() as *const *mut u64, + objects + .as_slice() + .len() + .try_into() + .expect("usize can't fit in u64"), + )), + ) + } + } + } + + // # Safety + // The array is static and cannot be changed. + unsafe impl Sync for RunloopModes {} + unsafe impl Send for RunloopModes {} + + static RUNLOOP_MODES: Lazy<RunloopModes> = Lazy::new(RunloopModes::new); + + unsafe { + cocoa::NSRunLoop::mainRunLoop().performInModes_block_(RUNLOOP_MODES.0, &*block); + } +} + +#[repr(transparent)] +struct Rect(pub cocoa::NSRect); + +unsafe impl Encode for Rect { + fn encode() -> Encoding { + unsafe { Encoding::from_str("{CGRect={CGPoint=dd}{CGSize=dd}}") } + } +} + +/// Create an NSString by copying a str. +fn nsstring(v: &str) -> cocoa::NSString { + unsafe { + StrongRef::new(cocoa::NSString( + cocoa::NSString::alloc().initWithBytes_length_encoding_( + v.as_ptr() as *const _, + v.len().try_into().expect("usize can't fit in u64"), + NSUTF8StringEncoding, + ), + )) + } + .autorelease() +} + +/// Create a String by copying an NSString +fn read_nsstring(s: cocoa::NSString) -> String { + let c_str = unsafe { std::ffi::CStr::from_ptr(s.UTF8String()) }; + c_str.to_str().expect("NSString isn't UTF8").to_owned() +} + +fn nsrect<X: Into<f64>, Y: Into<f64>, W: Into<f64>, H: Into<f64>>( + x: X, + y: Y, + w: W, + h: H, +) -> cocoa::NSRect { + cocoa::NSRect { + origin: cocoa::NSPoint { + x: x.into(), + y: y.into(), + }, + size: cocoa::NSSize { + width: w.into(), + height: h.into(), + }, + } +} + +struct AppDelegate { + app: Option<Application>, + windows: Vec<StrongRef<cocoa::NSWindow>>, +} + +impl AppDelegate { + pub fn new(app: Application) -> Self { + AppDelegate { + app: Some(app), + windows: Default::default(), + } + } +} + +objc_class! { + impl AppDelegate: NSObject /*<NSApplicationDelegate>*/ { + #[sel(applicationDidFinishLaunching:)] + fn application_did_finish_launching(&mut self, _notification: Ptr<cocoa::NSNotification>) { + // Activate the application (bringing windows to the active foreground later) + unsafe { cocoa::NSApplication::sharedApplication().activateIgnoringOtherApps_(runtime::YES) }; + + let mut first = true; + let mut windows = WindowRenderer::default(); + let app = self.app.take().unwrap(); + windows.rtl = app.rtl; + for window in app.windows { + let w = windows.render(window); + unsafe { + if first { + w.makeKeyAndOrderFront_(self.instance); + w.makeMainWindow(); + first = false; + } + } + } + self.windows = windows.unwrap(); + + } + + #[sel(applicationShouldTerminateAfterLastWindowClosed:)] + fn application_should_terminate_after_window_closed(&mut self, _app: Ptr<cocoa::NSApplication>) -> runtime::BOOL { + runtime::YES + } + } +} + +struct Window { + modal: bool, + title: String, + style: model::ElementStyle, +} + +objc_class! { + impl Window: NSWindow /*<NSWindowDelegate>*/ { + #[sel(init)] + fn init(&mut self) -> cocoa::id { + let style = &self.style; + let title = &self.title; + let w = cocoa::NSWindow(self.instance); + + unsafe { + if w.initWithContentRect_styleMask_backing_defer_( + nsrect( + 0, + 0, + style.horizontal_size_request.unwrap_or(800), + style.vertical_size_request.unwrap_or(500), + ), + cocoa::NSWindowStyleMaskTitled + | cocoa::NSWindowStyleMaskClosable + | cocoa::NSWindowStyleMaskResizable + | cocoa::NSWindowStyleMaskMiniaturizable, + cocoa::NSBackingStoreBuffered, + runtime::NO, + ).is_null() { + return std::ptr::null_mut(); + } + + w.setDelegate_(self.instance as _); + w.setMinSize_(cocoa::NSSize { + width: style.horizontal_size_request.unwrap_or(0) as f64, + height: style.vertical_size_request.unwrap_or(0) as f64, + }); + + if !title.is_empty() { + w.setTitle_(nsstring(title.as_str())); + } + } + + self.instance + } + + #[sel(windowWillClose:)] + fn window_will_close(&mut self, _notification: Ptr<cocoa::NSNotification>) { + if self.modal { + unsafe { + let nsapp = cocoa::NSApplication::sharedApplication(); + nsapp.stopModal(); + } + } + } + } +} + +impl From<Objc<Window>> for cocoa::NSWindow { + fn from(ptr: Objc<Window>) -> Self { + cocoa::NSWindow(ptr.instance) + } +} + +struct Button { + element: model::Button, +} + +impl Button { + pub fn with_title(self, title: &str) -> cocoa::NSButton { + let obj = self.into_object(); + unsafe { + let () = msg_send![obj.instance, setTitle: nsstring(title)]; + } + // # Safety + // NSButton is the superclass of Objc<Button>. + unsafe { std::mem::transmute(obj.autorelease()) } + } +} + +objc_class! { + impl Button: NSButton { + #[sel(initWithFrame:)] + fn init_with_frame(&mut self, frame_rect: Rect) -> cocoa::id { + unsafe { + let object: cocoa::id = msg_send![super(self.instance, class!(NSButton)), initWithFrame: frame_rect.0]; + if object.is_null() { + return object; + } + let () = msg_send![object, setBezelStyle: cocoa::NSBezelStyleRounded]; + let () = msg_send![object, setAction: sel!(didClick)]; + let () = msg_send![object, setTarget: object]; + object + } + } + + #[sel(didClick)] + fn did_click(&mut self) { + self.element.click.fire(&()); + } + } +} + +struct Checkbox { + element: model::Checkbox, +} + +objc_class! { + impl Checkbox: NSButton { + #[sel(initWithFrame:)] + fn init_with_frame(&mut self, frame_rect: Rect) -> cocoa::id { + unsafe { + let object: cocoa::id = msg_send![super(self.instance, class!(NSButton)), initWithFrame: frame_rect.0]; + if object.is_null() { + return object; + } + let () = msg_send![object, setButtonType: cocoa::NSButtonTypeSwitch]; + if let Some(label) = &self.element.label { + let () = msg_send![object, setTitle: nsstring(label.as_str())]; + } + let () = msg_send![object, setAction: sel!(didClick:)]; + let () = msg_send![object, setTarget: object]; + + match &self.element.checked { + Property::Binding(s) => { + if *s.borrow() { + let () = msg_send![object, setState: NSControlStateValueOn]; + } + } + Property::ReadOnly(_) => (), + Property::Static(_) => (), + } + + object + } + } + + #[sel(didClick:)] + fn did_click(&mut self, button: Objc<Checkbox>) { + match &self.element.checked { + Property::Binding(s) => { + let state = unsafe { std::mem::transmute::<_, cocoa::NSButton>(button).state() }; + *s.borrow_mut() = state == NSControlStateValueOn; + } + Property::ReadOnly(_) => (), + Property::Static(_) => (), + } + } + } +} + +impl Checkbox { + pub fn into_button(self) -> cocoa::NSButton { + let obj = self.into_object(); + // # Safety + // NSButton is the superclass of Objc<Checkbox>. + unsafe { std::mem::transmute(obj.autorelease()) } + } +} + +struct TextView; + +objc_class! { + impl TextView: NSTextView /*<NSTextViewDelegate>*/ { + #[sel(initWithFrame:)] + fn init_with_frame(&mut self, frame_rect: Rect) -> cocoa::id { + unsafe { + let object: cocoa::id = msg_send![super(self.instance, class!(NSTextView)), initWithFrame: frame_rect.0]; + if object.is_null() { + return object; + } + let () = msg_send![object, setDelegate: self.instance]; + object + } + } + + #[sel(textView:doCommandBySelector:)] + fn do_command_by_selector(&mut self, text_view: Ptr<cocoa::NSTextView>, selector: runtime::Sel) -> runtime::BOOL { + let Ptr(text_view) = text_view; + // Make Tab/Backtab navigate to key views rather than inserting tabs in the text view. + // We can't use the `NSText` `fieldEditor` property to implement this behavior because + // that will disable the Enter key. + if selector == sel!(insertTab:) { + unsafe { text_view.window().selectNextKeyView_(text_view.0) }; + return runtime::YES; + } else if selector == sel!(insertBacktab:) { + unsafe { text_view.window().selectPreviousKeyView_(text_view.0) }; + return runtime::YES; + } + runtime::NO + } + } +} + +impl From<Objc<TextView>> for cocoa::NSTextView { + fn from(tv: Objc<TextView>) -> Self { + // # Safety + // NSTextView is the superclass of Objc<TextView>. + unsafe { std::mem::transmute(tv) } + } +} + +// For some reason the bindgen code for the nslayoutanchor subclasses doesn't have +// `Into<NSLayoutAnchor>`, so we add our own. +trait IntoNSLayoutAnchor { + fn into_layout_anchor(self) -> cocoa::NSLayoutAnchor; +} + +impl IntoNSLayoutAnchor for cocoa::NSLayoutXAxisAnchor { + fn into_layout_anchor(self) -> cocoa::NSLayoutAnchor { + // # Safety + // NSLayoutXAxisAnchor is a subclass of NSLayoutAnchor + cocoa::NSLayoutAnchor(self.0) + } +} + +impl IntoNSLayoutAnchor for cocoa::NSLayoutYAxisAnchor { + fn into_layout_anchor(self) -> cocoa::NSLayoutAnchor { + // # Safety + // NSLayoutYAxisAnchor is a subclass of NSLayoutAnchor + cocoa::NSLayoutAnchor(self.0) + } +} + +unsafe fn constraint_equal<T, O>(anchor: T, to: O) +where + T: INSLayoutAnchor<()> + std::ops::Deref, + T::Target: Message + Sized, + O: IntoNSLayoutAnchor, +{ + anchor + .constraintEqualToAnchor_(to.into_layout_anchor()) + .setActive_(runtime::YES); +} + +#[derive(Default)] +struct WindowRenderer { + windows_to_retain: Vec<StrongRef<cocoa::NSWindow>>, + rtl: bool, +} + +impl WindowRenderer { + pub fn unwrap(self) -> Vec<StrongRef<cocoa::NSWindow>> { + self.windows_to_retain + } + + pub fn render(&mut self, window: TypedElement<model::Window>) -> StrongRef<cocoa::NSWindow> { + let style = window.style; + let model::Window { + close, + children, + content, + modal, + title, + } = window.element_type; + + let w = Window { + modal, + title, + style, + } + .into_object(); + + let nswindow: StrongRef<cocoa::NSWindow> = w.clone().cast(); + + unsafe { + // Don't release windows when closed: we retain windows at the top-level. + nswindow.setReleasedWhenClosed_(runtime::NO); + + if let Some(close) = close { + let nswindow = nswindow.weak(); + close.subscribe(move |&()| { + if let Some(nswindow) = nswindow.lock() { + nswindow.close(); + } + }); + } + + if let Some(e) = content { + // Use an NSBox as a container view so that the window's content can easily have + // constraints set up relative to the parent (they can't be set relative to the + // window). + let content_parent: StrongRef<cocoa::NSBox> = msg_send![class!(NSBox), new]; + content_parent.setTitlePosition_(cocoa::NSNoTitle); + content_parent.setTransparent_(runtime::YES); + content_parent.setContentViewMargins_(cocoa::NSSize { + width: 5.0, + height: 5.0, + }); + if ViewRenderer::new_with_selector(self.rtl, *content_parent, sel!(setContentView:)) + .render(*e) + { + nswindow.setContentView_((*content_parent).into()); + } + } + + for child in children { + let modal = child.element_type.modal; + let visible = child.style.visible.clone(); + let child_window = self.render(child); + + #[derive(Clone, Copy)] + struct ShowChild { + modal: bool, + } + + impl ShowChild { + pub fn show(&self, parent: cocoa::NSWindow, child: cocoa::NSWindow) { + unsafe { + parent.addChildWindow_ordered_(child, cocoa::NSWindowAbove); + child.makeKeyAndOrderFront_(parent.0); + if self.modal { + // Run the modal from the main nsapp.run() loop to prevent binding + // updates from being nested (as this will block until the modal is + // stopped). + enqueue(move || { + let nsapp = cocoa::NSApplication::sharedApplication(); + nsapp.runModalForWindow_(child); + }); + } + } + } + } + + let show_child = ShowChild { modal }; + + match visible { + Property::Static(visible) => { + if visible { + show_child.show(*nswindow, *child_window); + } + } + Property::Binding(b) => { + let child = child_window.weak(); + let parent = nswindow.weak(); + b.on_change(move |visible| { + let Some((w, child_window)) = parent.lock().zip(child.lock()) else { + return; + }; + if *visible { + show_child.show(*w, *child_window); + } else { + child_window.close(); + } + }); + if *b.borrow() { + show_child.show(*nswindow, *child_window); + } + } + Property::ReadOnly(_) => panic!("window visibility cannot be ReadOnly"), + } + } + } + self.windows_to_retain.push(nswindow.clone()); + nswindow + } +} + +struct ViewRenderer { + parent: cocoa::NSView, + add_subview: Box<dyn Fn(cocoa::NSView, &model::ElementStyle, cocoa::NSView)>, + ignore_vertical: bool, + ignore_horizontal: bool, + rtl: bool, +} + +impl ViewRenderer { + /// add_subview should add the rendered child view. + pub fn new<F>(rtl: bool, parent: impl Into<cocoa::NSView>, add_subview: F) -> Self + where + F: Fn(cocoa::NSView, &model::ElementStyle, cocoa::NSView) + 'static, + { + ViewRenderer { + parent: parent.into(), + add_subview: Box::new(add_subview), + ignore_vertical: false, + ignore_horizontal: false, + rtl, + } + } + + /// add_subview should be the selector to call on the parent view to add the rendered child view. + pub fn new_with_selector( + rtl: bool, + parent: impl Into<cocoa::NSView>, + add_subview: runtime::Sel, + ) -> Self { + Self::new(rtl, parent, move |parent, _style, child| { + let () = unsafe { (*parent.0).send_message(add_subview, (child,)) }.unwrap(); + }) + } + + /// Ignore vertical layout settings when rendering views. + pub fn ignore_vertical(mut self, setting: bool) -> Self { + self.ignore_vertical = setting; + self + } + + /// Ignore horizontal layout settings when rendering views. + pub fn ignore_horizontal(mut self, setting: bool) -> Self { + self.ignore_horizontal = setting; + self + } + + /// Render the given element. + /// + /// Returns whether the element was rendered. + pub fn render( + &self, + Element { + style, + element_type, + }: Element, + ) -> bool { + let Some(view) = render_element(element_type, &style, self.rtl) else { + return false; + }; + + (self.add_subview)(self.parent, &style, view); + + // Setting the content hugging priority to a high value causes stackviews to not stretch + // subviews during autolayout. + unsafe { + view.setContentHuggingPriority_forOrientation_( + NSLayoutPriorityDefaultHigh, + cocoa::NSLayoutConstraintOrientationHorizontal, + ); + view.setContentHuggingPriority_forOrientation_( + NSLayoutPriorityDefaultHigh, + cocoa::NSLayoutConstraintOrientationVertical, + ); + } + + // Set layout and writing direction based on RTL. + unsafe { + view.setUserInterfaceLayoutDirection_(if self.rtl { + cocoa::NSUserInterfaceLayoutDirectionRightToLeft + } else { + cocoa::NSUserInterfaceLayoutDirectionLeftToRight + }); + if let Ok(control) = cocoa::NSControl::try_from(view) { + control.setBaseWritingDirection_(if self.rtl { + cocoa::NSWritingDirectionRightToLeft + } else { + cocoa::NSWritingDirectionLeftToRight + }); + } + } + + let lmg = unsafe { self.parent.layoutMarginsGuide() }; + + if !matches!(style.horizontal_alignment, Alignment::Fill) { + if let Some(size) = style.horizontal_size_request { + unsafe { + view.widthAnchor() + .constraintGreaterThanOrEqualToConstant_(size as _) + .setActive_(runtime::YES); + } + } + } + + if !self.ignore_horizontal { + unsafe { + let la = view.leadingAnchor(); + let ta = view.trailingAnchor(); + let pla = lmg.leadingAnchor(); + let pta = lmg.trailingAnchor(); + match style.horizontal_alignment { + Alignment::Fill => { + constraint_equal(la, pla); + constraint_equal(ta, pta); + // Without the autoresizing mask set, Text within Scroll doesn't display + // properly (it shrinks to 0-width, likely due to some specific interaction + // of NSScrollView with autolayout). + view.setAutoresizingMask_(cocoa::NSViewWidthSizable); + } + Alignment::Start => { + constraint_equal(la, pla); + } + Alignment::Center => { + let ca = view.centerXAnchor(); + let pca = lmg.centerXAnchor(); + constraint_equal(ca, pca); + } + Alignment::End => { + constraint_equal(ta, pta); + } + } + } + } + + if !matches!(style.vertical_alignment, Alignment::Fill) { + if let Some(size) = style.vertical_size_request { + unsafe { + view.heightAnchor() + .constraintGreaterThanOrEqualToConstant_(size as _) + .setActive_(runtime::YES); + } + } + } + + if !self.ignore_vertical { + unsafe { + let ta = view.topAnchor(); + let ba = view.bottomAnchor(); + let pta = lmg.topAnchor(); + let pba = lmg.bottomAnchor(); + match style.vertical_alignment { + Alignment::Fill => { + constraint_equal(ta, pta); + constraint_equal(ba, pba); + // Set the autoresizing mask to be consistent with the horizontal settings + // (see the comment there as to why it's necessary). + view.setAutoresizingMask_(cocoa::NSViewHeightSizable); + } + Alignment::Start => { + constraint_equal(ta, pta); + } + Alignment::Center => { + let ca = view.centerYAnchor(); + let pca = lmg.centerYAnchor(); + constraint_equal(ca, pca); + } + Alignment::End => { + constraint_equal(ba, pba); + } + } + } + } + + match &style.visible { + Property::Static(ref v) => { + unsafe { view.setHidden_((!v).into()) }; + } + Property::Binding(b) => { + b.on_change(move |&visible| unsafe { + view.setHidden_((!visible).into()); + }); + unsafe { view.setHidden_((!*b.borrow()).into()) }; + } + Property::ReadOnly(_) => { + unimplemented!("ElementStyle::visible doesn't support ReadOnly") + } + } + + if let Ok(control) = cocoa::NSControl::try_from(view) { + match &style.enabled { + Property::Static(e) => { + unsafe { control.setEnabled_((*e).into()) }; + } + Property::Binding(b) => { + b.on_change(move |&enabled| unsafe { + control.setEnabled_(enabled.into()); + }); + unsafe { control.setEnabled_((*b.borrow()).into()) }; + } + Property::ReadOnly(_) => { + unimplemented!("ElementStyle::enabled doesn't support ReadOnly") + } + } + } else if let Ok(text) = cocoa::NSText::try_from(view) { + let normally_editable = unsafe { text.isEditable() } == runtime::YES; + let normally_selectable = unsafe { text.isSelectable() } == runtime::YES; + let set_enabled = move |enabled: bool| unsafe { + if !enabled { + let mut range = text.selectedRange(); + range.length = 0; + text.setSelectedRange_(range); + } + text.setEditable_((enabled && normally_editable).into()); + text.setSelectable_((enabled && normally_selectable).into()); + text.setBackgroundColor_(if enabled { + cocoa::NSColor::textBackgroundColor() + } else { + cocoa::NSColor::windowBackgroundColor() + }); + text.setTextColor_(if enabled { + cocoa::NSColor::textColor() + } else { + cocoa::NSColor::disabledControlTextColor() + }); + }; + match &style.enabled { + Property::Static(e) => set_enabled(*e), + Property::Binding(b) => { + b.on_change(move |&enabled| set_enabled(enabled)); + set_enabled(*b.borrow()); + } + Property::ReadOnly(_) => { + unimplemented!("ElementStyle::enabled doesn't support ReadOnly") + } + } + } + + unsafe { view.setNeedsDisplay_(runtime::YES) }; + + true + } +} + +fn render_element( + element_type: model::ElementType, + style: &model::ElementStyle, + rtl: bool, +) -> Option<cocoa::NSView> { + use model::ElementType::*; + Some(match element_type { + VBox(model::VBox { items, spacing }) => { + let sv = unsafe { StrongRef::new(cocoa::NSStackView::alloc()) }.autorelease(); + unsafe { + sv.init(); + sv.setOrientation_(cocoa::NSUserInterfaceLayoutOrientationVertical); + sv.setAlignment_(cocoa::NSLayoutAttributeLeading); + sv.setSpacing_(spacing as _); + if style.vertical_alignment != Alignment::Fill { + // Make sure the vbox stays as small as its content. + sv.setHuggingPriority_forOrientation_( + NSLayoutPriorityDefaultHigh, + cocoa::NSLayoutConstraintOrientationVertical, + ); + } + } + let renderer = ViewRenderer::new(rtl, sv, |parent, style, child| { + let gravity: cocoa::NSInteger = match style.vertical_alignment { + Alignment::Start | Alignment::Fill => 1, + Alignment::Center => 2, + Alignment::End => 3, + }; + let parent: cocoa::NSStackView = parent.try_into().unwrap(); + unsafe { parent.addView_inGravity_(child, gravity) }; + }) + .ignore_vertical(true); + for item in items { + renderer.render(item); + } + sv.into() + } + HBox(model::HBox { + mut items, + spacing, + affirmative_order, + }) => { + if affirmative_order { + items.reverse(); + } + let sv = unsafe { StrongRef::new(cocoa::NSStackView::alloc()) }.autorelease(); + unsafe { + sv.init(); + sv.setOrientation_(cocoa::NSUserInterfaceLayoutOrientationHorizontal); + sv.setAlignment_(cocoa::NSLayoutAttributeTop); + sv.setSpacing_(spacing as _); + if style.horizontal_alignment != Alignment::Fill { + // Make sure the hbox stays as small as its content. + sv.setHuggingPriority_forOrientation_( + NSLayoutPriorityDefaultHigh, + cocoa::NSLayoutConstraintOrientationHorizontal, + ); + } + } + let renderer = ViewRenderer::new(rtl, sv, |parent, style, child| { + let gravity: cocoa::NSInteger = match style.horizontal_alignment { + Alignment::Start | Alignment::Fill => 1, + Alignment::Center => 2, + Alignment::End => 3, + }; + let parent: cocoa::NSStackView = parent.try_into().unwrap(); + unsafe { parent.addView_inGravity_(child, gravity) }; + }) + .ignore_horizontal(true); + for item in items { + renderer.render(item); + } + sv.into() + } + Button(mut b) => { + if let Some(Label(model::Label { + text: Property::Static(text), + .. + })) = b.content.take().map(|e| e.element_type) + { + let button = self::Button { element: b }.with_title(text.as_str()); + button.into() + } else { + return None; + } + } + Checkbox(cb) => { + let button = self::Checkbox { element: cb }.into_button(); + button.into() + } + Label(model::Label { text, bold }) => { + let tf = cocoa::NSTextField(unsafe { + cocoa::NSTextField::wrappingLabelWithString_(nsstring("")) + }); + unsafe { tf.setSelectable_(runtime::NO) }; + if bold { + unsafe { tf.setFont_(cocoa::NSFont::boldSystemFontOfSize_(0.0)) }; + } + match text { + Property::Static(text) => { + unsafe { tf.setStringValue_(nsstring(text.as_str())) }; + } + Property::Binding(b) => { + unsafe { tf.setStringValue_(nsstring(b.borrow().as_str())) }; + b.on_change(move |s| unsafe { tf.setStringValue_(nsstring(s)) }); + } + Property::ReadOnly(_) => unimplemented!("ReadOnly not supported for Label::text"), + } + tf.into() + } + Progress(model::Progress { amount }) => { + fn update(progress: cocoa::NSProgressIndicator, value: Option<f32>) { + unsafe { + match value { + None => { + progress.setIndeterminate_(runtime::YES); + progress.startAnimation_(progress.0); + } + Some(v) => { + progress.setDoubleValue_(v as f64); + progress.setIndeterminate_(runtime::NO); + } + } + } + } + + let progress = unsafe { StrongRef::new(cocoa::NSProgressIndicator::alloc()) }; + unsafe { + progress.init(); + progress.setMinValue_(0.0); + progress.setMaxValue_(1.0); + } + match amount { + Property::Static(v) => update(*progress, v), + Property::Binding(s) => { + update(*progress, *s.borrow()); + let weak = progress.weak(); + s.on_change(move |v| { + if let Some(r) = weak.lock() { + update(*r, *v); + } + }); + } + Property::ReadOnly(_) => (), + } + progress.autorelease().into() + } + Scroll(model::Scroll { content }) => { + let sv = unsafe { StrongRef::new(cocoa::NSScrollView::alloc()) }.autorelease(); + unsafe { + sv.init(); + sv.setHasVerticalScroller_(runtime::YES); + } + if let Some(content) = content { + ViewRenderer::new_with_selector(rtl, sv, sel!(setDocumentView:)) + .ignore_vertical(true) + .render(*content); + } + sv.into() + } + TextBox(model::TextBox { + placeholder, + content, + editable, + }) => { + let tv: StrongRef<cocoa::NSTextView> = TextView.into_object().cast(); + unsafe { + tv.setEditable_(editable.into()); + + cocoa::NSTextView_NSSharing::setAllowsUndo_(&*tv, runtime::YES); + tv.setVerticallyResizable_(runtime::YES); + if rtl { + let ps = StrongRef::new(cocoa::NSMutableParagraphStyle::alloc()); + ps.init(); + ps.setAlignment_(cocoa::NSTextAlignmentRight); + // We don't `use cocoa::NSTextView_NSSharing` because it has some methods which + // conflict with others that make it inconvenient. + cocoa::NSTextView_NSSharing::setDefaultParagraphStyle_(&*tv, (*ps).into()); + } + { + let container = tv.textContainer(); + container.setSize_(cocoa::NSSize { + width: f64::MAX, + height: f64::MAX, + }); + container.setWidthTracksTextView_(runtime::YES); + } + if let Some(placeholder) = placeholder { + // It's unclear why dictionaryWithObject_forKey_ takes `u64` rather than `id` + // arguments. + let attrs = cocoa::NSDictionary( + <cocoa::NSDictionary as NSDictionary_NSDictionaryCreation< + cocoa::NSAttributedStringKey, + cocoa::id, + >>::dictionaryWithObject_forKey_( + cocoa::NSColor::placeholderTextColor().0 as u64, + cocoa::NSForegroundColorAttributeName.0 as u64, + ), + ); + let string = StrongRef::new(cocoa::NSAttributedString( + cocoa::NSAttributedString::alloc() + .initWithString_attributes_(nsstring(placeholder.as_str()), attrs), + )); + // XXX: `setPlaceholderAttributedString` is undocumented (discovered at + // https://stackoverflow.com/a/43028577 and works identically to NSTextField), + // though hopefully it will be exposed in a public API some day. + tv.performSelector_withObject_(sel!(setPlaceholderAttributedString:), string.0); + } + } + match content { + Property::Static(s) => unsafe { tv.setString_(nsstring(s.as_str())) }, + Property::ReadOnly(od) => { + let weak = tv.weak(); + od.register(move |s| { + if let Some(tv) = weak.lock() { + *s = read_nsstring(unsafe { tv.string() }); + } + }); + } + Property::Binding(b) => { + let weak = tv.weak(); + b.on_change(move |s| { + if let Some(tv) = weak.lock() { + unsafe { tv.setString_(nsstring(s.as_str())) }; + } + }); + unsafe { tv.setString_(nsstring(b.borrow().as_str())) }; + } + } + tv.autorelease().into() + } + }) +} diff --git a/toolkit/crashreporter/client/app/src/ui/macos/objc.rs b/toolkit/crashreporter/client/app/src/ui/macos/objc.rs new file mode 100644 index 0000000000..d4f3f1c419 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/ui/macos/objc.rs @@ -0,0 +1,242 @@ +/* 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/. */ + +//! Objective-C bindings and helpers. + +// Forward all exports from the `objc` crate. +pub use objc::*; + +/// An objc class instance which contains rust data `T`. +#[derive(Clone, Copy)] +#[repr(transparent)] +pub struct Objc<T> { + pub instance: cocoa::id, + _phantom: std::marker::PhantomData<*mut T>, +} + +impl<T> Objc<T> { + pub fn new(instance: cocoa::id) -> Self { + Objc { + instance, + _phantom: std::marker::PhantomData, + } + } + + pub fn data(&self) -> &T { + let data = *unsafe { (*self.instance).get_ivar::<usize>("rust_self") } as *mut T; + unsafe { &*data } + } + + pub fn data_mut(&mut self) -> &mut T { + let data = *unsafe { (*self.instance).get_ivar::<usize>("rust_self") } as *mut T; + unsafe { &mut *data } + } +} + +impl<T> std::ops::Deref for Objc<T> { + type Target = T; + fn deref(&self) -> &Self::Target { + self.data() + } +} + +impl<T> std::ops::DerefMut for Objc<T> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.data_mut() + } +} + +unsafe impl<T> Encode for Objc<T> { + fn encode() -> Encoding { + cocoa::id::encode() + } +} + +/// Wrapper to provide `Encode` for bindgen-generated types (bindgen should do this in the future). +#[repr(transparent)] +pub struct Ptr<T>(pub T); + +unsafe impl<T> Encode for Ptr<T> { + fn encode() -> Encoding { + cocoa::id::encode() + } +} + +/// A strong objective-c reference to `T`. +#[repr(transparent)] +pub struct StrongRef<T> { + ptr: rc::StrongPtr, + _phantom: std::marker::PhantomData<T>, +} + +impl<T> Clone for StrongRef<T> { + fn clone(&self) -> Self { + StrongRef { + ptr: self.ptr.clone(), + _phantom: self._phantom, + } + } +} + +impl<T> std::ops::Deref for StrongRef<T> { + type Target = T; + fn deref(&self) -> &Self::Target { + let obj: &cocoa::id = &*self.ptr; + unsafe { std::mem::transmute(obj) } + } +} + +impl<T> StrongRef<T> { + /// Assume the given pointer-wrapper is an already-retained strong reference. + /// + /// # Safety + /// The type _must_ be the same size as cocoa::id and contain only a cocoa::id. + pub unsafe fn new(v: T) -> Self { + std::mem::transmute_copy(&v) + } + + /// Retain the given pointer-wrapper. + /// + /// # Safety + /// The type _must_ be the same size as cocoa::id and contain only a cocoa::id. + #[allow(dead_code)] + pub unsafe fn retain(v: T) -> Self { + let obj: cocoa::id = std::mem::transmute_copy(&v); + StrongRef { + ptr: rc::StrongPtr::retain(obj), + _phantom: std::marker::PhantomData, + } + } + + pub fn autorelease(self) -> T { + let obj = self.ptr.autorelease(); + unsafe { std::mem::transmute_copy(&obj) } + } + + pub fn weak(&self) -> WeakRef<T> { + WeakRef { + ptr: self.ptr.weak(), + _phantom: std::marker::PhantomData, + } + } + + /// Unwrap the StrongRef value without affecting reference counts. + /// + /// This is the opposite of `new`. + #[allow(dead_code)] + pub fn unwrap(self: Self) -> T { + let v = unsafe { std::mem::transmute_copy(&self) }; + std::mem::forget(self); + v + } + + /// Cast to a base class. + /// + /// Bindgen pointer-wrappers have trival `From<Derived> for Base` implementations. + pub fn cast<U: From<T>>(self) -> StrongRef<U> { + StrongRef { + ptr: self.ptr, + _phantom: std::marker::PhantomData, + } + } +} + +/// A weak objective-c reference to `T`. +#[derive(Clone)] +#[repr(transparent)] +pub struct WeakRef<T> { + ptr: rc::WeakPtr, + _phantom: std::marker::PhantomData<T>, +} + +impl<T> WeakRef<T> { + pub fn lock(&self) -> Option<StrongRef<T>> { + let ptr = self.ptr.load(); + if ptr.is_null() { + None + } else { + Some(StrongRef { + ptr, + _phantom: std::marker::PhantomData, + }) + } + } +} + +/// A macro for creating an objc class. +/// +/// Classes _must_ be registered before use (`Objc<T>::register()`). +/// +/// Example: +/// ``` +/// struct Foo(u8); +/// +/// objc_class! { +/// impl Foo: NSObject { +/// #[sel(mySelector:)] +/// fn my_selector(&mut self, arg: u8) -> u8 { +/// self.0 + arg +/// } +/// } +/// } +/// +/// fn make_foo() -> StrongRef<Objc<Foo>> { +/// Foo(42).into_object() +/// } +/// ``` +/// +/// Call `T::into_object()` to create the objective-c class instance. +macro_rules! objc_class { + ( impl $name:ident : $base:ident $(<$($protocol:ident),+>)? { + $( + #[sel($($sel:tt)+)] + fn $mname:ident (&mut $self:ident $(, $argname:ident : $argtype:ty )*) $(-> $rettype:ty)? $body:block + )* + }) => { + impl Objc<$name> { + pub fn register() { + let mut decl = declare::ClassDecl::new(concat!("CR", stringify!($name)), class!($base)).expect(concat!("failed to declare ", stringify!($name), " class")); + $($(decl.add_protocol(runtime::Protocol::get(stringify!($protocol)).expect(concat!("failed to find ",stringify!($protocol)," protocol")));)+)? + decl.add_ivar::<usize>("rust_self"); + $({ + extern fn method_impl(obj: &mut runtime::Object, _: runtime::Sel $(, $argname: $argtype )*) $(-> $rettype)? { + Objc::<$name>::new(obj).$mname($($argname),*) + } + unsafe { + decl.add_method(sel!($($sel)+), method_impl as extern fn(&mut runtime::Object, runtime::Sel $(, $argname: $argtype )*) $(-> $rettype)?); + } + })* + { + extern fn dealloc_impl(obj: &runtime::Object, _: runtime::Sel) { + drop(unsafe { Box::from_raw(*obj.get_ivar::<usize>("rust_self") as *mut $name) }); + unsafe { + let _: () = msg_send![super(obj, class!(NSObject)), dealloc]; + } + } + unsafe { + decl.add_method(sel!(dealloc), dealloc_impl as extern fn(&runtime::Object, runtime::Sel)); + } + } + decl.register(); + } + + pub fn class() -> &'static runtime::Class { + runtime::Class::get(concat!("CR", stringify!($name))).expect("class not registered") + } + + $(fn $mname (&mut $self $(, $argname : $argtype )*) $(-> $rettype)? $body)* + } + + impl $name { + pub fn into_object(self) -> StrongRef<Objc<$name>> { + let obj: *mut runtime::Object = unsafe { msg_send![Objc::<Self>::class(), alloc] }; + unsafe { (*obj).set_ivar("rust_self", Box::into_raw(Box::new(self)) as usize) }; + let obj: *mut runtime::Object = unsafe { msg_send![obj, init] }; + unsafe { StrongRef::new(Objc::new(obj)) } + } + } + } +} + +pub(crate) use objc_class; diff --git a/toolkit/crashreporter/client/app/src/ui/macos/plist.rs b/toolkit/crashreporter/client/app/src/ui/macos/plist.rs new file mode 100644 index 0000000000..a5bbe0aa0a --- /dev/null +++ b/toolkit/crashreporter/client/app/src/ui/macos/plist.rs @@ -0,0 +1,44 @@ +/* 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/. */ + +//! The embedded Info.plist file. + +const DATA: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>English</string> + <key>CFBundleDisplayName</key> + <string>Crash Reporter</string> + <key>CFBundleExecutable</key> + <string>crashreporter</string> + <key>CFBundleIdentifier</key> + <string>org.mozilla.crashreporter</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>Crash Reporter</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleVersion</key> + <string>1.0</string> + <key>LSHasLocalizedDisplayName</key> + <true/> + <key>NSRequiresAquaSystemAppearance</key> + <false/> + <key>NSPrincipalClass</key> + <string>NSApplication</string> +</dict> +</plist>"#; + +const N: usize = DATA.len(); + +const PTR: *const [u8; N] = DATA.as_ptr() as *const [u8; N]; + +#[used] +#[link_section = "__TEXT,__info_plist"] +// # Safety +// The array pointer is created from `DATA` (a slice pointer) with `DATA.len()` as the length. +static PLIST: [u8; N] = unsafe { *PTR }; diff --git a/toolkit/crashreporter/client/app/src/ui/mod.rs b/toolkit/crashreporter/client/app/src/ui/mod.rs new file mode 100644 index 0000000000..8464b6a9b3 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/ui/mod.rs @@ -0,0 +1,295 @@ +/* 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/. */ + +//! The UI model, UI implementations, and functions using them. +//! +//! UIs must implement: +//! * a `fn run_loop(&self, app: model::Application)` method which should display the UI and block while +//! handling events until the application terminates, +//! * a `fn invoke(&self, f: model::InvokeFn)` method which invokes the given function +//! asynchronously (without blocking) on the UI loop thread. + +use crate::std::{rc::Rc, sync::Arc}; +use crate::{ + async_task::AsyncTask, config::Config, data, logic::ReportCrash, settings::Settings, std, + thread_bound::ThreadBound, +}; +use model::{ui, Application}; +use ui_impl::UI; + +mod model; + +#[cfg(all(not(test), any(target_os = "linux", target_os = "windows")))] +mod icon { + // Must be DWORD-aligned for Win32 CreateIconFromResource. + #[repr(align(4))] + struct Aligned<Bytes: ?Sized>(Bytes); + static PNG_DATA_ALIGNMENT: &'static Aligned<[u8]> = + &Aligned(*include_bytes!("crashreporter.png")); + pub static PNG_DATA: &'static [u8] = &PNG_DATA_ALIGNMENT.0; +} + +#[cfg(test)] +pub mod test { + pub mod model { + pub use crate::ui::model::*; + } +} + +cfg_if::cfg_if! { + if #[cfg(test)] { + #[path = "test.rs"] + pub mod ui_impl; + } else if #[cfg(target_os = "linux")] { + #[path = "gtk.rs"] + mod ui_impl; + } else if #[cfg(target_os = "windows")] { + #[path = "windows/mod.rs"] + mod ui_impl; + } else if #[cfg(target_os = "macos")] { + #[path = "macos/mod.rs"] + mod ui_impl; + } else { + mod ui_impl { + #[derive(Default)] + pub struct UI; + + impl UI { + pub fn run_loop(&self, _app: super::model::Application) { + unimplemented!(); + } + + pub fn invoke(&self, _f: super::model::InvokeFn) { + unimplemented!(); + } + } + } + } +} + +/// Display an error dialog with the given message. +#[cfg_attr(mock, allow(unused))] +pub fn error_dialog<M: std::fmt::Display>(config: &Config, message: M) { + let close = data::Event::default(); + // Config may not have localized strings + let string_or = |name, fallback: &str| { + if config.strings.is_none() { + fallback.into() + } else { + config.string(name) + } + }; + + let details = if config.strings.is_none() { + format!("Details: {}", message) + } else { + config + .build_string("crashreporter-error-details") + .arg("details", message.to_string()) + .get() + }; + + let window = ui! { + Window title(string_or("crashreporter-branded-title", "Firefox Crash Reporter")) hsize(600) vsize(400) + close_when(&close) halign(Alignment::Fill) valign(Alignment::Fill) { + VBox margin(10) spacing(10) halign(Alignment::Fill) valign(Alignment::Fill) { + Label text(string_or( + "crashreporter-error", + "The application had a problem and crashed. \ + Unfortunately, the crash reporter is unable to submit a report for the crash." + )), + Label text(details), + Button["close"] halign(Alignment::End) on_click(move || close.fire(&())) { + Label text(string_or("crashreporter-button-close", "Close")) + } + } + } + }; + + UI::default().run_loop(Application { + windows: vec![window], + rtl: config.is_rtl(), + }); +} + +#[derive(Default, Debug, PartialEq, Eq)] +pub enum SubmitState { + #[default] + Initial, + InProgress, + Success, + Failure, +} + +/// The UI for the main crash reporter windows. +pub struct ReportCrashUI { + state: Arc<ThreadBound<ReportCrashUIState>>, + ui: Arc<UI>, + config: Arc<Config>, + logic: Rc<AsyncTask<ReportCrash>>, +} + +/// The state of the creash UI. +pub struct ReportCrashUIState { + pub send_report: data::Synchronized<bool>, + pub include_address: data::Synchronized<bool>, + pub show_details: data::Synchronized<bool>, + pub details: data::Synchronized<String>, + pub comment: data::OnDemand<String>, + pub submit_state: data::Synchronized<SubmitState>, + pub close_window: data::Event<()>, +} + +impl ReportCrashUI { + pub fn new( + initial_settings: &Settings, + config: Arc<Config>, + logic: AsyncTask<ReportCrash>, + ) -> Self { + let send_report = data::Synchronized::new(initial_settings.submit_report); + let include_address = data::Synchronized::new(initial_settings.include_url); + + ReportCrashUI { + state: Arc::new(ThreadBound::new(ReportCrashUIState { + send_report, + include_address, + show_details: Default::default(), + details: Default::default(), + comment: Default::default(), + submit_state: Default::default(), + close_window: Default::default(), + })), + ui: Default::default(), + config, + logic: Rc::new(logic), + } + } + + pub fn async_task(&self) -> AsyncTask<ReportCrashUIState> { + let state = self.state.clone(); + let ui = Arc::downgrade(&self.ui); + AsyncTask::new(move |f| { + let Some(ui) = ui.upgrade() else { return }; + ui.invoke(Box::new(cc! { (state) move || { + f(state.borrow()); + }})); + }) + } + + pub fn run(&self) { + let ReportCrashUI { + state, + ui, + config, + logic, + } = self; + let ReportCrashUIState { + send_report, + include_address, + show_details, + details, + comment, + submit_state, + close_window, + } = state.borrow(); + + send_report.on_change(cc! { (logic) move |v| { + let v = *v; + logic.push(move |s| s.settings.borrow_mut().submit_report = v); + }}); + include_address.on_change(cc! { (logic) move |v| { + let v = *v; + logic.push(move |s| s.settings.borrow_mut().include_url = v); + }}); + + let input_enabled = submit_state.mapped(|s| s == &SubmitState::Initial); + let send_report_and_input_enabled = + data::Synchronized::join(send_report, &input_enabled, |s, e| *s && *e); + + let submit_status_text = submit_state.mapped(cc! { (config) move |s| { + config.string(match s { + SubmitState::Initial => "crashreporter-submit-status", + SubmitState::InProgress => "crashreporter-submit-in-progress", + SubmitState::Success => "crashreporter-submit-success", + SubmitState::Failure => "crashreporter-submit-failure", + }) + }}); + + let progress_visible = submit_state.mapped(|s| s == &SubmitState::InProgress); + + let details_window = ui! { + Window["crash-details-window"] title(config.string("crashreporter-view-report-title")) + visible(show_details) modal(true) hsize(600) vsize(400) + halign(Alignment::Fill) valign(Alignment::Fill) + { + VBox margin(10) spacing(10) halign(Alignment::Fill) valign(Alignment::Fill) { + Scroll halign(Alignment::Fill) valign(Alignment::Fill) { + TextBox["details-text"] content(details) halign(Alignment::Fill) valign(Alignment::Fill) + }, + Button["close-details"] halign(Alignment::End) on_click(cc! { (show_details) move || *show_details.borrow_mut() = false }) { + Label text(config.string("crashreporter-button-ok")) + } + } + } + }; + + let main_window = ui! { + Window title(config.string("crashreporter-branded-title")) hsize(600) vsize(400) + halign(Alignment::Fill) valign(Alignment::Fill) close_when(close_window) + child_window(details_window) + { + VBox margin(10) spacing(10) halign(Alignment::Fill) valign(Alignment::Fill) { + Label text(config.string("crashreporter-apology")) bold(true), + Label text(config.string("crashreporter-crashed-and-restore")), + Label text(config.string("crashreporter-plea")), + Checkbox["send"] checked(send_report) label(config.string("crashreporter-send-report")) + enabled(&input_enabled), + VBox margin_start(20) spacing(5) halign(Alignment::Fill) valign(Alignment::Fill) { + Button["details"] enabled(&send_report_and_input_enabled) on_click(cc! { (config, details, show_details, logic) move || { + // Immediately display the window to feel responsive, even if forming + // the details string takes a little while (it really shouldn't + // though). + *details.borrow_mut() = config.string("crashreporter-loading-details"); + logic.push(|s| s.update_details()); + *show_details.borrow_mut() = true; + }}) + { + Label text(config.string("crashreporter-button-details")) + }, + Scroll halign(Alignment::Fill) valign(Alignment::Fill) { + TextBox["comment"] placeholder(config.string("crashreporter-comment-prompt")) + content(comment) + editable(true) + enabled(&send_report_and_input_enabled) + halign(Alignment::Fill) valign(Alignment::Fill) + }, + Checkbox["include-url"] checked(include_address) + label(config.string("crashreporter-include-url")) enabled(&send_report_and_input_enabled), + Label text(&submit_status_text) margin_top(20), + Progress halign(Alignment::Fill) visible(&progress_visible), + }, + HBox valign(Alignment::End) halign(Alignment::End) spacing(10) affirmative_order(true) + { + Button["restart"] visible(config.restart_command.is_some()) + on_click(cc! { (logic) move || logic.push(|s| s.restart()) }) + enabled(&input_enabled) hsize(160) + { + Label text(config.string("crashreporter-button-restart")) + }, + Button["quit"] on_click(cc! { (logic) move || logic.push(|s| s.quit()) }) + enabled(&input_enabled) hsize(160) + { + Label text(config.string("crashreporter-button-quit")) + } + } + } + } + }; + + ui.run_loop(Application { + windows: vec![main_window], + rtl: config.is_rtl(), + }); + } +} diff --git a/toolkit/crashreporter/client/app/src/ui/model/button.rs b/toolkit/crashreporter/client/app/src/ui/model/button.rs new file mode 100644 index 0000000000..d522fad6fc --- /dev/null +++ b/toolkit/crashreporter/client/app/src/ui/model/button.rs @@ -0,0 +1,26 @@ +/* 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/. */ + +use super::{Element, ElementBuilder}; +use crate::data::Event; + +/// A clickable button. +#[derive(Default, Debug)] +pub struct Button { + pub content: Option<Box<Element>>, + pub click: Event<()>, +} + +impl ElementBuilder<Button> { + pub fn on_click<F>(&mut self, f: F) + where + F: Fn() + 'static, + { + self.element_type.click.subscribe(move |_| f()); + } + + pub fn add_child(&mut self, child: Element) { + Self::single_child(&mut self.element_type.content, child); + } +} diff --git a/toolkit/crashreporter/client/app/src/ui/model/checkbox.rs b/toolkit/crashreporter/client/app/src/ui/model/checkbox.rs new file mode 100644 index 0000000000..8923e33558 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/ui/model/checkbox.rs @@ -0,0 +1,22 @@ +/* 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/. */ + +use crate::data::Property; + +/// A checkbox (with optional label). +#[derive(Default, Debug)] +pub struct Checkbox { + pub checked: Property<bool>, + pub label: Option<String>, +} + +impl super::ElementBuilder<Checkbox> { + pub fn checked(&mut self, value: impl Into<Property<bool>>) { + self.element_type.checked = value.into(); + } + + pub fn label<S: Into<String>>(&mut self, label: S) { + self.element_type.label = Some(label.into()); + } +} diff --git a/toolkit/crashreporter/client/app/src/ui/model/hbox.rs b/toolkit/crashreporter/client/app/src/ui/model/hbox.rs new file mode 100644 index 0000000000..b6c0e27e8c --- /dev/null +++ b/toolkit/crashreporter/client/app/src/ui/model/hbox.rs @@ -0,0 +1,34 @@ +/* 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/. */ + +use super::{Element, ElementBuilder}; + +/// A box which lays out contents horizontally. +#[derive(Default, Debug)] +pub struct HBox { + pub items: Vec<Element>, + pub spacing: u32, + pub affirmative_order: bool, +} + +impl ElementBuilder<HBox> { + pub fn spacing(&mut self, value: u32) { + self.element_type.spacing = value; + } + + /// Whether children are in affirmative order (and should be reordered based on platform + /// conventions). + /// + /// The children passed to `add_child` should be in most-affirmative to least-affirmative order + /// (e.g., "OK" then "Cancel" buttons). + /// + /// This is mainly useful for dialog buttons. + pub fn affirmative_order(&mut self, value: bool) { + self.element_type.affirmative_order = value; + } + + pub fn add_child(&mut self, child: Element) { + self.element_type.items.push(child); + } +} diff --git a/toolkit/crashreporter/client/app/src/ui/model/label.rs b/toolkit/crashreporter/client/app/src/ui/model/label.rs new file mode 100644 index 0000000000..096ce022e3 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/ui/model/label.rs @@ -0,0 +1,22 @@ +/* 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/. */ + +use crate::data::Property; + +/// A text label. +#[derive(Debug, Default)] +pub struct Label { + pub text: Property<String>, + pub bold: bool, +} + +impl super::ElementBuilder<Label> { + pub fn text(&mut self, s: impl Into<Property<String>>) { + self.element_type.text = s.into(); + } + + pub fn bold(&mut self, value: bool) { + self.element_type.bold = value; + } +} diff --git a/toolkit/crashreporter/client/app/src/ui/model/mod.rs b/toolkit/crashreporter/client/app/src/ui/model/mod.rs new file mode 100644 index 0000000000..5ea2ddc59a --- /dev/null +++ b/toolkit/crashreporter/client/app/src/ui/model/mod.rs @@ -0,0 +1,344 @@ +/* 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/. */ + +//! The UI model. +//! +//! Model elements should generally be declared as types with all fields `pub` (to be accessed by +//! UI implementations), though accessor methods are acceptable if needed. An +//! `ElementBuilder<TYPE>` impl should be provided to create methods that will be used in the +//! [`ui!`] macro. The model types are accessible when being _consumed_ by a UI implementation, +//! whereas the `ElementBuilder` types are accessible when the model is being _created_. +//! +//! All elements should be listed in the `element_types!` macro in this file (note that [`Window`], +//! while an element, isn't listed here as it cannot be a child element). This populates the +//! `ElementType` enum and generates `From<Element>` for `ElementType`, and `TryFrom<ElementType>` +//! for the element (as well as reference `TryFrom`). +//! +//! The model is written to accommodate layout and text direction differences (e.g. for RTL +//! languages), and UI implementations are expected to account for this correctly. + +use crate::data::Property; +pub use button::Button; +pub use checkbox::Checkbox; +pub use hbox::HBox; +pub use label::Label; +pub use progress::Progress; +pub use scroll::Scroll; +pub use textbox::TextBox; +pub use vbox::VBox; +pub use window::Window; + +mod button; +mod checkbox; +mod hbox; +mod label; +mod progress; +mod scroll; +mod textbox; +mod vbox; +mod window; + +/// A GUI element, including general style attributes and a more specific type. +/// +/// `From<ElementBuilder<...>>` is implemented for all elements listed in `element_types!`. +#[derive(Debug)] +pub struct Element { + pub style: ElementStyle, + pub element_type: ElementType, +} + +// This macro creates the `ElementType` enum and corresponding `From<ElementBuilder>` impls for +// Element. The `ElementType` discriminants match the element type names. +macro_rules! element_types { + ( $($name:ident),* ) => { + /// A type of GUI element. + #[derive(Debug)] + pub enum ElementType { + $($name($name)),* + } + + $( + impl From<$name> for ElementType { + fn from(e: $name) -> ElementType { + ElementType::$name(e) + } + } + + impl TryFrom<ElementType> for $name { + type Error = &'static str; + + fn try_from(et: ElementType) -> Result<Self, Self::Error> { + if let ElementType::$name(v) = et { + Ok(v) + } else { + Err(concat!("ElementType was not ", stringify!($name))) + } + } + } + + impl<'a> TryFrom<&'a ElementType> for &'a $name { + type Error = &'static str; + + fn try_from(et: &'a ElementType) -> Result<Self, Self::Error> { + if let ElementType::$name(v) = et { + Ok(v) + } else { + Err(concat!("ElementType was not ", stringify!($name))) + } + } + } + + impl From<ElementBuilder<$name>> for Element { + fn from(b: ElementBuilder<$name>) -> Self { + Element { + style: b.style, + element_type: b.element_type.into(), + } + } + } + )* + } +} +element_types! { + Button, Checkbox, HBox, Label, Progress, Scroll, TextBox, VBox +} + +/// Common element style values. +#[derive(Debug)] +pub struct ElementStyle { + pub horizontal_alignment: Alignment, + pub vertical_alignment: Alignment, + pub horizontal_size_request: Option<u32>, + pub vertical_size_request: Option<u32>, + pub margin: Margin, + pub visible: Property<bool>, + pub enabled: Property<bool>, + #[cfg(test)] + pub id: Option<String>, +} + +impl Default for ElementStyle { + fn default() -> Self { + ElementStyle { + horizontal_alignment: Default::default(), + vertical_alignment: Default::default(), + horizontal_size_request: Default::default(), + vertical_size_request: Default::default(), + margin: Default::default(), + visible: true.into(), + enabled: true.into(), + #[cfg(test)] + id: Default::default(), + } + } +} + +/// A builder for `Element`s. +/// +/// Each element should add an `impl ElementBuilder<TYPE>` to add methods to their builder. +#[derive(Debug, Default)] +pub struct ElementBuilder<T> { + pub style: ElementStyle, + pub element_type: T, +} + +impl<T> ElementBuilder<T> { + /// Set horizontal alignment. + pub fn halign(&mut self, alignment: Alignment) { + self.style.horizontal_alignment = alignment; + } + + /// Set vertical alignment. + pub fn valign(&mut self, alignment: Alignment) { + self.style.vertical_alignment = alignment; + } + + /// Set the horizontal size request. + pub fn hsize(&mut self, value: u32) { + assert!(value <= i32::MAX as u32); + self.style.horizontal_size_request = Some(value); + } + + /// Set the vertical size request. + pub fn vsize(&mut self, value: u32) { + assert!(value <= i32::MAX as u32); + self.style.vertical_size_request = Some(value); + } + + /// Set start margin. + pub fn margin_start(&mut self, amount: u32) { + self.style.margin.start = amount; + } + + /// Set end margin. + pub fn margin_end(&mut self, amount: u32) { + self.style.margin.end = amount; + } + + /// Set start and end margins. + pub fn margin_horizontal(&mut self, amount: u32) { + self.margin_start(amount); + self.margin_end(amount) + } + + /// Set top margin. + pub fn margin_top(&mut self, amount: u32) { + self.style.margin.top = amount; + } + + /// Set bottom margin. + pub fn margin_bottom(&mut self, amount: u32) { + self.style.margin.bottom = amount; + } + + /// Set top and bottom margins. + pub fn margin_vertical(&mut self, amount: u32) { + self.margin_top(amount); + self.margin_bottom(amount) + } + + /// Set all margins. + pub fn margin(&mut self, amount: u32) { + self.margin_horizontal(amount); + self.margin_vertical(amount) + } + + /// Set visibility. + pub fn visible(&mut self, value: impl Into<Property<bool>>) { + self.style.visible = value.into(); + } + + /// Set whether an element is enabled. + /// + /// This generally should enable/disable interaction with an element. + pub fn enabled(&mut self, value: impl Into<Property<bool>>) { + self.style.enabled = value.into(); + } + + /// Set the element identifier. + #[cfg(test)] + pub fn id(&mut self, value: impl Into<String>) { + self.style.id = Some(value.into()); + } + + /// Set the element identifier (stub). + #[cfg(not(test))] + pub fn id(&mut self, _value: impl Into<String>) {} + + fn single_child(slot: &mut Option<Box<Element>>, child: Element) { + if slot.replace(Box::new(child)).is_some() { + panic!("{} can only have one child", std::any::type_name::<T>()); + } + } +} + +/// A typed [`Element`]. +/// +/// This is useful for the [`ui!`] macro when a method should accept a specific element type, since +/// the macro always creates [`ElementBuilder<T>`](ElementBuilder) and ends with a `.into()` (and this implements +/// `From<ElementBuilder<T>>`). +#[derive(Debug, Default)] +pub struct TypedElement<T> { + pub style: ElementStyle, + pub element_type: T, +} + +impl<T> From<ElementBuilder<T>> for TypedElement<T> { + fn from(b: ElementBuilder<T>) -> Self { + TypedElement { + style: b.style, + element_type: b.element_type, + } + } +} + +/// The alignment of an element in one direction. +/// +/// Note that rather than `Left`/`Right`, this class has `Start`/`End` as it is meant to be +/// layout-direction-aware. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +#[allow(dead_code)] +pub enum Alignment { + /// Align to the start of the direction. + #[default] + Start, + /// Align to the center of the direction. + Center, + /// Align to the end of the direction. + End, + /// Fill all available space. + Fill, +} + +/// The margins of an element. +/// +/// These are RTL-aware: for instance, `start` is the left margin in left-to-right languages and +/// the right margin in right-to-left languages. +#[derive(Default, Debug)] +pub struct Margin { + pub start: u32, + pub end: u32, + pub top: u32, + pub bottom: u32, +} + +/// A macro to allow a convenient syntax for creating elements. +/// +/// The macro expects the following syntax: +/// ``` +/// ElementTypeName some_method(arg1, arg2) other_method() { +/// Child ..., +/// Child2 ... +/// } +/// ``` +/// +/// The type is wrapped in an `ElementBuilder`, and methods are called on this builder with a +/// mutable reference. This means that element types must implement Default and must implement +/// builder methods on `ElementBuilder<ElementTypeName>`. The children block is optional, and calls +/// `add_child(child: Element)` for each provided child (so implement this method if desired). +/// +/// For testing, a string identifier can be set on any element with a `["my_identifier"]` following +/// the element type. +macro_rules! ui { + ( $el:ident + $([ $id:literal ])? + $( $method:ident $methodargs:tt )* + $({ $($contents:tt)* })? + ) => { + { + #[allow(unused_imports)] + use $crate::ui::model::*; + let mut el: ElementBuilder<$el> = Default::default(); + $( el.id($id); )? + $( el.$method $methodargs ; )* + $( ui! { @children (el) $($contents)* } )? + el.into() + } + }; + ( @children ($parent:expr) ) => {}; + ( @children ($parent:expr) + $el:ident + $([ $id:literal ])? + $( $method:ident $methodargs:tt )* + $({ $($contents:tt)* })? + $(, $($rest:tt)* )? + ) => { + $parent.add_child(ui!( $el $([$id])? $( $method $methodargs )* $({ $($contents)* })? )); + $(ui!( @children ($parent) $($rest)* ))? + }; +} + +pub(crate) use ui; + +/// An application, defined as a set of windows. +/// +/// When all windows are closed, the application is considered complete (and loops should exit). +pub struct Application { + pub windows: Vec<TypedElement<Window>>, + /// Whether the text direction should be right-to-left. + pub rtl: bool, +} + +/// A function to be invoked in the UI loop. +pub type InvokeFn = Box<dyn FnOnce() + Send + 'static>; diff --git a/toolkit/crashreporter/client/app/src/ui/model/progress.rs b/toolkit/crashreporter/client/app/src/ui/model/progress.rs new file mode 100644 index 0000000000..f3e4e4bf77 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/ui/model/progress.rs @@ -0,0 +1,19 @@ +/* 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/. */ + +use crate::data::Property; + +/// A progress indicator. +#[derive(Debug, Default)] +pub struct Progress { + /// Progress between 0 and 1, or None if indeterminate. + pub amount: Property<Option<f32>>, +} + +impl super::ElementBuilder<Progress> { + #[allow(dead_code)] + pub fn amount(&mut self, value: impl Into<Property<Option<f32>>>) { + self.element_type.amount = value.into(); + } +} diff --git a/toolkit/crashreporter/client/app/src/ui/model/scroll.rs b/toolkit/crashreporter/client/app/src/ui/model/scroll.rs new file mode 100644 index 0000000000..47efa4a81e --- /dev/null +++ b/toolkit/crashreporter/client/app/src/ui/model/scroll.rs @@ -0,0 +1,17 @@ +/* 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/. */ + +use super::{Element, ElementBuilder}; + +/// A scrollable region. +#[derive(Debug, Default)] +pub struct Scroll { + pub content: Option<Box<Element>>, +} + +impl ElementBuilder<Scroll> { + pub fn add_child(&mut self, child: Element) { + Self::single_child(&mut self.element_type.content, child); + } +} diff --git a/toolkit/crashreporter/client/app/src/ui/model/textbox.rs b/toolkit/crashreporter/client/app/src/ui/model/textbox.rs new file mode 100644 index 0000000000..08cd9ca1bc --- /dev/null +++ b/toolkit/crashreporter/client/app/src/ui/model/textbox.rs @@ -0,0 +1,27 @@ +/* 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/. */ + +use crate::data::Property; + +/// A text box. +#[derive(Debug, Default)] +pub struct TextBox { + pub placeholder: Option<String>, + pub content: Property<String>, + pub editable: bool, +} + +impl super::ElementBuilder<TextBox> { + pub fn placeholder(&mut self, text: impl Into<String>) { + self.element_type.placeholder = Some(text.into()); + } + + pub fn content(&mut self, value: impl Into<Property<String>>) { + self.element_type.content = value.into(); + } + + pub fn editable(&mut self, value: bool) { + self.element_type.editable = value; + } +} diff --git a/toolkit/crashreporter/client/app/src/ui/model/vbox.rs b/toolkit/crashreporter/client/app/src/ui/model/vbox.rs new file mode 100644 index 0000000000..6f1b09b1e2 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/ui/model/vbox.rs @@ -0,0 +1,22 @@ +/* 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/. */ + +use super::{Element, ElementBuilder}; + +/// A box which lays out contents vertically. +#[derive(Debug, Default)] +pub struct VBox { + pub items: Vec<Element>, + pub spacing: u32, +} + +impl ElementBuilder<VBox> { + pub fn spacing(&mut self, value: u32) { + self.element_type.spacing = value; + } + + pub fn add_child(&mut self, child: Element) { + self.element_type.items.push(child); + } +} diff --git a/toolkit/crashreporter/client/app/src/ui/model/window.rs b/toolkit/crashreporter/client/app/src/ui/model/window.rs new file mode 100644 index 0000000000..b56071ca19 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/ui/model/window.rs @@ -0,0 +1,46 @@ +/* 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/. */ + +use super::{Element, ElementBuilder, TypedElement}; +use crate::data::Event; + +/// A window. +#[derive(Debug, Default)] +pub struct Window { + pub title: String, + /// The window content is the first element. + pub content: Option<Box<Element>>, + /// Logical child windows. + pub children: Vec<TypedElement<Self>>, + pub modal: bool, + pub close: Option<Event<()>>, +} + +impl ElementBuilder<Window> { + /// Set the window title. + pub fn title(&mut self, s: impl Into<String>) { + self.element_type.title = s.into(); + } + + /// Set whether the window is modal (blocking interaction with other windows when displayed). + pub fn modal(&mut self, value: bool) { + self.element_type.modal = value; + } + + /// Register an event to close the window. + pub fn close_when(&mut self, event: &Event<()>) { + self.element_type.close = Some(event.clone()); + } + + /// Add a window as a logical child of this one. + /// + /// Logical children are always displayed above their parents. + pub fn child_window(&mut self, window: TypedElement<Window>) { + self.element_type.children.push(window); + } + + pub fn add_child(&mut self, child: Element) { + Self::single_child(&mut self.element_type.content, child); + } +} diff --git a/toolkit/crashreporter/client/app/src/ui/test.rs b/toolkit/crashreporter/client/app/src/ui/test.rs new file mode 100644 index 0000000000..db98c072da --- /dev/null +++ b/toolkit/crashreporter/client/app/src/ui/test.rs @@ -0,0 +1,270 @@ +/* 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/. */ + +//! A renderer for use in tests, which doesn't actually render a GUI but allows programmatic +//! interaction. +//! +//! The [`ui!`](super::ui) macro supports labeling any element with a string identifier, which can +//! be used to access the element in this UI. +//! +//! The [`Interact`] hook must be created to interact with the test UI, before the UI is run and on +//! the same thread as the UI. +//! +//! See how this UI is used in [`crate::test`]. + +use super::model::{self, Application, Element}; +use std::cell::RefCell; +use std::collections::HashMap; +use std::sync::{ + atomic::{AtomicBool, AtomicU8, Ordering::Relaxed}, + mpsc, Arc, Condvar, Mutex, +}; + +thread_local! { + static INTERACT: RefCell<Option<Arc<State>>> = Default::default(); +} + +/// A test UI which allows access to the UI elements. +#[derive(Default)] +pub struct UI { + interface: Mutex<Option<UIInterface>>, +} + +impl UI { + pub fn run_loop(&self, app: Application) { + let (tx, rx) = mpsc::channel(); + let interface = UIInterface { work: tx }; + + let elements = id_elements(&app); + INTERACT.with(cc! { (interface) move |r| { + if let Some(state) = &*r.borrow() { + state.set_interface(interface); + } + }}); + *self.interface.lock().unwrap() = Some(interface.clone()); + + // Close the UI when the root windows are closed. + // Use a bitfield rather than a count in case the `close` event is fired multiple times. + assert!(app.windows.len() <= 8); + let mut windows = 0u8; + for i in 0..app.windows.len() { + windows |= 1 << i; + } + let windows = Arc::new(AtomicU8::new(windows)); + for (index, window) in app.windows.iter().enumerate() { + if let Some(c) = &window.element_type.close { + c.subscribe(cc! { (interface, windows) move |&()| { + let old = windows + .fetch_update(Relaxed, Relaxed, |x| Some(x & !(1u8 << index))) + .unwrap(); + if old == 1u8 << index { + interface.work.send(Command::Finish).unwrap(); + } + }}); + } else { + // No close event, so we must assume a closed state (and assume that _some_ window + // will have a close event registered so we don't drop the interface now). + windows + .fetch_update(Relaxed, Relaxed, |x| Some(x & !(1u8 << index))) + .unwrap(); + } + } + + while let Ok(f) = rx.recv() { + match f { + Command::Invoke(f) => f(), + Command::Interact(f) => f(&elements), + Command::Finish => break, + } + } + + *self.interface.lock().unwrap() = None; + INTERACT.with(|r| { + if let Some(state) = &*r.borrow() { + state.clear_interface(); + } + }); + } + + pub fn invoke(&self, f: model::InvokeFn) { + let guard = self.interface.lock().unwrap(); + if let Some(interface) = &*guard { + let _ = interface.work.send(Command::Invoke(f)); + } + } +} + +/// Test interaction hook. +#[derive(Clone)] +pub struct Interact { + state: Arc<State>, +} + +impl Interact { + /// Create an interaction hook for the test UI. + /// + /// This should be done before running the UI, and must be done on the same thread that + /// later runs it. + pub fn hook() -> Self { + let v = Interact { + state: Default::default(), + }; + { + let state = v.state.clone(); + INTERACT.with(move |r| *r.borrow_mut() = Some(state)); + } + v + } + + /// Wait for the render thread to be ready for interaction. + pub fn wait_for_ready(&self) { + let mut guard = self.state.interface.lock().unwrap(); + while guard.is_none() && !self.state.cancel.load(Relaxed) { + guard = self.state.waiting_for_interface.wait(guard).unwrap(); + } + } + + /// Cancel an Interact (which causes `wait_for_ready` to always return). + pub fn cancel(&self) { + self.state.cancel.store(true, Relaxed); + self.state.waiting_for_interface.notify_all(); + } + + /// Run the given function on the element with the given type and identity. + /// + /// Panics if either the id is missing or the type is incorrect. + pub fn element<'a, 'b, T: 'b, F, R>(&self, id: &'a str, f: F) -> R + where + &'b T: TryFrom<&'b model::ElementType>, + F: FnOnce(&model::ElementStyle, &T) -> R + Send + 'a, + R: Send + 'a, + { + self.interact(id, move |element: &IdElement| match element { + IdElement::Generic(e) => Some(f(&e.style, (&e.element_type).try_into().ok()?)), + IdElement::Window(_) => None, + }) + .expect("incorrect element type") + } + + /// Run the given function on the window with the given identity. + /// + /// Panics if the id is missing or the type is incorrect. + pub fn window<'a, F, R>(&self, id: &'a str, f: F) -> R + where + F: FnOnce(&model::ElementStyle, &model::Window) -> R + Send + 'a, + R: Send + 'a, + { + self.interact(id, move |element| match element { + IdElement::Window(e) => Some(f(&e.style, &e.element_type)), + IdElement::Generic(_) => None, + }) + .expect("incorrect element type") + } + + fn interact<'a, 'b, F, R>(&self, id: &'a str, f: F) -> R + where + F: FnOnce(&IdElement<'b>) -> R + Send + 'a, + R: Send + 'a, + { + let (send, recv) = std::sync::mpsc::sync_channel(0); + { + let f: Box<dyn FnOnce(&IdElements<'b>) + Send + 'a> = Box::new(move |elements| { + let _ = send.send(elements.get(id).map(f)); + }); + + // # Safety + // The function is run while `'a` is still valid (we wait here for it to complete). + let f: Box<dyn FnOnce(&IdElements) + Send + 'static> = + unsafe { std::mem::transmute(f) }; + + let guard = self.state.interface.lock().unwrap(); + let interface = guard.as_ref().expect("renderer is not running"); + let _ = interface.work.send(Command::Interact(f)); + } + recv.recv().unwrap().expect("failed to get element") + } +} + +#[derive(Clone)] +struct UIInterface { + work: mpsc::Sender<Command>, +} + +enum Command { + Invoke(Box<dyn FnOnce() + Send + 'static>), + Interact(Box<dyn FnOnce(&IdElements) + Send + 'static>), + Finish, +} + +enum IdElement<'a> { + Generic(&'a Element), + Window(&'a model::TypedElement<model::Window>), +} + +type IdElements<'a> = HashMap<String, IdElement<'a>>; + +#[derive(Default)] +struct State { + interface: Mutex<Option<UIInterface>>, + waiting_for_interface: Condvar, + cancel: AtomicBool, +} + +impl State { + /// Set the interface for the interaction client to use. + pub fn set_interface(&self, interface: UIInterface) { + *self.interface.lock().unwrap() = Some(interface); + self.waiting_for_interface.notify_all(); + } + + /// Clear the UI interface. + pub fn clear_interface(&self) { + *self.interface.lock().unwrap() = None; + } +} + +fn id_elements<'a>(app: &'a Application) -> IdElements<'a> { + let mut elements: IdElements<'a> = Default::default(); + + let mut windows_to_visit: Vec<_> = app.windows.iter().collect(); + + let mut to_visit: Vec<&'a Element> = Vec::new(); + while let Some(window) = windows_to_visit.pop() { + if let Some(id) = &window.style.id { + elements.insert(id.to_owned(), IdElement::Window(window)); + } + windows_to_visit.extend(&window.element_type.children); + + if let Some(content) = &window.element_type.content { + to_visit.push(content); + } + } + + while let Some(el) = to_visit.pop() { + if let Some(id) = &el.style.id { + elements.insert(id.to_owned(), IdElement::Generic(el)); + } + + use model::ElementType::*; + match &el.element_type { + Button(model::Button { + content: Some(content), + .. + }) + | Scroll(model::Scroll { + content: Some(content), + }) => { + to_visit.push(content); + } + VBox(model::VBox { items, .. }) | HBox(model::HBox { items, .. }) => { + for item in items { + to_visit.push(item) + } + } + _ => (), + } + } + + elements +} diff --git a/toolkit/crashreporter/client/app/src/ui/windows/font.rs b/toolkit/crashreporter/client/app/src/ui/windows/font.rs new file mode 100644 index 0000000000..3ec48316eb --- /dev/null +++ b/toolkit/crashreporter/client/app/src/ui/windows/font.rs @@ -0,0 +1,56 @@ +/* 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/. */ + +use windows_sys::Win32::{Foundation::S_OK, Graphics::Gdi, UI::Controls}; + +/// Windows font handle (`HFONT`). +pub struct Font(Gdi::HFONT); + +impl Font { + /// Get the system theme caption font. + /// + /// Panics if the font cannot be retrieved. + pub fn caption() -> Self { + unsafe { + let mut font = std::mem::zeroed::<Gdi::LOGFONTW>(); + success!(hresult + Controls::GetThemeSysFont(0, Controls::TMT_CAPTIONFONT as i32, &mut font) + ); + Font(success!(pointer Gdi::CreateFontIndirectW(&font))) + } + } + + /// Get the system theme bold caption font. + /// + /// Returns `None` if the font cannot be retrieved. + pub fn caption_bold() -> Option<Self> { + unsafe { + let mut font = std::mem::zeroed::<Gdi::LOGFONTW>(); + if Controls::GetThemeSysFont(0, Controls::TMT_CAPTIONFONT as i32, &mut font) != S_OK { + return None; + } + font.lfWeight = Gdi::FW_BOLD as i32; + + let ptr = Gdi::CreateFontIndirectW(&font); + if ptr == 0 { + return None; + } + Some(Font(ptr)) + } + } +} + +impl std::ops::Deref for Font { + type Target = Gdi::HFONT; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Drop for Font { + fn drop(&mut self) { + unsafe { Gdi::DeleteObject(self.0 as _) }; + } +} diff --git a/toolkit/crashreporter/client/app/src/ui/windows/gdi.rs b/toolkit/crashreporter/client/app/src/ui/windows/gdi.rs new file mode 100644 index 0000000000..89828987bc --- /dev/null +++ b/toolkit/crashreporter/client/app/src/ui/windows/gdi.rs @@ -0,0 +1,43 @@ +/* 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/. */ + +//! GDI helpers. + +use windows_sys::Win32::{ + Foundation::HWND, + Graphics::Gdi::{self, GDI_ERROR, HDC, HGDIOBJ}, +}; + +/// A GDI drawing context. +pub struct DC { + hwnd: HWND, + hdc: HDC, +} + +impl DC { + /// Create a new DC. + pub fn new(hwnd: HWND) -> Option<Self> { + let hdc = unsafe { Gdi::GetDC(hwnd) }; + (hdc != 0).then_some(DC { hwnd, hdc }) + } + + /// Call the given function with a gdi object selected. + pub fn with_object_selected<R>(&self, object: HGDIOBJ, f: impl FnOnce(HDC) -> R) -> Option<R> { + let old_object = unsafe { Gdi::SelectObject(self.hdc, object) }; + if old_object == 0 || old_object == GDI_ERROR as isize { + return None; + } + let ret = f(self.hdc); + // The prior object must be selected before releasing the DC. Ignore errors; this is + // best-effort. + unsafe { Gdi::SelectObject(self.hdc, old_object) }; + Some(ret) + } +} + +impl Drop for DC { + fn drop(&mut self) { + unsafe { Gdi::ReleaseDC(self.hwnd, self.hdc) }; + } +} diff --git a/toolkit/crashreporter/client/app/src/ui/windows/layout.rs b/toolkit/crashreporter/client/app/src/ui/windows/layout.rs new file mode 100644 index 0000000000..7563b6b2f0 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/ui/windows/layout.rs @@ -0,0 +1,436 @@ +/* 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/. */ + +//! Helpers for window layout. + +use super::{ + model::{self, Alignment, Element, ElementStyle, Margin}, + ElementRef, WideString, +}; +use crate::data::Property; +use std::collections::HashMap; +use windows_sys::Win32::{ + Foundation::{HWND, SIZE}, + Graphics::Gdi, + UI::WindowsAndMessaging as win, +}; + +pub(super) type ElementMapping = HashMap<ElementRef, HWND>; + +/// Handles the layout of windows. +/// +/// This is done in two passes. The first pass calculates the sizes for all elements in the tree. +/// Once sizes are known, the second pass can appropriately position the elements (taking alignment +/// into account). +/// +/// Currently, the resize/reposition logic is tied into the methods here, which is an inconvenient +/// design because when adding support for a new element type you have to add new information in +/// disparate locations. +pub struct Layout<'a> { + elements: &'a ElementMapping, + sizes: HashMap<ElementRef, Size>, + last_positioned: Option<HWND>, +} + +// Unfortunately, there's no good way to get these margins. I just guessed and the first guesses +// seemed to be close enough. +const BUTTON_MARGIN: Margin = Margin { + start: 5, + end: 5, + top: 5, + bottom: 5, +}; +const CHECKBOX_MARGIN: Margin = Margin { + start: 15, + end: 0, + top: 0, + bottom: 0, +}; + +impl<'a> Layout<'a> { + pub(super) fn new(elements: &'a ElementMapping) -> Self { + Layout { + elements, + sizes: Default::default(), + last_positioned: None, + } + } + + /// Perform a layout of the element and all child elements. + pub fn layout(mut self, element: &Element, max_width: u32, max_height: u32) { + let max_size = Size { + width: max_width, + height: max_height, + }; + self.resize(element, &max_size); + self.reposition(element, &Position::default(), &max_size); + } + + fn resize(&mut self, element: &Element, max_size: &Size) -> Size { + let style = &element.style; + + let mut inner_size = max_size.inner_size(style); + let mut content_size = None; + + if !is_visible(style) { + self.sizes + .insert(ElementRef::new(element), Default::default()); + return Default::default(); + } + + // Resize inner content. + // + // These cases should result in `content_size` being set, if relevant. + use model::ElementType::*; + match &element.element_type { + Button(model::Button { + content: Some(content), + .. + }) => { + // Special case for buttons with a label. + if let Label(model::Label { + text: Property::Static(text), + .. + }) = &content.element_type + { + let mut size = inner_size.less_margin(&BUTTON_MARGIN); + self.measure_text(text.as_str(), element, &mut size); + content_size = Some(size.plus_margin(&BUTTON_MARGIN)); + } + } + Checkbox(model::Checkbox { + label: Some(label), .. + }) => { + let mut size = inner_size.less_margin(&CHECKBOX_MARGIN); + self.measure_text(label.as_str(), element, &mut size); + content_size = Some(size.plus_margin(&CHECKBOX_MARGIN)); + } + Label(model::Label { text, bold: _ }) => { + let mut size = inner_size.clone(); + match text { + Property::Static(text) => self.measure_text(text.as_str(), element, &mut size), + Property::Binding(b) => { + self.measure_text(b.borrow().as_str(), element, &mut size) + } + Property::ReadOnly(_) => { + unimplemented!("Label::text does not support ReadOnly") + } + } + content_size = Some(size); + } + VBox(model::VBox { items, spacing }) => { + let mut height = 0; + let mut max_width = 0; + let mut remaining_size = inner_size.clone(); + let mut resize_child = |c| { + let child_size = self.resize(c, &remaining_size); + height += child_size.height; + max_width = std::cmp::max(child_size.width, max_width); + remaining_size.height = remaining_size + .height + .saturating_sub(child_size.height + spacing); + }; + // First resize all non-Fill items; Fill items get the remaining space. + for item in items + .iter() + .filter(|i| i.style.vertical_alignment != Alignment::Fill) + { + resize_child(item); + } + for item in items + .iter() + .filter(|i| i.style.vertical_alignment == Alignment::Fill) + { + resize_child(item); + } + content_size = Some(Size { + width: max_width, + height: height + spacing * (items.len().saturating_sub(1) as u32), + }); + } + HBox(model::HBox { + items, + spacing, + affirmative_order: _, + }) => { + let mut width = 0; + let mut max_height = 0; + let mut remaining_size = inner_size.clone(); + let mut resize_child = |c| { + let child_size = self.resize(c, &remaining_size); + width += child_size.width; + max_height = std::cmp::max(child_size.height, max_height); + remaining_size.width = remaining_size + .width + .saturating_sub(child_size.width + spacing); + }; + // First resize all non-Fill items; Fill items get the remaining space. + for item in items + .iter() + .filter(|i| i.style.horizontal_alignment != Alignment::Fill) + { + resize_child(item); + } + for item in items + .iter() + .filter(|i| i.style.horizontal_alignment == Alignment::Fill) + { + resize_child(item); + } + content_size = Some(Size { + width: width + spacing * (items.len().saturating_sub(1) as u32), + height: max_height, + }); + } + Scroll(model::Scroll { + content: Some(content), + }) => { + content_size = Some(self.resize(content, &inner_size)); + } + Progress(model::Progress { .. }) => { + // Min size recommended by windows uxguide + content_size = Some(Size { + width: 160, + height: 15, + }); + } + // We don't support sizing by textbox content yet (need to read from the HWND due to + // Property::ReadOnly). + TextBox(_) => (), + _ => (), + } + + // Adjust from content size. + if let Some(content_size) = content_size { + inner_size.from_content_size(style, &content_size); + } + + // Compute/store (outer) size and return. + let size = inner_size.plus_margin(&style.margin); + self.sizes.insert(ElementRef::new(element), size); + size + } + + fn get_size(&self, element: &Element) -> &Size { + self.sizes + .get(&ElementRef::new(element)) + .expect("element not resized") + } + + fn reposition(&mut self, element: &Element, position: &Position, parent_size: &Size) { + let style = &element.style; + if !is_visible(style) { + return; + } + let size = self.get_size(element); + + let start_offset = match style.horizontal_alignment { + Alignment::Fill | Alignment::Start => 0, + Alignment::Center => parent_size.width.saturating_sub(size.width) / 2, + Alignment::End => parent_size.width.saturating_sub(size.width), + }; + let top_offset = match style.vertical_alignment { + Alignment::Fill | Alignment::Start => 0, + Alignment::Center => parent_size.height.saturating_sub(size.height) / 2, + Alignment::End => parent_size.height.saturating_sub(size.height), + }; + + let inner_position = Position { + start: position.start + start_offset, + top: position.top + top_offset, + } + .less_margin(&style.margin); + let inner_size = size.less_margin(&style.margin); + + // Set the window size/position if there is a handle associated with the element. + if let Some(&hwnd) = self.elements.get(&ElementRef::new(element)) { + unsafe { + win::SetWindowPos( + hwnd, + self.last_positioned.unwrap_or(win::HWND_TOP), + inner_position.start.try_into().unwrap(), + inner_position.top.try_into().unwrap(), + inner_size.width.try_into().unwrap(), + inner_size.height.try_into().unwrap(), + 0, + ); + Gdi::InvalidateRect(hwnd, std::ptr::null(), 1); + } + self.last_positioned = Some(hwnd); + } + + // Reposition content. + match &element.element_type { + model::ElementType::VBox(model::VBox { items, spacing }) => { + let mut position = inner_position; + let mut size = inner_size; + for item in items { + self.reposition(item, &position, &size); + let consumed = self.get_size(item).height + spacing; + if item.style.vertical_alignment != Alignment::End { + position.top += consumed; + } + size.height = size.height.saturating_sub(consumed); + } + } + model::ElementType::HBox(model::HBox { + items, + spacing, + // The default ordering matches the windows platform order + affirmative_order: _, + }) => { + let mut position = inner_position; + let mut size = inner_size; + for item in items { + self.reposition(item, &position, &inner_size); + let consumed = self.get_size(item).width + spacing; + if item.style.horizontal_alignment != Alignment::End { + position.start += consumed; + } + size.width = size.width.saturating_sub(consumed); + } + } + model::ElementType::Scroll(model::Scroll { + content: Some(content), + }) => { + self.reposition(content, &inner_position, &inner_size); + } + _ => (), + } + } + + /// The `size` represents the maximum size permitted for the text (which is used for word + /// breaking), and it will be set to the precise width and height of the text. The width should + /// not exceed the input `size` width, but the height may. + fn measure_text(&mut self, text: &str, element: &Element, size: &mut Size) { + let Some(&window) = self.elements.get(&ElementRef::new(element)) else { + return; + }; + let hdc = unsafe { Gdi::GetDC(window) }; + unsafe { Gdi::SelectObject(hdc, win::SendMessageW(window, win::WM_GETFONT, 0, 0) as _) }; + let mut height: u32 = 0; + let mut max_width: u32 = 0; + let mut char_fit = 0i32; + let mut win_size = unsafe { std::mem::zeroed::<SIZE>() }; + for mut line in text.lines() { + if line.is_empty() { + line = " "; + } + let text = WideString::new(line); + let mut text = text.as_slice(); + let mut extents = vec![0i32; text.len()]; + while !text.is_empty() { + unsafe { + Gdi::GetTextExtentExPointW( + hdc, + text.as_ptr(), + text.len() as i32, + size.width.try_into().unwrap(), + &mut char_fit, + extents.as_mut_ptr(), + &mut win_size, + ); + } + if char_fit == 0 { + return; + } + let mut split = char_fit as usize; + let mut split_end = split.saturating_sub(1); + if (char_fit as usize) < text.len() { + for i in (0..char_fit as usize).rev() { + // FIXME safer utf16 handling? + if text[i] == b' ' as u16 { + split = i + 1; + split_end = i.saturating_sub(1); + break; + } + } + } + text = &text[split..]; + max_width = std::cmp::max(max_width, extents[split_end].try_into().unwrap()); + let measured_height: u32 = win_size.cy.try_into().unwrap(); + height += measured_height; + } + } + unsafe { Gdi::ReleaseDC(window, hdc) }; + + assert!(max_width <= size.width); + size.width = max_width; + size.height = height; + } +} + +#[derive(Debug, Default, Clone, Copy)] +struct Size { + pub width: u32, + pub height: u32, +} + +impl Size { + pub fn inner_size(&self, style: &ElementStyle) -> Self { + let mut ret = self.less_margin(&style.margin); + if let Some(width) = style.horizontal_size_request { + ret.width = width; + } + if let Some(height) = style.vertical_size_request { + ret.height = height; + } + ret + } + + pub fn from_content_size(&mut self, style: &ElementStyle, content_size: &Self) { + if style.horizontal_size_request.is_none() && style.horizontal_alignment != Alignment::Fill + { + self.width = content_size.width; + } + if style.vertical_size_request.is_none() && style.vertical_alignment != Alignment::Fill { + self.height = content_size.height; + } + } + + pub fn plus_margin(&self, margin: &Margin) -> Self { + let mut ret = self.clone(); + ret.width += margin.start + margin.end; + ret.height += margin.top + margin.bottom; + ret + } + + pub fn less_margin(&self, margin: &Margin) -> Self { + let mut ret = self.clone(); + ret.width = ret.width.saturating_sub(margin.start + margin.end); + ret.height = ret.height.saturating_sub(margin.top + margin.bottom); + ret + } +} + +#[derive(Debug, Default, Clone, Copy)] +struct Position { + pub start: u32, + pub top: u32, +} + +impl Position { + #[allow(dead_code)] + pub fn plus_margin(&self, margin: &Margin) -> Self { + let mut ret = self.clone(); + ret.start = ret.start.saturating_sub(margin.start); + ret.top = ret.top.saturating_sub(margin.top); + ret + } + + pub fn less_margin(&self, margin: &Margin) -> Self { + let mut ret = self.clone(); + ret.start += margin.start; + ret.top += margin.top; + ret + } +} + +fn is_visible(style: &ElementStyle) -> bool { + match &style.visible { + Property::Static(v) => *v, + Property::Binding(s) => *s.borrow(), + _ => true, + } +} diff --git a/toolkit/crashreporter/client/app/src/ui/windows/mod.rs b/toolkit/crashreporter/client/app/src/ui/windows/mod.rs new file mode 100644 index 0000000000..c2f396b80d --- /dev/null +++ b/toolkit/crashreporter/client/app/src/ui/windows/mod.rs @@ -0,0 +1,949 @@ +/* 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/. */ + +//! A UI using the windows API. +//! +//! This UI contains some edge cases that aren't implemented, for instance: +//! * there are a few cases where specific hierarchies are handled differently (e.g. a Button +//! containing Label, Scroll behavior, etc). +//! * not all controls handle all Property variants (e.g. Checkbox doesn't handle ReadOnly, TextBox +//! doesn't handle Binding, etc). +//! +//! The error handling is also a _little_ fast-and-loose, as many functions return an error value +//! that is acceptable to following logic (though it still would be a good idea to improve this). +//! +//! The rendering treats VBox, HBox, and Scroll as strictly layout-only: they do not create any +//! associated windows, and the layout logic handles their behavior. + +// Our windows-targets doesn't link uxtheme correctly for GetThemeSysFont/GetThemeSysColor. +// This was working in windows-sys 0.48. +#[link(name = "uxtheme", kind = "static")] +extern "C" {} + +use super::model::{self, Application, Element, ElementStyle, TypedElement}; +use crate::data::Property; +use font::Font; +use once_cell::sync::Lazy; +use quit_token::QuitToken; +use std::cell::RefCell; +use std::collections::HashMap; +use std::pin::Pin; +use std::rc::Rc; +use widestring::WideString; +use window::{CustomWindowClass, Window, WindowBuilder}; +use windows_sys::Win32::{ + Foundation::{HINSTANCE, HWND, LPARAM, LRESULT, RECT, WPARAM}, + Graphics::Gdi, + System::{LibraryLoader::GetModuleHandleW, SystemServices, Threading::GetCurrentThreadId}, + UI::{Controls, Input::KeyboardAndMouse, Shell, WindowsAndMessaging as win}, +}; + +macro_rules! success { + ( nonzero $e:expr ) => {{ + let value = $e; + assert_ne!(value, 0); + value + }}; + ( lasterror $e:expr ) => {{ + unsafe { windows_sys::Win32::Foundation::SetLastError(0) }; + let value = $e; + assert!(value != 0 || windows_sys::Win32::Foundation::GetLastError() == 0); + value + }}; + ( hresult $e:expr ) => { + assert_eq!($e, windows_sys::Win32::Foundation::S_OK); + }; + ( pointer $e:expr ) => {{ + let ptr = $e; + assert_ne!(ptr, 0); + ptr + }}; +} + +mod font; +mod gdi; +mod layout; +mod quit_token; +mod twoway; +mod widestring; +mod window; + +/// A Windows API UI implementation. +pub struct UI { + thread_id: u32, +} + +/// Custom user messages. +#[repr(u32)] +enum UserMessage { + Invoke = win::WM_USER, +} + +fn get_invoke(msg: &win::MSG) -> Option<Box<model::InvokeFn>> { + if msg.message == UserMessage::Invoke as u32 { + Some(unsafe { Box::from_raw(msg.lParam as *mut model::InvokeFn) }) + } else { + None + } +} + +impl UI { + pub fn run_loop(&self, app: Application) { + // Initialize common controls. + { + let icc = Controls::INITCOMMONCONTROLSEX { + dwSize: std::mem::size_of::<Controls::INITCOMMONCONTROLSEX>() as _, + // Buttons, edit controls, and static controls are all included in 'standard'. + dwICC: Controls::ICC_STANDARD_CLASSES | Controls::ICC_PROGRESS_CLASS, + }; + success!(nonzero unsafe { Controls::InitCommonControlsEx(&icc) }); + } + + // Enable font smoothing (per + // https://learn.microsoft.com/en-us/windows/win32/gdi/cleartype-antialiasing ). + unsafe { + // We don't check for failure on these, they are best-effort. + win::SystemParametersInfoW( + win::SPI_SETFONTSMOOTHING, + 1, + std::ptr::null_mut(), + win::SPIF_UPDATEINIFILE | win::SPIF_SENDCHANGE, + ); + win::SystemParametersInfoW( + win::SPI_SETFONTSMOOTHINGTYPE, + 0, + win::FE_FONTSMOOTHINGCLEARTYPE as _, + win::SPIF_UPDATEINIFILE | win::SPIF_SENDCHANGE, + ); + } + + // Enable correct layout direction. + if unsafe { win::SetProcessDefaultLayout(if app.rtl { Gdi::LAYOUT_RTL } else { 0 }) } == 0 { + log::warn!("failed to set process layout direction"); + } + + let module: HINSTANCE = unsafe { GetModuleHandleW(std::ptr::null()) }; + + // Register custom classes. + AppWindow::register(module).expect("failed to register AppWindow window class"); + + { + // The quit token is cloned for each top-level window and dropped at the end of this + // scope. + let quit_token = QuitToken::new(); + + for window in app.windows { + let name = WideString::new(window.element_type.title.as_str()); + let w = top_level_window( + module, + AppWindow::new( + WindowRenderer::new(module, window.element_type, &window.style), + Some(quit_token.clone()), + ), + &name, + &window.style, + ); + + unsafe { win::ShowWindow(w.handle, win::SW_NORMAL) }; + unsafe { Gdi::UpdateWindow(w.handle) }; + } + } + + // Run the event loop. + let mut msg = unsafe { std::mem::zeroed::<win::MSG>() }; + while unsafe { win::GetMessageW(&mut msg, 0, 0, 0) } > 0 { + if let Some(f) = get_invoke(&msg) { + f(); + continue; + } + + unsafe { + // IsDialogMessageW is necessary to handle niceties like tab navigation + if win::IsDialogMessageW(win::GetAncestor(msg.hwnd, win::GA_ROOT), &mut msg) == 0 { + win::TranslateMessage(&msg); + win::DispatchMessageW(&msg); + } + } + } + + // Flush queue to properly drop late invokes (this is a very unlikely case) + while unsafe { win::PeekMessageW(&mut msg, 0, 0, 0, win::PM_REMOVE) } > 0 { + if let Some(f) = get_invoke(&msg) { + drop(f); + } + } + } + + pub fn invoke(&self, f: model::InvokeFn) { + let ptr: *mut model::InvokeFn = Box::into_raw(Box::new(f)); + if unsafe { + win::PostThreadMessageW(self.thread_id, UserMessage::Invoke as u32, 0, ptr as _) + } == 0 + { + let _ = unsafe { Box::from_raw(ptr) }; + log::warn!("failed to invoke function on thread message queue"); + } + } +} + +impl Default for UI { + fn default() -> Self { + UI { + thread_id: unsafe { GetCurrentThreadId() }, + } + } +} + +/// A reference to an Element. +#[derive(PartialEq, Eq, Hash, Clone, Copy)] +struct ElementRef(*const Element); + +impl ElementRef { + pub fn new(element: &Element) -> Self { + ElementRef(element as *const Element) + } + + /// # Safety + /// You must ensure the reference is still valid. + pub unsafe fn get(&self) -> &Element { + &*self.0 + } +} + +// Equivalent of win32 HIWORD macro +fn hiword(v: u32) -> u16 { + (v >> 16) as u16 +} + +// Equivalent of win32 LOWORD macro +fn loword(v: u32) -> u16 { + v as u16 +} + +// Equivalent of win32 MAKELONG macro +fn makelong(low: u16, high: u16) -> u32 { + (high as u32) << 16 | low as u32 +} + +fn top_level_window<W: window::WindowClass + window::WindowData>( + module: HINSTANCE, + class: W, + title: &WideString, + style: &ElementStyle, +) -> Window<W> { + class + .builder(module) + .name(title) + .style(win::WS_OVERLAPPEDWINDOW) + .pos(win::CW_USEDEFAULT, win::CW_USEDEFAULT) + .size( + style + .horizontal_size_request + .and_then(|i| i.try_into().ok()) + .unwrap_or(win::CW_USEDEFAULT), + style + .vertical_size_request + .and_then(|i| i.try_into().ok()) + .unwrap_or(win::CW_USEDEFAULT), + ) + .create() +} + +window::basic_window_classes! { + /// Static control (text, image, etc) class. + struct Static => "STATIC"; + + /// Button control class. + struct Button => "BUTTON"; + + /// Edit control class. + struct Edit => "EDIT"; + + /// Progress control class. + struct Progress => "msctls_progress32"; +} + +/// A top-level application window. +/// +/// This is used for the main window and modal windows. +struct AppWindow { + renderer: WindowRenderer, + _quit_token: Option<QuitToken>, +} + +impl AppWindow { + pub fn new(renderer: WindowRenderer, quit_token: Option<QuitToken>) -> Self { + AppWindow { + renderer, + _quit_token: quit_token, + } + } +} + +impl window::WindowClass for AppWindow { + fn class_name() -> WideString { + WideString::new("App Window") + } +} + +impl CustomWindowClass for AppWindow { + fn icon() -> win::HICON { + static ICON: Lazy<win::HICON> = Lazy::new(|| unsafe { + // If CreateIconFromResource fails it returns NULL, which is fine (a default icon will be + // used). + win::CreateIconFromResource( + // We take advantage of the fact that since Windows Vista, an RT_ICON resource entry + // can simply be a PNG image. + super::icon::PNG_DATA.as_ptr(), + super::icon::PNG_DATA.len() as u32, + true.into(), + // The 0x00030000 constant isn't available anywhere; the docs basically say to just + // pass it... + 0x00030000, + ) + }); + + *ICON + } + + fn message( + data: &RefCell<Self>, + hwnd: HWND, + umsg: u32, + wparam: WPARAM, + lparam: LPARAM, + ) -> Option<LRESULT> { + let me = data.borrow(); + let model = me.renderer.model(); + match umsg { + win::WM_CREATE => { + if let Some(close) = &model.close { + close.subscribe(move |&()| unsafe { + win::SendMessageW(hwnd, win::WM_CLOSE, 0, 0); + }); + } + + let mut renderer = me.renderer.child_renderer(hwnd); + if let Some(child) = &model.content { + renderer.render_child(child); + } + + drop(model); + let children = std::mem::take(&mut me.renderer.model_mut().children); + for child in children { + renderer.render_window(child); + } + } + win::WM_CLOSE => { + if model.modal { + // Modal windows should hide themselves rather than closing/destroying. + unsafe { win::ShowWindow(hwnd, win::SW_HIDE) }; + return Some(0); + } + } + win::WM_SHOWWINDOW => { + if model.modal { + // Modal windows should disable/enable their parent as they are shown/hid, + // respectively. + let shown = wparam != 0; + unsafe { + KeyboardAndMouse::EnableWindow( + win::GetWindow(hwnd, win::GW_OWNER), + (!shown).into(), + ) + }; + return Some(0); + } + } + win::WM_GETMINMAXINFO => { + let minmaxinfo = unsafe { (lparam as *mut win::MINMAXINFO).as_mut().unwrap() }; + minmaxinfo.ptMinTrackSize.x = me.renderer.min_size.0.try_into().unwrap(); + minmaxinfo.ptMinTrackSize.y = me.renderer.min_size.1.try_into().unwrap(); + return Some(0); + } + win::WM_SIZE => { + // When resized, recompute the layout. + let width = loword(lparam as _) as u32; + let height = hiword(lparam as _) as u32; + + if let Some(child) = &model.content { + me.renderer.layout(child, width, height); + unsafe { Gdi::UpdateWindow(hwnd) }; + } + return Some(0); + } + win::WM_GETFONT => return Some(**me.renderer.font() as _), + win::WM_COMMAND => { + let child = lparam as HWND; + let windows = me.renderer.windows.borrow(); + if let Some(&element) = windows.reverse().get(&child) { + // # Safety + // The ElementRefs all pertain to the model stored in the renderer. + let element = unsafe { element.get() }; + // Handle button presses. + use model::ElementType::*; + match &element.element_type { + Button(model::Button { click, .. }) => { + let code = hiword(wparam as _) as u32; + if code == win::BN_CLICKED { + click.fire(&()); + return Some(0); + } + } + Checkbox(model::Checkbox { checked, .. }) => { + let code = hiword(wparam as _) as u32; + if code == win::BN_CLICKED { + let check_state = + unsafe { win::SendMessageW(child, win::BM_GETCHECK, 0, 0) }; + if let Property::Binding(s) = checked { + *s.borrow_mut() = check_state == Controls::BST_CHECKED as isize; + } + return Some(0); + } + } + _ => (), + } + } + } + _ => (), + } + None + } +} + +/// State used while creating and updating windows. +struct WindowRenderer { + // We wrap with an Rc to get weak references in property callbacks (like that of + // `ElementStyle::visible`). + inner: Rc<WindowRendererInner>, +} + +impl std::ops::Deref for WindowRenderer { + type Target = WindowRendererInner; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +struct WindowRendererInner { + pub module: HINSTANCE, + /// The model is pinned and boxed to ensure that references in `windows` remain valid. + /// + /// We need to keep the model around so we can correctly perform layout as the window size + /// changes. Unfortunately the win32 API doesn't have any nice ways to automatically perform + /// layout. + pub model: RefCell<Pin<Box<model::Window>>>, + pub min_size: (u32, u32), + /// Mapping between model elements and windows. + /// + /// Element references pertain to elements in `model`. + pub windows: RefCell<twoway::TwoWay<ElementRef, HWND>>, + pub font: Font, + pub bold_font: Font, +} + +impl WindowRenderer { + pub fn new(module: HINSTANCE, model: model::Window, style: &model::ElementStyle) -> Self { + WindowRenderer { + inner: Rc::new(WindowRendererInner { + module, + model: RefCell::new(Box::pin(model)), + min_size: ( + style.horizontal_size_request.unwrap_or(0), + style.vertical_size_request.unwrap_or(0), + ), + windows: Default::default(), + font: Font::caption(), + bold_font: Font::caption_bold().unwrap_or_else(Font::caption), + }), + } + } + + pub fn child_renderer(&self, window: HWND) -> WindowChildRenderer { + WindowChildRenderer { + renderer: &self.inner, + window, + child_id: 0, + scroll: false, + } + } + + pub fn layout(&self, element: &Element, max_width: u32, max_height: u32) { + layout::Layout::new(self.inner.windows.borrow().forward()) + .layout(element, max_width, max_height); + } + + pub fn model(&self) -> std::cell::Ref<'_, model::Window> { + std::cell::Ref::map(self.inner.model.borrow(), |b| &**b) + } + + pub fn model_mut(&self) -> std::cell::RefMut<'_, model::Window> { + std::cell::RefMut::map(self.inner.model.borrow_mut(), |b| &mut **b) + } + + pub fn font(&self) -> &Font { + &self.inner.font + } +} + +struct WindowChildRenderer<'a> { + renderer: &'a Rc<WindowRendererInner>, + window: HWND, + child_id: i32, + scroll: bool, +} + +impl<'a> WindowChildRenderer<'a> { + fn add_child<W: window::WindowClass>(&mut self, class: W) -> WindowBuilder<W> { + let builder = class + .builder(self.renderer.module) + .style(win::WS_CHILD | win::WS_VISIBLE) + .parent(self.window) + .child_id(self.child_id); + self.child_id += 1; + builder + } + + fn add_window<W: window::WindowClass>(&mut self, class: W) -> WindowBuilder<W> { + class + .builder(self.renderer.module) + .style(win::WS_OVERLAPPEDWINDOW) + .pos(win::CW_USEDEFAULT, win::CW_USEDEFAULT) + .parent(self.window) + } + + fn render_window(&mut self, model: TypedElement<model::Window>) -> Window { + let name = WideString::new(model.element_type.title.as_str()); + let style = model.style; + let w = self + .add_window(AppWindow::new( + WindowRenderer::new(self.renderer.module, model.element_type, &style), + None, + )) + .size( + style + .horizontal_size_request + .and_then(|i| i.try_into().ok()) + .unwrap_or(win::CW_USEDEFAULT), + style + .vertical_size_request + .and_then(|i| i.try_into().ok()) + .unwrap_or(win::CW_USEDEFAULT), + ) + .name(&name) + .create(); + + enabled_property(&style.enabled, w.handle); + + let hwnd = w.handle; + let set_visible = move |visible| unsafe { + win::ShowWindow(hwnd, if visible { win::SW_SHOW } else { win::SW_HIDE }); + }; + + match &style.visible { + Property::Static(false) => set_visible(false), + Property::Binding(s) => { + s.on_change(move |v| set_visible(*v)); + if !*s.borrow() { + set_visible(false); + } + } + _ => (), + } + + w.generic() + } + + fn render_child(&mut self, element: &Element) { + if let Some(mut window) = self.render_element_type(&element.element_type) { + window.set_default_font(&self.renderer.font); + + // Store the element to handle mapping. + self.renderer + .windows + .borrow_mut() + .insert(ElementRef::new(element), window.handle); + + enabled_property(&element.style.enabled, window.handle); + } + + // Handle visibility properties. + match &element.style.visible { + Property::Static(false) => { + set_visibility(element, false, self.renderer.windows.borrow().forward()) + } + Property::Binding(s) => { + let weak_renderer = Rc::downgrade(self.renderer); + let element_ref = ElementRef::new(element); + let parent = self.window; + s.on_change(move |visible| { + let Some(renderer) = weak_renderer.upgrade() else { + return; + }; + // # Safety + // ElementRefs are valid as long as the renderer is (and we have a strong + // reference to it). + let element = unsafe { element_ref.get() }; + set_visibility(element, *visible, renderer.windows.borrow().forward()); + // Send WM_SIZE so that the parent recomputes the layout. + unsafe { + let mut rect = std::mem::zeroed::<RECT>(); + win::GetClientRect(parent, &mut rect); + win::SendMessageW( + parent, + win::WM_SIZE, + 0, + makelong( + (rect.right - rect.left) as u16, + (rect.bottom - rect.top) as u16, + ) as isize, + ); + } + }); + if !*s.borrow() { + set_visibility(element, false, self.renderer.windows.borrow().forward()); + } + } + _ => (), + } + } + + fn render_element_type(&mut self, element_type: &model::ElementType) -> Option<Window> { + use model::ElementType as ET; + match element_type { + ET::Label(model::Label { text, bold }) => { + let mut window = match text { + Property::Static(text) => { + let text = WideString::new(text.as_str()); + self.add_child(Static) + .name(&text) + .add_style(SystemServices::SS_LEFT | SystemServices::SS_NOPREFIX) + .create() + } + Property::Binding(b) => { + let text = WideString::new(b.borrow().as_str()); + let window = self + .add_child(Static) + .name(&text) + .add_style(SystemServices::SS_LEFT | SystemServices::SS_NOPREFIX) + .create(); + let handle = window.handle; + b.on_change(move |text| { + let text = WideString::new(text.as_str()); + unsafe { win::SetWindowTextW(handle, text.pcwstr()) }; + }); + window + } + Property::ReadOnly(_) => { + unimplemented!("ReadOnly property not supported for Label::text") + } + }; + if *bold { + window.set_font(&self.renderer.bold_font); + } + Some(window.generic()) + } + ET::TextBox(model::TextBox { + placeholder, + content, + editable, + }) => { + let scroll = self.scroll; + let window = self + .add_child(Edit) + .add_style( + (win::ES_LEFT + | win::ES_MULTILINE + | win::ES_WANTRETURN + | if *editable { 0 } else { win::ES_READONLY }) + as u32 + | win::WS_BORDER + | win::WS_TABSTOP + | if scroll { win::WS_VSCROLL } else { 0 }, + ) + .create(); + + fn to_control_text(s: &str) -> String { + s.replace("\n", "\r\n") + } + + fn from_control_text(s: &str) -> String { + s.replace("\r\n", "\n") + } + + struct SubClassData { + placeholder: Option<WideString>, + } + + // EM_SETCUEBANNER doesn't work with multiline edit controls (for no particular + // reason?), so we have to draw it ourselves. + unsafe extern "system" fn subclass_proc( + hwnd: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + _uidsubclass: usize, + dw_ref_data: usize, + ) -> LRESULT { + let ret = Shell::DefSubclassProc(hwnd, msg, wparam, lparam); + if msg == win::WM_PAINT + && KeyboardAndMouse::GetFocus() != hwnd + && win::GetWindowTextLengthW(hwnd) == 0 + { + let data = (dw_ref_data as *const SubClassData).as_ref().unwrap(); + if let Some(placeholder) = &data.placeholder { + let mut rect = std::mem::zeroed::<RECT>(); + win::GetClientRect(hwnd, &mut rect); + Gdi::InflateRect(&mut rect, -2, -2); + + let dc = gdi::DC::new(hwnd).expect("failed to create GDI DC"); + dc.with_object_selected( + win::SendMessageW(hwnd, win::WM_GETFONT, 0, 0) as _, + |hdc| { + Gdi::SetTextColor( + hdc, + Controls::GetThemeSysColor(0, Gdi::COLOR_GRAYTEXT), + ); + Gdi::SetBkMode(hdc, Gdi::TRANSPARENT as i32); + success!(nonzero Gdi::DrawTextW( + hdc, + placeholder.pcwstr(), + -1, + &mut rect, + Gdi::DT_LEFT | Gdi::DT_TOP | Gdi::DT_WORDBREAK, + )); + }, + ) + .expect("failed to select font gdi object"); + } + } + + // Multiline edit controls capture the tab key. We want it to work as usual in + // the dialog (focusing the next input control). + if msg == win::WM_GETDLGCODE && wparam == KeyboardAndMouse::VK_TAB as usize { + return 0; + } + + if msg == win::WM_DESTROY { + drop(unsafe { Box::from_raw(dw_ref_data as *mut SubClassData) }); + } + return ret; + } + + let subclassdata = Box::into_raw(Box::new(SubClassData { + placeholder: placeholder + .as_ref() + .map(|s| WideString::new(to_control_text(s))), + })); + + unsafe { + Shell::SetWindowSubclass( + window.handle, + Some(subclass_proc), + 0, + subclassdata as _, + ); + } + + // Set up content property. + match content { + Property::ReadOnly(od) => { + let handle = window.handle; + od.register(move |target| { + // GetWindowText requires the buffer be large enough for the terminating + // null character (otherwise it truncates the string), but + // GetWindowTextLength returns the length without the null character, so we + // add 1. + let length = unsafe { win::GetWindowTextLengthW(handle) } + 1; + let mut buf = vec![0u16; length as usize]; + unsafe { win::GetWindowTextW(handle, buf.as_mut_ptr(), length) }; + buf.pop(); // null character; `String` doesn't want that + *target = from_control_text(&String::from_utf16_lossy(&buf)); + }); + } + Property::Static(s) => { + let text = WideString::new(to_control_text(s)); + unsafe { win::SetWindowTextW(window.handle, text.pcwstr()) }; + } + Property::Binding(b) => { + let handle = window.handle; + b.on_change(move |text| { + let text = WideString::new(to_control_text(text.as_str())); + unsafe { win::SetWindowTextW(handle, text.pcwstr()) }; + }); + let text = WideString::new(to_control_text(b.borrow().as_str())); + unsafe { win::SetWindowTextW(window.handle, text.pcwstr()) }; + } + } + Some(window.generic()) + } + ET::Scroll(model::Scroll { content }) => { + if let Some(content) = content { + // Scrolling is implemented in a cooperative, non-universal way right now. + self.scroll = true; + self.render_child(content); + self.scroll = false; + } + None + } + ET::Button(model::Button { content, .. }) => { + if let Some(ET::Label(model::Label { + text: Property::Static(text), + .. + })) = content.as_ref().map(|e| &e.element_type) + { + let text = WideString::new(text); + + let window = self + .add_child(Button) + .add_style(win::BS_PUSHBUTTON as u32 | win::WS_TABSTOP) + .name(&text) + .create(); + Some(window.generic()) + } else { + None + } + } + ET::Checkbox(model::Checkbox { checked, label }) => { + let label = label.as_ref().map(WideString::new); + let mut builder = self + .add_child(Button) + .add_style((win::BS_AUTOCHECKBOX | win::BS_MULTILINE) as u32 | win::WS_TABSTOP); + if let Some(label) = &label { + builder = builder.name(label); + } + let window = builder.create(); + + fn set_check(handle: HWND, value: bool) { + unsafe { + win::SendMessageW( + handle, + win::BM_SETCHECK, + if value { + Controls::BST_CHECKED + } else { + Controls::BST_UNCHECKED + } as usize, + 0, + ); + } + } + + match checked { + Property::Static(checked) => set_check(window.handle, *checked), + Property::Binding(s) => { + let handle = window.handle; + s.on_change(move |v| { + set_check(handle, *v); + }); + set_check(window.handle, *s.borrow()); + } + _ => unimplemented!("ReadOnly properties not supported for Checkbox"), + } + + Some(window.generic()) + } + ET::Progress(model::Progress { amount }) => { + let window = self + .add_child(Progress) + .add_style(Controls::PBS_MARQUEE) + .create(); + + fn set_amount(handle: HWND, value: Option<f32>) { + match value { + None => unsafe { + win::SendMessageW(handle, Controls::PBM_SETMARQUEE, 1, 0); + }, + Some(v) => unsafe { + win::SendMessageW(handle, Controls::PBM_SETMARQUEE, 0, 0); + win::SendMessageW( + handle, + Controls::PBM_SETPOS, + (v.clamp(0f32, 1f32) * 100f32) as usize, + 0, + ); + }, + } + } + + match amount { + Property::Static(v) => set_amount(window.handle, *v), + Property::Binding(s) => { + let handle = window.handle; + s.on_change(move |v| set_amount(handle, *v)); + set_amount(window.handle, *s.borrow()); + } + _ => unimplemented!("ReadOnly properties not supported for Progress"), + } + + Some(window.generic()) + } + // VBox/HBox are virtual, their behaviors are implemented entirely in the renderer layout. + // No need for additional windows. + ET::VBox(model::VBox { items, .. }) => { + for item in items { + self.render_child(item); + } + None + } + ET::HBox(model::HBox { items, .. }) => { + for item in items { + self.render_child(item); + } + None + } + } + } +} + +/// Handle the enabled property. +/// +/// This function assumes the default state of the window is enabled. +fn enabled_property(enabled: &Property<bool>, window: HWND) { + match enabled { + Property::Static(false) => unsafe { + KeyboardAndMouse::EnableWindow(window, false.into()); + }, + Property::Binding(s) => { + let handle = window; + s.on_change(move |enabled| { + unsafe { KeyboardAndMouse::EnableWindow(handle, (*enabled).into()) }; + }); + if !*s.borrow() { + unsafe { KeyboardAndMouse::EnableWindow(window, false.into()) }; + } + } + _ => (), + } +} + +/// Set the visibility of the given element. This recurses down the element tree and hides children +/// as necessary. +fn set_visibility(element: &Element, visible: bool, windows: &HashMap<ElementRef, HWND>) { + if let Some(&hwnd) = windows.get(&ElementRef::new(element)) { + unsafe { + win::ShowWindow(hwnd, if visible { win::SW_SHOW } else { win::SW_HIDE }); + } + } else { + match &element.element_type { + model::ElementType::VBox(model::VBox { items, .. }) => { + for item in items { + set_visibility(item, visible, windows); + } + } + model::ElementType::HBox(model::HBox { items, .. }) => { + for item in items { + set_visibility(item, visible, windows); + } + } + model::ElementType::Scroll(model::Scroll { + content: Some(content), + }) => { + set_visibility(&*content, visible, windows); + } + _ => (), + } + } +} diff --git a/toolkit/crashreporter/client/app/src/ui/windows/quit_token.rs b/toolkit/crashreporter/client/app/src/ui/windows/quit_token.rs new file mode 100644 index 0000000000..f952db3db4 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/ui/windows/quit_token.rs @@ -0,0 +1,33 @@ +/* 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/. */ + +use std::rc::Rc; +use windows_sys::Win32::UI::WindowsAndMessaging::PostQuitMessage; + +/// A Cloneable token which will post a quit message (with code 0) to the main loop when the last +/// instance is dropped. +#[derive(Clone, Default)] +pub struct QuitToken(#[allow(dead_code)] Rc<QuitTokenInternal>); + +impl QuitToken { + pub fn new() -> Self { + Self::default() + } +} + +impl std::fmt::Debug for QuitToken { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.debug_struct(std::any::type_name::<Self>()) + .finish_non_exhaustive() + } +} + +#[derive(Default)] +struct QuitTokenInternal; + +impl Drop for QuitTokenInternal { + fn drop(&mut self) { + unsafe { PostQuitMessage(0) }; + } +} diff --git a/toolkit/crashreporter/client/app/src/ui/windows/twoway.rs b/toolkit/crashreporter/client/app/src/ui/windows/twoway.rs new file mode 100644 index 0000000000..8a18162e08 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/ui/windows/twoway.rs @@ -0,0 +1,36 @@ +/* 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/. */ + +use std::collections::HashMap; + +/// A two-way hashmap. +#[derive(Debug)] +pub struct TwoWay<K, V> { + forward: HashMap<K, V>, + reverse: HashMap<V, K>, +} + +impl<K, V> Default for TwoWay<K, V> { + fn default() -> Self { + TwoWay { + forward: Default::default(), + reverse: Default::default(), + } + } +} + +impl<K: Eq + std::hash::Hash + Clone, V: Eq + std::hash::Hash + Clone> TwoWay<K, V> { + pub fn insert(&mut self, key: K, value: V) { + self.forward.insert(key.clone(), value.clone()); + self.reverse.insert(value, key); + } + + pub fn forward(&self) -> &HashMap<K, V> { + &self.forward + } + + pub fn reverse(&self) -> &HashMap<V, K> { + &self.reverse + } +} diff --git a/toolkit/crashreporter/client/app/src/ui/windows/widestring.rs b/toolkit/crashreporter/client/app/src/ui/windows/widestring.rs new file mode 100644 index 0000000000..0dc713352b --- /dev/null +++ b/toolkit/crashreporter/client/app/src/ui/windows/widestring.rs @@ -0,0 +1,36 @@ +/* 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/. */ + +use std::ffi::OsStr; +use std::os::windows::ffi::OsStrExt; +use windows_sys::core::PCWSTR; + +/// Windows wide strings. +/// +/// These are utf16 encoded with a terminating null character (0). +pub struct WideString(Vec<u16>); + +impl WideString { + pub fn new(os_str: impl AsRef<OsStr>) -> Self { + // TODO: doesn't check whether the OsStr contains a null character, which could be treated + // as an error (as `CString::new` does). + WideString( + os_str + .as_ref() + .encode_wide() + .chain(std::iter::once(0)) + // Remove unicode BIDI markers (from fluent) which aren't rendered correctly. + .filter(|c| *c != 0x2068 && *c != 0x2069) + .collect(), + ) + } + + pub fn pcwstr(&self) -> PCWSTR { + self.0.as_ptr() + } + + pub fn as_slice(&self) -> &[u16] { + &self.0 + } +} diff --git a/toolkit/crashreporter/client/app/src/ui/windows/window.rs b/toolkit/crashreporter/client/app/src/ui/windows/window.rs new file mode 100644 index 0000000000..7e5e8f3f2a --- /dev/null +++ b/toolkit/crashreporter/client/app/src/ui/windows/window.rs @@ -0,0 +1,302 @@ +/* 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/. */ + +//! Types and helpers relating to windows and window classes. + +use super::Font; +use super::WideString; +use std::cell::RefCell; +use windows_sys::Win32::{ + Foundation::{HINSTANCE, HWND, LPARAM, LRESULT, WPARAM}, + Graphics::Gdi::{self, HBRUSH}, + UI::WindowsAndMessaging::{self as win, HCURSOR, HICON}, +}; + +/// Types representing a window class. +pub trait WindowClass: Sized + 'static { + fn class_name() -> WideString; + + fn builder(self, module: HINSTANCE) -> WindowBuilder<'static, Self> { + WindowBuilder { + name: None, + style: None, + x: 0, + y: 0, + width: 0, + height: 0, + parent: None, + child_id: 0, + module, + data: self, + } + } +} + +/// Window classes which have their own message handler. +/// +/// A type implementing this trait provides its data to a single window. +/// +/// `register` must be called before use. +pub trait CustomWindowClass: WindowClass { + /// Handle a message. + fn message( + data: &RefCell<Self>, + hwnd: HWND, + umsg: u32, + wparam: WPARAM, + lparam: LPARAM, + ) -> Option<LRESULT>; + + /// The class's default background brush. + fn background() -> HBRUSH { + (Gdi::COLOR_3DFACE + 1) as HBRUSH + } + + /// The class's default cursor. + fn cursor() -> HCURSOR { + unsafe { win::LoadCursorW(0, win::IDC_ARROW) } + } + + /// The class's default icon. + fn icon() -> HICON { + 0 + } + + /// Register the class. + fn register(module: HINSTANCE) -> anyhow::Result<()> { + unsafe extern "system" fn wnd_proc<W: CustomWindowClass>( + hwnd: HWND, + umsg: u32, + wparam: WPARAM, + lparam: LPARAM, + ) -> LRESULT { + if umsg == win::WM_CREATE { + let create_struct = &*(lparam as *const win::CREATESTRUCTW); + success!(lasterror win::SetWindowLongPtrW(hwnd, 0, create_struct.lpCreateParams as _)); + // Changes made with SetWindowLongPtr don't take effect until SetWindowPos is called... + success!(nonzero win::SetWindowPos( + hwnd, + win::HWND_TOP, + 0, + 0, + 0, + 0, + win::SWP_NOMOVE | win::SWP_NOSIZE | win::SWP_NOZORDER | win::SWP_FRAMECHANGED, + )); + } + + let result = unsafe { W::get(hwnd).as_ref() } + .and_then(|data| W::message(data, hwnd, umsg, wparam, lparam)); + if umsg == win::WM_DESTROY { + drop(Box::from_raw( + win::GetWindowLongPtrW(hwnd, 0) as *mut RefCell<W> + )); + } + result.unwrap_or_else(|| win::DefWindowProcW(hwnd, umsg, wparam, lparam)) + } + + let class_name = Self::class_name(); + let window_class = win::WNDCLASSW { + lpfnWndProc: Some(wnd_proc::<Self>), + hInstance: module, + lpszClassName: class_name.pcwstr(), + hbrBackground: Self::background(), + hIcon: Self::icon(), + hCursor: Self::cursor(), + cbWndExtra: std::mem::size_of::<isize>() as i32, + ..unsafe { std::mem::zeroed() } + }; + + if unsafe { win::RegisterClassW(&window_class) } == 0 { + anyhow::bail!("RegisterClassW failed") + } + Ok(()) + } + + /// Get the window data from a window created with this class. + /// + /// # Safety + /// This must only be called on window handles which were created with this class. + unsafe fn get(hwnd: HWND) -> *const RefCell<Self> { + win::GetWindowLongPtrW(hwnd, 0) as *const RefCell<Self> + } +} + +/// Types that can be stored as associated window data. +pub trait WindowData: Sized { + fn to_ptr(self) -> *mut RefCell<Self> { + std::ptr::null_mut() + } +} + +impl<T: CustomWindowClass> WindowData for T { + fn to_ptr(self) -> *mut RefCell<Self> { + Box::into_raw(Box::new(RefCell::new(self))) + } +} + +macro_rules! basic_window_classes { + () => {}; + ( $(#[$attr:meta])* struct $name:ident => $class:expr; $($rest:tt)* ) => { + #[derive(Default)] + $(#[$attr])* + struct $name; + + impl $crate::ui::ui_impl::window::WindowClass for $name { + fn class_name() -> $crate::ui::ui_impl::WideString { + $crate::ui::ui_impl::WideString::new($class) + } + } + + impl $crate::ui::ui_impl::window::WindowData for $name {} + + $crate::ui::ui_impl::window::basic_window_classes!($($rest)*); + } +} + +pub(crate) use basic_window_classes; + +pub struct WindowBuilder<'a, W> { + name: Option<&'a WideString>, + style: Option<u32>, + x: i32, + y: i32, + width: i32, + height: i32, + parent: Option<HWND>, + child_id: i32, + module: HINSTANCE, + data: W, +} + +impl<'a, W> WindowBuilder<'a, W> { + #[must_use] + pub fn name<'b>(self, s: &'b WideString) -> WindowBuilder<'b, W> { + WindowBuilder { + name: Some(s), + style: self.style, + x: self.x, + y: self.y, + width: self.width, + height: self.height, + parent: self.parent, + child_id: self.child_id, + module: self.module, + data: self.data, + } + } + + #[must_use] + pub fn style(mut self, d: u32) -> Self { + self.style = Some(d); + self + } + + #[must_use] + pub fn add_style(mut self, d: u32) -> Self { + *self.style.get_or_insert(0) |= d; + self + } + + #[must_use] + pub fn pos(mut self, x: i32, y: i32) -> Self { + self.x = x; + self.y = y; + self + } + + #[must_use] + pub fn size(mut self, width: i32, height: i32) -> Self { + self.width = width; + self.height = height; + self + } + + #[must_use] + pub fn parent(mut self, parent: HWND) -> Self { + self.parent = Some(parent); + self + } + + #[must_use] + pub fn child_id(mut self, id: i32) -> Self { + self.child_id = id; + self + } + + pub fn create(self) -> Window<W> + where + W: WindowClass + WindowData, + { + let class_name = W::class_name(); + let handle = unsafe { + win::CreateWindowExW( + 0, + class_name.pcwstr(), + self.name.map(|n| n.pcwstr()).unwrap_or(std::ptr::null()), + self.style.unwrap_or_default(), + self.x, + self.y, + self.width, + self.height, + self.parent.unwrap_or_default(), + self.child_id as _, + self.module, + self.data.to_ptr() as _, + ) + }; + assert!(handle != 0); + + Window { + handle, + child_id: self.child_id, + font_set: false, + _class: std::marker::PhantomData, + } + } +} + +/// A window handle with a known class type. +/// +/// Without a type parameter (defaulting to `()`), the window handle is generic (class type +/// unknown). +pub struct Window<W: 'static = ()> { + pub handle: HWND, + pub child_id: i32, + font_set: bool, + _class: std::marker::PhantomData<&'static RefCell<W>>, +} + +impl<W: CustomWindowClass> Window<W> { + /// Get the window data of the window. + #[allow(dead_code)] + pub fn data(&self) -> &RefCell<W> { + unsafe { W::get(self.handle).as_ref().unwrap() } + } +} + +impl<W> Window<W> { + /// Get a generic window handle. + pub fn generic(self) -> Window { + Window { + handle: self.handle, + child_id: self.child_id, + font_set: self.font_set, + _class: std::marker::PhantomData, + } + } + + /// Set a window's font. + pub fn set_font(&mut self, font: &Font) { + unsafe { win::SendMessageW(self.handle, win::WM_SETFONT, **font as _, 1 as _) }; + self.font_set = true; + } + + /// Set a window's font if not already set. + pub fn set_default_font(&mut self, font: &Font) { + if !self.font_set { + self.set_font(font); + } + } +} diff --git a/toolkit/crashreporter/client/cocoabind/Cargo.toml b/toolkit/crashreporter/client/cocoabind/Cargo.toml new file mode 100644 index 0000000000..70a0f36582 --- /dev/null +++ b/toolkit/crashreporter/client/cocoabind/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "cocoabind" +version = "0.1.0" +edition = "2021" + +[dependencies] +block = "0.1" +objc = "0.2" + +[build-dependencies] +bindgen = { version = "0.69", default-features = false, features = ["runtime"] } +mozbuild = "0.1.0" diff --git a/toolkit/crashreporter/client/cocoabind/build.rs b/toolkit/crashreporter/client/cocoabind/build.rs new file mode 100644 index 0000000000..ba91a87e97 --- /dev/null +++ b/toolkit/crashreporter/client/cocoabind/build.rs @@ -0,0 +1,74 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +use mozbuild::config::CC_BASE_FLAGS as CFLAGS; + +const TYPES: &[&str] = &[ + "ActionCell", + "Application", + "Array", + "AttributedString", + "Box", + "Button", + "ButtonCell", + "Cell", + "ClassDescription", + "Control", + "DefaultRunLoopMode", + "Dictionary", + "ForegroundColorAttributeName", + "LayoutDimension", + "LayoutGuide", + "LayoutXAxisAnchor", + "LayoutYAxisAnchor", + "MutableAttributedString", + "MutableParagraphStyle", + "MutableString", + "ModalPanelRunLoopMode", + "Panel", + "ProcessInfo", + "ProgressIndicator", + "Proxy", + "RunLoop", + "ScrollView", + "StackView", + "String", + "TextField", + "TextView", + "Value", + "View", + "Window", +]; + +fn main() { + let mut builder = bindgen::Builder::default() + .header_contents( + "cocoa_bindings.h", + "#define self self_ + #import <Cocoa/Cocoa.h> + ", + ) + .generate_block(true) + .prepend_enum_name(false) + .clang_args(CFLAGS) + .clang_args(["-x", "objective-c"]) + .clang_arg("-fblocks") + .derive_default(true) + .allowlist_item("TransformProcessType"); + for name in TYPES { + // (I|P) covers generated traits (interfaces and protocols). `(_.*)?` covers categories + // (which are generated as `CLASS_CATEGORY`). + builder = builder.allowlist_item(format!("(I|P)?NS{name}(_.*)?")); + } + let bindings = builder + .generate() + .expect("unable to generate cocoa bindings"); + let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("cocoa_bindings.rs")) + .expect("failed to write cocoa bindings"); + println!("cargo:rustc-link-lib=framework=AppKit"); + println!("cargo:rustc-link-lib=framework=Cocoa"); + println!("cargo:rustc-link-lib=framework=Foundation"); +} diff --git a/toolkit/crashreporter/client/cocoabind/src/lib.rs b/toolkit/crashreporter/client/cocoabind/src/lib.rs new file mode 100644 index 0000000000..b7271d9d92 --- /dev/null +++ b/toolkit/crashreporter/client/cocoabind/src/lib.rs @@ -0,0 +1,10 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(unused_imports)] + +include!(concat!(env!("OUT_DIR"), "/cocoa_bindings.rs")); diff --git a/toolkit/crashreporter/client/crashreporter.cpp b/toolkit/crashreporter/client/crashreporter.cpp deleted file mode 100644 index 2887b16170..0000000000 --- a/toolkit/crashreporter/client/crashreporter.cpp +++ /dev/null @@ -1,852 +0,0 @@ -/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include "crashreporter.h" - -#ifdef _MSC_VER -// Disable exception handler warnings. -# pragma warning(disable : 4530) -#endif - -#include <fstream> -#include <iomanip> -#include <sstream> -#include <memory> -#include <ctime> -#include <cstdlib> -#include <cstring> -#include <string> -#include <utility> - -#ifdef XP_LINUX -# include <dlfcn.h> -#endif - -#include "json/json.h" -#include "nss.h" -#include "sechash.h" - -using std::ifstream; -using std::ios; -using std::istream; -using std::istringstream; -using std::ofstream; -using std::ostream; -using std::ostringstream; -using std::string; -using std::unique_ptr; -using std::vector; - -namespace CrashReporter { - -StringTable gStrings; -Json::Value gData; -string gSettingsPath; -string gEventsPath; -string gPingPath; -int gArgc; -char** gArgv; -bool gAutoSubmit; - -enum SubmissionResult { Succeeded, Failed }; - -static unique_ptr<ofstream> gLogStream(nullptr); -static string gReporterDumpFile; -static string gExtraFile; -static string gMemoryFile; - -static const char kExtraDataExtension[] = ".extra"; -static const char kMemoryReportExtension[] = ".memory.json.gz"; - -void UIError(const string& message) { - if (gAutoSubmit) { - return; - } - - string errorMessage; - if (!gStrings[ST_CRASHREPORTERERROR].empty()) { - char buf[2048]; - snprintf(buf, 2048, gStrings[ST_CRASHREPORTERERROR].c_str(), - message.c_str()); - errorMessage = buf; - } else { - errorMessage = message; - } - - UIError_impl(errorMessage); -} - -static string Unescape(const string& str) { - string ret; - for (string::const_iterator iter = str.begin(); iter != str.end(); iter++) { - if (*iter == '\\') { - iter++; - if (*iter == '\\') { - ret.push_back('\\'); - } else if (*iter == 'n') { - ret.push_back('\n'); - } else if (*iter == 't') { - ret.push_back('\t'); - } - } else { - ret.push_back(*iter); - } - } - - return ret; -} - -bool ReadStrings(istream& in, StringTable& strings, bool unescape) { - while (!in.eof()) { - string line; - std::getline(in, line); - int sep = line.find('='); - if (sep >= 0) { - string key, value; - key = line.substr(0, sep); - value = line.substr(sep + 1); - if (unescape) value = Unescape(value); - strings[key] = value; - } - } - - return true; -} - -bool ReadStringsFromFile(const string& path, StringTable& strings, - bool unescape) { - ifstream* f = UIOpenRead(path, ios::in); - bool success = false; - if (f->is_open()) { - success = ReadStrings(*f, strings, unescape); - f->close(); - } - - delete f; - return success; -} - -static bool ReadExtraFile(const string& aExtraDataPath, Json::Value& aExtra) { - bool success = false; - ifstream* f = UIOpenRead(aExtraDataPath, ios::in); - if (f->is_open()) { - Json::CharReaderBuilder builder; - success = parseFromStream(builder, *f, &aExtra, nullptr); - } - - delete f; - return success; -} - -static string Basename(const string& file) { - string::size_type slashIndex = file.rfind(UI_DIR_SEPARATOR); - if (slashIndex != string::npos) { - return file.substr(slashIndex + 1); - } - return file; -} - -static bool ReadEventFile(const string& aPath, string& aEventVersion, - string& aTime, string& aUuid, Json::Value& aData) { - bool res = false; - ifstream* f = UIOpenRead(aPath, ios::binary); - - if (f->is_open()) { - std::getline(*f, aEventVersion, '\n'); - res = f->good(); - std::getline(*f, aTime, '\n'); - res &= f->good(); - std::getline(*f, aUuid, '\n'); - res &= f->good(); - - if (res) { - Json::CharReaderBuilder builder; - res = parseFromStream(builder, *f, &aData, nullptr); - } - } - - delete f; - return res; -} - -static void OverwriteEventFile(const string& aPath, const string& aEventVersion, - const string& aTime, const string& aUuid, - const Json::Value& aData) { - ofstream* f = UIOpenWrite(aPath, ios::binary | ios::trunc); - if (f->is_open()) { - f->write(aEventVersion.c_str(), aEventVersion.length()) << '\n'; - f->write(aTime.c_str(), aTime.length()) << '\n'; - f->write(aUuid.c_str(), aUuid.length()) << '\n'; - - Json::StreamWriterBuilder builder; - builder["indentation"] = ""; - std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter()); - writer->write(aData, f); - *f << "\n"; - } - - delete f; -} - -static void UpdateEventFile(const Json::Value& aExtraData, const string& aHash, - const string& aPingUuid) { - if (gEventsPath.empty()) { - // If there is no path for finding the crash event, skip this step. - return; - } - - string localId = CrashReporter::GetDumpLocalID(); - string path = gEventsPath + UI_DIR_SEPARATOR + localId; - string eventVersion; - string crashTime; - string crashUuid; - Json::Value eventData; - - if (!ReadEventFile(path, eventVersion, crashTime, crashUuid, eventData)) { - return; - } - - if (!aHash.empty()) { - eventData["MinidumpSha256Hash"] = aHash; - } - - if (!aPingUuid.empty()) { - eventData["CrashPingUUID"] = aPingUuid; - } - - if (aExtraData.isMember("StackTraces")) { - eventData["StackTraces"] = aExtraData["StackTraces"]; - } - - OverwriteEventFile(path, eventVersion, crashTime, crashUuid, eventData); -} - -static void WriteSubmissionEvent(SubmissionResult result, - const string& remoteId) { - if (gEventsPath.empty()) { - // If there is no path for writing the submission event, skip it. - return; - } - - string localId = CrashReporter::GetDumpLocalID(); - string fpath = gEventsPath + UI_DIR_SEPARATOR + localId + "-submission"; - ofstream* f = UIOpenWrite(fpath, ios::binary); - time_t tm; - time(&tm); - - if (f->is_open()) { - *f << "crash.submission.1\n"; - *f << tm << "\n"; - *f << localId << "\n"; - *f << (result == Succeeded ? "true" : "false") << "\n"; - *f << remoteId; - - f->close(); - } - - delete f; -} - -void LogMessage(const std::string& message) { - if (gLogStream.get()) { - char date[64]; - time_t tm; - time(&tm); - if (strftime(date, sizeof(date) - 1, "%c", localtime(&tm)) == 0) - date[0] = '\0'; - (*gLogStream) << "[" << date << "] " << message << '\n'; - } -} - -static void OpenLogFile() { - string logPath = gSettingsPath + UI_DIR_SEPARATOR + "submit.log"; - gLogStream.reset(UIOpenWrite(logPath, ios::app)); -} - -static bool ReadConfig() { - string iniPath; - if (!UIGetIniPath(iniPath)) { - return false; - } - - if (!ReadStringsFromFile(iniPath, gStrings, true)) return false; - - // See if we have a string override file, if so process it - char* overrideEnv = getenv("MOZ_CRASHREPORTER_STRINGS_OVERRIDE"); - if (overrideEnv && *overrideEnv && UIFileExists(overrideEnv)) - ReadStringsFromFile(overrideEnv, gStrings, true); - - return true; -} - -static string GetAdditionalFilename(const string& dumpfile, - const char* extension) { - string filename(dumpfile); - int dot = filename.rfind('.'); - if (dot < 0) return ""; - - filename.replace(dot, filename.length() - dot, extension); - return filename; -} - -static bool MoveCrashData(const string& toDir, string& dumpfile, - string& extrafile, string& memoryfile) { - if (!UIEnsurePathExists(toDir)) { - UIError(gStrings[ST_ERROR_CREATEDUMPDIR]); - return false; - } - - string newDump = toDir + UI_DIR_SEPARATOR + Basename(dumpfile); - string newExtra = toDir + UI_DIR_SEPARATOR + Basename(extrafile); - string newMemory = toDir + UI_DIR_SEPARATOR + Basename(memoryfile); - - if (!UIMoveFile(dumpfile, newDump)) { - UIError(gStrings[ST_ERROR_DUMPFILEMOVE]); - return false; - } - - if (!UIMoveFile(extrafile, newExtra)) { - UIError(gStrings[ST_ERROR_EXTRAFILEMOVE]); - return false; - } - - if (!memoryfile.empty()) { - // Ignore errors from moving the memory file - if (!UIMoveFile(memoryfile, newMemory)) { - UIDeleteFile(memoryfile); - newMemory.erase(); - } - memoryfile = newMemory; - } - - dumpfile = newDump; - extrafile = newExtra; - - return true; -} - -static bool AddSubmittedReport(const string& serverResponse) { - StringTable responseItems; - istringstream in(serverResponse); - ReadStrings(in, responseItems, false); - - if (responseItems.find("StopSendingReportsFor") != responseItems.end()) { - // server wants to tell us to stop sending reports for a certain version - string reportPath = gSettingsPath + UI_DIR_SEPARATOR + "EndOfLife" + - responseItems["StopSendingReportsFor"]; - - ofstream* reportFile = UIOpenWrite(reportPath, ios::trunc); - if (reportFile->is_open()) { - // don't really care about the contents - *reportFile << 1 << "\n"; - reportFile->close(); - } - delete reportFile; - } - - if (responseItems.find("Discarded") != responseItems.end()) { - // server discarded this report... save it so the user can resubmit it - // manually - return false; - } - - if (responseItems.find("CrashID") == responseItems.end()) return false; - - string submittedDir = gSettingsPath + UI_DIR_SEPARATOR + "submitted"; - if (!UIEnsurePathExists(submittedDir)) { - return false; - } - - string path = - submittedDir + UI_DIR_SEPARATOR + responseItems["CrashID"] + ".txt"; - - ofstream* file = UIOpenWrite(path, ios::trunc); - if (!file->is_open()) { - delete file; - return false; - } - - char buf[1024]; - snprintf(buf, 1024, gStrings["CrashID"].c_str(), - responseItems["CrashID"].c_str()); - *file << buf << "\n"; - - if (responseItems.find("ViewURL") != responseItems.end()) { - snprintf(buf, 1024, gStrings["CrashDetailsURL"].c_str(), - responseItems["ViewURL"].c_str()); - *file << buf << "\n"; - } - - file->close(); - delete file; - - WriteSubmissionEvent(Succeeded, responseItems["CrashID"]); - return true; -} - -void DeleteDump() { - const char* noDelete = getenv("MOZ_CRASHREPORTER_NO_DELETE_DUMP"); - if (!noDelete || *noDelete == '\0') { - if (!gReporterDumpFile.empty()) UIDeleteFile(gReporterDumpFile); - if (!gExtraFile.empty()) UIDeleteFile(gExtraFile); - if (!gMemoryFile.empty()) UIDeleteFile(gMemoryFile); - } -} - -void SendCompleted(bool success, const string& serverResponse) { - if (success) { - if (AddSubmittedReport(serverResponse)) { - DeleteDump(); - } else { - string directory = gReporterDumpFile; - int slashpos = directory.find_last_of("/\\"); - if (slashpos < 2) return; - directory.resize(slashpos); - UIPruneSavedDumps(directory); - WriteSubmissionEvent(Failed, ""); - } - } else { - WriteSubmissionEvent(Failed, ""); - } -} - -static string ComputeDumpHash() { -#ifdef XP_LINUX - // On Linux we rely on the system-provided libcurl which uses nss so we have - // to also use the system-provided nss instead of the ones we have bundled. - const char* libnssNames[] = { - "libnss3.so", -# ifndef HAVE_64BIT_BUILD - // 32-bit versions on 64-bit hosts - "/usr/lib32/libnss3.so", -# endif - }; - void* lib = nullptr; - - for (const char* libname : libnssNames) { - lib = dlopen(libname, RTLD_NOW); - - if (lib) { - break; - } - } - - if (!lib) { - return ""; - } - - SECStatus (*NSS_Initialize)(const char*, const char*, const char*, - const char*, PRUint32); - HASHContext* (*HASH_Create)(HASH_HashType); - void (*HASH_Destroy)(HASHContext*); - void (*HASH_Begin)(HASHContext*); - void (*HASH_Update)(HASHContext*, const unsigned char*, unsigned int); - void (*HASH_End)(HASHContext*, unsigned char*, unsigned int*, unsigned int); - - *(void**)(&NSS_Initialize) = dlsym(lib, "NSS_Initialize"); - *(void**)(&HASH_Create) = dlsym(lib, "HASH_Create"); - *(void**)(&HASH_Destroy) = dlsym(lib, "HASH_Destroy"); - *(void**)(&HASH_Begin) = dlsym(lib, "HASH_Begin"); - *(void**)(&HASH_Update) = dlsym(lib, "HASH_Update"); - *(void**)(&HASH_End) = dlsym(lib, "HASH_End"); - - if (!HASH_Create || !HASH_Destroy || !HASH_Begin || !HASH_Update || - !HASH_End) { - return ""; - } -#endif - // Minimal NSS initialization so we can use the hash functions - const PRUint32 kNssFlags = NSS_INIT_READONLY | NSS_INIT_NOROOTINIT | - NSS_INIT_NOMODDB | NSS_INIT_NOCERTDB; - if (NSS_Initialize(nullptr, "", "", "", kNssFlags) != SECSuccess) { - return ""; - } - - HASHContext* hashContext = HASH_Create(HASH_AlgSHA256); - - if (!hashContext) { - return ""; - } - - HASH_Begin(hashContext); - - ifstream* f = UIOpenRead(gReporterDumpFile, ios::binary); - bool error = false; - - // Read the minidump contents - if (f->is_open()) { - uint8_t buff[4096]; - - do { - f->read((char*)buff, sizeof(buff)); - - if (f->bad()) { - error = true; - break; - } - - HASH_Update(hashContext, buff, f->gcount()); - } while (!f->eof()); - - f->close(); - } else { - error = true; - } - - delete f; - - // Finalize the hash computation - uint8_t result[SHA256_LENGTH]; - uint32_t resultLen = 0; - - HASH_End(hashContext, result, &resultLen, SHA256_LENGTH); - - if (resultLen != SHA256_LENGTH) { - error = true; - } - - HASH_Destroy(hashContext); - - if (!error) { - ostringstream hash; - - for (size_t i = 0; i < SHA256_LENGTH; i++) { - hash << std::setw(2) << std::setfill('0') << std::hex - << static_cast<unsigned int>(result[i]); - } - - return hash.str(); - } - return ""; // If we encountered an error, return an empty hash -} - -string GetDumpLocalID() { - string localId = Basename(gReporterDumpFile); - string::size_type dot = localId.rfind('.'); - - if (dot == string::npos) return ""; - - return localId.substr(0, dot); -} - -string GetProgramPath(const string& exename) { - string path = gArgv[0]; - size_t pos = path.rfind(UI_CRASH_REPORTER_FILENAME BIN_SUFFIX); - path.erase(pos); -#ifdef XP_MACOSX - // On macOS the crash reporter client is shipped as an application bundle - // contained within Firefox' main application bundle. So when it's invoked - // its current working directory looks like: - // Firefox.app/Contents/MacOS/crashreporter.app/Contents/MacOS/ - // The other applications we ship with Firefox are stored in the main bundle - // (Firefox.app/Contents/MacOS/) so we we need to go back three directories - // to reach them. - path.erase(pos - 1); - for (size_t i = 0; i < 3; i++) { - pos = path.rfind(UI_DIR_SEPARATOR, pos - 1); - } - - path.erase(pos + 1); -#endif // XP_MACOSX - path.append(exename + BIN_SUFFIX); - - return path; -} - -} // namespace CrashReporter - -using namespace CrashReporter; - -Json::Value kEmptyJsonString(""); - -void RewriteStrings(Json::Value& aExtraData) { - // rewrite some UI strings with the values from the query parameters - string product = aExtraData.get("ProductName", kEmptyJsonString).asString(); - Json::Value mozilla("Mozilla"); - string vendor = aExtraData.get("Vendor", mozilla).asString(); - - char buf[4096]; - snprintf(buf, sizeof(buf), gStrings[ST_CRASHREPORTERVENDORTITLE].c_str(), - vendor.c_str()); - gStrings[ST_CRASHREPORTERTITLE] = buf; - - string str = gStrings[ST_CRASHREPORTERPRODUCTERROR]; - // Only do the replacement here if the string has two - // format specifiers to start. Otherwise - // we assume it has the product name hardcoded. - string::size_type pos = str.find("%s"); - if (pos != string::npos) pos = str.find("%s", pos + 2); - if (pos != string::npos) { - // Leave a format specifier for UIError to fill in - snprintf(buf, sizeof(buf), gStrings[ST_CRASHREPORTERPRODUCTERROR].c_str(), - product.c_str(), "%s"); - gStrings[ST_CRASHREPORTERERROR] = buf; - } else { - // product name is hardcoded - gStrings[ST_CRASHREPORTERERROR] = str; - } - - snprintf(buf, sizeof(buf), gStrings[ST_CRASHREPORTERDESCRIPTION].c_str(), - product.c_str()); - gStrings[ST_CRASHREPORTERDESCRIPTION] = buf; - - snprintf(buf, sizeof(buf), gStrings[ST_CHECKSUBMIT].c_str(), vendor.c_str()); - gStrings[ST_CHECKSUBMIT] = buf; - - snprintf(buf, sizeof(buf), gStrings[ST_RESTART].c_str(), product.c_str()); - gStrings[ST_RESTART] = buf; - - snprintf(buf, sizeof(buf), gStrings[ST_QUIT].c_str(), product.c_str()); - gStrings[ST_QUIT] = buf; - - snprintf(buf, sizeof(buf), gStrings[ST_ERROR_ENDOFLIFE].c_str(), - product.c_str()); - gStrings[ST_ERROR_ENDOFLIFE] = buf; -} - -bool CheckEndOfLifed(const Json::Value& aVersion) { - if (!aVersion.isString()) { - return false; - } - - string reportPath = - gSettingsPath + UI_DIR_SEPARATOR + "EndOfLife" + aVersion.asString(); - return UIFileExists(reportPath); -} - -int main(int argc, char** argv) { - gArgc = argc; - gArgv = argv; - - string autoSubmitEnv = UIGetEnv("MOZ_CRASHREPORTER_AUTO_SUBMIT"); - gAutoSubmit = !autoSubmitEnv.empty(); - - if (!ReadConfig()) { - UIError("Couldn't read configuration."); - return 0; - } - - if (!UIInit()) { - return 0; - } - - if (argc > 1) { - gReporterDumpFile = argv[1]; - } - - if (gReporterDumpFile.empty()) { - // no dump file specified, run the default UI - if (!gAutoSubmit) { - UIShowDefaultUI(); - } - } else { - // Start by running minidump analyzer to gather stack traces. - string reporterDumpFile = gReporterDumpFile; - vector<string> args = {reporterDumpFile}; - string dumpAllThreadsEnv = UIGetEnv("MOZ_CRASHREPORTER_DUMP_ALL_THREADS"); - if (!dumpAllThreadsEnv.empty()) { - args.insert(args.begin(), "--full"); - } - UIRunProgram(CrashReporter::GetProgramPath(UI_MINIDUMP_ANALYZER_FILENAME), - args, - /* wait */ true); - - // go ahead with the crash reporter - gExtraFile = GetAdditionalFilename(gReporterDumpFile, kExtraDataExtension); - if (gExtraFile.empty()) { - UIError(gStrings[ST_ERROR_BADARGUMENTS]); - return 0; - } - - if (!UIFileExists(gExtraFile)) { - UIError(gStrings[ST_ERROR_EXTRAFILEEXISTS]); - return 0; - } - - gMemoryFile = - GetAdditionalFilename(gReporterDumpFile, kMemoryReportExtension); - if (!UIFileExists(gMemoryFile)) { - gMemoryFile.erase(); - } - - Json::Value extraData; - if (!ReadExtraFile(gExtraFile, extraData)) { - UIError(gStrings[ST_ERROR_EXTRAFILEREAD]); - return 0; - } - - if (!extraData.isMember("ProductName")) { - UIError(gStrings[ST_ERROR_NOPRODUCTNAME]); - return 0; - } - - // There is enough information in the extra file to rewrite strings - // to be product specific - RewriteStrings(extraData); - - if (!extraData.isMember("ServerURL")) { - UIError(gStrings[ST_ERROR_NOSERVERURL]); - return 0; - } - - // Hopefully the settings path exists in the environment. Try that before - // asking the platform-specific code to guess. - gSettingsPath = UIGetEnv("MOZ_CRASHREPORTER_DATA_DIRECTORY"); - if (gSettingsPath.empty()) { - string product = - extraData.get("ProductName", kEmptyJsonString).asString(); - string vendor = extraData.get("Vendor", kEmptyJsonString).asString(); - - if (!UIGetSettingsPath(vendor, product, gSettingsPath)) { - gSettingsPath.clear(); - } - } - - if (gSettingsPath.empty() || !UIEnsurePathExists(gSettingsPath)) { - UIError(gStrings[ST_ERROR_NOSETTINGSPATH]); - return 0; - } - - OpenLogFile(); - - gEventsPath = UIGetEnv("MOZ_CRASHREPORTER_EVENTS_DIRECTORY"); - gPingPath = UIGetEnv("MOZ_CRASHREPORTER_PING_DIRECTORY"); - - // Assemble and send the crash ping - string hash = ComputeDumpHash(); - - string pingUuid; - SendCrashPing(extraData, hash, pingUuid, gPingPath); - UpdateEventFile(extraData, hash, pingUuid); - - if (!UIFileExists(gReporterDumpFile)) { - UIError(gStrings[ST_ERROR_DUMPFILEEXISTS]); - return 0; - } - - string pendingDir = gSettingsPath + UI_DIR_SEPARATOR + "pending"; - if (!MoveCrashData(pendingDir, gReporterDumpFile, gExtraFile, - gMemoryFile)) { - return 0; - } - - string sendURL = extraData.get("ServerURL", kEmptyJsonString).asString(); - // we don't need to actually send these - extraData.removeMember("ServerURL"); - extraData.removeMember("StackTraces"); - - extraData["SubmittedFrom"] = "Client"; - extraData["Throttleable"] = "1"; - - // re-set XUL_APP_FILE for xulrunner wrapped apps - const char* appfile = getenv("MOZ_CRASHREPORTER_RESTART_XUL_APP_FILE"); - if (appfile && *appfile) { - const char prefix[] = "XUL_APP_FILE="; - char* env = (char*)malloc(strlen(appfile) + strlen(prefix) + 1); - if (!env) { - UIError("Out of memory"); - return 0; - } - strcpy(env, prefix); - strcat(env, appfile); - putenv(env); - free(env); - } - - vector<string> restartArgs; - - if (!extraData.isMember("WindowsErrorReporting")) { - // We relaunch the application associated with the client, but only when - // we encountered a crash caught by the exception handler. Crashes handled - // by WER are prevented from directly restarting the application. - string programPath = GetProgramPath(MOZ_APP_NAME); -#ifndef XP_WIN - const char* moz_app_launcher = getenv("MOZ_APP_LAUNCHER"); - if (moz_app_launcher) { - programPath = moz_app_launcher; - } -#endif // XP_WIN - - restartArgs.push_back(programPath); - - ostringstream paramName; - int i = 1; - paramName << "MOZ_CRASHREPORTER_RESTART_ARG_" << i++; - const char* param = getenv(paramName.str().c_str()); - while (param && *param) { - restartArgs.push_back(param); - - paramName.str(""); - paramName << "MOZ_CRASHREPORTER_RESTART_ARG_" << i++; - param = getenv(paramName.str().c_str()); - } - } - - // allow override of the server url via environment variable - // XXX: remove this in the far future when our robot - // masters force everyone to use XULRunner - char* urlEnv = getenv("MOZ_CRASHREPORTER_URL"); - if (urlEnv && *urlEnv) { - sendURL = urlEnv; - } - - // see if this version has been end-of-lifed - - if (extraData.isMember("Version") && - CheckEndOfLifed(extraData["Version"])) { - UIError(gStrings[ST_ERROR_ENDOFLIFE]); - DeleteDump(); - return 0; - } - - StringTable files; - files["upload_file_minidump"] = gReporterDumpFile; - if (!gMemoryFile.empty()) { - files["memory_report"] = gMemoryFile; - } - - if (!UIShowCrashUI(files, extraData, sendURL, restartArgs)) { - DeleteDump(); - } - } - - UIShutdown(); - - return 0; -} - -#if defined(XP_WIN) && !defined(__GNUC__) -# include <windows.h> - -// We need WinMain in order to not be a console app. This function is unused -// if we are a console application. -int WINAPI wWinMain(HINSTANCE, HINSTANCE, LPWSTR args, int) { - // Remove everything except close window from the context menu - { - HKEY hkApp; - RegCreateKeyExW(HKEY_CURRENT_USER, L"Software\\Classes\\Applications", 0, - nullptr, REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, nullptr, - &hkApp, nullptr); - RegCloseKey(hkApp); - if (RegCreateKeyExW(HKEY_CURRENT_USER, - L"Software\\Classes\\Applications\\crashreporter.exe", - 0, nullptr, REG_OPTION_VOLATILE, KEY_SET_VALUE, nullptr, - &hkApp, nullptr) == ERROR_SUCCESS) { - RegSetValueExW(hkApp, L"IsHostApp", 0, REG_NONE, 0, 0); - RegSetValueExW(hkApp, L"NoOpenWith", 0, REG_NONE, 0, 0); - RegSetValueExW(hkApp, L"NoStartPage", 0, REG_NONE, 0, 0); - RegCloseKey(hkApp); - } - } - - char** argv = static_cast<char**>(malloc(__argc * sizeof(char*))); - for (int i = 0; i < __argc; i++) { - argv[i] = strdup(WideToUTF8(__wargv[i]).c_str()); - } - - // Do the real work. - return main(__argc, argv); -} -#endif diff --git a/toolkit/crashreporter/client/crashreporter.exe.manifest b/toolkit/crashreporter/client/crashreporter.exe.manifest deleted file mode 100644 index 81aa1465c6..0000000000 --- a/toolkit/crashreporter/client/crashreporter.exe.manifest +++ /dev/null @@ -1,42 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="yes"?> -<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> -<assemblyIdentity - version="1.0.0.0" - processorArchitecture="*" - name="CrashReporter" - type="win32" -/> -<description>Crash Reporter</description> -<dependency> - <dependentAssembly> - <assemblyIdentity - type="win32" - name="Microsoft.Windows.Common-Controls" - version="6.0.0.0" - processorArchitecture="*" - publicKeyToken="6595b64144ccf1df" - language="*" - /> - </dependentAssembly> -</dependency> -<ms_asmv3:trustInfo xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3"> - <ms_asmv3:security> - <ms_asmv3:requestedPrivileges> - <ms_asmv3:requestedExecutionLevel level="asInvoker" uiAccess="false" /> - </ms_asmv3:requestedPrivileges> - </ms_asmv3:security> -</ms_asmv3:trustInfo> - <ms_asmv3:application xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3"> - <ms_asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings"> - <gdiScaling xmlns="http://schemas.microsoft.com/SMI/2017/WindowsSettings">true</gdiScaling> - </ms_asmv3:windowsSettings> - </ms_asmv3:application> - <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> - <application> - <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> - <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> - <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> - <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> - </application> - </compatibility> -</assembly> diff --git a/toolkit/crashreporter/client/crashreporter.h b/toolkit/crashreporter/client/crashreporter.h deleted file mode 100644 index fa7085da18..0000000000 --- a/toolkit/crashreporter/client/crashreporter.h +++ /dev/null @@ -1,158 +0,0 @@ -/* 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/. */ - -#ifndef CRASHREPORTER_H__ -#define CRASHREPORTER_H__ - -#ifdef _MSC_VER -# pragma warning(push) -// Disable exception handler warnings. -# pragma warning(disable : 4530) -#endif - -#include <string> -#include <map> -#include <vector> -#include <stdlib.h> -#include <stdio.h> -#include <fstream> - -#define MAX_COMMENT_LENGTH 10000 - -#if defined(XP_WIN) - -# include <windows.h> - -# define UI_DIR_SEPARATOR "\\" - -std::string WideToUTF8(const std::wstring& wide, bool* success = 0); - -#else - -# define UI_DIR_SEPARATOR "/" - -#endif - -#include "json/json.h" - -#define UI_CRASH_REPORTER_FILENAME "crashreporter" -#define UI_MINIDUMP_ANALYZER_FILENAME "minidump-analyzer" -#define UI_PING_SENDER_FILENAME "pingsender" - -typedef std::map<std::string, std::string> StringTable; - -#define ST_CRASHREPORTERTITLE "CrashReporterTitle" -#define ST_CRASHREPORTERVENDORTITLE "CrashReporterVendorTitle" -#define ST_CRASHREPORTERERROR "CrashReporterErrorText" -#define ST_CRASHREPORTERPRODUCTERROR "CrashReporterProductErrorText2" -#define ST_CRASHREPORTERHEADER "CrashReporterSorry" -#define ST_CRASHREPORTERDESCRIPTION "CrashReporterDescriptionText2" -#define ST_CRASHREPORTERDEFAULT "CrashReporterDefault" -#define ST_VIEWREPORT "Details" -#define ST_VIEWREPORTTITLE "ViewReportTitle" -#define ST_COMMENTGRAYTEXT "CommentGrayText" -#define ST_EXTRAREPORTINFO "ExtraReportInfo" -#define ST_CHECKSUBMIT "CheckSendReport" -#define ST_CHECKURL "CheckIncludeURL" -#define ST_REPORTPRESUBMIT "ReportPreSubmit2" -#define ST_REPORTDURINGSUBMIT "ReportDuringSubmit2" -#define ST_REPORTSUBMITSUCCESS "ReportSubmitSuccess" -#define ST_SUBMITFAILED "ReportSubmitFailed" -#define ST_QUIT "Quit2" -#define ST_RESTART "Restart" -#define ST_OK "Ok" -#define ST_CLOSE "Close" - -#define ST_ERROR_BADARGUMENTS "ErrorBadArguments" -#define ST_ERROR_EXTRAFILEEXISTS "ErrorExtraFileExists" -#define ST_ERROR_EXTRAFILEREAD "ErrorExtraFileRead" -#define ST_ERROR_EXTRAFILEMOVE "ErrorExtraFileMove" -#define ST_ERROR_DUMPFILEEXISTS "ErrorDumpFileExists" -#define ST_ERROR_DUMPFILEMOVE "ErrorDumpFileMove" -#define ST_ERROR_NOPRODUCTNAME "ErrorNoProductName" -#define ST_ERROR_NOSERVERURL "ErrorNoServerURL" -#define ST_ERROR_NOSETTINGSPATH "ErrorNoSettingsPath" -#define ST_ERROR_CREATEDUMPDIR "ErrorCreateDumpDir" -#define ST_ERROR_ENDOFLIFE "ErrorEndOfLife" - -//============================================================================= -// implemented in crashreporter.cpp and ping.cpp -//============================================================================= - -namespace CrashReporter { -extern StringTable gStrings; -extern std::string gSettingsPath; -extern std::string gEventsPath; -extern int gArgc; -extern char** gArgv; -extern bool gAutoSubmit; - -void UIError(const std::string& message); - -// The UI finished sending the report -void SendCompleted(bool success, const std::string& serverResponse); - -bool ReadStrings(std::istream& in, StringTable& strings, bool unescape); -bool ReadStringsFromFile(const std::string& path, StringTable& strings, - bool unescape); -void LogMessage(const std::string& message); -void DeleteDump(); - -std::string GetDumpLocalID(); -std::string GetProgramPath(const std::string& exename); - -// Telemetry ping -bool SendCrashPing(Json::Value& extra, const std::string& hash, - std::string& pingUuid, const std::string& pingDir); - -static const unsigned int kSaveCount = 10; -} // namespace CrashReporter - -//============================================================================= -// implemented in the platform-specific files -//============================================================================= - -bool UIInit(); -void UIShutdown(); - -// Run the UI for when the app was launched without a dump file -void UIShowDefaultUI(); - -// Run the UI for when the app was launched with a dump file -// Return true if the user sent (or tried to send) the crash report, -// false if they chose not to, and it should be deleted. -bool UIShowCrashUI(const StringTable& files, const Json::Value& queryParameters, - const std::string& sendURL, - const std::vector<std::string>& restartArgs); - -void UIError_impl(const std::string& message); - -bool UIGetIniPath(std::string& path); -bool UIGetSettingsPath(const std::string& vendor, const std::string& product, - std::string& settingsPath); -bool UIEnsurePathExists(const std::string& path); -bool UIFileExists(const std::string& path); -bool UIMoveFile(const std::string& oldfile, const std::string& newfile); -bool UIDeleteFile(const std::string& oldfile); -std::ifstream* UIOpenRead(const std::string& filename, - std::ios_base::openmode mode); -std::ofstream* UIOpenWrite(const std::string& filename, - std::ios_base::openmode mode); -void UIPruneSavedDumps(const std::string& directory); - -// Run the program specified by exename, passing it the parameters in arg. -// If wait is true, wait for the program to terminate execution before -// returning. Returns true if the program was launched correctly, false -// otherwise. -bool UIRunProgram(const std::string& exename, - const std::vector<std::string>& args, bool wait = false); - -// Read the environment variable specified by name -std::string UIGetEnv(const std::string& name); - -#ifdef _MSC_VER -# pragma warning(pop) -#endif - -#endif diff --git a/toolkit/crashreporter/client/crashreporter.ico b/toolkit/crashreporter/client/crashreporter.ico Binary files differdeleted file mode 100644 index 29ac3c6189..0000000000 --- a/toolkit/crashreporter/client/crashreporter.ico +++ /dev/null diff --git a/toolkit/crashreporter/client/crashreporter.rc b/toolkit/crashreporter/client/crashreporter.rc deleted file mode 100755 index f6042bf2e5..0000000000 --- a/toolkit/crashreporter/client/crashreporter.rc +++ /dev/null @@ -1,143 +0,0 @@ -/* 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/. */ - -// Microsoft Visual C++ generated resource script. -// -#include "resource.h" - -#define APSTUDIO_READONLY_SYMBOLS -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 2 resource. -// -#include "winresrc.h" - -///////////////////////////////////////////////////////////////////////////// -#undef APSTUDIO_READONLY_SYMBOLS - -///////////////////////////////////////////////////////////////////////////// -// English (U.S.) resources - -#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) -#ifdef _WIN32 -LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US -#pragma code_page(1252) -#endif //_WIN32 - -#ifdef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// TEXTINCLUDE -// - -1 TEXTINCLUDE -BEGIN - "resource.h\0" -END - -2 TEXTINCLUDE -BEGIN - "#include ""winresrc.h""\r\n" - "\0" -END - -3 TEXTINCLUDE -BEGIN - "\r\n" - "\0" -END - -#endif // APSTUDIO_INVOKED - -///////////////////////////////////////////////////////////////////////////// -// -// Icon -// - -// Icon with lowest ID value placed first to ensure application icon -// remains consistent on all systems. -IDI_MAINICON ICON "crashreporter.ico" - -///////////////////////////////////////////////////////////////////////////// -// -// AVI -// - -IDR_THROBBER AVI "Throbber-small.avi" - -///////////////////////////////////////////////////////////////////////////// -// -// Dialog -// - -IDD_SENDDIALOG DIALOGEX 0, 0, 241, 187 -STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU -EXSTYLE WS_EX_APPWINDOW -CAPTION "Sending Crash Report..." -FONT 8, "MS Shell Dlg", 400, 0, 0x1 -BEGIN - CONTROL "",IDC_DESCRIPTIONTEXT,"RICHEDIT50W",ES_MULTILINE | ES_READONLY,8,7,226,12,WS_EX_TRANSPARENT - CONTROL "tell mozilla about this crash so they can fix it",IDC_SUBMITREPORTCHECK, - "Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,25,222,10 - CHECKBOX "details...",IDC_VIEWREPORTBUTTON,24,40,54,14,BS_PUSHLIKE - EDITTEXT IDC_COMMENTTEXT,24,59,210,43,ES_MULTILINE | ES_WANTRETURN | WS_VSCROLL - CONTROL "include the address of the page i was on",IDC_INCLUDEURLCHECK, - "Button",BS_AUTOCHECKBOX | WS_TABSTOP,24,107,210,10 - CONTROL "",IDC_THROBBER,"SysAnimate32",ACS_TRANSPARENT | NOT WS_VISIBLE | WS_TABSTOP,4,152,16,16 - LTEXT "your crash report will be submitted when you restart",IDC_PROGRESSTEXT,24,152,210,10,SS_NOPREFIX - DEFPUSHBUTTON "restart firefox",IDC_RESTARTBUTTON,84,166,68,14 - PUSHBUTTON "quit without sending",IDC_CLOSEBUTTON,157,166,77,14 -END - -IDD_VIEWREPORTDIALOG DIALOGEX 0, 0, 208, 126 -STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CAPTION -CAPTION "view report" -FONT 8, "MS Shell Dlg", 400, 0, 0x1 -BEGIN - CONTROL "",IDC_VIEWREPORTTEXT,"RICHEDIT50W",ES_MULTILINE | ES_READONLY | WS_BORDER | WS_VSCROLL | WS_TABSTOP,7,7,194,92 - DEFPUSHBUTTON "OK",IDOK,151,105,50,14 -END - - -///////////////////////////////////////////////////////////////////////////// -// -// DESIGNINFO -// - -#ifdef APSTUDIO_INVOKED -GUIDELINES DESIGNINFO -BEGIN - IDD_SENDDIALOG, DIALOG - BEGIN - LEFTMARGIN, 8 - RIGHTMARGIN, 234 - TOPMARGIN, 7 - BOTTOMMARGIN, 180 - END - - IDD_VIEWREPORTDIALOG, DIALOG - BEGIN - LEFTMARGIN, 7 - RIGHTMARGIN, 201 - TOPMARGIN, 7 - BOTTOMMARGIN, 119 - END -END -#endif // APSTUDIO_INVOKED - -#endif // English (U.S.) resources -///////////////////////////////////////////////////////////////////////////// - - - -#ifndef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 3 resource. -// - - -///////////////////////////////////////////////////////////////////////////// -#endif // not APSTUDIO_INVOKED - diff --git a/toolkit/crashreporter/client/crashreporter_gtk_common.cpp b/toolkit/crashreporter/client/crashreporter_gtk_common.cpp deleted file mode 100644 index b9a957cfd9..0000000000 --- a/toolkit/crashreporter/client/crashreporter_gtk_common.cpp +++ /dev/null @@ -1,361 +0,0 @@ -/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include <unistd.h> -#include <dlfcn.h> -#include <errno.h> -#include <glib.h> -#include <gtk/gtk.h> -#include <signal.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <sys/stat.h> -#include <sys/types.h> -#include <sys/wait.h> -#include <gdk/gdkkeysyms.h> - -#include <algorithm> -#include <string> -#include <vector> - -#include "common/linux/http_upload.h" -#include "crashreporter.h" -#include "crashreporter_gtk_common.h" - -#ifndef GDK_KEY_Escape -# define GDK_KEY_Escape GDK_Escape -#endif - -using std::string; -using std::vector; - -using namespace CrashReporter; - -GtkWidget* gWindow = 0; -GtkWidget* gSubmitReportCheck = 0; -GtkWidget* gIncludeURLCheck = 0; -GtkWidget* gThrobber = 0; -GtkWidget* gProgressLabel = 0; -GtkWidget* gCloseButton = 0; -GtkWidget* gRestartButton = 0; - -bool gInitialized = false; -bool gDidTrySend = false; -StringTable gFiles; -Json::Value gQueryParameters; -string gHttpProxy; -string gAuth; -string gCACertificateFile; -string gSendURL; -string gURLParameter; -vector<string> gRestartArgs; -GThread* gSendThreadID; - -// From crashreporter_linux.cpp -void SendReport(); -void DisableGUIAndSendReport(); -void TryInitGnome(); -void UpdateSubmit(); - -static bool RestartApplication() { - char** argv = reinterpret_cast<char**>( - malloc(sizeof(char*) * (gRestartArgs.size() + 1))); - - if (!argv) return false; - - unsigned int i; - for (i = 0; i < gRestartArgs.size(); i++) { - argv[i] = (char*)gRestartArgs[i].c_str(); - } - argv[i] = 0; - - pid_t pid = fork(); - if (pid == -1) { - free(argv); - return false; - } - - if (pid == 0) { - (void)execv(argv[0], argv); - _exit(1); - } - - free(argv); - - return true; -} - -// Quit the app, used as a timeout callback -gboolean CloseApp(gpointer data) { - if (!gAutoSubmit) { - gtk_main_quit(); - } - g_thread_join(gSendThreadID); - return FALSE; -} - -static gboolean ReportCompleted(gpointer success) { - gtk_widget_hide(gThrobber); - string str = - success ? gStrings[ST_REPORTSUBMITSUCCESS] : gStrings[ST_SUBMITFAILED]; - gtk_label_set_text(GTK_LABEL(gProgressLabel), str.c_str()); - g_timeout_add(5000, CloseApp, 0); - return FALSE; -} - -#define HTTP_PROXY_DIR "/system/http_proxy" - -void LoadProxyinfo() { - class GConfClient; - typedef GConfClient* (*_gconf_default_fn)(); - typedef gboolean (*_gconf_bool_fn)(GConfClient*, const gchar*, GError**); - typedef gint (*_gconf_int_fn)(GConfClient*, const gchar*, GError**); - typedef gchar* (*_gconf_string_fn)(GConfClient*, const gchar*, GError**); - - if (getenv("http_proxy")) - return; // libcurl can use the value from the environment - - static void* gconfLib = dlopen("libgconf-2.so.4", RTLD_LAZY); - if (!gconfLib) return; - - _gconf_default_fn gconf_client_get_default = - (_gconf_default_fn)dlsym(gconfLib, "gconf_client_get_default"); - _gconf_bool_fn gconf_client_get_bool = - (_gconf_bool_fn)dlsym(gconfLib, "gconf_client_get_bool"); - _gconf_int_fn gconf_client_get_int = - (_gconf_int_fn)dlsym(gconfLib, "gconf_client_get_int"); - _gconf_string_fn gconf_client_get_string = - (_gconf_string_fn)dlsym(gconfLib, "gconf_client_get_string"); - - if (!(gconf_client_get_default && gconf_client_get_bool && - gconf_client_get_int && gconf_client_get_string)) { - dlclose(gconfLib); - return; - } - - GConfClient* conf = gconf_client_get_default(); - - if (gconf_client_get_bool(conf, HTTP_PROXY_DIR "/use_http_proxy", nullptr)) { - gint port; - gchar *host = nullptr, *httpproxy = nullptr; - - host = gconf_client_get_string(conf, HTTP_PROXY_DIR "/host", nullptr); - port = gconf_client_get_int(conf, HTTP_PROXY_DIR "/port", nullptr); - - if (port && host && *host != '\0') { - httpproxy = g_strdup_printf("http://%s:%d/", host, port); - gHttpProxy = httpproxy; - } - - g_free(host); - g_free(httpproxy); - - if (gconf_client_get_bool(conf, HTTP_PROXY_DIR "/use_authentication", - nullptr)) { - gchar *user, *password, *auth = nullptr; - - user = gconf_client_get_string( - conf, HTTP_PROXY_DIR "/authentication_user", nullptr); - password = gconf_client_get_string( - conf, HTTP_PROXY_DIR "/authentication_password", nullptr); - - if (user && password) { - auth = g_strdup_printf("%s:%s", user, password); - gAuth = auth; - } - - g_free(user); - g_free(password); - g_free(auth); - } - } - - g_object_unref(conf); - - // Don't dlclose gconfLib as libORBit-2 uses atexit(). -} - -gpointer SendThread(gpointer args) { - Json::StreamWriterBuilder builder; - builder["indentation"] = ""; - string parameters(writeString(builder, gQueryParameters)); - - string response, error; - long response_code; - - bool success = google_breakpad::HTTPUpload::SendRequest( - gSendURL, parameters, gFiles, gHttpProxy, gAuth, gCACertificateFile, - &response, &response_code, &error); - if (success) { - LogMessage("Crash report submitted successfully"); - } else { - LogMessage("Crash report submission failed: " + error); - } - - SendCompleted(success, response); - - if (!gAutoSubmit) { - // Apparently glib is threadsafe, and will schedule this - // on the main thread, see: - // http://library.gnome.org/devel/gtk-faq/stable/x499.html - g_idle_add(ReportCompleted, (gpointer)success); - } - - return nullptr; -} - -gboolean WindowDeleted(GtkWidget* window, GdkEvent* event, gpointer userData) { - SaveSettings(); - gtk_main_quit(); - return TRUE; -} - -gboolean check_escape(GtkWidget* window, GdkEventKey* event, - gpointer userData) { - if (event->keyval == GDK_KEY_Escape) { - gtk_main_quit(); - return TRUE; - } - return FALSE; -} - -static void MaybeSubmitReport() { - if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gSubmitReportCheck))) { - gDidTrySend = true; - DisableGUIAndSendReport(); - } else { - gtk_main_quit(); - } -} - -void CloseClicked(GtkButton* button, gpointer userData) { - SaveSettings(); - MaybeSubmitReport(); -} - -void RestartClicked(GtkButton* button, gpointer userData) { - SaveSettings(); - RestartApplication(); - MaybeSubmitReport(); -} - -static void UpdateURL() { - if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gIncludeURLCheck))) { - gQueryParameters["URL"] = gURLParameter; - } else { - gQueryParameters.removeMember("URL"); - } -} - -void SubmitReportChecked(GtkButton* sender, gpointer userData) { - UpdateSubmit(); -} - -void IncludeURLClicked(GtkButton* sender, gpointer userData) { UpdateURL(); } - -/* === Crashreporter UI Functions === */ - -bool UIInit() { - // breakpad probably left us with blocked signals, unblock them here - sigset_t signals, old; - sigfillset(&signals); - sigprocmask(SIG_UNBLOCK, &signals, &old); - - // tell glib we're going to use threads - g_thread_init(nullptr); - - if (gtk_init_check(&gArgc, &gArgv)) { - gInitialized = true; - - if (gStrings.find("isRTL") != gStrings.end() && gStrings["isRTL"] == "yes") - gtk_widget_set_default_direction(GTK_TEXT_DIR_RTL); - - return true; - } - - return false; -} - -void UIShowDefaultUI() { - GtkWidget* errorDialog = gtk_message_dialog_new( - nullptr, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", - gStrings[ST_CRASHREPORTERDEFAULT].c_str()); - - gtk_window_set_title(GTK_WINDOW(errorDialog), - gStrings[ST_CRASHREPORTERTITLE].c_str()); - gtk_dialog_run(GTK_DIALOG(errorDialog)); -} - -void UIError_impl(const string& message) { - if (!gInitialized) { - // Didn't initialize, this is the best we can do - printf("Error: %s\n", message.c_str()); - return; - } - - GtkWidget* errorDialog = - gtk_message_dialog_new(nullptr, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, - GTK_BUTTONS_CLOSE, "%s", message.c_str()); - - gtk_window_set_title(GTK_WINDOW(errorDialog), - gStrings[ST_CRASHREPORTERTITLE].c_str()); - gtk_dialog_run(GTK_DIALOG(errorDialog)); -} - -bool UIGetIniPath(string& path) { - path = gArgv[0]; - path.append(".ini"); - - return true; -} - -/* - * Settings are stored in ~/.vendor/product, or - * ~/.product if vendor is empty. - */ -bool UIGetSettingsPath(const string& vendor, const string& product, - string& settingsPath) { - char* home = getenv("HOME"); - - if (!home) return false; - - settingsPath = home; - settingsPath += "/."; - if (!vendor.empty()) { - string lc_vendor; - std::transform(vendor.begin(), vendor.end(), back_inserter(lc_vendor), - (int (*)(int))std::tolower); - settingsPath += lc_vendor + "/"; - } - string lc_product; - std::transform(product.begin(), product.end(), back_inserter(lc_product), - (int (*)(int))std::tolower); - settingsPath += lc_product + "/Crash Reports"; - return true; -} - -bool UIMoveFile(const string& file, const string& newfile) { - if (!rename(file.c_str(), newfile.c_str())) return true; - if (errno != EXDEV) return false; - - // use system /bin/mv instead, time to fork - pid_t pID = vfork(); - if (pID < 0) { - // Failed to fork - return false; - } - if (pID == 0) { - char* const args[4] = {const_cast<char*>("mv"), strdup(file.c_str()), - strdup(newfile.c_str()), 0}; - if (args[1] && args[2]) execve("/bin/mv", args, 0); - free(args[1]); - free(args[2]); - exit(-1); - } - int status; - waitpid(pID, &status, 0); - return UIFileExists(newfile); -} diff --git a/toolkit/crashreporter/client/crashreporter_gtk_common.h b/toolkit/crashreporter/client/crashreporter_gtk_common.h deleted file mode 100644 index 208c7ba6b0..0000000000 --- a/toolkit/crashreporter/client/crashreporter_gtk_common.h +++ /dev/null @@ -1,50 +0,0 @@ -/* 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/. */ - -#ifndef CRASHREPORTER_GTK_COMMON_H__ -#define CRASHREPORTER_GTK_COMMON_H__ - -#include <glib.h> -#include <gtk/gtk.h> - -#include <string> -#include <vector> - -#include "json/json.h" - -const char kIniFile[] = "crashreporter.ini"; - -extern GtkWidget* gWindow; -extern GtkWidget* gSubmitReportCheck; -extern GtkWidget* gIncludeURLCheck; -extern GtkWidget* gThrobber; -extern GtkWidget* gProgressLabel; -extern GtkWidget* gCloseButton; -extern GtkWidget* gRestartButton; - -extern std::vector<std::string> gRestartArgs; -extern GThread* gSendThreadID; - -extern bool gInitialized; -extern bool gDidTrySend; -extern StringTable gFiles; -extern Json::Value gQueryParameters; -extern std::string gHttpProxy; -extern std::string gAuth; -extern std::string gCACertificateFile; -extern std::string gSendURL; -extern std::string gURLParameter; - -void LoadProxyinfo(); -gboolean CloseApp(gpointer data); -gpointer SendThread(gpointer args); -gboolean WindowDeleted(GtkWidget* window, GdkEvent* event, gpointer userData); -gboolean check_escape(GtkWidget* window, GdkEventKey* event, gpointer data); -void SubmitReportChecked(GtkButton* sender, gpointer userData); -void IncludeURLClicked(GtkButton* sender, gpointer userData); -void CloseClicked(GtkButton* button, gpointer userData); -void RestartClicked(GtkButton* button, gpointer userData); -void SaveSettings(void); - -#endif // CRASHREPORTER_GTK_COMMON_H__ diff --git a/toolkit/crashreporter/client/crashreporter_linux.cpp b/toolkit/crashreporter/client/crashreporter_linux.cpp deleted file mode 100644 index 644b34654e..0000000000 --- a/toolkit/crashreporter/client/crashreporter_linux.cpp +++ /dev/null @@ -1,525 +0,0 @@ -/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include <dlfcn.h> -#include <fcntl.h> -#include <glib.h> -#include <gtk/gtk.h> -#include <string.h> - -#include <cctype> - -#include "crashreporter.h" -#include "crashreporter_gtk_common.h" - -#define LABEL_MAX_CHAR_WIDTH 48 - -using std::ios; -using std::string; -using std::vector; - -using namespace CrashReporter; - -static GtkWidget* gViewReportButton = 0; -static GtkWidget* gCommentTextLabel = 0; -static GtkWidget* gCommentText = 0; - -static bool gCommentFieldHint = true; - -// handle from dlopen'ing libgnome -static void* gnomeLib = nullptr; -// handle from dlopen'ing libgnomeui -static void* gnomeuiLib = nullptr; - -static void LoadSettings() { - /* - * NOTE! This code needs to stay in sync with the preference checking - * code in in nsExceptionHandler.cpp. - */ - - bool includeURL = true; - bool submitReport = true; - StringTable settings; - if (ReadStringsFromFile(gSettingsPath + "/" + kIniFile, settings, true)) { - if (settings.find("IncludeURL") != settings.end()) { - includeURL = settings["IncludeURL"][0] != '0'; - } - if (settings.find("SubmitReport") != settings.end()) { - submitReport = settings["SubmitReport"][0] != '0'; - } - } - - if (gIncludeURLCheck) { - gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(gIncludeURLCheck), - includeURL); - } - gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(gSubmitReportCheck), - submitReport); -} - -static string Escape(const string& str) { - string ret; - for (auto c : str) { - if (c == '\\') { - ret += "\\\\"; - } else if (c == '\n') { - ret += "\\n"; - } else if (c == '\t') { - ret += "\\t"; - } else { - ret.push_back(c); - } - } - - return ret; -} - -static bool WriteStrings(std::ostream& out, const string& header, - StringTable& strings, bool escape) { - out << "[" << header << "]\n"; - for (const auto& iter : strings) { - out << iter.first << "="; - if (escape) { - out << Escape(iter.second); - } else { - out << iter.second; - } - - out << '\n'; - } - - return true; -} - -static bool WriteStringsToFile(const string& path, const string& header, - StringTable& strings, bool escape) { - std::ofstream* f = UIOpenWrite(path, ios::trunc); - bool success = false; - if (f->is_open()) { - success = WriteStrings(*f, header, strings, escape); - f->close(); - } - - delete f; - return success; -} - -void SaveSettings() { - /* - * NOTE! This code needs to stay in sync with the preference setting - * code in in nsExceptionHandler.cpp. - */ - - StringTable settings; - - ReadStringsFromFile(gSettingsPath + "/" + kIniFile, settings, true); - if (gIncludeURLCheck != 0) - settings["IncludeURL"] = - gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gIncludeURLCheck)) ? "1" - : "0"; - settings["SubmitReport"] = - gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gSubmitReportCheck)) ? "1" - : "0"; - - WriteStringsToFile(gSettingsPath + "/" + kIniFile, "Crash Reporter", settings, - true); -} - -void SendReport() { - LoadProxyinfo(); - - // spawn a thread to do the sending - gSendThreadID = g_thread_create(SendThread, nullptr, TRUE, nullptr); -} - -void DisableGUIAndSendReport() { - // disable all our gui controls, show the throbber + change the progress text - gtk_widget_set_sensitive(gSubmitReportCheck, FALSE); - gtk_widget_set_sensitive(gViewReportButton, FALSE); - gtk_widget_set_sensitive(gCommentText, FALSE); - if (gIncludeURLCheck) gtk_widget_set_sensitive(gIncludeURLCheck, FALSE); - gtk_widget_set_sensitive(gCloseButton, FALSE); - if (gRestartButton) gtk_widget_set_sensitive(gRestartButton, FALSE); - gtk_widget_show_all(gThrobber); - gtk_label_set_text(GTK_LABEL(gProgressLabel), - gStrings[ST_REPORTDURINGSUBMIT].c_str()); - - SendReport(); -} - -static void ShowReportInfo(GtkTextView* viewReportTextView) { - GtkTextBuffer* buffer = gtk_text_view_get_buffer(viewReportTextView); - - GtkTextIter start, end; - gtk_text_buffer_get_start_iter(buffer, &start); - gtk_text_buffer_get_end_iter(buffer, &end); - - gtk_text_buffer_delete(buffer, &start, &end); - - for (Json::ValueConstIterator iter = gQueryParameters.begin(); - iter != gQueryParameters.end(); ++iter) { - gtk_text_buffer_insert(buffer, &end, iter.name().c_str(), - iter.name().length()); - gtk_text_buffer_insert(buffer, &end, ": ", -1); - string value; - if (iter->isString()) { - value = iter->asString(); - } else { - Json::StreamWriterBuilder builder; - builder["indentation"] = ""; - value = writeString(builder, *iter); - } - gtk_text_buffer_insert(buffer, &end, value.c_str(), value.length()); - gtk_text_buffer_insert(buffer, &end, "\n", -1); - } - - gtk_text_buffer_insert(buffer, &end, "\n", -1); - gtk_text_buffer_insert(buffer, &end, gStrings[ST_EXTRAREPORTINFO].c_str(), - -1); -} - -void UpdateSubmit() { - if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gSubmitReportCheck))) { - gtk_widget_set_sensitive(gViewReportButton, TRUE); - gtk_widget_set_sensitive(gCommentText, TRUE); - if (gIncludeURLCheck) gtk_widget_set_sensitive(gIncludeURLCheck, TRUE); - gtk_label_set_text(GTK_LABEL(gProgressLabel), - gStrings[ST_REPORTPRESUBMIT].c_str()); - } else { - gtk_widget_set_sensitive(gViewReportButton, FALSE); - gtk_widget_set_sensitive(gCommentText, FALSE); - if (gIncludeURLCheck) gtk_widget_set_sensitive(gIncludeURLCheck, FALSE); - gtk_label_set_text(GTK_LABEL(gProgressLabel), ""); - } -} - -static void ViewReportClicked(GtkButton* button, gpointer userData) { - GtkDialog* dialog = GTK_DIALOG(gtk_dialog_new_with_buttons( - gStrings[ST_VIEWREPORTTITLE].c_str(), GTK_WINDOW(gWindow), - GTK_DIALOG_MODAL, GTK_STOCK_OK, GTK_RESPONSE_OK, nullptr)); - - GtkWidget* scrolled = gtk_scrolled_window_new(0, 0); - gtk_container_add(GTK_CONTAINER(gtk_dialog_get_content_area(dialog)), - scrolled); - gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled), - GTK_POLICY_AUTOMATIC, GTK_POLICY_ALWAYS); - gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), - GTK_SHADOW_IN); - gtk_widget_set_vexpand(scrolled, TRUE); - - GtkWidget* viewReportTextView = gtk_text_view_new(); - gtk_container_add(GTK_CONTAINER(scrolled), viewReportTextView); - gtk_text_view_set_editable(GTK_TEXT_VIEW(viewReportTextView), FALSE); - gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(viewReportTextView), GTK_WRAP_WORD); - gtk_widget_set_size_request(GTK_WIDGET(viewReportTextView), -1, 100); - - ShowReportInfo(GTK_TEXT_VIEW(viewReportTextView)); - - gtk_dialog_set_default_response(dialog, GTK_RESPONSE_OK); - gtk_widget_set_size_request(GTK_WIDGET(dialog), 400, 200); - gtk_widget_show_all(GTK_WIDGET(dialog)); - gtk_dialog_run(dialog); - gtk_widget_destroy(GTK_WIDGET(dialog)); -} - -static void CommentChanged(GtkTextBuffer* buffer, gpointer userData) { - GtkTextIter start, end; - gtk_text_buffer_get_start_iter(buffer, &start); - gtk_text_buffer_get_end_iter(buffer, &end); - const char* comment = gtk_text_buffer_get_text(buffer, &start, &end, TRUE); - if (comment[0] == '\0' || gCommentFieldHint) { - gQueryParameters.removeMember("Comments"); - } else { - gQueryParameters["Comments"] = comment; - } -} - -static void CommentInsert(GtkTextBuffer* buffer, GtkTextIter* location, - gchar* text, gint len, gpointer userData) { - GtkTextIter start, end; - gtk_text_buffer_get_start_iter(buffer, &start); - gtk_text_buffer_get_end_iter(buffer, &end); - const char* comment = gtk_text_buffer_get_text(buffer, &start, &end, TRUE); - - // limit to 500 bytes in utf-8 - if (strlen(comment) + len > MAX_COMMENT_LENGTH) { - g_signal_stop_emission_by_name(buffer, "insert-text"); - } -} - -static void UpdateHintText(GtkWidget* widget, gboolean gainedFocus, - bool* hintShowing, const char* hintText) { - GtkTextBuffer* buffer = nullptr; - if (GTK_IS_TEXT_VIEW(widget)) - buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget)); - - if (gainedFocus) { - if (*hintShowing) { - if (buffer == nullptr) { // sort of cheating - gtk_entry_set_text(GTK_ENTRY(widget), ""); - } else { // GtkTextView - gtk_text_buffer_set_text(buffer, "", 0); - } - gtk_widget_modify_text(widget, GTK_STATE_NORMAL, nullptr); - *hintShowing = false; - } - } else { - // lost focus - const char* text = nullptr; - if (buffer == nullptr) { - text = gtk_entry_get_text(GTK_ENTRY(widget)); - } else { - GtkTextIter start, end; - gtk_text_buffer_get_start_iter(buffer, &start); - gtk_text_buffer_get_end_iter(buffer, &end); - text = gtk_text_buffer_get_text(buffer, &start, &end, TRUE); - } - - if (text == nullptr || text[0] == '\0') { - *hintShowing = true; - - if (buffer == nullptr) { - gtk_entry_set_text(GTK_ENTRY(widget), hintText); - } else { - gtk_text_buffer_set_text(buffer, hintText, -1); - } - - gtk_widget_modify_text( - widget, GTK_STATE_NORMAL, - >k_widget_get_style(widget)->text[GTK_STATE_INSENSITIVE]); - } - } -} - -static gboolean CommentFocusChange(GtkWidget* widget, GdkEventFocus* event, - gpointer userData) { - UpdateHintText(widget, event->in, &gCommentFieldHint, - gStrings[ST_COMMENTGRAYTEXT].c_str()); - - return FALSE; -} - -typedef struct _GnomeProgram GnomeProgram; -typedef struct _GnomeModuleInfo GnomeModuleInfo; -typedef GnomeProgram* (*_gnome_program_init_fn)(const char*, const char*, - const GnomeModuleInfo*, int, - char**, const char*, ...); -typedef const GnomeModuleInfo* (*_libgnomeui_module_info_get_fn)(); - -void TryInitGnome() { - gnomeLib = dlopen("libgnome-2.so.0", RTLD_LAZY); - if (!gnomeLib) return; - - gnomeuiLib = dlopen("libgnomeui-2.so.0", RTLD_LAZY); - if (!gnomeuiLib) return; - - _gnome_program_init_fn gnome_program_init = - (_gnome_program_init_fn)(dlsym(gnomeLib, "gnome_program_init")); - _libgnomeui_module_info_get_fn libgnomeui_module_info_get = - (_libgnomeui_module_info_get_fn)(dlsym(gnomeuiLib, - "libgnomeui_module_info_get")); - - if (gnome_program_init && libgnomeui_module_info_get) { - gnome_program_init("crashreporter", "1.0", libgnomeui_module_info_get(), - gArgc, gArgv, nullptr); - } -} - -/* === Crashreporter UI Functions === */ - -/* - * Anything not listed here is in crashreporter_gtk_common.cpp: - * UIInit - * UIShowDefaultUI - * UIError_impl - * UIGetIniPath - * UIGetSettingsPath - * UIEnsurePathExists - * UIFileExists - * UIMoveFile - * UIDeleteFile - * UIOpenRead - * UIOpenWrite - */ - -void UIShutdown() { - if (gnomeuiLib) dlclose(gnomeuiLib); - // Don't dlclose gnomeLib as libgnomevfs and libORBit-2 use atexit(). -} - -bool UIShowCrashUI(const StringTable& files, const Json::Value& queryParameters, - const string& sendURL, const vector<string>& restartArgs) { - gFiles = files; - gQueryParameters = queryParameters; - gSendURL = sendURL; - gRestartArgs = restartArgs; - if (gQueryParameters.isMember("URL")) { - gURLParameter = gQueryParameters["URL"].asString(); - } - - if (gAutoSubmit) { - SendReport(); - CloseApp(nullptr); - return true; - } - - gWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL); - gtk_window_set_title(GTK_WINDOW(gWindow), - gStrings[ST_CRASHREPORTERTITLE].c_str()); - gtk_window_set_resizable(GTK_WINDOW(gWindow), FALSE); - gtk_window_set_position(GTK_WINDOW(gWindow), GTK_WIN_POS_CENTER); - gtk_container_set_border_width(GTK_CONTAINER(gWindow), 12); - g_signal_connect(gWindow, "delete-event", G_CALLBACK(WindowDeleted), 0); - g_signal_connect(gWindow, "key_press_event", G_CALLBACK(check_escape), - nullptr); - - GtkWidget* vbox = gtk_vbox_new(FALSE, 6); - gtk_container_add(GTK_CONTAINER(gWindow), vbox); - - GtkWidget* titleLabel = gtk_label_new(""); - gtk_box_pack_start(GTK_BOX(vbox), titleLabel, FALSE, FALSE, 0); - gtk_misc_set_alignment(GTK_MISC(titleLabel), 0, 0.5); - char* markup = - g_strdup_printf("<b>%s</b>", gStrings[ST_CRASHREPORTERHEADER].c_str()); - gtk_label_set_markup(GTK_LABEL(titleLabel), markup); - g_free(markup); - - GtkWidget* descriptionLabel = - gtk_label_new(gStrings[ST_CRASHREPORTERDESCRIPTION].c_str()); - gtk_box_pack_start(GTK_BOX(vbox), descriptionLabel, TRUE, TRUE, 0); - // force the label to line wrap - gtk_label_set_max_width_chars(GTK_LABEL(descriptionLabel), - LABEL_MAX_CHAR_WIDTH); - gtk_label_set_line_wrap(GTK_LABEL(descriptionLabel), TRUE); - gtk_label_set_selectable(GTK_LABEL(descriptionLabel), TRUE); - gtk_misc_set_alignment(GTK_MISC(descriptionLabel), 0, 0.5); - - // this is honestly how they suggest you indent a section - GtkWidget* indentBox = gtk_hbox_new(FALSE, 0); - gtk_box_pack_start(GTK_BOX(vbox), indentBox, FALSE, FALSE, 0); - gtk_box_pack_start(GTK_BOX(indentBox), gtk_label_new(""), FALSE, FALSE, 6); - - GtkWidget* innerVBox1 = gtk_vbox_new(FALSE, 0); - gtk_box_pack_start(GTK_BOX(indentBox), innerVBox1, TRUE, TRUE, 0); - - gSubmitReportCheck = - gtk_check_button_new_with_label(gStrings[ST_CHECKSUBMIT].c_str()); - gtk_box_pack_start(GTK_BOX(innerVBox1), gSubmitReportCheck, FALSE, FALSE, 0); - g_signal_connect(gSubmitReportCheck, "clicked", - G_CALLBACK(SubmitReportChecked), 0); - - // indent again, below the "submit report" checkbox - GtkWidget* indentBox2 = gtk_hbox_new(FALSE, 0); - gtk_box_pack_start(GTK_BOX(innerVBox1), indentBox2, FALSE, FALSE, 0); - gtk_box_pack_start(GTK_BOX(indentBox2), gtk_label_new(""), FALSE, FALSE, 6); - - GtkWidget* innerVBox = gtk_vbox_new(FALSE, 0); - gtk_box_pack_start(GTK_BOX(indentBox2), innerVBox, TRUE, TRUE, 0); - gtk_box_set_spacing(GTK_BOX(innerVBox), 6); - - GtkWidget* viewReportButtonBox = gtk_hbutton_box_new(); - gtk_box_pack_start(GTK_BOX(innerVBox), viewReportButtonBox, FALSE, FALSE, 0); - gtk_box_set_spacing(GTK_BOX(viewReportButtonBox), 6); - gtk_button_box_set_layout(GTK_BUTTON_BOX(viewReportButtonBox), - GTK_BUTTONBOX_START); - - gViewReportButton = - gtk_button_new_with_label(gStrings[ST_VIEWREPORT].c_str()); - gtk_box_pack_start(GTK_BOX(viewReportButtonBox), gViewReportButton, FALSE, - FALSE, 0); - g_signal_connect(gViewReportButton, "clicked", G_CALLBACK(ViewReportClicked), - 0); - - GtkWidget* scrolled = gtk_scrolled_window_new(0, 0); - gtk_container_add(GTK_CONTAINER(innerVBox), scrolled); - gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled), - GTK_POLICY_NEVER, GTK_POLICY_ALWAYS); - gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), - GTK_SHADOW_IN); - gtk_scrolled_window_set_min_content_height(GTK_SCROLLED_WINDOW(scrolled), - 100); - - gCommentTextLabel = gtk_label_new(gStrings[ST_COMMENTGRAYTEXT].c_str()); - gCommentText = gtk_text_view_new(); - gtk_label_set_mnemonic_widget(GTK_LABEL(gCommentTextLabel), gCommentText); - gtk_text_view_set_accepts_tab(GTK_TEXT_VIEW(gCommentText), FALSE); - g_signal_connect(gCommentText, "focus-in-event", - G_CALLBACK(CommentFocusChange), 0); - g_signal_connect(gCommentText, "focus-out-event", - G_CALLBACK(CommentFocusChange), 0); - - GtkTextBuffer* commentBuffer = - gtk_text_view_get_buffer(GTK_TEXT_VIEW(gCommentText)); - g_signal_connect(commentBuffer, "changed", G_CALLBACK(CommentChanged), 0); - g_signal_connect(commentBuffer, "insert-text", G_CALLBACK(CommentInsert), 0); - - gtk_container_add(GTK_CONTAINER(scrolled), gCommentText); - gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(gCommentText), GTK_WRAP_WORD_CHAR); - gtk_widget_set_size_request(GTK_WIDGET(gCommentText), -1, 100); - - if (gQueryParameters.isMember("URL")) { - gIncludeURLCheck = - gtk_check_button_new_with_label(gStrings[ST_CHECKURL].c_str()); - gtk_box_pack_start(GTK_BOX(innerVBox), gIncludeURLCheck, FALSE, FALSE, 0); - g_signal_connect(gIncludeURLCheck, "clicked", G_CALLBACK(IncludeURLClicked), - 0); - // on by default - gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(gIncludeURLCheck), TRUE); - } - - GtkWidget* progressBox = gtk_hbox_new(FALSE, 6); - gtk_box_pack_start(GTK_BOX(vbox), progressBox, TRUE, TRUE, 0); - - // Get the throbber image from alongside the executable - char* dir = g_path_get_dirname(gArgv[0]); - char* path = g_build_filename(dir, "Throbber-small.gif", nullptr); - g_free(dir); - gThrobber = gtk_image_new_from_file(path); - gtk_box_pack_start(GTK_BOX(progressBox), gThrobber, FALSE, FALSE, 0); - - gProgressLabel = gtk_label_new(gStrings[ST_REPORTPRESUBMIT].c_str()); - gtk_box_pack_start(GTK_BOX(progressBox), gProgressLabel, TRUE, TRUE, 0); - // force the label to line wrap - gtk_label_set_max_width_chars(GTK_LABEL(gProgressLabel), - LABEL_MAX_CHAR_WIDTH); - gtk_label_set_line_wrap(GTK_LABEL(gProgressLabel), TRUE); - - GtkWidget* buttonBox = gtk_hbutton_box_new(); - gtk_box_pack_end(GTK_BOX(vbox), buttonBox, FALSE, FALSE, 0); - gtk_box_set_spacing(GTK_BOX(buttonBox), 6); - gtk_button_box_set_layout(GTK_BUTTON_BOX(buttonBox), GTK_BUTTONBOX_END); - - gCloseButton = gtk_button_new_with_label(gStrings[ST_QUIT].c_str()); - gtk_box_pack_start(GTK_BOX(buttonBox), gCloseButton, FALSE, FALSE, 0); - gtk_widget_set_can_default(gCloseButton, TRUE); - g_signal_connect(gCloseButton, "clicked", G_CALLBACK(CloseClicked), 0); - - gRestartButton = 0; - if (!restartArgs.empty()) { - gRestartButton = gtk_button_new_with_label(gStrings[ST_RESTART].c_str()); - gtk_box_pack_start(GTK_BOX(buttonBox), gRestartButton, FALSE, FALSE, 0); - gtk_widget_set_can_default(gRestartButton, TRUE); - g_signal_connect(gRestartButton, "clicked", G_CALLBACK(RestartClicked), 0); - } - - gtk_widget_grab_focus(gSubmitReportCheck); - - gtk_widget_grab_default(gRestartButton ? gRestartButton : gCloseButton); - - LoadSettings(); - - UpdateSubmit(); - - UpdateHintText(gCommentText, FALSE, &gCommentFieldHint, - gStrings[ST_COMMENTGRAYTEXT].c_str()); - - gtk_widget_show_all(gWindow); - // stick this here to avoid the show_all above... - gtk_widget_hide(gThrobber); - - gtk_main(); - - return gDidTrySend; -} diff --git a/toolkit/crashreporter/client/crashreporter_osx.h b/toolkit/crashreporter/client/crashreporter_osx.h deleted file mode 100644 index d9aeddb4b0..0000000000 --- a/toolkit/crashreporter/client/crashreporter_osx.h +++ /dev/null @@ -1,108 +0,0 @@ -/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#ifndef CRASHREPORTER_OSX_H__ -#define CRASHREPORTER_OSX_H__ - -#include <Cocoa/Cocoa.h> -#include "HTTPMultipartUpload.h" -#include "crashreporter.h" -#include "json/json.h" - -// Defined below -@class TextViewWithPlaceHolder; - -@interface CrashReporterUI : NSObject { - IBOutlet NSWindow* mWindow; - - /* Crash reporter view */ - IBOutlet NSTextField* mHeaderLabel; - IBOutlet NSTextField* mDescriptionLabel; - IBOutlet NSButton* mViewReportButton; - IBOutlet NSScrollView* mCommentScrollView; - IBOutlet TextViewWithPlaceHolder* mCommentText; - IBOutlet NSButton* mSubmitReportButton; - IBOutlet NSButton* mIncludeURLButton; - IBOutlet NSButton* mEmailMeButton; - IBOutlet NSTextField* mEmailText; - IBOutlet NSButton* mCloseButton; - IBOutlet NSButton* mRestartButton; - IBOutlet NSProgressIndicator* mProgressIndicator; - IBOutlet NSTextField* mProgressText; - - /* Error view */ - IBOutlet NSView* mErrorView; - IBOutlet NSTextField* mErrorHeaderLabel; - IBOutlet NSTextField* mErrorLabel; - IBOutlet NSButton* mErrorCloseButton; - - /* For "show info" alert */ - IBOutlet NSWindow* mViewReportWindow; - IBOutlet NSTextView* mViewReportTextView; - IBOutlet NSButton* mViewReportOkButton; - - HTTPMultipartUpload* mPost; -} - -- (void)showCrashUI:(const StringTable&)files - queryParameters:(const Json::Value&)queryParameters - sendURL:(const std::string&)sendURL; -- (void)showErrorUI:(const std::string&)message; -- (void)showReportInfo; -- (void)maybeSubmitReport; -- (void)closeMeDown:(id)unused; - -- (IBAction)submitReportClicked:(id)sender; -- (IBAction)viewReportClicked:(id)sender; -- (IBAction)viewReportOkClicked:(id)sender; -- (IBAction)closeClicked:(id)sender; -- (IBAction)restartClicked:(id)sender; -- (IBAction)includeURLClicked:(id)sender; - -- (void)textDidChange:(NSNotification*)aNotification; -- (BOOL)textView:(NSTextView*)aTextView - shouldChangeTextInRange:(NSRange)affectedCharRange - replacementString:(NSString*)replacementString; - -- (void)doInitialResizing; -- (float)setStringFitVertically:(NSControl*)control - string:(NSString*)str - resizeWindow:(BOOL)resizeWindow; -- (void)setView:(NSView*)v animate:(BOOL)animate; -- (void)enableControls:(BOOL)enabled; -- (void)updateSubmit; -- (void)updateURL; -- (void)updateEmail; -- (void)sendReport; -- (bool)setupPost; -- (void)uploadThread:(HTTPMultipartUpload*)post; -- (void)uploadComplete:(NSData*)data; - -- (BOOL)applicationShouldTerminateAfterLastWindowClosed: - (NSApplication*)theApplication; -- (void)applicationWillTerminate:(NSNotification*)aNotification; - -@end - -/* - * Subclass NSTextView to provide a text view with placeholder text. - * Also provide a setEnabled implementation. - */ -@interface TextViewWithPlaceHolder : NSTextView { - NSMutableAttributedString* mPlaceHolderString; -} - -- (BOOL)becomeFirstResponder; -- (void)drawRect:(NSRect)rect; -- (BOOL)resignFirstResponder; -- (void)setPlaceholder:(NSString*)placeholder; -- (void)insertTab:(id)sender; -- (void)insertBacktab:(id)sender; -- (void)setEnabled:(BOOL)enabled; -- (void)dealloc; - -@end - -#endif diff --git a/toolkit/crashreporter/client/crashreporter_osx.mm b/toolkit/crashreporter/client/crashreporter_osx.mm deleted file mode 100644 index b6d5d8ac6f..0000000000 --- a/toolkit/crashreporter/client/crashreporter_osx.mm +++ /dev/null @@ -1,805 +0,0 @@ -/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#import <Cocoa/Cocoa.h> -#import <CoreFoundation/CoreFoundation.h> -#include "crashreporter.h" -#include "crashreporter_osx.h" -#include <crt_externs.h> -#include <spawn.h> -#include <sys/stat.h> -#include <sys/types.h> -#include <fcntl.h> -#include <sstream> - -using std::ostringstream; -using std::string; -using std::vector; - -using namespace CrashReporter; - -static NSAutoreleasePool* gMainPool; -static CrashReporterUI* gUI = 0; -static StringTable gFiles; -static Json::Value gQueryParameters; -static string gURLParameter; -static string gSendURL; -static vector<string> gRestartArgs; -static bool gDidTrySend = false; -static bool gRTLlayout = false; - -static cpu_type_t pref_cpu_types[2] = { -#if defined(__i386__) - CPU_TYPE_X86, -#elif defined(__x86_64__) - CPU_TYPE_X86_64, -#elif defined(__ppc__) - CPU_TYPE_POWERPC, -#elif defined(__aarch64__) - CPU_TYPE_ARM64, -#endif - CPU_TYPE_ANY}; - -#define NSSTR(s) [NSString stringWithUTF8String:(s).c_str()] - -static NSString* Str(const char* aName) { - string str = gStrings[aName]; - if (str.empty()) str = "?"; - return NSSTR(str); -} - -static bool RestartApplication() { - vector<char*> argv(gRestartArgs.size() + 1); - - posix_spawnattr_t spawnattr; - if (posix_spawnattr_init(&spawnattr) != 0) { - return false; - } - - // Set spawn attributes. - size_t attr_count = sizeof(pref_cpu_types) / sizeof(pref_cpu_types[0]); - size_t attr_ocount = 0; - if (posix_spawnattr_setbinpref_np(&spawnattr, attr_count, pref_cpu_types, - &attr_ocount) != 0 || - attr_ocount != attr_count) { - posix_spawnattr_destroy(&spawnattr); - return false; - } - - unsigned int i; - for (i = 0; i < gRestartArgs.size(); i++) { - argv[i] = (char*)gRestartArgs[i].c_str(); - } - argv[i] = 0; - - char** env = NULL; - char*** nsEnv = _NSGetEnviron(); - if (nsEnv) env = *nsEnv; - int result = posix_spawnp(NULL, argv[0], NULL, &spawnattr, &argv[0], env); - - posix_spawnattr_destroy(&spawnattr); - - return result == 0; -} - -@implementation CrashReporterUI - -- (void)awakeFromNib { - gUI = self; - [mWindow center]; - - [mWindow setTitle:[[NSBundle mainBundle] - objectForInfoDictionaryKey:@"CFBundleName"]]; - [NSApp activateIgnoringOtherApps:YES]; -} - -- (void)showCrashUI:(const StringTable&)files - queryParameters:(const Json::Value&)queryParameters - sendURL:(const string&)sendURL { - gFiles = files; - gQueryParameters = queryParameters; - gSendURL = sendURL; - - if (gAutoSubmit) { - gDidTrySend = true; - [self sendReport]; - return; - } - - [mWindow setTitle:Str(ST_CRASHREPORTERTITLE)]; - [mHeaderLabel setStringValue:Str(ST_CRASHREPORTERHEADER)]; - - NSRect viewReportFrame = [mViewReportButton frame]; - [mViewReportButton setTitle:Str(ST_VIEWREPORT)]; - [mViewReportButton sizeToFit]; - if (gRTLlayout) { - // sizeToFit will keep the left side fixed, so realign - float oldWidth = viewReportFrame.size.width; - viewReportFrame = [mViewReportButton frame]; - viewReportFrame.origin.x += oldWidth - viewReportFrame.size.width; - [mViewReportButton setFrame:viewReportFrame]; - } - - [mSubmitReportButton setTitle:Str(ST_CHECKSUBMIT)]; - [mIncludeURLButton setTitle:Str(ST_CHECKURL)]; - [mViewReportOkButton setTitle:Str(ST_OK)]; - - [mCommentText setPlaceholder:Str(ST_COMMENTGRAYTEXT)]; - if (gRTLlayout) [mCommentText toggleBaseWritingDirection:self]; - - if (gQueryParameters.isMember("URL")) { - // save the URL value in case the checkbox gets unchecked - gURLParameter = gQueryParameters["URL"].asString(); - } else { - // no URL specified, hide checkbox - [mIncludeURLButton removeFromSuperview]; - // shrink window to fit - NSRect frame = [mWindow frame]; - NSRect includeURLFrame = [mIncludeURLButton frame]; - NSRect emailFrame = [mEmailMeButton frame]; - int buttonMask = [mViewReportButton autoresizingMask]; - int checkMask = [mSubmitReportButton autoresizingMask]; - int commentScrollMask = [mCommentScrollView autoresizingMask]; - - [mViewReportButton setAutoresizingMask:NSViewMinYMargin]; - [mSubmitReportButton setAutoresizingMask:NSViewMinYMargin]; - [mCommentScrollView setAutoresizingMask:NSViewMinYMargin]; - - // remove all the space in between - frame.size.height -= includeURLFrame.origin.y - emailFrame.origin.y; - [mWindow setFrame:frame display:true animate:NO]; - - [mViewReportButton setAutoresizingMask:buttonMask]; - [mSubmitReportButton setAutoresizingMask:checkMask]; - [mCommentScrollView setAutoresizingMask:commentScrollMask]; - } - - // resize some buttons horizontally and possibly some controls vertically - [self doInitialResizing]; - - // load default state of submit checkbox - // we don't just do this via IB because we want the default to be - // off a certain percentage of the time - BOOL submitChecked = YES; - NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults]; - if (nil != [userDefaults objectForKey:@"submitReport"]) { - submitChecked = [userDefaults boolForKey:@"submitReport"]; - } else { - [userDefaults setBool:submitChecked forKey:@"submitReport"]; - } - [mSubmitReportButton setState:(submitChecked ? NSOnState : NSOffState)]; - - // load default state of include URL checkbox - BOOL includeChecked = YES; - if (nil != [userDefaults objectForKey:@"IncludeURL"]) { - includeChecked = [userDefaults boolForKey:@"IncludeURL"]; - } else { - [userDefaults setBool:includeChecked forKey:@"IncludeURL"]; - } - [mIncludeURLButton setState:(includeChecked ? NSOnState : NSOffState)]; - - [self updateSubmit]; - [self updateURL]; - [self updateEmail]; - - [mWindow makeKeyAndOrderFront:nil]; -} - -- (void)showErrorUI:(const string&)message { - [self setView:mErrorView animate:NO]; - - [mErrorHeaderLabel setStringValue:Str(ST_CRASHREPORTERHEADER)]; - [self setStringFitVertically:mErrorLabel - string:NSSTR(message) - resizeWindow:YES]; - [mErrorCloseButton setTitle:Str(ST_OK)]; - - [mErrorCloseButton setKeyEquivalent:@"\r"]; - [mWindow makeFirstResponder:mErrorCloseButton]; - [mWindow makeKeyAndOrderFront:nil]; -} - -- (void)showReportInfo { - NSDictionary* boldAttr = @{ - NSFontAttributeName : - [NSFont boldSystemFontOfSize:[NSFont smallSystemFontSize]], - NSForegroundColorAttributeName : NSColor.textColor, - }; - NSDictionary* normalAttr = @{ - NSFontAttributeName : - [NSFont systemFontOfSize:[NSFont smallSystemFontSize]], - NSForegroundColorAttributeName : NSColor.textColor, - }; - - [mViewReportTextView setString:@""]; - for (Json::ValueConstIterator iter = gQueryParameters.begin(); - iter != gQueryParameters.end(); ++iter) { - NSAttributedString* key = - [[NSAttributedString alloc] initWithString:NSSTR(iter.name() + ": ") - attributes:boldAttr]; - string str; - if (iter->isString()) { - str = iter->asString(); - } else { - Json::StreamWriterBuilder builder; - builder["indentation"] = ""; - str = writeString(builder, *iter); - } - NSAttributedString* value = - [[NSAttributedString alloc] initWithString:NSSTR(str + "\n") - attributes:normalAttr]; - [[mViewReportTextView textStorage] appendAttributedString:key]; - [[mViewReportTextView textStorage] appendAttributedString:value]; - [key release]; - [value release]; - } - - NSAttributedString* extra = [[NSAttributedString alloc] - initWithString:NSSTR("\n" + gStrings[ST_EXTRAREPORTINFO]) - attributes:normalAttr]; - [[mViewReportTextView textStorage] appendAttributedString:extra]; - [extra release]; -} - -- (void)maybeSubmitReport { - if ([mSubmitReportButton state] == NSOnState) { - [self setStringFitVertically:mProgressText - string:Str(ST_REPORTDURINGSUBMIT) - resizeWindow:YES]; - // disable all the controls - [self enableControls:NO]; - [mSubmitReportButton setEnabled:NO]; - [mRestartButton setEnabled:NO]; - [mCloseButton setEnabled:NO]; - [mProgressIndicator startAnimation:self]; - gDidTrySend = true; - [self sendReport]; - } else { - [NSApp terminate:self]; - } -} - -- (void)closeMeDown:(id)unused { - [NSApp terminate:self]; -} - -- (IBAction)submitReportClicked:(id)sender { - [self updateSubmit]; - NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults]; - [userDefaults setBool:([mSubmitReportButton state] == NSOnState) - forKey:@"submitReport"]; - [userDefaults synchronize]; -} - -- (IBAction)viewReportClicked:(id)sender { - [self showReportInfo]; - [NSApp beginSheet:mViewReportWindow - modalForWindow:mWindow - modalDelegate:nil - didEndSelector:nil - contextInfo:nil]; -} - -- (IBAction)viewReportOkClicked:(id)sender { - [mViewReportWindow orderOut:nil]; - [NSApp endSheet:mViewReportWindow]; -} - -- (IBAction)closeClicked:(id)sender { - [self maybeSubmitReport]; -} - -- (IBAction)restartClicked:(id)sender { - RestartApplication(); - [self maybeSubmitReport]; -} - -- (IBAction)includeURLClicked:(id)sender { - [self updateURL]; - NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults]; - [userDefaults setBool:([mIncludeURLButton state] == NSOnState) - forKey:@"IncludeURL"]; - [userDefaults synchronize]; -} - -- (void)textDidChange:(NSNotification*)aNotification { - // update comment parameter - if ([[[mCommentText textStorage] mutableString] length] > 0) - gQueryParameters["Comments"] = - [[[mCommentText textStorage] mutableString] UTF8String]; - else - gQueryParameters.removeMember("Comments"); -} - -// Limit the comment field to 500 bytes in UTF-8 -- (BOOL)textView:(NSTextView*)aTextView - shouldChangeTextInRange:(NSRange)affectedCharRange - replacementString:(NSString*)replacementString { - // current string length + replacement text length - replaced range length - if (([[aTextView string] lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + - [replacementString lengthOfBytesUsingEncoding:NSUTF8StringEncoding] - - [[[aTextView string] substringWithRange:affectedCharRange] - lengthOfBytesUsingEncoding:NSUTF8StringEncoding]) > - MAX_COMMENT_LENGTH) { - return NO; - } - return YES; -} - -- (void)doInitialResizing { - NSRect windowFrame = [mWindow frame]; - NSRect restartFrame = [mRestartButton frame]; - NSRect closeFrame = [mCloseButton frame]; - // resize close button to fit text - float oldCloseWidth = closeFrame.size.width; - [mCloseButton setTitle:Str(ST_QUIT)]; - [mCloseButton sizeToFit]; - closeFrame = [mCloseButton frame]; - // move close button left if it grew - if (!gRTLlayout) { - closeFrame.origin.x -= closeFrame.size.width - oldCloseWidth; - } - - if (gRestartArgs.size() == 0) { - [mRestartButton removeFromSuperview]; - if (!gRTLlayout) { - closeFrame.origin.x = restartFrame.origin.x + - (restartFrame.size.width - closeFrame.size.width); - } else { - closeFrame.origin.x = restartFrame.origin.x; - } - [mCloseButton setFrame:closeFrame]; - [mCloseButton setKeyEquivalent:@"\r"]; - } else { - [mRestartButton setTitle:Str(ST_RESTART)]; - // resize "restart" button - float oldRestartWidth = restartFrame.size.width; - [mRestartButton sizeToFit]; - restartFrame = [mRestartButton frame]; - if (!gRTLlayout) { - // move left by the amount that the button grew - restartFrame.origin.x -= restartFrame.size.width - oldRestartWidth; - closeFrame.origin.x -= restartFrame.size.width - oldRestartWidth; - } else { - // shift the close button right in RTL - closeFrame.origin.x += restartFrame.size.width - oldRestartWidth; - } - [mRestartButton setFrame:restartFrame]; - [mCloseButton setFrame:closeFrame]; - // possibly resize window if both buttons no longer fit - // leave 20 px from either side of the window, and 12 px - // between the buttons - float neededWidth = - closeFrame.size.width + restartFrame.size.width + 2 * 20 + 12; - - if (neededWidth > windowFrame.size.width) { - windowFrame.size.width = neededWidth; - [mWindow setFrame:windowFrame display:true animate:NO]; - } - [mRestartButton setKeyEquivalent:@"\r"]; - } - - NSButton* checkboxes[] = {mSubmitReportButton, mIncludeURLButton}; - - for (auto checkbox : checkboxes) { - NSRect frame = [checkbox frame]; - [checkbox sizeToFit]; - if (gRTLlayout) { - // sizeToFit will keep the left side fixed, so realign - float oldWidth = frame.size.width; - frame = [checkbox frame]; - frame.origin.x += oldWidth - frame.size.width; - [checkbox setFrame:frame]; - } - // keep existing spacing on left side, + 20 px spare on right - float neededWidth = - frame.origin.x + checkbox.intrinsicContentSize.width + 20; - if (neededWidth > windowFrame.size.width) { - windowFrame.size.width = neededWidth; - [mWindow setFrame:windowFrame display:true animate:NO]; - } - } - - // do this down here because we may have made the window wider - // up above - [self setStringFitVertically:mDescriptionLabel - string:Str(ST_CRASHREPORTERDESCRIPTION) - resizeWindow:YES]; - - // now pin all the controls (except quit/submit) in place, - // if we lengthen the window after this, it's just to lengthen - // the progress text, so nothing above that text should move. - NSView* views[] = {mSubmitReportButton, mViewReportButton, - mCommentScrollView, mIncludeURLButton, - mProgressIndicator, mProgressText}; - for (auto view : views) { - [view setAutoresizingMask:NSViewMinYMargin]; - } -} - -- (float)setStringFitVertically:(NSControl*)control - string:(NSString*)str - resizeWindow:(BOOL)resizeWindow { - // hack to make the text field grow vertically - NSRect frame = [control frame]; - float oldHeight = frame.size.height; - - frame.size.height = 10000; - NSSize oldCellSize = [[control cell] cellSizeForBounds:frame]; - [control setStringValue:str]; - NSSize newCellSize = [[control cell] cellSizeForBounds:frame]; - - float delta = newCellSize.height - oldCellSize.height; - frame.origin.y -= delta; - frame.size.height = oldHeight + delta; - [control setFrame:frame]; - - if (resizeWindow) { - NSRect frame = [mWindow frame]; - frame.origin.y -= delta; - frame.size.height += delta; - [mWindow setFrame:frame display:true animate:NO]; - } - - return delta; -} - -- (void)setView:(NSView*)v animate:(BOOL)animate { - NSRect frame = [mWindow frame]; - - NSRect oldViewFrame = [[mWindow contentView] frame]; - NSRect newViewFrame = [v frame]; - - frame.origin.y += oldViewFrame.size.height - newViewFrame.size.height; - frame.size.height -= oldViewFrame.size.height - newViewFrame.size.height; - - frame.origin.x += oldViewFrame.size.width - newViewFrame.size.width; - frame.size.width -= oldViewFrame.size.width - newViewFrame.size.width; - - [mWindow setContentView:v]; - [mWindow setFrame:frame display:true animate:animate]; -} - -- (void)enableControls:(BOOL)enabled { - [mViewReportButton setEnabled:enabled]; - [mIncludeURLButton setEnabled:enabled]; - [mCommentText setEnabled:enabled]; - [mCommentScrollView setHasVerticalScroller:enabled]; -} - -- (void)updateSubmit { - if ([mSubmitReportButton state] == NSOnState) { - [self setStringFitVertically:mProgressText - string:Str(ST_REPORTPRESUBMIT) - resizeWindow:YES]; - [mProgressText setHidden:NO]; - // enable all the controls - [self enableControls:YES]; - } else { - // not submitting, disable all the controls under - // the submit checkbox, and hide the status text - [mProgressText setHidden:YES]; - [self enableControls:NO]; - } -} - -- (void)updateURL { - if ([mIncludeURLButton state] == NSOnState && !gURLParameter.empty()) { - gQueryParameters["URL"] = gURLParameter; - } else { - gQueryParameters.removeMember("URL"); - } -} - -- (void)updateEmail { - // In order to remove the email fields, we have to edit the .nib files which - // we can't do with current xcode so we make them hidden; updating the - // crashreporter interface for mac is covered in bug #1696164 - [mEmailMeButton setHidden:YES]; - [mEmailText setHidden:YES]; -} - -- (void)sendReport { - if (![self setupPost]) { - LogMessage("Crash report submission failed: could not set up POST data"); - - if (gAutoSubmit) { - [NSApp terminate:self]; - } - - [self setStringFitVertically:mProgressText - string:Str(ST_SUBMITFAILED) - resizeWindow:YES]; - // quit after 5 seconds - [self performSelector:@selector(closeMeDown:) - withObject:nil - afterDelay:5.0]; - } - - [NSThread detachNewThreadSelector:@selector(uploadThread:) - toTarget:self - withObject:mPost]; -} - -- (bool)setupPost { - NSURL* url = [NSURL - URLWithString:[NSSTR(gSendURL) stringByAddingPercentEscapesUsingEncoding: - NSUTF8StringEncoding]]; - if (!url) return false; - - mPost = [[HTTPMultipartUpload alloc] initWithURL:url]; - if (!mPost) return false; - - for (StringTable::const_iterator i = gFiles.begin(); i != gFiles.end(); i++) { - [mPost addFileAtPath:NSSTR(i->second) name:NSSTR(i->first)]; - } - - Json::StreamWriterBuilder builder; - builder["indentation"] = ""; - string output = writeString(builder, gQueryParameters).append("\r\n"); - NSMutableString* parameters = - [[NSMutableString alloc] initWithUTF8String:output.c_str()]; - - [mPost setParameters:parameters]; - [parameters release]; - - return true; -} - -- (void)uploadComplete:(NSData*)data { - NSHTTPURLResponse* response = [mPost response]; - [mPost release]; - - bool success; - string reply; - if (!data || !response || [response statusCode] != 200) { - success = false; - reply = ""; - - // if data is nil, we probably logged an error in uploadThread - if (data != nil && response != nil) { - ostringstream message; - message << "Crash report submission failed: server returned status " - << [response statusCode]; - LogMessage(message.str()); - } - } else { - success = true; - LogMessage("Crash report submitted successfully"); - - NSString* encodingName = [response textEncodingName]; - NSStringEncoding encoding; - if (encodingName) { - encoding = CFStringConvertEncodingToNSStringEncoding( - CFStringConvertIANACharSetNameToEncoding((CFStringRef)encodingName)); - } else { - encoding = NSISOLatin1StringEncoding; - } - NSString* r = [[NSString alloc] initWithData:data encoding:encoding]; - reply = [r UTF8String]; - [r release]; - } - - SendCompleted(success, reply); - - if (gAutoSubmit) { - [NSApp terminate:self]; - } - - [mProgressIndicator stopAnimation:self]; - if (success) { - [self setStringFitVertically:mProgressText - string:Str(ST_REPORTSUBMITSUCCESS) - resizeWindow:YES]; - } else { - [self setStringFitVertically:mProgressText - string:Str(ST_SUBMITFAILED) - resizeWindow:YES]; - } - // quit after 5 seconds - [self performSelector:@selector(closeMeDown:) withObject:nil afterDelay:5.0]; -} - -- (void)uploadThread:(HTTPMultipartUpload*)post { - NSAutoreleasePool* autoreleasepool = [[NSAutoreleasePool alloc] init]; - NSError* error = nil; - NSData* data = [post send:&error]; - if (error) { - data = nil; - NSString* errorDesc = [error localizedDescription]; - string message = [errorDesc UTF8String]; - LogMessage("Crash report submission failed: " + message); - } - - [self performSelectorOnMainThread:@selector(uploadComplete:) - withObject:data - waitUntilDone:YES]; - - [autoreleasepool release]; -} - -// to get auto-quit when we close the window -- (BOOL)applicationShouldTerminateAfterLastWindowClosed: - (NSApplication*)theApplication { - return YES; -} - -- (void)applicationWillTerminate:(NSNotification*)aNotification { - // since we use [NSApp terminate:] we never return to main, - // so do our cleanup here - if (!gDidTrySend) DeleteDump(); -} - -@end - -@implementation TextViewWithPlaceHolder - -- (BOOL)becomeFirstResponder { - [self setNeedsDisplay:YES]; - return [super becomeFirstResponder]; -} - -- (void)drawRect:(NSRect)rect { - [super drawRect:rect]; - if (mPlaceHolderString && [[self string] isEqualToString:@""] && - self != [[self window] firstResponder]) - [mPlaceHolderString drawInRect:[self frame]]; -} - -- (BOOL)resignFirstResponder { - [self setNeedsDisplay:YES]; - return [super resignFirstResponder]; -} - -- (void)setPlaceholder:(NSString*)placeholder { - NSColor* txtColor = [NSColor disabledControlTextColor]; - NSDictionary* txtDict = [NSDictionary - dictionaryWithObjectsAndKeys:txtColor, NSForegroundColorAttributeName, - nil]; - mPlaceHolderString = - [[NSMutableAttributedString alloc] initWithString:placeholder - attributes:txtDict]; - if (gRTLlayout) - [mPlaceHolderString setAlignment:NSTextAlignmentRight - range:NSMakeRange(0, [placeholder length])]; -} - -- (void)insertTab:(id)sender { - // don't actually want to insert tabs, just tab to next control - [[self window] selectNextKeyView:sender]; -} - -- (void)insertBacktab:(id)sender { - [[self window] selectPreviousKeyView:sender]; -} - -- (void)setEnabled:(BOOL)enabled { - [self setSelectable:enabled]; - [self setEditable:enabled]; - if (![[self string] isEqualToString:@""]) { - NSAttributedString* colorString; - NSColor* txtColor; - if (enabled) - txtColor = [NSColor textColor]; - else - txtColor = [NSColor disabledControlTextColor]; - NSDictionary* txtDict = [NSDictionary - dictionaryWithObjectsAndKeys:txtColor, NSForegroundColorAttributeName, - nil]; - colorString = [[NSAttributedString alloc] initWithString:[self string] - attributes:txtDict]; - [[self textStorage] setAttributedString:colorString]; - [self setInsertionPointColor:txtColor]; - [colorString release]; - } -} - -- (void)dealloc { - [mPlaceHolderString release]; - [super dealloc]; -} - -@end - -/* === Crashreporter UI Functions === */ - -bool UIInit() { - gMainPool = [[NSAutoreleasePool alloc] init]; - [NSApplication sharedApplication]; - - if (gStrings.find("isRTL") != gStrings.end() && gStrings["isRTL"] == "yes") - gRTLlayout = true; - - if (gAutoSubmit) { - gUI = [[CrashReporterUI alloc] init]; - } else { - [[NSBundle mainBundle] - loadNibNamed:(gRTLlayout ? @"MainMenuRTL" : @"MainMenu") - owner:NSApp - topLevelObjects:nil]; - } - - return true; -} - -void UIShutdown() { [gMainPool release]; } - -void UIShowDefaultUI() { - [gUI showErrorUI:gStrings[ST_CRASHREPORTERDEFAULT]]; - [NSApp run]; -} - -bool UIShowCrashUI(const StringTable& files, const Json::Value& queryParameters, - const string& sendURL, const vector<string>& restartArgs) { - gRestartArgs = restartArgs; - - [gUI showCrashUI:files queryParameters:queryParameters sendURL:sendURL]; - [NSApp run]; - - return gDidTrySend; -} - -void UIError_impl(const string& message) { - if (!gUI) { - // UI failed to initialize, printing is the best we can do - printf("Error: %s\n", message.c_str()); - return; - } - - [gUI showErrorUI:message]; - [NSApp run]; -} - -bool UIGetIniPath(string& path) { - NSString* tmpPath = [NSString stringWithUTF8String:gArgv[0]]; - NSString* iniName = [tmpPath lastPathComponent]; - iniName = [iniName stringByAppendingPathExtension:@"ini"]; - tmpPath = [tmpPath stringByDeletingLastPathComponent]; - tmpPath = [tmpPath stringByDeletingLastPathComponent]; - tmpPath = [tmpPath stringByAppendingPathComponent:@"Resources"]; - tmpPath = [tmpPath stringByAppendingPathComponent:iniName]; - path = [tmpPath UTF8String]; - return true; -} - -bool UIGetSettingsPath(const string& vendor, const string& product, - string& settingsPath) { - NSArray* paths = NSSearchPathForDirectoriesInDomains( - NSApplicationSupportDirectory, NSUserDomainMask, YES); - NSString* destPath = [paths firstObject]; - - // Note that MacOS ignores the vendor when creating the profile hierarchy - - // all application preferences directories live alongside one another in - // ~/Library/Application Support/ - destPath = [destPath stringByAppendingPathComponent:NSSTR(product)]; - // Thunderbird stores its profile in ~/Library/Thunderbird, - // but we're going to put stuff in ~/Library/Application Support/Thunderbird - // anyway, so we have to ensure that path exists. - string tempPath = [destPath UTF8String]; - if (!UIEnsurePathExists(tempPath)) return false; - - destPath = [destPath stringByAppendingPathComponent:@"Crash Reports"]; - - settingsPath = [destPath UTF8String]; - - return true; -} - -bool UIMoveFile(const string& file, const string& newfile) { - if (!rename(file.c_str(), newfile.c_str())) return true; - if (errno != EXDEV) return false; - - NSFileManager* fileManager = [NSFileManager defaultManager]; - NSString* source = - [fileManager stringWithFileSystemRepresentation:file.c_str() - length:file.length()]; - NSString* dest = - [fileManager stringWithFileSystemRepresentation:newfile.c_str() - length:newfile.length()]; - if (!source || !dest) return false; - - [fileManager moveItemAtPath:source toPath:dest error:NULL]; - return UIFileExists(newfile); -} diff --git a/toolkit/crashreporter/client/crashreporter_unix_common.cpp b/toolkit/crashreporter/client/crashreporter_unix_common.cpp deleted file mode 100644 index e6514d4423..0000000000 --- a/toolkit/crashreporter/client/crashreporter_unix_common.cpp +++ /dev/null @@ -1,139 +0,0 @@ -/* 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 "crashreporter.h" - -#include <algorithm> -#include <sys/wait.h> - -#include <dirent.h> -#include <errno.h> -#include <sys/stat.h> -#include <unistd.h> - -using namespace CrashReporter; -using std::ios_base; -using std::sort; -using std::string; -using std::vector; - -struct FileData { - time_t timestamp; - string path; -}; - -static bool CompareFDTime(const FileData& fd1, const FileData& fd2) { - return fd1.timestamp > fd2.timestamp; -} - -void UIPruneSavedDumps(const string& directory) { - DIR* dirfd = opendir(directory.c_str()); - if (!dirfd) return; - - vector<FileData> dumpfiles; - - while (dirent* dir = readdir(dirfd)) { - FileData fd; - fd.path = directory + '/' + dir->d_name; - if (fd.path.size() < 5) continue; - - if (fd.path.compare(fd.path.size() - 4, 4, ".dmp") != 0) continue; - - struct stat st; - if (stat(fd.path.c_str(), &st)) { - closedir(dirfd); - return; - } - - fd.timestamp = st.st_mtime; - - dumpfiles.push_back(fd); - } - - closedir(dirfd); - - sort(dumpfiles.begin(), dumpfiles.end(), CompareFDTime); - - while (dumpfiles.size() > kSaveCount) { - // get the path of the oldest file - string path = dumpfiles[dumpfiles.size() - 1].path; - UIDeleteFile(path); - - // s/.dmp/.extra/ - path.replace(path.size() - 4, 4, ".extra"); - UIDeleteFile(path); - - dumpfiles.pop_back(); - } -} - -bool UIRunProgram(const string& exename, const vector<string>& args, - bool wait) { - pid_t pid = fork(); - - if (pid == -1) { - return false; - } else if (pid == 0) { - // Child - size_t argvLen = args.size() + 2; - vector<char*> argv(argvLen); - - argv[0] = const_cast<char*>(exename.c_str()); - - for (size_t i = 0; i < args.size(); i++) { - argv[i + 1] = const_cast<char*>(args[i].c_str()); - } - - argv[argvLen - 1] = nullptr; - - // Run the program - int rv = execv(exename.c_str(), argv.data()); - - if (rv == -1) { - exit(EXIT_FAILURE); - } - } else { - // Parent - if (wait) { - waitpid(pid, nullptr, 0); - } - } - - return true; -} - -bool UIEnsurePathExists(const string& path) { - int ret = mkdir(path.c_str(), S_IRWXU); - int e = errno; - if (ret == -1 && e != EEXIST) return false; - - return true; -} - -bool UIFileExists(const string& path) { - struct stat sb; - int ret = stat(path.c_str(), &sb); - if (ret == -1 || !(sb.st_mode & S_IFREG)) return false; - - return true; -} - -bool UIDeleteFile(const string& file) { return (unlink(file.c_str()) != -1); } - -std::ifstream* UIOpenRead(const string& filename, ios_base::openmode mode) { - return new std::ifstream(filename.c_str(), mode); -} - -std::ofstream* UIOpenWrite(const string& filename, ios_base::openmode mode) { - return new std::ofstream(filename.c_str(), mode); -} - -string UIGetEnv(const string& name) { - const char* var = getenv(name.c_str()); - if (var && *var) { - return var; - } - - return ""; -} diff --git a/toolkit/crashreporter/client/crashreporter_win.cpp b/toolkit/crashreporter/client/crashreporter_win.cpp deleted file mode 100644 index 35018bda4a..0000000000 --- a/toolkit/crashreporter/client/crashreporter_win.cpp +++ /dev/null @@ -1,1266 +0,0 @@ -/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#ifdef WIN32_LEAN_AND_MEAN -# undef WIN32_LEAN_AND_MEAN -#endif - -#include "crashreporter.h" - -#include <windows.h> -#include <commctrl.h> -#include <richedit.h> -#include <shellapi.h> -#include <shlobj.h> -#include <shlwapi.h> -#include <math.h> -#include <set> -#include <algorithm> -#include "resource.h" -#include "windows/sender/crash_report_sender.h" -#include "common/windows/string_utils-inl.h" - -#define SUBMIT_REPORT_VALUE L"SubmitCrashReport" -#define INCLUDE_URL_VALUE L"IncludeURL" - -#define WM_UPLOADCOMPLETE WM_APP - -// Thanks, Windows.h :( -#undef min -#undef max - -using std::ifstream; -using std::ios; -using std::ios_base; -using std::map; -using std::ofstream; -using std::set; -using std::string; -using std::vector; -using std::wstring; - -using namespace CrashReporter; - -typedef struct { - HWND hDlg; - Json::Value queryParameters; - map<wstring, wstring> files; - wstring sendURL; - - wstring serverResponse; -} SendThreadData; - -/* - * Per http://msdn2.microsoft.com/en-us/library/ms645398(VS.85).aspx - * "The DLGTEMPLATEEX structure is not defined in any standard header file. - * The structure definition is provided here to explain the format of an - * extended template for a dialog box. - */ -typedef struct { - WORD dlgVer; - WORD signature; - DWORD helpID; - DWORD exStyle; - // There's more to this struct, but it has weird variable-length - // members, and I only actually need to touch exStyle on an existing - // instance, so I've omitted the rest. -} DLGTEMPLATEEX; - -static HANDLE gThreadHandle; -static SendThreadData gSendData = { - 0, -}; -static vector<string> gRestartArgs; -static Json::Value gQueryParameters; -static wstring gCrashReporterKey(L"Software\\Mozilla\\Crash Reporter"); -static string gURLParameter; -static int gCheckboxPadding = 6; -static bool gRTLlayout = false; - -// When vertically resizing the dialog, these items should move down -static set<UINT> gAttachedBottom; - -// Default set of items for gAttachedBottom -static const UINT kDefaultAttachedBottom[] = { - IDC_SUBMITREPORTCHECK, IDC_VIEWREPORTBUTTON, IDC_COMMENTTEXT, - IDC_INCLUDEURLCHECK, IDC_PROGRESSTEXT, IDC_THROBBER, - IDC_CLOSEBUTTON, IDC_RESTARTBUTTON, -}; - -static wstring UTF8ToWide(const string& utf8, bool* success = 0); -static DWORD WINAPI SendThreadProc(LPVOID param); - -static wstring Str(const char* key) { return UTF8ToWide(gStrings[key]); } - -/* === win32 helper functions === */ - -static void DoInitCommonControls() { - INITCOMMONCONTROLSEX ic; - ic.dwSize = sizeof(INITCOMMONCONTROLSEX); - ic.dwICC = ICC_PROGRESS_CLASS; - InitCommonControlsEx(&ic); - // also get the rich edit control - LoadLibrary(L"Msftedit.dll"); -} - -static bool GetBoolValue(HKEY hRegKey, LPCTSTR valueName, DWORD* value) { - DWORD type, dataSize; - dataSize = sizeof(DWORD); - if (RegQueryValueEx(hRegKey, valueName, nullptr, &type, (LPBYTE)value, - &dataSize) == ERROR_SUCCESS && - type == REG_DWORD) - return true; - - return false; -} - -static bool CheckBoolKey(const wchar_t* key, const wchar_t* valueName, - bool* enabled) { - /* - * NOTE! This code needs to stay in sync with the preference checking - * code in in nsExceptionHandler.cpp. - */ - *enabled = false; - bool found = false; - HKEY hRegKey; - DWORD val; - // see if our reg key is set globally - if (RegOpenKey(HKEY_LOCAL_MACHINE, key, &hRegKey) == ERROR_SUCCESS) { - if (GetBoolValue(hRegKey, valueName, &val)) { - *enabled = (val == 1); - found = true; - } - RegCloseKey(hRegKey); - } else { - // look for it in user settings - if (RegOpenKey(HKEY_CURRENT_USER, key, &hRegKey) == ERROR_SUCCESS) { - if (GetBoolValue(hRegKey, valueName, &val)) { - *enabled = (val == 1); - found = true; - } - RegCloseKey(hRegKey); - } - } - - return found; -} - -static void SetBoolKey(const wchar_t* key, const wchar_t* value, bool enabled) { - /* - * NOTE! This code needs to stay in sync with the preference setting - * code in in nsExceptionHandler.cpp. - */ - HKEY hRegKey; - - if (RegCreateKey(HKEY_CURRENT_USER, key, &hRegKey) == ERROR_SUCCESS) { - DWORD data = (enabled ? 1 : 0); - RegSetValueEx(hRegKey, value, 0, REG_DWORD, (LPBYTE)&data, sizeof(data)); - RegCloseKey(hRegKey); - } -} - -static string FormatLastError() { - DWORD err = GetLastError(); - LPWSTR s; - string message = "Crash report submission failed: "; - // odds are it's a WinInet error - HANDLE hInetModule = GetModuleHandle(L"WinInet.dll"); - if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | - FORMAT_MESSAGE_FROM_SYSTEM | - FORMAT_MESSAGE_FROM_HMODULE, - hInetModule, err, 0, (LPWSTR)&s, 0, nullptr) != 0) { - message += WideToUTF8(s, nullptr); - LocalFree(s); - // strip off any trailing newlines - string::size_type n = message.find_last_not_of("\r\n"); - if (n < message.size() - 1) { - message.erase(n + 1); - } - } else { - char buf[64]; - sprintf(buf, "Unknown error, error code: 0x%08x", - static_cast<unsigned int>(err)); - message += buf; - } - return message; -} - -#define TS_DRAW 2 -#define BP_CHECKBOX 3 - -typedef HANDLE(WINAPI* OpenThemeDataPtr)(HWND hwnd, LPCWSTR pszClassList); -typedef HRESULT(WINAPI* CloseThemeDataPtr)(HANDLE hTheme); -typedef HRESULT(WINAPI* GetThemePartSizePtr)(HANDLE hTheme, HDC hdc, - int iPartId, int iStateId, - RECT* prc, int ts, SIZE* psz); -typedef HRESULT(WINAPI* GetThemeContentRectPtr)(HANDLE hTheme, HDC hdc, - int iPartId, int iStateId, - const RECT* pRect, - RECT* pContentRect); - -static void GetThemeSizes(HWND hwnd) { - HMODULE themeDLL = LoadLibrary(L"uxtheme.dll"); - - if (!themeDLL) return; - - OpenThemeDataPtr openTheme = - (OpenThemeDataPtr)GetProcAddress(themeDLL, "OpenThemeData"); - CloseThemeDataPtr closeTheme = - (CloseThemeDataPtr)GetProcAddress(themeDLL, "CloseThemeData"); - GetThemePartSizePtr getThemePartSize = - (GetThemePartSizePtr)GetProcAddress(themeDLL, "GetThemePartSize"); - - if (!openTheme || !closeTheme || !getThemePartSize) { - FreeLibrary(themeDLL); - return; - } - - HANDLE buttonTheme = openTheme(hwnd, L"Button"); - if (!buttonTheme) { - FreeLibrary(themeDLL); - return; - } - HDC hdc = GetDC(hwnd); - SIZE s; - getThemePartSize(buttonTheme, hdc, BP_CHECKBOX, 0, nullptr, TS_DRAW, &s); - gCheckboxPadding = s.cx; - closeTheme(buttonTheme); - FreeLibrary(themeDLL); -} - -// Gets the position of a window relative to another window's client area -static void GetRelativeRect(HWND hwnd, HWND hwndParent, RECT* r) { - GetWindowRect(hwnd, r); - MapWindowPoints(nullptr, hwndParent, (POINT*)r, 2); -} - -static void SetDlgItemVisible(HWND hwndDlg, UINT item, bool visible) { - HWND hwnd = GetDlgItem(hwndDlg, item); - - ShowWindow(hwnd, visible ? SW_SHOW : SW_HIDE); -} - -/* === Crash Reporting Dialog === */ - -static void StretchDialog(HWND hwndDlg, int ydiff) { - RECT r; - GetWindowRect(hwndDlg, &r); - r.bottom += ydiff; - MoveWindow(hwndDlg, r.left, r.top, r.right - r.left, r.bottom - r.top, TRUE); -} - -static void ReflowDialog(HWND hwndDlg, int ydiff) { - // Move items attached to the bottom down/up by as much as - // the window resize - for (set<UINT>::const_iterator item = gAttachedBottom.begin(); - item != gAttachedBottom.end(); item++) { - RECT r; - HWND hwnd = GetDlgItem(hwndDlg, *item); - GetRelativeRect(hwnd, hwndDlg, &r); - r.top += ydiff; - r.bottom += ydiff; - MoveWindow(hwnd, r.left, r.top, r.right - r.left, r.bottom - r.top, TRUE); - } -} - -static DWORD WINAPI SendThreadProc(LPVOID param) { - bool finishedOk; - SendThreadData* td = (SendThreadData*)param; - - if (td->sendURL.empty()) { - finishedOk = false; - LogMessage("No server URL, not sending report"); - } else { - Json::StreamWriterBuilder builder; - builder["indentation"] = ""; - string parameters(Json::writeString(builder, td->queryParameters)); - google_breakpad::CrashReportSender sender(L""); - finishedOk = (sender.SendCrashReport(td->sendURL, parameters, td->files, - &td->serverResponse) == - google_breakpad::RESULT_SUCCEEDED); - if (finishedOk) { - LogMessage("Crash report submitted successfully"); - } else { - // get an error string and print it to the log - // XXX: would be nice to get the HTTP status code here, filed: - // http://code.google.com/p/google-breakpad/issues/detail?id=220 - LogMessage(FormatLastError()); - } - } - - if (gAutoSubmit) { - // Ordinarily this is done on the main thread in CrashReporterDialogProc, - // for auto submit we don't run that and it should be safe to finish up - // here as is done on other platforms. - SendCompleted(finishedOk, WideToUTF8(gSendData.serverResponse)); - } else { - PostMessage(td->hDlg, WM_UPLOADCOMPLETE, finishedOk ? 1 : 0, 0); - } - - return 0; -} - -static void EndCrashReporterDialog(HWND hwndDlg, int code) { - // Save the current values to the registry - SetBoolKey(gCrashReporterKey.c_str(), INCLUDE_URL_VALUE, - IsDlgButtonChecked(hwndDlg, IDC_INCLUDEURLCHECK) != 0); - SetBoolKey(gCrashReporterKey.c_str(), SUBMIT_REPORT_VALUE, - IsDlgButtonChecked(hwndDlg, IDC_SUBMITREPORTCHECK) != 0); - - EndDialog(hwndDlg, code); -} - -static void MaybeResizeProgressText(HWND hwndDlg) { - HWND hwndProgress = GetDlgItem(hwndDlg, IDC_PROGRESSTEXT); - HDC hdc = GetDC(hwndProgress); - HFONT hfont = (HFONT)SendMessage(hwndProgress, WM_GETFONT, 0, 0); - if (hfont) SelectObject(hdc, hfont); - SIZE size; - RECT rect; - GetRelativeRect(hwndProgress, hwndDlg, &rect); - - wchar_t text[1024]; - GetWindowText(hwndProgress, text, 1024); - - if (!GetTextExtentPoint32(hdc, text, wcslen(text), &size)) return; - - if (size.cx < (rect.right - rect.left)) return; - - // Figure out how much we need to resize things vertically - // This is sort of a fudge, but it should be good enough. - int wantedHeight = - size.cy * (int)ceil((float)size.cx / (float)(rect.right - rect.left)); - int diff = wantedHeight - (rect.bottom - rect.top); - if (diff <= 0) return; - - MoveWindow(hwndProgress, rect.left, rect.top, rect.right - rect.left, - wantedHeight, TRUE); - - gAttachedBottom.clear(); - gAttachedBottom.insert(IDC_CLOSEBUTTON); - gAttachedBottom.insert(IDC_RESTARTBUTTON); - - StretchDialog(hwndDlg, diff); - - for (size_t i = 0; i < sizeof(kDefaultAttachedBottom) / sizeof(UINT); i++) { - gAttachedBottom.insert(kDefaultAttachedBottom[i]); - } -} - -static void MaybeSendReport(HWND hwndDlg) { - if (!IsDlgButtonChecked(hwndDlg, IDC_SUBMITREPORTCHECK)) { - EndCrashReporterDialog(hwndDlg, 0); - return; - } - - // disable all the form controls - EnableWindow(GetDlgItem(hwndDlg, IDC_SUBMITREPORTCHECK), false); - EnableWindow(GetDlgItem(hwndDlg, IDC_VIEWREPORTBUTTON), false); - EnableWindow(GetDlgItem(hwndDlg, IDC_COMMENTTEXT), false); - EnableWindow(GetDlgItem(hwndDlg, IDC_INCLUDEURLCHECK), false); - EnableWindow(GetDlgItem(hwndDlg, IDC_CLOSEBUTTON), false); - EnableWindow(GetDlgItem(hwndDlg, IDC_RESTARTBUTTON), false); - - SetDlgItemText(hwndDlg, IDC_PROGRESSTEXT, Str(ST_REPORTDURINGSUBMIT).c_str()); - MaybeResizeProgressText(hwndDlg); - // start throbber - // play entire AVI, and loop - Animate_Play(GetDlgItem(hwndDlg, IDC_THROBBER), 0, -1, -1); - SetDlgItemVisible(hwndDlg, IDC_THROBBER, true); - gThreadHandle = nullptr; - gSendData.hDlg = hwndDlg; - gSendData.queryParameters = gQueryParameters; - - gThreadHandle = - CreateThread(nullptr, 0, SendThreadProc, &gSendData, 0, nullptr); -} - -static void RestartApplication() { - wstring cmdLine; - - for (unsigned int i = 0; i < gRestartArgs.size(); i++) { - cmdLine += L"\"" + UTF8ToWide(gRestartArgs[i]) + L"\" "; - } - - STARTUPINFO si; - PROCESS_INFORMATION pi; - - ZeroMemory(&si, sizeof(si)); - si.cb = sizeof(si); - si.dwFlags = STARTF_USESHOWWINDOW; - si.wShowWindow = SW_SHOWNORMAL; - ZeroMemory(&pi, sizeof(pi)); - - if (CreateProcess(nullptr, (LPWSTR)cmdLine.c_str(), nullptr, nullptr, FALSE, - 0, nullptr, nullptr, &si, &pi)) { - CloseHandle(pi.hProcess); - CloseHandle(pi.hThread); - } -} - -static void ShowReportInfo(HWND hwndDlg) { - wstring description; - - for (Json::ValueConstIterator iter = gQueryParameters.begin(); - iter != gQueryParameters.end(); ++iter) { - description += UTF8ToWide(iter.name()); - description += L": "; - string value; - if (iter->isString()) { - value = iter->asString(); - } else { - Json::StreamWriterBuilder builder; - builder["indentation"] = ""; - value = Json::writeString(builder, *iter); - } - description += UTF8ToWide(value); - description += L"\n"; - } - - description += L"\n"; - description += Str(ST_EXTRAREPORTINFO); - - SetDlgItemText(hwndDlg, IDC_VIEWREPORTTEXT, description.c_str()); -} - -static void UpdateURL(HWND hwndDlg) { - if (IsDlgButtonChecked(hwndDlg, IDC_INCLUDEURLCHECK)) { - gQueryParameters["URL"] = gURLParameter; - } else { - gQueryParameters.removeMember("URL"); - } -} - -static void UpdateComment(HWND hwndDlg) { - wchar_t comment[MAX_COMMENT_LENGTH + 1]; - GetDlgItemTextW(hwndDlg, IDC_COMMENTTEXT, comment, - sizeof(comment) / sizeof(comment[0])); - if (wcslen(comment) > 0) - gQueryParameters["Comments"] = WideToUTF8(comment); - else - gQueryParameters.removeMember("Comments"); -} - -/* - * Dialog procedure for the "view report" dialog. - */ -static BOOL CALLBACK ViewReportDialogProc(HWND hwndDlg, UINT message, - WPARAM wParam, LPARAM lParam) { - switch (message) { - case WM_INITDIALOG: { - SetWindowText(hwndDlg, Str(ST_VIEWREPORTTITLE).c_str()); - SetDlgItemText(hwndDlg, IDOK, Str(ST_OK).c_str()); - SendDlgItemMessage(hwndDlg, IDC_VIEWREPORTTEXT, EM_SETTARGETDEVICE, - (WPARAM) nullptr, 0); - ShowReportInfo(hwndDlg); - SetFocus(GetDlgItem(hwndDlg, IDOK)); - return FALSE; - } - - case WM_COMMAND: { - if (HIWORD(wParam) == BN_CLICKED && LOWORD(wParam) == IDOK) - EndDialog(hwndDlg, 0); - return FALSE; - } - } - return FALSE; -} - -// Return the number of bytes this string will take encoded -// in UTF-8 -static inline int BytesInUTF8(wchar_t* str) { - // Just count size of buffer for UTF-8, minus one - // (we don't need to count the null terminator) - return WideCharToMultiByte(CP_UTF8, 0, str, -1, nullptr, 0, nullptr, - nullptr) - - 1; -} - -// Calculate the length of the text in this edit control (in bytes, -// in the UTF-8 encoding) after replacing the current selection -// with |insert|. -static int NewTextLength(HWND hwndEdit, wchar_t* insert) { - wchar_t current[MAX_COMMENT_LENGTH + 1]; - - GetWindowText(hwndEdit, current, MAX_COMMENT_LENGTH + 1); - DWORD selStart, selEnd; - SendMessage(hwndEdit, EM_GETSEL, (WPARAM)&selStart, (LPARAM)&selEnd); - - int selectionLength = 0; - if (selEnd - selStart > 0) { - wchar_t selection[MAX_COMMENT_LENGTH + 1]; - google_breakpad::WindowsStringUtils::safe_wcsncpy( - selection, MAX_COMMENT_LENGTH + 1, current + selStart, - selEnd - selStart); - selection[selEnd - selStart] = '\0'; - selectionLength = BytesInUTF8(selection); - } - - // current string length + replacement text length - // - replaced selection length - return BytesInUTF8(current) + BytesInUTF8(insert) - selectionLength; -} - -// Window procedure for subclassing edit controls -static LRESULT CALLBACK EditSubclassProc(HWND hwnd, UINT uMsg, WPARAM wParam, - LPARAM lParam) { - static WNDPROC super = nullptr; - - if (super == nullptr) super = (WNDPROC)GetWindowLongPtr(hwnd, GWLP_USERDATA); - - switch (uMsg) { - case WM_PAINT: { - HDC hdc; - PAINTSTRUCT ps; - RECT r; - wchar_t windowText[1024]; - - GetWindowText(hwnd, windowText, 1024); - // if the control contains text or is focused, draw it normally - if (GetFocus() == hwnd || windowText[0] != '\0') - return CallWindowProc(super, hwnd, uMsg, wParam, lParam); - - GetClientRect(hwnd, &r); - hdc = BeginPaint(hwnd, &ps); - FillRect(hdc, &r, - GetSysColorBrush(IsWindowEnabled(hwnd) ? COLOR_WINDOW - : COLOR_BTNFACE)); - SetTextColor(hdc, GetSysColor(COLOR_GRAYTEXT)); - SelectObject(hdc, (HFONT)GetStockObject(DEFAULT_GUI_FONT)); - SetBkMode(hdc, TRANSPARENT); - wchar_t* txt = (wchar_t*)GetProp(hwnd, L"PROP_GRAYTEXT"); - // Get the actual edit control rect - CallWindowProc(super, hwnd, EM_GETRECT, 0, (LPARAM)&r); - UINT format = DT_EDITCONTROL | DT_NOPREFIX | DT_WORDBREAK | DT_INTERNAL; - if (gRTLlayout) format |= DT_RIGHT; - if (txt) DrawText(hdc, txt, wcslen(txt), &r, format); - EndPaint(hwnd, &ps); - return 0; - } - - // We handle WM_CHAR and WM_PASTE to limit the comment box to 500 - // bytes in UTF-8. - case WM_CHAR: { - // Leave accelerator keys and non-printing chars (except LF) alone - if (wParam & (1 << 24) || wParam & (1 << 29) || - (wParam < ' ' && wParam != '\n')) - break; - - wchar_t ch[2] = {(wchar_t)wParam, 0}; - if (NewTextLength(hwnd, ch) > MAX_COMMENT_LENGTH) return 0; - - break; - } - - case WM_PASTE: { - if (IsClipboardFormatAvailable(CF_UNICODETEXT) && OpenClipboard(hwnd)) { - HGLOBAL hg = GetClipboardData(CF_UNICODETEXT); - wchar_t* pastedText = (wchar_t*)GlobalLock(hg); - int newSize = 0; - - if (pastedText) newSize = NewTextLength(hwnd, pastedText); - - GlobalUnlock(hg); - CloseClipboard(); - - if (newSize > MAX_COMMENT_LENGTH) return 0; - } - break; - } - - case WM_SETFOCUS: - case WM_KILLFOCUS: { - RECT r; - GetClientRect(hwnd, &r); - InvalidateRect(hwnd, &r, TRUE); - break; - } - - case WM_DESTROY: { - // cleanup our property - HGLOBAL hData = RemoveProp(hwnd, L"PROP_GRAYTEXT"); - if (hData) GlobalFree(hData); - } - } - - return CallWindowProc(super, hwnd, uMsg, wParam, lParam); -} - -// Resize a control to fit this text -static int ResizeControl(HWND hwndButton, RECT& rect, wstring text, - bool shiftLeft, int userDefinedPadding) { - HDC hdc = GetDC(hwndButton); - HFONT hfont = (HFONT)SendMessage(hwndButton, WM_GETFONT, 0, 0); - if (hfont) SelectObject(hdc, hfont); - SIZE size, oldSize; - int sizeDiff = 0; - - wchar_t oldText[1024]; - GetWindowText(hwndButton, oldText, 1024); - - if (GetTextExtentPoint32(hdc, text.c_str(), text.length(), &size) - // default text on the button - && GetTextExtentPoint32(hdc, oldText, wcslen(oldText), &oldSize)) { - /* - Expand control widths to accomidate wider text strings. For most - controls (including buttons) the text padding is defined by the - dialog's rc file. Some controls (such as checkboxes) have padding - that extends to the end of the dialog, in which case we ignore the - rc padding and rely on a user defined value passed in through - userDefinedPadding. - */ - int textIncrease = size.cx - oldSize.cx; - if (textIncrease < 0) return 0; - int existingTextPadding; - if (userDefinedPadding == 0) - existingTextPadding = (rect.right - rect.left) - oldSize.cx; - else - existingTextPadding = userDefinedPadding; - sizeDiff = textIncrease + existingTextPadding; - - if (shiftLeft) { - // shift left by the amount the button should grow - rect.left -= sizeDiff; - } else { - // grow right instead - rect.right += sizeDiff; - } - MoveWindow(hwndButton, rect.left, rect.top, rect.right - rect.left, - rect.bottom - rect.top, TRUE); - } - return sizeDiff; -} - -// The window was resized horizontally, so widen some of our -// controls to make use of the space -static void StretchControlsToFit(HWND hwndDlg) { - int controls[] = {IDC_DESCRIPTIONTEXT, IDC_SUBMITREPORTCHECK, IDC_COMMENTTEXT, - IDC_INCLUDEURLCHECK, IDC_PROGRESSTEXT}; - - RECT dlgRect; - GetClientRect(hwndDlg, &dlgRect); - - for (size_t i = 0; i < sizeof(controls) / sizeof(controls[0]); i++) { - RECT r; - HWND hwndControl = GetDlgItem(hwndDlg, controls[i]); - GetRelativeRect(hwndControl, hwndDlg, &r); - // 6 pixel spacing on the right - if (r.right + 6 != dlgRect.right) { - r.right = dlgRect.right - 6; - MoveWindow(hwndControl, r.left, r.top, r.right - r.left, r.bottom - r.top, - TRUE); - } - } -} - -static void SubmitReportChecked(HWND hwndDlg) { - bool enabled = (IsDlgButtonChecked(hwndDlg, IDC_SUBMITREPORTCHECK) != 0); - EnableWindow(GetDlgItem(hwndDlg, IDC_VIEWREPORTBUTTON), enabled); - EnableWindow(GetDlgItem(hwndDlg, IDC_COMMENTTEXT), enabled); - EnableWindow(GetDlgItem(hwndDlg, IDC_INCLUDEURLCHECK), enabled); - SetDlgItemVisible(hwndDlg, IDC_PROGRESSTEXT, enabled); -} - -static INT_PTR DialogBoxParamMaybeRTL(UINT idd, HWND hwndParent, - DLGPROC dlgProc, LPARAM param) { - INT_PTR rv = 0; - if (gRTLlayout) { - // We need to toggle the WS_EX_LAYOUTRTL style flag on the dialog - // template. - HRSRC hDialogRC = FindResource(nullptr, MAKEINTRESOURCE(idd), RT_DIALOG); - HGLOBAL hDlgTemplate = LoadResource(nullptr, hDialogRC); - DLGTEMPLATEEX* pDlgTemplate = (DLGTEMPLATEEX*)LockResource(hDlgTemplate); - unsigned long sizeDlg = SizeofResource(nullptr, hDialogRC); - HGLOBAL hMyDlgTemplate = GlobalAlloc(GPTR, sizeDlg); - DLGTEMPLATEEX* pMyDlgTemplate = (DLGTEMPLATEEX*)GlobalLock(hMyDlgTemplate); - memcpy(pMyDlgTemplate, pDlgTemplate, sizeDlg); - - pMyDlgTemplate->exStyle |= WS_EX_LAYOUTRTL; - - rv = DialogBoxIndirectParam(nullptr, (LPCDLGTEMPLATE)pMyDlgTemplate, - hwndParent, dlgProc, param); - GlobalUnlock(hMyDlgTemplate); - GlobalFree(hMyDlgTemplate); - } else { - rv = DialogBoxParam(nullptr, MAKEINTRESOURCE(idd), hwndParent, dlgProc, - param); - } - - return rv; -} - -static BOOL CALLBACK CrashReporterDialogProc(HWND hwndDlg, UINT message, - WPARAM wParam, LPARAM lParam) { - static int sHeight = 0; - - bool success; - bool enabled; - - switch (message) { - case WM_INITDIALOG: { - GetThemeSizes(hwndDlg); - RECT r; - GetClientRect(hwndDlg, &r); - sHeight = r.bottom - r.top; - - SetWindowText(hwndDlg, Str(ST_CRASHREPORTERTITLE).c_str()); - HICON hIcon = - LoadIcon(GetModuleHandle(nullptr), MAKEINTRESOURCE(IDI_MAINICON)); - SendMessage(hwndDlg, WM_SETICON, ICON_SMALL, (LPARAM)hIcon); - SendMessage(hwndDlg, WM_SETICON, ICON_BIG, (LPARAM)hIcon); - - // resize the "View Report" button based on the string length - RECT rect; - HWND hwnd = GetDlgItem(hwndDlg, IDC_VIEWREPORTBUTTON); - GetRelativeRect(hwnd, hwndDlg, &rect); - ResizeControl(hwnd, rect, Str(ST_VIEWREPORT), false, 0); - SetDlgItemText(hwndDlg, IDC_VIEWREPORTBUTTON, Str(ST_VIEWREPORT).c_str()); - - hwnd = GetDlgItem(hwndDlg, IDC_SUBMITREPORTCHECK); - GetRelativeRect(hwnd, hwndDlg, &rect); - long maxdiff = ResizeControl(hwnd, rect, Str(ST_CHECKSUBMIT), false, - gCheckboxPadding); - SetDlgItemText(hwndDlg, IDC_SUBMITREPORTCHECK, - Str(ST_CHECKSUBMIT).c_str()); - - if (!CheckBoolKey(gCrashReporterKey.c_str(), SUBMIT_REPORT_VALUE, - &enabled)) - enabled = true; - - CheckDlgButton(hwndDlg, IDC_SUBMITREPORTCHECK, - enabled ? BST_CHECKED : BST_UNCHECKED); - SubmitReportChecked(hwndDlg); - - HWND hwndComment = GetDlgItem(hwndDlg, IDC_COMMENTTEXT); - WNDPROC OldWndProc = (WNDPROC)SetWindowLongPtr( - hwndComment, GWLP_WNDPROC, (LONG_PTR)EditSubclassProc); - - // Subclass comment edit control to get placeholder text - SetWindowLongPtr(hwndComment, GWLP_USERDATA, (LONG_PTR)OldWndProc); - wstring commentGrayText = Str(ST_COMMENTGRAYTEXT); - wchar_t* hMem = (wchar_t*)GlobalAlloc( - GPTR, (commentGrayText.length() + 1) * sizeof(wchar_t)); - wcscpy(hMem, commentGrayText.c_str()); - SetProp(hwndComment, L"PROP_GRAYTEXT", hMem); - - hwnd = GetDlgItem(hwndDlg, IDC_INCLUDEURLCHECK); - GetRelativeRect(hwnd, hwndDlg, &rect); - long diff = - ResizeControl(hwnd, rect, Str(ST_CHECKURL), false, gCheckboxPadding); - maxdiff = std::max(diff, maxdiff); - SetDlgItemText(hwndDlg, IDC_INCLUDEURLCHECK, Str(ST_CHECKURL).c_str()); - - // want this on by default - if (CheckBoolKey(gCrashReporterKey.c_str(), INCLUDE_URL_VALUE, - &enabled) && - !enabled) { - CheckDlgButton(hwndDlg, IDC_INCLUDEURLCHECK, BST_UNCHECKED); - } else { - CheckDlgButton(hwndDlg, IDC_INCLUDEURLCHECK, BST_CHECKED); - } - - SetDlgItemText(hwndDlg, IDC_PROGRESSTEXT, - Str(ST_REPORTPRESUBMIT).c_str()); - - RECT closeRect; - HWND hwndClose = GetDlgItem(hwndDlg, IDC_CLOSEBUTTON); - GetRelativeRect(hwndClose, hwndDlg, &closeRect); - - RECT restartRect; - HWND hwndRestart = GetDlgItem(hwndDlg, IDC_RESTARTBUTTON); - GetRelativeRect(hwndRestart, hwndDlg, &restartRect); - - // set the close button text and shift the buttons around - // since the size may need to change - int sizeDiff = ResizeControl(hwndClose, closeRect, Str(ST_QUIT), true, 0); - restartRect.left -= sizeDiff; - restartRect.right -= sizeDiff; - SetDlgItemText(hwndDlg, IDC_CLOSEBUTTON, Str(ST_QUIT).c_str()); - - if (gRestartArgs.size() > 0) { - // Resize restart button to fit text - ResizeControl(hwndRestart, restartRect, Str(ST_RESTART), true, 0); - SetDlgItemText(hwndDlg, IDC_RESTARTBUTTON, Str(ST_RESTART).c_str()); - } else { - // No restart arguments, so just hide the restart button - SetDlgItemVisible(hwndDlg, IDC_RESTARTBUTTON, false); - } - // See if we need to widen the window - // Leave 6 pixels on either side + 6 pixels between the buttons - int neededSize = closeRect.right - closeRect.left + restartRect.right - - restartRect.left + 6 * 3; - GetClientRect(hwndDlg, &r); - // We may already have resized one of the checkboxes above - maxdiff = std::max(maxdiff, neededSize - (r.right - r.left)); - - if (maxdiff > 0) { - // widen window - GetWindowRect(hwndDlg, &r); - r.right += maxdiff; - MoveWindow(hwndDlg, r.left, r.top, r.right - r.left, r.bottom - r.top, - TRUE); - // shift both buttons right - if (restartRect.left + maxdiff < 6) maxdiff += 6; - closeRect.left += maxdiff; - closeRect.right += maxdiff; - restartRect.left += maxdiff; - restartRect.right += maxdiff; - MoveWindow(hwndClose, closeRect.left, closeRect.top, - closeRect.right - closeRect.left, - closeRect.bottom - closeRect.top, TRUE); - StretchControlsToFit(hwndDlg); - } - // need to move the restart button regardless - MoveWindow(hwndRestart, restartRect.left, restartRect.top, - restartRect.right - restartRect.left, - restartRect.bottom - restartRect.top, TRUE); - - // Resize the description text last, in case the window was resized - // before this. - SendDlgItemMessage(hwndDlg, IDC_DESCRIPTIONTEXT, EM_SETEVENTMASK, - (WPARAM) nullptr, ENM_REQUESTRESIZE); - - wstring description = Str(ST_CRASHREPORTERHEADER); - description += L"\n\n"; - description += Str(ST_CRASHREPORTERDESCRIPTION); - SetDlgItemText(hwndDlg, IDC_DESCRIPTIONTEXT, description.c_str()); - - // Make the title bold. - CHARFORMAT fmt = { - 0, - }; - fmt.cbSize = sizeof(fmt); - fmt.dwMask = CFM_BOLD; - fmt.dwEffects = CFE_BOLD; - SendDlgItemMessage(hwndDlg, IDC_DESCRIPTIONTEXT, EM_SETSEL, 0, - Str(ST_CRASHREPORTERHEADER).length()); - SendDlgItemMessage(hwndDlg, IDC_DESCRIPTIONTEXT, EM_SETCHARFORMAT, - SCF_SELECTION, (LPARAM)&fmt); - SendDlgItemMessage(hwndDlg, IDC_DESCRIPTIONTEXT, EM_SETSEL, 0, 0); - // Force redraw. - SendDlgItemMessage(hwndDlg, IDC_DESCRIPTIONTEXT, EM_SETTARGETDEVICE, - (WPARAM) nullptr, 0); - // Force resize. - SendDlgItemMessage(hwndDlg, IDC_DESCRIPTIONTEXT, EM_REQUESTRESIZE, 0, 0); - - // if no URL was given, hide the URL checkbox - if (!gQueryParameters.isMember("URL")) { - RECT urlCheckRect; - GetWindowRect(GetDlgItem(hwndDlg, IDC_INCLUDEURLCHECK), &urlCheckRect); - - SetDlgItemVisible(hwndDlg, IDC_INCLUDEURLCHECK, false); - - gAttachedBottom.erase(IDC_VIEWREPORTBUTTON); - gAttachedBottom.erase(IDC_SUBMITREPORTCHECK); - gAttachedBottom.erase(IDC_COMMENTTEXT); - - StretchDialog(hwndDlg, urlCheckRect.top - urlCheckRect.bottom); - - gAttachedBottom.insert(IDC_VIEWREPORTBUTTON); - gAttachedBottom.insert(IDC_SUBMITREPORTCHECK); - gAttachedBottom.insert(IDC_COMMENTTEXT); - } - - MaybeResizeProgressText(hwndDlg); - - // Open the AVI resource for the throbber - Animate_Open(GetDlgItem(hwndDlg, IDC_THROBBER), - MAKEINTRESOURCE(IDR_THROBBER)); - - UpdateURL(hwndDlg); - - SetFocus(GetDlgItem(hwndDlg, IDC_SUBMITREPORTCHECK)); - return FALSE; - } - case WM_SIZE: { - ReflowDialog(hwndDlg, HIWORD(lParam) - sHeight); - sHeight = HIWORD(lParam); - InvalidateRect(hwndDlg, nullptr, TRUE); - return FALSE; - } - case WM_NOTIFY: { - NMHDR* notification = reinterpret_cast<NMHDR*>(lParam); - if (notification->code == EN_REQUESTRESIZE) { - // Resizing the rich edit control to fit the description text. - REQRESIZE* reqresize = reinterpret_cast<REQRESIZE*>(lParam); - RECT newSize = reqresize->rc; - RECT oldSize; - GetRelativeRect(notification->hwndFrom, hwndDlg, &oldSize); - - // resize the text box as requested - MoveWindow(notification->hwndFrom, newSize.left, newSize.top, - newSize.right - newSize.left, newSize.bottom - newSize.top, - TRUE); - - // Resize the dialog to fit (the WM_SIZE handler will move the controls) - StretchDialog(hwndDlg, newSize.bottom - oldSize.bottom); - } - return FALSE; - } - case WM_COMMAND: { - if (HIWORD(wParam) == BN_CLICKED) { - switch (LOWORD(wParam)) { - case IDC_VIEWREPORTBUTTON: - DialogBoxParamMaybeRTL(IDD_VIEWREPORTDIALOG, hwndDlg, - (DLGPROC)ViewReportDialogProc, 0); - break; - case IDC_SUBMITREPORTCHECK: - SubmitReportChecked(hwndDlg); - break; - case IDC_INCLUDEURLCHECK: - UpdateURL(hwndDlg); - break; - case IDC_CLOSEBUTTON: - MaybeSendReport(hwndDlg); - break; - case IDC_RESTARTBUTTON: - RestartApplication(); - MaybeSendReport(hwndDlg); - break; - } - } else if (HIWORD(wParam) == EN_CHANGE) { - switch (LOWORD(wParam)) { - case IDC_COMMENTTEXT: - UpdateComment(hwndDlg); - } - } - - return FALSE; - } - case WM_UPLOADCOMPLETE: { - WaitForSingleObject(gThreadHandle, INFINITE); - success = (wParam == 1); - SendCompleted(success, WideToUTF8(gSendData.serverResponse)); - // hide throbber - Animate_Stop(GetDlgItem(hwndDlg, IDC_THROBBER)); - SetDlgItemVisible(hwndDlg, IDC_THROBBER, false); - - SetDlgItemText(hwndDlg, IDC_PROGRESSTEXT, - success ? Str(ST_REPORTSUBMITSUCCESS).c_str() - : Str(ST_SUBMITFAILED).c_str()); - MaybeResizeProgressText(hwndDlg); - // close dialog after 5 seconds - SetTimer(hwndDlg, 0, 5000, nullptr); - // - return TRUE; - } - - case WM_TIMER: { - // The "1" gets used down in UIShowCrashUI to indicate that we at least - // tried to send the report. - EndCrashReporterDialog(hwndDlg, 1); - return FALSE; - } - - case WM_CLOSE: { - EndCrashReporterDialog(hwndDlg, 0); - return FALSE; - } - } - return FALSE; -} - -static wstring UTF8ToWide(const string& utf8, bool* success) { - wchar_t* buffer = nullptr; - int buffer_size = - MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, nullptr, 0); - if (buffer_size == 0) { - if (success) *success = false; - return L""; - } - - buffer = new wchar_t[buffer_size]; - if (buffer == nullptr) { - if (success) *success = false; - return L""; - } - - MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, buffer, buffer_size); - wstring str = buffer; - delete[] buffer; - - if (success) *success = true; - - return str; -} - -static string WideToMBCP(const wstring& wide, unsigned int cp, - bool* success = nullptr) { - char* buffer = nullptr; - int buffer_size = WideCharToMultiByte(cp, 0, wide.c_str(), -1, nullptr, 0, - nullptr, nullptr); - if (buffer_size == 0) { - if (success) *success = false; - return ""; - } - - buffer = new char[buffer_size]; - if (buffer == nullptr) { - if (success) *success = false; - return ""; - } - - WideCharToMultiByte(cp, 0, wide.c_str(), -1, buffer, buffer_size, nullptr, - nullptr); - string mb = buffer; - delete[] buffer; - - if (success) *success = true; - - return mb; -} - -string WideToUTF8(const wstring& wide, bool* success) { - return WideToMBCP(wide, CP_UTF8, success); -} - -/* === Crashreporter UI Functions === */ - -bool UIInit() { - for (size_t i = 0; i < sizeof(kDefaultAttachedBottom) / sizeof(UINT); i++) { - gAttachedBottom.insert(kDefaultAttachedBottom[i]); - } - - DoInitCommonControls(); - - return true; -} - -void UIShutdown() {} - -void UIShowDefaultUI() { - MessageBox(nullptr, Str(ST_CRASHREPORTERDEFAULT).c_str(), L"Crash Reporter", - MB_OK | MB_ICONSTOP); -} - -bool UIShowCrashUI(const StringTable& files, const Json::Value& queryParameters, - const string& sendURL, const vector<string>& restartArgs) { - gSendData.hDlg = nullptr; - gSendData.sendURL = UTF8ToWide(sendURL); - - for (StringTable::const_iterator i = files.begin(); i != files.end(); i++) { - gSendData.files[UTF8ToWide(i->first)] = UTF8ToWide(i->second); - } - - gQueryParameters = queryParameters; - - if (gQueryParameters.isMember("Vendor")) { - gCrashReporterKey = L"Software\\"; - string vendor = gQueryParameters["Vendor"].asString(); - if (!vendor.empty()) { - gCrashReporterKey += UTF8ToWide(vendor) + L"\\"; - } - string productName = gQueryParameters["ProductName"].asString(); - gCrashReporterKey += UTF8ToWide(productName) + L"\\Crash Reporter"; - } - - if (gQueryParameters.isMember("URL")) { - gURLParameter = gQueryParameters["URL"].asString(); - } - - gRestartArgs = restartArgs; - - if (gStrings.find("isRTL") != gStrings.end() && gStrings["isRTL"] == "yes") - gRTLlayout = true; - - if (gAutoSubmit) { - gSendData.queryParameters = gQueryParameters; - - gThreadHandle = - CreateThread(nullptr, 0, SendThreadProc, &gSendData, 0, nullptr); - WaitForSingleObject(gThreadHandle, INFINITE); - // SendCompleted was called from SendThreadProc - return true; - } - - return 1 == DialogBoxParamMaybeRTL(IDD_SENDDIALOG, nullptr, - (DLGPROC)CrashReporterDialogProc, 0); -} - -void UIError_impl(const string& message) { - wstring title = Str(ST_CRASHREPORTERTITLE); - if (title.empty()) title = L"Crash Reporter Error"; - - MessageBox(nullptr, UTF8ToWide(message).c_str(), title.c_str(), - MB_OK | MB_ICONSTOP); -} - -bool UIGetIniPath(string& path) { - wchar_t fileName[MAX_PATH]; - if (GetModuleFileName(nullptr, fileName, MAX_PATH)) { - // get crashreporter ini - wchar_t* s = wcsrchr(fileName, '.'); - if (s) { - wcscpy(s, L".ini"); - path = WideToUTF8(fileName); - return true; - } - } - - return false; -} - -bool UIGetSettingsPath(const string& vendor, const string& product, - string& settings_path) { - wchar_t path[MAX_PATH] = {}; - HRESULT hRes = SHGetFolderPath(nullptr, CSIDL_APPDATA, nullptr, 0, path); - if (FAILED(hRes)) { - // This provides a fallback for getting the path to APPDATA by querying the - // registry when the call to SHGetFolderPath is unable to provide this path - // (Bug 513958). - HKEY key; - DWORD type, dwRes; - DWORD size = sizeof(path) - 1; - dwRes = ::RegOpenKeyExW(HKEY_CURRENT_USER, - L"Software\\Microsoft\\Windows\\CurrentVersion\\Exp" - L"lorer\\Shell Folders", - 0, KEY_READ, &key); - if (dwRes != ERROR_SUCCESS) return false; - - dwRes = - RegQueryValueExW(key, L"AppData", nullptr, &type, (LPBYTE)&path, &size); - ::RegCloseKey(key); - // The call to RegQueryValueExW must succeed, the type must be REG_SZ, the - // buffer size must not equal 0, and the buffer size be a multiple of 2. - if (dwRes != ERROR_SUCCESS || type != REG_SZ || size == 0 || size % 2 != 0) - return false; - } - - if (!vendor.empty()) { - PathAppend(path, UTF8ToWide(vendor).c_str()); - } - PathAppend(path, UTF8ToWide(product).c_str()); - PathAppend(path, L"Crash Reports"); - settings_path = WideToUTF8(path); - return true; -} - -bool UIEnsurePathExists(const string& path) { - if (CreateDirectory(UTF8ToWide(path).c_str(), nullptr) == 0) { - if (GetLastError() != ERROR_ALREADY_EXISTS) return false; - } - - return true; -} - -bool UIFileExists(const string& path) { - DWORD attrs = GetFileAttributes(UTF8ToWide(path).c_str()); - return (attrs != INVALID_FILE_ATTRIBUTES); -} - -bool UIMoveFile(const string& oldfile, const string& newfile) { - if (oldfile == newfile) return true; - - return MoveFile(UTF8ToWide(oldfile).c_str(), UTF8ToWide(newfile).c_str()) == - TRUE; -} - -bool UIDeleteFile(const string& oldfile) { - return DeleteFile(UTF8ToWide(oldfile).c_str()) == TRUE; -} - -ifstream* UIOpenRead(const string& filename, ios_base::openmode mode) { -#if defined(_MSC_VER) - ifstream* file = new ifstream(); - file->open(UTF8ToWide(filename).c_str(), mode); -#else // GCC - ifstream* file = - new ifstream(WideToMBCP(UTF8ToWide(filename), CP_ACP).c_str(), mode); -#endif // _MSC_VER - - return file; -} - -ofstream* UIOpenWrite(const string& filename, ios_base::openmode mode) { -#if defined(_MSC_VER) - ofstream* file = new ofstream(); - file->open(UTF8ToWide(filename).c_str(), mode); -#else // GCC - ofstream* file = - new ofstream(WideToMBCP(UTF8ToWide(filename), CP_ACP).c_str(), mode); -#endif // _MSC_VER - - return file; -} - -struct FileData { - FILETIME timestamp; - wstring path; -}; - -static bool CompareFDTime(const FileData& fd1, const FileData& fd2) { - return CompareFileTime(&fd1.timestamp, &fd2.timestamp) > 0; -} - -void UIPruneSavedDumps(const std::string& directory) { - wstring wdirectory = UTF8ToWide(directory); - - WIN32_FIND_DATA fdata; - wstring findpath = wdirectory + L"\\*.dmp"; - HANDLE dirlist = FindFirstFile(findpath.c_str(), &fdata); - if (dirlist == INVALID_HANDLE_VALUE) return; - - vector<FileData> dumpfiles; - - for (BOOL ok = true; ok; ok = FindNextFile(dirlist, &fdata)) { - FileData fd = {fdata.ftLastWriteTime, wdirectory + L"\\" + fdata.cFileName}; - dumpfiles.push_back(fd); - } - - sort(dumpfiles.begin(), dumpfiles.end(), CompareFDTime); - - while (dumpfiles.size() > kSaveCount) { - // get the path of the oldest file - wstring path = (--dumpfiles.end())->path; - DeleteFile(path.c_str()); - - // s/.dmp/.extra/ - path.replace(path.size() - 4, 4, L".extra"); - DeleteFile(path.c_str()); - - dumpfiles.pop_back(); - } - FindClose(dirlist); -} - -bool UIRunProgram(const string& exename, const std::vector<std::string>& args, - bool wait) { - wstring cmdLine = L"\"" + UTF8ToWide(exename) + L"\" "; - - for (auto arg : args) { - cmdLine += L"\"" + UTF8ToWide(arg) + L"\" "; - } - - STARTUPINFO si = {}; - si.cb = sizeof(si); - PROCESS_INFORMATION pi = {}; - - if (!CreateProcess(/* lpApplicationName */ nullptr, (LPWSTR)cmdLine.c_str(), - /* lpProcessAttributes */ nullptr, - /* lpThreadAttributes */ nullptr, - /* bInheritHandles */ false, - NORMAL_PRIORITY_CLASS | CREATE_NO_WINDOW, - /* lpEnvironment */ nullptr, - /* lpCurrentDirectory */ nullptr, &si, &pi)) { - return false; - } - - if (wait) { - WaitForSingleObject(pi.hProcess, INFINITE); - } - - CloseHandle(pi.hProcess); - CloseHandle(pi.hThread); - return true; -} - -string UIGetEnv(const string& name) { - const wchar_t* var = _wgetenv(UTF8ToWide(name).c_str()); - if (var && *var) { - return WideToUTF8(var); - } - - return ""; -} diff --git a/toolkit/crashreporter/client/gtkbind/Cargo.toml b/toolkit/crashreporter/client/gtkbind/Cargo.toml new file mode 100644 index 0000000000..813d43dbe3 --- /dev/null +++ b/toolkit/crashreporter/client/gtkbind/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "gtkbind" +version = "0.1.0" +edition = "2021" + +[build-dependencies] +bindgen = { version = "0.69.0", default-features = false, features = ["runtime"] } +mozbuild = "0.1.0" diff --git a/toolkit/crashreporter/client/gtkbind/build.rs b/toolkit/crashreporter/client/gtkbind/build.rs new file mode 100644 index 0000000000..fa3402fdf2 --- /dev/null +++ b/toolkit/crashreporter/client/gtkbind/build.rs @@ -0,0 +1,43 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +use mozbuild::config::{ + CC_BASE_FLAGS as CFLAGS, MOZ_GTK3_CFLAGS as GTK_CFLAGS, MOZ_GTK3_LIBS as GTK_LIBS, +}; + +const HEADER: &str = r#" +#include "gtk/gtk.h" +#include "pango/pango.h" +#include "gdk-pixbuf/gdk-pixbuf.h" +"#; + +fn main() { + let bindings = bindgen::Builder::default() + .header_contents("gtk_bindings.h", HEADER) + .clang_args(CFLAGS) + .clang_args(GTK_CFLAGS) + .allowlist_function("gtk_.*") + .allowlist_function( + "g_(application|main_context|memory_input_stream|object|signal|timeout)_.*", + ) + .allowlist_function("gdk_pixbuf_new_from_stream") + .allowlist_function("pango_attr_.*") + // The gtk/glib valist functions generate FFI-unsafe signatures on aarch64 which cause + // compile errors. We don't use them anyway. + .blocklist_function(".*_valist") + .derive_default(true) + .generate() + .expect("unable to generate gtk bindings"); + for flag in GTK_LIBS { + if let Some(lib) = flag.strip_prefix("-l") { + println!("cargo:rustc-link-lib={lib}"); + } else if let Some(path) = flag.strip_prefix("-L") { + println!("cargo:rustc-link-search={path}"); + } + } + let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("gtk_bindings.rs")) + .expect("failed to write gtk bindings"); +} diff --git a/toolkit/crashreporter/process_reader/src/process_reader.rs b/toolkit/crashreporter/client/gtkbind/src/lib.rs index 1473aafa09..714f3ed047 100644 --- a/toolkit/crashreporter/process_reader/src/process_reader.rs +++ b/toolkit/crashreporter/client/gtkbind/src/lib.rs @@ -2,5 +2,8 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -#[cfg(target_os = "windows")] -mod windows; +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] + +include!(concat!(env!("OUT_DIR"), "/gtk_bindings.rs")); diff --git a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/classes.nib b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/classes.nib deleted file mode 100644 index 254131e431..0000000000 --- a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/classes.nib +++ /dev/null @@ -1,100 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> -<plist version="1.0"> -<dict> - <key>IBClasses</key> - <array> - <dict> - <key>ACTIONS</key> - <dict> - <key>closeClicked</key> - <string>id</string> - <key>includeURLClicked</key> - <string>id</string> - <key>restartClicked</key> - <string>id</string> - <key>submitReportClicked</key> - <string>id</string> - <key>viewReportClicked</key> - <string>id</string> - <key>viewReportOkClicked</key> - <string>id</string> - </dict> - <key>CLASS</key> - <string>CrashReporterUI</string> - <key>LANGUAGE</key> - <string>ObjC</string> - <key>OUTLETS</key> - <dict> - <key>mCloseButton</key> - <string>NSButton</string> - <key>mCommentScrollView</key> - <string>NSScrollView</string> - <key>mCommentText</key> - <string>TextViewWithPlaceHolder</string> - <key>mDescriptionLabel</key> - <string>NSTextField</string> - <key>mEmailMeButton</key> - <string>NSButton</string> - <key>mEmailText</key> - <string>NSTextField</string> - <key>mErrorCloseButton</key> - <string>NSButton</string> - <key>mErrorHeaderLabel</key> - <string>NSTextField</string> - <key>mErrorLabel</key> - <string>NSTextField</string> - <key>mErrorView</key> - <string>NSView</string> - <key>mHeaderLabel</key> - <string>NSTextField</string> - <key>mIncludeURLButton</key> - <string>NSButton</string> - <key>mProgressIndicator</key> - <string>NSProgressIndicator</string> - <key>mProgressText</key> - <string>NSTextField</string> - <key>mRestartButton</key> - <string>NSButton</string> - <key>mSubmitReportButton</key> - <string>NSButton</string> - <key>mViewReportButton</key> - <string>NSButton</string> - <key>mViewReportOkButton</key> - <string>NSButton</string> - <key>mViewReportTextView</key> - <string>NSTextView</string> - <key>mViewReportWindow</key> - <string>NSWindow</string> - <key>mWindow</key> - <string>NSWindow</string> - </dict> - <key>SUPERCLASS</key> - <string>NSObject</string> - </dict> - <dict> - <key>ACTIONS</key> - <dict> - <key>insertTab</key> - <string>id</string> - </dict> - <key>CLASS</key> - <string>TextViewWithPlaceHolder</string> - <key>LANGUAGE</key> - <string>ObjC</string> - <key>SUPERCLASS</key> - <string>NSTextView</string> - </dict> - <dict> - <key>CLASS</key> - <string>FirstResponder</string> - <key>LANGUAGE</key> - <string>ObjC</string> - <key>SUPERCLASS</key> - <string>NSObject</string> - </dict> - </array> - <key>IBVersion</key> - <string>1</string> -</dict> -</plist> diff --git a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/info.nib b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/info.nib deleted file mode 100644 index 517349ffce..0000000000 --- a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/info.nib +++ /dev/null @@ -1,18 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> -<plist version="1.0"> -<dict> - <key>IBFramework Version</key> - <string>629</string> - <key>IBOldestOS</key> - <integer>5</integer> - <key>IBOpenObjects</key> - <array> - <integer>2</integer> - </array> - <key>IBSystem Version</key> - <string>9C7010</string> - <key>targetFramework</key> - <string>IBCocoaFramework</string> -</dict> -</plist> diff --git a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/keyedobjects.nib b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/keyedobjects.nib Binary files differdeleted file mode 100644 index bfdcccb74c..0000000000 --- a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/keyedobjects.nib +++ /dev/null diff --git a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/classes.nib b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/classes.nib deleted file mode 100644 index 254131e431..0000000000 --- a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/classes.nib +++ /dev/null @@ -1,100 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> -<plist version="1.0"> -<dict> - <key>IBClasses</key> - <array> - <dict> - <key>ACTIONS</key> - <dict> - <key>closeClicked</key> - <string>id</string> - <key>includeURLClicked</key> - <string>id</string> - <key>restartClicked</key> - <string>id</string> - <key>submitReportClicked</key> - <string>id</string> - <key>viewReportClicked</key> - <string>id</string> - <key>viewReportOkClicked</key> - <string>id</string> - </dict> - <key>CLASS</key> - <string>CrashReporterUI</string> - <key>LANGUAGE</key> - <string>ObjC</string> - <key>OUTLETS</key> - <dict> - <key>mCloseButton</key> - <string>NSButton</string> - <key>mCommentScrollView</key> - <string>NSScrollView</string> - <key>mCommentText</key> - <string>TextViewWithPlaceHolder</string> - <key>mDescriptionLabel</key> - <string>NSTextField</string> - <key>mEmailMeButton</key> - <string>NSButton</string> - <key>mEmailText</key> - <string>NSTextField</string> - <key>mErrorCloseButton</key> - <string>NSButton</string> - <key>mErrorHeaderLabel</key> - <string>NSTextField</string> - <key>mErrorLabel</key> - <string>NSTextField</string> - <key>mErrorView</key> - <string>NSView</string> - <key>mHeaderLabel</key> - <string>NSTextField</string> - <key>mIncludeURLButton</key> - <string>NSButton</string> - <key>mProgressIndicator</key> - <string>NSProgressIndicator</string> - <key>mProgressText</key> - <string>NSTextField</string> - <key>mRestartButton</key> - <string>NSButton</string> - <key>mSubmitReportButton</key> - <string>NSButton</string> - <key>mViewReportButton</key> - <string>NSButton</string> - <key>mViewReportOkButton</key> - <string>NSButton</string> - <key>mViewReportTextView</key> - <string>NSTextView</string> - <key>mViewReportWindow</key> - <string>NSWindow</string> - <key>mWindow</key> - <string>NSWindow</string> - </dict> - <key>SUPERCLASS</key> - <string>NSObject</string> - </dict> - <dict> - <key>ACTIONS</key> - <dict> - <key>insertTab</key> - <string>id</string> - </dict> - <key>CLASS</key> - <string>TextViewWithPlaceHolder</string> - <key>LANGUAGE</key> - <string>ObjC</string> - <key>SUPERCLASS</key> - <string>NSTextView</string> - </dict> - <dict> - <key>CLASS</key> - <string>FirstResponder</string> - <key>LANGUAGE</key> - <string>ObjC</string> - <key>SUPERCLASS</key> - <string>NSObject</string> - </dict> - </array> - <key>IBVersion</key> - <string>1</string> -</dict> -</plist> diff --git a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/info.nib b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/info.nib deleted file mode 100644 index 4a2251aaf5..0000000000 --- a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/info.nib +++ /dev/null @@ -1,18 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> -<plist version="1.0"> -<dict> - <key>IBFramework Version</key> - <string>629</string> - <key>IBOldestOS</key> - <integer>5</integer> - <key>IBOpenObjects</key> - <array> - <integer>2</integer> - </array> - <key>IBSystem Version</key> - <string>9D34</string> - <key>targetFramework</key> - <string>IBCocoaFramework</string> -</dict> -</plist> diff --git a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/keyedobjects.nib b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/keyedobjects.nib Binary files differdeleted file mode 100644 index 6c93849b94..0000000000 --- a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/keyedobjects.nib +++ /dev/null diff --git a/toolkit/crashreporter/client/moz.build b/toolkit/crashreporter/client/moz.build deleted file mode 100644 index 82e19b8637..0000000000 --- a/toolkit/crashreporter/client/moz.build +++ /dev/null @@ -1,97 +0,0 @@ -# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- -# vim: set filetype=python: -# 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/. - -if CONFIG["OS_TARGET"] != "Android": - Program("crashreporter") - - UNIFIED_SOURCES += [ - "../CrashAnnotations.cpp", - "crashreporter.cpp", - "ping.cpp", - ] - - LOCAL_INCLUDES += [ - "/toolkit/components/jsoncpp/include", - ] - - USE_LIBS += [ - "jsoncpp", - ] - -if CONFIG["OS_ARCH"] == "WINNT": - UNIFIED_SOURCES += [ - "crashreporter_win.cpp", - ] - include("/toolkit/crashreporter/breakpad-client/windows/sender/objs.mozbuild") - SOURCES += objs_sender - SOURCES += [ - "../google-breakpad/src/common/windows/http_upload.cc", - ] - DEFINES["UNICODE"] = True - DEFINES["_UNICODE"] = True - USE_LIBS += [ - "nss", - ] - OS_LIBS += [ - "advapi32", - "comctl32", - "gdi32", - "ole32", - "shell32", - "wininet", - "shlwapi", - "user32", - ] -elif CONFIG["OS_ARCH"] == "Darwin": - UNIFIED_SOURCES += [ - "../google-breakpad/src/common/mac/HTTPMultipartUpload.m", - "crashreporter_osx.mm", - "crashreporter_unix_common.cpp", - ] - LOCAL_INCLUDES += [ - "../google-breakpad/src/common/mac", - ] - OS_LIBS += ["-framework Cocoa"] - USE_LIBS += [ - "nss", - ] - LDFLAGS += ["-Wl,-rpath,@executable_path/../../../"] -elif CONFIG["OS_ARCH"] == "SunOS": - SOURCES += [ - "crashreporter_linux.cpp", - "crashreporter_unix.cpp", - ] - USE_LIBS += [ - "breakpad_solaris_common_s", - ] - -if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": - UNIFIED_SOURCES += [ - "../google-breakpad/src/common/linux/http_upload.cc", - "crashreporter_gtk_common.cpp", - "crashreporter_linux.cpp", - "crashreporter_unix_common.cpp", - ] - OS_LIBS += CONFIG["MOZ_GTK3_LIBS"] - OS_LIBS += CONFIG["MOZ_GTHREAD_LIBS"] - CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"] - CXXFLAGS += CONFIG["MOZ_GTHREAD_CFLAGS"] - -if CONFIG["OS_ARCH"] == "Linux" or CONFIG["OS_ARCH"] == "SunOS": - FINAL_TARGET_FILES += [ - "Throbber-small.gif", - ] - -DEFINES["MOZ_APP_NAME"] = '"%s"' % CONFIG["MOZ_APP_NAME"] -DEFINES["BIN_SUFFIX"] = '"%s"' % CONFIG["BIN_SUFFIX"] - -RCINCLUDE = "crashreporter.rc" - -# Don't use the STL wrappers in the crashreporter clients; they don't -# link with -lmozalloc, and it really doesn't matter here anyway. -DisableStlWrapping() - -include("/toolkit/crashreporter/crashreporter.mozbuild") diff --git a/toolkit/crashreporter/client/ping.cpp b/toolkit/crashreporter/client/ping.cpp deleted file mode 100644 index b49211c9c1..0000000000 --- a/toolkit/crashreporter/client/ping.cpp +++ /dev/null @@ -1,324 +0,0 @@ -/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include "crashreporter.h" - -#include <cstring> -#include <ctime> -#include <string> - -#if defined(XP_LINUX) -# include <fcntl.h> -# include <unistd.h> -# include <sys/stat.h> -#elif defined(XP_MACOSX) -# include <CoreFoundation/CoreFoundation.h> -#elif defined(XP_WIN) -# include <objbase.h> -#endif - -#include "json/json.h" - -#include "CrashAnnotations.h" - -using std::string; - -namespace CrashReporter { - -struct UUID { - uint32_t m0; - uint16_t m1; - uint16_t m2; - uint8_t m3[8]; -}; - -// Generates an UUID; the code here is mostly copied from nsUUIDGenerator.cpp -static string GenerateUUID() { - UUID id = {}; - -#if defined(XP_WIN) // Windows - HRESULT hr = CoCreateGuid((GUID*)&id); - if (FAILED(hr)) { - return ""; - } -#elif defined(XP_MACOSX) // MacOS X - CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault); - if (!uuid) { - return ""; - } - - CFUUIDBytes bytes = CFUUIDGetUUIDBytes(uuid); - memcpy(&id, &bytes, sizeof(UUID)); - - CFRelease(uuid); -#elif defined(HAVE_ARC4RANDOM_BUF) // Android, BSD, ... - arc4random_buf(&id, sizeof(UUID)); -#else // Linux - int fd = open("/dev/urandom", O_RDONLY); - - if (fd == -1) { - return ""; - } - - if (read(fd, &id, sizeof(UUID)) != sizeof(UUID)) { - close(fd); - return ""; - } - - close(fd); -#endif - - /* Put in the version */ - id.m2 &= 0x0fff; - id.m2 |= 0x4000; - - /* Put in the variant */ - id.m3[0] &= 0x3f; - id.m3[0] |= 0x80; - - const char* kUUIDFormatString = - "%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x"; - const size_t kUUIDFormatStringLength = 36; - char str[kUUIDFormatStringLength + 1] = {'\0'}; - - int num = snprintf(str, kUUIDFormatStringLength + 1, kUUIDFormatString, id.m0, - id.m1, id.m2, id.m3[0], id.m3[1], id.m3[2], id.m3[3], - id.m3[4], id.m3[5], id.m3[6], id.m3[7]); - - if (num != kUUIDFormatStringLength) { - return ""; - } - - return str; -} - -const char kISO8601Date[] = "%F"; -const char kISO8601DateHours[] = "%FT%H:00:00.000Z"; - -// Return the current date as a string in the specified format, the following -// constants are provided: -// - kISO8601Date, the ISO 8601 date format, YYYY-MM-DD -// - kISO8601DateHours, the ISO 8601 full date format, YYYY-MM-DDTHH:00:00.000Z -static string CurrentDate(string format) { - time_t now; - time(&now); - char buf[64]; // This should be plenty - strftime(buf, sizeof buf, format.c_str(), gmtime(&now)); - return buf; -} - -const char kTelemetryClientId[] = "TelemetryClientId"; -const char kTelemetryUrl[] = "TelemetryServerURL"; -const char kTelemetrySessionId[] = "TelemetrySessionId"; -const int kTelemetryVersion = 4; - -// Create the payload.metadata node of the crash ping using fields extracted -// from the .extra file -static Json::Value CreateMetadataNode(const Json::Value& aExtra) { - Json::Value node; - - for (Json::ValueConstIterator iter = aExtra.begin(); iter != aExtra.end(); - ++iter) { - Annotation annotation; - - if (AnnotationFromString(annotation, iter.memberName())) { - if (IsAnnotationAllowedForPing(annotation)) { - node[iter.memberName()] = *iter; - } - } - } - - return node; -} - -// Create the payload node of the crash ping -static Json::Value CreatePayloadNode(const Json::Value& aExtra, - const string& aHash, - const string& aSessionId) { - Json::Value payload; - - payload["sessionId"] = aSessionId; - payload["version"] = 1; - payload["crashDate"] = CurrentDate(kISO8601Date); - payload["crashTime"] = CurrentDate(kISO8601DateHours); - payload["hasCrashEnvironment"] = true; - payload["crashId"] = CrashReporter::GetDumpLocalID(); - payload["minidumpSha256Hash"] = aHash; - payload["processType"] = "main"; // This is always a main crash - if (aExtra.isMember("StackTraces")) { - payload["stackTraces"] = aExtra["StackTraces"]; - } - - // Assemble the payload metadata - payload["metadata"] = CreateMetadataNode(aExtra); - - return payload; -} - -// Create the application node of the crash ping -static Json::Value CreateApplicationNode( - const string& aVendor, const string& aName, const string& aVersion, - const string& aDisplayVersion, const string& aPlatformVersion, - const string& aChannel, const string& aBuildId, const string& aArchitecture, - const string& aXpcomAbi) { - Json::Value application; - - application["vendor"] = aVendor; - application["name"] = aName; - application["buildId"] = aBuildId; - application["displayVersion"] = aDisplayVersion; - application["platformVersion"] = aPlatformVersion; - application["version"] = aVersion; - application["channel"] = aChannel; - if (!aArchitecture.empty()) { - application["architecture"] = aArchitecture; - } - if (!aXpcomAbi.empty()) { - application["xpcomAbi"] = aXpcomAbi; - } - - return application; -} - -// Create the root node of the crash ping -static Json::Value CreateRootNode( - const Json::Value& aExtra, const string& aUuid, const string& aHash, - const string& aClientId, const string& aSessionId, const string& aName, - const string& aVersion, const string& aChannel, const string& aBuildId) { - Json::Value root; - root["type"] = "crash"; // This is a crash ping - root["id"] = aUuid; - root["version"] = kTelemetryVersion; - root["creationDate"] = CurrentDate(kISO8601DateHours); - root["clientId"] = aClientId; - - // Parse the telemetry environment - Json::Value environment; - Json::Reader reader; - string architecture; - string xpcomAbi; - string displayVersion; - string platformVersion; - - if (reader.parse(aExtra["TelemetryEnvironment"].asString(), environment, - /* collectComments */ false)) { - if (environment.isMember("build") && environment["build"].isObject()) { - Json::Value build = environment["build"]; - if (build.isMember("architecture") && build["architecture"].isString()) { - architecture = build["architecture"].asString(); - } - if (build.isMember("xpcomAbi") && build["xpcomAbi"].isString()) { - xpcomAbi = build["xpcomAbi"].asString(); - } - if (build.isMember("displayVersion") && - build["displayVersion"].isString()) { - displayVersion = build["displayVersion"].asString(); - } - if (build.isMember("platformVersion") && - build["platformVersion"].isString()) { - platformVersion = build["platformVersion"].asString(); - } - } - - root["environment"] = environment; - } - - root["payload"] = CreatePayloadNode(aExtra, aHash, aSessionId); - root["application"] = CreateApplicationNode( - aExtra["Vendor"].asString(), aName, aVersion, displayVersion, - platformVersion, aChannel, aBuildId, architecture, xpcomAbi); - - return root; -} - -// Generates the URL used to submit the crash ping, see TelemetrySend.sys.mjs -string GenerateSubmissionUrl(const string& aUrl, const string& aId, - const string& aName, const string& aVersion, - const string& aChannel, const string& aBuildId) { - return aUrl + "/submit/telemetry/" + aId + "/crash/" + aName + "/" + - aVersion + "/" + aChannel + "/" + aBuildId + - "?v=" + std::to_string(kTelemetryVersion); -} - -// Write out the ping into the specified file. -// -// Returns true if the ping was written out successfully, false otherwise. -static bool WritePing(const string& aPath, const string& aPing) { - std::ofstream* f = UIOpenWrite(aPath, std::ios::trunc); - bool success = false; - - if (f->is_open()) { - *f << aPing; - f->close(); - success = f->good(); - } - - delete f; - return success; -} - -// Assembles the crash ping using the JSON data extracted from the .extra file -// and sends it using the crash sender. All the telemetry specific data but the -// environment will be stripped from the annotations so that it won't be sent -// together with the crash report. -// -// Note that the crash ping sender is invoked in a fire-and-forget way so this -// won't block waiting for the ping to be delivered. -// -// Returns true if the ping was assembled and handed over to the pingsender -// correctly, also populates the aPingUuid parameter with the ping UUID. Returns -// false otherwise and leaves the aPingUuid parameter unmodified. -bool SendCrashPing(Json::Value& aExtra, const string& aHash, string& aPingUuid, - const string& pingDir) { - // Remove the telemetry-related data from the crash annotations - Json::Value value; - aExtra.removeMember(kTelemetryClientId, &value); - string clientId = value.asString(); - aExtra.removeMember(kTelemetryUrl, &value); - string serverUrl = value.asString(); - aExtra.removeMember(kTelemetrySessionId, &value); - string sessionId = value.asString(); - - if (clientId.empty() || serverUrl.empty() || sessionId.empty()) { - return false; - } - - string buildId = aExtra["BuildID"].asString(); - string channel = aExtra["ReleaseChannel"].asString(); - string name = aExtra["ProductName"].asString(); - string version = aExtra["Version"].asString(); - string uuid = GenerateUUID(); - string url = - GenerateSubmissionUrl(serverUrl, uuid, name, version, channel, buildId); - - if (serverUrl.empty() || uuid.empty()) { - return false; - } - - Json::Value root = CreateRootNode(aExtra, uuid, aHash, clientId, sessionId, - name, version, channel, buildId); - - // Write out the result to the pending pings directory - Json::StreamWriterBuilder builder; - builder["indentation"] = ""; - string ping = Json::writeString(builder, root); - string pingPath = pingDir + UI_DIR_SEPARATOR + uuid + ".json"; - - if (!WritePing(pingPath, ping)) { - return false; - } - - // Hand over the ping to the sender - std::vector<string> args = {url, pingPath}; - if (UIRunProgram(CrashReporter::GetProgramPath(UI_PING_SENDER_FILENAME), - args)) { - aPingUuid = uuid; - return true; - } else { - return false; - } -} - -} // namespace CrashReporter diff --git a/toolkit/crashreporter/client/resource.h b/toolkit/crashreporter/client/resource.h deleted file mode 100644 index 2e7917daa4..0000000000 --- a/toolkit/crashreporter/client/resource.h +++ /dev/null @@ -1,35 +0,0 @@ -/* 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/. */ - -//{{NO_DEPENDENCIES}} -// Microsoft Visual C++ generated include file. -// Used by crashreporter.rc -// -#define IDD_SENDDIALOG 102 -#define IDR_THROBBER 103 -#define IDD_VIEWREPORTDIALOG 104 -#define IDI_MAINICON 105 -#define IDC_PROGRESS 1003 -#define IDC_DESCRIPTIONTEXT 1004 -#define IDC_CLOSEBUTTON 1005 -#define IDC_VIEWREPORTBUTTON 1006 -#define IDC_SUBMITREPORTCHECK 1007 -#define IDC_INCLUDEURLCHECK 1010 -#define IDC_COMMENTTEXT 1011 -#define IDC_RESTARTBUTTON 1012 -#define IDC_DESCRIPTIONLABEL 1013 -#define IDC_PROGRESSTEXT 1014 -#define IDC_THROBBER 1015 -#define IDC_VIEWREPORTTEXT 1016 - -// Next default values for new objects -// -#ifdef APSTUDIO_INVOKED -# ifndef APSTUDIO_READONLY_SYMBOLS -# define _APS_NEXT_RESOURCE_VALUE 106 -# define _APS_NEXT_COMMAND_VALUE 40001 -# define _APS_NEXT_CONTROL_VALUE 1017 -# define _APS_NEXT_SYMED_VALUE 101 -# endif -#endif diff --git a/toolkit/crashreporter/docs/index.rst b/toolkit/crashreporter/docs/index.rst index fe1af45d8f..d08c28fb92 100644 --- a/toolkit/crashreporter/docs/index.rst +++ b/toolkit/crashreporter/docs/index.rst @@ -258,6 +258,15 @@ Environment variables used internally - ``MOZ_CRASHREPORTER_STRINGS_OVERRIDE`` - Overrides the path used to load the .ini file holding the strings used in the crash reporter client UI. +Environment variables used for development +------------------------------------------ + +Set these at build time (e.g. ``ac_add_options`` in ``.mozconfig``). + +- ``MOZ_CRASHREPORTER_MOCK`` - When set, causes the crash reporter client to + mock its interfaces to the system so that you can test the GUI behavior. The + GUI will not interact with the host system at all when this is set. + Other topics ============ diff --git a/toolkit/crashreporter/moz.build b/toolkit/crashreporter/moz.build index 952ca405bb..2fb1071de9 100644 --- a/toolkit/crashreporter/moz.build +++ b/toolkit/crashreporter/moz.build @@ -67,10 +67,12 @@ if CONFIG["MOZ_CRASHREPORTER"]: DIRS += ["rust_minidump_writer_linux"] if CONFIG["OS_TARGET"] != "Android": - DIRS += ["minidump-analyzer"] + DIRS += [ + "client/app", + "minidump-analyzer", + ] DIRS += [ - "client", "mozannotation_client", "mozannotation_server", ] @@ -118,9 +120,12 @@ if CONFIG["MOZ_CRASHREPORTER"]: DEFINES["MOZ_PHC"] = True LOCAL_INCLUDES += [ + "/toolkit/components/jsoncpp/include", "google-breakpad/src", ] + USE_LIBS += ["jsoncpp"] + PYTHON_UNITTEST_MANIFESTS += [ "tools/python.toml", ] diff --git a/toolkit/crashreporter/mozannotation_client/src/lib.rs b/toolkit/crashreporter/mozannotation_client/src/lib.rs index df2bd5bf94..2df133c4b9 100644 --- a/toolkit/crashreporter/mozannotation_client/src/lib.rs +++ b/toolkit/crashreporter/mozannotation_client/src/lib.rs @@ -101,6 +101,9 @@ extern "C" { #[cfg(target_os = "windows")] pub const ANNOTATION_SECTION: &'static [u8; 8] = b"mozannot"; +#[cfg(target_os = "macos")] +pub const ANNOTATION_SECTION: &'static [u8; 16] = b"mozannotation\0\0\0"; + // TODO: Use the following constants in the assembly below when constant // expressions are stabilized: https://github.com/rust-lang/rust/issues/93332 #[cfg(any(target_os = "linux", target_os = "android"))] diff --git a/toolkit/crashreporter/mozannotation_server/Cargo.toml b/toolkit/crashreporter/mozannotation_server/Cargo.toml index 3a5834a645..bd3fb283bf 100644 --- a/toolkit/crashreporter/mozannotation_server/Cargo.toml +++ b/toolkit/crashreporter/mozannotation_server/Cargo.toml @@ -8,17 +8,10 @@ license = "MPL-2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -goblin = { version = "0.7", features = ["elf32", "elf64", "pe32", "pe64"] } -memoffset = "0.8" mozannotation_client = { path = "../mozannotation_client/" } +process_reader = { path = "../process_reader/" } thin-vec = { version = "0.2.7", features = ["gecko-ffi"] } thiserror = "1.0.38" [target."cfg(any(target_os = \"linux\", target_os = \"android\"))".dependencies] -libc = "0.2" - -[target."cfg(target_os = \"windows\")".dependencies] -winapi = { version = "0.3", features = ["minwindef", "memoryapi", "psapi"] } - -[target."cfg(target_os = \"macos\")".dependencies] -mach2 = { version = "0.4" } +memoffset = "0.8" diff --git a/toolkit/crashreporter/mozannotation_server/src/errors.rs b/toolkit/crashreporter/mozannotation_server/src/errors.rs index 037d432e5e..b8103ad4e4 100644 --- a/toolkit/crashreporter/mozannotation_server/src/errors.rs +++ b/toolkit/crashreporter/mozannotation_server/src/errors.rs @@ -5,77 +5,13 @@ use thiserror::Error; #[derive(Debug, Error)] -pub enum RetrievalError { - #[error("The process handle/PID was invalid")] - InvalidProcessHandle, - #[error("Could not find the address of the annotations vector")] - AnnotationTableNotFound(#[from] FindAnnotationsAddressError), +pub enum AnnotationsRetrievalError { + #[error("Address was out of bounds")] + InvalidAddress, #[error("Corrupt or wrong annotation table")] InvalidAnnotationTable, #[error("The data read from the target process is invalid")] InvalidData, - #[cfg(any(target_os = "linux", target_os = "android"))] - #[error("Could not attach to the target process")] - AttachError(#[from] PtraceError), - #[error("Could not read from the target process address space")] - ReadFromProcessError(#[from] ReadError), - #[error("waitpid() failed when attaching to the process")] - WaitPidError, -} - -#[derive(Debug, Error)] -pub enum FindAnnotationsAddressError { - #[error("Could not convert address {0}")] - ConvertAddressError(#[from] std::num::TryFromIntError), - #[error("goblin failed to parse a module")] - GoblinError(#[from] goblin::error::Error), - #[error("Address was out of bounds")] - InvalidAddress, - #[error("IO error for file {0}")] - IOError(#[from] std::io::Error), - #[error("Could not find the address of the annotations vector")] - NotFound, - #[error("Could not parse address {0}")] - ParseAddressError(#[from] std::num::ParseIntError), - #[error("Could not parse a line in /proc/<pid>/maps")] - ProcMapsParseError, - #[cfg(any(target_os = "linux", target_os = "android"))] - #[error("Program header was not found")] - ProgramHeaderNotFound, - #[cfg(target_os = "windows")] - #[error("Section was not found")] - SectionNotFound, - #[cfg(target_os = "windows")] - #[error("Cannot enumerate the target process's modules")] - EnumProcessModulesError, - #[error("Could not read memory from the target process")] - ReadError(#[from] ReadError), - #[cfg(target_os = "macos")] - #[error("Failure when requesting the task information")] - TaskInfoError, - #[cfg(target_os = "macos")] - #[error("The task dyld information format is unknown or invalid")] - ImageFormatError, -} - -#[derive(Debug, Error)] -pub enum ReadError { - #[cfg(any(target_os = "linux", target_os = "android"))] - #[error("ptrace-specific error")] - PtraceError(#[from] PtraceError), - #[cfg(target_os = "windows")] - #[error("ReadProcessMemory failed")] - ReadProcessMemoryError, - #[cfg(target_os = "macos")] - #[error("mach call failed")] - MachError, -} - -#[cfg(any(target_os = "linux", target_os = "android"))] -#[derive(Debug, Error)] -pub enum PtraceError { - #[error("Could not read from the target process address space")] - ReadError(#[source] std::io::Error), - #[error("Could not trace the process")] - TraceError(#[source] std::io::Error), + #[error("Could not execute operation on the target process")] + ProcessReaderError(#[from] process_reader::error::ProcessReaderError), } diff --git a/toolkit/crashreporter/mozannotation_server/src/lib.rs b/toolkit/crashreporter/mozannotation_server/src/lib.rs index 6ed5b3cc41..27682247f8 100644 --- a/toolkit/crashreporter/mozannotation_server/src/lib.rs +++ b/toolkit/crashreporter/mozannotation_server/src/lib.rs @@ -3,12 +3,18 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ mod errors; -mod process_reader; use crate::errors::*; +#[cfg(any(target_os = "linux", target_os = "android"))] +use memoffset::offset_of; +use process_reader::error::ProcessReaderError; use process_reader::ProcessReader; +#[cfg(any(target_os = "windows", target_os = "macos"))] +use mozannotation_client::ANNOTATION_SECTION; use mozannotation_client::{Annotation, AnnotationContents, AnnotationMutex}; +#[cfg(any(target_os = "linux", target_os = "android"))] +use mozannotation_client::{MozAnnotationNote, ANNOTATION_TYPE}; use std::cmp::min; use std::iter::FromIterator; use std::mem::{size_of, ManuallyDrop}; @@ -29,12 +35,7 @@ pub struct CAnnotation { data: AnnotationData, } -#[cfg(target_os = "windows")] -type ProcessHandle = winapi::shared::ntdef::HANDLE; -#[cfg(any(target_os = "linux", target_os = "android"))] -type ProcessHandle = libc::pid_t; -#[cfg(any(target_os = "macos"))] -type ProcessHandle = mach2::mach_types::task_t; +pub type ProcessHandle = process_reader::ProcessHandle; /// Return the annotations of a given process. /// @@ -68,19 +69,23 @@ pub unsafe extern "C" fn mozannotation_free(ptr: *mut ThinVec<CAnnotation>) { pub fn retrieve_annotations( process: ProcessHandle, max_annotations: usize, -) -> Result<Box<ThinVec<CAnnotation>>, RetrievalError> { +) -> Result<Box<ThinVec<CAnnotation>>, AnnotationsRetrievalError> { let reader = ProcessReader::new(process)?; - let address = reader.find_annotations()?; + let address = find_annotations(&reader)?; - let mut mutex = reader.copy_object_shallow::<AnnotationMutex>(address)?; + let mut mutex = reader + .copy_object_shallow::<AnnotationMutex>(address) + .map_err(ProcessReaderError::from)?; let mutex = unsafe { mutex.assume_init_mut() }; // TODO: we should clear the poison value here before getting the mutex // contents. Right now we have to fail if the mutex was poisoned. - let annotation_table = mutex.get_mut().map_err(|_e| RetrievalError::InvalidData)?; + let annotation_table = mutex + .get_mut() + .map_err(|_e| AnnotationsRetrievalError::InvalidData)?; if !annotation_table.verify() { - return Err(RetrievalError::InvalidAnnotationTable); + return Err(AnnotationsRetrievalError::InvalidAnnotationTable); } let vec_pointer = annotation_table.get_ptr(); @@ -97,8 +102,47 @@ pub fn retrieve_annotations( Ok(Box::new(annotations)) } +fn find_annotations(reader: &ProcessReader) -> Result<usize, AnnotationsRetrievalError> { + #[cfg(any(target_os = "linux", target_os = "android"))] + { + let libxul_address = reader.find_module("libxul.so")?; + let note_address = reader.find_program_note( + libxul_address, + ANNOTATION_TYPE, + size_of::<MozAnnotationNote>(), + )?; + + let note = reader + .copy_object::<MozAnnotationNote>(note_address) + .map_err(ProcessReaderError::from)?; + let desc = note.desc; + let ehdr = (-note.ehdr) as usize; + let offset = desc + ehdr + - (offset_of!(MozAnnotationNote, ehdr) - offset_of!(MozAnnotationNote, desc)); + + usize::checked_add(libxul_address, offset).ok_or(AnnotationsRetrievalError::InvalidAddress) + } + #[cfg(any(target_os = "macos"))] + { + let libxul_address = reader.find_module("XUL")?; + reader + .find_section(libxul_address, ANNOTATION_SECTION) + .map_err(AnnotationsRetrievalError::from) + } + #[cfg(any(target_os = "windows"))] + { + let libxul_address = reader.find_module("xul.dll")?; + reader + .find_section(libxul_address, ANNOTATION_SECTION) + .map_err(AnnotationsRetrievalError::from) + } +} + // Read an annotation from the given address -fn read_annotation(reader: &ProcessReader, address: usize) -> Result<CAnnotation, ReadError> { +fn read_annotation( + reader: &ProcessReader, + address: usize, +) -> Result<CAnnotation, process_reader::error::ReadError> { let raw_annotation = ManuallyDrop::new(reader.copy_object::<Annotation>(address)?); let mut annotation = CAnnotation { id: raw_annotation.id, @@ -116,16 +160,16 @@ fn read_annotation(reader: &ProcessReader, address: usize) -> Result<CAnnotation annotation.data = AnnotationData::ByteBuffer(string); } AnnotationContents::CStringPointer => { - let buffer = copy_null_terminated_string_pointer(reader, raw_annotation.address)?; - annotation.data = AnnotationData::ByteBuffer(buffer); + let string = copy_null_terminated_string_pointer(reader, raw_annotation.address)?; + annotation.data = AnnotationData::ByteBuffer(string); } AnnotationContents::CString => { - let buffer = copy_null_terminated_string(reader, raw_annotation.address)?; - annotation.data = AnnotationData::ByteBuffer(buffer); + let string = copy_null_terminated_string(reader, raw_annotation.address)?; + annotation.data = AnnotationData::ByteBuffer(string); } AnnotationContents::ByteBuffer(size) | AnnotationContents::OwnedByteBuffer(size) => { - let buffer = copy_bytebuffer(reader, raw_annotation.address, size)?; - annotation.data = AnnotationData::ByteBuffer(buffer); + let string = copy_bytebuffer(reader, raw_annotation.address, size)?; + annotation.data = AnnotationData::ByteBuffer(string); } }; @@ -135,7 +179,7 @@ fn read_annotation(reader: &ProcessReader, address: usize) -> Result<CAnnotation fn copy_null_terminated_string_pointer( reader: &ProcessReader, address: usize, -) -> Result<ThinVec<u8>, ReadError> { +) -> Result<ThinVec<u8>, process_reader::error::ReadError> { let buffer_address = reader.copy_object::<usize>(address)?; copy_null_terminated_string(reader, buffer_address) } @@ -143,56 +187,15 @@ fn copy_null_terminated_string_pointer( fn copy_null_terminated_string( reader: &ProcessReader, address: usize, -) -> Result<ThinVec<u8>, ReadError> { - // Try copying the string word-by-word first, this is considerably faster - // than one byte at a time. - if let Ok(string) = copy_null_terminated_string_word_by_word(reader, address) { - return Ok(string); - } - - // Reading the string one word at a time failed, let's try again one byte - // at a time. It's slow but it might work in situations where the string - // alignment causes word-by-word access to straddle page boundaries. - let mut length = 0; - let mut string = ThinVec::<u8>::new(); - - loop { - let char = reader.copy_object::<u8>(address + length)?; - length += 1; - string.push(char); - - if char == 0 { - break; - } - } - - Ok(string) +) -> Result<ThinVec<u8>, process_reader::error::ReadError> { + let string = reader.copy_null_terminated_string(address)?; + Ok(ThinVec::<u8>::from(string.as_bytes())) } -fn copy_null_terminated_string_word_by_word( +fn copy_nscstring( reader: &ProcessReader, address: usize, -) -> Result<ThinVec<u8>, ReadError> { - const WORD_SIZE: usize = size_of::<usize>(); - let mut length = 0; - let mut string = ThinVec::<u8>::new(); - - loop { - let array = reader.copy_array::<u8>(address + length, WORD_SIZE)?; - let null_terminator = array.iter().position(|&e| e == 0); - length += null_terminator.unwrap_or(WORD_SIZE); - string.extend(array.into_iter()); - - if null_terminator.is_some() { - string.truncate(length); - break; - } - } - - Ok(string) -} - -fn copy_nscstring(reader: &ProcessReader, address: usize) -> Result<ThinVec<u8>, ReadError> { +) -> Result<ThinVec<u8>, process_reader::error::ReadError> { // HACK: This assumes the layout of the nsCString object let length_address = address + size_of::<usize>(); let length = reader.copy_object::<u32>(length_address)?; @@ -211,7 +214,7 @@ fn copy_bytebuffer( reader: &ProcessReader, address: usize, size: u32, -) -> Result<ThinVec<u8>, ReadError> { +) -> Result<ThinVec<u8>, process_reader::error::ReadError> { let value = reader.copy_array::<u8>(address, size as _)?; Ok(ThinVec::<u8>::from_iter(value.into_iter())) } diff --git a/toolkit/crashreporter/mozannotation_server/src/process_reader/windows.rs b/toolkit/crashreporter/mozannotation_server/src/process_reader/windows.rs deleted file mode 100644 index ffdcd95937..0000000000 --- a/toolkit/crashreporter/mozannotation_server/src/process_reader/windows.rs +++ /dev/null @@ -1,193 +0,0 @@ -/* 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 https://mozilla.org/MPL/2.0/. */ - -use std::{ - convert::TryInto, - mem::{size_of, MaybeUninit}, - ptr::null_mut, -}; - -use winapi::{ - shared::{ - basetsd::SIZE_T, - minwindef::{DWORD, FALSE, HMODULE}, - }, - um::{ - memoryapi::ReadProcessMemory, - psapi::{K32EnumProcessModules, K32GetModuleInformation, MODULEINFO}, - }, -}; - -use crate::{ - errors::{FindAnnotationsAddressError, ReadError, RetrievalError}, - ProcessHandle, -}; - -use super::ProcessReader; - -impl ProcessReader { - pub fn new(process: ProcessHandle) -> Result<ProcessReader, RetrievalError> { - Ok(ProcessReader { process }) - } - - pub fn find_annotations(&self) -> Result<usize, FindAnnotationsAddressError> { - let modules = self.get_module_list()?; - - modules - .iter() - .find_map(|&module| { - self.get_module_info(module).and_then(|info| { - self.find_annotations_in_module( - info.lpBaseOfDll as usize, - info.SizeOfImage as usize, - ) - .ok() - }) - }) - .ok_or(FindAnnotationsAddressError::InvalidAddress) - } - - fn get_module_list(&self) -> Result<Vec<HMODULE>, FindAnnotationsAddressError> { - let mut module_num: usize = 100; - let mut required_buffer_size: DWORD = 0; - let mut module_array = Vec::<MaybeUninit<HMODULE>>::with_capacity(module_num); - - loop { - let buffer_size: DWORD = (module_num * size_of::<HMODULE>()).try_into()?; - let res = unsafe { - K32EnumProcessModules( - self.process, - module_array.as_mut_ptr() as *mut _, - buffer_size, - &mut required_buffer_size as *mut _, - ) - }; - - module_num = required_buffer_size as usize / size_of::<HMODULE>(); - - if res == 0 { - if required_buffer_size > buffer_size { - module_array = Vec::<MaybeUninit<HMODULE>>::with_capacity(module_num); - } else { - return Err(FindAnnotationsAddressError::EnumProcessModulesError); - } - } else { - break; - } - } - - // SAFETY: module_array has been filled by K32EnumProcessModules() - let module_array: Vec<HMODULE> = unsafe { - module_array.set_len(module_num); - std::mem::transmute(module_array) - }; - - Ok(module_array) - } - - fn get_module_info(&self, module: HMODULE) -> Option<MODULEINFO> { - let mut info: MaybeUninit<MODULEINFO> = MaybeUninit::uninit(); - let res = unsafe { - K32GetModuleInformation( - self.process, - module, - info.as_mut_ptr(), - size_of::<MODULEINFO>() as DWORD, - ) - }; - - if res == 0 { - None - } else { - let info = unsafe { info.assume_init() }; - Some(info) - } - } - - fn find_annotations_in_module( - &self, - module_address: usize, - size: usize, - ) -> Result<usize, FindAnnotationsAddressError> { - // We read only the first page from the module, this should be more than - // enough to read the header and section list. In the future we might do - // this incrementally but for now goblin requires an array to parse - // so we can't do it just yet. - let page_size = 4096; - if size < page_size { - // Don't try to read from the target module if it's too small - return Err(FindAnnotationsAddressError::ReadError( - ReadError::ReadProcessMemoryError, - )); - } - - let bytes = self.copy_array(module_address as _, 4096)?; - let header = goblin::pe::header::Header::parse(&bytes)?; - - // Skip the PE header so we can parse the sections - let optional_header_offset = header.dos_header.pe_pointer as usize - + goblin::pe::header::SIZEOF_PE_MAGIC - + goblin::pe::header::SIZEOF_COFF_HEADER; - let offset = - &mut (optional_header_offset + header.coff_header.size_of_optional_header as usize); - - let sections = header.coff_header.sections(&bytes, offset)?; - - for section in sections { - if section.name.eq(mozannotation_client::ANNOTATION_SECTION) { - let address = module_address.checked_add(section.virtual_address as usize); - return address.ok_or(FindAnnotationsAddressError::InvalidAddress); - } - } - - Err(FindAnnotationsAddressError::SectionNotFound) - } - - pub fn copy_object_shallow<T>(&self, src: usize) -> Result<MaybeUninit<T>, ReadError> { - let mut object = MaybeUninit::<T>::uninit(); - let res = unsafe { - ReadProcessMemory( - self.process, - src as _, - object.as_mut_ptr() as _, - size_of::<T>() as SIZE_T, - null_mut(), - ) - }; - - if res != FALSE { - Ok(object) - } else { - Err(ReadError::ReadProcessMemoryError) - } - } - - pub fn copy_object<T>(&self, src: usize) -> Result<T, ReadError> { - let object = self.copy_object_shallow(src)?; - Ok(unsafe { object.assume_init() }) - } - - pub fn copy_array<T>(&self, src: usize, num: usize) -> Result<Vec<T>, ReadError> { - let num_of_bytes = num * size_of::<T>(); - let mut array: Vec<MaybeUninit<T>> = Vec::with_capacity(num); - let res = unsafe { - ReadProcessMemory( - self.process, - src as _, - array.as_mut_ptr() as _, - num_of_bytes as SIZE_T, - null_mut(), - ) - }; - - if res != FALSE { - unsafe { - array.set_len(num); - Ok(std::mem::transmute(array)) - } - } else { - Err(ReadError::ReadProcessMemoryError) - } - } -} diff --git a/toolkit/crashreporter/mozwer-rust/Cargo.toml b/toolkit/crashreporter/mozwer-rust/Cargo.toml index 561a10b015..89cf3d4356 100644 --- a/toolkit/crashreporter/mozwer-rust/Cargo.toml +++ b/toolkit/crashreporter/mozwer-rust/Cargo.toml @@ -7,7 +7,9 @@ license = "MPL-2.0" [dependencies] libc = "0.2.0" -mozilla-central-workspace-hack = { version = "0.1", features = ["mozwer_s"], optional = true } +mozilla-central-workspace-hack = { version = "0.1", features = [ + "mozwer_s", +], optional = true } process_reader = { path = "../process_reader/" } rust-ini = "0.10" serde = { version = "1.0", features = ["derive"] } @@ -23,6 +25,7 @@ features = [ "Win32_System_Com", "Win32_System_Diagnostics_Debug", "Win32_System_ErrorReporting", + "Win32_System_Memory", "Win32_System_ProcessStatus", "Win32_System_SystemInformation", "Win32_System_SystemServices", diff --git a/toolkit/crashreporter/mozwer-rust/lib.rs b/toolkit/crashreporter/mozwer-rust/lib.rs index 198a14a34b..61fc894462 100644 --- a/toolkit/crashreporter/mozwer-rust/lib.rs +++ b/toolkit/crashreporter/mozwer-rust/lib.rs @@ -9,7 +9,7 @@ use serde::Serialize; use serde_json::ser::to_writer; use std::convert::TryInto; use std::ffi::{c_void, OsString}; -use std::fs::{read_to_string, DirBuilder, File}; +use std::fs::{read_to_string, DirBuilder, File, OpenOptions}; use std::io::{BufRead, BufReader, Write}; use std::mem::{size_of, transmute, zeroed}; use std::os::windows::ffi::{OsStrExt, OsStringExt}; @@ -248,8 +248,9 @@ fn handle_main_process_crash( fn handle_child_process_crash(crash_report: CrashReport, child_process: HANDLE) -> Result<()> { let parent_process = get_parent_process(child_process)?; let process_reader = ProcessReader::new(parent_process).map_err(|_e| ())?; + let libxul_address = process_reader.find_module("xul.dll").map_err(|_e| ())?; let wer_notify_proc = process_reader - .find_section("xul.dll", "mozwerpt") + .find_section(libxul_address, b"mozwerpt") .map_err(|_e| ())?; let wer_notify_proc = unsafe { transmute::<_, LPTHREAD_START_ROUTINE>(wer_notify_proc) }; @@ -524,8 +525,7 @@ impl ApplicationInformation { let install_time = ApplicationInformation::get_install_time( &crash_reports_dir, &application_data.build_id, - ) - .unwrap_or("0".to_string()); + ); Ok(ApplicationInformation { install_path, @@ -596,10 +596,29 @@ impl ApplicationInformation { } } - fn get_install_time(crash_reports_path: &Path, build_id: &str) -> Result<String> { + fn get_install_time(crash_reports_path: &Path, build_id: &str) -> String { let file_name = "InstallTime".to_owned() + build_id; let file_path = crash_reports_path.join(file_name); - read_to_string(file_path).map_err(|_e| ()) + + // If the file isn't present we'll attempt to atomically create it and + // populate it. This code essentially matches the corresponding code in + // nsExceptionHandler.cpp SetupExtraData(). + if let Ok(mut file) = OpenOptions::new() + .create_new(true) + .write(true) + .open(&file_path) + { + // SAFETY: No risks in calling `time()` with a null pointer. + let _ = write!(&mut file, "{}", unsafe { time(null_mut()) }.to_string()); + } + + // As a last resort, if we can't read the file we fall back to the + // current time. This might cause us to overstate the number of users + // affected by a crash, but given it's very unlikely to hit this particular + // path it won't be a problem. + // + // SAFETY: No risks in calling `time()` with a null pointer. + read_to_string(&file_path).unwrap_or(unsafe { time(null_mut()) }.to_string()) } } diff --git a/toolkit/crashreporter/mozwer/moz.build b/toolkit/crashreporter/mozwer/moz.build index 1f6418fd48..41aed9b59b 100644 --- a/toolkit/crashreporter/mozwer/moz.build +++ b/toolkit/crashreporter/mozwer/moz.build @@ -10,6 +10,13 @@ OS_LIBS += [ "advapi32", "bcrypt", ] +# Version string comparison is generally wrong, but by the time it would +# actually matter, either bug 1489995 would be fixed, or the build would +# require version >= 1.78. +if CONFIG["RUSTC_VERSION"] and CONFIG["RUSTC_VERSION"] >= "1.78.0": + OS_LIBS += [ + "synchronization", + ] DEFFILE = "mozwer.def" USE_STATIC_LIBS = True diff --git a/toolkit/crashreporter/nsExceptionHandler.cpp b/toolkit/crashreporter/nsExceptionHandler.cpp index aca266749a..868005a5c7 100644 --- a/toolkit/crashreporter/nsExceptionHandler.cpp +++ b/toolkit/crashreporter/nsExceptionHandler.cpp @@ -7,6 +7,7 @@ #include "nsExceptionHandler.h" #include "nsExceptionHandlerUtils.h" +#include "json/json.h" #include "nsAppDirectoryServiceDefs.h" #include "nsComponentManagerUtils.h" #include "nsDirectoryServiceDefs.h" @@ -95,6 +96,9 @@ using mozilla::InjectCrashRunnable; #endif +#include <fstream> +#include <optional> + #include <stdlib.h> #include <time.h> #include <prenv.h> @@ -195,7 +199,7 @@ static const XP_CHAR dumpFileExtension[] = XP_TEXT(".dmp"); static const XP_CHAR extraFileExtension[] = XP_TEXT(".extra"); static const XP_CHAR memoryReportExtension[] = XP_TEXT(".memory.json.gz"); -static xpstring* defaultMemoryReportPath = nullptr; +static std::optional<xpstring> defaultMemoryReportPath = {}; static const char kCrashMainID[] = "crash.main.3\n"; @@ -433,26 +437,26 @@ static void CreateFileFromPath(const xpstring& path, nsIFile** file) { NS_NewLocalFile(nsDependentString(path.c_str()), false, file); } -static xpstring* CreatePathFromFile(nsIFile* file) { +static std::optional<xpstring> CreatePathFromFile(nsIFile* file) { nsAutoString path; nsresult rv = file->GetPath(path); if (NS_FAILED(rv)) { - return nullptr; + return {}; } - return new xpstring(static_cast<wchar_t*>(path.get()), path.Length()); + return xpstring(static_cast<wchar_t*>(path.get()), path.Length()); } #else static void CreateFileFromPath(const xpstring& path, nsIFile** file) { NS_NewNativeLocalFile(nsDependentCString(path.c_str()), false, file); } -MAYBE_UNUSED static xpstring* CreatePathFromFile(nsIFile* file) { +MAYBE_UNUSED static std::optional<xpstring> CreatePathFromFile(nsIFile* file) { nsAutoCString path; nsresult rv = file->GetNativePath(path); if (NS_FAILED(rv)) { - return nullptr; + return {}; } - return new xpstring(path.get(), path.Length()); + return xpstring(path.get(), path.Length()); } #endif @@ -2252,7 +2256,7 @@ static nsresult SetupCrashReporterDirectory(nsIFile* aAppDataDirectory, NS_ENSURE_SUCCESS(rv, rv); EnsureDirectoryExists(directory); - xpstring* directoryPath = CreatePathFromFile(directory); + std::optional<xpstring> directoryPath = CreatePathFromFile(directory); if (!directoryPath) { return NS_ERROR_FAILURE; @@ -2264,8 +2268,6 @@ static nsresult SetupCrashReporterDirectory(nsIFile* aAppDataDirectory, setenv(aEnvVarName, directoryPath->c_str(), /* overwrite */ 1); #endif - delete directoryPath; - if (aDirectory) { directory.forget(aDirectory); } @@ -2760,177 +2762,54 @@ nsresult AppendObjCExceptionInfoToAppNotes(void* inException) { */ static nsresult PrefSubmitReports(bool* aSubmitReports, bool writePref) { nsresult rv; -#if defined(XP_WIN) +#if defined(XP_WIN) || defined(XP_MACOSX) || defined(XP_UNIX) /* - * NOTE! This needs to stay in sync with the preference checking code - * in toolkit/crashreporter/client/crashreporter_win.cpp + * NOTE! This needs to stay in sync with the code in + * toolkit/crashreporter/client/app/src/{logic,settings}.rs */ - nsCOMPtr<nsIXULAppInfo> appinfo = - do_GetService("@mozilla.org/xre/app-info;1", &rv); - NS_ENSURE_SUCCESS(rv, rv); - - nsAutoCString appVendor, appName; - rv = appinfo->GetVendor(appVendor); - NS_ENSURE_SUCCESS(rv, rv); - rv = appinfo->GetName(appName); + nsCOMPtr<nsIFile> reporterSettings; + rv = NS_GetSpecialDirectory("UAppData", getter_AddRefs(reporterSettings)); NS_ENSURE_SUCCESS(rv, rv); + reporterSettings->AppendNative("Crash Reports"_ns); + reporterSettings->AppendNative("crashreporter_settings.json"_ns); - nsCOMPtr<nsIWindowsRegKey> regKey( - do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv)); - NS_ENSURE_SUCCESS(rv, rv); - - nsAutoCString regPath; - - regPath.AppendLiteral("Software\\"); - - // We need to ensure the registry keys are created so we can properly - // write values to it - - // Create appVendor key - if (!appVendor.IsEmpty()) { - regPath.Append(appVendor); - regKey->Create(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER, - NS_ConvertUTF8toUTF16(regPath), - nsIWindowsRegKey::ACCESS_SET_VALUE); - regPath.Append('\\'); - } - - // Create appName key - regPath.Append(appName); - regKey->Create(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER, - NS_ConvertUTF8toUTF16(regPath), - nsIWindowsRegKey::ACCESS_SET_VALUE); - regPath.Append('\\'); - - // Create Crash Reporter key - regPath.AppendLiteral("Crash Reporter"); - regKey->Create(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER, - NS_ConvertUTF8toUTF16(regPath), - nsIWindowsRegKey::ACCESS_SET_VALUE); - - // If we're saving the pref value, just write it to ROOT_KEY_CURRENT_USER - // and we're done. - if (writePref) { - rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER, - NS_ConvertUTF8toUTF16(regPath), - nsIWindowsRegKey::ACCESS_SET_VALUE); - NS_ENSURE_SUCCESS(rv, rv); - - uint32_t value = *aSubmitReports ? 1 : 0; - rv = regKey->WriteIntValue(u"SubmitCrashReport"_ns, value); - regKey->Close(); - return rv; - } - - // We're reading the pref value, so we need to first look under - // ROOT_KEY_LOCAL_MACHINE to see if it's set there, and then fall back to - // ROOT_KEY_CURRENT_USER. If it's not set in either place, the pref defaults - // to "true". - uint32_t value; - rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE, - NS_ConvertUTF8toUTF16(regPath), - nsIWindowsRegKey::ACCESS_QUERY_VALUE); - if (NS_SUCCEEDED(rv)) { - rv = regKey->ReadIntValue(u"SubmitCrashReport"_ns, &value); - regKey->Close(); - if (NS_SUCCEEDED(rv)) { - *aSubmitReports = !!value; - return NS_OK; - } - } - - rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER, - NS_ConvertUTF8toUTF16(regPath), - nsIWindowsRegKey::ACCESS_QUERY_VALUE); - if (NS_FAILED(rv)) { - *aSubmitReports = true; - return NS_OK; - } + std::optional<xpstring> file_path = CreatePathFromFile(reporterSettings); - rv = regKey->ReadIntValue(u"SubmitCrashReport"_ns, &value); - // default to true on failure - if (NS_FAILED(rv)) { - value = 1; - rv = NS_OK; + if (!file_path) { + return NS_ERROR_FAILURE; } - regKey->Close(); - *aSubmitReports = !!value; - return NS_OK; -#elif defined(XP_MACOSX) - rv = NS_OK; - if (writePref) { - CFPropertyListRef cfValue = - (CFPropertyListRef)(*aSubmitReports ? kCFBooleanTrue : kCFBooleanFalse); - ::CFPreferencesSetAppValue(CFSTR("submitReport"), cfValue, - reporterClientAppID); - if (!::CFPreferencesAppSynchronize(reporterClientAppID)) - rv = NS_ERROR_FAILURE; - } else { - *aSubmitReports = true; - Boolean keyExistsAndHasValidFormat = false; - Boolean prefValue = ::CFPreferencesGetAppBooleanValue( - CFSTR("submitReport"), reporterClientAppID, - &keyExistsAndHasValidFormat); - if (keyExistsAndHasValidFormat) *aSubmitReports = !!prefValue; - } - return rv; -#elif defined(XP_UNIX) - /* - * NOTE! This needs to stay in sync with the preference checking code - * in toolkit/crashreporter/client/crashreporter_linux.cpp - */ - nsCOMPtr<nsIFile> reporterINI; - rv = NS_GetSpecialDirectory("UAppData", getter_AddRefs(reporterINI)); - NS_ENSURE_SUCCESS(rv, rv); - reporterINI->AppendNative("Crash Reports"_ns); - reporterINI->AppendNative("crashreporter.ini"_ns); + Json::Value root; bool exists; - rv = reporterINI->Exists(&exists); + rv = reporterSettings->Exists(&exists); NS_ENSURE_SUCCESS(rv, rv); if (!exists) { if (!writePref) { - // If reading the pref, default to true if .ini doesn't exist. + // If reading the pref, default to true if the settings file doesn't + // exist. *aSubmitReports = true; return NS_OK; } - // Create the file so the INI processor can write to it. - rv = reporterINI->Create(nsIFile::NORMAL_FILE_TYPE, 0600); + // Create the file so the JSON processor can write to it. + rv = reporterSettings->Create(nsIFile::NORMAL_FILE_TYPE, 0600); NS_ENSURE_SUCCESS(rv, rv); + } else { + // Read the root value + std::ifstream file(*file_path); + file >> root; } - nsCOMPtr<nsIINIParserFactory> iniFactory = - do_GetService("@mozilla.org/xpcom/ini-parser-factory;1", &rv); - NS_ENSURE_SUCCESS(rv, rv); - - nsCOMPtr<nsIINIParser> iniParser; - rv = iniFactory->CreateINIParser(reporterINI, getter_AddRefs(iniParser)); - NS_ENSURE_SUCCESS(rv, rv); - - // If we're writing the pref, just set and we're done. if (writePref) { - nsCOMPtr<nsIINIParserWriter> iniWriter = do_QueryInterface(iniParser); - NS_ENSURE_TRUE(iniWriter, NS_ERROR_FAILURE); - - rv = iniWriter->SetString("Crash Reporter"_ns, "SubmitReport"_ns, - *aSubmitReports ? "1"_ns : "0"_ns); - NS_ENSURE_SUCCESS(rv, rv); - rv = iniWriter->WriteFile(reporterINI); - return rv; - } - - nsAutoCString submitReportValue; - rv = iniParser->GetString("Crash Reporter"_ns, "SubmitReport"_ns, - submitReportValue); - - // Default to "true" if the pref can't be found. - if (NS_FAILED(rv)) - *aSubmitReports = true; - else if (submitReportValue.EqualsASCII("0")) - *aSubmitReports = false; - else + root["submit_report"] = *aSubmitReports; + std::ofstream file(*file_path); + file << root; + } else if (root["submit_report"].isBool()) { + *aSubmitReports = root["submit_report"].asBool(); + } else { + // Default to "true" if the pref can't be found. *aSubmitReports = true; + } return NS_OK; #else @@ -2973,19 +2852,17 @@ static void SetCrashEventsDir(nsIFile* aDir) { EnsureDirectoryExists(eventsDir); } - xpstring* path = CreatePathFromFile(eventsDir); + std::optional<xpstring> path = CreatePathFromFile(eventsDir); if (!path) { return; // There's no clean failure from this } - eventsDirectory = xpstring(*path); + eventsDirectory = *path; #ifdef XP_WIN SetEnvironmentVariableW(eventsDirectoryEnv, path->c_str()); #else setenv(eventsDirectoryEnv, path->c_str(), /* overwrite */ 1); #endif - - delete path; } void SetProfileDirectory(nsIFile* aDir) { diff --git a/toolkit/crashreporter/process_reader/Cargo.toml b/toolkit/crashreporter/process_reader/Cargo.toml index f84a00a29f..5fb20cf95a 100644 --- a/toolkit/crashreporter/process_reader/Cargo.toml +++ b/toolkit/crashreporter/process_reader/Cargo.toml @@ -10,14 +10,18 @@ license = "MPL-2.0" [dependencies] goblin = { version = "0.7", features = ["elf32", "elf64", "pe32", "pe64"] } memoffset = "0.9" -mozilla-central-workspace-hack = { version = "0.1", optional = true } thiserror = "1.0" +[target."cfg(any(target_os = \"linux\", target_os = \"android\"))".dependencies] +libc = "0.2" + [target."cfg(target_os = \"windows\")".dependencies] -[dependencies.windows-sys] -version = "0.52" -features = [ +windows-sys = { version = "0.52", features = [ "Win32_Foundation", "Win32_System_Diagnostics_Debug", "Win32_System_ProcessStatus", -] +] } +mozilla-central-workspace-hack = { version = "0.1", optional = true } + +[target."cfg(target_os = \"macos\")".dependencies] +mach2 = { version = "0.4" } diff --git a/toolkit/crashreporter/process_reader/src/error.rs b/toolkit/crashreporter/process_reader/src/error.rs index af4d803a7a..2c681f78fb 100644 --- a/toolkit/crashreporter/process_reader/src/error.rs +++ b/toolkit/crashreporter/process_reader/src/error.rs @@ -8,6 +8,8 @@ use thiserror::Error; pub enum ProcessReaderError { #[error("Could not convert address {0}")] ConvertAddressError(#[from] std::num::TryFromIntError), + #[error("Could not parse address {0}")] + ParseAddressError(#[from] std::num::ParseIntError), #[cfg(target_os = "windows")] #[error("Cannot enumerate the target process's modules")] EnumProcessModulesError, @@ -17,14 +19,52 @@ pub enum ProcessReaderError { InvalidAddress, #[error("Could not read from the target process address space")] ReadFromProcessError(#[from] ReadError), - #[cfg(target_os = "windows")] + #[cfg(any(target_os = "windows", target_os = "macos"))] #[error("Section was not found")] SectionNotFound, + #[cfg(any(target_os = "linux", target_os = "android"))] + #[error("Could not attach to the target process")] + AttachError(#[from] PtraceError), + #[cfg(any(target_os = "linux", target_os = "android"))] + #[error("Note not found")] + NoteNotFound, + #[cfg(any(target_os = "linux", target_os = "android"))] + #[error("waitpid() failed when attaching to the process")] + WaitPidError, + #[cfg(any(target_os = "linux", target_os = "android"))] + #[error("Could not parse a line in /proc/<pid>/maps")] + ProcMapsParseError, + #[error("Module not found")] + ModuleNotFound, + #[cfg(any(target_os = "linux", target_os = "android"))] + #[error("IO error for file {0}")] + IOError(#[from] std::io::Error), + #[cfg(target_os = "macos")] + #[error("Failure when requesting the task information")] + TaskInfoError, + #[cfg(target_os = "macos")] + #[error("The task dyld information format is unknown or invalid")] + ImageFormatError, } #[derive(Debug, Error)] pub enum ReadError { + #[cfg(target_os = "macos")] + #[error("mach call failed")] + MachError, + #[cfg(any(target_os = "linux", target_os = "android"))] + #[error("ptrace-specific error")] + PtraceError(#[from] PtraceError), #[cfg(target_os = "windows")] #[error("ReadProcessMemory failed")] ReadProcessMemoryError, } + +#[cfg(any(target_os = "linux", target_os = "android"))] +#[derive(Debug, Error)] +pub enum PtraceError { + #[error("Could not read from the target process address space")] + ReadError(#[source] std::io::Error), + #[error("Could not trace the process")] + TraceError(#[source] std::io::Error), +} diff --git a/toolkit/crashreporter/process_reader/src/lib.rs b/toolkit/crashreporter/process_reader/src/lib.rs index 1189ff144e..09936e46f1 100644 --- a/toolkit/crashreporter/process_reader/src/lib.rs +++ b/toolkit/crashreporter/process_reader/src/lib.rs @@ -2,12 +2,78 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use std::{ffi::CString, mem::size_of}; + +use error::ReadError; + +pub mod error; +mod platform; + #[cfg(target_os = "windows")] -type ProcessHandle = windows_sys::Win32::Foundation::HANDLE; +pub type ProcessHandle = windows_sys::Win32::Foundation::HANDLE; + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub type ProcessHandle = libc::pid_t; + +#[cfg(any(target_os = "macos"))] +pub type ProcessHandle = mach2::mach_types::task_t; pub struct ProcessReader { process: ProcessHandle, } -mod error; -mod process_reader; +impl ProcessReader { + pub fn copy_null_terminated_string(&self, address: usize) -> Result<CString, ReadError> { + // Try copying the string word-by-word first, this is considerably + // faster than one byte at a time. + if let Ok(string) = self.copy_null_terminated_string_word_by_word(address) { + return Ok(string); + } + + // Reading the string one word at a time failed, let's try again one + // byte at a time. It's slow but it might work in situations where the + // string alignment causes word-by-word access to straddle page + // boundaries. + let mut length = 0; + let mut string = Vec::<u8>::new(); + + loop { + let char = self.copy_object::<u8>(address + length)?; + length += 1; + string.push(char); + + if char == 0 { + break; + } + } + + // SAFETY: If we reach this point we've read at least one byte and we + // know that the last one we read is nul. + Ok(unsafe { CString::from_vec_with_nul_unchecked(string) }) + } + + fn copy_null_terminated_string_word_by_word( + &self, + address: usize, + ) -> Result<CString, ReadError> { + const WORD_SIZE: usize = size_of::<usize>(); + let mut length = 0; + let mut string = Vec::<u8>::new(); + + loop { + let array = self.copy_array::<u8>(address + length, WORD_SIZE)?; + let null_terminator = array.iter().position(|&e| e == 0); + length += null_terminator.unwrap_or(WORD_SIZE); + string.extend(array.into_iter()); + + if null_terminator.is_some() { + string.truncate(length + 1); + break; + } + } + + // SAFETY: If we reach this point we've read at least one byte and we + // know that the last one we read is nul. + Ok(unsafe { CString::from_vec_with_nul_unchecked(string) }) + } +} diff --git a/toolkit/crashreporter/mozannotation_server/src/process_reader.rs b/toolkit/crashreporter/process_reader/src/platform.rs index b405b4b725..0ac354cd66 100644 --- a/toolkit/crashreporter/mozannotation_server/src/process_reader.rs +++ b/toolkit/crashreporter/process_reader/src/platform.rs @@ -2,12 +2,6 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use crate::ProcessHandle; - -pub struct ProcessReader { - process: ProcessHandle, -} - #[cfg(target_os = "windows")] mod windows; diff --git a/toolkit/crashreporter/mozannotation_server/src/process_reader/linux.rs b/toolkit/crashreporter/process_reader/src/platform/linux.rs index 34f8ef90d5..4a87e5480b 100644 --- a/toolkit/crashreporter/mozannotation_server/src/process_reader/linux.rs +++ b/toolkit/crashreporter/process_reader/src/platform/linux.rs @@ -2,7 +2,6 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use mozannotation_client::MozAnnotationNote; use std::{ cmp::min, fs::File, @@ -13,12 +12,10 @@ use std::{ }; use crate::{ - errors::{FindAnnotationsAddressError, PtraceError, ReadError, RetrievalError}, - ProcessHandle, + error::{ProcessReaderError, PtraceError, ReadError}, + ProcessHandle, ProcessReader, }; -use super::ProcessReader; - use goblin::elf::{ self, program_header::{PF_R, PT_NOTE}, @@ -28,11 +25,9 @@ use libc::{ c_int, c_long, c_void, pid_t, ptrace, waitpid, EINTR, PTRACE_ATTACH, PTRACE_DETACH, PTRACE_PEEKDATA, __WALL, }; -use memoffset::offset_of; -use mozannotation_client::ANNOTATION_TYPE; impl ProcessReader { - pub fn new(process: ProcessHandle) -> Result<ProcessReader, RetrievalError> { + pub fn new(process: ProcessHandle) -> Result<ProcessReader, ProcessReaderError> { let pid: pid_t = process; ptrace_attach(pid)?; @@ -46,7 +41,7 @@ impl ProcessReader { EINTR => continue, _ => { ptrace_detach(pid)?; - return Err(RetrievalError::WaitPidError); + return Err(ProcessReaderError::WaitPidError); } } } else { @@ -57,73 +52,78 @@ impl ProcessReader { Ok(ProcessReader { process: pid }) } - pub fn find_annotations(&self) -> Result<usize, FindAnnotationsAddressError> { + pub fn find_module(&self, module_name: &str) -> Result<usize, ProcessReaderError> { let maps_file = File::open(format!("/proc/{}/maps", self.process))?; BufReader::new(maps_file) .lines() .flatten() - .find_map(|line| self.find_annotations_in_module(&line).ok()) - .ok_or(FindAnnotationsAddressError::NotFound) + .map(|line| parse_proc_maps_line(&line)) + .filter_map(Result::ok) + .find_map(|(name, address)| { + if name.is_some_and(|name| name.eq(module_name)) { + Some(address) + } else { + None + } + }) + .ok_or(ProcessReaderError::ModuleNotFound) } - fn find_annotations_in_module(&self, line: &str) -> Result<usize, FindAnnotationsAddressError> { - parse_proc_maps_line(line).and_then(|module_address| { - let header_bytes = self.copy_array(module_address, size_of::<elf::Header>())?; - let elf_header = Elf::parse_header(&header_bytes)?; - - let program_header_bytes = self.copy_array( - module_address + (elf_header.e_phoff as usize), - (elf_header.e_phnum as usize) * (elf_header.e_phentsize as usize), - )?; - - let mut elf = Elf::lazy_parse(elf_header)?; - let context = goblin::container::Ctx { - container: elf.header.container()?, - le: elf.header.endianness()?, - }; - - elf.program_headers = ProgramHeader::parse( - &program_header_bytes, - 0, - elf_header.e_phnum as usize, - context, - )?; - - self.find_mozannotation_note(module_address, &elf) - .ok_or(FindAnnotationsAddressError::ProgramHeaderNotFound) - }) + pub fn find_program_note( + &self, + module_address: usize, + note_type: u32, + note_size: usize, + ) -> Result<usize, ProcessReaderError> { + let header_bytes = self.copy_array(module_address, size_of::<elf::Header>())?; + let elf_header = Elf::parse_header(&header_bytes)?; + + let program_header_bytes = self.copy_array( + module_address + (elf_header.e_phoff as usize), + (elf_header.e_phnum as usize) * (elf_header.e_phentsize as usize), + )?; + + let mut elf = Elf::lazy_parse(elf_header)?; + let context = goblin::container::Ctx { + container: elf.header.container()?, + le: elf.header.endianness()?, + }; + + elf.program_headers = ProgramHeader::parse( + &program_header_bytes, + 0, + elf_header.e_phnum as usize, + context, + )?; + + self.find_note_in_headers(&elf, module_address, note_type, note_size) } - // Looks through the program headers for the note contained in the - // mozannotation_client crate. If the note is found return the address of the - // note's desc field as well as its contents. - fn find_mozannotation_note(&self, module_address: usize, elf: &Elf) -> Option<usize> { + fn find_note_in_headers( + &self, + elf: &Elf, + address: usize, + note_type: u32, + note_size: usize, + ) -> Result<usize, ProcessReaderError> { for program_header in elf.program_headers.iter() { // We're looking for a note in the program headers, it needs to be // readable and it needs to be at least as large as the - // MozAnnotationNote structure. + // requested size. if (program_header.p_type == PT_NOTE) && ((program_header.p_flags & PF_R) != 0 - && (program_header.p_memsz as usize >= size_of::<MozAnnotationNote>())) + && (program_header.p_memsz as usize >= note_size)) { // Iterate over the notes - let notes_address = module_address + program_header.p_offset as usize; + let notes_address = address + program_header.p_offset as usize; let mut notes_offset = 0; let notes_size = program_header.p_memsz as usize; while notes_offset < notes_size { let note_address = notes_address + notes_offset; if let Ok(note) = self.copy_object::<goblin::elf::note::Nhdr32>(note_address) { - if note.n_type == ANNOTATION_TYPE { - if let Ok(note) = self.copy_object::<MozAnnotationNote>(note_address) { - let desc = note.desc; - let ehdr = (-note.ehdr) as usize; - let offset = desc + ehdr - - (offset_of!(MozAnnotationNote, ehdr) - - offset_of!(MozAnnotationNote, desc)); - - return usize::checked_add(module_address, offset); - } + if note.n_type == note_type { + return Ok(note_address); } notes_offset += size_of::<goblin::elf::note::Nhdr32>() @@ -136,7 +136,7 @@ impl ProcessReader { } } - None + Err(ProcessReaderError::NoteNotFound) } pub fn copy_object_shallow<T>(&self, src: usize) -> Result<MaybeUninit<T>, ReadError> { @@ -189,38 +189,46 @@ impl Drop for ProcessReader { } } -fn parse_proc_maps_line(line: &str) -> Result<usize, FindAnnotationsAddressError> { +fn parse_proc_maps_line(line: &str) -> Result<(Option<String>, usize), ProcessReaderError> { let mut splits = line.trim().splitn(6, ' '); let address_str = splits .next() - .ok_or(FindAnnotationsAddressError::ProcMapsParseError)?; + .ok_or(ProcessReaderError::ProcMapsParseError)?; let _perms_str = splits .next() - .ok_or(FindAnnotationsAddressError::ProcMapsParseError)?; + .ok_or(ProcessReaderError::ProcMapsParseError)?; let _offset_str = splits .next() - .ok_or(FindAnnotationsAddressError::ProcMapsParseError)?; + .ok_or(ProcessReaderError::ProcMapsParseError)?; let _dev_str = splits .next() - .ok_or(FindAnnotationsAddressError::ProcMapsParseError)?; + .ok_or(ProcessReaderError::ProcMapsParseError)?; let _inode_str = splits .next() - .ok_or(FindAnnotationsAddressError::ProcMapsParseError)?; - let _path_str = splits + .ok_or(ProcessReaderError::ProcMapsParseError)?; + let path_str = splits .next() - .ok_or(FindAnnotationsAddressError::ProcMapsParseError)?; + .ok_or(ProcessReaderError::ProcMapsParseError)?; let address = get_proc_maps_address(address_str)?; - Ok(address) + // Note that we don't care if the mapped file has been deleted because + // we're reading everything from memory. + let name = path_str + .trim_end_matches(" (deleted)") + .rsplit('/') + .next() + .map(String::from); + + Ok((name, address)) } -fn get_proc_maps_address(addresses: &str) -> Result<usize, FindAnnotationsAddressError> { +fn get_proc_maps_address(addresses: &str) -> Result<usize, ProcessReaderError> { let begin = addresses .split('-') .next() - .ok_or(FindAnnotationsAddressError::ProcMapsParseError)?; - usize::from_str_radix(begin, 16).map_err(FindAnnotationsAddressError::from) + .ok_or(ProcessReaderError::ProcMapsParseError)?; + usize::from_str_radix(begin, 16).map_err(ProcessReaderError::from) } fn uninit_as_bytes_mut<T>(elem: &mut MaybeUninit<T>) -> &mut [MaybeUninit<u8>] { diff --git a/toolkit/crashreporter/mozannotation_server/src/process_reader/macos.rs b/toolkit/crashreporter/process_reader/src/platform/macos.rs index 52a3957ca9..a0dfa2b8fd 100644 --- a/toolkit/crashreporter/mozannotation_server/src/process_reader/macos.rs +++ b/toolkit/crashreporter/process_reader/src/platform/macos.rs @@ -15,12 +15,10 @@ use mach2::{ use std::mem::{size_of, MaybeUninit}; use crate::{ - errors::{FindAnnotationsAddressError, ReadError, RetrievalError}, - ProcessHandle, + error::{ProcessReaderError, ReadError}, + ProcessHandle, ProcessReader, }; -use super::ProcessReader; - #[repr(C)] #[derive(Copy, Clone, Debug)] struct AllImagesInfo { @@ -55,23 +53,22 @@ struct ImageInfo { } const DATA_SEGMENT: &[u8; 16] = b"__DATA\0\0\0\0\0\0\0\0\0\0"; -const MOZANNOTATION_SECTION: &[u8; 16] = b"mozannotation\0\0\0"; impl ProcessReader { - pub fn new(process: ProcessHandle) -> Result<ProcessReader, RetrievalError> { + pub fn new(process: ProcessHandle) -> Result<ProcessReader, ProcessReaderError> { Ok(ProcessReader { process }) } - pub fn find_annotations(&self) -> Result<usize, FindAnnotationsAddressError> { + pub fn find_module(&self, module_name: &str) -> Result<usize, ProcessReaderError> { let dyld_info = self.task_info()?; if (dyld_info.all_image_info_format as u32) != TASK_DYLD_ALL_IMAGE_INFO_64 { - return Err(FindAnnotationsAddressError::ImageFormatError); + return Err(ProcessReaderError::ImageFormatError); } let all_image_info_size = dyld_info.all_image_info_size; let all_image_info_addr = dyld_info.all_image_info_addr; if (all_image_info_size as usize) < size_of::<AllImagesInfo>() { - return Err(FindAnnotationsAddressError::ImageFormatError); + return Err(ProcessReaderError::ImageFormatError); } let all_images_info = self.copy_object::<AllImagesInfo>(all_image_info_addr as _)?; @@ -84,80 +81,94 @@ impl ProcessReader { images .iter() - .find_map(|image| self.find_annotations_in_image(image)) - .ok_or(FindAnnotationsAddressError::NotFound) - } - - fn task_info(&self) -> Result<task_dyld_info, FindAnnotationsAddressError> { - let mut info = std::mem::MaybeUninit::<task_dyld_info>::uninit(); - let mut count = (std::mem::size_of::<task_dyld_info>() / std::mem::size_of::<u32>()) as u32; - - let res = unsafe { - task_info( - self.process, - TASK_DYLD_INFO, - info.as_mut_ptr().cast(), - &mut count, - ) - }; - - if res == KERN_SUCCESS { - // SAFETY: this will be initialized if the call succeeded - unsafe { Ok(info.assume_init()) } - } else { - Err(FindAnnotationsAddressError::TaskInfoError) - } + .find(|&image| { + let image_path = self.copy_null_terminated_string(image.file_path as usize); + + if let Ok(image_path) = image_path { + if let Some(image_name) = image_path.into_bytes().rsplit(|&b| b == b'/').next() + { + image_name.eq(module_name.as_bytes()) + } else { + false + } + } else { + false + } + }) + .map(|image| image.load_address as usize) + .ok_or(ProcessReaderError::ModuleNotFound) } - fn find_annotations_in_image(&self, image: &ImageInfo) -> Option<usize> { - self.copy_object::<Header64>(image.load_address as _) - .map_err(FindAnnotationsAddressError::from) - .and_then(|header| { - let image_address = image.load_address as usize; - let mut address = image_address + size_of::<Header64>(); - - if header.magic == MH_MAGIC_64 - && (header.filetype == MH_EXECUTE || header.filetype == MH_DYLIB) - { - let end_of_commands = address + (header.sizeofcmds as usize); - - while address < end_of_commands { - let command = self.copy_object::<LoadCommandHeader>(address)?; - - if command.cmd == LC_SEGMENT_64 { - if let Ok(offset) = self.find_annotations_in_segment(address) { - return image_address - .checked_add(offset) - .ok_or(FindAnnotationsAddressError::InvalidAddress); - } - } - - address += command.cmdsize as usize; + pub fn find_section( + &self, + module_address: usize, + section_name: &[u8; 16], + ) -> Result<usize, ProcessReaderError> { + let header = self.copy_object::<Header64>(module_address)?; + let mut address = module_address + size_of::<Header64>(); + + if header.magic == MH_MAGIC_64 + && (header.filetype == MH_EXECUTE || header.filetype == MH_DYLIB) + { + let end_of_commands = address + (header.sizeofcmds as usize); + + while address < end_of_commands { + let command = self.copy_object::<LoadCommandHeader>(address)?; + + if command.cmd == LC_SEGMENT_64 { + if let Ok(offset) = self.find_section_in_segment(address, section_name) { + return module_address + .checked_add(offset) + .ok_or(ProcessReaderError::InvalidAddress); } } - Err(FindAnnotationsAddressError::NotFound) - }) - .ok() + address += command.cmdsize as usize; + } + } + + Err(ProcessReaderError::SectionNotFound) } - fn find_annotations_in_segment( + fn find_section_in_segment( &self, segment_address: usize, - ) -> Result<usize, FindAnnotationsAddressError> { + section_name: &[u8; 16], + ) -> Result<usize, ProcessReaderError> { let segment = self.copy_object::<SegmentCommand64>(segment_address)?; if segment.segname.eq(DATA_SEGMENT) { let sections_addr = segment_address + size_of::<SegmentCommand64>(); let sections = self.copy_array::<Section64>(sections_addr, segment.nsects as usize)?; for section in §ions { - if section.sectname.eq(MOZANNOTATION_SECTION) { + if section.sectname.eq(section_name) { return Ok(section.offset as usize); } } } - Err(FindAnnotationsAddressError::InvalidAddress) + Err(ProcessReaderError::SectionNotFound) + } + + fn task_info(&self) -> Result<task_dyld_info, ProcessReaderError> { + let mut info = std::mem::MaybeUninit::<task_dyld_info>::uninit(); + let mut count = (std::mem::size_of::<task_dyld_info>() / std::mem::size_of::<u32>()) as u32; + + let res = unsafe { + task_info( + self.process, + TASK_DYLD_INFO, + info.as_mut_ptr().cast(), + &mut count, + ) + }; + + if res == KERN_SUCCESS { + // SAFETY: this will be initialized if the call succeeded + unsafe { Ok(info.assume_init()) } + } else { + Err(ProcessReaderError::TaskInfoError) + } } pub fn copy_object_shallow<T>(&self, src: usize) -> Result<MaybeUninit<T>, ReadError> { diff --git a/toolkit/crashreporter/process_reader/src/process_reader/windows.rs b/toolkit/crashreporter/process_reader/src/platform/windows.rs index 1dd1a13049..2d8c63444e 100644 --- a/toolkit/crashreporter/process_reader/src/process_reader/windows.rs +++ b/toolkit/crashreporter/process_reader/src/platform/windows.rs @@ -15,7 +15,7 @@ use windows_sys::Win32::{ System::{ Diagnostics::Debug::ReadProcessMemory, ProcessStatus::{ - GetModuleBaseNameW, K32EnumProcessModules, K32GetModuleInformation, MODULEINFO, + K32EnumProcessModules, K32GetModuleBaseNameW, K32GetModuleInformation, MODULEINFO, }, }, }; @@ -30,32 +30,54 @@ impl ProcessReader { Ok(ProcessReader { process }) } + pub fn find_module(&self, module_name: &str) -> Result<usize, ProcessReaderError> { + let modules = self.get_module_list()?; + + let module = modules.iter().find_map(|&module| { + let name = self.get_module_name(module); + // Crude way of mimicking Windows lower-case comparisons but + // sufficient for our use-cases. + if name.is_some_and(|name| name.eq_ignore_ascii_case(module_name)) { + self.get_module_info(module) + .map(|module| module.lpBaseOfDll as usize) + } else { + None + } + }); + + module.ok_or(ProcessReaderError::ModuleNotFound) + } + pub fn find_section( &self, - module_name: &str, - section_name: &str, + module_address: usize, + section_name: &[u8; 8], ) -> Result<usize, ProcessReaderError> { - let modules = self.get_module_list()?; + // We read only the first page from the module, this should be more than + // enough to read the header and section list. In the future we might do + // this incrementally but for now goblin requires an array to parse + // so we can't do it just yet. + const PAGE_SIZE: usize = 4096; + let bytes = self.copy_array(module_address as _, PAGE_SIZE)?; + let header = goblin::pe::header::Header::parse(&bytes)?; - modules - .iter() - .filter(|&&module| { - let name = self.get_module_name(module); - // Crude way of mimicking Windows lower-case comparisons but - // sufficient for our use-cases. - name.is_some_and(|name| name.eq_ignore_ascii_case(module_name)) - }) - .find_map(|&module| { - self.get_module_info(module).and_then(|info| { - self.find_section_in_module( - section_name, - info.lpBaseOfDll as usize, - info.SizeOfImage as usize, - ) - .ok() - }) - }) - .ok_or(ProcessReaderError::InvalidAddress) + // Skip the PE header so we can parse the sections + let optional_header_offset = header.dos_header.pe_pointer as usize + + goblin::pe::header::SIZEOF_PE_MAGIC + + goblin::pe::header::SIZEOF_COFF_HEADER; + let offset = + &mut (optional_header_offset + header.coff_header.size_of_optional_header as usize); + + let sections = header.coff_header.sections(&bytes, offset)?; + + for section in sections { + if section.name.eq(section_name) { + let address = module_address.checked_add(section.virtual_address as usize); + return address.ok_or(ProcessReaderError::InvalidAddress); + } + } + + Err(ProcessReaderError::SectionNotFound) } fn get_module_list(&self) -> Result<Vec<HMODULE>, ProcessReaderError> { @@ -97,8 +119,9 @@ impl ProcessReader { fn get_module_name(&self, module: HMODULE) -> Option<String> { let mut path: [u16; MAX_PATH as usize] = [0; MAX_PATH as usize]; - let res = - unsafe { GetModuleBaseNameW(self.process, module, (&mut path).as_mut_ptr(), MAX_PATH) }; + let res = unsafe { + K32GetModuleBaseNameW(self.process, module, (&mut path).as_mut_ptr(), MAX_PATH) + }; if res == 0 { None @@ -128,46 +151,6 @@ impl ProcessReader { } } - fn find_section_in_module( - &self, - section_name: &str, - module_address: usize, - size: usize, - ) -> Result<usize, ProcessReaderError> { - // We read only the first page from the module, this should be more than - // enough to read the header and section list. In the future we might do - // this incrementally but for now goblin requires an array to parse - // so we can't do it just yet. - let page_size = 4096; - if size < page_size { - // Don't try to read from the target module if it's too small - return Err(ProcessReaderError::ReadFromProcessError( - ReadError::ReadProcessMemoryError, - )); - } - - let bytes = self.copy_array(module_address as _, 4096)?; - let header = goblin::pe::header::Header::parse(&bytes)?; - - // Skip the PE header so we can parse the sections - let optional_header_offset = header.dos_header.pe_pointer as usize - + goblin::pe::header::SIZEOF_PE_MAGIC - + goblin::pe::header::SIZEOF_COFF_HEADER; - let offset = - &mut (optional_header_offset + header.coff_header.size_of_optional_header as usize); - - let sections = header.coff_header.sections(&bytes, offset)?; - - for section in sections { - if section.name.eq(section_name.as_bytes()) { - let address = module_address.checked_add(section.virtual_address as usize); - return address.ok_or(ProcessReaderError::InvalidAddress); - } - } - - Err(ProcessReaderError::SectionNotFound) - } - pub fn copy_object_shallow<T>(&self, src: usize) -> Result<MaybeUninit<T>, ReadError> { let mut object = MaybeUninit::<T>::uninit(); let res = unsafe { diff --git a/toolkit/crashreporter/rust_minidump_writer_linux/src/lib.rs b/toolkit/crashreporter/rust_minidump_writer_linux/src/lib.rs index fe5f30943e..21ae630c5e 100644 --- a/toolkit/crashreporter/rust_minidump_writer_linux/src/lib.rs +++ b/toolkit/crashreporter/rust_minidump_writer_linux/src/lib.rs @@ -3,7 +3,6 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ extern crate minidump_writer; -use anyhow; use libc::pid_t; use minidump_writer::crash_context::CrashContext; use minidump_writer::minidump_writer::MinidumpWriter; diff --git a/toolkit/crashreporter/test/nsTestCrasher.cpp b/toolkit/crashreporter/test/nsTestCrasher.cpp index 2148a60088..42b6a68b87 100644 --- a/toolkit/crashreporter/test/nsTestCrasher.cpp +++ b/toolkit/crashreporter/test/nsTestCrasher.cpp @@ -80,7 +80,7 @@ uint64_t x64CrashCFITest_EOF(uint64_t returnpfn, void*); #endif // XP_WIN && HAVE_64BIT_BUILD && !defined(__MINGW32__) } -// Keep these in sync with CrashTestUtils.jsm! +// Keep these in sync with CrashTestUtils.sys.mjs! const int16_t CRASH_INVALID_POINTER_DEREF = 0; const int16_t CRASH_PURE_VIRTUAL_CALL = 1; const int16_t CRASH_OOM = 3; diff --git a/toolkit/crashreporter/test/unit/head_win64cfi.js b/toolkit/crashreporter/test/unit/head_win64cfi.js index 4df99213db..2daa64aecc 100644 --- a/toolkit/crashreporter/test/unit/head_win64cfi.js +++ b/toolkit/crashreporter/test/unit/head_win64cfi.js @@ -184,7 +184,7 @@ function assertStack(stack, expected) { // Performs a crash, runs minidump-analyzer, and checks expected stack analysis. // -// how: The crash to perform. Constants defined in both CrashTestUtils.jsm +// how: The crash to perform. Constants defined in both CrashTestUtils.sys.mjs // and nsTestCrasher.cpp (i.e. CRASH_X64CFI_PUSH_NONVOL) // expectedStack: An array of {"symbol", "trust"} where trust is "cfi", // "context", "scan", et al. May be null if you don't need to check the stack. diff --git a/toolkit/crashreporter/test/unit/test_crash_stack_overflow.js b/toolkit/crashreporter/test/unit/test_crash_stack_overflow.js index a47c217238..7c1d3821ec 100644 --- a/toolkit/crashreporter/test/unit/test_crash_stack_overflow.js +++ b/toolkit/crashreporter/test/unit/test_crash_stack_overflow.js @@ -12,7 +12,7 @@ add_task(async function run_test() { crashType = CrashTestUtils.CRASH_STACK_OVERFLOW; crashReporter.annotateCrashReport("TestKey", "TestValue"); }, - async function (mdump, extra, extraFile) { + async function (mdump, extra) { Assert.equal(extra.TestKey, "TestValue"); }, // process will exit with a zero exit status diff --git a/toolkit/crashreporter/test/unit/test_crashreporter_appmem.js b/toolkit/crashreporter/test/unit/test_crashreporter_appmem.js index f42fe10f0c..dcf5bd3153 100644 --- a/toolkit/crashreporter/test/unit/test_crashreporter_appmem.js +++ b/toolkit/crashreporter/test/unit/test_crashreporter_appmem.js @@ -4,7 +4,7 @@ add_task(async function run_test() { let appAddr = CrashTestUtils.saveAppMemory(); crashReporter.registerAppMemory(appAddr, 32); }, - function (mdump, extra) { + function (mdump) { Assert.ok(mdump.exists()); Assert.ok(mdump.fileSize > 0); Assert.ok(CrashTestUtils.dumpCheckMemory(mdump.path)); diff --git a/toolkit/crashreporter/test/unit/test_event_files.js b/toolkit/crashreporter/test/unit/test_event_files.js index 844d7700ff..f4054089ea 100644 --- a/toolkit/crashreporter/test/unit/test_event_files.js +++ b/toolkit/crashreporter/test/unit/test_event_files.js @@ -24,7 +24,7 @@ add_task(async function test_main_process_crash() { crashType = CrashTestUtils.CRASH_MOZ_CRASH; crashReporter.annotateCrashReport("ShutdownProgress", "event-test"); }, - (minidump, extra) => { + minidump => { basename = minidump.leafName; Object.defineProperty(cm, "_eventsDirs", { value: [getEventDir()] }); cm.aggregateEventsFiles().then(resolve, reject); diff --git a/toolkit/crashreporter/test/unit/test_override_exception_handler.js b/toolkit/crashreporter/test/unit/test_override_exception_handler.js index 1b4dec8a61..caabc12de8 100644 --- a/toolkit/crashreporter/test/unit/test_override_exception_handler.js +++ b/toolkit/crashreporter/test/unit/test_override_exception_handler.js @@ -5,7 +5,7 @@ add_task(async function run_test() { function () { CrashTestUtils.TryOverrideExceptionHandler(); }, - function (mdump, extra) {}, + function () {}, true ); }); diff --git a/toolkit/crashreporter/test/unit_ipc/test_content_memory_list.js b/toolkit/crashreporter/test/unit_ipc/test_content_memory_list.js index 733d224160..b3770c7bcc 100644 --- a/toolkit/crashreporter/test/unit_ipc/test_content_memory_list.js +++ b/toolkit/crashreporter/test/unit_ipc/test_content_memory_list.js @@ -18,7 +18,7 @@ add_task(async function run_test() { is_win7_or_newer = true; } - await do_content_crash(null, function (mdump, extra) { + await do_content_crash(null, function (mdump) { Assert.ok(mdump.exists()); Assert.ok(mdump.fileSize > 0); if (is_win7_or_newer) { diff --git a/toolkit/crashreporter/tools/symbolstore.py b/toolkit/crashreporter/tools/symbolstore.py index 8bc7a7120a..b7ad7752a8 100755 --- a/toolkit/crashreporter/tools/symbolstore.py +++ b/toolkit/crashreporter/tools/symbolstore.py @@ -23,6 +23,7 @@ import ctypes import errno +import io import os import platform import re @@ -390,13 +391,13 @@ def validate_install_manifests(install_manifest_args): def make_file_mapping(install_manifests): file_mapping = {} for manifest, destination in install_manifests: - destination = os.path.abspath(destination) + absolute_destination = os.path.abspath(destination) reg = FileRegistry() manifest.populate_registry(reg) for dst, src in reg: if hasattr(src, "path"): # Any paths that get compared to source file names need to go through realpath. - abs_dest = realpath(os.path.join(destination, dst)) + abs_dest = realpath(os.path.join(absolute_destination, dst)) file_mapping[abs_dest] = realpath(src.path) return file_mapping @@ -551,7 +552,58 @@ class Dumper: Get the commandline used to invoke dump_syms. """ # The Mac dumper overrides this. - return [self.dump_syms, "--inlines", file] + cmdline = [ + self.dump_syms, + "--inlines", + ] + + cmdline.extend(self.dump_syms_extra_info()) + cmdline.append(file) + + return cmdline + + def dump_syms_extra_info(self): + """ + Returns an array with the additional parameters to add information + about the build to the dump_syms command-line + """ + cmdline = [ + "--extra-info", + "RELEASECHANNEL " + buildconfig.substs["MOZ_UPDATE_CHANNEL"], + "--extra-info", + "VERSION " + buildconfig.substs["MOZ_APP_VERSION"], + ] + + if buildconfig.substs.get("MOZ_APP_VENDOR") is not None: + cmdline.extend( + [ + "--extra-info", + "VENDOR " + buildconfig.substs["MOZ_APP_VENDOR"], + ] + ) + + if buildconfig.substs.get("MOZ_APP_BASENAME") is not None: + cmdline.extend( + [ + "--extra-info", + "PRODUCTNAME " + buildconfig.substs["MOZ_APP_BASENAME"], + ] + ) + + # Add the build ID if it's present + path = os.path.join(buildconfig.topobjdir, "buildid.h") + try: + buildid = io.open(path, "r", encoding="utf-8").read().split()[2] + cmdline.extend( + [ + "--extra-info", + "BUILDID " + buildid, + ] + ) + except Exception: + pass + + return cmdline def ProcessFileWork( self, file, arch_num, arch, vcs_root, dsymbundle=None, count_ctors=False @@ -859,9 +911,8 @@ class Dumper_Linux(Dumper): full_path = os.path.normpath(os.path.join(self.symbol_path, rel_path)) shutil.move(file_dbg, full_path) print(rel_path) - else: - if os.path.isfile(file_dbg): - os.unlink(file_dbg) + elif os.path.isfile(file_dbg): + os.unlink(file_dbg) class Dumper_Solaris(Dumper): @@ -902,11 +953,22 @@ class Dumper_Mac(Dumper): # in order to dump all the symbols. if dsymbundle: # This is the .dSYM bundle. - return ( - [self.dump_syms] - + arch.split() - + ["--inlines", "-j", "2", dsymbundle, file] + cmdline = [self.dump_syms] + + cmdline.extend(arch.split()) + cmdline.extend( + [ + "--inlines", + "-j", + "2", + ] ) + + cmdline.extend(self.dump_syms_extra_info()) + cmdline.extend([dsymbundle, file]) + + return cmdline + return Dumper.dump_syms_cmdline(self, file, arch) def GenerateDSYM(self, file): @@ -1066,13 +1128,13 @@ to canonical locations in the source repository. Specify if len(args) < 3: parser.error("not enough arguments") - exit(1) + sys.exit(1) try: manifests = validate_install_manifests(options.install_manifests) except (IOError, ValueError) as e: parser.error(str(e)) - exit(1) + sys.exit(1) file_mapping = make_file_mapping(manifests) _, bucket = get_s3_region_and_bucket() dumper = GetPlatformSpecificDumper( diff --git a/toolkit/library/moz.build b/toolkit/library/moz.build index 185d5f74d0..9fa86b386b 100644 --- a/toolkit/library/moz.build +++ b/toolkit/library/moz.build @@ -59,7 +59,7 @@ def Libxul(name, output_category=None): ] if CONFIG["ACCESSIBILITY"]: - DELAYLOAD_DLLS += ["oleacc.dll"] + DELAYLOAD_DLLS += ["oleacc.dll", "UIAutomationCore.dll"] if CONFIG["MOZ_WEBRTC"]: DELAYLOAD_DLLS += ["msdmo.dll"] @@ -100,6 +100,7 @@ def Libxul(name, output_category=None): LDFLAGS += ["-Wl,-U,_OBJC_CLASS_$_NSCustomTouchBarItem"] LDFLAGS += ["-Wl,-U,_OBJC_CLASS_$_NSPopoverTouchBarItem"] LDFLAGS += ["-lresolv"] + LDFLAGS += ["-Wl,-rpath,@executable_path/../Frameworks/ChannelPrefs.framework"] if CONFIG["MOZ_DEBUG_SYMBOLS"] and CONFIG["CC_TYPE"] == "clang-cl": LDFLAGS += ["-NATVIS:%s/toolkit/library/gecko.natvis" % TOPSRCDIR] @@ -232,6 +233,9 @@ if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": "-framework CoreSymbolication", "cups", ] + USE_LIBS += [ + "ChannelPrefs", + ] if CONFIG["MOZ_WIDGET_TOOLKIT"] == "uikit": OS_LIBS += [ @@ -377,6 +381,7 @@ if CONFIG["OS_ARCH"] == "WINNT": if CONFIG["ACCESSIBILITY"]: OS_LIBS += [ "oleacc", + "uiautomationcore", ] # Prevent winapi-rs from statically linking diff --git a/toolkit/library/rust/gkrust-features.mozbuild b/toolkit/library/rust/gkrust-features.mozbuild index 7b0724960f..bca833213c 100644 --- a/toolkit/library/rust/gkrust-features.mozbuild +++ b/toolkit/library/rust/gkrust-features.mozbuild @@ -21,7 +21,7 @@ if CONFIG["MOZ_WEBRENDER_DEBUGGER"]: if CONFIG["MOZ_PULSEAUDIO"]: gkrust_features += ["cubeb_pulse_rust"] -if CONFIG["MOZ_AUDIOUNIT_RUST"]: +if CONFIG["MOZ_AUDIOUNIT_RUST"] and CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": gkrust_features += ["cubeb_coreaudio_rust"] if CONFIG["MOZ_RUST_SIMD"]: diff --git a/toolkit/library/rust/moz.build b/toolkit/library/rust/moz.build index 0935410268..0926e21a5b 100644 --- a/toolkit/library/rust/moz.build +++ b/toolkit/library/rust/moz.build @@ -21,6 +21,7 @@ for feature in gkrust_features: # Target directory doesn't matter a lot here, since we can't share panic=abort # compilation artifacts with gkrust. RUST_TESTS = [ + "crashreporter", "firefox-on-glean", "l10nregistry", "selectors", @@ -34,6 +35,8 @@ RUST_TESTS = [ if CONFIG["TARGET_OS"] in ("WINNT", "OSX"): RUST_TESTS += ["nmhproxy"] +RUST_TEST_FEATURES.append("crashreporter/mock") + # Code coverage builds link a bunch of Gecko bindings code from the style # crate, which is not used by our tests but would cause link errors. # diff --git a/toolkit/library/rust/shared/Cargo.toml b/toolkit/library/rust/shared/Cargo.toml index 4e7ab800a2..70f783a00f 100644 --- a/toolkit/library/rust/shared/Cargo.toml +++ b/toolkit/library/rust/shared/Cargo.toml @@ -22,11 +22,11 @@ static_prefs = { path = "../../../../modules/libpref/init/static_prefs" } profiler_helper = { path = "../../../../tools/profiler/rust-helper", optional = true } mozurl = { path = "../../../../netwerk/base/mozurl" } webrender_bindings = { path = "../../../../gfx/webrender_bindings" } -cubeb-coreaudio = { git = "https://github.com/mozilla/cubeb-coreaudio-rs", rev = "d23ab55eab684b46f46e1da177c8814f6103a009", optional = true } +cubeb-coreaudio = { git = "https://github.com/mozilla/cubeb-coreaudio-rs", rev = "cc58f92f28015e4e25eba9e482007cf464c10474", optional = true } cubeb-pulse = { git = "https://github.com/mozilla/cubeb-pulse-rs", rev="8ff972c8e2ec1782ff262ac4071c0415e69b1367", optional = true, features=["pulse-dlopen"] } cubeb-sys = { version = "0.12.0", optional = true, features=["gecko-in-tree"] } -audioipc2-client = { git = "https://github.com/mozilla/audioipc", rev = "596bdb7fbb5745ea415726e16bd497e6c850a540", optional = true } -audioipc2-server = { git = "https://github.com/mozilla/audioipc", rev = "596bdb7fbb5745ea415726e16bd497e6c850a540", optional = true } +audioipc2-client = { git = "https://github.com/mozilla/audioipc", rev = "409e11f8de6288e9ddfe269654523735302e59e6", optional = true } +audioipc2-server = { git = "https://github.com/mozilla/audioipc", rev = "409e11f8de6288e9ddfe269654523735302e59e6", optional = true } encoding_glue = { path = "../../../../intl/encoding_glue" } authrs_bridge = { path = "../../../../dom/webauthn/authrs_bridge" } gkrust_utils = { path = "../../../../xpcom/rust/gkrust_utils" } @@ -44,7 +44,7 @@ bitsdownload = { path = "../../../components/bitsdownload", optional = true } storage = { path = "../../../../storage/rust" } bookmark_sync = { path = "../../../components/places/bookmark_sync", optional = true } chardetng_c = "0.1.1" -audio_thread_priority = { version = "0.31", default_features = false } +audio_thread_priority = { version = "0.32", default_features = false } mdns_service = { path="../../../../dom/media/webrtc/transport/mdns_service", optional = true } neqo_glue = { path = "../../../../netwerk/socket/neqo_glue" } wgpu_bindings = { path = "../../../../gfx/wgpu_bindings" } @@ -67,22 +67,24 @@ mozannotation_server = { path = "../../../crashreporter/mozannotation_server", gecko-profiler = { path = "../../../../tools/profiler/rust-api"} midir_impl = { path = "../../../../dom/midi/midir_impl", optional = true } dom = { path = "../../../../dom/base/rust" } +dom_fragmentdirectives = { path="../../../../dom/base/fragmentdirectives" } origin-trials-ffi = { path = "../../../../dom/origin-trials/ffi" } jog = { path = "../../../components/glean/bindings/jog" } dap_ffi = { path = "../../../components/telemetry/dap/ffi" } data-encoding-ffi = { path = "../../../../dom/fs/parent/rust/data-encoding-ffi" } -uniffi-example-arithmetic = { git = "https://github.com/mozilla/uniffi-rs.git", rev = "afb29ebdc1d9edf15021b1c5332fc9f285bbe13b", optional = true } -uniffi-example-geometry = { git = "https://github.com/mozilla/uniffi-rs.git", rev = "afb29ebdc1d9edf15021b1c5332fc9f285bbe13b", optional = true } -uniffi-example-rondpoint = { git = "https://github.com/mozilla/uniffi-rs.git", rev = "afb29ebdc1d9edf15021b1c5332fc9f285bbe13b", optional = true } -uniffi-example-sprites = { git = "https://github.com/mozilla/uniffi-rs.git", rev = "afb29ebdc1d9edf15021b1c5332fc9f285bbe13b", optional = true } -uniffi-example-todolist = { git = "https://github.com/mozilla/uniffi-rs.git", rev = "afb29ebdc1d9edf15021b1c5332fc9f285bbe13b", optional = true } +uniffi-example-arithmetic = { git = "https://github.com/mozilla/uniffi-rs.git", rev = "d52c5460ae42ecad1e73a5b394ac96d48f4769de", optional = true } +uniffi-example-geometry = { git = "https://github.com/mozilla/uniffi-rs.git", rev = "d52c5460ae42ecad1e73a5b394ac96d48f4769de", optional = true } +uniffi-example-rondpoint = { git = "https://github.com/mozilla/uniffi-rs.git", rev = "d52c5460ae42ecad1e73a5b394ac96d48f4769de", optional = true } +uniffi-example-sprites = { git = "https://github.com/mozilla/uniffi-rs.git", rev = "d52c5460ae42ecad1e73a5b394ac96d48f4769de", optional = true } +uniffi-example-todolist = { git = "https://github.com/mozilla/uniffi-rs.git", rev = "d52c5460ae42ecad1e73a5b394ac96d48f4769de", optional = true } uniffi-example-custom-types = { path = "../../../components/uniffi-example-custom-types/", optional = true } uniffi-fixture-callbacks = { path = "../../../components/uniffi-fixture-callbacks/", optional = true } uniffi-fixture-external-types = { path = "../../../components/uniffi-fixture-external-types/", optional = true } +uniffi-fixture-refcounts = { path = "../../../components/uniffi-fixture-refcounts/", optional = true } binary_http = { path = "../../../../netwerk/protocol/http/binary_http" } oblivious_http = { path = "../../../../netwerk/protocol/http/oblivious_http" } mime-guess-ffi = { path = "../../../../dom/fs/parent/rust/mime-guess-ffi" } -uniffi = { version = "0.25.2" } +uniffi = { workspace = true } # Note: `modern_sqlite` means rusqlite's bindings file be for a sqlite with # version less than or equal to what we link to. This isn't a problem because we @@ -113,6 +115,7 @@ viaduct = "0.1" webext_storage_bridge = { path = "../../../components/extensions/storage/webext_storage_bridge" } tabs = { version = "0.1" } suggest = { version = "0.1" } +relevancy = { version = "0.1" } [target.'cfg(target_os = "windows")'.dependencies] detect_win32k_conflicts = { path = "../../../xre/detect_win32k_conflicts" } @@ -143,7 +146,7 @@ thread_sanitizer = ["xpcom/thread_sanitizer"] uniffi_fixtures = [ "uniffi-example-arithmetic", "uniffi-example-geometry", "uniffi-example-rondpoint", "uniffi-example-sprites", "uniffi-example-todolist", "uniffi-example-custom-types", "uniffi-fixture-callbacks", - "uniffi-fixture-external-types", + "uniffi-fixture-external-types", "uniffi-fixture-refcounts", ] webmidi_midir_impl = ["midir_impl"] icu4x = ["jsrust_shared/icu4x"] diff --git a/toolkit/library/rust/shared/lib.rs b/toolkit/library/rust/shared/lib.rs index 0179c27a05..7108e3c17c 100644 --- a/toolkit/library/rust/shared/lib.rs +++ b/toolkit/library/rust/shared/lib.rs @@ -24,6 +24,7 @@ extern crate cubeb_coreaudio; #[cfg(feature = "cubeb_pulse_rust")] extern crate cubeb_pulse; extern crate data_storage; +extern crate dom_fragmentdirectives; extern crate encoding_glue; extern crate fog_control; extern crate gecko_profiler; @@ -57,18 +58,15 @@ extern crate webext_storage_bridge; extern crate tabs; #[cfg(not(target_os = "android"))] -mod reexport_tabs { +mod reexport_appservices_uniffi_scaffolding { tabs::uniffi_reexport_scaffolding!(); + relevancy::uniffi_reexport_scaffolding!(); + suggest::uniffi_reexport_scaffolding!(); } #[cfg(not(target_os = "android"))] extern crate suggest; -#[cfg(not(target_os = "android"))] -mod reexport_suggest { - suggest::uniffi_reexport_scaffolding!(); -} - #[cfg(feature = "webrtc")] extern crate mdns_service; extern crate neqo_glue; @@ -138,6 +136,7 @@ mod uniffi_fixtures { uniffi_fixture_callbacks::uniffi_reexport_scaffolding!(); uniffi_custom_types::uniffi_reexport_scaffolding!(); uniffi_fixture_external_types::uniffi_reexport_scaffolding!(); + uniffi_fixture_refcounts::uniffi_reexport_scaffolding!(); uniffi_geometry::uniffi_reexport_scaffolding!(); uniffi_rondpoint::uniffi_reexport_scaffolding!(); uniffi_sprites::uniffi_reexport_scaffolding!(); @@ -176,7 +175,7 @@ pub unsafe extern "C" fn debug_log(target: *const c_char, message: *const c_char // Define extern "C" versions of these UniFFI functions, so that they can be called from C++ #[no_mangle] pub extern "C" fn uniffi_rustbuffer_alloc( - size: i32, + size: u64, call_status: &mut uniffi::RustCallStatus, ) -> uniffi::RustBuffer { uniffi::uniffi_rustbuffer_alloc(size, call_status) diff --git a/toolkit/locales/en-US/chrome/global/narrate.properties b/toolkit/locales/en-US/chrome/global/narrate.properties index c622049a81..3df4f5515b 100644 --- a/toolkit/locales/en-US/chrome/global/narrate.properties +++ b/toolkit/locales/en-US/chrome/global/narrate.properties @@ -6,7 +6,7 @@ # instead of having to read it themselves." This is the name # of the feature and it is the label for the popup button. # %S is the keyboard shortcut for the listen command -listen-label = Listen (%S) +read-aloud-label = Read aloud (%S) # %S is the keyboard shortcut for the skip back command previous-label = Back (%S) # %S is the keyboard shortcut for the start command diff --git a/toolkit/locales/en-US/crashreporter/crashreporter.ftl b/toolkit/locales/en-US/crashreporter/crashreporter.ftl new file mode 100644 index 0000000000..95099d7f7b --- /dev/null +++ b/toolkit/locales/en-US/crashreporter/crashreporter.ftl @@ -0,0 +1,65 @@ +# 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/. + +crashreporter-branded-title = { -brand-short-name } Crash Reporter + +crashreporter-apology = We’re Sorry +crashreporter-crashed-and-restore = { -brand-short-name } had a problem and crashed. We’ll try to restore your tabs and windows when it restarts. +crashreporter-plea = To help us diagnose and fix the problem, you can send us a crash report. + +crashreporter-information = This application is run after a crash to report the problem to { -vendor-short-name }. It should not be run directly. + +crashreporter-error = { -brand-short-name } had a problem and crashed. Unfortunately, the crash reporter is unable to submit a report for this crash. +# $details (String) - the reason that a crash report cannot be submitted +crashreporter-error-details = Details: { $details } + +crashreporter-no-run-message = This application is run after a crash to report the problem to the application vendor. It should not be run directly. + +crashreporter-button-details = Details… +crashreporter-loading-details = Loading… + +crashreporter-view-report-title = Report Contents + +crashreporter-comment-prompt = Add a comment (comments are publicly visible) + +crashreporter-report-info = This report also contains technical information about the state of the application when it crashed. + +crashreporter-send-report = Tell { -vendor-short-name } about this crash so they can fix it. + +crashreporter-include-url = Include the address of the page I was on. + +crashreporter-submit-status = Your crash report will be submitted before you quit or restart. +crashreporter-submit-in-progress = Submitting your report… +crashreporter-submit-success = Report submitted successfully! +crashreporter-submit-failure = There was a problem submitting your report. + +crashreporter-resubmit-status = Resending reports that previously failed to send… + +crashreporter-button-quit = Quit { -brand-short-name } + +crashreporter-button-restart = Restart { -brand-short-name } + +crashreporter-button-ok = OK +crashreporter-button-close = Close + +# $id (String) - the crash id from the server, typically a UUID +crashreporter-crash-identifier = Crash ID: { $id } + +# $url (String) - the url which the user can use to view the submitted crash report +crashreporter-crash-details = You can view details of this crash at { $url }. + +# Error strings + +crashreporter-error-minidump-analyzer = Failed to run minidump-analyzer +# $path (String) - the file path +crashreporter-error-opening-file = Failed to open file ({ $path }) +# $path (String) - the file path +crashreporter-error-loading-file = Failed to load file ({ $path }) +# $path (String) - the path +crashreporter-error-creating-dir = Failed to create directory ({ $path }) +crashreporter-error-no-home-dir = Missing home directory +# $from (String) - the source path +# $to (String) - the destination path +crashreporter-error-moving-path = Failed to move { $from } to { $to } +crashreporter-error-version-eol = Version end of life: crash reports are no longer accepted. diff --git a/toolkit/locales/en-US/crashreporter/crashreporter.ini b/toolkit/locales/en-US/crashreporter/crashreporter.ini deleted file mode 100644 index ce4622c228..0000000000 --- a/toolkit/locales/en-US/crashreporter/crashreporter.ini +++ /dev/null @@ -1,58 +0,0 @@ -; This Source Code Form is subject to the terms of the Mozilla Public -; License, v. 2.0. If a copy of the MPL was not distributed with this -; file, You can obtain one at http://mozilla.org/MPL/2.0/. - -# This file is in the UTF-8 encoding -[Strings] -# LOCALIZATION NOTE (isRTL): -# Leave this entry empty unless your language requires right-to-left layout, -# for example like Arabic, Hebrew, Persian. If your language needs RTL, please -# use the untranslated English word "yes" as value -isRTL= -CrashReporterTitle=Crash Reporter -# LOCALIZATION NOTE (CrashReporterVendorTitle): %s is replaced with the vendor name. (i.e. "Mozilla") -CrashReporterVendorTitle=%s Crash Reporter -# LOCALIZATION NOTE (CrashReporterErrorText): %s is replaced with another string containing detailed information. -CrashReporterErrorText=The application had a problem and crashed.\n\nUnfortunately, the crash reporter is unable to submit a report for this crash.\n\nDetails: %s -# LOCALIZATION NOTE (CrashReporterProductErrorText2): The first %s is replaced with the product name (i.e. "Firefox"), the second is replaced with another string containing detailed information. These two substitutions can not be reordered! -CrashReporterProductErrorText2=%s had a problem and crashed.\n\nUnfortunately, the crash reporter is unable to submit a crash report.\n\nDetails: %s -CrashReporterSorry=We're Sorry -# LOCALIZATION NOTE (CrashReporterDescriptionText2): The %s is replaced with the product name. -CrashReporterDescriptionText2=%s had a problem and crashed.\n\nTo help us diagnose and fix the problem, you can send us a crash report. -CrashReporterDefault=This application is run after a crash to report the problem to the application vendor. It should not be run directly. -Details=Details… -ViewReportTitle=Report Contents -CommentGrayText=Add a comment (comments are publicly visible) -ExtraReportInfo=This report also contains technical information about the state of the application when it crashed. -# LOCALIZATION NOTE (CheckSendReport): The %s is replaced with the vendor name. -CheckSendReport=Tell %s about this crash so they can fix it -CheckIncludeURL=Include the address of the page I was on -ReportPreSubmit2=Your crash report will be submitted before you quit or restart. -ReportDuringSubmit2=Submitting your report… -ReportSubmitSuccess=Report submitted successfully! -ReportSubmitFailed=There was a problem submitting your report. -ReportResubmit=Resending reports that previously failed to send… -# LOCALIZATION NOTE (Quit2): The %s is replaced with the product name. -Quit2=Quit %s -# LOCALIZATION NOTE (Restart): The %s is replaced with the product name. -Restart=Restart %s -Ok=OK -Close=Close - -# LOCALIZATION NOTE (CrashID): The %s is replaced with the Crash ID from the server, which is a string like abc12345-6789-0abc-def1-23456abcdef1 -CrashID=Crash ID: %s -# LOCALIZATION NOTE (CrashDetailsURL): The %s is replaced with a URL that the user can visit to view the crash details. -CrashDetailsURL=You can view details of this crash at %s -ErrorBadArguments=The application passed an invalid argument. -ErrorExtraFileExists=The application didn't leave an application data file. -ErrorExtraFileRead=Couldn't read the application data file. -ErrorExtraFileMove=Couldn't move application data file. -ErrorDumpFileExists=The application did not leave a crash dump file. -ErrorDumpFileMove=Couldn't move crash dump. -ErrorNoProductName=The application did not identify itself. -ErrorNoServerURL=The application did not specify a crash reporting server. -ErrorNoSettingsPath=Couldn't find the crash reporter's settings. -ErrorCreateDumpDir=Couldn't create pending dump directory. -# LOCALIZATION NOTE (ErrorEndOfLife): The %s is replaced with the product name. -ErrorEndOfLife=The version of %s you are using is no longer supported. Crash reports are no longer being accepted for this version. Please consider upgrading to a supported version. - diff --git a/toolkit/locales/en-US/toolkit/about/aboutReader.ftl b/toolkit/locales/en-US/toolkit/about/aboutReader.ftl index 8444f09288..8e0297573c 100644 --- a/toolkit/locales/en-US/toolkit/about/aboutReader.ftl +++ b/toolkit/locales/en-US/toolkit/about/aboutReader.ftl @@ -5,14 +5,20 @@ about-reader-loading = Loading… about-reader-load-error = Failed to load article from page -about-reader-color-scheme-light = Light - .title = Color Scheme Light -about-reader-color-scheme-dark = Dark - .title = Color Scheme Dark -about-reader-color-scheme-sepia = Sepia - .title = Color Scheme Sepia -about-reader-color-scheme-auto = Auto - .title = Color Scheme Auto +about-reader-color-theme-light = Light + .title = Color Theme Light +about-reader-color-theme-dark = Dark + .title = Color Theme Dark +about-reader-color-theme-sepia = Sepia + .title = Color Theme Sepia +about-reader-color-theme-auto = Auto + .title = Color Theme Auto +about-reader-color-theme-gray = Gray + .title = Color Theme Gray +about-reader-color-theme-contrast = Contrast + .title = Color Theme Contrast +about-reader-color-theme-custom = Custom colors + .title = Color Theme Custom # An estimate for how long it takes to read an article, # expressed as a range covering both slow and fast readers. @@ -49,4 +55,31 @@ about-reader-font-type-sans-serif = Sans-serif about-reader-toolbar-close = Close Reader View about-reader-toolbar-type-controls = Type controls +about-reader-toolbar-color-controls = Colors about-reader-toolbar-savetopocket = Save To { -pocket-brand-name } + +## Reader View colors menu + +about-reader-colors-menu-header = Theme + +about-reader-fxtheme-tab = Default +about-reader-customtheme-tab = Custom + +## These are used as labels for the custom theme color pickers. +## The .title element is used to make the editing functionality +## clear and give context for screen reader users. + +about-reader-custom-colors-foreground = Text + .title = Edit color +about-reader-custom-colors-background = Background + .title = Edit color + +about-reader-custom-colors-unvisited-links = Unvisited links + .title = Edit color +about-reader-custom-colors-visited-links = Visited links + .title = Edit color + +about-reader-custom-colors-selection-highlight = Highlighter for read aloud + .title = Edit color + +about-reader-custom-colors-reset-button = Reset defaults diff --git a/toolkit/locales/en-US/toolkit/about/aboutSupport.ftl b/toolkit/locales/en-US/toolkit/about/aboutSupport.ftl index 71203540ba..f54f5e4014 100644 --- a/toolkit/locales/en-US/toolkit/about/aboutSupport.ftl +++ b/toolkit/locales/en-US/toolkit/about/aboutSupport.ftl @@ -452,3 +452,15 @@ pointing-device-mouse = Mouse pointing-device-touchscreen = Touchscreen pointing-device-pen-digitizer = Pen Digitizer pointing-device-none = No pointing devices + +## Content Analysis (DLP) + +# DLP stands for Data Loss Prevention, an industry term for external software +# that enterprises can set up to prevent sensitive data from being transferred +# to external websites. +content-analysis-title = Content Analysis (DLP) +content-analysis-active = Active +content-analysis-connected-to-agent = Connected to Agent +content-analysis-agent-path = Agent Path +content-analysis-agent-failed-signature-verification = Agent Failed Signature Verification +content-analysis-request-count = Request Count diff --git a/toolkit/locales/en-US/toolkit/contentanalysis/contentanalysis.ftl b/toolkit/locales/en-US/toolkit/contentanalysis/contentanalysis.ftl index fbd9cd2ff8..41c18a48e3 100644 --- a/toolkit/locales/en-US/toolkit/contentanalysis/contentanalysis.ftl +++ b/toolkit/locales/en-US/toolkit/contentanalysis/contentanalysis.ftl @@ -20,8 +20,12 @@ contentanalysis-slow-agent-dialog-body-clipboard = { $agent } is reviewing what # Variables: # $agent - The name of the DLP agent doing the analysis contentanalysis-slow-agent-dialog-body-dropped-text = { $agent } is reviewing the text you dropped against your organization’s data policies. This may take a moment. +# Variables: +# $agent - The name of the DLP agent doing the analysis +contentanalysis-slow-agent-dialog-body-print = { $agent } is reviewing what you printed against your organization’s data policies. This may take a moment. contentanalysis-operationtype-clipboard = clipboard contentanalysis-operationtype-dropped-text = dropped text +contentanalysis-operationtype-print = print # $filename - The filename associated with the request, such as "aFile.txt" contentanalysis-customdisplaystring-description = upload of “{ $filename }” @@ -42,8 +46,17 @@ contentanalysis-genericresponse-message = Content Analysis responded with { $res # $content - Description of the content being blocked, such as "clipboard" or "aFile.txt" contentanalysis-block-message = Your organization uses data-loss prevention software that has blocked this content: { $content }. # Variables: +# $agent - The name of the DLP agent doing the analysis +# $content - Description of the content being blocked, such as "clipboard" or "aFile.txt" +contentanalysis-unspecified-error-message = An error occurred in communicating with { $agent }. Transfer denied for resource: { $content }. +# Variables: +# $agent - The name of the DLP agent doing the analysis +# $content - Description of the content being blocked, such as "clipboard" or "aFile.txt" +contentanalysis-no-agent-connected-message = Unable to connect to { $agent }. Transfer denied for resource: { $content }. +# Variables: +# $agent - The name of the DLP agent doing the analysis # $content - Description of the content being blocked, such as "clipboard" or "aFile.txt" -contentanalysis-error-message = An error occurred in communicating with the data-loss prevention software. Transfer denied for resource: { $content }. +contentanalysis-invalid-agent-signature-message = Failed signature verification for { $agent }. Transfer denied for resource: { $content }. contentanalysis-inprogress-quit-title = Quit { -brand-shorter-name }? contentanalysis-inprogress-quit-message = Several actions are in progress. If you quit { -brand-shorter-name }, these actions will not be completed. diff --git a/toolkit/locales/en-US/toolkit/formautofill/formAutofill.ftl b/toolkit/locales/en-US/toolkit/formautofill/formAutofill.ftl index 34489e5b66..2a0e99274e 100644 --- a/toolkit/locales/en-US/toolkit/formautofill/formAutofill.ftl +++ b/toolkit/locales/en-US/toolkit/formautofill/formAutofill.ftl @@ -53,6 +53,9 @@ credit-card-capture-update-button = .label = Update existing card .accessKey = U +# Label for the button in the dropdown menu used to clear the populated form. +autofill-clear-form-label = Clear Autofill Form + # Used as a label for the button, displayed at the bottom of the dropdown suggestion, to open Form Autofill browser preferences. autofill-manage-addresses-label = Manage addresses @@ -70,3 +73,22 @@ autofill-card-network-mastercard = MasterCard autofill-card-network-mir = MIR autofill-card-network-unionpay = Union Pay autofill-card-network-visa = Visa + +# The warning text that is displayed for informing users what categories are +# about to be filled. The text would be, for example, +# Also autofills organization, phone, email. +# Variables: +# $categories - one or more of the categories, see autofill-category-X below +autofill-phishing-warningmessage-extracategory = Also autofills { $categories } + +# Variation when all are in the same category. +# Variables: +# $categories - one or more of the categories +autofill-phishing-warningmessage = Autofills { $categories } + +# Used in autofill drop down suggestion to indicate what other categories Form Autofill will attempt to fill. +autofill-category-address = address +autofill-category-name = name +autofill-category-organization = organization +autofill-category-tel = phone +autofill-category-email = email diff --git a/toolkit/locales/en-US/toolkit/global/processTypes.ftl b/toolkit/locales/en-US/toolkit/global/processTypes.ftl index 0c37076410..05bfc1e94f 100644 --- a/toolkit/locales/en-US/toolkit/global/processTypes.ftl +++ b/toolkit/locales/en-US/toolkit/global/processTypes.ftl @@ -21,6 +21,9 @@ process-type-extension = Extension # process used to open file:// URLs process-type-file = Local File +# process used to instantiate new child processes +process-type-forkserver = Fork Server + # process used to isolate a webpage from other web pages # to improve security process-type-webisolated = Isolated Web Content diff --git a/toolkit/locales/en-US/toolkit/pdfviewer/viewer.ftl b/toolkit/locales/en-US/toolkit/pdfviewer/viewer.ftl index 3804a3bd4d..8aea43959e 100644 --- a/toolkit/locales/en-US/toolkit/pdfviewer/viewer.ftl +++ b/toolkit/locales/en-US/toolkit/pdfviewer/viewer.ftl @@ -318,8 +318,10 @@ pdfjs-editor-stamp-button-label = Add or edit images pdfjs-editor-highlight-button = .title = Highlight pdfjs-editor-highlight-button-label = Highlight -pdfjs-highlight-floating-button = +pdfjs-highlight-floating-button1 = .title = Highlight + .aria-label = Highlight +pdfjs-highlight-floating-button-label = Highlight ## Remove button for the various kind of editor. diff --git a/toolkit/locales/jar.mn b/toolkit/locales/jar.mn index 5c82246137..cc22aed586 100644 --- a/toolkit/locales/jar.mn +++ b/toolkit/locales/jar.mn @@ -11,6 +11,9 @@ #ifdef MOZ_LAYOUT_DEBUGGER layoutdebug/layoutdebug.ftl (../../layout/tools/layout-debug/ui/content/layoutdebug.ftl) #endif +#if defined(NIGHTLY_BUILD) + preview/megalist.ftl (../components/satchel/megalist/content/megalist.ftl) +#endif @AB_CD@.jar: % locale global @AB_CD@ %locale/@AB_CD@/global/ diff --git a/toolkit/locales/l10n.mk b/toolkit/locales/l10n.mk index a39e030231..81938cced4 100644 --- a/toolkit/locales/l10n.mk +++ b/toolkit/locales/l10n.mk @@ -119,14 +119,6 @@ ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT)) ifneq (en,$(LPROJ_ROOT)) mv '$(STAGEDIST)'/en.lproj '$(STAGEDIST)'/$(LPROJ_ROOT).lproj endif -ifdef MOZ_CRASHREPORTER -# On Mac OS X, the crashreporter.ini file needs to be moved from under the -# application bundle's Resources directory where all other l10n files are -# located to the crash reporter bundle's Resources directory. - mv '$(STAGEDIST)'/crashreporter.app/Contents/Resources/crashreporter.ini \ - '$(STAGEDIST)'/../MacOS/crashreporter.app/Contents/Resources/crashreporter.ini - $(RM) -rf '$(STAGEDIST)'/crashreporter.app -endif endif ifeq (WINNT,$(OS_ARCH)) $(MAKE) -C ../installer/windows CONFIG_DIR=l10ngen l10ngen/helper.exe diff --git a/toolkit/locales/moz.build b/toolkit/locales/moz.build index 8f334ce496..7a9cc3b2e5 100644 --- a/toolkit/locales/moz.build +++ b/toolkit/locales/moz.build @@ -20,15 +20,6 @@ FINAL_TARGET_FILES.res += [ "!multilocale.txt", ] -if CONFIG["MOZ_CRASHREPORTER"]: - if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": - # TODO: fixing bug 1223748 should let us remove this special case - LOCALIZED_FILES["crashreporter.app"].Contents.Resources += [ - "en-US/crashreporter/crashreporter.ini" - ] - else: - LOCALIZED_FILES += ["en-US/crashreporter/crashreporter.ini"] - LOCALIZED_GENERATED_FILES += ["update.locale"] update_locale = LOCALIZED_GENERATED_FILES["update.locale"] update_locale.script = "generate_update_locale.py" diff --git a/toolkit/modules/ActorManagerParent.sys.mjs b/toolkit/modules/ActorManagerParent.sys.mjs index 5b31421ec6..46d6951475 100644 --- a/toolkit/modules/ActorManagerParent.sys.mjs +++ b/toolkit/modules/ActorManagerParent.sys.mjs @@ -127,8 +127,8 @@ let JSWINDOWACTORS = { esModuleURI: "resource://gre/actors/AutoCompleteParent.sys.mjs", // These two messages are also used, but are currently synchronous calls // through the per-process message manager. - // "FormAutoComplete:GetSelectedIndex", - // "FormAutoComplete:SelectBy" + // "AutoComplete:GetSelectedIndex", + // "AutoComplete:SelectBy" }, child: { diff --git a/toolkit/modules/AppMenuNotifications.sys.mjs b/toolkit/modules/AppMenuNotifications.sys.mjs index 77487437ac..3e148f6e21 100644 --- a/toolkit/modules/AppMenuNotifications.sys.mjs +++ b/toolkit/modules/AppMenuNotifications.sys.mjs @@ -30,7 +30,7 @@ export var AppMenuNotifications = { Services.obs.removeObserver(this, "appMenu-notifications-request"); }, - observe(subject, topic, status) { + observe(subject, topic) { switch (topic) { case "xpcom-shutdown": this.uninit(); diff --git a/toolkit/modules/AsanReporter.sys.mjs b/toolkit/modules/AsanReporter.sys.mjs index 172e24ca4b..50929fbad9 100644 --- a/toolkit/modules/AsanReporter.sys.mjs +++ b/toolkit/modules/AsanReporter.sys.mjs @@ -56,7 +56,7 @@ export const AsanReporter = { processDirectory(); }, - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { if (aTopic == "ipc:content-shutdown") { aSubject.QueryInterface(Ci.nsIPropertyBag2); if (!aSubject.get("abnormal")) { @@ -135,7 +135,7 @@ function submitToServer(data) { tool: "asan-nightly-program", }; - var xhr = new XMLHttpRequest(); + var xhr = new XMLHttpRequest({ mozAnon: !auth_token }); xhr.open("POST", api_url, true); xhr.setRequestHeader("Content-Type", "application/json"); diff --git a/toolkit/modules/AsyncPrefs.sys.mjs b/toolkit/modules/AsyncPrefs.sys.mjs index 5fa0ea6d6f..07f08c119e 100644 --- a/toolkit/modules/AsyncPrefs.sys.mjs +++ b/toolkit/modules/AsyncPrefs.sys.mjs @@ -31,6 +31,11 @@ const kAllowedPrefs = new Set([ "reader.color_scheme", "reader.content_width", "reader.line_height", + "reader.custom_colors.foreground", + "reader.custom_colors.background", + "reader.custom_colors.unvisited-links", + "reader.custom_colors.visited-links", + "reader.custom_colors.selection-highlight", "security.tls.version.enable-deprecated", "security.xfocsp.errorReporting.automatic", @@ -103,7 +108,7 @@ export var AsyncPrefs = { return AsyncPrefsParent.set(pref, value); }, - reset(pref, value) { + reset(pref) { if (kInChildProcess) { return ChromeUtils.domProcessChild.getActor("AsyncPrefs").reset(pref); } diff --git a/toolkit/modules/BrowserTelemetryUtils.sys.mjs b/toolkit/modules/BrowserTelemetryUtils.sys.mjs index c3035974e8..39b1267642 100644 --- a/toolkit/modules/BrowserTelemetryUtils.sys.mjs +++ b/toolkit/modules/BrowserTelemetryUtils.sys.mjs @@ -64,7 +64,7 @@ export var BrowserTelemetryUtils = { } else if (currentTime >= this._lastRecordSiteOrigin + this.min_interval) { this._lastRecordSiteOrigin = currentTime; - Glean.geckoview.documentSiteOrigins.accumulateSamples([originCount]); + Glean.geckoview.documentSiteOrigins.accumulateSingleSample(originCount); } }, }; diff --git a/toolkit/modules/E10SUtils.sys.mjs b/toolkit/modules/E10SUtils.sys.mjs index 34da17fc50..c91bb2a80d 100644 --- a/toolkit/modules/E10SUtils.sys.mjs +++ b/toolkit/modules/E10SUtils.sys.mjs @@ -133,6 +133,7 @@ function validatedWebRemoteType( ) { // To load into the Privileged Mozilla Content Process you must be https, // and be an exact match or a subdomain of an allowlisted domain. + // This code is duplicated in ProcessIolation.cpp, please update both. if ( lazy.separatePrivilegedMozillaWebContentProcess && aTargetUri.asciiHost && diff --git a/toolkit/modules/FindBarContent.sys.mjs b/toolkit/modules/FindBarContent.sys.mjs index ef3a197d50..cdeead2a6e 100644 --- a/toolkit/modules/FindBarContent.sys.mjs +++ b/toolkit/modules/FindBarContent.sys.mjs @@ -20,7 +20,7 @@ export class FindBarContent { this.addedEventListener = false; } - start(event) { + start() { this.inPassThrough = true; } @@ -100,7 +100,7 @@ export class FindBarContent { this.actor.sendAsyncMessage("Findbar:Keypress", fakeEvent); } - onMouseup(event) { + onMouseup() { if (this.findMode != FIND_NORMAL) { this.actor.sendAsyncMessage("Findbar:Mouseup", {}); } diff --git a/toolkit/modules/Finder.sys.mjs b/toolkit/modules/Finder.sys.mjs index 0b54b2caad..f5d0452e39 100644 --- a/toolkit/modules/Finder.sys.mjs +++ b/toolkit/modules/Finder.sys.mjs @@ -100,7 +100,7 @@ Finder.prototype = { this._listeners = this._listeners.filter(l => l != aListener); }, - _setResults(options, mode) { + _setResults(options) { if (typeof options.storeResult != "boolean") { options.storeResult = true; } diff --git a/toolkit/modules/FinderHighlighter.sys.mjs b/toolkit/modules/FinderHighlighter.sys.mjs index 2ebf718bd0..655792e498 100644 --- a/toolkit/modules/FinderHighlighter.sys.mjs +++ b/toolkit/modules/FinderHighlighter.sys.mjs @@ -146,7 +146,7 @@ function mockAnonymousContentNode(domNode) { duration ); }, - setCutoutRectsForElement(id, rects) { + setCutoutRectsForElement() { // no-op for now. }, }; @@ -217,7 +217,7 @@ FinderHighlighter.prototype = { * @param {nsIDOMWindow} window * @return {Object} */ - getForWindow(window, propName = null) { + getForWindow(window) { if (!gWindows.has(window)) { gWindows.set(window, { detectedGeometryChange: false, @@ -661,7 +661,7 @@ FinderHighlighter.prototype = { this.setScrollMarks(window, Array.from(marks), onHorizontalScrollbar); if (!this._marksListener) { - this._marksListener = event => { + this._marksListener = () => { this.updateScrollMarks(); }; @@ -1966,7 +1966,7 @@ FinderHighlighter.prototype = { // Start of nsIEditActionListener implementations - WillDeleteText(textNode, offset, length) { + WillDeleteText(textNode, offset) { let editor = this._getEditableNode(textNode).editor; let controller = editor.selectionController; let fSelection = controller.getSelection( @@ -2131,7 +2131,7 @@ FinderHighlighter.prototype = { }, // Unimplemented - notifyDocumentStateChanged(aDirty) {}, + notifyDocumentStateChanged() {}, }; }, }; diff --git a/toolkit/modules/FinderParent.sys.mjs b/toolkit/modules/FinderParent.sys.mjs index 8c8437f5e9..1f66ec95b0 100644 --- a/toolkit/modules/FinderParent.sys.mjs +++ b/toolkit/modules/FinderParent.sys.mjs @@ -143,7 +143,7 @@ FinderParent.prototype = { let actor = windowGlobal.getActor("Finder"); return actor.sendQuery(aMessageName, aArgs).then( result => result, - r => {} + () => {} ); } diff --git a/toolkit/modules/GMPInstallManager.sys.mjs b/toolkit/modules/GMPInstallManager.sys.mjs index 41f57b9a63..c187215096 100644 --- a/toolkit/modules/GMPInstallManager.sys.mjs +++ b/toolkit/modules/GMPInstallManager.sys.mjs @@ -49,13 +49,23 @@ const LOCAL_GMP_SOURCES = [ }, ]; +function getLocalSources() { + if (GMPPrefs.getBool(GMPPrefs.KEY_ALLOW_LOCAL_SOURCES, true)) { + return LOCAL_GMP_SOURCES; + } + + let log = getScopedLogger("GMPInstallManager.checkForAddons"); + log.info("ignoring local sources"); + return []; +} + function downloadJSON(uri) { let log = getScopedLogger("GMPInstallManager.checkForAddons"); log.info("fetching config from: " + uri); return new Promise((resolve, reject) => { let xmlHttp = new lazy.ServiceRequest({ mozAnon: true }); - xmlHttp.onload = function (aResponse) { + xmlHttp.onload = function () { resolve(JSON.parse(this.responseText)); }; @@ -152,6 +162,36 @@ GMPInstallManager.prototype = { }, /** + * Determines the root to use for verifying content signatures. + * @param url + * The Balrog URL, i.e. the return value of _getURL(). + */ + _getContentSignatureRootForURL(url) { + // The prod and stage URLs of Balrog are documented at: + // https://mozilla-balrog.readthedocs.io/en/latest/infrastructure.html + // Note: we are matching by prefix without the full domain nor slash, to + // enable us to move to a different host name in the future if desired. + if (url.startsWith("https://aus")) { + return Ci.nsIContentSignatureVerifier.ContentSignatureProdRoot; + } + if (url.startsWith("https://stage.")) { + return Ci.nsIContentSignatureVerifier.ContentSignatureStageRoot; + } + if (Services.env.exists("XPCSHELL_TEST_PROFILE_DIR")) { + return Ci.nsIX509CertDB.AppXPCShellRoot; + } + // When content signature verification for GMP was added (bug 1714621), a + // pref existed to configure an arbitrary root, which enabled local testing. + // This pref was removed later in bug 1769669, and replaced with hard-coded + // roots (prod and tests only). Support for testing against the stage server + // was restored in bug 1771992. + // Note: other verifiers ultimately fall back to ContentSignatureLocalRoot, + // to support local development. Here we use ContentSignatureProdRoot to + // minimize risk (and the unclear demand for "local" development). + return Ci.nsIContentSignatureVerifier.ContentSignatureProdRoot; + }, + + /** * Records telemetry results on if fetching update.xml from Balrog succeeded * when content signature was used to verify the response from Balrog. * @param didGetAddonList @@ -325,9 +365,10 @@ GMPInstallManager.prototype = { } let url = await this._getURL(); + let trustedContentSignatureRoot = this._getContentSignatureRootForURL(url); log.info( - `Fetching product addon list url=${url}, allowNonBuiltIn=${allowNonBuiltIn}, certs=${certs}, checkContentSignature=${checkContentSignature}` + `Fetching product addon list url=${url}, allowNonBuiltIn=${allowNonBuiltIn}, certs=${certs}, checkContentSignature=${checkContentSignature}, trustedContentSignatureRoot=${trustedContentSignatureRoot}` ); let success = true; @@ -337,7 +378,8 @@ GMPInstallManager.prototype = { url, allowNonBuiltIn, certs, - checkContentSignature + checkContentSignature, + trustedContentSignatureRoot ); if (checkContentSignature) { @@ -354,10 +396,12 @@ GMPInstallManager.prototype = { } } + let localSources = getLocalSources(); + try { if (!success) { log.info("Falling back to local config"); - let fallbackSources = LOCAL_GMP_SOURCES.filter(function (gmpSource) { + let fallbackSources = localSources.filter(function (gmpSource) { return gmpSource.installByDefault; }); res = await downloadLocalConfig(fallbackSources); @@ -379,7 +423,7 @@ GMPInstallManager.prototype = { // the user has requested be forced installed regardless of our update // server configuration. try { - let forcedSources = LOCAL_GMP_SOURCES.filter(function (gmpSource) { + let forcedSources = localSources.filter(function (gmpSource) { return GMPPrefs.getBool( GMPPrefs.KEY_PLUGIN_FORCE_INSTALL, false, diff --git a/toolkit/modules/GMPUtils.sys.mjs b/toolkit/modules/GMPUtils.sys.mjs index 211c30a45f..e2fee781e1 100644 --- a/toolkit/modules/GMPUtils.sys.mjs +++ b/toolkit/modules/GMPUtils.sys.mjs @@ -134,6 +134,7 @@ export var GMPPrefs = { KEY_PLUGIN_FORCE_SUPPORTED: "media.{0}.forceSupported", KEY_PLUGIN_FORCE_INSTALL: "media.{0}.forceInstall", KEY_PLUGIN_ALLOW_X64_ON_ARM64: "media.{0}.allow-x64-plugin-on-arm64", + KEY_ALLOW_LOCAL_SOURCES: "media.gmp-manager.allowLocalSources", KEY_URL: "media.gmp-manager.url", KEY_URL_OVERRIDE: "media.gmp-manager.url.override", KEY_CERT_CHECKATTRS: "media.gmp-manager.cert.checkAttributes", diff --git a/toolkit/modules/HiddenFrame.sys.mjs b/toolkit/modules/HiddenFrame.sys.mjs index 23ad57c0d3..74fe755796 100644 --- a/toolkit/modules/HiddenFrame.sys.mjs +++ b/toolkit/modules/HiddenFrame.sys.mjs @@ -89,7 +89,7 @@ HiddenFrame.prototype = { "nsISupportsWeakReference", ]), }; - this._listener.onStateChange = (wbp, request, stateFlags, status) => { + this._listener.onStateChange = (wbp, request, stateFlags) => { if (!request) { return; } diff --git a/toolkit/modules/LightweightThemeConsumer.sys.mjs b/toolkit/modules/LightweightThemeConsumer.sys.mjs index 0d3993370b..c2ad888c22 100644 --- a/toolkit/modules/LightweightThemeConsumer.sys.mjs +++ b/toolkit/modules/LightweightThemeConsumer.sys.mjs @@ -30,7 +30,7 @@ const toolkitVariableMap = [ "--lwt-accent-color", { lwtProperty: "accentcolor", - processColor(rgbaChannels, element) { + processColor(rgbaChannels) { if (!rgbaChannels || rgbaChannels.a == 0) { return "white"; } @@ -44,7 +44,7 @@ const toolkitVariableMap = [ "--lwt-text-color", { lwtProperty: "textcolor", - processColor(rgbaChannels, element) { + processColor(rgbaChannels) { if (!rgbaChannels) { rgbaChannels = { r: 0, g: 0, b: 0 }; } @@ -147,7 +147,7 @@ const toolkitVariableMap = [ "--lwt-toolbar-field-highlight", { lwtProperty: "toolbar_field_highlight", - processColor(rgbaChannels, element) { + processColor(rgbaChannels) { if (!rgbaChannels) { return null; } @@ -226,7 +226,7 @@ export function LightweightThemeConsumer(aDocument) { LightweightThemeConsumer.prototype = { _lastData: null, - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { if (aTopic != "lightweight-theme-styling-update") { return; } @@ -311,29 +311,25 @@ LightweightThemeConsumer.prototype = { if (!theme) { theme = { id: DEFAULT_THEME_ID }; } - - let active = (this._active = Object.keys(theme).length); + let hasTheme = theme.id != DEFAULT_THEME_ID || useDarkTheme; let root = this._doc.documentElement; - - if (active && theme.headerURL) { + if (hasTheme && theme.headerURL) { root.setAttribute("lwtheme-image", "true"); } else { root.removeAttribute("lwtheme-image"); } - let hasTheme = theme.id != DEFAULT_THEME_ID || useDarkTheme; - - this._setExperiment(active, themeData.experiment, theme.experimental); - _setImage(this._win, root, active, "--lwt-header-image", theme.headerURL); + this._setExperiment(hasTheme, themeData.experiment, theme.experimental); + _setImage(this._win, root, hasTheme, "--lwt-header-image", theme.headerURL); _setImage( this._win, root, - active, + hasTheme, "--lwt-additional-images", theme.additionalBackgrounds ); - _setProperties(root, active, theme, hasTheme); + _setProperties(root, hasTheme, theme); if (hasTheme) { if (updateGlobalThemeData) { @@ -352,7 +348,7 @@ LightweightThemeConsumer.prototype = { _setDarkModeAttributes(this._doc, root, theme._processedColors, hasTheme); - let contentThemeData = _getContentProperties(this._doc, active, theme); + let contentThemeData = _getContentProperties(this._doc, hasTheme, theme); Services.ppmm.sharedData.set(`theme/${this._winId}`, contentThemeData); // We flush sharedData because contentThemeData can be responsible for // painting large background surfaces. If this data isn't delivered to the @@ -363,7 +359,7 @@ LightweightThemeConsumer.prototype = { this._win.dispatchEvent(new CustomEvent("windowlwthemeupdate")); }, - _setExperiment(active, experiment, properties) { + _setExperiment(hasTheme, experiment, properties) { const root = this._doc.documentElement; if (this._lastExperimentData) { const { stylesheet, usedVariables } = this._lastExperimentData; @@ -379,7 +375,7 @@ LightweightThemeConsumer.prototype = { this._lastExperimentData = {}; - if (!active || !experiment) { + if (!hasTheme || !experiment) { return; } @@ -427,11 +423,11 @@ LightweightThemeConsumer.prototype = { }, }; -function _getContentProperties(doc, active, data) { - if (!active) { - return {}; +function _getContentProperties(doc, hasTheme, data) { + let properties = { hasTheme }; + if (!hasTheme) { + return properties; } - let properties = {}; for (let property in data) { if (lazy.ThemeContentPropertyList.includes(property)) { properties[property] = _cssColorToRGBA(doc, data[property]); @@ -472,8 +468,8 @@ function _setImage(aWin, aRoot, aActive, aVariableName, aURLs) { ); } -function _setProperty(elem, active, variableName, value) { - if (active && value) { +function _setProperty(elem, hasTheme, variableName, value) { + if (hasTheme && value) { elem.style.setProperty(variableName, value); } else { elem.style.removeProperty(variableName); @@ -664,7 +660,7 @@ function _determineIfColorPairIsDark( return !_isColorDark(color.r, color.g, color.b); } -function _setProperties(root, active, themeData, hasTheme) { +function _setProperties(root, hasTheme, themeData) { let propertyOverrides = new Map(); let doc = root.ownerDocument; @@ -706,7 +702,7 @@ function _setProperties(root, active, themeData, hasTheme) { // Add processed color to themeData. themeData._processedColors[lwtProperty] = val; - _setProperty(elem, active, cssVarName, val); + _setProperty(elem, hasTheme, cssVarName, val); } } } diff --git a/toolkit/modules/NewTabUtils.sys.mjs b/toolkit/modules/NewTabUtils.sys.mjs index 00067ada12..4fb4c7f912 100644 --- a/toolkit/modules/NewTabUtils.sys.mjs +++ b/toolkit/modules/NewTabUtils.sys.mjs @@ -602,12 +602,12 @@ var PlacesProvider = { } }, - handleError(aError) { + handleError() { // Should we somehow handle this error? aCallback([]); }, - handleCompletion(aReason) { + handleCompletion() { // The Places query breaks ties in frecency by place ID descending, but // that's different from how Links.compareLinks breaks ties, because // compareLinks doesn't have access to place IDs. It's very important @@ -2101,7 +2101,7 @@ var Links = { * Implements the nsIObserver interface to get notified about browser history * sanitization. */ - observe: function Links_observe(aSubject, aTopic, aData) { + observe: function Links_observe() { // Make sure to update open about:newtab instances. If there are no opened // pages we can just wait for the next new tab to populate the cache again. if (AllPages.length && AllPages.enabled) { @@ -2182,7 +2182,7 @@ var Telemetry = { /** * Listens for gather telemetry topic. */ - observe: function Telemetry_observe(aSubject, aTopic, aData) { + observe: function Telemetry_observe() { this._collect(); }, }; diff --git a/toolkit/modules/PopupNotifications.sys.mjs b/toolkit/modules/PopupNotifications.sys.mjs index 2f4893a2f6..bb53e0a5f6 100644 --- a/toolkit/modules/PopupNotifications.sys.mjs +++ b/toolkit/modules/PopupNotifications.sys.mjs @@ -689,7 +689,7 @@ PopupNotifications.prototype = { */ suppressWhileOpen(panel) { this._hidePanel().catch(console.error); - panel.addEventListener("popuphidden", aEvent => { + panel.addEventListener("popuphidden", () => { this._update(); }); }, @@ -1369,7 +1369,7 @@ PopupNotifications.prototype = { true ); } - this._popupshownListener = function (e) { + this._popupshownListener = function () { target.removeEventListener( "popupshown", this._popupshownListener, @@ -1939,10 +1939,14 @@ PopupNotifications.prototype = { } if (type == "buttoncommand" || type == "secondarybuttoncommand") { - if (Services.focus.activeWindow != this.window) { + // TODO: Bug 1892756. + if ( + Services.focus.activeWindow != this.window || + notificationEl.matches(":-moz-window-inactive") + ) { Services.console.logStringMessage( "PopupNotifications._onButtonEvent: " + - "Button click happened before the window was focused" + "Button click happened before the window was focused / active" ); this.window.focus(); return; diff --git a/toolkit/modules/ProcessType.sys.mjs b/toolkit/modules/ProcessType.sys.mjs index f8dcfb6fae..4e1ef25552 100644 --- a/toolkit/modules/ProcessType.sys.mjs +++ b/toolkit/modules/ProcessType.sys.mjs @@ -14,6 +14,7 @@ export const ProcessType = Object.freeze({ rdd: "process-type-rdd", socket: "process-type-socket", utility: "process-type-utility", + forkServer: "process-type-forkserver", // Utility with actor names utility_audioDecoder_Generic: diff --git a/toolkit/modules/Region.sys.mjs b/toolkit/modules/Region.sys.mjs index a07e73f378..559273461f 100644 --- a/toolkit/modules/Region.sys.mjs +++ b/toolkit/modules/Region.sys.mjs @@ -843,7 +843,7 @@ class RegionDetector { } } - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { log.info(`Observed ${aTopic}`); switch (aTopic) { case GEOLOCATION_TOPIC: diff --git a/toolkit/modules/ShortcutUtils.sys.mjs b/toolkit/modules/ShortcutUtils.sys.mjs index e45855602b..3632c52e69 100644 --- a/toolkit/modules/ShortcutUtils.sys.mjs +++ b/toolkit/modules/ShortcutUtils.sys.mjs @@ -56,9 +56,12 @@ export var ShortcutUtils = { }, getModifierString(elemMod) { + if (!elemMod) { + return ""; + } + let elemString = ""; let haveCloverLeaf = false; - if (elemMod.match("accel")) { if (Services.appinfo.OS == "Darwin") { haveCloverLeaf = true; diff --git a/toolkit/modules/SubDialog.sys.mjs b/toolkit/modules/SubDialog.sys.mjs index 6cb8d3127e..07659e0a6b 100644 --- a/toolkit/modules/SubDialog.sys.mjs +++ b/toolkit/modules/SubDialog.sys.mjs @@ -229,7 +229,7 @@ SubDialog.prototype = { bubbles: true, detail: { dialog: this, abort: true }, }); - this._frame.contentWindow.close(); + this._frame.contentWindow?.close(); // It's possible that we're aborting this dialog before we've had a // chance to set up the contentWindow.close function override in // _onContentLoaded. If so, call this.close() directly to clean things @@ -419,6 +419,11 @@ SubDialog.prototype = { ); }; + // Defining resizeDialog on the contentWindow object to resize dialogs when prompted + this._frame.contentWindow.resizeDialog = () => { + return this.resizeDialog(); + }; + // Make window.close calls work like dialog closing. let oldClose = this._frame.contentWindow.close; this._frame.contentWindow.close = () => { @@ -484,6 +489,33 @@ SubDialog.prototype = { }, async resizeDialog() { + this.resizeHorizontally(); + this.resizeVertically(); + + this._overlay.dispatchEvent( + new CustomEvent("dialogopen", { + bubbles: true, + detail: { dialog: this }, + }) + ); + this._overlay.style.visibility = "inherit"; + this._overlay.style.opacity = ""; // XXX: focus hack continued from _onContentLoaded + + if (this._box.getAttribute("resizable") == "true") { + this._onResize = this._onResize.bind(this); + this._resizeObserver = new this._window.MutationObserver(this._onResize); + this._resizeObserver.observe(this._box, { attributes: true }); + } + + this._trapFocus(); + + this._resizeCallback?.({ + title: this._titleElement, + frame: this._frame, + }); + }, + + resizeHorizontally() { // Do this on load to wait for the CSS to load and apply before calculating the size. let docEl = this._frame.contentDocument.documentElement; @@ -529,30 +561,6 @@ SubDialog.prototype = { boxMinWidth = `min(80vw, ${boxMinWidth})`; } this._box.style.minWidth = boxMinWidth; - - this.resizeVertically(); - - this._overlay.dispatchEvent( - new CustomEvent("dialogopen", { - bubbles: true, - detail: { dialog: this }, - }) - ); - this._overlay.style.visibility = "inherit"; - this._overlay.style.opacity = ""; // XXX: focus hack continued from _onContentLoaded - - if (this._box.getAttribute("resizable") == "true") { - this._onResize = this._onResize.bind(this); - this._resizeObserver = new this._window.MutationObserver(this._onResize); - this._resizeObserver.observe(this._box, { attributes: true }); - } - - this._trapFocus(); - - this._resizeCallback?.({ - title: this._titleElement, - frame: this._frame, - }); }, resizeVertically() { diff --git a/toolkit/modules/Troubleshoot.sys.mjs b/toolkit/modules/Troubleshoot.sys.mjs index 43b8c1ca51..c716f1d941 100644 --- a/toolkit/modules/Troubleshoot.sys.mjs +++ b/toolkit/modules/Troubleshoot.sys.mjs @@ -22,6 +22,7 @@ const PREFS_FOR_DISPLAY = [ "apz.", "browser.cache.", "browser.contentblocking.category", + "browser.contentanalysis.", "browser.display.", "browser.download.always_ask_before_handling_new_types", "browser.download.enable_spam_prevention", @@ -999,6 +1000,24 @@ var dataProviders = { }); }, + contentAnalysis: async function contentAnalysis(done) { + const contentAnalysis = Cc["@mozilla.org/contentanalysis;1"].getService( + Ci.nsIContentAnalysis + ); + if (!contentAnalysis.isActive) { + done({ active: false }); + return; + } + let info = await contentAnalysis.getDiagnosticInfo(); + done({ + active: true, + connected: info.connectedToAgent, + agentPath: info.agentPath, + failedSignatureVerification: info.failedSignatureVerification, + requestCount: info.requestCount, + }); + }, + async normandy(done) { if (!AppConstants.MOZ_NORMANDY) { done(); diff --git a/toolkit/modules/UpdateUtils.sys.mjs b/toolkit/modules/UpdateUtils.sys.mjs index 01149dbae3..be9ce9d540 100644 --- a/toolkit/modules/UpdateUtils.sys.mjs +++ b/toolkit/modules/UpdateUtils.sys.mjs @@ -269,7 +269,7 @@ export var UpdateUtils = { initialConfig[prefName] = initialValue; } catch (e) {} - Services.prefs.addObserver(prefName, async (subject, topic, data) => { + Services.prefs.addObserver(prefName, async () => { let config = { ...gUpdateConfigCache }; config[prefName] = await UpdateUtils.readUpdateConfigSetting( prefName diff --git a/toolkit/modules/moz.build b/toolkit/modules/moz.build index 07924cedba..ab7dcb284b 100644 --- a/toolkit/modules/moz.build +++ b/toolkit/modules/moz.build @@ -227,7 +227,6 @@ EXTRA_JS_MODULES.sessionstore += [ "sessionstore/PrivacyFilter.sys.mjs", "sessionstore/PrivacyLevel.sys.mjs", "sessionstore/SessionHistory.sys.mjs", - "sessionstore/Utils.sys.mjs", ] EXTRA_JS_MODULES.third_party.fathom += ["third_party/fathom/fathom.mjs"] diff --git a/toolkit/modules/sessionstore/Utils.sys.mjs b/toolkit/modules/sessionstore/Utils.sys.mjs deleted file mode 100644 index 627cd22686..0000000000 --- a/toolkit/modules/sessionstore/Utils.sys.mjs +++ /dev/null @@ -1,29 +0,0 @@ -/* 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/. */ - -export var Utils = Object.freeze({ - /** - * Restores frame tree |data|, starting at the given root |frame|. As the - * function recurses into descendant frames it will call cb(frame, data) for - * each frame it encounters, starting with the given root. - */ - restoreFrameTreeData(frame, data, cb) { - // Restore data for the root frame. - // The callback can abort by returning false. - if (cb(frame, data) === false) { - return; - } - - if (!data.hasOwnProperty("children")) { - return; - } - - // Recurse into child frames. - SessionStoreUtils.forEachNonDynamicChildFrame(frame, (subframe, index) => { - if (data.children[index]) { - this.restoreFrameTreeData(subframe, data.children[index], cb); - } - }); - }, -}); diff --git a/toolkit/modules/tests/browser/browser_Finder.js b/toolkit/modules/tests/browser/browser_Finder.js index 7bcf7e8a00..03724d2548 100644 --- a/toolkit/modules/tests/browser/browser_Finder.js +++ b/toolkit/modules/tests/browser/browser_Finder.js @@ -44,7 +44,7 @@ add_task(async function () { findResult = await promiseFind; is(findResult.result, Ci.nsITypeAheadFind.FIND_FOUND, "should find link"); - await SpecialPowers.spawn(tab.linkedBrowser, [], async function (arg) { + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { Assert.ok( !!content.document.getElementsByTagName("a")[0].style.outline, "outline set" @@ -61,7 +61,7 @@ add_task(async function () { "should find link again" ); - await SpecialPowers.spawn(tab.linkedBrowser, [], async function (arg) { + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { Assert.ok( !content.document.getElementsByTagName("a")[0].style.outline, "outline not set" diff --git a/toolkit/modules/tests/browser/browser_FinderHighlighter.js b/toolkit/modules/tests/browser/browser_FinderHighlighter.js index 7e377e47d1..be4f0fefef 100644 --- a/toolkit/modules/tests/browser/browser_FinderHighlighter.js +++ b/toolkit/modules/tests/browser/browser_FinderHighlighter.js @@ -33,7 +33,7 @@ add_task(async function testModalResults() { insertCalls: [5, 6], removeCalls: [4, 5], // eslint-disable-next-line object-shorthand - extraTest: function (maskNode, outlineNode, rects) { + extraTest: function (maskNode, outlineNode) { Assert.equal( outlineNode.getElementsByTagName("div").length, 2, diff --git a/toolkit/modules/tests/browser/browser_Troubleshoot.js b/toolkit/modules/tests/browser/browser_Troubleshoot.js index d627f175e4..dbfbe69909 100644 --- a/toolkit/modules/tests/browser/browser_Troubleshoot.js +++ b/toolkit/modules/tests/browser/browser_Troubleshoot.js @@ -1288,6 +1288,27 @@ const SNAPSHOT_SCHEMA = { }, }, }, + contentAnalysis: { + type: "object", + properties: { + active: { + required: true, + type: "boolean", + }, + connected: { + type: "boolean", + }, + agentPath: { + type: "string", + }, + failedSignatureVerification: { + type: "boolean", + }, + requestCount: { + type: "number", + }, + }, + }, }, }; @@ -1346,9 +1367,9 @@ function validateObject_array(array, schema) { array.forEach(elt => validateObject(elt, schema.items)); } -function validateObject_string(str, schema) {} -function validateObject_boolean(bool, schema) {} -function validateObject_number(num, schema) {} +function validateObject_string() {} +function validateObject_boolean() {} +function validateObject_number() {} function validationErr(msg, obj, schema) { return new Error( diff --git a/toolkit/modules/tests/browser/browser_web_channel.js b/toolkit/modules/tests/browser/browser_web_channel.js index 9dfa59485b..289e4d1d14 100644 --- a/toolkit/modules/tests/browser/browser_web_channel.js +++ b/toolkit/modules/tests/browser/browser_web_channel.js @@ -23,10 +23,10 @@ var gTests = [ { desc: "WebChannel generic message", run() { - return new Promise(function (resolve, reject) { + return new Promise(function (resolve) { let tab; let channel = new WebChannel("generic", Services.io.newURI(HTTP_PATH)); - channel.listen(function (id, message, target) { + channel.listen(function (id, message) { is(id, "generic"); is(message.something.nested, "hello"); channel.stopListening(); @@ -44,9 +44,9 @@ var gTests = [ { desc: "WebChannel generic message in a private window.", async run() { - let promiseTestDone = new Promise(function (resolve, reject) { + let promiseTestDone = new Promise(function (resolve) { let channel = new WebChannel("generic", Services.io.newURI(HTTP_PATH)); - channel.listen(function (id, message, target) { + channel.listen(function (id, message) { is(id, "generic"); is(message.something.nested, "hello"); channel.stopListening(); @@ -66,7 +66,7 @@ var gTests = [ { desc: "WebChannel two way communication", run() { - return new Promise(function (resolve, reject) { + return new Promise(function (resolve) { let tab; let channel = new WebChannel("twoway", Services.io.newURI(HTTP_PATH)); @@ -102,7 +102,7 @@ var gTests = [ Services.io.newURI(HTTP_IFRAME_PATH) ); let promiseTestDone = new Promise(function (resolve, reject) { - parentChannel.listen(function (id, message, sender) { + parentChannel.listen(function () { reject(new Error("WebChannel message incorrectly sent to parent")); }); @@ -218,14 +218,14 @@ var gTests = [ { desc: "WebChannel multichannel", run() { - return new Promise(function (resolve, reject) { + return new Promise(function (resolve) { let tab; let channel = new WebChannel( "multichannel", Services.io.newURI(HTTP_PATH) ); - channel.listen(function (id, message, sender) { + channel.listen(function (id) { is(id, "multichannel"); gBrowser.removeTab(tab); resolve(); @@ -246,8 +246,8 @@ var gTests = [ // an unsolicted message is sent from Chrome->Content which is then // echoed back. If the echo is received here, then the content // received the message. - let messagePromise = new Promise(function (resolve, reject) { - channel.listen(function (id, message, sender) { + let messagePromise = new Promise(function (resolve) { + channel.listen(function (id, message) { is(id, "echo"); is(message.command, "unsolicited"); @@ -283,8 +283,8 @@ var gTests = [ // an unsolicted message is sent from Chrome->Content which is then // echoed back. If the echo is received here, then the content // received the message. - let messagePromise = new Promise(function (resolve, reject) { - channel.listen(function (id, message, sender) { + let messagePromise = new Promise(function (resolve) { + channel.listen(function (id, message) { is(id, "echo"); is(message.command, "unsolicited"); @@ -326,7 +326,7 @@ var gTests = [ // and should not be echoed back. The second, `done`, is sent to the // correct principal and should be echoed back. let messagePromise = new Promise(function (resolve, reject) { - channel.listen(function (id, message, sender) { + channel.listen(function (id, message) { is(id, "echo"); if (message.command === "done") { @@ -435,8 +435,8 @@ var gTests = [ * message. */ let channel = new WebChannel("objects", Services.io.newURI(HTTP_PATH)); - let testDonePromise = new Promise((resolve, reject) => { - channel.listen((id, message, sender) => { + let testDonePromise = new Promise(resolve => { + channel.listen((id, message) => { is(id, "objects"); is(message.type, "string"); resolve(); @@ -466,7 +466,7 @@ var gTests = [ let testDonePromise = new Promise((resolve, reject) => { let sawObject = false; let sawString = false; - channel.listen((id, message, sender) => { + channel.listen((id, message) => { is(id, "objects"); if (message.type === "object") { ok(!sawObject); @@ -509,9 +509,9 @@ var gTests = [ // The channel where we see the response when the content sees the error let echoChannel = new WebChannel("echo", Services.io.newURI(HTTP_PATH)); - let testDonePromise = new Promise((resolve, reject) => { + let testDonePromise = new Promise(resolve => { // listen for the confirmation that content saw the error. - echoChannel.listen((id, message, sender) => { + echoChannel.listen((id, message) => { is(id, "echo"); is(message.error, "oh no"); is(message.errno, ERRNO_UNKNOWN_ERROR); @@ -519,7 +519,7 @@ var gTests = [ }); // listen for a message telling us to simulate an error. - channel.listen((id, message, sender) => { + channel.listen((id, message) => { is(id, "error"); is(message.command, "oops"); throw new Error("oh no"); @@ -545,9 +545,9 @@ var gTests = [ // The channel where we see the response when the content sees the error let echoChannel = new WebChannel("echo", Services.io.newURI(HTTP_PATH)); - let testDonePromise = new Promise((resolve, reject) => { + let testDonePromise = new Promise(resolve => { // listen for the confirmation that content saw the error. - echoChannel.listen((id, message, sender) => { + echoChannel.listen((id, message) => { is(id, "echo"); is(message.error, "No Such Channel"); is(message.errno, ERRNO_NO_SUCH_CHANNEL); diff --git a/toolkit/modules/tests/browser/head.js b/toolkit/modules/tests/browser/head.js index 7c3f75b106..84bfad9328 100644 --- a/toolkit/modules/tests/browser/head.js +++ b/toolkit/modules/tests/browser/head.js @@ -87,7 +87,7 @@ function promiseTestHighlighterOutput( browser, [{ word, expectedResult, extraTest: extraTest.toSource() }], async function ({ word, expectedResult, extraTest }) { - return new Promise((resolve, reject) => { + return new Promise(resolve => { let stubbed = {}; let callCounts = { insertCalls: [], diff --git a/toolkit/modules/tests/modules/MockDocument.sys.mjs b/toolkit/modules/tests/modules/MockDocument.sys.mjs index 163beb8450..d4c2b07205 100644 --- a/toolkit/modules/tests/modules/MockDocument.sys.mjs +++ b/toolkit/modules/tests/modules/MockDocument.sys.mjs @@ -40,7 +40,7 @@ export const MockDocument = { // Mock the document.location object so we can unit test without a frame. We use a proxy // instead of just assigning to the property since it's not configurable or writable. let document = new Proxy(aDoc, { - get(target, property, receiver) { + get(target, property) { // document.location is normally null when a document is outside of a "browsing context". // See https://html.spec.whatwg.org/#the-location-interface if (property == "location") { diff --git a/toolkit/modules/tests/modules/PromiseTestUtils.sys.mjs b/toolkit/modules/tests/modules/PromiseTestUtils.sys.mjs index 2286ac03da..ffdb8c37d6 100644 --- a/toolkit/modules/tests/modules/PromiseTestUtils.sys.mjs +++ b/toolkit/modules/tests/modules/PromiseTestUtils.sys.mjs @@ -11,8 +11,8 @@ import { Assert } from "resource://testing-common/Assert.sys.mjs"; export var PromiseTestUtils = { /** * Array of objects containing the details of the Promise rejections that are - * currently left uncaught. This includes DOM Promise and Promise.jsm. When - * rejections in DOM Promises are consumed, they are removed from this list. + * currently left uncaught. When rejections in DOM Promises are consumed, they + * are removed from this list. * * The objects contain at least the following properties: * { diff --git a/toolkit/modules/tests/xpcshell/test_DeferredTask.js b/toolkit/modules/tests/xpcshell/test_DeferredTask.js index e6c58e03db..cb1cc2d600 100644 --- a/toolkit/modules/tests/xpcshell/test_DeferredTask.js +++ b/toolkit/modules/tests/xpcshell/test_DeferredTask.js @@ -389,7 +389,7 @@ add_test(function test_finalize() { let finalized = false; // Let idleDispatch take longer. - replaceIdleDispatch((callback, timeout) => { + replaceIdleDispatch(callback => { Assert.ok(!idleStarted); idleStarted = true; do_timeout(T, callback); diff --git a/toolkit/modules/tests/xpcshell/test_FinderIterator.js b/toolkit/modules/tests/xpcshell/test_FinderIterator.js index fcbe97ba5f..5125740c55 100644 --- a/toolkit/modules/tests/xpcshell/test_FinderIterator.js +++ b/toolkit/modules/tests/xpcshell/test_FinderIterator.js @@ -7,7 +7,7 @@ let finderIterator = new FinderIterator(); var gFindResults = []; // Stub the method that instantiates nsIFind and does all the interaction with // the docShell to be searched through. -finderIterator._iterateDocument = function* (word, window, finder) { +finderIterator._iterateDocument = function* () { for (let range of gFindResults) { yield range; } @@ -117,7 +117,7 @@ add_task(async function test_valid_arguments() { entireWord: false, finder: gMockFinder, listener: { - onIteratorRangeFound(range) { + onIteratorRangeFound() { ++count; }, }, @@ -136,7 +136,7 @@ add_task(async function test_valid_arguments() { finderIterator.start({ entireWord: false, listener: { - onIteratorRangeFound(range) { + onIteratorRangeFound() { ++count; }, }, @@ -154,7 +154,7 @@ add_task(async function test_valid_arguments() { caseSensitive: false, entireWord: false, listener: { - onIteratorRangeFound(range) { + onIteratorRangeFound() { ++count; }, }, @@ -170,7 +170,7 @@ add_task(async function test_valid_arguments() { finderIterator.start({ caseSensitive: false, listener: { - onIteratorRangeFound(range) { + onIteratorRangeFound() { ++count; }, }, @@ -188,7 +188,7 @@ add_task(async function test_valid_arguments() { caseSensitive: false, entireWord: false, listener: { - onIteratorRangeFound(range) { + onIteratorRangeFound() { ++count; }, }, @@ -221,7 +221,7 @@ add_task(async function test_valid_arguments() { entireWord: true, finder: gMockFinder, listener: { - onIteratorRangeFound(range) { + onIteratorRangeFound() { ++count; }, }, @@ -246,7 +246,7 @@ add_task(async function test_stop() { entireWord: false, finder: gMockFinder, listener: { - onIteratorRangeFound(range) { + onIteratorRangeFound() { ++count; }, }, @@ -274,7 +274,7 @@ add_task(async function test_reset() { entireWord: false, finder: gMockFinder, listener: { - onIteratorRangeFound(range) { + onIteratorRangeFound() { ++count; }, }, @@ -317,7 +317,7 @@ add_task(async function test_parallel_starts() { entireWord: false, finder: gMockFinder, listener: { - onIteratorRangeFound(range) { + onIteratorRangeFound() { ++count; }, }, @@ -334,7 +334,7 @@ add_task(async function test_parallel_starts() { entireWord: false, finder: gMockFinder, listener: { - onIteratorRangeFound(range) { + onIteratorRangeFound() { ++count2; }, }, @@ -385,7 +385,7 @@ add_task(async function test_allowDistance() { entireWord: false, finder: gMockFinder, listener: { - onIteratorRangeFound(range) { + onIteratorRangeFound() { ++count; }, }, @@ -399,7 +399,7 @@ add_task(async function test_allowDistance() { entireWord: false, finder: gMockFinder, listener: { - onIteratorRangeFound(range) { + onIteratorRangeFound() { ++count2; }, }, @@ -414,7 +414,7 @@ add_task(async function test_allowDistance() { entireWord: false, finder: gMockFinder, listener: { - onIteratorRangeFound(range) { + onIteratorRangeFound() { ++count3; }, }, diff --git a/toolkit/modules/tests/xpcshell/test_GMPInstallManager.js b/toolkit/modules/tests/xpcshell/test_GMPInstallManager.js index a0d12b8a6a..be1be93968 100644 --- a/toolkit/modules/tests/xpcshell/test_GMPInstallManager.js +++ b/toolkit/modules/tests/xpcshell/test_GMPInstallManager.js @@ -837,6 +837,60 @@ add_task(async function test_checkForAddons_contentSignatureFailure() { }); /** + * Tests that the signature verification URL is as expected. + */ +add_task(async function test_checkForAddons_get_verifier_url() { + const previousUrlOverride = setupContentSigTestPrefs(); + + let installManager = new GMPInstallManager(); + // checkForAddons() calls _getContentSignatureRootForURL() with the return + // value of _getURL(), which is effectively KEY_URL_OVERRIDE or KEY_URL + // followed by some normalization. + const rootForUrl = async () => { + const url = await installManager._getURL(); + return installManager._getContentSignatureRootForURL(url); + }; + + Assert.equal( + await rootForUrl(), + Ci.nsIX509CertDB.AppXPCShellRoot, + "XPCShell root used by default in xpcshell test" + ); + + const defaultPrefs = Services.prefs.getDefaultBranch(""); + const defaultUrl = defaultPrefs.getStringPref(GMPPrefs.KEY_URL); + Preferences.set(GMPPrefs.KEY_URL_OVERRIDE, defaultUrl); + Assert.equal( + await rootForUrl(), + Ci.nsIContentSignatureVerifier.ContentSignatureProdRoot, + "Production cert should be used for the default Balrog URL: " + defaultUrl + ); + + // The current Balrog endpoint is at aus5.mozilla.org. Confirm that the prod + // cert is used even if we bump the version (e.g. aus6): + const potentialProdUrl = "https://aus1337.mozilla.org/potential/prod/URL"; + Preferences.set(GMPPrefs.KEY_URL_OVERRIDE, potentialProdUrl); + Assert.equal( + await rootForUrl(), + Ci.nsIContentSignatureVerifier.ContentSignatureProdRoot, + "Production cert should be used for: " + potentialProdUrl + ); + + // Stage URL documented at https://mozilla-balrog.readthedocs.io/en/latest/infrastructure.html + const stageUrl = "https://stage.balrog.nonprod.cloudops.mozgcp.net/etc."; + Preferences.set(GMPPrefs.KEY_URL_OVERRIDE, stageUrl); + Assert.equal( + await rootForUrl(), + Ci.nsIContentSignatureVerifier.ContentSignatureStageRoot, + "Stage cert should be used with the stage URL: " + stageUrl + ); + + installManager.uninit(); + + revertContentSigTestPrefs(previousUrlOverride); +}); + +/** * Tests that checkForAddons() works as expected when certificate pinning * checking is enabled. We plan to move away from cert pinning in favor of * content signature checks, but part of doing this is comparing the telemetry @@ -1175,7 +1229,7 @@ add_test(function test_installAddon_noServer() { GMPInstallManager.overrideLeaveDownloadedZip = true; let installPromise = installManager.installAddon(gmpAddon); installPromise.then( - extractedPaths => { + () => { do_throw("No server for install should reject"); }, err => { @@ -1324,8 +1378,8 @@ function mockRequest(inputStatus, inputResponse, options) { this._options = options || {}; } mockRequest.prototype = { - overrideMimeType(aMimetype) {}, - setRequestHeader(aHeader, aValue) {}, + overrideMimeType() {}, + setRequestHeader() {}, status: null, channel: { set notificationCallbacks(aVal) {} }, open(aMethod, aUrl) { @@ -1339,7 +1393,7 @@ mockRequest.prototype = { }, responseXML: null, responseText: null, - send(aBody) { + send() { executeSoon(() => { try { if (this._options.dropRequest) { @@ -1427,9 +1481,8 @@ mockRequest.prototype = { } } }, - addEventListener(aEvent, aValue, aCapturing) { - // eslint-disable-next-line no-eval - eval("this._on" + aEvent + " = aValue"); + addEventListener(aEvent, aValue) { + this[`_on${aEvent}`] = aValue; }, get wrappedJSObject() { return this; diff --git a/toolkit/modules/tests/xpcshell/test_Integration.js b/toolkit/modules/tests/xpcshell/test_Integration.js index 8213e32592..d0f53cc22a 100644 --- a/toolkit/modules/tests/xpcshell/test_Integration.js +++ b/toolkit/modules/tests/xpcshell/test_Integration.js @@ -170,7 +170,7 @@ add_task(async function test_override_super_multiple() { * ensures that this does not block other functions from being registered. */ add_task(async function test_override_error() { - let errorOverrideFn = base => { + let errorOverrideFn = () => { throw new Error("Expected error."); }; diff --git a/toolkit/modules/tests/xpcshell/test_Log.js b/toolkit/modules/tests/xpcshell/test_Log.js index c9fc367dc3..5989dbb78f 100644 --- a/toolkit/modules/tests/xpcshell/test_Log.js +++ b/toolkit/modules/tests/xpcshell/test_Log.js @@ -362,7 +362,6 @@ add_task(async function log_template_literal_message() { /* * Check that we format JS Errors reasonably. - * This needs to stay a generator to exercise Task.jsm's stack rewriting. */ add_task(async function format_errors() { let pFormat = new Log.ParameterFormatter(); @@ -386,8 +385,6 @@ add_task(async function format_errors() { // lineNumber:columnNumber. Assert.ok(str.includes(":1:12)")); // Make sure that we use human-readable stack traces - // Check that the error doesn't contain any reference to "Task.jsm" - Assert.ok(!str.includes("Task.jsm")); Assert.ok(str.includes("format_errors")); } }); diff --git a/toolkit/modules/tests/xpcshell/test_PermissionsUtils.js b/toolkit/modules/tests/xpcshell/test_PermissionsUtils.js index a712044359..80154a0c67 100644 --- a/toolkit/modules/tests/xpcshell/test_PermissionsUtils.js +++ b/toolkit/modules/tests/xpcshell/test_PermissionsUtils.js @@ -2,7 +2,7 @@ * http://creativecommons.org/publicdomain/zero/1.0/ */ -// Tests that PerrmissionsUtils.jsm works as expected, including: +// Tests that PerrmissionsUtils.sys.mjs works as expected, including: // * PermissionsUtils.importfromPrefs() // <ROOT>.[whitelist|blacklist].add preferences are emptied when // converted into permissions on startup. diff --git a/toolkit/modules/tests/xpcshell/test_UpdateUtils_updatechannel.js b/toolkit/modules/tests/xpcshell/test_UpdateUtils_updatechannel.js index 81db867ba4..2dc4654492 100644 --- a/toolkit/modules/tests/xpcshell/test_UpdateUtils_updatechannel.js +++ b/toolkit/modules/tests/xpcshell/test_UpdateUtils_updatechannel.js @@ -25,6 +25,7 @@ add_task(async function test_updatechannel() { Assert.equal(UpdateUtils.getUpdateChannel(true), currentChannel); Assert.equal(UpdateUtils.getUpdateChannel(false), currentChannel); + defaultPrefs.unlock(PREF_APP_UPDATE_CHANNEL); defaultPrefs.set(PREF_APP_UPDATE_CHANNEL, TEST_CHANNEL); Assert.equal(UpdateUtils.UpdateChannel, TEST_CHANNEL); Assert.equal(UpdateUtils.getUpdateChannel(true), TEST_CHANNEL); diff --git a/toolkit/modules/tests/xpcshell/test_UpdateUtils_url.js b/toolkit/modules/tests/xpcshell/test_UpdateUtils_url.js index 00176a5f3a..b8618291f8 100644 --- a/toolkit/modules/tests/xpcshell/test_UpdateUtils_url.js +++ b/toolkit/modules/tests/xpcshell/test_UpdateUtils_url.js @@ -34,6 +34,7 @@ const gAppInfo = getAppInfo(); const gDefaultPrefBranch = Services.prefs.getDefaultBranch(null); function setUpdateChannel(aChannel) { + gDefaultPrefBranch.unlockPref(PREF_APP_UPDATE_CHANNEL); gDefaultPrefBranch.setCharPref(PREF_APP_UPDATE_CHANNEL, aChannel); } diff --git a/toolkit/modules/tests/xpcshell/test_sqlite.js b/toolkit/modules/tests/xpcshell/test_sqlite.js index e0a79e137d..bea349ded4 100644 --- a/toolkit/modules/tests/xpcshell/test_sqlite.js +++ b/toolkit/modules/tests/xpcshell/test_sqlite.js @@ -334,12 +334,9 @@ add_task(async function test_execute_invalid_statement() { await new Promise(resolve => { Assert.equal(c._connectionData._anonymousStatements.size, 0); - c.execute("SELECT invalid FROM unknown").then( - do_throw, - function onError(error) { - resolve(); - } - ); + c.execute("SELECT invalid FROM unknown").then(do_throw, function onError() { + resolve(); + }); }); // Ensure we don't leak the statement instance. @@ -366,15 +363,11 @@ add_task(async function test_on_row_exception_ignored() { } let i = 0; - let hasResult = await c.execute( - "SELECT * FROM DIRS", - null, - function onRow(row) { - i++; + let hasResult = await c.execute("SELECT * FROM DIRS", null, function onRow() { + i++; - throw new Error("Some silly error."); - } - ); + throw new Error("Some silly error."); + }); Assert.equal(hasResult, true); Assert.equal(i, 10); @@ -418,7 +411,7 @@ add_task(async function test_on_row_stop_iteration() { let hasResult = await c.execute( `SELECT * FROM dirs WHERE path="nonexistent"`, null, - function onRow(row) { + function onRow() { i++; } ); @@ -473,7 +466,7 @@ add_task(async function test_execute_transaction_rollback() { // We should never get here. do_throw(); - }).then(do_throw, function onError(error) { + }).then(do_throw, function onError() { deferred.resolve(); }); @@ -490,7 +483,7 @@ add_task(async function test_close_during_transaction() { await c.execute("INSERT INTO dirs (path) VALUES ('foo')"); - let promise = c.executeTransaction(async function transaction(conn) { + let promise = c.executeTransaction(async function transaction() { await c.execute("INSERT INTO dirs (path) VALUES ('bar')"); }); await c.close(); @@ -801,7 +794,7 @@ add_task(async function test_discard_while_active() { let discarded = -1; let first = true; let sql = "SELECT * FROM dirs"; - await c.executeCached(sql, null, function onRow(row) { + await c.executeCached(sql, null, function onRow() { if (!first) { return; } @@ -1010,7 +1003,7 @@ add_task(async function test_direct() { let deferred = Promise.withResolvers(); begin.executeAsync({ - handleCompletion(reason) { + handleCompletion() { deferred.resolve(); }, }); @@ -1021,7 +1014,7 @@ add_task(async function test_direct() { deferred = Promise.withResolvers(); print("Executing async."); statement.executeAsync({ - handleResult(resultSet) {}, + handleResult() {}, handleError(error) { print( @@ -1031,7 +1024,7 @@ add_task(async function test_direct() { deferred.reject(); }, - handleCompletion(reason) { + handleCompletion() { print("Completed."); deferred.resolve(); }, @@ -1041,7 +1034,7 @@ add_task(async function test_direct() { deferred = Promise.withResolvers(); end.executeAsync({ - handleCompletion(reason) { + handleCompletion() { deferred.resolve(); }, }); diff --git a/toolkit/modules/tests/xpcshell/test_web_channel.js b/toolkit/modules/tests/xpcshell/test_web_channel.js index 6d62762e97..a45f52efde 100644 --- a/toolkit/modules/tests/xpcshell/test_web_channel.js +++ b/toolkit/modules/tests/xpcshell/test_web_channel.js @@ -37,7 +37,7 @@ var MockWebChannelBroker = { * Test channel listening with originOrPermission being an nsIURI. */ add_task(function test_web_channel_listen() { - return new Promise((resolve, reject) => { + return new Promise(resolve => { let channel = new WebChannel( VALID_WEB_CHANNEL_ID, VALID_WEB_CHANNEL_ORIGIN, @@ -94,7 +94,7 @@ add_task(function test_web_channel_listen() { * Test channel listening with originOrPermission being a permission string. */ add_task(function test_web_channel_listen_permission() { - return new Promise((resolve, reject) => { + return new Promise(resolve => { // add a new permission PermissionTestUtils.add( VALID_WEB_CHANNEL_ORIGIN, diff --git a/toolkit/modules/tests/xpcshell/test_web_channel_broker.js b/toolkit/modules/tests/xpcshell/test_web_channel_broker.js index 232e02e935..3e3d4efc0e 100644 --- a/toolkit/modules/tests/xpcshell/test_web_channel_broker.js +++ b/toolkit/modules/tests/xpcshell/test_web_channel_broker.js @@ -44,7 +44,7 @@ add_test(function test_web_channel_broker_channel_map() { * Test WebChannelBroker _listener test */ add_task(function test_web_channel_broker_listener() { - return new Promise((resolve, reject) => { + return new Promise(resolve => { var channel = { id: VALID_WEB_CHANNEL_ID, _originCheckCallback: requestPrincipal => { diff --git a/toolkit/moz.build b/toolkit/moz.build index 30ec614ef1..e60f9c586b 100644 --- a/toolkit/moz.build +++ b/toolkit/moz.build @@ -55,6 +55,7 @@ if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": DIRS += ["system/unixproxy"] elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": DIRS += [ + "mozapps/macos-frameworks", "system/commonproxy", "system/osxproxy", ] diff --git a/toolkit/moz.configure b/toolkit/moz.configure index 693492a4bd..3e89ca77de 100644 --- a/toolkit/moz.configure +++ b/toolkit/moz.configure @@ -205,10 +205,8 @@ def audio_backends_default(target): return ("oss",) elif target.os == "OpenBSD": return ("sndio",) - elif target.os == "OSX": + elif target.kernel == "Darwin": return ("audiounit",) - elif target.os == "iOS": - return None elif target.os == "NetBSD": return ("sunaudio",) elif target.os == "SunOS": @@ -259,11 +257,7 @@ def imply_alsa(values, target): @depends("--enable-audio-backends", target) def imply_audiounit(values, target): - if ( - any("audiounit" in value for value in values) - and target.os != "OSX" - and target.kernel != "Darwin" - ): + if any("audiounit" in value for value in values) and target.kernel != "Darwin": die("Cannot enable AudioUnit on %s", target.os) return any("audiounit" in value for value in values) or None @@ -2783,20 +2777,6 @@ with only_when(compile_environment & wasm_sandboxing.hunspell): set_config("MOZ_WASI_EMULATED_CLOCK", True, when=wasi_emulated_clock) -# new Notification Store implementation -# ============================================================== - - -@depends(milestone) -def new_notification_store(milestone): - if milestone.is_nightly: - return True - - -set_config("MOZ_NEW_NOTIFICATION_STORE", True, when=new_notification_store) -set_define("MOZ_NEW_NOTIFICATION_STORE", True, when=new_notification_store) - - # Auxiliary files persistence on application close # ============================================================== @@ -3054,6 +3034,10 @@ def oxidized_breakpad(target): set_config("MOZ_OXIDIZED_BREAKPAD", True, when=oxidized_breakpad) set_define("MOZ_OXIDIZED_BREAKPAD", True, when=oxidized_breakpad) +# Environment variable to mock the crashreporter for testing +option(env="MOZ_CRASHREPORTER_MOCK", help="Mock the crashreporter to test native GUIs") +set_config("MOZ_CRASHREPORTER_MOCK", True, when="MOZ_CRASHREPORTER_MOCK") + # Wine # ============================================================== diff --git a/toolkit/mozapps/defaultagent/BackgroundTask_defaultagent.sys.mjs b/toolkit/mozapps/defaultagent/BackgroundTask_defaultagent.sys.mjs index 691f76c319..c9da853cb5 100644 --- a/toolkit/mozapps/defaultagent/BackgroundTask_defaultagent.sys.mjs +++ b/toolkit/mozapps/defaultagent/BackgroundTask_defaultagent.sys.mjs @@ -100,7 +100,8 @@ const kNotificationAction = Object.freeze({ export async function runBackgroundTask(commandLine) { Services.fog.initializeFOG( undefined, - "firefox.desktop.background.defaultagent" + "firefox.desktop.background.defaultagent", + /* disableInternalPings */ true ); let defaultAgent = Cc["@mozilla.org/default-agent;1"].getService( diff --git a/toolkit/mozapps/defaultagent/nsIWindowsMutex.idl b/toolkit/mozapps/defaultagent/nsIWindowsMutex.idl index 69090aa764..39ab84bf1f 100644 --- a/toolkit/mozapps/defaultagent/nsIWindowsMutex.idl +++ b/toolkit/mozapps/defaultagent/nsIWindowsMutex.idl @@ -36,7 +36,7 @@ interface nsIWindowsMutex : nsISupports * * @return {boolean} true if locked, false if unlocked. */ - bool isLocked(); + boolean isLocked(); /** * Unlocks the mutex. diff --git a/toolkit/mozapps/extensions/.eslintrc.js b/toolkit/mozapps/extensions/.eslintrc.js index 2e1909b8fb..ef31f0780e 100644 --- a/toolkit/mozapps/extensions/.eslintrc.js +++ b/toolkit/mozapps/extensions/.eslintrc.js @@ -14,7 +14,7 @@ module.exports = { "no-unused-vars": [ "error", { - args: "none", + argsIgnorePattern: "^_", vars: "all", }, ], @@ -26,7 +26,7 @@ module.exports = { "no-unused-vars": [ "error", { - args: "none", + argsIgnorePattern: "^_", vars: "local", }, ], diff --git a/toolkit/mozapps/extensions/AbuseReporter.sys.mjs b/toolkit/mozapps/extensions/AbuseReporter.sys.mjs index 0d8df5c9f3..944cef507c 100644 --- a/toolkit/mozapps/extensions/AbuseReporter.sys.mjs +++ b/toolkit/mozapps/extensions/AbuseReporter.sys.mjs @@ -102,7 +102,7 @@ export class AbuseReportError extends Error { async function responseToErrorInfo(response) { return JSON.stringify({ status: response.status, - responseText: await response.text().catch(err => ""), + responseText: await response.text().catch(() => ""), }); } @@ -244,7 +244,7 @@ export const AbuseReporter = { } let errorInfo = await responseToErrorInfo(response).catch( - err => undefined + () => undefined ); if (response.status === 404) { @@ -550,7 +550,7 @@ export const AbuseReporter = { * A string that identify how the report has been triggered. */ class AbuseReport { - constructor({ addon, createErrorType, reportData, reportEntryPoint }) { + constructor({ addon, reportData, reportEntryPoint }) { this[PRIVATE_REPORT_PROPS] = { aborted: false, abortController: new AbortController(), @@ -599,7 +599,7 @@ class AbuseReport { // Leave errorInfo empty if there is no response or fails to // be converted into an error info object. const errorInfo = response - ? await responseToErrorInfo(response).catch(err => undefined) + ? await responseToErrorInfo(response).catch(() => undefined) : undefined; throw new AbuseReportError(errorType, errorInfo); diff --git a/toolkit/mozapps/extensions/AddonContentPolicy.cpp b/toolkit/mozapps/extensions/AddonContentPolicy.cpp index 983935f7c5..90d1283309 100644 --- a/toolkit/mozapps/extensions/AddonContentPolicy.cpp +++ b/toolkit/mozapps/extensions/AddonContentPolicy.cpp @@ -319,7 +319,7 @@ AddonContentPolicy::ValidateAddonCSP(const nsAString& aPolicyString, nsCOMPtr<nsIURI> selfURI; principal->GetURI(getter_AddRefs(selfURI)); RefPtr<nsCSPContext> csp = new nsCSPContext(); - rv = csp->SetRequestContextWithPrincipal(principal, selfURI, u""_ns, 0); + rv = csp->SetRequestContextWithPrincipal(principal, selfURI, ""_ns, 0); NS_ENSURE_SUCCESS(rv, rv); csp->AppendPolicy(aPolicyString, false, false); diff --git a/toolkit/mozapps/extensions/AddonManager.sys.mjs b/toolkit/mozapps/extensions/AddonManager.sys.mjs index c69cf4029e..a07a651895 100644 --- a/toolkit/mozapps/extensions/AddonManager.sys.mjs +++ b/toolkit/mozapps/extensions/AddonManager.sys.mjs @@ -160,7 +160,7 @@ var PrefObserver = { this.observe(null, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, PREF_LOGGING_ENABLED); }, - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { if (aTopic == "xpcom-shutdown") { Services.prefs.removeObserver(PREF_LOGGING_ENABLED, this); Services.obs.removeObserver(this, "xpcom-shutdown"); @@ -339,7 +339,7 @@ BrowserListener.prototype = { } }, - observe(subject, topic, data) { + observe(subject) { if (subject != this.messageManager) { return; } @@ -349,7 +349,7 @@ BrowserListener.prototype = { this.cancelInstall(); }, - onLocationChange(webProgress, request, location) { + onLocationChange() { if ( this.browser.contentPrincipal && this.principal.subsumes(this.browser.contentPrincipal) @@ -361,19 +361,19 @@ BrowserListener.prototype = { this.cancelInstall(); }, - onDownloadCancelled(install) { + onDownloadCancelled() { this.unregister(); }, - onDownloadFailed(install) { + onDownloadFailed() { this.unregister(); }, - onInstallFailed(install) { + onInstallFailed() { this.unregister(); }, - onInstallEnded(install) { + onInstallEnded() { this.unregister(); }, @@ -553,7 +553,7 @@ var AddonManagerInternal = { this.pendingProviders.add(aProvider); } - return new Promise((resolve, reject) => { + return new Promise(resolve => { logger.debug("Calling shutdown blocker for " + name); resolve(aProvider.shutdown()); }).catch(err => { @@ -1311,7 +1311,7 @@ var AddonManagerInternal = { } updates.push( - new Promise((resolve, reject) => { + new Promise(resolve => { addon.findUpdates( { onUpdateAvailable(aAddon, aInstall) { @@ -3892,7 +3892,7 @@ export var AddonManagerPrivate = { * This can be used as an implementation for Addon.findUpdates() when * no update mechanism is available. */ - callNoUpdateListeners(addon, listener, reason, appVersion, platformVersion) { + callNoUpdateListeners(addon, listener) { if ("onNoCompatibilityUpdateAvailable" in listener) { safeCall(listener.onNoCompatibilityUpdateAvailable.bind(listener), addon); } @@ -4739,7 +4739,7 @@ AMTelemetry = { // Observer Service notification callback. - observe(subject, topic, data) { + observe(subject, topic) { switch (topic) { case "addon-install-blocked": { const { installs } = subject.wrappedJSObject; diff --git a/toolkit/mozapps/extensions/amInstallTrigger.sys.mjs b/toolkit/mozapps/extensions/amInstallTrigger.sys.mjs index 068c0f07c4..b7cabba5b6 100644 --- a/toolkit/mozapps/extensions/amInstallTrigger.sys.mjs +++ b/toolkit/mozapps/extensions/amInstallTrigger.sys.mjs @@ -62,7 +62,7 @@ RemoteMediator.prototype = { } }, - enabled(url) { + enabled() { let params = { mimetype: XPINSTALL_MIMETYPE, }; @@ -237,14 +237,14 @@ InstallTrigger.prototype = { ); }, - startSoftwareUpdate(url, flags) { + startSoftwareUpdate(url) { let filename = Services.io.newURI(url).QueryInterface(Ci.nsIURL).filename; let args = {}; args[filename] = { URL: url }; return this.install(args); }, - installChrome(type, url, skin) { + installChrome(type, url) { return this.startSoftwareUpdate(url); }, diff --git a/toolkit/mozapps/extensions/amManager.sys.mjs b/toolkit/mozapps/extensions/amManager.sys.mjs index 5ce396d601..2571af55e4 100644 --- a/toolkit/mozapps/extensions/amManager.sys.mjs +++ b/toolkit/mozapps/extensions/amManager.sys.mjs @@ -67,7 +67,7 @@ export function amManager() { } amManager.prototype = { - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { switch (aTopic) { case "addons-startup": AddonManagerPrivate.startup(); @@ -129,7 +129,7 @@ amManager.prototype = { if (aCallback) { aInstall.addListener({ - onDownloadCancelled(aInstall) { + onDownloadCancelled() { callCallback(USER_CANCELLED); }, @@ -141,11 +141,11 @@ amManager.prototype = { } }, - onInstallFailed(aInstall) { + onInstallFailed() { callCallback(EXECUTION_ERROR); }, - onInstallEnded(aInstall, aStatus) { + onInstallEnded() { callCallback(SUCCESS); }, }); @@ -165,7 +165,7 @@ amManager.prototype = { return retval; }, - notify(aTimer) { + notify() { AddonManagerPrivate.backgroundUpdateTimerHandler(); }, diff --git a/toolkit/mozapps/extensions/amWebAPI.sys.mjs b/toolkit/mozapps/extensions/amWebAPI.sys.mjs index abe838af89..ae1b94cac7 100644 --- a/toolkit/mozapps/extensions/amWebAPI.sys.mjs +++ b/toolkit/mozapps/extensions/amWebAPI.sys.mjs @@ -214,7 +214,7 @@ export class WebAPI extends APIObject { super.init(window, broker, {}); - window.addEventListener("unload", event => { + window.addEventListener("unload", () => { this.broker.sendCleanup(this.allInstalls); }); } @@ -263,7 +263,7 @@ export class WebAPI extends APIObject { return lazy.AMO_ABUSEREPORT; } - eventListenerAdded(type) { + eventListenerAdded() { if (this.listenerCount == 0) { this.broker.setAddonListener(data => { let event = new this.window.AddonEvent(data.event, data); @@ -273,7 +273,7 @@ export class WebAPI extends APIObject { this.listenerCount++; } - eventListenerRemoved(type) { + eventListenerRemoved() { this.listenerCount--; if (this.listenerCount == 0) { this.broker.setAddonListener(null); diff --git a/toolkit/mozapps/extensions/content/aboutaddons.html b/toolkit/mozapps/extensions/content/aboutaddons.html index 55d6625c08..d0930ef42d 100644 --- a/toolkit/mozapps/extensions/content/aboutaddons.html +++ b/toolkit/mozapps/extensions/content/aboutaddons.html @@ -13,9 +13,6 @@ content="default-src chrome:; style-src chrome: 'unsafe-inline'; img-src chrome: file: jar: https: http:; connect-src chrome: data: https: http:; object-src 'none'" /> <meta name="color-scheme" content="light dark" /> - <link rel="stylesheet" href="chrome://global/content/tabprompts.css" /> - <link rel="stylesheet" href="chrome://global/skin/tabprompts.css" /> - <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" /> <link rel="stylesheet" diff --git a/toolkit/mozapps/extensions/content/aboutaddons.js b/toolkit/mozapps/extensions/content/aboutaddons.js index 39c4656210..d7d9c28ae6 100644 --- a/toolkit/mozapps/extensions/content/aboutaddons.js +++ b/toolkit/mozapps/extensions/content/aboutaddons.js @@ -322,12 +322,12 @@ function checkForUpdate(addon) { onDownloadFailed: failed, onInstallCancelled: failed, onInstallFailed: failed, - onInstallEnded: (...args) => { + onInstallEnded: () => { detachUpdateHandler(install); install.removeListener(updateListener); resolve({ installed: true, pending: false, found: true }); }, - onInstallPostponed: (...args) => { + onInstallPostponed: () => { detachUpdateHandler(install); install.removeListener(updateListener); resolve({ installed: false, pending: true, found: true }); @@ -375,7 +375,7 @@ const OPTIONS_TYPE_MAP = { // Check if an add-on has the provided options type, accounting for the pref // to disable inline options. -function getOptionsType(addon, type) { +function getOptionsType(addon) { return OPTIONS_TYPE_MAP[addon.optionsType]; } @@ -1064,7 +1064,7 @@ class AddonPageOptions extends HTMLElement { } } - async checkForUpdates(e) { + async checkForUpdates() { let message = document.getElementById("updates-message"); message.state = "updating"; message.hidden = false; @@ -2096,11 +2096,11 @@ class AddonDetails extends HTMLElement { } } - onDisabled(addon) { + onDisabled() { this.extensionShutdown(); } - onEnabled(addon) { + onEnabled() { this.extensionStartup(); } @@ -2451,45 +2451,27 @@ class AddonCard extends HTMLElement { async setAddonPermission(permission, type, action) { let { addon } = this; - let origins = [], - permissions = []; + let perms = { origins: [], permissions: [] }; + if (!["add", "remove"].includes(action)) { throw new Error("invalid action for permission change"); } - if (type == "permission") { - if ( - action == "add" && - !addon.optionalPermissions.permissions.includes(permission) - ) { - throw new Error("permission missing from manifest"); - } - permissions = [permission]; - } else if (type == "origin") { - if (action === "add") { - let { origins } = addon.optionalPermissions; - let patternSet = new MatchPatternSet(origins, { ignorePath: true }); - if (!patternSet.subsumes(new MatchPattern(permission))) { - throw new Error("origin missing from manifest"); - } - } - origins = [permission]; - // If this is one of the "all sites" permissions - if (Extension.isAllSitesPermission(permission)) { - // Grant/revoke ALL "all sites" optional permissions from the manifest. - origins = addon.optionalPermissions.origins.filter(perm => - Extension.isAllSitesPermission(perm) - ); - } + if (type === "permission") { + perms.permissions = [permission]; + } else if (type === "origin") { + perms.origins = [permission]; } else { throw new Error("unknown permission type changed"); } - let policy = WebExtensionPolicy.getByID(addon.id); - ExtensionPermissions[action]( - addon.id, - { origins, permissions }, - policy?.extension + + let normalized = ExtensionPermissions.normalizeOptional( + perms, + addon.optionalPermissions ); + + let policy = WebExtensionPolicy.getByID(addon.id); + ExtensionPermissions[action](addon.id, normalized, policy?.extension); } async handleEvent(e) { @@ -2968,18 +2950,18 @@ class AddonCard extends HTMLElement { this.sendEvent("update-postponed"); } - onDisabled(addon) { + onDisabled() { if (!this.reloading) { this.update(); } } - onEnabled(addon) { + onEnabled() { this.reloading = false; this.update(); } - onInstalled(addon) { + onInstalled() { // When a temporary addon is reloaded, onInstalled is triggered instead of // onEnabled. this.reloading = false; diff --git a/toolkit/mozapps/extensions/content/aboutaddonsCommon.js b/toolkit/mozapps/extensions/content/aboutaddonsCommon.js index 9315e35861..fd91ba58be 100644 --- a/toolkit/mozapps/extensions/content/aboutaddonsCommon.js +++ b/toolkit/mozapps/extensions/content/aboutaddonsCommon.js @@ -5,7 +5,7 @@ "use strict"; -/* exported attachUpdateHandler, detachUpdateHandler, gBrowser, +/* exported attachUpdateHandler, detachUpdateHandler, * getBrowserElement, installAddonsFromFilePicker, * isCorrectlySigned, isDisabledUnsigned, isDiscoverEnabled, * isPending, loadReleaseNotes, openOptionsInTab, promiseEvent, @@ -196,21 +196,6 @@ function showPermissionsPrompt(addon) { }); } -// Stub tabbrowser implementation for use by the tab-modal alert code -// when an alert/prompt/confirm method is called in a WebExtensions options_ui -// page (See Bug 1385548 for rationale). -var gBrowser = { - getTabModalPromptBox(browser) { - const parentWindow = window.docShell.chromeEventHandler.ownerGlobal; - - if (parentWindow.gBrowser) { - return parentWindow.gBrowser.getTabModalPromptBox(browser); - } - - return null; - }, -}; - function isCorrectlySigned(addon) { // Add-ons without an "isCorrectlySigned" property are correctly signed as // they aren't the correct type for signing. diff --git a/toolkit/mozapps/extensions/content/abuse-reports.js b/toolkit/mozapps/extensions/content/abuse-reports.js index c6461a071b..38fa0f9f46 100644 --- a/toolkit/mozapps/extensions/content/abuse-reports.js +++ b/toolkit/mozapps/extensions/content/abuse-reports.js @@ -185,7 +185,7 @@ async function openAbuseReport({ addonId, reportEntryPoint }) { // to be async, but it is so that both the implementations will be providing // the same type signatures (returning a promise) to the callers, independently // from which abuse reporting feature is enabled. -async function openAbuseReportAMOForm({ addonId, reportEntryPoint }) { +async function openAbuseReportAMOForm({ addonId }) { const amoUrl = AbuseReporter.getAMOFormURL({ addonId }); windowRoot.ownerGlobal.openTrustedLinkIn(amoUrl, "tab", { // Make sure the newly open tab is going to be focused, independently diff --git a/toolkit/mozapps/extensions/content/shortcuts.js b/toolkit/mozapps/extensions/content/shortcuts.js index 59420226df..99ab1c5161 100644 --- a/toolkit/mozapps/extensions/content/shortcuts.js +++ b/toolkit/mozapps/extensions/content/shortcuts.js @@ -283,7 +283,7 @@ ChromeUtils.defineESModuleGetters(this, { } return Object.entries(modifierMap) - .filter(([key, isDown]) => isDown) + .filter(([, isDown]) => isDown) .map(([key]) => key) .concat(getStringForEvent(e)) .join("+"); diff --git a/toolkit/mozapps/extensions/content/view-controller.js b/toolkit/mozapps/extensions/content/view-controller.js index 978a44d176..b82c1d6e6d 100644 --- a/toolkit/mozapps/extensions/content/view-controller.js +++ b/toolkit/mozapps/extensions/content/view-controller.js @@ -89,7 +89,7 @@ var gViewController = { } }, - observe(subject, topic, data) { + observe(subject, topic) { if (topic == "EM-ping") { this.readyForLoadView = true; Services.obs.notifyObservers(window, "EM-pong"); diff --git a/toolkit/mozapps/extensions/internal/AddonRepository.sys.mjs b/toolkit/mozapps/extensions/internal/AddonRepository.sys.mjs index e53e4af7a4..8d4d178924 100644 --- a/toolkit/mozapps/extensions/internal/AddonRepository.sys.mjs +++ b/toolkit/mozapps/extensions/internal/AddonRepository.sys.mjs @@ -466,13 +466,13 @@ export var AddonRepository = { request.open("GET", url, true); request.responseType = "json"; - request.addEventListener("error", aEvent => { + request.addEventListener("error", () => { reject(new Error(`GET ${url} failed`)); }); - request.addEventListener("timeout", aEvent => { + request.addEventListener("timeout", () => { reject(new Error(`GET ${url} timed out`)); }); - request.addEventListener("load", aEvent => { + request.addEventListener("load", () => { let response = request.response; if (!response || (request.status != 200 && request.status != 0)) { reject(new Error(`GET ${url} failed (status ${request.status})`)); diff --git a/toolkit/mozapps/extensions/internal/AddonTestUtils.sys.mjs b/toolkit/mozapps/extensions/internal/AddonTestUtils.sys.mjs index f53d32092d..7e4adacd11 100644 --- a/toolkit/mozapps/extensions/internal/AddonTestUtils.sys.mjs +++ b/toolkit/mozapps/extensions/internal/AddonTestUtils.sys.mjs @@ -3,7 +3,7 @@ */ /* eslint "mozilla/no-aArgs": 1 */ -/* eslint "no-unused-vars": [2, {"args": "none", "varsIgnorePattern": "^(Cc|Ci|Cr|Cu|EXPORTED_SYMBOLS)$"}] */ +/* eslint "no-unused-vars": [2, {"argsIgnorePattern": "^_", "varsIgnorePattern": "^(Cc|Ci|Cr|Cu|EXPORTED_SYMBOLS)$"}] */ /* eslint "semi": [2, "always"] */ /* eslint "valid-jsdoc": [2, {requireReturn: false}] */ @@ -424,6 +424,35 @@ export var AddonTestUtils = { }); }, + getXPIExports() { + return ChromeUtils.importESModule( + "resource://gre/modules/addons/XPIExports.sys.mjs" + ).XPIExports; + }, + + getWeakSignatureInstallPrefName() { + return this.getXPIExports().XPIInstall.getWeakSignatureInstallPrefName(); + }, + + setWeakSignatureInstallAllowed(allowed) { + const prefName = this.getWeakSignatureInstallPrefName(); + let cleanupCalled = false; + const cleanup = () => { + if (cleanupCalled) { + return; + } + this.testScope.info( + `=== clear ${prefName} pref value set by this test file ===` + ); + Services.prefs.clearUserPref(prefName); + cleanupCalled = true; + }; + this.testScope.registerCleanupFunction(cleanup); + this.testScope.info(`=== set ${prefName} pref value to ${allowed} ===`); + Services.prefs.setBoolPref(prefName, allowed); + return cleanup; + }, + /** * Iterates over the entries in a given directory. * diff --git a/toolkit/mozapps/extensions/internal/GMPProvider.sys.mjs b/toolkit/mozapps/extensions/internal/GMPProvider.sys.mjs index aaac109fc0..22ca2365d1 100644 --- a/toolkit/mozapps/extensions/internal/GMPProvider.sys.mjs +++ b/toolkit/mozapps/extensions/internal/GMPProvider.sys.mjs @@ -340,7 +340,7 @@ GMPWrapper.prototype = { return { source: "gmp-plugin" }; }, - isCompatibleWith(aAppVersion, aPlatformVersion) { + isCompatibleWith() { return true; }, @@ -377,7 +377,7 @@ GMPWrapper.prototype = { * Widevine is not yet installed, or if the user toggles prefs to enable EME. * For the function used in those cases see `checkForUpdates`. */ - findUpdates(aListener, aReason, aAppVersion, aPlatformVersion) { + findUpdates(aListener, aReason) { this._log.trace( "findUpdates() - " + this._plugin.id + " - reason=" + aReason ); @@ -895,7 +895,7 @@ var GMPProvider = { } }, - observe(subject, topic, data) { + observe(subject, topic) { if (topic == FIRST_CONTENT_PROCESS_TOPIC) { lazy.AddonManagerPrivate.registerProvider(GMPProvider, ["plugin"]); Services.obs.notifyObservers(null, "gmp-provider-registered"); diff --git a/toolkit/mozapps/extensions/internal/ProductAddonChecker.sys.mjs b/toolkit/mozapps/extensions/internal/ProductAddonChecker.sys.mjs index 1615a551c8..64b337af3f 100644 --- a/toolkit/mozapps/extensions/internal/ProductAddonChecker.sys.mjs +++ b/toolkit/mozapps/extensions/internal/ProductAddonChecker.sys.mjs @@ -118,12 +118,18 @@ async function conservativeFetch(input) { * @param contentSignatureHeader * The contents of the 'content-signature' header received along with * `data`. + * @param trustedRoot + * The identifier of the trusted root to use for certificate validation. * @return A promise that will resolve to nothing if the signature verification * succeeds, or rejects on failure, with an Error that sets its * addonCheckerErr property disambiguate failure cases and a message * explaining the error. */ -async function verifyGmpContentSignature(data, contentSignatureHeader) { +async function verifyGmpContentSignature( + data, + contentSignatureHeader, + trustedRoot +) { if (!contentSignatureHeader) { logger.warn( "Unexpected missing content signature header during content signature validation" @@ -186,13 +192,6 @@ async function verifyGmpContentSignature(data, contentSignatureHeader) { "@mozilla.org/security/contentsignatureverifier;1" ].createInstance(Ci.nsIContentSignatureVerifier); - // See bug 1771992. In the future, this may need to handle staging and dev - // environments in addition to just production and testing. - let root = Ci.nsIContentSignatureVerifier.ContentSignatureProdRoot; - if (Services.env.exists("XPCSHELL_TEST_PROFILE_DIR")) { - root = Ci.nsIX509CertDB.AppXPCShellRoot; - } - let valid; try { valid = await verifier.asyncVerifyContentSignature( @@ -200,7 +199,7 @@ async function verifyGmpContentSignature(data, contentSignatureHeader) { signature, certChain, "aus.content-signature.mozilla.org", - root + trustedRoot ); } catch (err) { logger.warn(`Unexpected error while validating content signature: ${err}`); @@ -329,6 +328,9 @@ function downloadXMLWithRequest( * @param verifyContentSignature * When true, will verify the content signature information from the * response header. Failure to verify will result in an error. + * @param trustedContentSignatureRoot + * The trusted root to use for certificate validation. + * Must be set if verifyContentSignature is true. * @return a promise that resolves to the DOM document downloaded or rejects * with a JS exception in case of error. */ @@ -336,7 +338,8 @@ async function downloadXML( url, allowNonBuiltIn = false, allowedCerts = null, - verifyContentSignature = false + verifyContentSignature = false, + trustedContentSignatureRoot = null ) { let request = await downloadXMLWithRequest( url, @@ -346,7 +349,8 @@ async function downloadXML( if (verifyContentSignature) { await verifyGmpContentSignature( request.response, - request.getResponseHeader("content-signature") + request.getResponseHeader("content-signature"), + trustedContentSignatureRoot ); } return request.responseXML; @@ -422,7 +426,7 @@ function downloadFile(url, options = { httpsOnlyNoUpgrade: false }) { return new Promise((resolve, reject) => { let sr = new lazy.ServiceRequest(); - sr.onload = function (response) { + sr.onload = function () { logger.info("downloadFile File download. status=" + sr.status); if (sr.status != 200 && sr.status != 206) { reject(Components.Exception("File download failed", sr.status)); @@ -535,6 +539,9 @@ export const ProductAddonChecker = { * @param verifyContentSignature * When true, will verify the content signature information from the * response header. Failure to verify will result in an error. + * @param trustedContentSignatureRoot + * The trusted root to use for certificate validation. + * Must be set if verifyContentSignature is true. * @return a promise that resolves to an object containing the list of add-ons * and whether the local fallback was used, or rejects with a JS * exception in case of error. In the case of an error, a best effort @@ -545,13 +552,15 @@ export const ProductAddonChecker = { url, allowNonBuiltIn = false, allowedCerts = null, - verifyContentSignature = false + verifyContentSignature = false, + trustedContentSignatureRoot = null ) { return downloadXML( url, allowNonBuiltIn, allowedCerts, - verifyContentSignature + verifyContentSignature, + trustedContentSignatureRoot ).then(parseXML); }, diff --git a/toolkit/mozapps/extensions/internal/SitePermsAddonProvider.sys.mjs b/toolkit/mozapps/extensions/internal/SitePermsAddonProvider.sys.mjs index 7ca952dc8a..8af3c2affe 100644 --- a/toolkit/mozapps/extensions/internal/SitePermsAddonProvider.sys.mjs +++ b/toolkit/mozapps/extensions/internal/SitePermsAddonProvider.sys.mjs @@ -190,7 +190,7 @@ class SitePermsAddonWrapper { return 0; } - async updateBlocklistState(options = {}) {} + async updateBlocklistState() {} get blocklistState() { return Ci.nsIBlocklistService.STATE_NOT_BLOCKED; @@ -277,7 +277,7 @@ class SitePermsAddonWrapper { return { source: "siteperm-addon-provider", method: "synthetic-install" }; } - isCompatibleWith(aAppVersion, aPlatformVersion) { + isCompatibleWith() { return true; } } diff --git a/toolkit/mozapps/extensions/internal/XPIDatabase.sys.mjs b/toolkit/mozapps/extensions/internal/XPIDatabase.sys.mjs index af0b02444a..d7541167fa 100644 --- a/toolkit/mozapps/extensions/internal/XPIDatabase.sys.mjs +++ b/toolkit/mozapps/extensions/internal/XPIDatabase.sys.mjs @@ -31,6 +31,7 @@ ChromeUtils.defineESModuleGetters(lazy, { DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", ExtensionData: "resource://gre/modules/Extension.sys.mjs", ExtensionUtils: "resource://gre/modules/ExtensionUtils.sys.mjs", + ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs", PermissionsUtils: "resource://gre/modules/PermissionsUtils.sys.mjs", QuarantinedDomains: "resource://gre/modules/ExtensionPermissions.sys.mjs", }); @@ -192,6 +193,7 @@ const PROP_JSON_FIELDS = [ "targetApplications", "targetPlatforms", "signedState", + "signedTypes", "signedDate", "seen", "dependencies", @@ -1556,6 +1558,7 @@ function defineAddonWrapperProperty(name, getter) { "validInstallOrigins", "dependencies", "signedState", + "signedTypes", "sitePermissions", "siteOrigin", "isCorrectlySigned", @@ -2175,7 +2178,7 @@ export const XPIDatabase = { */ async verifySignatures() { try { - let addons = await this.getAddonList(a => true); + let addons = await this.getAddonList(() => true); let changes = { enabled: [], @@ -2188,17 +2191,31 @@ export const XPIDatabase = { continue; } - let signedState = await XPIExports.verifyBundleSignedState( - addon._sourceBundle, - addon - ); + let { signedState, signedTypes } = + await XPIExports.verifyBundleSignedState(addon._sourceBundle, addon); + + const changedProperties = []; if (signedState != addon.signedState) { addon.signedState = signedState; + changedProperties.push("signedState"); + } + + if ( + !lazy.ObjectUtils.deepEqual( + signedTypes?.toSorted(), + addon.signedTypes?.toSorted() + ) + ) { + addon.signedTypes = signedTypes; + changedProperties.push("signedTypes"); + } + + if (changedProperties.length) { lazy.AddonManagerPrivate.callAddonListeners( "onPropertyChanged", addon.wrapper, - ["signedState"] + changedProperties ); } @@ -2426,7 +2443,7 @@ export const XPIDatabase = { if (!this.addonDB) { return []; } - return _filterDB(this.addonDB, aAddon => true); + return _filterDB(this.addonDB, () => true); }, /** @@ -3084,24 +3101,11 @@ export const XPIDatabaseReconcile = { * The new state of the add-on * @param {AddonInternal?} [aNewAddon] * The manifest for the new add-on if it has already been loaded - * @param {string?} [aOldAppVersion] - * The version of the application last run with this profile or null - * if it is a new profile or the version is unknown - * @param {string?} [aOldPlatformVersion] - * The version of the platform last run with this profile or null - * if it is a new profile or the version is unknown * @returns {boolean} * A boolean indicating if flushing caches is required to complete * changing this add-on */ - addMetadata( - aLocation, - aId, - aAddonState, - aNewAddon, - aOldAppVersion, - aOldPlatformVersion - ) { + addMetadata(aLocation, aId, aAddonState, aNewAddon) { logger.debug(`New add-on ${aId} installed in ${aLocation.name}`); // We treat this is a new install if, @@ -3348,6 +3352,10 @@ export const XPIDatabaseReconcile = { let signedDateMissing = aOldAddon.signedDate === undefined && (aOldAddon.signedState || checkSigning); + // signedTypes must be set if signedState is set. + let signedTypesMissing = + aOldAddon.signedTypes === undefined && + (aOldAddon.signedState || checkSigning); // If maxVersion was inadvertently updated for a locale, force a reload // from the manifest. See Bug 1646016 for details. @@ -3360,7 +3368,12 @@ export const XPIDatabaseReconcile = { } let manifest = null; - if (checkSigning || aReloadMetadata || signedDateMissing) { + if ( + checkSigning || + aReloadMetadata || + signedDateMissing || + signedTypesMissing + ) { try { manifest = XPIExports.XPIInstall.syncLoadManifest( aAddonState, @@ -3384,6 +3397,10 @@ export const XPIDatabaseReconcile = { aOldAddon.signedDate = manifest.signedDate; } + if (signedTypesMissing) { + aOldAddon.signedTypes = manifest.signedTypes; + } + // May be updating from a version of the app that didn't support all the // properties of the currently-installed add-ons. if (aReloadMetadata) { diff --git a/toolkit/mozapps/extensions/internal/XPIInstall.sys.mjs b/toolkit/mozapps/extensions/internal/XPIInstall.sys.mjs index 0402c2f2ca..4a26785da8 100644 --- a/toolkit/mozapps/extensions/internal/XPIInstall.sys.mjs +++ b/toolkit/mozapps/extensions/internal/XPIInstall.sys.mjs @@ -21,6 +21,7 @@ import { XPIExports } from "resource://gre/modules/addons/XPIExports.sys.mjs"; import { computeSha256HashAsString, getHashStringForCrypto, + hasStrongSignature, } from "resource://gre/modules/addons/crypto-utils.sys.mjs"; import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; import { @@ -94,6 +95,9 @@ const PREF_XPI_ENABLED = "xpinstall.enabled"; const PREF_XPI_DIRECT_WHITELISTED = "xpinstall.whitelist.directRequest"; const PREF_XPI_FILE_WHITELISTED = "xpinstall.whitelist.fileRequest"; const PREF_XPI_WHITELIST_REQUIRED = "xpinstall.whitelist.required"; +const PREF_XPI_WEAK_SIGNATURES_ALLOWED = + "xpinstall.signatures.weakSignaturesTemporarilyAllowed"; +const PREF_XPI_WEAK_SIGNATURES_ALLOWED_DEFAULT = true; const PREF_SELECTED_THEME = "extensions.activeThemeID"; @@ -230,7 +234,15 @@ class Package { let root = Ci.nsIX509CertDB.AddonsPublicRoot; if ( - !AppConstants.MOZ_REQUIRE_SIGNING && + (!AppConstants.MOZ_REQUIRE_SIGNING || + // Allow mochitests to switch to dev-root on all channels. + Cu.isInAutomation || + // Allow xpcshell tests to switch to dev-root on all channels, + // included tests where "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer" + // pref is set to false and Cu.isInAutomation is going to be false (e.g. test_signed_langpack.js). + // TODO(Bug 1598804): we should be able to remove the following checks once Cu.isAutomation is fixed. + (Services.env.exists("XPCSHELL_TEST_PROFILE_DIR") && + Services.appinfo.name === "XPCShell")) && Services.prefs.getBoolPref(PREF_XPI_SIGNATURES_DEV_ROOT, false) ) { root = Ci.nsIX509CertDB.AddonsStageRoot; @@ -287,7 +299,7 @@ DirPackage = class DirPackage extends Package { return IOUtils.read(PathUtils.join(this.filePath, ...path)); } - async verifySignedStateForRoot(addonId, root) { + async verifySignedStateForRoot() { return { signedState: AddonManager.SIGNEDSTATE_UNKNOWN, cert: null }; } }; @@ -341,8 +353,11 @@ XPIPackage = class XPIPackage extends Package { aZipReader.close(); } resolve({ - signedState: getSignedStatus(aRv, cert, addonId), cert, + signedState: getSignedStatus(aRv, cert, addonId), + signedTypes: aSignatureInfos?.map( + signatureInfo => signatureInfo.signatureAlgorithm + ), }); }, }; @@ -511,17 +526,18 @@ async function loadManifestFromWebManifest(aPackage, aLocation) { addon.siteOrigin = manifest.install_origins[0]; } - if (manifest.options_ui) { + const { optionsPageProperties } = extension; + if (optionsPageProperties) { // Store just the relative path here, the AddonWrapper getURL // wrapper maps this to a full URL. - addon.optionsURL = manifest.options_ui.page; - if (manifest.options_ui.open_in_tab) { + addon.optionsURL = optionsPageProperties.page; + if (optionsPageProperties.open_in_tab) { addon.optionsType = AddonManager.OPTIONS_TYPE_TAB; } else { addon.optionsType = AddonManager.OPTIONS_TYPE_INLINE_BROWSER; } - addon.optionsBrowserStyle = manifest.options_ui.browser_style; + addon.optionsBrowserStyle = optionsPageProperties.browser_style; } // WebExtensions don't use iconURLs @@ -693,9 +709,13 @@ var loadManifest = async function (aPackage, aLocation, aOldAddon) { addon.rootURI = aPackage.rootURI.spec; addon.location = aLocation; - let { signedState, cert } = verifiedSignedState; + let { cert, signedState, signedTypes } = verifiedSignedState; addon.signedState = signedState; addon.signedDate = cert?.validity?.notBefore / 1000 || null; + // An array of the algorithms used by the signatures found in the signed XPI files, + // as an array of integers (see nsIAppSignatureInfo_SignatureAlgorithm enum defined + // in nsIX509CertDB.idl). + addon.signedTypes = signedTypes; if (!addon.id) { if (cert) { @@ -909,18 +929,21 @@ function shouldVerifySignedState(aAddonType, aLocation) { * The nsIFile for the bundle to check, either a directory or zip file. * @param {AddonInternal} aAddon * The add-on object to verify. - * @returns {Promise<number>} - * A Promise that resolves to an AddonManager.SIGNEDSTATE_* constant. + * @returns {Promise<{ signedState: number, signedTypes: Array<number>}>?} + * A Promise that resolves to object including a signedState property set to + * an AddonManager.SIGNEDSTATE_* constant and a signedTypes property set to + * either an array of Ci.nsIAppSignatureInfo SignatureAlgorithm enum values + * or undefined if the file wasn't signed. */ export var verifyBundleSignedState = async function (aBundle, aAddon) { let pkg = Package.get(aBundle); try { - let { signedState } = await pkg.verifySignedState( + let { signedState, signedTypes } = await pkg.verifySignedState( aAddon.id, aAddon.type, aAddon.location ); - return signedState; + return { signedState, signedTypes }; } finally { pkg.close(); } @@ -1633,6 +1656,49 @@ class AddonInstall { "signature verification failed", ]); } + + // Restrict install for signed extension only signed with weak signature algorithms, unless the + // restriction is explicitly disabled through prefs or enterprise policies. + if ( + !XPIInstall.isWeakSignatureInstallAllowed() && + this.addon.signedDate && + !hasStrongSignature(this.addon) + ) { + const addonAllowedByPolicies = Services.policies.getExtensionSettings( + this.addon.id + )?.temporarily_allow_weak_signatures; + + const globallyAllowedByPolicies = + Services.policies.getExtensionSettings( + "*" + )?.temporarily_allow_weak_signatures; + + const allowedByPolicies = + (globallyAllowedByPolicies && + (addonAllowedByPolicies || addonAllowedByPolicies == null)) || + addonAllowedByPolicies; + + if ( + !allowedByPolicies && + (!this.existingAddon || hasStrongSignature(this.existingAddon)) + ) { + // Reject if it is a new install or installing over an existing addon including + // strong cryptographic signatures. + return Promise.reject([ + AddonManager.ERROR_CORRUPT_FILE, + "install rejected due to the package not including a strong cryptographic signature", + ]); + } + + // Still allow installs using weak signatures to install if either: + // - it is explicitly allowed through Enterprise Policies Settings + // - or there is an existing addon with a weak signature. + logger.warn( + allowedByPolicies + ? `Allow weak signature install for ${this.addon.id} XPI due to Enterprise Policies` + : `Allow weak signature install over existing "${this.existingAddon.id}" XPI` + ); + } } } finally { pkg.close(); @@ -2321,7 +2387,7 @@ var DownloadAddonInstall = class extends AddonInstall { } } - observe(aSubject, aTopic, aData) { + observe() { // Network is going offline this.cancel(); } @@ -2592,7 +2658,7 @@ var DownloadAddonInstall = class extends AddonInstall { new UpdateChecker( this.addon, { - onUpdateFinished: aAddon => this.downloadCompleted(), + onUpdateFinished: () => this.downloadCompleted(), }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED ); @@ -3880,7 +3946,7 @@ class SystemAddonInstaller extends DirectoryInstaller { } // old system add-on upgrade dirs get automatically removed - uninstallAddon(aAddon) {} + uninstallAddon() {} } var AppUpdate = { @@ -4344,6 +4410,17 @@ export var XPIInstall = { return Services.prefs.getBoolPref(PREF_XPI_FILE_WHITELISTED, true); }, + isWeakSignatureInstallAllowed() { + return Services.prefs.getBoolPref( + PREF_XPI_WEAK_SIGNATURES_ALLOWED, + PREF_XPI_WEAK_SIGNATURES_ALLOWED_DEFAULT + ); + }, + + getWeakSignatureInstallPrefName() { + return PREF_XPI_WEAK_SIGNATURES_ALLOWED; + }, + /** * Called to test whether installing XPI add-ons from a URI is allowed. * diff --git a/toolkit/mozapps/extensions/internal/XPIProvider.sys.mjs b/toolkit/mozapps/extensions/internal/XPIProvider.sys.mjs index 12d4fa1172..3c090b5f0f 100644 --- a/toolkit/mozapps/extensions/internal/XPIProvider.sys.mjs +++ b/toolkit/mozapps/extensions/internal/XPIProvider.sys.mjs @@ -120,7 +120,7 @@ const XPI_PERMISSION = "install"; const XPI_SIGNATURE_CHECK_PERIOD = 24 * 60 * 60; -const DB_SCHEMA = 35; +const DB_SCHEMA = 36; XPCOMUtils.defineLazyPreferenceGetter( lazy, @@ -1598,7 +1598,7 @@ var XPIStates = { * * @returns {XPIState?} */ - findAddon(aId, aFilter = location => true) { + findAddon(aId, aFilter = () => true) { // Fortunately the Map iterator returns in order of insertion, which is // also our highest -> lowest priority order. for (let location of this.locations()) { @@ -2706,7 +2706,7 @@ export var XPIProvider = { "profile-before-change", "test-load-xpi-database", ]; - let observer = (subject, topic, data) => { + let observer = (subject, topic) => { if ( topic == "xul-window-visible" && !Services.wm.getMostRecentWindow("devtools:toolbox") diff --git a/toolkit/mozapps/extensions/internal/crypto-utils.sys.mjs b/toolkit/mozapps/extensions/internal/crypto-utils.sys.mjs index b1304bcdfc..9ef096df0f 100644 --- a/toolkit/mozapps/extensions/internal/crypto-utils.sys.mjs +++ b/toolkit/mozapps/extensions/internal/crypto-utils.sys.mjs @@ -8,6 +8,14 @@ const CryptoHash = Components.Constructor( "initWithString" ); +const XPI_WEAK_SIGNATURES = [Ci.nsIAppSignatureInfo.PKCS7_WITH_SHA1]; + +export function hasStrongSignature(addon) { + return !!addon.signedTypes?.filter( + algorithm => !XPI_WEAK_SIGNATURES.includes(algorithm) + ).length; +} + export function computeHashAsString(hashType, input) { const data = new Uint8Array(new TextEncoder().encode(input)); const crypto = CryptoHash(hashType); @@ -42,7 +50,7 @@ export function computeSha1HashAsString(input) { /** * Returns the string representation (hex) of a given CryptoHashInstance. * - * @param {CryptoHash} aCrypto + * @param {nsICryptoHash} aCrypto * @returns {string} * The hex representation of a SHA256 hash. */ diff --git a/toolkit/mozapps/extensions/test/browser/.eslintrc.js b/toolkit/mozapps/extensions/test/browser/.eslintrc.js index f2b9e072f9..6987757bdc 100644 --- a/toolkit/mozapps/extensions/test/browser/.eslintrc.js +++ b/toolkit/mozapps/extensions/test/browser/.eslintrc.js @@ -8,7 +8,7 @@ module.exports = { rules: { "no-unused-vars": [ "error", - { args: "none", varsIgnorePattern: "^end_test$" }, + { argsIgnorePattern: "^_", varsIgnorePattern: "^end_test$" }, ], }, }; diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/manifest.mf b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/manifest.mf deleted file mode 100644 index 725ac8016f..0000000000 --- a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/manifest.mf +++ /dev/null @@ -1,8 +0,0 @@ -Manifest-Version: 1.0 - -Name: manifest.json -Digest-Algorithms: MD5 SHA1 SHA256 -MD5-Digest: mCLu38qfGN3trj7qKQQeEA== -SHA1-Digest: A1BaJErQY6KqnYDijP0lglrehk0= -SHA256-Digest: p2vjGP7DRqrK81NfT4LqnF7a5p8+lEuout5WLBhk9AA= - diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/mozilla.rsa b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/mozilla.rsa Binary files differdeleted file mode 100644 index 046a0285c7..0000000000 --- a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/mozilla.rsa +++ /dev/null diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/mozilla.sf b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/mozilla.sf deleted file mode 100644 index ad4e81b574..0000000000 --- a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/mozilla.sf +++ /dev/null @@ -1,5 +0,0 @@ -Signature-Version: 1.0 -MD5-Digest-Manifest: LrrwWBKNYWeVd205Hq+JwQ== -SHA1-Digest-Manifest: MeqqQN+uuf0MVesMXxbBtYN+5tU= -SHA256-Digest-Manifest: iWCxfAJX593Cn4l8R63jaQETO5HX3XOhcnpQ7nMiPlg= - diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/manifest.mf b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/manifest.mf deleted file mode 100644 index 1da3c41b23..0000000000 --- a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/manifest.mf +++ /dev/null @@ -1,8 +0,0 @@ -Manifest-Version: 1.0 - -Name: manifest.json -Digest-Algorithms: MD5 SHA1 SHA256 -MD5-Digest: 3dL7JFDBPC63pSFI5x+Z7Q== -SHA1-Digest: l1cKPyWJIYdZyvumH9VfJ6fpqVA= -SHA256-Digest: QHTjPqTMXxt5tl8zOaAzpQ8FZLqZx8LRF9LmzY+RCDQ= - diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/mozilla.rsa b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/mozilla.rsa Binary files differdeleted file mode 100644 index 170a361620..0000000000 --- a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/mozilla.rsa +++ /dev/null diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/mozilla.sf b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/mozilla.sf deleted file mode 100644 index 5301e431f7..0000000000 --- a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/mozilla.sf +++ /dev/null @@ -1,5 +0,0 @@ -Signature-Version: 1.0 -MD5-Digest-Manifest: c30hzcI1ISlt46ODjVVJ2w== -SHA1-Digest-Manifest: 2yMpQHuLM0J61T7vt11NHoYI1tU= -SHA256-Digest-Manifest: qtsYxiv1zGWBp7JWxLWrIztIdxIt+i3CToReEx5fkyw= - diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/manifest.mf b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/manifest.mf deleted file mode 100644 index e508bcd22f..0000000000 --- a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/manifest.mf +++ /dev/null @@ -1,8 +0,0 @@ -Manifest-Version: 1.0 - -Name: manifest.json -Digest-Algorithms: MD5 SHA1 SHA256 -MD5-Digest: Wzo/k6fhArpFb4UB2hIKlg== -SHA1-Digest: D/WDy9api0X7OgRM6Gkvfbyzogo= -SHA256-Digest: IWBdbytHgPLtCMKKhiZ3jenxKmKiRAhh3ce8iP5AVWU= - diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/mozilla.rsa b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/mozilla.rsa Binary files differdeleted file mode 100644 index a026680e91..0000000000 --- a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/mozilla.rsa +++ /dev/null diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/mozilla.sf b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/mozilla.sf deleted file mode 100644 index 16a1461f37..0000000000 --- a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/mozilla.sf +++ /dev/null @@ -1,5 +0,0 @@ -Signature-Version: 1.0 -MD5-Digest-Manifest: ovtNeIie34gMM5l18zP2MA== -SHA1-Digest-Manifest: c5owdrvcOINxKp/HprYkWXXI/js= -SHA256-Digest-Manifest: uLPmoONlxFYxWeSTOEPJ9hN2yMDDZMJL1PoNIWcqKG4= - diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/manifest.mf b/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/manifest.mf deleted file mode 100644 index eea5cbd501..0000000000 --- a/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/manifest.mf +++ /dev/null @@ -1,8 +0,0 @@ -Manifest-Version: 1.0 - -Name: manifest.json -Digest-Algorithms: MD5 SHA1 SHA256 -MD5-Digest: b4Q2C4GsIJfRLsXc7T2ldQ== -SHA1-Digest: UG5rHxpzKmdlGrquXaguiAGDu8E= -SHA256-Digest: WZrN9SdGBux9t3lV7TVIvyUG/L1px4er2dU3TsBpC4s= - diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/mozilla.rsa b/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/mozilla.rsa Binary files differdeleted file mode 100644 index 68621e19be..0000000000 --- a/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/mozilla.rsa +++ /dev/null diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/mozilla.sf b/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/mozilla.sf deleted file mode 100644 index fe6baa8dac..0000000000 --- a/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/mozilla.sf +++ /dev/null @@ -1,5 +0,0 @@ -Signature-Version: 1.0 -MD5-Digest-Manifest: zqRm8+jxS0iRUGWeArGkXg== -SHA1-Digest-Manifest: pa/31Ll1PYx0dPBQ6C+fd1/wJO4= -SHA256-Digest-Manifest: DJELIyswfwgeL0kaRqogXW2bzUKhn+Pickfv6WHBsW8= - diff --git a/toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/manifest.mf b/toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/manifest.mf deleted file mode 100644 index a8c72c4794..0000000000 --- a/toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/manifest.mf +++ /dev/null @@ -1,12 +0,0 @@ -Manifest-Version: 1.0 - -Name: manifest.json -Digest-Algorithms: MD5 SHA1 -MD5-Digest: Rnoaa6yWePDor5y5/SLFaw== -SHA1-Digest: k51DtKj7bYrwkFJDdmYNDQeUBlA= - -Name: options.html -Digest-Algorithms: MD5 SHA1 -MD5-Digest: vTjxWlRpioEhTZGKTNUqIw== -SHA1-Digest: Y/mr6A34LsvekgRpdhyZRwPF1Vw= - diff --git a/toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/mozilla.rsa b/toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/mozilla.rsa Binary files differdeleted file mode 100644 index 8b6320adda..0000000000 --- a/toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/mozilla.rsa +++ /dev/null diff --git a/toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/mozilla.sf b/toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/mozilla.sf deleted file mode 100644 index ba5fd22caa..0000000000 --- a/toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/mozilla.sf +++ /dev/null @@ -1,4 +0,0 @@ -Signature-Version: 1.0 -MD5-Digest-Manifest: rdmx8VMNzkZ5tRf7tt8G1w== -SHA1-Digest-Manifest: gjtTe8X9Tg46Hz2h4Tru3T02hmE= - diff --git a/toolkit/mozapps/extensions/test/browser/addons/options_signed/manifest.json b/toolkit/mozapps/extensions/test/browser/addons/options_signed/manifest.json deleted file mode 100644 index e808cd5ab6..0000000000 --- a/toolkit/mozapps/extensions/test/browser/addons/options_signed/manifest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "manifest_version": 2, - - "name": "Test options_ui", - "description": "Test add-ons manager handling options_ui with no id in manifest.json", - "version": "1.2", - - "options_ui": { - "page": "options.html" - } -} diff --git a/toolkit/mozapps/extensions/test/browser/addons/options_signed/options.html b/toolkit/mozapps/extensions/test/browser/addons/options_signed/options.html deleted file mode 100644 index ea804601b5..0000000000 --- a/toolkit/mozapps/extensions/test/browser/addons/options_signed/options.html +++ /dev/null @@ -1,9 +0,0 @@ -<!DOCTYPE html> -<html> - <head> - <meta charset="utf-8" /> - </head> - <body> - <div id="options-test-panel" /> - </body> -</html> diff --git a/toolkit/mozapps/extensions/test/browser/browser.toml b/toolkit/mozapps/extensions/test/browser/browser.toml index 0b4a2d8f0a..6556b95584 100644 --- a/toolkit/mozapps/extensions/test/browser/browser.toml +++ b/toolkit/mozapps/extensions/test/browser/browser.toml @@ -8,8 +8,6 @@ support-files = [ "addons/browser_dragdrop_incompat.xpi", "addons/browser_installssl.xpi", "addons/browser_theme.xpi", - "addons/options_signed.xpi", - "addons/options_signed/*", "addon_prefs.xhtml", "discovery/api_response.json", "discovery/api_response_empty.json", @@ -40,7 +38,6 @@ generated-files = [ "addons/browser_dragdrop_incompat.xpi", "addons/browser_installssl.xpi", "addons/browser_theme.xpi", - "addons/options_signed.xpi", ] skip-if = [ diff --git a/toolkit/mozapps/extensions/test/browser/browser_AMBrowserExtensionsImport.js b/toolkit/mozapps/extensions/test/browser/browser_AMBrowserExtensionsImport.js index 654e3cd91e..0d3956f941 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_AMBrowserExtensionsImport.js +++ b/toolkit/mozapps/extensions/test/browser/browser_AMBrowserExtensionsImport.js @@ -43,7 +43,7 @@ const ADDON_SEARCH_RESULTS = {}; const mockAddonRepository = ({ addons = [] }) => { return { - async getMappedAddons(browserID, extensionIDs) { + async getMappedAddons() { return Promise.resolve({ addons, matchedIDs: [], diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug572561.js b/toolkit/mozapps/extensions/test/browser/browser_bug572561.js index 6f8a56bfba..51493570da 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_bug572561.js +++ b/toolkit/mozapps/extensions/test/browser/browser_bug572561.js @@ -17,22 +17,22 @@ var gInstallProperties = [ var gInstall; var gExpectedCancel = false; var gTestInstallListener = { - onInstallStarted(aInstall) { + onInstallStarted() { check_hidden(false); }, - onInstallEnded(aInstall) { + onInstallEnded() { check_hidden(false); run_next_test(); }, - onInstallCancelled(aInstall) { + onInstallCancelled() { ok(gExpectedCancel, "Should expect install cancel"); check_hidden(false); run_next_test(); }, - onInstallFailed(aInstall) { + onInstallFailed() { ok(false, "Did not expect onInstallFailed"); run_next_test(); }, diff --git a/toolkit/mozapps/extensions/test/browser/browser_dragdrop.js b/toolkit/mozapps/extensions/test/browser/browser_dragdrop.js index ae8625a18a..52881b39d1 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_dragdrop.js +++ b/toolkit/mozapps/extensions/test/browser/browser_dragdrop.js @@ -16,7 +16,7 @@ const dragService = Cc["@mozilla.org/widget/dragservice;1"].getService( async function checkInstallConfirmation(...names) { let notificationCount = 0; let observer = { - observe(aSubject, aTopic, aData) { + observe(aSubject) { let installInfo = aSubject.wrappedJSObject; isnot( installInfo.browser, diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_abuse_report.js b/toolkit/mozapps/extensions/test/browser/browser_html_abuse_report.js index 3ad8510aea..f20e5b357b 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_html_abuse_report.js +++ b/toolkit/mozapps/extensions/test/browser/browser_html_abuse_report.js @@ -603,7 +603,7 @@ add_task(async function test_abusereport_messagebars() { await AbuseReportTestUtils.promiseReportRendered(); AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message"); }, - ([submittingDetails, submittedDetails]) => { + ([, submittedDetails]) => { const buttonsL10nId = Array.from( submittedDetails.messagebar.querySelectorAll("button") ).map(el => el.getAttribute("data-l10n-id")); @@ -634,10 +634,8 @@ add_task(async function test_abusereport_messagebars() { await addon.uninstall(true); AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message"); }; - const assertMessageBarDetails = async ([ - submittingDetails, - submittedDetails, - ]) => AbuseReportTestUtils.assertFluentStrings(submittedDetails.messagebar); + const assertMessageBarDetails = async ([, submittedDetails]) => + AbuseReportTestUtils.assertFluentStrings(submittedDetails.messagebar); await assertMessageBars( ["submitting", "submitted-and-removed"], testFn, @@ -657,7 +655,7 @@ add_task(async function test_abusereport_messagebars() { await AbuseReportTestUtils.promiseReportRendered(); AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message"); }, - ([submittingDetails, submittedDetails]) => + ([, submittedDetails]) => AbuseReportTestUtils.assertFluentStrings(submittedDetails.messagebar) ); diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_detail_permissions.js b/toolkit/mozapps/extensions/test/browser/browser_html_detail_permissions.js index 939fe421c3..32543f3bc9 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_html_detail_permissions.js +++ b/toolkit/mozapps/extensions/test/browser/browser_html_detail_permissions.js @@ -613,7 +613,7 @@ add_task(async function testPermissionsViewStates() { let card = getAddonCard(view, addon.id); await Assert.rejects( card.setAddonPermission("webRequest", "permission", "add"), - /permission missing from manifest/, + /was not declared in optional_permissions/, "unable to set the addon permission" ); diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_discover_view.js b/toolkit/mozapps/extensions/test/browser/browser_html_discover_view.js index bc84ffaf89..30cb45dc60 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_html_discover_view.js +++ b/toolkit/mozapps/extensions/test/browser/browser_html_discover_view.js @@ -76,7 +76,7 @@ class DiscoveryAPIHandler { }); } - unblockResponses(responseText) { + unblockResponses() { throw new Error("You need to call blockNextResponses first!"); } diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_list_view_recommendations.js b/toolkit/mozapps/extensions/test/browser/browser_html_list_view_recommendations.js index db4067ab35..bc6ae47a8f 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_html_list_view_recommendations.js +++ b/toolkit/mozapps/extensions/test/browser/browser_html_list_view_recommendations.js @@ -119,7 +119,7 @@ async function installAddon({ card, recommendedList, manifestExtra = {} }) { return extension; } -async function testListRecommendations({ type, manifestExtra = {} }) { +async function testListRecommendations({ type }) { let win = await loadInitialView(type); let doc = win.document; diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_options_ui_in_tab.js b/toolkit/mozapps/extensions/test/browser/browser_html_options_ui_in_tab.js index 68faecfec0..fec3911bcd 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_html_options_ui_in_tab.js +++ b/toolkit/mozapps/extensions/test/browser/browser_html_options_ui_in_tab.js @@ -10,15 +10,19 @@ add_task(async function enableHtmlViews() { }); }); -async function testOptionsInTab({ id, options_ui_options }) { +async function testOptionsInTab({ + id, + options_ui_options = {}, + manifest = { + manifest_version: 2, + options_ui: { page: "options.html", ...options_ui_options }, + }, +}) { let extension = ExtensionTestUtils.loadExtension({ manifest: { name: "Prefs extension", browser_specific_settings: { gecko: { id } }, - options_ui: { - page: "options.html", - ...options_ui_options, - }, + ...manifest, }, background() { browser.test.sendMessage( @@ -84,10 +88,15 @@ async function testOptionsInTab({ id, options_ui_options }) { } add_task(async function testPreferencesLink() { - let id = "prefs@mochi.test"; + let id = "options_ui_open_in_tab@mochi.test"; await testOptionsInTab({ id, options_ui_options: { open_in_tab: true } }); }); +add_task(async function testOptionsPageOpensInNewTab() { + let id = "options_page@mochi.test"; + await testOptionsInTab({ id, manifest: { options_page: "options.html" } }); +}); + add_task(async function testPreferencesInlineDisabled() { await SpecialPowers.pushPrefEnv({ set: [["extensions.htmlaboutaddons.inline-options.enabled", false]], diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_updates.js b/toolkit/mozapps/extensions/test/browser/browser_html_updates.js index 78ffc5678c..757c322829 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_html_updates.js +++ b/toolkit/mozapps/extensions/test/browser/browser_html_updates.js @@ -53,7 +53,7 @@ add_task(async function testChangeAutoUpdates() { let win = await loadInitialView("extension"); let doc = win.document; - let getInputs = updateRow => ({ + let getInputs = () => ({ default: updatesRow.querySelector('input[value="1"]'), on: updatesRow.querySelector('input[value="2"]'), off: updatesRow.querySelector('input[value="0"]'), diff --git a/toolkit/mozapps/extensions/test/browser/browser_installtrigger_install.js b/toolkit/mozapps/extensions/test/browser/browser_installtrigger_install.js index 1d50da2833..b8e8ac80d0 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_installtrigger_install.js +++ b/toolkit/mozapps/extensions/test/browser/browser_installtrigger_install.js @@ -243,6 +243,7 @@ add_task(async function testInstallTriggerFromSubframe() { const testCases = [ ["blank iframe with no attributes", SECURE_TESTROOT, {}, expected.http], + ["iframe srcdoc=''", SECURE_TESTROOT, { srcdoc: "" }, expected.http], // These are blocked by a Firefox doorhanger and the user can't allow it neither. [ @@ -258,12 +259,6 @@ add_task(async function testInstallTriggerFromSubframe() { expected.otherBlockedOnOrigin, ], [ - "iframe srcdoc=''", - SECURE_TESTROOT, - { srcdoc: "" }, - expected.httpBlockedOnOrigin, - ], - [ "blank iframe embedded into a top-level sandbox page", `${SECURE_TESTROOT}sandboxed.html`, {}, diff --git a/toolkit/mozapps/extensions/test/browser/browser_local_install.js b/toolkit/mozapps/extensions/test/browser/browser_local_install.js index 5200b69e39..a26a4e283b 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_local_install.js +++ b/toolkit/mozapps/extensions/test/browser/browser_local_install.js @@ -66,7 +66,7 @@ AddonTestUtils.registerJSON(server, "/updates-now-compatible.json", { add_task(async function test_local_install_blocklisted() { let id = "amosigned-xpi@tests.mozilla.org"; - let version = "2.1"; + let version = "2.2"; await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: [ diff --git a/toolkit/mozapps/extensions/test/browser/browser_page_options_install_addon.js b/toolkit/mozapps/extensions/test/browser/browser_page_options_install_addon.js index 7bc7c08345..586f2b7720 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_page_options_install_addon.js +++ b/toolkit/mozapps/extensions/test/browser/browser_page_options_install_addon.js @@ -10,7 +10,7 @@ MockFilePicker.init(window.browsingContext); async function checkInstallConfirmation(...names) { let notificationCount = 0; let observer = { - observe(aSubject, aTopic, aData) { + observe(aSubject) { var installInfo = aSubject.wrappedJSObject; isnot( installInfo.browser, diff --git a/toolkit/mozapps/extensions/test/browser/browser_shortcuts_duplicate_check.js b/toolkit/mozapps/extensions/test/browser/browser_shortcuts_duplicate_check.js index 912ce8d62f..a8de1f96aa 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_shortcuts_duplicate_check.js +++ b/toolkit/mozapps/extensions/test/browser/browser_shortcuts_duplicate_check.js @@ -235,7 +235,7 @@ add_task(async function testDuplicateShortcutOnMacOSCtrlKey() { ); }; - const clearWarning = async inputEl => { + const clearWarning = async () => { anotherCommandInput.blur(); await TestUtils.waitForCondition( () => BrowserTestUtils.isHidden(errorEl), diff --git a/toolkit/mozapps/extensions/test/browser/browser_sidebar_hidden_categories.js b/toolkit/mozapps/extensions/test/browser/browser_sidebar_hidden_categories.js index 4cb641c2a0..0bb59d7748 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_sidebar_hidden_categories.js +++ b/toolkit/mozapps/extensions/test/browser/browser_sidebar_hidden_categories.js @@ -18,7 +18,7 @@ function installLocale() { return new Promise(resolve => { gInstall = gProvider.createInstalls(gInstallProperties)[0]; gInstall.addTestListener({ - onInstallEnded(aInstall) { + onInstallEnded() { gInstall.removeTestListener(this); resolve(); }, diff --git a/toolkit/mozapps/extensions/test/browser/browser_updatessl.js b/toolkit/mozapps/extensions/test/browser/browser_updatessl.js index 9dbeec4a84..792c24de58 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_updatessl.js +++ b/toolkit/mozapps/extensions/test/browser/browser_updatessl.js @@ -25,14 +25,7 @@ var gStart = 0; var gLast = 0; var HTTPObserver = { - observeActivity( - aChannel, - aType, - aSubtype, - aTimestamp, - aSizeData, - aStringData - ) { + observeActivity(aChannel, aType, aSubtype) { aChannel.QueryInterface(Ci.nsIChannel); dump( diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_install.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_install.js index 24d34c3f4d..a5823517d6 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_webapi_install.js +++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_install.js @@ -17,12 +17,11 @@ const TESTPAGE = `${SECURE_TESTROOT}${TESTPATH}`; const XPI_URL = `${SECURE_TESTROOT}../xpinstall/amosigned.xpi`; const XPI_ADDON_ID = "amosigned-xpi@tests.mozilla.org"; -const XPI_SHA = - "sha256:91121ed2c27f670f2307b9aebdd30979f147318c7fb9111c254c14ddbb84e4b0"; - const ID = "amosigned-xpi@tests.mozilla.org"; -// eh, would be good to just stat the real file instead of this... -const XPI_LEN = 4287; +// Actual XPI file size and hash are computed in the add_setup callback. +let XPI_LEN = -1; +let XPI_SHA = + "sha256:0000000000000000000000000000000000000000000000000000000000000000"; AddonTestUtils.initMochitest(this); @@ -50,6 +49,17 @@ add_setup(async function () { ], }); info("added preferences"); + + // Get the file size (used in this test file to assert the + // expected maxProgress value set in the addon download + // dialog). + const xpiFilePath = getTestFilePath("../xpinstall/amosigned.xpi"); + const xpiStat = await IOUtils.stat(xpiFilePath); + XPI_LEN = xpiStat.size; + + // Compute the file hash. + const xpiFileHash = await IOUtils.computeHexDigest(xpiFilePath, "sha256"); + XPI_SHA = `sha256:${xpiFileHash}`; }); // Wrapper around a common task to run in the content process to test @@ -95,7 +105,7 @@ async function testInstall(browser, args, steps, description) { let receivedEvents = []; let prevEvent = null; events.forEach(event => { - install.addEventListener(event, e => { + install.addEventListener(event, () => { receivedEvents.push({ event, state: install.state, @@ -165,7 +175,7 @@ async function testInstall(browser, args, steps, description) { } } catch (err) { if (!nextStep.expectError) { - throw new Error("Install failed unexpectedly"); + throw new Error("Install failed unexpectedly: " + err); } } } else if (nextStep.action == "cancel") { @@ -287,12 +297,12 @@ add_task( "install with empty string for hash works" ) ); -add_task( - makeRegularTest( +add_task(async function test_install_successfully_with_filehash() { + await makeRegularTest( { url: XPI_URL, addonId, hash: XPI_SHA }, "install with hash works" - ) -); + ); +}); add_task( makeInstallTest(async function (browser) { @@ -531,7 +541,7 @@ add_task( add_task( makeInstallTest(async function (browser) { let id = "amosigned-xpi@tests.mozilla.org"; - let version = "2.1"; + let version = "2.2"; await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: [ diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_theme.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_theme.js index dd1df90907..f271cbd9e3 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_webapi_theme.js +++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_theme.js @@ -13,7 +13,7 @@ add_task(async function test_theme_install() { await BrowserTestUtils.withNewTab(TESTPAGE, async browser => { let updates = []; - function observer(subject, topic, data) { + function observer(subject) { updates.push(JSON.stringify(subject.wrappedJSObject)); } Services.obs.addObserver(observer, "lightweight-theme-styling-update"); diff --git a/toolkit/mozapps/extensions/test/browser/head.js b/toolkit/mozapps/extensions/test/browser/head.js index 482429177c..949b375ec0 100644 --- a/toolkit/mozapps/extensions/test/browser/head.js +++ b/toolkit/mozapps/extensions/test/browser/head.js @@ -204,7 +204,7 @@ function run_next_test() { executeSoon(() => log_exceptions(test)); } -var get_tooltip_info = async function (addonEl, managerWindow) { +var get_tooltip_info = async function (addonEl) { // Extract from title attribute. const { addon } = addonEl; const name = addon.name; @@ -324,7 +324,7 @@ function open_manager( aLongerTimeout, aWin = window ) { - let p = new Promise((resolve, reject) => { + let p = new Promise(resolve => { async function setup_manager(aManagerWindow) { if (aLoadCallback) { log_exceptions(aLoadCallback, aManagerWindow); @@ -354,7 +354,7 @@ function open_manager( } info("Loading manager window in tab"); - Services.obs.addObserver(function observer(aSubject, aTopic, aData) { + Services.obs.addObserver(function observer(aSubject, aTopic) { Services.obs.removeObserver(observer, aTopic); if (aSubject.location.href != MANAGER_URI) { info("Ignoring load event for " + aSubject.location.href); @@ -434,7 +434,7 @@ function wait_for_window_open(aCallback) { ); }, - onCloseWindow(aWindow) {}, + onCloseWindow() {}, }); }); @@ -487,7 +487,7 @@ function promiseAddonsByIDs(aIDs) { */ async function install_addon(path, cb, pathPrefix = TESTROOT) { let install = await AddonManager.getInstallForURL(pathPrefix + path); - let p = new Promise((resolve, reject) => { + let p = new Promise(resolve => { install.addListener({ onInstallEnded: () => resolve(install.addon), }); @@ -946,7 +946,7 @@ MockProvider.prototype = { * true if the newly enabled add-on will only become enabled after a * restart */ - addonChanged: function MP_addonChanged(aId, aType, aPendingRestart) { + addonChanged: function MP_addonChanged() { // Not implemented }, @@ -965,7 +965,7 @@ MockProvider.prototype = { * @param {object} aOptions * Options for the install */ - getInstallForURL: function MP_getInstallForURL(aUrl, aOptions) { + getInstallForURL: function MP_getInstallForURL() { // Not yet implemented }, @@ -975,7 +975,7 @@ MockProvider.prototype = { * @param aFile * The file to be installed */ - getInstallForFile: function MP_getInstallForFile(aFile) { + getInstallForFile: function MP_getInstallForFile() { // Not yet implemented }, @@ -996,7 +996,7 @@ MockProvider.prototype = { * The mimetype to check for * @return true if the mimetype is supported */ - supportsMimetype: function MP_supportsMimetype(aMimetype) { + supportsMimetype: function MP_supportsMimetype() { return false; }, @@ -1007,7 +1007,7 @@ MockProvider.prototype = { * The URI being installed from * @return true if installing is allowed */ - isInstallAllowed: function MP_isInstallAllowed(aUri) { + isInstallAllowed: function MP_isInstallAllowed() { return false; }, }; @@ -1143,11 +1143,11 @@ MockAddon.prototype = { ]); }, - isCompatibleWith(aAppVersion, aPlatformVersion) { + isCompatibleWith() { return true; }, - findUpdates(aListener, aReason, aAppVersion, aPlatformVersion) { + findUpdates() { // Tests can implement this if they need to }, diff --git a/toolkit/mozapps/extensions/test/browser/head_abuse_report.js b/toolkit/mozapps/extensions/test/browser/head_abuse_report.js index f3a683e8d5..78c9206e0a 100644 --- a/toolkit/mozapps/extensions/test/browser/head_abuse_report.js +++ b/toolkit/mozapps/extensions/test/browser/head_abuse_report.js @@ -66,7 +66,7 @@ function waitForNewWindow() { } function waitClosedWindow(win) { - return new Promise((resolve, reject) => { + return new Promise(resolve => { function onWindowClosed() { if (win && !win.closed) { // If a specific window reference has been passed, then check @@ -215,7 +215,7 @@ const AbuseReportTestUtils = { return abuseReportEl.ownerGlobal.ABUSE_REPORT_REASONS[reason]; }, - async promiseReportOpened({ addonId, reportEntryPoint, managerWindow }) { + async promiseReportOpened({ addonId, reportEntryPoint }) { let abuseReportEl; if (!this.getReportDialog()) { diff --git a/toolkit/mozapps/extensions/test/browser/moz.build b/toolkit/mozapps/extensions/test/browser/moz.build index 4cc6314d0e..74095966b1 100644 --- a/toolkit/mozapps/extensions/test/browser/moz.build +++ b/toolkit/mozapps/extensions/test/browser/moz.build @@ -14,7 +14,6 @@ addons = [ "browser_dragdrop_incompat", "browser_installssl", "browser_theme", - "options_signed", ] output_dir = ( diff --git a/toolkit/mozapps/extensions/test/xpcshell/.eslintrc.js b/toolkit/mozapps/extensions/test/xpcshell/.eslintrc.js index 8e3971b385..26a3e6177f 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/.eslintrc.js +++ b/toolkit/mozapps/extensions/test/xpcshell/.eslintrc.js @@ -4,7 +4,7 @@ module.exports = { rules: { "no-unused-vars": [ "error", - { args: "none", varsIgnorePattern: "^end_test$" }, + { argsIgnorePattern: "^_", varsIgnorePattern: "^end_test$" }, ], }, overrides: [ @@ -14,7 +14,7 @@ module.exports = { "no-unused-vars": [ "error", { - args: "none", + argsIgnorePattern: "^_", vars: "local", }, ], diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/langpack_signed.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/langpack_signed.xpi Binary files differindex f60d00348e..ab7db3926e 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/langpack_signed.xpi +++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/langpack_signed.xpi diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long.xpi Binary files differindex f95f3df91e..78124db3ae 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long.xpi +++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long.xpi diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/privileged.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/privileged.xpi Binary files differindex c22acaacd2..111385485b 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/privileged.xpi +++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/privileged.xpi diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed1.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed1.xpi Binary files differindex e2ba7d6fd8..2e9d976a34 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed1.xpi +++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed1.xpi diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed2.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed2.xpi Binary files differindex ccb20796f2..2d6b9c020b 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed2.xpi +++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed2.xpi diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/webext-implicit-id.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/webext-implicit-id.xpi Binary files differindex 6b4abaa691..05a5ea2881 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/data/webext-implicit-id.xpi +++ b/toolkit/mozapps/extensions/test/xpcshell/data/webext-implicit-id.xpi diff --git a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js index 23614cdb2a..2f26d940f0 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js +++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js @@ -935,7 +935,7 @@ class EventChecker { return this.checkAddonEvent("onInstalled", addon); } - onUninstalling(addon, requiresRestart) { + onUninstalling(addon) { return this.checkAddonEvent("onUninstalling", addon); } @@ -1033,7 +1033,7 @@ class EventChecker { }); } - onInstallEnded(install, newAddon) { + onInstallEnded(install) { return this.checkInstall("onInstallEnded", install, { state: "STATE_INSTALLED", error: 0, @@ -1221,3 +1221,10 @@ async function installBuiltinExtension(extensionData, waitForStartup = true) { } return wrapper; } + +function useAMOStageCert() { + // NOTE: add_task internally calls add_test which mutate the add_task properties object, + // and so we should not reuse the same object as add_task options passed to multiple + // add_task calls. + return { pref_set: [["xpinstall.signatures.dev-root", true]] }; +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/head_compat.js b/toolkit/mozapps/extensions/test/xpcshell/head_compat.js index 79ddb8dd3f..76fafee50e 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/head_compat.js +++ b/toolkit/mozapps/extensions/test/xpcshell/head_compat.js @@ -22,7 +22,7 @@ AddonManager.addExternalExtensionLoader({ Object.assign(addon, manifest); return addon; }, - loadScope(addon, file) { + loadScope() { return { install() {}, uninstall() {}, diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_appversion.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_appversion.js index e8d03f088b..08e41b47fd 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_appversion.js +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_appversion.js @@ -227,7 +227,7 @@ function createAddon(addon) { * If a lastTest is provided checks that the notification dialog got passed * the newly blocked items compared to the previous test. */ -async function checkState(test, lastTest, callback) { +async function checkState(test) { let addons = await AddonManager.getAddonsByIDs(ADDONS.map(a => a.id)); const bls = Ci.nsIBlocklistService; diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf.js index 1f6cb3db05..f7e3a57152 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf.js +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf.js @@ -11,11 +11,11 @@ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1"); AddonTestUtils.useRealCertChecks = true; // A real, signed XPI for use in the test. -const SIGNED_ADDON_XPI_FILE = do_get_file("../data/webext-implicit-id.xpi"); -const SIGNED_ADDON_ID = "webext_implicit_id@tests.mozilla.org"; -const SIGNED_ADDON_VERSION = "1.0"; +const SIGNED_ADDON_XPI_FILE = do_get_file("amosigned.xpi"); +const SIGNED_ADDON_ID = "amosigned-xpi@tests.mozilla.org"; +const SIGNED_ADDON_VERSION = "2.2"; const SIGNED_ADDON_KEY = `${SIGNED_ADDON_ID}:${SIGNED_ADDON_VERSION}`; -const SIGNED_ADDON_SIGN_TIME = 1459980789000; // notBefore of certificate. +const SIGNED_ADDON_SIGN_TIME = 1711462525000; // notBefore of certificate. // A real, signed sitepermission XPI for use in the test. const SIGNED_SITEPERM_XPI_FILE = do_get_file("webmidi_permission.xpi"); @@ -78,7 +78,7 @@ add_task(async function signed_xpi_initially_unblocked() { await Blocklist.getAddonBlocklistEntry(addon), { state: Ci.nsIBlocklistService.STATE_BLOCKED, - url: "https://addons.mozilla.org/en-US/xpcshell/blocked-addon/webext_implicit_id@tests.mozilla.org/1.0/", + url: `https://addons.mozilla.org/en-US/xpcshell/blocked-addon/${SIGNED_ADDON_ID}/${SIGNED_ADDON_VERSION}/`, }, "Blocked addon should have blocked entry" ); @@ -174,7 +174,9 @@ add_task(async function signed_temporary() { await Assert.rejects( AddonManager.installTemporaryAddon(SIGNED_ADDON_XPI_FILE), - /Add-on webext_implicit_id@tests.mozilla.org is not compatible with application version/, + new RegExp( + `Add-on ${SIGNED_ADDON_ID} is not compatible with application version` + ), "Blocklisted add-on cannot be installed" ); }); @@ -183,19 +185,27 @@ add_task(async function signed_temporary() { // It can still be blocked by a stash, which is tested in // privileged_addon_blocked_by_stash in test_blocklist_mlbf_stashes.js. add_task(async function privileged_xpi_not_blocked() { + const PRIV_ADDON_ID = "test@tests.mozilla.org"; + const PRIV_ADDON_VERSION = "2.0buildid20240326.152244"; mockMLBF({ - blocked: ["test@tests.mozilla.org:2.0"], + blocked: [`${PRIV_ADDON_ID}:${PRIV_ADDON_VERSION}`], notblocked: [], generationTime: 1546297200000, // 1 jan 2019 = after the cert's notBefore }); await ExtensionBlocklistMLBF._onUpdate(); + // Prevent install to fail due to privileged.xpi version using + // an addon version that hits a manifest warning (see PRIV_ADDON_VERSION). + // TODO(Bug 1824240): remove this once privileged.xpi can be resigned with a + // version format that does not hit a manifest warning. + ExtensionTestUtils.failOnSchemaWarnings(false); const install = await promiseInstallFile( do_get_file("../data/signing_checks/privileged.xpi") ); + ExtensionTestUtils.failOnSchemaWarnings(true); Assert.equal(install.error, 0, "Install should not have an error"); - let addon = await promiseAddonByID("test@tests.mozilla.org"); + let addon = await promiseAddonByID(PRIV_ADDON_ID); Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_PRIVILEGED); Assert.equal(addon.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); await addon.uninstall(); @@ -205,13 +215,17 @@ add_task(async function privileged_xpi_not_blocked() { // It can still be blocked by a stash, which is tested in // langpack_blocked_by_stash in test_blocklist_mlbf_stashes.js. add_task( - // We do not support langpacks on Android. - { skip_if: () => AppConstants.platform == "android" }, + { + // langpack_signed.xpi is signed with AMO staging signature. + pref_set: [["xpinstall.signatures.dev-root", true]], + // We do not support langpacks on Android. + skip_if: () => AppConstants.platform == "android", + }, async function langpack_not_blocked_on_Nightly() { mockMLBF({ blocked: ["langpack-klingon@firefox.mozilla.org:1.0"], notblocked: [], - generationTime: 1546297200000, // 1 jan 2019 = after the cert's notBefore + generationTime: 1712243366640, // 4 apr 2024 = after the cert's notBefore }); await ExtensionBlocklistMLBF._onUpdate(); @@ -226,12 +240,16 @@ add_task( Assert.equal( addon.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED, - "Langpacks cannot be blocked via the MLBF" + "Langpacks cannot be blocked via the MLBF on nightly" ); } else { // On non-Nightly, langpacks are submitted through AMO so we will enforce // the MLBF blocklist for them. - Assert.equal(addon.blocklistState, Ci.nsIBlocklistService.STATE_BLOCKED); + Assert.equal( + addon.blocklistState, + Ci.nsIBlocklistService.STATE_BLOCKED, + "Langpacks can be blocked via the MLBF on non-Nightly channels" + ); } await addon.uninstall(); } diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_update.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_update.js index b98d6e345d..77e5df2540 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_update.js +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_update.js @@ -25,7 +25,7 @@ add_task(async function collapse_multiple_pending_update_requests() { // Add a spy to the RemoteSettings client, so we can verify that the number // of RemoteSettings accesses matches with what we expect. const originalClientGet = ExtensionBlocklistMLBF._client.get; - const spyClientGet = (tag, returnValue) => { + const spyClientGet = tag => { ExtensionBlocklistMLBF._client.get = async function () { // Record the method call. observed.push(tag); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_severities.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_severities.js index fffbb8a51e..059d014197 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_severities.js +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_severities.js @@ -451,7 +451,7 @@ add_task(async function test_pt4() { await promiseRestartManager(); await checkInitialState(); - await loadBlocklist("empty", args => { + await loadBlocklist("empty", () => { dump("Checking notification pt 4\n"); // See note in other callback - we no longer notify for non-blocked add-ons. ok(false, "Should not get a notification as there are no blocked addons."); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklistchange.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklistchange.js index 7383e093ee..b021a13c80 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklistchange.js +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklistchange.js @@ -40,7 +40,7 @@ const useMLBF = Services.prefs.getBoolPref( var testserver = createHttpServer({ hosts: ["example.com"] }); -function permissionPromptHandler(subject, topic, data) { +function permissionPromptHandler(subject) { ok( subject?.wrappedJSObject?.info?.resolve, "Got a permission prompt notification as expected" @@ -280,7 +280,7 @@ if (useMLBF) { } // XXXgijs: according to https://bugzilla.mozilla.org/show_bug.cgi?id=1257565#c111 -// this code and the related code in Blocklist.jsm (specific to XML blocklist) is +// this code and the related code in Blocklist.sys.mjs (specific to XML blocklist) is // dead code and can be removed. See https://bugzilla.mozilla.org/show_bug.cgi?id=1549550 . // // Don't need the full interface, attempts to call other methods will just @@ -343,16 +343,16 @@ function Pload_blocklist(aId) { // Does a background update check for add-ons and returns a promise that // resolves when any started installs complete function Pbackground_update() { - return new Promise((resolve, reject) => { + return new Promise(resolve => { let installCount = 0; let backgroundCheckCompleted = false; AddonManager.addInstallListener({ - onNewInstall(aInstall) { + onNewInstall() { installCount++; }, - onInstallEnded(aInstall) { + onInstallEnded() { installCount--; // Wait until all started installs have completed if (installCount) { diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Device.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Device.js index 9b1d84b77d..68c9357356 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Device.js +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Device.js @@ -63,7 +63,7 @@ async function run_test() { do_test_finished(); } - Services.obs.addObserver(function (aSubject, aTopic, aData) { + Services.obs.addObserver(function () { // If we wait until after we go through the event loop, gfxInfo is sure to // have processed the gfxItems event. executeSoon(checkBlacklist); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_DriverNew.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_DriverNew.js index a1bcde5566..fa274bde2d 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_DriverNew.js +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_DriverNew.js @@ -57,7 +57,7 @@ async function run_test() { do_test_finished(); } - Services.obs.addObserver(function (aSubject, aTopic, aData) { + Services.obs.addObserver(function () { // If we wait until after we go through the event loop, gfxInfo is sure to // have processed the gfxItems event. executeSoon(checkBlacklist); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_DriverNew.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_DriverNew.js index ec74d813ae..a6a1dc1435 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_DriverNew.js +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_DriverNew.js @@ -102,7 +102,7 @@ async function run_test() { do_test_finished(); } - Services.obs.addObserver(function (aSubject, aTopic, aData) { + Services.obs.addObserver(function () { // If we wait until after we go through the event loop, gfxInfo is sure to // have processed the gfxItems event. executeSoon(checkBlacklist); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_DriverOld.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_DriverOld.js index ff887a92eb..93ca432c6d 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_DriverOld.js +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_DriverOld.js @@ -58,7 +58,7 @@ async function run_test() { do_test_finished(); } - Services.obs.addObserver(function (aSubject, aTopic, aData) { + Services.obs.addObserver(function () { // If we wait until after we go through the event loop, gfxInfo is sure to // have processed the gfxItems event. executeSoon(checkBlacklist); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_OK.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_OK.js index 1eef119663..4be9215161 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_OK.js +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_OK.js @@ -58,7 +58,7 @@ async function run_test() { do_test_finished(); } - Services.obs.addObserver(function (aSubject, aTopic, aData) { + Services.obs.addObserver(function () { // If we wait until after we go through the event loop, gfxInfo is sure to // have processed the gfxItems event. executeSoon(checkBlacklist); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_GTE_DriverOld.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_GTE_DriverOld.js index 182c825ffb..d8b082bedc 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_GTE_DriverOld.js +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_GTE_DriverOld.js @@ -58,7 +58,7 @@ async function run_test() { do_test_finished(); } - Services.obs.addObserver(function (aSubject, aTopic, aData) { + Services.obs.addObserver(function () { // If we wait until after we go through the event loop, gfxInfo is sure to // have processed the gfxItems event. executeSoon(checkBlacklist); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_GTE_OK.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_GTE_OK.js index 2cc3686007..0e419ec761 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_GTE_OK.js +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_GTE_OK.js @@ -60,7 +60,7 @@ async function run_test() { do_test_finished(); } - Services.obs.addObserver(function (aSubject, aTopic, aData) { + Services.obs.addObserver(function () { // If we wait until after we go through the event loop, gfxInfo is sure to // have processed the gfxItems event. executeSoon(checkBlacklist); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_No_Comparison.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_No_Comparison.js index 169cdc5e62..4d457cff14 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_No_Comparison.js +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_No_Comparison.js @@ -59,7 +59,7 @@ async function run_test() { do_test_finished(); } - Services.obs.addObserver(function (aSubject, aTopic, aData) { + Services.obs.addObserver(function () { // If we wait until after we go through the event loop, gfxInfo is sure to // have processed the gfxItems event. executeSoon(checkBlacklist); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OK.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OK.js index 04d766e027..991bfa8a96 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OK.js +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OK.js @@ -59,7 +59,7 @@ async function run_test() { do_test_finished(); } - Services.obs.addObserver(function (aSubject, aTopic, aData) { + Services.obs.addObserver(function () { // If we wait until after we go through the event loop, gfxInfo is sure to // have processed the gfxItems event. executeSoon(checkBlacklist); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OS.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OS.js index ce5a61cb75..3f525b48ce 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OS.js +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OS.js @@ -58,7 +58,7 @@ async function run_test() { do_test_finished(); } - Services.obs.addObserver(function (aSubject, aTopic, aData) { + Services.obs.addObserver(function () { // If we wait until after we go through the event loop, gfxInfo is sure to // have processed the gfxItems event. executeSoon(checkBlacklist); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_match.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_match.js index 7a4ec276ee..d9d357be76 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_match.js +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_match.js @@ -60,7 +60,7 @@ async function run_test() { do_test_finished(); } - Services.obs.addObserver(function (aSubject, aTopic, aData) { + Services.obs.addObserver(function () { // If we wait until after we go through the event loop, gfxInfo is sure to // have processed the gfxItems event. executeSoon(checkBlacklist); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_mismatch_DriverVersion.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_mismatch_DriverVersion.js index 61dba8db96..a1c8107f1d 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_mismatch_DriverVersion.js +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_mismatch_DriverVersion.js @@ -60,7 +60,7 @@ async function run_test() { do_test_finished(); } - Services.obs.addObserver(function (aSubject, aTopic, aData) { + Services.obs.addObserver(function () { // If we wait until after we go through the event loop, gfxInfo is sure to // have processed the gfxItems event. executeSoon(checkBlacklist); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_mismatch_OSVersion.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_mismatch_OSVersion.js index 117e2a34ee..be076d98c3 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_mismatch_OSVersion.js +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_mismatch_OSVersion.js @@ -61,7 +61,7 @@ async function run_test() { do_test_finished(); } - Services.obs.addObserver(function (aSubject, aTopic, aData) { + Services.obs.addObserver(function () { // If we wait until after we go through the event loop, gfxInfo is sure to // have processed the gfxItems event. executeSoon(checkBlacklist); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Vendor.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Vendor.js index 37bc0d3c89..e5d137db60 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Vendor.js +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Vendor.js @@ -58,7 +58,7 @@ async function run_test() { do_test_finished(); } - Services.obs.addObserver(function (aSubject, aTopic, aData) { + Services.obs.addObserver(function () { // If we wait until after we go through the event loop, gfxInfo is sure to // have processed the gfxItems event. executeSoon(checkBlacklist); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Version.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Version.js index 9a6a904465..cbd1699d53 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Version.js +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Version.js @@ -180,7 +180,7 @@ async function run_test() { do_test_finished(); } - Services.obs.addObserver(function (aSubject, aTopic, aData) { + Services.obs.addObserver(function () { // If we wait until after we go through the event loop, gfxInfo is sure to // have processed the gfxItems event. executeSoon(checkBlocklist); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_prefs.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_prefs.js index 34e92b0e80..6e5f71b364 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_prefs.js +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_prefs.js @@ -53,7 +53,7 @@ async function run_test() { createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8"); await promiseStartupManager(); - function blacklistAdded(aSubject, aTopic, aData) { + function blacklistAdded() { // If we wait until after we go through the event loop, gfxInfo is sure to // have processed the gfxItems event. executeSoon(ensureBlacklistSet); @@ -95,7 +95,7 @@ async function run_test() { ]); } - function blacklistRemoved(aSubject, aTopic, aData) { + function blacklistRemoved() { // If we wait until after we go through the event loop, gfxInfo is sure to // have processed the gfxItems event. executeSoon(ensureBlacklistUnset); diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/xpcshell.toml b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/xpcshell.toml index 2aee95e952..edb77da1bc 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/xpcshell.toml +++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/xpcshell.toml @@ -5,6 +5,7 @@ firefox-appdir = "browser" support-files = [ "../data/**", "../../xpinstall/webmidi_permission.xpi", + "../../xpinstall/amosigned.xpi", ] ["test_android_blocklist_dump.js"] diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_AbuseReporter.js b/toolkit/mozapps/extensions/test/xpcshell/test_AbuseReporter.js index e5dffe0b00..c94ef29e31 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_AbuseReporter.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_AbuseReporter.js @@ -800,11 +800,7 @@ add_task(async function test_report_recommended() { }); add_task(async function test_query_amo_details() { - async function assertReportOnAMODetails({ - addonId, - addonType = "extension", - expectedReport, - } = {}) { + async function assertReportOnAMODetails({ addonId, expectedReport } = {}) { // Clear last report timestamp and any telemetry event recorded so far. clearAbuseReportState(); Services.telemetry.clearEvents(); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository.js b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository.js index 6f1c99eaa8..9071328680 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository.js @@ -2,7 +2,7 @@ * http://creativecommons.org/publicdomain/zero/1.0/ */ -// Tests AddonRepository.jsm +// Tests AddonRepository.sys.mjs var gServer = createHttpServer({ hosts: ["example.com"] }); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_appIsShuttingDown.js b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_appIsShuttingDown.js index 4f23026ed3..c1f63496a3 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_appIsShuttingDown.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_appIsShuttingDown.js @@ -2,8 +2,8 @@ * http://creativecommons.org/publicdomain/zero/1.0/ */ -// Tests AddonRepository.jsm when backgroundUpdateChecks are hit while the application -// shutdown has been already initiated (See Bug 1841444). +// Tests AddonRepository.sys.mjs when backgroundUpdateChecks are hit while the +// application shutdown has been already initiated (See Bug 1841444). const { sinon } = ChromeUtils.importESModule( "resource://testing-common/Sinon.sys.mjs" diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_cache.js b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_cache.js index 2a1bc2721b..c7b10afa19 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_cache.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_cache.js @@ -465,7 +465,7 @@ function check_cache(aExpectedToFind, aExpectedImmediately) { for (let i = 0; i < REPOSITORY_ADDONS.length; i++) { lookups.push( - new Promise((resolve, reject) => { + new Promise(resolve => { let immediatelyFound = true; let expected = aExpectedToFind[i] ? REPOSITORY_ADDONS[i] : null; // can't Promise-wrap this because we're also testing whether the callback is diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_cache_locale.js b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_cache_locale.js index 37e60e27dd..cf47e388d5 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_cache_locale.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_cache_locale.js @@ -152,7 +152,7 @@ function promiseLocaleChanged(requestedLocale) { } return new Promise(resolve => { let localeObserver = { - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { switch (aTopic) { case REQ_LOC_CHANGE_EVENT: let reqLocs = Services.locale.requestedLocales; @@ -169,7 +169,7 @@ function promiseLocaleChanged(requestedLocale) { function promiseMetaDataUpdate() { return new Promise(resolve => { - let listener = args => { + let listener = () => { Services.prefs.removeObserver(PREF_METADATA_LASTUPDATE, listener); resolve(); }; diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_ProductAddonChecker_signatures.js b/toolkit/mozapps/extensions/test/xpcshell/test_ProductAddonChecker_signatures.js index 5ae61568ef..93602ffac9 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_ProductAddonChecker_signatures.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_ProductAddonChecker_signatures.js @@ -98,7 +98,8 @@ add_task(async function test_valid_content_signature() { signedBaseUri + goodXmlPath + "?" + validSignatureQuery, /*allowNonBuiltIn*/ false, /*allowedCerts*/ false, - /*verifyContentSignature*/ true + /*verifyContentSignature*/ true, + /*trustedContentSignatureRoot*/ Ci.nsIX509CertDB.AppXPCShellRoot ); Assert.ok(true, "Should successfully get addon list"); @@ -122,7 +123,8 @@ add_task(async function test_invalid_content_signature() { signedBaseUri + goodXmlPath + "?" + invalidSignatureQuery, /*allowNonBuiltIn*/ false, /*allowedCerts*/ false, - /*verifyContentSignature*/ true + /*verifyContentSignature*/ true, + /*trustedContentSignatureRoot*/ Ci.nsIX509CertDB.AppXPCShellRoot ); Assert.ok(false, "Should fail to get addon list"); } catch (e) { @@ -143,7 +145,8 @@ add_task(async function test_missing_content_signature_header() { signedBaseUri + goodXmlPath + "?" + missingSignatureQuery, /*allowNonBuiltIn*/ false, /*allowedCerts*/ false, - /*verifyContentSignature*/ true + /*verifyContentSignature*/ true, + /*trustedContentSignatureRoot*/ Ci.nsIX509CertDB.AppXPCShellRoot ); Assert.ok(false, "Should fail to get addon list"); } catch (e) { @@ -165,7 +168,8 @@ add_task(async function test_incomplete_content_signature_header() { signedBaseUri + goodXmlPath + "?" + incompleteSignatureQuery, /*allowNonBuiltIn*/ false, /*allowedCerts*/ false, - /*verifyContentSignature*/ true + /*verifyContentSignature*/ true, + /*trustedContentSignatureRoot*/ Ci.nsIX509CertDB.AppXPCShellRoot ); Assert.ok(false, "Should fail to get addon list"); } catch (e) { @@ -187,7 +191,8 @@ add_task(async function test_bad_x5u_content_signature_header() { signedBaseUri + goodXmlPath + "?" + badX5uSignatureQuery, /*allowNonBuiltIn*/ false, /*allowedCerts*/ false, - /*verifyContentSignature*/ true + /*verifyContentSignature*/ true, + /*trustedContentSignatureRoot*/ Ci.nsIX509CertDB.AppXPCShellRoot ); Assert.ok(false, "Should fail to get addon list"); } catch (e) { diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_addon_manager_telemetry_events.js b/toolkit/mozapps/extensions/test/xpcshell/test_addon_manager_telemetry_events.js index 6a533f540a..d4103420ff 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_addon_manager_telemetry_events.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_addon_manager_telemetry_events.js @@ -40,7 +40,7 @@ function getTelemetryEvents(includeMethods = EVENT_METHODS) { ); return snapshot.parent - .filter(([timestamp, category, method]) => { + .filter(([, category, method]) => { const includeMethod = includeMethods ? includeMethods.includes(method) : true; @@ -68,11 +68,9 @@ function assertNoTelemetryEvents() { return; } - let filteredEvents = snapshot.parent.filter( - ([timestamp, category, method]) => { - return category === EVENT_CATEGORY; - } - ); + let filteredEvents = snapshot.parent.filter(([_timestamp, category]) => { + return category === EVENT_CATEGORY; + }); Assert.deepEqual(filteredEvents, [], "Got no AMTelemetry events as expected"); } diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_delay_update_webextension.js b/toolkit/mozapps/extensions/test/xpcshell/test_delay_update_webextension.js index 7b1c6fbef9..579f99687f 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_delay_update_webextension.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_delay_update_webextension.js @@ -206,7 +206,7 @@ add_task(async function delay_updates_complete() { }, }, background() { - browser.runtime.onUpdateAvailable.addListener(details => { + browser.runtime.onUpdateAvailable.addListener(() => { browser.test.notifyPass("reload"); browser.runtime.reload(); }); @@ -273,7 +273,7 @@ add_task(async function delay_updates_defer() { }, }, background() { - browser.runtime.onUpdateAvailable.addListener(details => { + browser.runtime.onUpdateAvailable.addListener(() => { // Upgrade will only proceed when "allow" message received. browser.test.onMessage.addListener(msg => { if (msg == "allow") { @@ -371,7 +371,7 @@ add_task(async function delay_updates_staged() { }, }, background() { - browser.runtime.onUpdateAvailable.addListener(details => { + browser.runtime.onUpdateAvailable.addListener(() => { browser.test.sendMessage("denied"); }); browser.test.sendMessage("ready"); @@ -443,7 +443,7 @@ add_task(async function delay_updates_staged_no_update_url() { }, }, background() { - browser.runtime.onUpdateAvailable.addListener(details => { + browser.runtime.onUpdateAvailable.addListener(() => { browser.test.sendMessage("denied"); }); browser.test.sendMessage("ready"); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_installOrigins.js b/toolkit/mozapps/extensions/test/xpcshell/test_installOrigins.js index 7ef584b54d..0b1690f5a8 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_installOrigins.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_installOrigins.js @@ -179,7 +179,7 @@ function testInstallEvent(expectTelemetry) { let events = snapshot.parent .filter( - ([timestamp, category, method, object, value, extra]) => + ([, category, method, , , extra]) => category === "addonsManager" && method == "install" && extra.step == expectTelemetry.step @@ -229,15 +229,13 @@ function promiseCompleteWebInstall( installInfo.install(); }); - TestUtils.topicObserved("addon-install-confirmation").then( - (subject, data) => { - info(`==== test got addon-install-confirmation`); - let installInfo = subject.wrappedJSObject; - for (let installer of installInfo.installs) { - installer.install(); - } + TestUtils.topicObserved("addon-install-confirmation").then(subject => { + info(`==== test got addon-install-confirmation`); + let installInfo = subject.wrappedJSObject; + for (let installer of installInfo.installs) { + installer.install(); } - ); + }); TestUtils.topicObserved("webextension-permission-prompt").then( ([subject]) => { const { info } = subject.wrappedJSObject || {}; diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_install_cancel.js b/toolkit/mozapps/extensions/test/xpcshell/test_install_cancel.js index 0be6ec0359..b5902bc6ba 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_install_cancel.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_install_cancel.js @@ -35,7 +35,7 @@ class TestListener { function startListener(listener) { let observer = { - observe(subject, topic, data) { + observe(subject) { let channel = subject.QueryInterface(Ci.nsIHttpChannel); if (channel.URI.spec === "http://example.com/addons/test.xpi") { let channelListener = new TestListener(listener); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_provider_markSafe.js b/toolkit/mozapps/extensions/test/xpcshell/test_provider_markSafe.js index e8062a2caf..033acf3dfc 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_provider_markSafe.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_provider_markSafe.js @@ -12,7 +12,7 @@ function mockAddonProvider(name) { AddonManager.isInstallEnabled("made-up-mimetype"); }, - supportsMimetype(mimetype) { + supportsMimetype() { this.apiAccessed = true; return false; }, diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_provider_shutdown.js b/toolkit/mozapps/extensions/test/xpcshell/test_provider_shutdown.js index 498b28a0c9..47612f1565 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_provider_shutdown.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_provider_shutdown.js @@ -31,7 +31,7 @@ function mockAddonProvider(aName) { mockProvider.doneResolve = resolve; mockProvider.doneReject = reject; }); - mockProvider.shutdownPromise = new Promise((resolve, reject) => { + mockProvider.shutdownPromise = new Promise(resolve => { mockProvider.shutdownResolve = resolve; }); return mockProvider; diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_reload.js b/toolkit/mozapps/extensions/test/xpcshell/test_reload.js index 993c4a9c53..92e67c53c1 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_reload.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_reload.js @@ -99,11 +99,11 @@ add_task(async function test_can_reload_permanent_addon() { let disabledCalled = false; let enabledCalled = false; AddonManager.addAddonListener({ - onDisabled: aAddon => { + onDisabled: () => { Assert.ok(!enabledCalled); disabledCalled = true; }, - onEnabled: aAddon => { + onEnabled: () => { Assert.ok(disabledCalled); enabledCalled = true; }, diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_signed_install.js b/toolkit/mozapps/extensions/test/xpcshell/test_signed_install.js index 065463864d..99b6bca376 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_signed_install.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_signed_install.js @@ -195,25 +195,25 @@ async function test_update_working(file1, file2, expectedSignedState) { await install.addon.uninstall(); } -add_task(async function setup() { +add_setup(async function setup() { createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "4", "4"); await promiseStartupManager(); }); // Try to install a broken add-on -add_task(async function test_install_invalid_modified() { +add_task(useAMOStageCert(), async function test_install_invalid_modified() { let file = createBrokenAddonModify(do_get_file(DATA + ADDONS.signed1)); await test_install_broken(file, AddonManager.ERROR_CORRUPT_FILE); file.remove(true); }); -add_task(async function test_install_invalid_added() { +add_task(useAMOStageCert(), async function test_install_invalid_added() { let file = createBrokenAddonAdd(do_get_file(DATA + ADDONS.signed1)); await test_install_broken(file, AddonManager.ERROR_CORRUPT_FILE); file.remove(true); }); -add_task(async function test_install_invalid_removed() { +add_task(useAMOStageCert(), async function test_install_invalid_removed() { let file = createBrokenAddonRemove(do_get_file(DATA + ADDONS.signed1)); await test_install_broken(file, AddonManager.ERROR_CORRUPT_FILE); file.remove(true); @@ -226,58 +226,62 @@ add_task(async function test_install_invalid_unsigned() { }); // Try to install a signed add-on -add_task(async function test_install_valid() { +add_task(useAMOStageCert(), async function test_install_valid() { let file = do_get_file(DATA + ADDONS.signed1); await test_install_working(file, AddonManager.SIGNEDSTATE_SIGNED); }); add_task( - { - pref_set: [["xpinstall.signatures.dev-root", true]], - // `xpinstall.signatures.dev-root` is not taken into account on release - // builds because `MOZ_REQUIRE_SIGNING` is set to `true`. - skip_if: () => AppConstants.MOZ_REQUIRE_SIGNING, - }, - async function test_install_valid_file_with_different_root_cert() { - const TEST_CASES = [ - { - title: "XPI without ID in manifest", - xpi: "data/webext-implicit-id.xpi", - expectedMessage: - /Cannot find id for addon .+ Preference xpinstall.signatures.dev-root is set/, - }, - { - title: "XPI with ID in manifest", - xpi: DATA + ADDONS.signed1, - expectedMessage: /Add-on test@somewhere.com is not correctly signed/, - }, - ]; + useAMOStageCert(), + async function test_install_implicit_id_with_different_root_cert() { + info( + `test install error on fail to verify signature on XPI without ID in manifest` + ); - for (const { title, xpi, expectedMessage } of TEST_CASES) { - info(`test_install_valid_file_with_different_root_cert: ${title}`); + const xpi = do_get_file("data/webext-implicit-id.xpi"); + const expectedMessage = + /Cannot find id for addon .+ Preference xpinstall.signatures.dev-root is set/; - const file = do_get_file(xpi); + const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => { + await test_install_broken( + xpi, + AddonManager.ERROR_CORRUPT_FILE, + // We don't expect the `addon` property on the `install` object to be + // `null` because that seems to happen later (when the signature is + // checked). + false + ); + }); - const awaitConsole = new Promise(resolve => { - Services.console.registerListener(function listener(message) { - if (expectedMessage.test(message.message)) { - Services.console.unregisterListener(listener); - resolve(); - } - }); - }); + AddonTestUtils.checkMessages(messages, { + expected: [{ message: expectedMessage }], + }); + } +); +add_task( + async function test_install_stage_signed_invalid_with_prod_root_cert() { + info( + `test install error on fail to verify signature on XPI with ID in manifest` + ); + + const xpi = do_get_file(DATA + ADDONS.signed1); + const expectedMessage = /Add-on test@somewhere.com is not correctly signed/; + + const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => { await test_install_broken( - file, + xpi, AddonManager.ERROR_CORRUPT_FILE, // We don't expect the `addon` property on the `install` object to be // `null` because that seems to happen later (when the signature is // checked). false ); + }); - await awaitConsole; - } + AddonTestUtils.checkMessages(messages, { + expected: [{ message: expectedMessage }], + }); } ); @@ -291,25 +295,34 @@ add_task(async function test_install_valid_sha256() { // Try to install an add-on with the "Mozilla Extensions" OU add_task(async function test_install_valid_privileged() { let file = do_get_file(DATA + ADDONS.privileged); - await test_install_working(file, AddonManager.SIGNEDSTATE_PRIVILEGED); + try { + // Prevent install to fail due to privileged.xpi version using + // a version format that hits a manifest warning. + // TODO(Bug 1824240): remove this once privileged.xpi can be resigned with a + // version format that does not hit a manifest warning. + ExtensionTestUtils.failOnSchemaWarnings(false); + await test_install_working(file, AddonManager.SIGNEDSTATE_PRIVILEGED); + } finally { + ExtensionTestUtils.failOnSchemaWarnings(true); + } }); // Try to update to a broken add-on -add_task(async function test_update_invalid_modified() { +add_task(useAMOStageCert(), async function test_update_invalid_modified() { let file1 = do_get_file(DATA + ADDONS.signed1); let file2 = createBrokenAddonModify(do_get_file(DATA + ADDONS.signed2)); await test_update_broken(file1, file2, AddonManager.ERROR_CORRUPT_FILE); file2.remove(true); }); -add_task(async function test_update_invalid_added() { +add_task(useAMOStageCert(), async function test_update_invalid_added() { let file1 = do_get_file(DATA + ADDONS.signed1); let file2 = createBrokenAddonAdd(do_get_file(DATA + ADDONS.signed2)); await test_update_broken(file1, file2, AddonManager.ERROR_CORRUPT_FILE); file2.remove(true); }); -add_task(async function test_update_invalid_removed() { +add_task(useAMOStageCert(), async function test_update_invalid_removed() { let file1 = do_get_file(DATA + ADDONS.signed1); let file2 = createBrokenAddonRemove(do_get_file(DATA + ADDONS.signed2)); await test_update_broken(file1, file2, AddonManager.ERROR_CORRUPT_FILE); @@ -317,7 +330,7 @@ add_task(async function test_update_invalid_removed() { }); // Try to update to an unsigned add-on -add_task(async function test_update_invalid_unsigned() { +add_task(useAMOStageCert(), async function test_update_invalid_unsigned() { let file1 = do_get_file(DATA + ADDONS.signed1); let file2 = do_get_file(DATA + ADDONS.unsigned); await test_update_broken( @@ -328,7 +341,7 @@ add_task(async function test_update_invalid_unsigned() { }); // Try to update to a signed add-on -add_task(async function test_update_valid() { +add_task(useAMOStageCert(), async function test_update_valid() { let file1 = do_get_file(DATA + ADDONS.signed1); let file2 = do_get_file(DATA + ADDONS.signed2); await test_update_working(file1, file2, AddonManager.SIGNEDSTATE_SIGNED); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_signed_langpack.js b/toolkit/mozapps/extensions/test/xpcshell/test_signed_langpack.js index 8ad83b2ecb..421d40f4a0 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_signed_langpack.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_signed_langpack.js @@ -32,7 +32,7 @@ async function installShouldFail(file) { // Test that the preference controlling langpack signing works properly // (and that the general preference for addon signing does not affect // language packs). -add_task(async function () { +add_task(useAMOStageCert(), async function () { AddonTestUtils.useRealCertChecks = true; createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9"); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_signed_long.js b/toolkit/mozapps/extensions/test/xpcshell/test_signed_long.js index 2aa76e8ff8..6213aa0f1b 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_signed_long.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_signed_long.js @@ -4,7 +4,7 @@ const ID = "123456789012345678901234567890123456789012345678901@somewhere.com"; // Tests that signature verification works correctly on an extension with // an ID that does not fit into a certificate CN field. -add_task(async function test_long_id() { +add_task(useAMOStageCert(), async function test_long_id() { createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1"); await promiseStartupManager(); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_signed_verify.js b/toolkit/mozapps/extensions/test/xpcshell/test_signed_verify.js index c17cb941cb..e801485c73 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_signed_verify.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_signed_verify.js @@ -25,9 +25,439 @@ function verifySignatures() { createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "4", "4"); -add_task(async function test_no_change() { +add_setup(async () => { await promiseStartupManager(); +}); + +add_task(function test_hasStrongSignature_helper() { + const { hasStrongSignature } = ChromeUtils.importESModule( + "resource://gre/modules/addons/crypto-utils.sys.mjs" + ); + const { PKCS7_WITH_SHA1, PKCS7_WITH_SHA256, COSE_WITH_SHA256 } = + Ci.nsIAppSignatureInfo; + const testCases = [ + [false, "SHA1 only", [PKCS7_WITH_SHA1]], + [true, "SHA256 only", [PKCS7_WITH_SHA256]], + [true, "COSE only", [COSE_WITH_SHA256]], + [true, "SHA1 and SHA256", [PKCS7_WITH_SHA1, PKCS7_WITH_SHA256]], + [true, "SHA1 and COSE", [PKCS7_WITH_SHA1, COSE_WITH_SHA256]], + [true, "SHA256 and COSE", [PKCS7_WITH_SHA256, COSE_WITH_SHA256]], + ]; + for (const [expect, msg, signedTypes] of testCases) { + Assert.equal(hasStrongSignature({ signedTypes }), expect, msg); + } +}); + +add_task(async function test_addon_signedTypes() { + // This test is allowing weak signatures to run assertions on the AddonWrapper.signedTypes + // property also for extensions only including SHA1 signatures. + const resetWeakSignaturePref = + AddonTestUtils.setWeakSignatureInstallAllowed(true); + + const { PKCS7_WITH_SHA1, COSE_WITH_SHA256 } = Ci.nsIAppSignatureInfo; + + const { addon: addonSignedCOSE } = await promiseInstallFile( + do_get_file("amosigned-mv3-cose.xpi") + ); + const { addon: addonSignedSHA1 } = await promiseInstallFile( + do_get_file("amosigned-sha1only.xpi") + ); + + Assert.deepEqual( + addonSignedCOSE.signedTypes.sort(), + [COSE_WITH_SHA256, PKCS7_WITH_SHA1].sort(), + `Expect ${addonSignedCOSE.id} to be signed with both COSE and SHA1` + ); + + Assert.deepEqual( + addonSignedSHA1.signedTypes, + [PKCS7_WITH_SHA1], + `Expect ${addonSignedSHA1.id} to be signed with SHA1 only` + ); + + await addonSignedSHA1.uninstall(); + await addonSignedCOSE.uninstall(); + + resetWeakSignaturePref(); +}); + +add_task( + async function test_install_error_on_new_install_with_weak_signature() { + // Ensure restrictions on weak signatures are enabled (this should be removed when + // the new behavior is riding the train). + const resetWeakSignaturePref = + AddonTestUtils.setWeakSignatureInstallAllowed(false); + + const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => { + let install = await AddonManager.getInstallForFile( + do_get_file("amosigned-sha1only.xpi") + ); + + await Assert.equal( + install.state, + AddonManager.STATE_DOWNLOAD_FAILED, + "Expect install state to be STATE_DOWNLOAD_FAILED" + ); + + await Assert.rejects( + install.install(), + /Install failed: onDownloadFailed/, + "Expected install to fail" + ); + }); + + resetWeakSignaturePref(); + + // Checking the message expected to be logged in the Browser Console. + AddonTestUtils.checkMessages(messages, { + expected: [ + { + message: + /Invalid XPI: install rejected due to the package not including a strong cryptographic signature/, + }, + ], + }); + } +); + +/** + * Test helper used to simulate an update from a given pre-installed add-on xpi to a new xpi file for the same + * add-on and assert the expected result and logged messages. + * + * @param {object} params + * @param {string} params.currentAddonXPI + * The path to the add-on xpi to be pre-installed and then updated to `newAddonXPI`. + * @param {string} params.newAddonXPI + * The path to the add-on xpi to be installed as an update over `currentAddonXPI`. + * @param {string} params.newAddonVersion + * The add-on version expected for `newAddonXPI`. + * @param {boolean} params.expectedInstallOK + * Set to true for an update scenario that is expected to be successful. + * @param {Array<string|RegExp>} params.expectedMessages + * Array of strings or RegExp for console messages expected to be logged. + * @param {Array<string|RegExp>} params.forbiddenMessages + * Array of strings or RegExp for console messages expected to NOT be logged. + */ +async function testWeakSignatureXPIUpdate({ + currentAddonXPI, + newAddonXPI, + newAddonVersion, + expectedInstallOK, + expectedMessages, + forbiddenMessages, +}) { + // Temporarily allow weak signature to install the xpi as a new install. + let resetWeakSignaturePref = + AddonTestUtils.setWeakSignatureInstallAllowed(true); + + const { addon: addonFirstInstall } = await promiseInstallFile( + currentAddonXPI + ); + const addonId = addonFirstInstall.id; + const initialAddonVersion = addonFirstInstall.version; + + resetWeakSignaturePref(); + + // Make sure the install over is executed while weak signature is not allowed + // for new installs to confirm that installing over is allowed. + resetWeakSignaturePref = AddonTestUtils.setWeakSignatureInstallAllowed(false); + + info("Install over the existing installed addon"); + let addonInstalledOver; + const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => { + const fileURL = Services.io.newFileURI(newAddonXPI).spec; + let install = await AddonManager.getInstallForURL(fileURL, { + existingAddon: addonFirstInstall, + version: newAddonVersion, + }); + + addonInstalledOver = await install.install().catch(err => { + if (expectedInstallOK) { + ok(false, `Unexpected error hit on installing update XPI: ${err}`); + } else { + ok(true, `Install failed as expected: ${err}`); + } + }); + }); + + resetWeakSignaturePref(); + + if (expectedInstallOK) { + Assert.equal( + addonInstalledOver.id, + addonFirstInstall.id, + "Expect addon id to be the same" + ); + Assert.equal( + addonInstalledOver.version, + newAddonVersion, + "Got expected addon version after update xpi install completed" + ); + await addonInstalledOver.uninstall(); + } else { + Assert.equal( + addonInstalledOver?.version, + undefined, + "Expect update addon xpi not installed successfully" + ); + Assert.equal( + (await AddonManager.getAddonByID(addonId)).version, + initialAddonVersion, + "Expect the addon version to match the initial XPI version" + ); + await addonFirstInstall.uninstall(); + } + + Assert.equal( + await AddonManager.getAddonByID(addonId), + undefined, + "Expect the test addon to be fully uninstalled" + ); + + // Checking the message logged in the Browser Console. + AddonTestUtils.checkMessages(messages, { + expected: expectedMessages, + forbidden: forbiddenMessages, + }); +} + +add_task(async function test_weak_install_over_weak_existing() { + const addonId = "amosigned-xpi@tests.mozilla.org"; + await testWeakSignatureXPIUpdate({ + currentAddonXPI: do_get_file("amosigned-sha1only.xpi"), + newAddonXPI: do_get_file("amosigned-sha1only.xpi"), + newAddonVersion: "2.1", + expectedInstallOK: true, + expectedMessages: [ + { + message: new RegExp( + `Allow weak signature install over existing "${addonId}" XPI` + ), + }, + ], + }); +}); + +add_task(async function test_update_weak_to_strong_signature() { + const addonId = "amosigned-xpi@tests.mozilla.org"; + await testWeakSignatureXPIUpdate({ + currentAddonXPI: do_get_file("amosigned-sha1only.xpi"), + newAddonXPI: do_get_file("amosigned.xpi"), + newAddonVersion: "2.2", + expectedInstallOK: true, + forbiddenMessages: [ + { + message: new RegExp( + `Allow weak signature install over existing "${addonId}" XPI` + ), + }, + ], + }); +}); + +add_task(async function test_update_strong_to_weak_signature() { + const addonId = "amosigned-xpi@tests.mozilla.org"; + await testWeakSignatureXPIUpdate({ + currentAddonXPI: do_get_file("amosigned.xpi"), + newAddonXPI: do_get_file("amosigned-sha1only.xpi"), + newAddonVersion: "2.1", + expectedInstallOK: false, + expectedMessages: [ + { + message: new RegExp( + "Invalid XPI: install rejected due to the package not including a strong cryptographic signature" + ), + }, + ], + forbiddenMessages: [ + { + message: new RegExp( + `Allow weak signature install over existing "${addonId}" XPI` + ), + }, + ], + }); +}); + +add_task(async function test_signedTypes_stored_in_addonDB() { + const { addon: addonAfterInstalled } = await promiseInstallFile( + do_get_file("amosigned-mv3-cose.xpi") + ); + const addonId = addonAfterInstalled.id; + + const { PKCS7_WITH_SHA1, COSE_WITH_SHA256 } = Ci.nsIAppSignatureInfo; + const expectedSignedTypes = [COSE_WITH_SHA256, PKCS7_WITH_SHA1].sort(); + + Assert.deepEqual( + addonAfterInstalled.signedTypes.sort(), + expectedSignedTypes, + `Got expected ${addonId} signedTyped after install` + ); + + await promiseRestartManager(); + + const addonAfterAOMRestart = await AddonManager.getAddonByID(addonId); + + Assert.deepEqual( + addonAfterAOMRestart.signedTypes.sort(), + expectedSignedTypes, + `Got expected ${addonId} signedTyped after AOM restart` + ); + + const removeSignedStateFromAddonDB = async () => { + const addon_db_file = Services.dirsvc.get("ProfD", Ci.nsIFile); + addon_db_file.append("extensions.json"); + const addon_db_data = await IOUtils.readJSON(addon_db_file.path); + + const addon_db_data_tampered = { + ...addon_db_data, + addons: addon_db_data.addons.map(addonData => { + // Tamper the data of the test extension to mock the + // scenario. + delete addonData.signedTypes; + return addonData; + }), + }; + await IOUtils.writeJSON(addon_db_file.path, addon_db_data_tampered); + }; + + // Shutdown the AddonManager and tamper the AddonDB to confirm XPIProvider.checkForChanges + // calls originated internally when opening a profile created from a previous Firefox version + // is going to populate the new signedTypes property. + info( + "Check that XPIProvider.checkForChanges(true) will recompute missing signedTypes properties" + ); + await promiseShutdownManager(); + await removeSignedStateFromAddonDB(); + await promiseStartupManager(); + + // Expect the signedTypes property to be undefined because of the + // AddonDB data being tampered earlier in this test. + const addonAfterAppUpgrade = await AddonManager.getAddonByID(addonId); + Assert.deepEqual( + addonAfterAppUpgrade.signedTypes, + undefined, + `Got empty ${addonId} signedTyped set to undefied after AddonDB data tampered` + ); + + // Mock call to XPIDatabase.checkForChanges expected to be hit when the application + // is updated. + AddonTestUtils.getXPIExports().XPIProvider.checkForChanges( + /* aAppChanged */ true + ); + + Assert.deepEqual( + addonAfterAppUpgrade.signedTypes?.sort(), + expectedSignedTypes.sort(), + `Got expected ${addonId} signedTyped after XPIProvider.checkForChanges recomputed it` + ); + + // Shutdown the AddonManager and tamper the AddonDB to confirm XPIDatabase.updateCompatibility + // would populate signedTypes if missing. + info( + "Check that XPIDatabase.updateCompatibility will recompute missing signedTypes properties" + ); + await promiseShutdownManager(); + await removeSignedStateFromAddonDB(); + await promiseStartupManager(); + + // Expect the signedTypes property to be undefined because of the + // AddonDB data being tampered earlier in this test. + const addonAfterUpdateCompatibility = await AddonManager.getAddonByID( + addonId + ); + Assert.deepEqual( + addonAfterUpdateCompatibility.signedTypes, + undefined, + `Got empty ${addonId} signedTyped set to undefied after AddonDB data tampered` + ); + + // Mock call to XPIDatabase.updateCompatibility expected to be originated from + // XPIDatabaseReconcile.processFileChanges and confirm that signedTypes has been + // recomputed as expected. + AddonTestUtils.getXPIExports().XPIDatabaseReconcile.processFileChanges( + {}, + true + ); + Assert.deepEqual( + addonAfterUpdateCompatibility.signedTypes?.sort(), + expectedSignedTypes.sort(), + `Got expected ${addonId} signedTyped after XPIDatabase.updateCompatibility recomputed it` + ); + + // Restart the AddonManager and confirm that XPIDatabase.verifySignatures would not recompute + // the content of the signedTypes array if all values are still the same. + info( + "Check that XPIDatabase.updateCompatibility will recompute missing signedTypes properties" + ); + await promiseRestartManager(); + + let listener = { + onPropertyChanged(_addon, properties) { + Assert.deepEqual( + properties, + [], + `No properties should have been changed for ${_addon.id}` + ); + Assert.ok( + false, + `onPropertyChanged should have not been called for ${_addon.id}` + ); + }, + }; + + AddonManager.addAddonListener(listener); + await verifySignatures(); + AddonManager.removeAddonListener(listener); + + // Shutdown the AddonManager and tamper the AddonDB to set signedTypes to undefined + // then confirm that XPIDatabase.verifySignatures does not hit an exception due to + // signedTypes assumed to always be set to an array. + info( + "Check that XPIDatabase.verifySignatures does not fail when signedTypes is undefined" + ); + await promiseShutdownManager(); + await removeSignedStateFromAddonDB(); + await promiseStartupManager(); + + // Expect the signedTypes property to be undefined because of the + // AddonDB data being tampered earlier in this test. + const addonUndefinedSignedTypes = await AddonManager.getAddonByID(addonId); + Assert.deepEqual( + addonUndefinedSignedTypes.signedTypes, + undefined, + `Got empty ${addonId} signedTyped set to undefied after AddonDB data tampered` + ); + await verifySignatures(); + Assert.deepEqual( + addonUndefinedSignedTypes.signedTypes?.sort(), + expectedSignedTypes.sort(), + `Got expected ${addonId} signedTyped after XPIDatabase.verifySignatures recomputed it` + ); + + await addonUndefinedSignedTypes.uninstall(); +}); + +add_task( + { + pref_set: [["xpinstall.signatures.required", false]], + // Skip this test on builds where disabling xpi signature checks is not supported. + skip_if: () => AppConstants.MOZ_REQUIRE_SIGNING, + }, + async function test_weak_signature_not_restricted_on_disabled_signature_checks() { + // Ensure the restriction on installing xpi using only weak signatures is enabled. + let resetWeakSignaturePref = + AddonTestUtils.setWeakSignatureInstallAllowed(false); + const { addon } = await promiseInstallFile( + do_get_file("amosigned-sha1only.xpi") + ); + Assert.notEqual(addon, null, "Expect addon to be installed"); + resetWeakSignaturePref(); + await addon.uninstall(); + } +); +add_task(useAMOStageCert(), async function test_no_change() { // Install the first add-on await promiseInstallFile(do_get_file(`${DATA}/signed1.xpi`)); @@ -43,7 +473,7 @@ add_task(async function test_no_change() { await manuallyInstall(do_get_file(`${DATA}/signed2.xpi`), profileDir, ID); let listener = { - onPropetyChanged(_addon, properties) { + onPropertyChanged(_addon) { Assert.ok(false, `Got unexpected onPropertyChanged for ${_addon.id}`); }, }; @@ -63,7 +493,7 @@ add_task(async function test_no_change() { AddonManager.removeAddonListener(listener); }); -add_task(async function test_diable() { +add_task(useAMOStageCert(), async function test_disable() { // Install the first add-on await promiseInstallFile(do_get_file(`${DATA}/signed1.xpi`)); @@ -96,7 +526,7 @@ add_task(async function test_diable() { Assert.deepEqual( changedProperties, - ["signedState", "appDisabled"], + ["signedState", "signedTypes", "appDisabled"], "Got onPropertyChanged events for signedState and appDisabled" ); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_delay_update.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_delay_update.js index 4490ec065b..b7dfd0ba72 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_system_delay_update.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_delay_update.js @@ -26,7 +26,7 @@ registerCleanupFunction(() => { distroDir.remove(true); }); -AddonTestUtils.usePrivilegedSignatures = id => "system"; +AddonTestUtils.usePrivilegedSignatures = () => "system"; createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42"); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_repository.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_repository.js index dc1c96e7bc..342410992b 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_system_repository.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_repository.js @@ -29,7 +29,7 @@ add_task(async function test_app_addons() { `http://localhost:${gServer.identity.primaryPort}/get?%IDS%` ); - gServer.registerPathHandler("/get", (request, response) => { + gServer.registerPathHandler("/get", () => { do_throw("Unexpected request to server."); }); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_reset.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_reset.js index 93e4c516fa..37e056c140 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_system_reset.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_reset.js @@ -3,7 +3,7 @@ const updatesDir = FileUtils.getDir("ProfD", ["features"]); -AddonTestUtils.usePrivilegedSignatures = id => "system"; +AddonTestUtils.usePrivilegedSignatures = () => "system"; add_task(async function setup() { // Build the test sets @@ -489,7 +489,7 @@ add_task(async function test_bad_app_cert() { await promiseShutdownManager(); - AddonTestUtils.usePrivilegedSignatures = id => "system"; + AddonTestUtils.usePrivilegedSignatures = () => "system"; }); // A failed upgrade should revert to the default set. diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_update_blank.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_blank.js index 56d10436c7..a60d72f8b3 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_system_update_blank.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_blank.js @@ -6,7 +6,7 @@ let distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "empty"]); distroDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); registerDirectory("XREAppFeat", distroDir); -AddonTestUtils.usePrivilegedSignatures = id => "system"; +AddonTestUtils.usePrivilegedSignatures = () => "system"; add_task(() => initSystemAddonDirs()); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_update_checkSizeHash.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_checkSizeHash.js index f9ac09255a..456164dfae 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_system_update_checkSizeHash.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_checkSizeHash.js @@ -6,7 +6,7 @@ let distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "empty"]); distroDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); registerDirectory("XREAppFeat", distroDir); -AddonTestUtils.usePrivilegedSignatures = id => "system"; +AddonTestUtils.usePrivilegedSignatures = () => "system"; /** * Defines the set of initial conditions to run each test against. Each should diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_update_custom.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_custom.js index b0310d3ceb..22250d6724 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_system_update_custom.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_custom.js @@ -6,7 +6,7 @@ let distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "empty"]); distroDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); registerDirectory("XREAppFeat", distroDir); -AddonTestUtils.usePrivilegedSignatures = id => "system"; +AddonTestUtils.usePrivilegedSignatures = () => "system"; add_task(initSystemAddonDirs); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_update_empty.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_empty.js index 3fae4272be..16c2084842 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_system_update_empty.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_empty.js @@ -6,7 +6,7 @@ let distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "empty"]); distroDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); registerDirectory("XREAppFeat", distroDir); -AddonTestUtils.usePrivilegedSignatures = id => "system"; +AddonTestUtils.usePrivilegedSignatures = () => "system"; add_task(() => initSystemAddonDirs()); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_update_installTelemetryInfo.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_installTelemetryInfo.js index f67894289d..bc690cad71 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_system_update_installTelemetryInfo.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_installTelemetryInfo.js @@ -7,7 +7,7 @@ let distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "empty"]); distroDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); registerDirectory("XREAppFeat", distroDir); -AddonTestUtils.usePrivilegedSignatures = id => "system"; +AddonTestUtils.usePrivilegedSignatures = () => "system"; add_task(() => initSystemAddonDirs()); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_update_newset.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_newset.js index fd93ba5d38..3a1746d28c 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_system_update_newset.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_newset.js @@ -6,7 +6,7 @@ let distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "empty"]); distroDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); registerDirectory("XREAppFeat", distroDir); -AddonTestUtils.usePrivilegedSignatures = id => "system"; +AddonTestUtils.usePrivilegedSignatures = () => "system"; add_task(() => initSystemAddonDirs()); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_update_overlapping.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_overlapping.js index a6c5bc905c..61d3d91300 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_system_update_overlapping.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_overlapping.js @@ -7,7 +7,7 @@ distroDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); registerDirectory("XREAppFeat", distroDir); add_task(() => initSystemAddonDirs()); -AddonTestUtils.usePrivilegedSignatures = id => "system"; +AddonTestUtils.usePrivilegedSignatures = () => "system"; /** * Defines the set of initial conditions to run each test against. Each should diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_update_uninstall_check.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_uninstall_check.js index bf2dd85772..b2daa307db 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_system_update_uninstall_check.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_uninstall_check.js @@ -35,7 +35,7 @@ add_task(async function test_systems_update_uninstall_check() { }, ]); - const listener = (msg, { method, params, reason }) => { + const listener = (msg, { method, params }) => { if (params.id === "system2@tests.mozilla.org" && method === "uninstall") { Assert.ok( false, diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_update_upgrades.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_upgrades.js index d270f33190..8e2d7776ef 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_system_update_upgrades.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_update_upgrades.js @@ -6,7 +6,7 @@ let distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "empty"]); distroDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); registerDirectory("XREAppFeat", distroDir); -AddonTestUtils.usePrivilegedSignatures = id => "system"; +AddonTestUtils.usePrivilegedSignatures = () => "system"; add_task(() => initSystemAddonDirs()); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_upgrades.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_upgrades.js index f02003805c..ddf98c73ec 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_system_upgrades.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_upgrades.js @@ -20,7 +20,7 @@ const systemDefaults = FileUtils.getDir("ProfD", [ systemDefaults.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); registerDirectory("XREAppFeat", systemDefaults); -AddonTestUtils.usePrivilegedSignatures = id => "system"; +AddonTestUtils.usePrivilegedSignatures = () => "system"; const ADDON_ID = "updates@test"; diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_temporary.js b/toolkit/mozapps/extensions/test/xpcshell/test_temporary.js index 80faa57fc1..0bef760261 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_temporary.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_temporary.js @@ -80,7 +80,7 @@ add_task(async function test_new_temporary() { Assert.equal(aInstall.version, "1.0"); installedCalled = true; }, - onInstallStarted: aInstall => { + onInstallStarted: () => { do_throw("onInstallStarted called unexpectedly"); }, }); @@ -416,7 +416,7 @@ add_task(async function test_replace_permanent() { } installedCalled = true; }, - onInstallStarted: aInstall => { + onInstallStarted: () => { do_throw("onInstallStarted called unexpectedly"); }, }); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_update.js b/toolkit/mozapps/extensions/test/xpcshell/test_update.js index 1bd41e8d71..708b9de264 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_update.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_update.js @@ -595,25 +595,22 @@ add_task(async function test_params() { let resultsPromise = new Promise(resolve => { let results = new Map(); - testserver.registerPathHandler( - "/data/param_test.json", - function (request, response) { - let params = new URLSearchParams(request.queryString); - let itemId = params.get("item_id"); - ok( - !results.has(itemId), - `Should not see a duplicate request for item ${itemId}` - ); - - results.set(itemId, params); - - if (results.size === PARAM_IDS.length) { - resolve(results); - } - - request.setStatusLine(null, 500, "Server Error"); + testserver.registerPathHandler("/data/param_test.json", function (request) { + let params = new URLSearchParams(request.queryString); + let itemId = params.get("item_id"); + ok( + !results.has(itemId), + `Should not see a duplicate request for item ${itemId}` + ); + + results.set(itemId, params); + + if (results.size === PARAM_IDS.length) { + resolve(results); } - ); + + request.setStatusLine(null, 500, "Server Error"); + }); }); let addons = await getAddons(PARAM_IDS); @@ -746,11 +743,11 @@ add_task(async function test_no_auto_update() { equal(aInstall.existingAddon.id, "addon1@tests.mozilla.org"); }, - onDownloadFailed(aInstall) { + onDownloadFailed() { ok(false, "Should not have seen onDownloadFailed event"); }, - onDownloadCancelled(aInstall) { + onDownloadCancelled() { ok(false, "Should not have seen onDownloadCancelled event"); }, @@ -764,11 +761,11 @@ add_task(async function test_no_auto_update() { resolve(); }, - onInstallFailed(aInstall) { + onInstallFailed() { ok(false, "Should not have seen onInstallFailed event"); }, - onInstallCancelled(aInstall) { + onInstallCancelled() { ok(false, "Should not have seen onInstallCancelled event"); }, }; diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_updateCancel.js b/toolkit/mozapps/extensions/test/xpcshell/test_updateCancel.js index ac201f434c..bba7a1b77b 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_updateCancel.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_updateCancel.js @@ -27,7 +27,7 @@ function makeCancelListener() { }); return { - onUpdateAvailable(addon, install) { + onUpdateAvailable() { reject("Should not have seen onUpdateAvailable notification"); }, diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_update_addontype.js b/toolkit/mozapps/extensions/test/xpcshell/test_update_addontype.js index ca324cf4ef..221f9e003e 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_update_addontype.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_update_addontype.js @@ -67,7 +67,7 @@ add_task(async function test_update_theme_to_extension() { await Assert.rejects( install.install(), - err => install.error == AddonManager.ERROR_UNEXPECTED_ADDON_TYPE, + () => install.error == AddonManager.ERROR_UNEXPECTED_ADDON_TYPE, "Refusing to change addon type from theme to extension" ); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_update_noSystemAddonUpdate.js b/toolkit/mozapps/extensions/test/xpcshell/test_update_noSystemAddonUpdate.js index f13187ab33..93ff750066 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_update_noSystemAddonUpdate.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_update_noSystemAddonUpdate.js @@ -25,7 +25,7 @@ add_task(async function test_systems_update_uninstall_check() { await setupSystemAddonConditions(initialSetup, distroDir); const testserver = createHttpServer({ hosts: ["example.com"] }); - testserver.registerPathHandler("/update.json", (request, response) => { + testserver.registerPathHandler("/update.json", request => { Assert.ok( !request._queryString.includes("system2@tests.mozilla.org"), "System addon should not request update from normal update process" diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_updateid.js b/toolkit/mozapps/extensions/test/xpcshell/test_updateid.js index c88c8e637b..06dd536b9a 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_updateid.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_updateid.js @@ -74,7 +74,7 @@ add_task(async function test_update_new_id() { await Assert.rejects( install.install(), - err => install.error == AddonManager.ERROR_INCORRECT_ID, + () => install.error == AddonManager.ERROR_INCORRECT_ID, "Upgrade to a different ID fails" ); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_updateversion.js b/toolkit/mozapps/extensions/test/xpcshell/test_updateversion.js index 4d1510c40f..5d6e045f21 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_updateversion.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_updateversion.js @@ -65,7 +65,7 @@ add_task(async function test_update_version_mismatch() { await Assert.rejects( install.install(), - err => install.error == AddonManager.ERROR_UNEXPECTED_ADDON_VERSION, + () => install.error == AddonManager.ERROR_UNEXPECTED_ADDON_VERSION, "Should refuse installation when downloaded version does not match" ); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_webextension.js b/toolkit/mozapps/extensions/test/xpcshell/test_webextension.js index cd4b376117..db5f1d5925 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_webextension.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension.js @@ -316,6 +316,73 @@ add_task(async function test_options_ui() { ); await addon.uninstall(); + + info("Test again with options_page manifest property"); + const ID3 = "options_page_alias@tests.mozilla.org"; + addon = await promiseInstallWebExtension({ + manifest: { + browser_specific_settings: { gecko: { id: ID3 } }, + options_page: "options.html", + }, + }); + + checkAddon(ID3, addon, { + optionsType: AddonManager.OPTIONS_TYPE_TAB, + }); + + ok( + OPTIONS_RE.test(addon.optionsURL), + "Addon should have a moz-extension: options URL for /options.html" + ); + + await addon.uninstall(); + + info("Test options_page and options_page set to a different page"); + + const ID4 = "options_page_warning@tests.mozilla.org"; + addon = await promiseInstallWebExtension({ + manifest: { + browser_specific_settings: { gecko: { id: ID4 } }, + options_page: "options_page.html", + options_ui: { + page: "options.html", + open_in_tab: false, + }, + }, + }); + + checkAddon(ID4, addon, { + optionsType: AddonManager.OPTIONS_TYPE_INLINE_BROWSER, + }); + + ok( + OPTIONS_RE.test(addon.optionsURL), + "Addon should have a moz-extension: options URL for /options.html" + ); + + await addon.uninstall(); + + info("Test options_page and options_page are both set to the same page"); + + const ID5 = "options_page_and_ui_same_page@tests.mozilla.org"; + addon = await promiseInstallWebExtension({ + manifest: { + browser_specific_settings: { gecko: { id: ID5 } }, + options_page: "options.html", + options_ui: { page: "options.html" }, + }, + }); + + checkAddon(ID5, addon, { + optionsType: AddonManager.OPTIONS_TYPE_INLINE_BROWSER, + }); + + ok( + OPTIONS_RE.test(addon.optionsURL), + "Addon should have a moz-extension: options URL for /options.html" + ); + + await addon.uninstall(); }); // Test that experiments permissions add the appropriate dependencies. diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js index 1b2080e8db..913c802609 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js @@ -11,8 +11,10 @@ add_task(async function setup() { await promiseStartupManager(); }); +// Extension without id in the manifest file and signed with AMO prod root +// (https://addons.mozilla.org/en-US/firefox/addon/reference-static-theme/). const IMPLICIT_ID_XPI = "data/webext-implicit-id.xpi"; -const IMPLICIT_ID_ID = "webext_implicit_id@tests.mozilla.org"; +const IMPLICIT_ID_ID = "{46607a7b-1b2a-40ce-9afe-91cda52c46a6}"; // webext-implicit-id.xpi has a minimal manifest with no // applications or browser_specific_settings, so its id comes diff --git a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.toml b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.toml index 6b1cb010d4..e8d3807ab9 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.toml +++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.toml @@ -4,7 +4,12 @@ tags = "addons" head = "head_addons.js" firefox-appdir = "browser" dupe-manifest = true -support-files = ["data/**"] +support-files = [ + "data/**", + "../xpinstall/amosigned.xpi", + "../xpinstall/amosigned-mv3-cose.xpi", + "../xpinstall/amosigned-sha1only.xpi", +] ["test_AMBrowserExtensionsImport.js"] diff --git a/toolkit/mozapps/extensions/test/xpinstall/amosigned-mv3-cose.xpi b/toolkit/mozapps/extensions/test/xpinstall/amosigned-mv3-cose.xpi Binary files differnew file mode 100644 index 0000000000..e09747686b --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/amosigned-mv3-cose.xpi diff --git a/toolkit/mozapps/extensions/test/xpinstall/amosigned-sha1only.xpi b/toolkit/mozapps/extensions/test/xpinstall/amosigned-sha1only.xpi Binary files differnew file mode 100644 index 0000000000..f2948e6994 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpinstall/amosigned-sha1only.xpi diff --git a/toolkit/mozapps/extensions/test/xpinstall/amosigned.xpi b/toolkit/mozapps/extensions/test/xpinstall/amosigned.xpi Binary files differindex f2948e6994..9e895dbbb4 100644 --- a/toolkit/mozapps/extensions/test/xpinstall/amosigned.xpi +++ b/toolkit/mozapps/extensions/test/xpinstall/amosigned.xpi diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser.toml b/toolkit/mozapps/extensions/test/xpinstall/browser.toml index f6ca43982e..f2c73b0a1d 100644 --- a/toolkit/mozapps/extensions/test/xpinstall/browser.toml +++ b/toolkit/mozapps/extensions/test/xpinstall/browser.toml @@ -96,7 +96,7 @@ https_first_disabled = true # Bug 1737265 ["browser_doorhanger_installs.js"] https_first_disabled = true # Bug 1737265 skip-if = [ - "os == 'win' && os_version == '10.0' && bits == 64", #Bug 1615449 + "os == 'win' && os_version == '10.2009' && bits == 64", #Bug 1615449 ] ["browser_empty.js"] diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_auth.js b/toolkit/mozapps/extensions/test/xpinstall/browser_auth.js index 2248af4270..171504049c 100644 --- a/toolkit/mozapps/extensions/test/xpinstall/browser_auth.js +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_auth.js @@ -42,7 +42,7 @@ function get_auth_info() { return ["testuser", "testpass"]; } -function download_failed(install) { +function download_failed() { ok(false, "Install should not have failed"); } diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_auth4.js b/toolkit/mozapps/extensions/test/xpinstall/browser_auth4.js index 46ee2b5cb6..0763d2e55c 100644 --- a/toolkit/mozapps/extensions/test/xpinstall/browser_auth4.js +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_auth4.js @@ -45,7 +45,7 @@ function get_auth_info() { return ["testuser", "testpass"]; } -function download_failed(install) { +function download_failed() { ok(false, "Install should not have failed"); } diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_bug645699.js b/toolkit/mozapps/extensions/test/xpinstall/browser_bug645699.js index 690ac2b3eb..ca05f822b6 100644 --- a/toolkit/mozapps/extensions/test/xpinstall/browser_bug645699.js +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_bug645699.js @@ -54,7 +54,7 @@ function allow_blocked(installInfo) { return false; } -function confirm_install(panel) { +function confirm_install() { ok(false, "Should not see the install dialog"); return false; } diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_bug645699_postDownload.js b/toolkit/mozapps/extensions/test/xpinstall/browser_bug645699_postDownload.js index aa8b948c14..a857d11405 100644 --- a/toolkit/mozapps/extensions/test/xpinstall/browser_bug645699_postDownload.js +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_bug645699_postDownload.js @@ -40,7 +40,7 @@ function allow_blocked(installInfo) { return false; } -function confirm_install(panel) { +function confirm_install() { ok(false, "Should not see the install dialog"); return false; } diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_bug672485.js b/toolkit/mozapps/extensions/test/xpinstall/browser_bug672485.js index 216d543458..2961d327d3 100644 --- a/toolkit/mozapps/extensions/test/xpinstall/browser_bug672485.js +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_bug672485.js @@ -36,7 +36,7 @@ function test() { ); } -function confirm_install(panel) { +function confirm_install() { ok(false, "Should not see the install dialog"); return false; } diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_doorhanger_installs.js b/toolkit/mozapps/extensions/test/xpinstall/browser_doorhanger_installs.js index 01c8089180..073c44eecd 100644 --- a/toolkit/mozapps/extensions/test/xpinstall/browser_doorhanger_installs.js +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_doorhanger_installs.js @@ -52,7 +52,7 @@ async function waitForProgressNotification( let topic = getObserverTopic(notificationId); let observerPromise = new Promise(resolve => { - Services.obs.addObserver(function observer(aSubject, aTopic, aData) { + Services.obs.addObserver(function observer(aSubject, aTopic) { // Ignore the progress notification unless that is the notification we want if ( notificationId != PROGRESS_NOTIFICATION && @@ -208,7 +208,7 @@ async function waitForNotification( let observerPromise; if (aId !== "addon-webext-permissions") { observerPromise = new Promise(resolve => { - Services.obs.addObserver(function observer(aSubject, aTopic, aData) { + Services.obs.addObserver(function observer(aSubject, aTopic) { // Ignore the progress notification unless that is the notification we want if ( aId != PROGRESS_NOTIFICATION && @@ -298,7 +298,7 @@ function acceptInstallDialog(installDialog) { installDialog.button.click(); } -async function waitForSingleNotification(aCallback) { +async function waitForSingleNotification() { while (PopupNotifications.panel.childNodes.length != 1) { await new Promise(resolve => executeSoon(resolve)); @@ -697,27 +697,37 @@ var TESTS = [ let installDialogPromise = waitForInstallDialog(); - let tab = await BrowserTestUtils.openNewForegroundTab( - gBrowser, - TESTROOT + "installtrigger.html?" + triggers - ); + try { + // Prevent install to fail due to privileged.xpi version using + // an addon version that hits a manifest warning (see PRIV_ADDON_VERSION). + // TODO(Bug 1824240): remove this once privileged.xpi can be resigned with a + // version format that does not hit a manifest warning. + ExtensionTestUtils.failOnSchemaWarnings(false); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); - let notificationPromise = acceptAppMenuNotificationWhenShown( - "addon-installed", - "test@tests.mozilla.org", - { incognitoHidden: true } - ); + let notificationPromise = acceptAppMenuNotificationWhenShown( + "addon-installed", + "test@tests.mozilla.org", + { incognitoHidden: true } + ); - (await installDialogPromise).button.click(); - await notificationPromise; + (await installDialogPromise).button.click(); + await notificationPromise; - let installs = await AddonManager.getAllInstalls(); - is(installs.length, 0, "Should be no pending installs"); + let installs = await AddonManager.getAllInstalls(); + is(installs.length, 0, "Should be no pending installs"); - let addon = await AddonManager.getAddonByID("test@tests.mozilla.org"); - await addon.uninstall(); + let addon = await AddonManager.getAddonByID("test@tests.mozilla.org"); + await addon.uninstall(); + + await BrowserTestUtils.removeTab(tab); + } finally { + ExtensionTestUtils.failOnSchemaWarnings(true); + } - await BrowserTestUtils.removeTab(tab); await SpecialPowers.popPrefEnv(); AddonManager.checkUpdateSecurity = true; }, @@ -1477,7 +1487,7 @@ var TESTS = [ var gTestStart = null; var XPInstallObserver = { - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { var installInfo = aSubject.wrappedJSObject; info( "Observed " + aTopic + " for " + installInfo.installs.length + " installs" diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_enabled.js b/toolkit/mozapps/extensions/test/xpinstall/browser_enabled.js index b8ee1b254f..f00d6ca0d8 100644 --- a/toolkit/mozapps/extensions/test/xpinstall/browser_enabled.js +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_enabled.js @@ -51,17 +51,17 @@ add_task(async function test_disabled() { add_task(async function test_disabled2() { let installDisabledCalled = false; - Harness.installDisabledCallback = installInfo => { + Harness.installDisabledCallback = () => { installDisabledCalled = true; ok(true, "Saw installation disabled"); }; - Harness.installBlockedCallback = installInfo => { + Harness.installBlockedCallback = () => { ok(false, "Should never see the blocked install notification"); return false; }; - Harness.installConfirmCallback = panel => { + Harness.installConfirmCallback = () => { ok(false, "Should never see an install confirmation dialog"); return false; }; diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_hash.js b/toolkit/mozapps/extensions/test/xpinstall/browser_hash.js index ab7d21b64e..2563b64dd4 100644 --- a/toolkit/mozapps/extensions/test/xpinstall/browser_hash.js +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_hash.js @@ -1,12 +1,17 @@ // ---------------------------------------------------------------------------- // Test whether an install succeeds when a valid hash is included // This verifies bug 302284 -function test() { +add_task(async function test_install_with_hash() { // This test currently depends on InstallTrigger.install availability. setInstallTriggerPrefs(); - Harness.installEndedCallback = install_ended; - Harness.installsCompletedCallback = finish_test; + const xpiFilePath = getTestFilePath("./amosigned.xpi"); + const xpiFileHash = await IOUtils.computeHexDigest(xpiFilePath, "sha256"); + + const deferredInstallCompleted = Promise.withResolvers(); + + Harness.installEndedCallback = (install, addon) => addon.uninstall(); + Harness.installsCompletedCallback = deferredInstallCompleted.resolve; Harness.setup(); PermissionTestUtils.add( @@ -19,7 +24,7 @@ function test() { JSON.stringify({ "Unsigned XPI": { URL: TESTROOT + "amosigned.xpi", - Hash: "sha1:ee95834ad862245a9ef99ccecc2a857cadc16404", + Hash: `sha256:${xpiFileHash}`, toString() { return this.URL; }, @@ -31,17 +36,13 @@ function test() { gBrowser, TESTROOT + "installtrigger.html?" + triggers ); -} - -function install_ended(install, addon) { - return addon.uninstall(); -} -function finish_test(count) { + info("Wait for the install to be completed"); + const count = await deferredInstallCompleted.promise; is(count, 1, "1 Add-on should have been successfully installed"); PermissionTestUtils.remove("http://example.com", "install"); gBrowser.removeCurrentTab(); Harness.finish(); -} +}); diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_hash2.js b/toolkit/mozapps/extensions/test/xpinstall/browser_hash2.js index 9fd0c66292..e7779792ac 100644 --- a/toolkit/mozapps/extensions/test/xpinstall/browser_hash2.js +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_hash2.js @@ -1,12 +1,17 @@ // ---------------------------------------------------------------------------- // Test whether an install succeeds using case-insensitive hashes // This verifies bug 603021 -function test() { +add_task(async function test_install_hash_case_insensitive() { // This test currently depends on InstallTrigger.install availability. setInstallTriggerPrefs(); - Harness.installEndedCallback = install_ended; - Harness.installsCompletedCallback = finish_test; + const xpiFilePath = getTestFilePath("./amosigned.xpi"); + const xpiFileHash = await IOUtils.computeHexDigest(xpiFilePath, "sha256"); + + const deferredInstallCompleted = Promise.withResolvers(); + + Harness.installEndedCallback = (install, addon) => addon.uninstall(); + Harness.installsCompletedCallback = deferredInstallCompleted.resolve; Harness.setup(); PermissionTestUtils.add( @@ -19,7 +24,7 @@ function test() { JSON.stringify({ "Unsigned XPI": { URL: TESTROOT + "amosigned.xpi", - Hash: "sha1:EE95834AD862245A9EF99CCECC2A857CADC16404", + Hash: `sha256:${xpiFileHash.toUpperCase()}`, toString() { return this.URL; }, @@ -31,17 +36,13 @@ function test() { gBrowser, TESTROOT + "installtrigger.html?" + triggers ); -} - -function install_ended(install, addon) { - return addon.uninstall(); -} -function finish_test(count) { + info("Wait for the install to be completed"); + const count = await deferredInstallCompleted.promise; is(count, 1, "1 Add-on should have been successfully installed"); PermissionTestUtils.remove("http://example.com", "install"); gBrowser.removeCurrentTab(); Harness.finish(); -} +}); diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_httphash.js b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash.js index 1ce8eb55af..6f788e5122 100644 --- a/toolkit/mozapps/extensions/test/xpinstall/browser_httphash.js +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash.js @@ -2,12 +2,21 @@ // Test whether an install succeeds when a valid hash is included in the HTTPS // request // This verifies bug 591070 -function test() { +add_task(async function test_instal_hash_https() { // This test currently depends on InstallTrigger.install availability. setInstallTriggerPrefs(); - Harness.installEndedCallback = install_ended; - Harness.installsCompletedCallback = finish_test; + await SpecialPowers.pushPrefEnv({ + set: [[PREF_INSTALL_REQUIREBUILTINCERTS, false]], + }); + + const xpiFilePath = getTestFilePath("./amosigned.xpi"); + const xpiFileHash = await IOUtils.computeHexDigest(xpiFilePath, "sha256"); + + const deferredInstallCompleted = Promise.withResolvers(); + + Harness.installEndedCallback = (install, addon) => addon.uninstall(); + Harness.installsCompletedCallback = deferredInstallCompleted.resolve; Harness.setup(); PermissionTestUtils.add( @@ -15,13 +24,8 @@ function test() { "install", Services.perms.ALLOW_ACTION ); - Services.prefs.setBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, false); - var url = "https://example.com/browser/" + RELATIVE_DIR + "hashRedirect.sjs"; - url += - "?sha1:ee95834ad862245a9ef99ccecc2a857cadc16404|" + - TESTROOT + - "amosigned.xpi"; + const url = `https://example.com/browser/${RELATIVE_DIR}hashRedirect.sjs?sha256:${xpiFileHash}|${TESTROOT}amosigned.xpi`; var triggers = encodeURIComponent( JSON.stringify({ @@ -38,18 +42,14 @@ function test() { gBrowser, TESTROOT + "installtrigger.html?" + triggers ); -} - -function install_ended(install, addon) { - return addon.uninstall(); -} -function finish_test(count) { + info("Wait for the install to be completed"); + const count = await deferredInstallCompleted.promise; is(count, 1, "1 Add-on should have been successfully installed"); PermissionTestUtils.remove("http://example.com", "install"); - Services.prefs.clearUserPref(PREF_INSTALL_REQUIREBUILTINCERTS); + await SpecialPowers.popPrefEnv(); gBrowser.removeCurrentTab(); Harness.finish(); -} +}); diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_httphash3.js b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash3.js index ffb5a3ddb4..d76ba58135 100644 --- a/toolkit/mozapps/extensions/test/xpinstall/browser_httphash3.js +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash3.js @@ -1,12 +1,25 @@ // ---------------------------------------------------------------------------- // Tests that the HTTPS hash is ignored when InstallTrigger is passed a hash. // This verifies bug 591070 -function test() { +add_task(async function test_installTrigger_hash_override() { // This test currently depends on InstallTrigger.install availability. + // NOTE: this test is covering a feature that we don't support anymore on any + // on the Firefox channels, and so we can remove this test along with + // removing InstallTrigger implementation (even if the InstallTrigger global + // is going to stay defined as null on all channels). setInstallTriggerPrefs(); - Harness.installEndedCallback = install_ended; - Harness.installsCompletedCallback = finish_test; + await SpecialPowers.pushPrefEnv({ + set: [[PREF_INSTALL_REQUIREBUILTINCERTS, false]], + }); + + const xpiFilePath = getTestFilePath("./amosigned.xpi"); + const xpiFileHash = await IOUtils.computeHexDigest(xpiFilePath, "sha256"); + + const deferredInstallCompleted = Promise.withResolvers(); + + Harness.installEndedCallback = (install, addon) => addon.uninstall(); + Harness.installsCompletedCallback = deferredInstallCompleted.resolve; Harness.setup(); PermissionTestUtils.add( @@ -14,7 +27,6 @@ function test() { "install", Services.perms.ALLOW_ACTION ); - Services.prefs.setBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, false); var url = "https://example.com/browser/" + RELATIVE_DIR + "hashRedirect.sjs"; url += "?sha1:foobar|" + TESTROOT + "amosigned.xpi"; @@ -23,7 +35,7 @@ function test() { JSON.stringify({ "Unsigned XPI": { URL: url, - Hash: "sha1:ee95834ad862245a9ef99ccecc2a857cadc16404", + Hash: `sha256:${xpiFileHash}`, toString() { return this.URL; }, @@ -35,18 +47,14 @@ function test() { gBrowser, TESTROOT + "installtrigger.html?" + triggers ); -} - -function install_ended(install, addon) { - return addon.uninstall(); -} -function finish_test(count) { + info("Wait for the install to be completed"); + const count = await deferredInstallCompleted.promise; is(count, 1, "1 Add-on should have been successfully installed"); PermissionTestUtils.remove("http://example.com", "install"); - Services.prefs.clearUserPref(PREF_INSTALL_REQUIREBUILTINCERTS); + await SpecialPowers.popPrefEnv(); gBrowser.removeCurrentTab(); Harness.finish(); -} +}); diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_httphash5.js b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash5.js index 727f13180b..489582dc0a 100644 --- a/toolkit/mozapps/extensions/test/xpinstall/browser_httphash5.js +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash5.js @@ -1,12 +1,21 @@ // ---------------------------------------------------------------------------- // Test that only the first HTTPS hash is used // This verifies bug 591070 -function test() { +add_task(async function test_only_first_https_hash_used() { // This test currently depends on InstallTrigger.install availability. setInstallTriggerPrefs(); - Harness.installEndedCallback = install_ended; - Harness.installsCompletedCallback = finish_test; + await SpecialPowers.pushPrefEnv({ + set: [[PREF_INSTALL_REQUIREBUILTINCERTS, false]], + }); + + const xpiFilePath = getTestFilePath("./amosigned.xpi"); + const xpiFileHash = await IOUtils.computeHexDigest(xpiFilePath, "sha256"); + + const deferredInstallCompleted = Promise.withResolvers(); + + Harness.installEndedCallback = (install, addon) => addon.uninstall(); + Harness.installsCompletedCallback = deferredInstallCompleted.resolve; Harness.setup(); PermissionTestUtils.add( @@ -14,10 +23,9 @@ function test() { "install", Services.perms.ALLOW_ACTION ); - Services.prefs.setBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, false); var url = "https://example.com/browser/" + RELATIVE_DIR + "hashRedirect.sjs"; - url += "?sha1:ee95834ad862245a9ef99ccecc2a857cadc16404|"; + url += `?sha256:${xpiFileHash}|`; url += "https://example.com/browser/" + RELATIVE_DIR + "hashRedirect.sjs"; url += "?sha1:foobar|" + TESTROOT + "amosigned.xpi"; @@ -36,18 +44,14 @@ function test() { gBrowser, TESTROOT + "installtrigger.html?" + triggers ); -} - -function install_ended(install, addon) { - return addon.uninstall(); -} -function finish_test(count) { + info("Wait for the install to be completed"); + const count = await deferredInstallCompleted.promise; is(count, 1, "1 Add-on should have been successfully installed"); PermissionTestUtils.remove("http://example.com", "install"); - Services.prefs.clearUserPref(PREF_INSTALL_REQUIREBUILTINCERTS); + await SpecialPowers.popPrefEnv(); gBrowser.removeCurrentTab(); Harness.finish(); -} +}); diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_localfile3.js b/toolkit/mozapps/extensions/test/xpinstall/browser_localfile3.js index c8d8532ed0..6d4014b7e4 100644 --- a/toolkit/mozapps/extensions/test/xpinstall/browser_localfile3.js +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_localfile3.js @@ -27,7 +27,7 @@ function test() { }); } -function allow_blocked(installInfo) { +function allow_blocked() { ok(true, "Seen blocked"); return false; } diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_localfile4.js b/toolkit/mozapps/extensions/test/xpinstall/browser_localfile4.js index 771832a72b..96052129d4 100644 --- a/toolkit/mozapps/extensions/test/xpinstall/browser_localfile4.js +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_localfile4.js @@ -40,7 +40,7 @@ function test() { ); } -function allow_blocked(installInfo) { +function allow_blocked() { ok(true, "Seen blocked"); return false; } diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_localfile4_postDownload.js b/toolkit/mozapps/extensions/test/xpinstall/browser_localfile4_postDownload.js index 8f8484a1c9..5e1707611d 100644 --- a/toolkit/mozapps/extensions/test/xpinstall/browser_localfile4_postDownload.js +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_localfile4_postDownload.js @@ -39,7 +39,7 @@ function test() { ); } -function allow_blocked(installInfo) { +function allow_blocked() { ok(true, "Seen blocked"); return false; } diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_offline.js b/toolkit/mozapps/extensions/test/xpinstall/browser_offline.js index 946ad1dff7..8ab0194519 100644 --- a/toolkit/mozapps/extensions/test/xpinstall/browser_offline.js +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_offline.js @@ -28,7 +28,7 @@ function test() { ); } -function download_progress(addon, value, maxValue) { +function download_progress() { try { // Tests always connect to localhost, and per bug 87717, localhost is now // reachable in offline mode. To avoid this, disable any proxy. diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger_xorigin.js b/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger_xorigin.js index 7edbf318a0..8b43cc1383 100644 --- a/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger_xorigin.js +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger_xorigin.js @@ -40,7 +40,7 @@ function test() { ); } -function install_blocked(installInfo) { +function install_blocked() { wasOriginBlocked = true; } diff --git a/toolkit/mozapps/extensions/test/xpinstall/head.js b/toolkit/mozapps/extensions/test/xpinstall/head.js index 33ac33c830..a114b6efcd 100644 --- a/toolkit/mozapps/extensions/test/xpinstall/head.js +++ b/toolkit/mozapps/extensions/test/xpinstall/head.js @@ -133,7 +133,6 @@ var Harness = { Services.obs.addObserver(this, "addon-install-failed"); // For browser_auth tests which trigger auth dialogs. - Services.obs.addObserver(this, "tabmodal-dialog-loaded"); Services.obs.addObserver(this, "common-dialog-loaded"); this._boundWin = Cu.getWeakReference(win); // need this so our addon manager listener knows which window to use. @@ -157,7 +156,6 @@ var Harness = { Services.obs.removeObserver(self, "addon-install-blocked"); Services.obs.removeObserver(self, "addon-install-failed"); - Services.obs.removeObserver(self, "tabmodal-dialog-loaded"); Services.obs.removeObserver(self, "common-dialog-loaded"); AddonManager.removeInstallListener(self); @@ -512,7 +510,7 @@ var Harness = { // nsIObserver - observe(subject, topic, data) { + observe(subject, topic) { var installInfo = subject.wrappedJSObject; switch (topic) { case "addon-install-started": @@ -553,11 +551,6 @@ var Harness = { ); }, this); break; - case "tabmodal-dialog-loaded": - let browser = subject.ownerGlobal.gBrowser.selectedBrowser; - let prompt = browser.tabModalPromptBox.getPrompt(subject); - this.promptReady(prompt.Dialog); - break; case "common-dialog-loaded": this.promptReady(subject.Dialog); break; diff --git a/toolkit/mozapps/extensions/test/xpinstall/restartless.xpi b/toolkit/mozapps/extensions/test/xpinstall/restartless.xpi Binary files differdeleted file mode 100644 index 9fee8f60b1..0000000000 --- a/toolkit/mozapps/extensions/test/xpinstall/restartless.xpi +++ /dev/null diff --git a/toolkit/mozapps/extensions/test/xpinstall/triggerredirect.html b/toolkit/mozapps/extensions/test/xpinstall/triggerredirect.html index 1b098d6948..40ebd7f0d8 100644 --- a/toolkit/mozapps/extensions/test/xpinstall/triggerredirect.html +++ b/toolkit/mozapps/extensions/test/xpinstall/triggerredirect.html @@ -10,7 +10,7 @@ <script type="text/javascript"> /* globals InstallTrigger */ /* exported startInstall */ -function installCallback(url, status) { +function installCallback() { document.location = "#foo"; dump("Sending InstallComplete\n"); diff --git a/toolkit/mozapps/installer/packager.mk b/toolkit/mozapps/installer/packager.mk index aeba614e40..6d36d51375 100644 --- a/toolkit/mozapps/installer/packager.mk +++ b/toolkit/mozapps/installer/packager.mk @@ -83,14 +83,14 @@ ifdef ENABLE_MOZSEARCH_PLUGIN cd $(topobjdir)/ && cp _build_manifests/install/dist_include '$(ABS_DIST)/$(PKG_PATH)$(MOZSEARCH_INCLUDEMAP_BASENAME).map' @echo 'Generating mozsearch scip index...' $(RM) $(MOZSEARCH_SCIP_INDEX_BASENAME).zip - cp $(topsrcdir)/.cargo/config.in $(topsrcdir)/.cargo/config + cp $(topsrcdir)/.cargo/config.toml.in $(topsrcdir)/.cargo/config.toml cd $(topsrcdir)/ && \ CARGO=$(MOZ_FETCHES_DIR)/rustc/bin/cargo \ RUSTC=$(MOZ_FETCHES_DIR)/rustc/bin/rustc \ $(MOZ_FETCHES_DIR)/rustc/bin/rust-analyzer scip . && \ zip -r5D '$(ABS_DIST)/$(PKG_PATH)$(MOZSEARCH_SCIP_INDEX_BASENAME).zip' \ index.scip - rm $(topsrcdir)/.cargo/config + rm $(topsrcdir)/.cargo/config.toml ifeq ($(MOZ_BUILD_APP),mobile/android) @echo 'Generating mozsearch java/kotlin semanticdb tarball...' $(RM) $(MOZSEARCH_JAVA_INDEX_BASENAME).zip diff --git a/toolkit/mozapps/macos-frameworks/ChannelPrefs/ChannelPrefs.h b/toolkit/mozapps/macos-frameworks/ChannelPrefs/ChannelPrefs.h new file mode 100644 index 0000000000..670fa2e091 --- /dev/null +++ b/toolkit/mozapps/macos-frameworks/ChannelPrefs/ChannelPrefs.h @@ -0,0 +1,17 @@ +/* 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/. */ + +#ifndef ChannelPrefs_h_ +#define ChannelPrefs_h_ + +#import <Foundation/Foundation.h> + +extern "C" { + +// Returns the channel name, as an autoreleased string. +extern NSString* ChannelPrefsGetChannel(void) __attribute__((weak_import)) +__attribute__((visibility("default"))); +} + +#endif // ChannelPrefs_h_ diff --git a/toolkit/mozapps/macos-frameworks/ChannelPrefs/ChannelPrefs.mm b/toolkit/mozapps/macos-frameworks/ChannelPrefs/ChannelPrefs.mm new file mode 100644 index 0000000000..f437bb857d --- /dev/null +++ b/toolkit/mozapps/macos-frameworks/ChannelPrefs/ChannelPrefs.mm @@ -0,0 +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/. */ + +#include "ChannelPrefs.h" + +#include "mozilla/HelperMacros.h" + +NSString* ChannelPrefsGetChannel() { + return [NSString stringWithCString:MOZ_STRINGIFY(MOZ_UPDATE_CHANNEL) + encoding:NSUTF8StringEncoding]; +} diff --git a/toolkit/mozapps/macos-frameworks/ChannelPrefs/Info.plist b/toolkit/mozapps/macos-frameworks/ChannelPrefs/Info.plist new file mode 100644 index 0000000000..f7ce763d75 --- /dev/null +++ b/toolkit/mozapps/macos-frameworks/ChannelPrefs/Info.plist @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleExecutable</key> + <string>ChannelPrefs</string> + <key>CFBundleIdentifier</key> + <string>org.mozilla.channelprefs</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>ChannelPrefs</string> + <key>CFBundlePackageType</key> + <string>FMWK</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleShortVersionString</key> + <string>1.0</string> + <key>CFBundleVersion</key> + <string>1.0</string> +</dict> +</plist> diff --git a/toolkit/mozapps/macos-frameworks/ChannelPrefs/Makefile.in b/toolkit/mozapps/macos-frameworks/ChannelPrefs/Makefile.in new file mode 100644 index 0000000000..43552a771e --- /dev/null +++ b/toolkit/mozapps/macos-frameworks/ChannelPrefs/Makefile.in @@ -0,0 +1,14 @@ +# vim:set ts=8 sw=8 sts=8 noet: +# 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 $(topsrcdir)/config/rules.mk + +ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT)) +libs:: + rm -rf $(DIST)/bin/ChannelPrefs.framework + + $(NSINSTALL) $(DIST)/bin/ChannelPrefs $(DIST)/bin/ChannelPrefs.framework + $(NSINSTALL) $(srcdir)/Info.plist $(DIST)/bin/ChannelPrefs.framework/Resources +endif diff --git a/toolkit/mozapps/macos-frameworks/ChannelPrefs/README.md b/toolkit/mozapps/macos-frameworks/ChannelPrefs/README.md new file mode 100644 index 0000000000..a6285323f6 --- /dev/null +++ b/toolkit/mozapps/macos-frameworks/ChannelPrefs/README.md @@ -0,0 +1,70 @@ +# ChannelPrefs macOS Framework + +## Summary + +The ChannelPrefs macOS Framework is used to initialize the `app.update.channel` +pref during startup. + +## What is `app.update.channel` and what is it used for? + +`app.update.channel` is used to set the download channel for application +updates. + +## Why do we need a Framework instead of compiling the download channel directly into the executable? + +There are three main use cases that make it necessary for the +`app.update.channel` pref to be set by external means, such as a macOS +Framework: + + 1. Allowing users on the Beta channel to test RC builds + +Beta users have their update channel set to 'beta'. However, RC builds by +definition have their channel set to `release`, since these are release +candidates that could get released to our release population. If we were to +indiscriminately update our Beta users to an RC build, we would lose our entire +Beta population since everyone would get switched to the `release` channel. + + 2. Switching users to another channel, such as ESR + +In contrast to the Beta use case outlined above, there are times where we +explicitly WANT to switch users to a different channel. An example of this is +when hardware or a particular macOS version have reached their EOL. In this +case, we usually switch users to our ESR channel for extended support. + + 3. QA update testing + +QA requires a way to temporarily switch the update channel to a test channel in +order to test updates before new releases. + +## How does the ChannelPrefs macOS Framework address these use cases? + +We are able to accommodate all three use cases above by enabling the updater to +ignore certain files on disk if they were already present, but continue to force +update them if so desired. + +In the case of a Beta user updating to an RC build, the updater would encounter +a ChannelPrefs macOS Framework inside the .app bundle that has an update channel +of `beta`. In this case, the updater will not update the Framework, but update +everything else. This beta user is now able to run the RC build with the update +channel still set to `beta`. + +In the case of switching users to the ESR channel, the updater will be set to +forcefully update the ChannelPrefs macOS Framework, even if already present on +disk. After the update, the user will now be set to the `esr` channel and start +receiving updates through this channel. + +Before releases, QA replaces the ChannelPrefs macOS Framework within the .app +bundle and point the channel at a test channel in order to test updates. During +testing, the new Framework file would remain in place for typical update +testing, but gets replaced in case QA was testing channel switching. + +## Why is a macOS Framework the best solution to store the update channel? + +Apple has started strengthening code signature checks and the requirements on +developers such as ourselves on how their apps are signed. In particular, +most files in the .app bundle are now included in signature verifications. + +A macOS Framework is the ideal solution to store the update channel because +Frameworks are the only component within a .app bundle that can be replaced +without invalidating the code signature on the .app bundle, as long as both the +previous and the new Framework are signed. diff --git a/toolkit/mozapps/macos-frameworks/ChannelPrefs/moz.build b/toolkit/mozapps/macos-frameworks/ChannelPrefs/moz.build new file mode 100644 index 0000000000..13f4186b99 --- /dev/null +++ b/toolkit/mozapps/macos-frameworks/ChannelPrefs/moz.build @@ -0,0 +1,22 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Toolkit", "Application Update") + +Framework("ChannelPrefs") + +EXPORTS += [ + "ChannelPrefs.h", +] + +UNIFIED_SOURCES += [ + "ChannelPrefs.mm", +] + +OS_LIBS += [ + "-framework Foundation", +] diff --git a/toolkit/mozapps/macos-frameworks/ChannelPrefsUtil.h b/toolkit/mozapps/macos-frameworks/ChannelPrefsUtil.h new file mode 100644 index 0000000000..0f2c38632e --- /dev/null +++ b/toolkit/mozapps/macos-frameworks/ChannelPrefsUtil.h @@ -0,0 +1,16 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef ChannelPrefsUtil_h_ +#define ChannelPrefsUtil_h_ + +#include "Units.h" + +class ChannelPrefsUtil { + public: + static bool GetChannelPrefValue(nsACString& aValue); +}; + +#endif // ChannelPrefsUtil_h_ diff --git a/toolkit/mozapps/macos-frameworks/ChannelPrefsUtil.mm b/toolkit/mozapps/macos-frameworks/ChannelPrefsUtil.mm new file mode 100644 index 0000000000..4113b84f5b --- /dev/null +++ b/toolkit/mozapps/macos-frameworks/ChannelPrefsUtil.mm @@ -0,0 +1,26 @@ +/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: + * 2 -*- */ +/* vim: set ts=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/. */ + +#import <ChannelPrefs/ChannelPrefs.h> + +#include "ChannelPrefsUtil.h" + +#include "nsCocoaUtils.h" + +/* static */ +bool ChannelPrefsUtil::GetChannelPrefValue(nsACString& aValue) { + // `ChannelPrefsGetChannel` is resolved at runtime and requires + // the ChannelPrefs framework to be loaded. + if (ChannelPrefsGetChannel) { + nsAutoString value; + nsCocoaUtils::GetStringForNSString(ChannelPrefsGetChannel(), value); + CopyUTF16toUTF8(value, aValue); + return true; + } + + return false; +} diff --git a/toolkit/components/corroborator/moz.build b/toolkit/mozapps/macos-frameworks/moz.build index 6db15ef997..b1a315500c 100644 --- a/toolkit/components/corroborator/moz.build +++ b/toolkit/mozapps/macos-frameworks/moz.build @@ -4,14 +4,19 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. - with Files("**"): - BUG_COMPONENT = ("Toolkit", "General") + BUG_COMPONENT = ("Toolkit", "Application Update") + +FINAL_LIBRARY = "xul" + +DIRS += [ + "ChannelPrefs", +] -EXTRA_JS_MODULES += [ - "Corroborate.sys.mjs", +EXPORTS += [ + "ChannelPrefsUtil.h", ] -XPCSHELL_TESTS_MANIFESTS += [ - "test/xpcshell/xpcshell.toml", +UNIFIED_SOURCES += [ + "ChannelPrefsUtil.mm", ] diff --git a/toolkit/mozapps/update/common/readstrings.cpp b/toolkit/mozapps/update/common/readstrings.cpp index 17c2d002a1..28dc8ea6ff 100644 --- a/toolkit/mozapps/update/common/readstrings.cpp +++ b/toolkit/mozapps/update/common/readstrings.cpp @@ -38,16 +38,6 @@ class AutoFILE { FILE* fp_; }; -class AutoCharArray { - public: - explicit AutoCharArray(size_t len) { ptr_ = new char[len]; } - ~AutoCharArray() { delete[] ptr_; } - operator char*() { return ptr_; } - - private: - char* ptr_; -}; - static const char kNL[] = "\r\n"; static const char kEquals[] = "="; static const char kWhitespace[] = " \t"; @@ -153,7 +143,8 @@ int ReadStrings(const NS_tchar* path, const char* keyList, } size_t flen = size_t(len); - AutoCharArray fileContents(flen + 1); + + char* fileContents = new char[flen + 1]; if (!fileContents) { return READ_STRINGS_MEM_ERROR; } @@ -170,12 +161,53 @@ int ReadStrings(const NS_tchar* path, const char* keyList, fileContents[flen] = '\0'; - char* buffer = fileContents; + int result = ReadStringsFromBuffer(fileContents, keyList, numStrings, results, + section); + delete[] fileContents; + return result; +} + +// A wrapper function to read strings for the updater. +// Added for compatibility with the original code. +int ReadStrings(const NS_tchar* path, StringTable* results) { + const unsigned int kNumStrings = 2; + const char* kUpdaterKeys = "Title\0Info\0"; + mozilla::UniquePtr<char[]> updater_strings[kNumStrings]; + + int result = ReadStrings(path, kUpdaterKeys, kNumStrings, updater_strings); + + if (result == OK) { + results->title.swap(updater_strings[0]); + results->info.swap(updater_strings[1]); + } + + return result; +} + +/** + * A very basic parser for updater.ini taken mostly from nsINIParser.cpp + * that can be used by standalone apps. + * + * @param stringBuffer The string buffer to parse + * @param keyList List of zero-delimited keys ending with two zero + * characters + * @param numStrings Number of strings to read into results buffer - must be + * equal to the number of keys + * @param results Array of strings. Array's length must be equal to + * numStrings. Each string will be populated with the value + * corresponding to the key with the same index in keyList. + * @param section Optional name of the section to read; defaults to + * "Strings" + */ +int ReadStringsFromBuffer(char* stringBuffer, const char* keyList, + unsigned int numStrings, + mozilla::UniquePtr<char[]>* results, + const char* section) { bool inStringsSection = false; unsigned int read = 0; - while (char* token = NS_strtok(kNL, &buffer)) { + while (char* token = NS_strtok(kNL, &stringBuffer)) { if (token[0] == '#' || token[0] == ';') { // it's a comment continue; } @@ -233,23 +265,6 @@ int ReadStrings(const NS_tchar* path, const char* keyList, return (read == numStrings) ? OK : PARSE_ERROR; } -// A wrapper function to read strings for the updater. -// Added for compatibility with the original code. -int ReadStrings(const NS_tchar* path, StringTable* results) { - const unsigned int kNumStrings = 2; - const char* kUpdaterKeys = "Title\0Info\0"; - mozilla::UniquePtr<char[]> updater_strings[kNumStrings]; - - int result = ReadStrings(path, kUpdaterKeys, kNumStrings, updater_strings); - - if (result == OK) { - results->title.swap(updater_strings[0]); - results->info.swap(updater_strings[1]); - } - - return result; -} - IniReader::IniReader(const NS_tchar* iniPath, const char* section /* = nullptr */) { if (iniPath) { diff --git a/toolkit/mozapps/update/common/readstrings.h b/toolkit/mozapps/update/common/readstrings.h index 9e0ebbefb5..8fa8a49756 100644 --- a/toolkit/mozapps/update/common/readstrings.h +++ b/toolkit/mozapps/update/common/readstrings.h @@ -24,6 +24,15 @@ struct StringTable { mozilla::UniquePtr<char[]> info; }; +struct MARChannelStringTable { + MARChannelStringTable() { + MARChannelID = mozilla::MakeUnique<char[]>(1); + MARChannelID[0] = '\0'; + } + + mozilla::UniquePtr<char[]> MARChannelID; +}; + /** * This function reads in localized strings from updater.ini */ @@ -38,6 +47,15 @@ int ReadStrings(const NS_tchar* path, const char* keyList, const char* section = nullptr); /** + * This function reads in localized strings corresponding to the keys from a + * given string buffer. + */ +int ReadStringsFromBuffer(char* stringBuffer, const char* keyList, + unsigned int numStrings, + mozilla::UniquePtr<char[]>* results, + const char* section = nullptr); + +/** * This class is meant to be a slightly cleaner interface into the ReadStrings * function. */ diff --git a/toolkit/mozapps/update/nsIUpdateService.idl b/toolkit/mozapps/update/nsIUpdateService.idl index ab1712587c..972d0f4f55 100644 --- a/toolkit/mozapps/update/nsIUpdateService.idl +++ b/toolkit/mozapps/update/nsIUpdateService.idl @@ -380,7 +380,7 @@ interface nsIApplicationUpdateService : nsISupports * check starting does not necessarily mean that the check will * succeed or that an update will be downloaded. */ - bool checkForBackgroundUpdates(); + boolean checkForBackgroundUpdates(); /** * Selects the best update to install from a list of available updates. @@ -654,7 +654,7 @@ interface nsIUpdateProcessor : nsISupports * @throws NS_ERROR_NOT_IMPLEMENTED * If this is called on a non-Windows platform. */ - bool getServiceRegKeyExists(); + boolean getServiceRegKeyExists(); /** * Attempts to restart the application manually on program exit with the same @@ -724,7 +724,7 @@ interface nsIUpdateSyncManager : nsISupports * Returns whether another instance of this application is running. * @returns true if another instance has the lock open, false if not */ - bool isOtherInstanceRunning(); + boolean isOtherInstanceRunning(); /** * Should only be used for testing. diff --git a/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_multiUpdate.js b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_multiUpdate.js index 07e7bf51fa..0ca510cfcf 100644 --- a/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_multiUpdate.js +++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_multiUpdate.js @@ -64,7 +64,7 @@ add_task(async function doorhanger_bc_multiUpdate() { is( PanelUI.menuButton.getAttribute("badge-status"), - "", + null, "Should not have restart badge during staging" ); diff --git a/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_multiUpdate_promptWaitTime.js b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_multiUpdate_promptWaitTime.js index 00c61bcbb4..8012250f36 100644 --- a/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_multiUpdate_promptWaitTime.js +++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_multiUpdate_promptWaitTime.js @@ -74,7 +74,7 @@ add_task(async function doorhanger_bc_multiUpdate() { is( PanelUI.menuButton.getAttribute("badge-status"), - "", + null, "Should not have restart badge during staging" ); diff --git a/toolkit/mozapps/update/tests/browser/head.js b/toolkit/mozapps/update/tests/browser/head.js index c5acdad8e4..b606331fd3 100644 --- a/toolkit/mozapps/update/tests/browser/head.js +++ b/toolkit/mozapps/update/tests/browser/head.js @@ -368,9 +368,15 @@ function copyTestUpdater(attempt = 0) { testUpdater.copyToFollowingLinks(greBinDir, FILE_UPDATER_BIN); let greDir = getGREDir(); - let updateSettingsIni = greDir.clone(); - updateSettingsIni.append(FILE_UPDATE_SETTINGS_INI); - writeFile(updateSettingsIni, UPDATE_SETTINGS_CONTENTS); + + // On macOS, update settings is a Framework, not an INI. This was already + // built into updater-xpcshell using the `UpdateSettings-xpcshell` + // Framework, so we don't need to do any additional work here. + if (AppConstants.platform != "macosx") { + let updateSettingsIni = greDir.clone(); + updateSettingsIni.append(FILE_UPDATE_SETTINGS_INI); + writeFile(updateSettingsIni, UPDATE_SETTINGS_CONTENTS); + } let precomplete = greDir.clone(); precomplete.append(FILE_PRECOMPLETE); diff --git a/toolkit/mozapps/update/tests/data/README.md b/toolkit/mozapps/update/tests/data/README.md new file mode 100644 index 0000000000..52a7e9a5b7 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/README.md @@ -0,0 +1,15 @@ +README +====== + +I needed to make changes to some of the test MARs in this directory and it was a bit difficult to figure out how, so I want to document what I did so that this is easier next time. In my specific case, I wanted to rename some files in several MARs. These are approximately the steps I used to make changes to `partial_mac.mar`: + + - `./mach build` so that `<obj>/dist/bin/signmar` is available. + - We want use the [Python MAR tool](https://github.com/mozilla-releng/build-mar) to build the replacement MAR, but it currently has a problem that we need to fix before we can build a MAR that will allow tests to pass on Apple silicon. Once [this issue](https://github.com/mozilla-releng/build-mar/issues/63) is addressed, we can just use `pip install mar`. Until then, we need to checkout the project and tweak it before we use it. First clone the repository: `cd ~ && git clone https://github.com/mozilla-releng/build-mar`. Next, we want to remove [this line](https://github.com/mozilla-releng/build-mar/blob/2d37c446015b97d6f700fef5766a49609bdc22ea/src/mardor/utils.py#L158). Then make the tweaked version available with `cd ~/build-mar && virtualenv mardor && source mardor/bin/activate && pip install .`. + - Made a temporary working directory: `cd /path/to/mozilla/repo && mkdir temp && cd temp` + - Extracted the MAR that I wanted to change: `mar -J -x ../toolkit/mozapps/update/tests/data/partial_mac.mar`. The `-J` specifies a compression type of `xz`. You can also specify `--auto` to automatically detect the compression type (though you may want to know the original compression later for recompression) or you can check the compression type by running `mar -T ../toolkit/mozapps/update/tests/data/partial_mac.mar` and looking for the `Compression type:` line. + - Made the changes that I wanted to make to the extracted files. This included moving the old files to their new locations and updating those paths in `updatev2.manifest` and `updatev3.manifest`. + - Run `mar -T ../toolkit/mozapps/update/tests/data/partial_mac.mar` to get a complete list of the files originally in that MAR as well as the product/version and channel strings (in this case `xpcshell-test` and `*` respectively). + - Create the new MAR: `mar -J -c partial_mac_unsigned.mar -V '*' -H xpcshell-test <file1> <file2> ...`, individually specifying each file path listed in by `mar -T`, substituting with renamed paths as necessary. + - I had a bit of trouble figuring out the signing. Eventually I discovered the following things: (a) The test MAR signatures successfully verify against `toolkit/mozapps/update/updater/xpcshellCertificate.der` and (b) according to [this comment](https://searchfox.org/mozilla-central/rev/fc00627e34639ef1014e87d9fa24091905e9dc5d/toolkit/mozapps/update/updater/moz.build#41-43), that certificate was generated from `mycert` in `modules/libmar/tests/unit/data`. Thus, I signed the MAR like this: `../<obj>/dist/bin/signmar -d ../modules/libmar/tests/unit/data -n mycert -s partial_mac_unsigned.mar partial_mac.mar`. + - I wanted to do some verification to make sure that the new MAR looked right. First I verified the signature: `../<obj>/dist/bin/signmar -D ../toolkit/mozapps/update/updater/xpcshellCertificate.der -v partial_mac.mar`. This appears to output nothing on success, but it's probably good to check to make sure `echo $?` is `0`. I also compared the output of `mar -T partial_mac.mar` to that of the original. I saw a few unexpected size changes which I believe are likely due to slight differences in the compression used (maybe the algorithm changed slightly since this was last generated?). To make sure that these did not correspond to effective changes, I extracted the new MAR with `mkdir cmp && cd cmp && mar -J -x ../partial_mac.mar && cd ..` and compared the resulting files to make sure they had the expected contents. + - Overwrite the original MAR with the new one and remove the `temp` directory: `cd .. && mv -f temp/partial_mac.mar toolkit/mozapps/update/tests/data/partial_mac.mar && rm -rf temp` diff --git a/toolkit/mozapps/update/tests/data/complete_mac.mar b/toolkit/mozapps/update/tests/data/complete_mac.mar Binary files differindex c54088610a..f4603f1eef 100644 --- a/toolkit/mozapps/update/tests/data/complete_mac.mar +++ b/toolkit/mozapps/update/tests/data/complete_mac.mar diff --git a/toolkit/mozapps/update/tests/data/partial_mac.mar b/toolkit/mozapps/update/tests/data/partial_mac.mar Binary files differindex bcc04b9939..9b17747e43 100644 --- a/toolkit/mozapps/update/tests/data/partial_mac.mar +++ b/toolkit/mozapps/update/tests/data/partial_mac.mar diff --git a/toolkit/mozapps/update/tests/data/shared.js b/toolkit/mozapps/update/tests/data/shared.js index 60de6feeb8..e8553f7273 100644 --- a/toolkit/mozapps/update/tests/data/shared.js +++ b/toolkit/mozapps/update/tests/data/shared.js @@ -71,17 +71,20 @@ const FILE_ACTIVE_UPDATE_XML = "active-update.xml"; const FILE_ACTIVE_UPDATE_XML_TMP = "active-update.xml.tmp"; const FILE_APPLICATION_INI = "application.ini"; const FILE_BACKUP_UPDATE_CONFIG_JSON = "backup-update-config.json"; -const FILE_BACKUP_UPDATE_LOG = "backup-update.log"; const FILE_BACKUP_UPDATE_ELEVATED_LOG = "backup-update-elevated.log"; +const FILE_BACKUP_UPDATE_LOG = "backup-update.log"; const FILE_BT_RESULT = "bt.result"; -const FILE_LAST_UPDATE_LOG = "last-update.log"; +const FILE_CHANNEL_PREFS = + AppConstants.platform == "macosx" ? "ChannelPrefs" : "channel-prefs.js"; const FILE_LAST_UPDATE_ELEVATED_LOG = "last-update-elevated.log"; +const FILE_LAST_UPDATE_LOG = "last-update.log"; const FILE_PRECOMPLETE = "precomplete"; const FILE_PRECOMPLETE_BAK = "precomplete.bak"; const FILE_UPDATE_CONFIG_JSON = "update-config.json"; -const FILE_UPDATE_LOG = "update.log"; const FILE_UPDATE_ELEVATED_LOG = "update-elevated.log"; +const FILE_UPDATE_LOG = "update.log"; const FILE_UPDATE_MAR = "update.mar"; +const FILE_UPDATE_SETTINGS_FRAMEWORK = "UpdateSettings"; const FILE_UPDATE_SETTINGS_INI = "update-settings.ini"; const FILE_UPDATE_SETTINGS_INI_BAK = "update-settings.ini.bak"; const FILE_UPDATE_STATUS = "update.status"; @@ -223,6 +226,7 @@ function setUpdateChannel(aChannel) { debugDump( "setting default pref " + PREF_APP_UPDATE_CHANNEL + " to " + gChannel ); + gDefaultPrefBranch.unlockPref(PREF_APP_UPDATE_CHANNEL); gDefaultPrefBranch.setCharPref(PREF_APP_UPDATE_CHANNEL, gChannel); gPrefRoot.addObserver(PREF_APP_UPDATE_CHANNEL, observer); } diff --git a/toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js b/toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js index 4b12edf6f0..6374f82b52 100644 --- a/toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js +++ b/toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js @@ -181,6 +181,7 @@ var gDebugTestLog = false; var gTestsToLog = []; var gRealDump; var gFOS; +var gUpdateBin; var gTestFiles = []; var gTestDirs = []; @@ -189,6 +190,23 @@ var gTestDirs = []; var gTestFilesCommon = [ { description: "Should never change", + fileName: FILE_CHANNEL_PREFS, + relPathDir: + AppConstants.platform == "macosx" + ? "Contents/Frameworks/ChannelPrefs.framework/" + : DIR_RESOURCES + "defaults/pref/", + originalContents: "ShouldNotBeReplaced\n", + compareContents: "ShouldNotBeReplaced\n", + originalFile: null, + compareFile: null, + originalPerms: 0o767, + comparePerms: 0o767, + }, +]; + +var gTestFilesCommonNonMac = [ + { + description: "Should never change", fileName: FILE_UPDATE_SETTINGS_INI, relPathDir: DIR_RESOURCES, originalContents: UPDATE_SETTINGS_CONTENTS, @@ -198,19 +216,32 @@ var gTestFilesCommon = [ originalPerms: 0o767, comparePerms: 0o767, }, +]; + +if (AppConstants.platform != "macosx") { + gTestFilesCommon = gTestFilesCommon.concat(gTestFilesCommonNonMac); +} + +var gTestFilesCommonMac = [ { description: "Should never change", - fileName: "channel-prefs.js", - relPathDir: DIR_RESOURCES + "defaults/pref/", - originalContents: "ShouldNotBeReplaced\n", - compareContents: "ShouldNotBeReplaced\n", + fileName: FILE_UPDATE_SETTINGS_FRAMEWORK, + relPathDir: + "Contents/MacOS/updater.app/Contents/Frameworks/UpdateSettings.framework/", + originalContents: null, + compareContents: null, originalFile: null, compareFile: null, - originalPerms: 0o767, - comparePerms: 0o767, + originalPerms: null, + comparePerms: null, + existingFile: true, }, ]; +if (AppConstants.platform == "macosx") { + gTestFilesCommon = gTestFilesCommon.concat(gTestFilesCommonMac); +} + // Files for a complete successful update. This can be used for a complete // failed update by calling setTestFilesAndDirsForFailure. var gTestFilesCompleteSuccess = [ @@ -657,6 +688,20 @@ var gTestFilesPartialSuccess = [ // Concatenate the common files to the end of the array. gTestFilesPartialSuccess = gTestFilesPartialSuccess.concat(gTestFilesCommon); +/** + * Searches `gTestFiles` for the file with the given filename. This is currently + * not very efficient (it searches the whole array every time). + * + * @param filename + * The name of the file to search for (i.e. the `fileName` attribute). + * @returns + * The object in `gTestFiles` that describes the requested file. + * Or `null`, if the file is not in `gTestFiles`. + */ +function getTestFileByName(filename) { + return gTestFiles.find(f => f.fileName == filename) ?? null; +} + var gTestDirsCommon = [ { relPathDir: DIR_RESOURCES + "3/", @@ -2019,8 +2064,13 @@ function runUpdate( Services.env.set("MOZ_TEST_SHORTER_WAIT_PID", "1"); } - let updateBin = copyTestUpdaterToBinDir(); - Assert.ok(updateBin.exists(), MSG_SHOULD_EXIST + getMsgPath(updateBin.path)); + if (!gUpdateBin) { + gUpdateBin = copyTestUpdaterToBinDir(); + } + Assert.ok( + gUpdateBin.exists(), + MSG_SHOULD_EXIST + getMsgPath(gUpdateBin.path) + ); let updatesDirPath = aPatchDirPath || getUpdateDirFile(DIR_PATCH).path; let installDirPath = aInstallDirPath || getApplyDirFile().path; @@ -2045,13 +2095,13 @@ function runUpdate( args[3] = pid; } - let launchBin = gIsServiceTest && isInvalidArgTest ? callbackApp : updateBin; + let launchBin = gIsServiceTest && isInvalidArgTest ? callbackApp : gUpdateBin; if (!isInvalidArgTest) { args = args.concat([callbackApp.parent.path, callbackApp.path]); args = args.concat(gCallbackArgs); } else if (gIsServiceTest) { - args = ["launch-service", updateBin.path].concat(args); + args = ["launch-service", gUpdateBin.path].concat(args); } else if (aCallbackPath) { args = args.concat([callbackApp.parent.path, aCallbackPath]); } @@ -3125,6 +3175,13 @@ async function setupUpdaterTest( helperBin.copyToFollowingLinks(afterApplyBinDir, gCallbackBinFile); helperBin.copyToFollowingLinks(afterApplyBinDir, gPostUpdateBinFile); + // On macOS, some test files (like the Update Settings file) may be within the + // updater app bundle, so make sure it is in place now in case we want to + // manipulate it. + if (!gUpdateBin) { + gUpdateBin = copyTestUpdaterToBinDir(); + } + gTestFiles.forEach(function SUT_TF_FE(aTestFile) { debugDump("start - setup test file: " + aTestFile.fileName); if (aTestFile.originalFile || aTestFile.originalContents) { @@ -3163,6 +3220,24 @@ async function setupUpdaterTest( aTestFile.comparePerms = testFile.permissions; } } + } else if (aTestFile.existingFile) { + const testFile = getApplyDirFile( + aTestFile.relPathDir + aTestFile.fileName + ); + if (aTestFile.removeOriginalFile) { + testFile.remove(false); + } else { + const fileContents = readFileBytes(testFile); + if (!aTestFile.originalContents && !aTestFile.originalFile) { + aTestFile.originalContents = fileContents; + } + if (!aTestFile.compareContents && !aTestFile.compareFile) { + aTestFile.compareContents = fileContents; + } + if (!aTestFile.comparePerms) { + aTestFile.comparePerms = testFile.permissions; + } + } } debugDump("finish - setup test file: " + aTestFile.fileName); }); @@ -3424,21 +3499,13 @@ function checkUpdateLogContents( // Remove leading timestamps updateLogContents = removeTimeStamps(updateLogContents); - // The channel-prefs.js is defined in gTestFilesCommon which will always be - // located to the end of gTestFiles when it is present. - if ( - gTestFiles.length > 1 && - gTestFiles[gTestFiles.length - 1].fileName == "channel-prefs.js" && - !gTestFiles[gTestFiles.length - 1].originalContents - ) { + const channelPrefs = getTestFileByName(FILE_CHANNEL_PREFS); + if (channelPrefs && !channelPrefs.originalContents) { updateLogContents = updateLogContents.replace(/.*defaults\/.*/g, ""); } - if ( - gTestFiles.length > 2 && - gTestFiles[gTestFiles.length - 2].fileName == FILE_UPDATE_SETTINGS_INI && - !gTestFiles[gTestFiles.length - 2].originalContents - ) { + const updateSettings = getTestFileByName(FILE_UPDATE_SETTINGS_INI); + if (updateSettings && !updateSettings.originalContents) { updateLogContents = updateLogContents.replace( /.*update-settings.ini.*/g, "" @@ -3529,21 +3596,11 @@ function checkUpdateLogContents( // Remove leading timestamps compareLogContents = removeTimeStamps(compareLogContents); - // The channel-prefs.js is defined in gTestFilesCommon which will always be - // located to the end of gTestFiles. - if ( - gTestFiles.length > 1 && - gTestFiles[gTestFiles.length - 1].fileName == "channel-prefs.js" && - !gTestFiles[gTestFiles.length - 1].originalContents - ) { + if (channelPrefs && !channelPrefs.originalContents) { compareLogContents = compareLogContents.replace(/.*defaults\/.*/g, ""); } - if ( - gTestFiles.length > 2 && - gTestFiles[gTestFiles.length - 2].fileName == FILE_UPDATE_SETTINGS_INI && - !gTestFiles[gTestFiles.length - 2].originalContents - ) { + if (updateSettings && !updateSettings.originalContents) { compareLogContents = compareLogContents.replace( /.*update-settings.ini.*/g, "" @@ -4879,3 +4936,33 @@ function resetEnvironment() { Services.env.set("MOZ_NO_SERVICE_FALLBACK", ""); } } + +/** + * `gTestFiles` needs to be set such that it contains the Update Settings file + * before this function is called. + */ +function setUpdateSettingsUseWrongChannel() { + if (AppConstants.platform == "macosx") { + let replacementUpdateSettings = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + replacementUpdateSettings = replacementUpdateSettings.parent; + replacementUpdateSettings.append("UpdateSettings-WrongChannel"); + + const updateSettings = getTestFileByName(FILE_UPDATE_SETTINGS_FRAMEWORK); + if (!updateSettings) { + throw new Error( + "gTestFiles does not contain the update settings framework" + ); + } + updateSettings.existingFile = false; + updateSettings.originalContents = readFileBytes(replacementUpdateSettings); + } else { + const updateSettings = getTestFileByName(FILE_UPDATE_SETTINGS_INI); + if (!updateSettings) { + throw new Error("gTestFiles does not contain the update settings INI"); + } + updateSettings.originalContents = UPDATE_SETTINGS_CONTENTS.replace( + "xpcshell-test", + "wrong-channel" + ); + } +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/languagePackUpdates.js b/toolkit/mozapps/update/tests/unit_aus_update/languagePackUpdates.js index 9e23fab5e0..1e27efb753 100644 --- a/toolkit/mozapps/update/tests/unit_aus_update/languagePackUpdates.js +++ b/toolkit/mozapps/update/tests/unit_aus_update/languagePackUpdates.js @@ -177,9 +177,12 @@ add_task(async function testLangpackStaged() { copyTestUpdaterToBinDir(); let greDir = getGREDir(); - let updateSettingsIni = greDir.clone(); - updateSettingsIni.append(FILE_UPDATE_SETTINGS_INI); - writeFile(updateSettingsIni, UPDATE_SETTINGS_CONTENTS); + + if (AppConstants.platform != "macosx") { + let updateSettingsIni = greDir.clone(); + updateSettingsIni.append(FILE_UPDATE_SETTINGS_INI); + writeFile(updateSettingsIni, UPDATE_SETTINGS_CONTENTS); + } await downloadUpdate(); diff --git a/toolkit/mozapps/update/tests/unit_aus_update/verifyChannelPrefsFile.js b/toolkit/mozapps/update/tests/unit_aus_update/verifyChannelPrefsFile.js deleted file mode 100644 index 4cca77c73d..0000000000 --- a/toolkit/mozapps/update/tests/unit_aus_update/verifyChannelPrefsFile.js +++ /dev/null @@ -1,38 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -/** - * This test exists solely to ensure that channel-prefs.js is not changed. - * If it does get changed, it will cause a variation of Bug 1431342. - * To summarize, our updater doesn't update that file. But, on macOS, it is - * still used to compute the application's signature. This means that if Firefox - * updates and that file has been changed, the signature no will no longer - * validate. - */ - -const expectedChannelPrefsContents = `/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -// -// This pref is in its own file for complex reasons. See the comment in -// browser/app/Makefile.in, bug 756325, and bug 1431342 for details. Do not add -// other prefs to this file. - -pref("app.update.channel", "${UpdateUtils.UpdateChannel}"); -`; - -async function run_test() { - let channelPrefsFile = Services.dirsvc.get("GreD", Ci.nsIFile); - channelPrefsFile.append("defaults"); - channelPrefsFile.append("pref"); - channelPrefsFile.append("channel-prefs.js"); - - const contents = await IOUtils.readUTF8(channelPrefsFile.path); - Assert.equal( - contents, - expectedChannelPrefsContents, - "Channel Prefs file should should not change" - ); -} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/xpcshell.toml b/toolkit/mozapps/update/tests/unit_aus_update/xpcshell.toml index 74790016e4..160427ac5e 100644 --- a/toolkit/mozapps/update/tests/unit_aus_update/xpcshell.toml +++ b/toolkit/mozapps/update/tests/unit_aus_update/xpcshell.toml @@ -83,7 +83,3 @@ reason = "Update directory migration is currently Windows only" ["urlConstruction.js"] skip-if = ["socketprocess_networking"] # Bug 1759035 - -["verifyChannelPrefsFile.js"] -run-if = ["appname == 'firefox'"] -reason = "File being verified is Firefox-specific." diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseStageSuccessComplete_unix.js b/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseStageSuccessComplete_unix.js index 29c2c2a30e..faac7510d0 100644 --- a/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseStageSuccessComplete_unix.js +++ b/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseStageSuccessComplete_unix.js @@ -10,9 +10,12 @@ async function run_test() { } const STATE_AFTER_STAGE = STATE_APPLIED; gTestFiles = gTestFilesCompleteSuccess; - gTestFiles[gTestFiles.length - 1].originalContents = null; - gTestFiles[gTestFiles.length - 1].compareContents = "FromComplete\n"; - gTestFiles[gTestFiles.length - 1].comparePerms = 0o644; + const channelPrefs = getTestFileByName(FILE_CHANNEL_PREFS); + if (channelPrefs) { + channelPrefs.originalContents = null; + channelPrefs.compareContents = "FromComplete\n"; + channelPrefs.comparePerms = 0o644; + } gTestDirs = gTestDirsCompleteSuccess; await setupUpdaterTest(FILE_COMPLETE_MAR, true); setupSymLinks(); diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFailurePartial.js b/toolkit/mozapps/update/tests/unit_base_updater/marFailurePartial.js index de8db067bc..379fb00a27 100644 --- a/toolkit/mozapps/update/tests/unit_base_updater/marFailurePartial.js +++ b/toolkit/mozapps/update/tests/unit_base_updater/marFailurePartial.js @@ -10,7 +10,7 @@ async function run_test() { return; } gTestFiles = gTestFilesPartialSuccess; - gTestFiles[11].originalFile = "partial.png"; + getTestFileByName("0exe0.exe").originalFile = "partial.png"; gTestDirs = gTestDirsPartialSuccess; setTestFilesAndDirsForFailure(); await setupUpdaterTest(FILE_PARTIAL_MAR, false); diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailureComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailureComplete_win.js index b93b023934..11181f4420 100644 --- a/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailureComplete_win.js +++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailureComplete_win.js @@ -15,10 +15,8 @@ async function run_test() { gTestFiles = gTestFilesCompleteSuccess; gTestDirs = gTestDirsCompleteSuccess; await setupUpdaterTest(FILE_COMPLETE_MAR, false); - await runHelperFileInUse( - gTestFiles[13].relPathDir + gTestFiles[13].fileName, - false - ); + const testFile = getTestFileByName("0exe0.exe"); + await runHelperFileInUse(testFile.relPathDir + testFile.fileName, false); await stageUpdate(STATE_AFTER_STAGE, true); checkPostUpdateRunningFile(false); checkFilesAfterUpdateSuccess(getStageDirFile, true); diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailurePartial_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailurePartial_win.js index b41da12396..def7f8db9c 100644 --- a/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailurePartial_win.js +++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailurePartial_win.js @@ -15,10 +15,8 @@ async function run_test() { gTestFiles = gTestFilesPartialSuccess; gTestDirs = gTestDirsPartialSuccess; await setupUpdaterTest(FILE_PARTIAL_MAR, false); - await runHelperFileInUse( - gTestFiles[11].relPathDir + gTestFiles[11].fileName, - false - ); + const testFile = getTestFileByName("0exe0.exe"); + await runHelperFileInUse(testFile.relPathDir + testFile.fileName, false); await stageUpdate(STATE_AFTER_STAGE, true); checkPostUpdateRunningFile(false); checkFilesAfterUpdateSuccess(getStageDirFile, true); diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessComplete_win.js index 4b946ac3e4..d1f938bb02 100644 --- a/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessComplete_win.js +++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessComplete_win.js @@ -11,10 +11,8 @@ async function run_test() { gTestFiles = gTestFilesCompleteSuccess; gTestDirs = gTestDirsCompleteSuccess; await setupUpdaterTest(FILE_COMPLETE_MAR, false); - await runHelperFileInUse( - gTestFiles[13].relPathDir + gTestFiles[13].fileName, - false - ); + const testFile = getTestFileByName("0exe0.exe"); + await runHelperFileInUse(testFile.relPathDir + testFile.fileName, false); runUpdate(STATE_SUCCEEDED, false, 0, true); await waitForHelperExit(); await checkPostUpdateAppLog(); diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessPartial_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessPartial_win.js index 15c3a1121a..f851c543a6 100644 --- a/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessPartial_win.js +++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessPartial_win.js @@ -11,10 +11,8 @@ async function run_test() { gTestFiles = gTestFilesPartialSuccess; gTestDirs = gTestDirsPartialSuccess; await setupUpdaterTest(FILE_PARTIAL_MAR, false); - await runHelperFileInUse( - gTestFiles[11].relPathDir + gTestFiles[11].fileName, - false - ); + const testFile = getTestFileByName("0exe0.exe"); + await runHelperFileInUse(testFile.relPathDir + testFile.fileName, false); runUpdate(STATE_SUCCEEDED, false, 0, true); await waitForHelperExit(); await checkPostUpdateAppLog(); diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailureComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailureComplete_win.js index 698ccb7fe5..5517d5ed81 100644 --- a/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailureComplete_win.js +++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailureComplete_win.js @@ -12,7 +12,7 @@ async function run_test() { gTestDirs = gTestDirsCompleteSuccess; setTestFilesAndDirsForFailure(); await setupUpdaterTest(FILE_COMPLETE_MAR, false); - await runHelperLockFile(gTestFiles[3]); + await runHelperLockFile(getTestFileByName("searchpluginspng0.png")); runUpdate(STATE_FAILED_WRITE_ERROR, false, 1, true); await waitForHelperExit(); standardInit(); diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailurePartial_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailurePartial_win.js index c8c019ec5c..03eb6122c8 100644 --- a/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailurePartial_win.js +++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailurePartial_win.js @@ -12,7 +12,7 @@ async function run_test() { gTestDirs = gTestDirsPartialSuccess; setTestFilesAndDirsForFailure(); await setupUpdaterTest(FILE_PARTIAL_MAR, false); - await runHelperLockFile(gTestFiles[2]); + await runHelperLockFile(getTestFileByName("searchpluginspng1.png")); runUpdate(STATE_FAILED_READ_ERROR, false, 1, true); await waitForHelperExit(); standardInit(); diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailureComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailureComplete_win.js index 7b582dbd45..4d5fe599cf 100644 --- a/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailureComplete_win.js +++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailureComplete_win.js @@ -13,7 +13,7 @@ async function run_test() { gTestDirs = gTestDirsCompleteSuccess; setTestFilesAndDirsForFailure(); await setupUpdaterTest(FILE_COMPLETE_MAR, false); - await runHelperLockFile(gTestFiles[3]); + await runHelperLockFile(getTestFileByName("searchpluginspng0.png")); await stageUpdate(STATE_AFTER_STAGE, true); checkPostUpdateRunningFile(false); // Files aren't checked after staging since this test locks a file which diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailurePartial_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailurePartial_win.js index bf3abd8c37..3d4a8e0c51 100644 --- a/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailurePartial_win.js +++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailurePartial_win.js @@ -13,7 +13,7 @@ async function run_test() { gTestDirs = gTestDirsPartialSuccess; setTestFilesAndDirsForFailure(); await setupUpdaterTest(FILE_PARTIAL_MAR, false); - await runHelperLockFile(gTestFiles[2]); + await runHelperLockFile(getTestFileByName("searchpluginspng1.png")); await stageUpdate(STATE_AFTER_STAGE, true); checkPostUpdateRunningFile(false); // Files aren't checked after staging since this test locks a file which diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marMissingUpdateSettings.js b/toolkit/mozapps/update/tests/unit_base_updater/marMissingUpdateSettings.js index b0a0cfe657..fbb0e7c4cd 100644 --- a/toolkit/mozapps/update/tests/unit_base_updater/marMissingUpdateSettings.js +++ b/toolkit/mozapps/update/tests/unit_base_updater/marMissingUpdateSettings.js @@ -14,7 +14,17 @@ async function run_test() { return; } gTestFiles = gTestFilesCompleteSuccess; - gTestFiles[gTestFiles.length - 2].originalContents = null; + if (AppConstants.platform == "macosx") { + // On macOS, the update settings Framework already exists. Remove it. + const updateSettings = getTestFileByName(FILE_UPDATE_SETTINGS_FRAMEWORK); + updateSettings.removeOriginalFile = true; + } else { + // On non-macOS, the update settings INI will normally be written out with + // the contents specified by `originalContents`. Setting this to `null` + // prevents anything from being written out. + const updateSettings = getTestFileByName(FILE_UPDATE_SETTINGS_INI); + updateSettings.originalContents = null; + } gTestDirs = gTestDirsCompleteSuccess; setTestFilesAndDirsForFailure(); await setupUpdaterTest(FILE_COMPLETE_MAR, false); diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marMissingUpdateSettingsStage.js b/toolkit/mozapps/update/tests/unit_base_updater/marMissingUpdateSettingsStage.js index e26d2aefc3..a6d204aa92 100644 --- a/toolkit/mozapps/update/tests/unit_base_updater/marMissingUpdateSettingsStage.js +++ b/toolkit/mozapps/update/tests/unit_base_updater/marMissingUpdateSettingsStage.js @@ -15,7 +15,17 @@ async function run_test() { } const STATE_AFTER_STAGE = STATE_FAILED; gTestFiles = gTestFilesCompleteSuccess; - gTestFiles[gTestFiles.length - 2].originalContents = null; + if (AppConstants.platform == "macosx") { + // On macOS, the update settings Framework already exists. Remove it. + const updateSettings = getTestFileByName(FILE_UPDATE_SETTINGS_FRAMEWORK); + updateSettings.removeOriginalFile = true; + } else { + // On non-macOS, the update settings INI will normally be written out with + // the contents specified by `originalContents`. Setting this to `null` + // prevents anything from being written out. + const updateSettings = getTestFileByName(FILE_UPDATE_SETTINGS_INI); + updateSettings.originalContents = null; + } gTestDirs = gTestDirsCompleteSuccess; setTestFilesAndDirsForFailure(); await setupUpdaterTest(FILE_COMPLETE_MAR, false); diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marStageFailurePartial.js b/toolkit/mozapps/update/tests/unit_base_updater/marStageFailurePartial.js index a1a0de0fe4..47c184e67b 100644 --- a/toolkit/mozapps/update/tests/unit_base_updater/marStageFailurePartial.js +++ b/toolkit/mozapps/update/tests/unit_base_updater/marStageFailurePartial.js @@ -11,7 +11,7 @@ async function run_test() { } const STATE_AFTER_STAGE = STATE_FAILED; gTestFiles = gTestFilesPartialSuccess; - gTestFiles[11].originalFile = "partial.png"; + getTestFileByName("0exe0.exe").originalFile = "partial.png"; gTestDirs = gTestDirsPartialSuccess; setTestFilesAndDirsForFailure(); await setupUpdaterTest(FILE_PARTIAL_MAR, false); diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessComplete.js b/toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessComplete.js index 943a45ba95..2c9c3298d4 100644 --- a/toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessComplete.js +++ b/toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessComplete.js @@ -11,9 +11,10 @@ async function run_test() { } const STATE_AFTER_STAGE = gIsServiceTest ? STATE_APPLIED_SVC : STATE_APPLIED; gTestFiles = gTestFilesCompleteSuccess; - gTestFiles[gTestFiles.length - 1].originalContents = null; - gTestFiles[gTestFiles.length - 1].compareContents = "FromComplete\n"; - gTestFiles[gTestFiles.length - 1].comparePerms = 0o644; + const channelPrefs = getTestFileByName(FILE_CHANNEL_PREFS); + channelPrefs.originalContents = null; + channelPrefs.compareContents = "FromComplete\n"; + channelPrefs.comparePerms = 0o644; gTestDirs = gTestDirsCompleteSuccess; setupSymLinks(); await setupUpdaterTest(FILE_COMPLETE_MAR, false); diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessPartial.js b/toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessPartial.js index dd5c240919..e565ffe550 100644 --- a/toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessPartial.js +++ b/toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessPartial.js @@ -11,9 +11,10 @@ async function run_test() { } const STATE_AFTER_STAGE = gIsServiceTest ? STATE_APPLIED_SVC : STATE_APPLIED; gTestFiles = gTestFilesPartialSuccess; - gTestFiles[gTestFiles.length - 1].originalContents = null; - gTestFiles[gTestFiles.length - 1].compareContents = "FromPartial\n"; - gTestFiles[gTestFiles.length - 1].comparePerms = 0o644; + const channelPrefs = getTestFileByName(FILE_CHANNEL_PREFS); + channelPrefs.originalContents = null; + channelPrefs.compareContents = "FromPartial\n"; + channelPrefs.comparePerms = 0o644; gTestDirs = gTestDirsPartialSuccess; preventDistributionFiles(); await setupUpdaterTest(FILE_PARTIAL_MAR, true); diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marSuccessPartial.js b/toolkit/mozapps/update/tests/unit_base_updater/marSuccessPartial.js index 8e8e9d094a..f4f856278c 100644 --- a/toolkit/mozapps/update/tests/unit_base_updater/marSuccessPartial.js +++ b/toolkit/mozapps/update/tests/unit_base_updater/marSuccessPartial.js @@ -10,9 +10,10 @@ async function run_test() { return; } gTestFiles = gTestFilesPartialSuccess; - gTestFiles[gTestFiles.length - 1].originalContents = null; - gTestFiles[gTestFiles.length - 1].compareContents = "FromPartial\n"; - gTestFiles[gTestFiles.length - 1].comparePerms = 0o644; + const channelPrefs = getTestFileByName(FILE_CHANNEL_PREFS); + channelPrefs.originalContents = null; + channelPrefs.compareContents = "FromPartial\n"; + channelPrefs.comparePerms = 0o644; gTestDirs = gTestDirsPartialSuccess; // The third parameter will test that a relative path that contains a // directory traversal to the post update binary doesn't execute. diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marSuccessPartialWhileBackgroundTaskRunning.js b/toolkit/mozapps/update/tests/unit_base_updater/marSuccessPartialWhileBackgroundTaskRunning.js index 37511bd789..e80e52e9a3 100644 --- a/toolkit/mozapps/update/tests/unit_base_updater/marSuccessPartialWhileBackgroundTaskRunning.js +++ b/toolkit/mozapps/update/tests/unit_base_updater/marSuccessPartialWhileBackgroundTaskRunning.js @@ -45,8 +45,7 @@ async function run_test() { // won't be updated if it already exists. The manipulations below arrange a) // for the file to exist and b) for the comparison afterward to succeed. gTestFiles = gTestFilesPartialSuccess; - let channelPrefs = gTestFiles[gTestFiles.length - 1]; - Assert.equal("channel-prefs.js", channelPrefs.fileName); + let channelPrefs = getTestFileByName(FILE_CHANNEL_PREFS); let f = gGREDirOrig.clone(); f.append("defaults"); f.append("pref"); diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marWrongChannel.js b/toolkit/mozapps/update/tests/unit_base_updater/marWrongChannel.js index d31188dcca..5d6ee61776 100644 --- a/toolkit/mozapps/update/tests/unit_base_updater/marWrongChannel.js +++ b/toolkit/mozapps/update/tests/unit_base_updater/marWrongChannel.js @@ -14,8 +14,7 @@ async function run_test() { return; } gTestFiles = gTestFilesCompleteSuccess; - gTestFiles[gTestFiles.length - 2].originalContents = - UPDATE_SETTINGS_CONTENTS.replace("xpcshell-test", "wrong-channel"); + setUpdateSettingsUseWrongChannel(); gTestDirs = gTestDirsCompleteSuccess; setTestFilesAndDirsForFailure(); await setupUpdaterTest(FILE_COMPLETE_MAR, false); diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marWrongChannelStage.js b/toolkit/mozapps/update/tests/unit_base_updater/marWrongChannelStage.js index 4d512fd12a..9aca22df66 100644 --- a/toolkit/mozapps/update/tests/unit_base_updater/marWrongChannelStage.js +++ b/toolkit/mozapps/update/tests/unit_base_updater/marWrongChannelStage.js @@ -15,8 +15,7 @@ async function run_test() { } const STATE_AFTER_STAGE = STATE_FAILED; gTestFiles = gTestFilesCompleteSuccess; - gTestFiles[gTestFiles.length - 2].originalContents = - UPDATE_SETTINGS_CONTENTS.replace("xpcshell-test", "wrong-channel"); + setUpdateSettingsUseWrongChannel(); gTestDirs = gTestDirsCompleteSuccess; setTestFilesAndDirsForFailure(); await setupUpdaterTest(FILE_COMPLETE_MAR, false); diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFailurePartialSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marFailurePartialSvc.js index de8db067bc..379fb00a27 100644 --- a/toolkit/mozapps/update/tests/unit_service_updater/marFailurePartialSvc.js +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFailurePartialSvc.js @@ -10,7 +10,7 @@ async function run_test() { return; } gTestFiles = gTestFilesPartialSuccess; - gTestFiles[11].originalFile = "partial.png"; + getTestFileByName("0exe0.exe").originalFile = "partial.png"; gTestDirs = gTestDirsPartialSuccess; setTestFilesAndDirsForFailure(); await setupUpdaterTest(FILE_PARTIAL_MAR, false); diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailureCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailureCompleteSvc_win.js index b93b023934..11181f4420 100644 --- a/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailureCompleteSvc_win.js +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailureCompleteSvc_win.js @@ -15,10 +15,8 @@ async function run_test() { gTestFiles = gTestFilesCompleteSuccess; gTestDirs = gTestDirsCompleteSuccess; await setupUpdaterTest(FILE_COMPLETE_MAR, false); - await runHelperFileInUse( - gTestFiles[13].relPathDir + gTestFiles[13].fileName, - false - ); + const testFile = getTestFileByName("0exe0.exe"); + await runHelperFileInUse(testFile.relPathDir + testFile.fileName, false); await stageUpdate(STATE_AFTER_STAGE, true); checkPostUpdateRunningFile(false); checkFilesAfterUpdateSuccess(getStageDirFile, true); diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailurePartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailurePartialSvc_win.js index b41da12396..def7f8db9c 100644 --- a/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailurePartialSvc_win.js +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailurePartialSvc_win.js @@ -15,10 +15,8 @@ async function run_test() { gTestFiles = gTestFilesPartialSuccess; gTestDirs = gTestDirsPartialSuccess; await setupUpdaterTest(FILE_PARTIAL_MAR, false); - await runHelperFileInUse( - gTestFiles[11].relPathDir + gTestFiles[11].fileName, - false - ); + const testFile = getTestFileByName("0exe0.exe"); + await runHelperFileInUse(testFile.relPathDir + testFile.fileName, false); await stageUpdate(STATE_AFTER_STAGE, true); checkPostUpdateRunningFile(false); checkFilesAfterUpdateSuccess(getStageDirFile, true); diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessCompleteSvc_win.js index 4b946ac3e4..d1f938bb02 100644 --- a/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessCompleteSvc_win.js +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessCompleteSvc_win.js @@ -11,10 +11,8 @@ async function run_test() { gTestFiles = gTestFilesCompleteSuccess; gTestDirs = gTestDirsCompleteSuccess; await setupUpdaterTest(FILE_COMPLETE_MAR, false); - await runHelperFileInUse( - gTestFiles[13].relPathDir + gTestFiles[13].fileName, - false - ); + const testFile = getTestFileByName("0exe0.exe"); + await runHelperFileInUse(testFile.relPathDir + testFile.fileName, false); runUpdate(STATE_SUCCEEDED, false, 0, true); await waitForHelperExit(); await checkPostUpdateAppLog(); diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessPartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessPartialSvc_win.js index 15c3a1121a..f851c543a6 100644 --- a/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessPartialSvc_win.js +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessPartialSvc_win.js @@ -11,10 +11,8 @@ async function run_test() { gTestFiles = gTestFilesPartialSuccess; gTestDirs = gTestDirsPartialSuccess; await setupUpdaterTest(FILE_PARTIAL_MAR, false); - await runHelperFileInUse( - gTestFiles[11].relPathDir + gTestFiles[11].fileName, - false - ); + const testFile = getTestFileByName("0exe0.exe"); + await runHelperFileInUse(testFile.relPathDir + testFile.fileName, false); runUpdate(STATE_SUCCEEDED, false, 0, true); await waitForHelperExit(); await checkPostUpdateAppLog(); diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailureCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailureCompleteSvc_win.js index 698ccb7fe5..5517d5ed81 100644 --- a/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailureCompleteSvc_win.js +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailureCompleteSvc_win.js @@ -12,7 +12,7 @@ async function run_test() { gTestDirs = gTestDirsCompleteSuccess; setTestFilesAndDirsForFailure(); await setupUpdaterTest(FILE_COMPLETE_MAR, false); - await runHelperLockFile(gTestFiles[3]); + await runHelperLockFile(getTestFileByName("searchpluginspng0.png")); runUpdate(STATE_FAILED_WRITE_ERROR, false, 1, true); await waitForHelperExit(); standardInit(); diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailurePartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailurePartialSvc_win.js index c8c019ec5c..03eb6122c8 100644 --- a/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailurePartialSvc_win.js +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailurePartialSvc_win.js @@ -12,7 +12,7 @@ async function run_test() { gTestDirs = gTestDirsPartialSuccess; setTestFilesAndDirsForFailure(); await setupUpdaterTest(FILE_PARTIAL_MAR, false); - await runHelperLockFile(gTestFiles[2]); + await runHelperLockFile(getTestFileByName("searchpluginspng1.png")); runUpdate(STATE_FAILED_READ_ERROR, false, 1, true); await waitForHelperExit(); standardInit(); diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailureCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailureCompleteSvc_win.js index 7b582dbd45..4d5fe599cf 100644 --- a/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailureCompleteSvc_win.js +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailureCompleteSvc_win.js @@ -13,7 +13,7 @@ async function run_test() { gTestDirs = gTestDirsCompleteSuccess; setTestFilesAndDirsForFailure(); await setupUpdaterTest(FILE_COMPLETE_MAR, false); - await runHelperLockFile(gTestFiles[3]); + await runHelperLockFile(getTestFileByName("searchpluginspng0.png")); await stageUpdate(STATE_AFTER_STAGE, true); checkPostUpdateRunningFile(false); // Files aren't checked after staging since this test locks a file which diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailurePartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailurePartialSvc_win.js index bf3abd8c37..3d4a8e0c51 100644 --- a/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailurePartialSvc_win.js +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailurePartialSvc_win.js @@ -13,7 +13,7 @@ async function run_test() { gTestDirs = gTestDirsPartialSuccess; setTestFilesAndDirsForFailure(); await setupUpdaterTest(FILE_PARTIAL_MAR, false); - await runHelperLockFile(gTestFiles[2]); + await runHelperLockFile(getTestFileByName("searchpluginspng1.png")); await stageUpdate(STATE_AFTER_STAGE, true); checkPostUpdateRunningFile(false); // Files aren't checked after staging since this test locks a file which diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marStageFailurePartialSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marStageFailurePartialSvc.js index a1a0de0fe4..47c184e67b 100644 --- a/toolkit/mozapps/update/tests/unit_service_updater/marStageFailurePartialSvc.js +++ b/toolkit/mozapps/update/tests/unit_service_updater/marStageFailurePartialSvc.js @@ -11,7 +11,7 @@ async function run_test() { } const STATE_AFTER_STAGE = STATE_FAILED; gTestFiles = gTestFilesPartialSuccess; - gTestFiles[11].originalFile = "partial.png"; + getTestFileByName("0exe0.exe").originalFile = "partial.png"; gTestDirs = gTestDirsPartialSuccess; setTestFilesAndDirsForFailure(); await setupUpdaterTest(FILE_PARTIAL_MAR, false); diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessCompleteSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessCompleteSvc.js index 943a45ba95..2c9c3298d4 100644 --- a/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessCompleteSvc.js +++ b/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessCompleteSvc.js @@ -11,9 +11,10 @@ async function run_test() { } const STATE_AFTER_STAGE = gIsServiceTest ? STATE_APPLIED_SVC : STATE_APPLIED; gTestFiles = gTestFilesCompleteSuccess; - gTestFiles[gTestFiles.length - 1].originalContents = null; - gTestFiles[gTestFiles.length - 1].compareContents = "FromComplete\n"; - gTestFiles[gTestFiles.length - 1].comparePerms = 0o644; + const channelPrefs = getTestFileByName(FILE_CHANNEL_PREFS); + channelPrefs.originalContents = null; + channelPrefs.compareContents = "FromComplete\n"; + channelPrefs.comparePerms = 0o644; gTestDirs = gTestDirsCompleteSuccess; setupSymLinks(); await setupUpdaterTest(FILE_COMPLETE_MAR, false); diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessPartialSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessPartialSvc.js index dd5c240919..e565ffe550 100644 --- a/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessPartialSvc.js +++ b/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessPartialSvc.js @@ -11,9 +11,10 @@ async function run_test() { } const STATE_AFTER_STAGE = gIsServiceTest ? STATE_APPLIED_SVC : STATE_APPLIED; gTestFiles = gTestFilesPartialSuccess; - gTestFiles[gTestFiles.length - 1].originalContents = null; - gTestFiles[gTestFiles.length - 1].compareContents = "FromPartial\n"; - gTestFiles[gTestFiles.length - 1].comparePerms = 0o644; + const channelPrefs = getTestFileByName(FILE_CHANNEL_PREFS); + channelPrefs.originalContents = null; + channelPrefs.compareContents = "FromPartial\n"; + channelPrefs.comparePerms = 0o644; gTestDirs = gTestDirsPartialSuccess; preventDistributionFiles(); await setupUpdaterTest(FILE_PARTIAL_MAR, true); diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marSuccessPartialSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marSuccessPartialSvc.js index 8e8e9d094a..f4f856278c 100644 --- a/toolkit/mozapps/update/tests/unit_service_updater/marSuccessPartialSvc.js +++ b/toolkit/mozapps/update/tests/unit_service_updater/marSuccessPartialSvc.js @@ -10,9 +10,10 @@ async function run_test() { return; } gTestFiles = gTestFilesPartialSuccess; - gTestFiles[gTestFiles.length - 1].originalContents = null; - gTestFiles[gTestFiles.length - 1].compareContents = "FromPartial\n"; - gTestFiles[gTestFiles.length - 1].comparePerms = 0o644; + const channelPrefs = getTestFileByName(FILE_CHANNEL_PREFS); + channelPrefs.originalContents = null; + channelPrefs.compareContents = "FromPartial\n"; + channelPrefs.comparePerms = 0o644; gTestDirs = gTestDirsPartialSuccess; // The third parameter will test that a relative path that contains a // directory traversal to the post update binary doesn't execute. diff --git a/toolkit/mozapps/update/updater/Makefile.in b/toolkit/mozapps/update/updater/Makefile.in index 70cf32378a..ec3ad9773a 100644 --- a/toolkit/mozapps/update/updater/Makefile.in +++ b/toolkit/mozapps/update/updater/Makefile.in @@ -25,4 +25,7 @@ libs:: $(call py_action,preprocessor updater.app/Contents/Resources/English.lproj/InfoPlist.strings,-Fsubstitution --output-encoding utf-16 -DAPP_NAME='$(MOZ_APP_DISPLAYNAME)' $(srcdir)/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in -o $(DIST)/bin/updater.app/Contents/Resources/English.lproj/InfoPlist.strings) $(NSINSTALL) -D $(DIST)/bin/updater.app/Contents/MacOS $(NSINSTALL) $(DIST)/bin/org.mozilla.updater $(DIST)/bin/updater.app/Contents/MacOS + $(NSINSTALL) -D $(DIST)/bin/updater.app/Contents/Frameworks + $(NSINSTALL) $(DIST)/bin/UpdateSettings $(DIST)/bin/updater.app/Contents/Frameworks/UpdateSettings.framework + $(NSINSTALL) $(srcdir)/macos-frameworks/UpdateSettings/Info.plist $(DIST)/bin/updater.app/Contents/Frameworks/UpdateSettings.framework/Resources endif diff --git a/toolkit/mozapps/update/updater/launchchild_osx.mm b/toolkit/mozapps/update/updater/launchchild_osx.mm index 917f282d9f..b64e85980c 100644 --- a/toolkit/mozapps/update/updater/launchchild_osx.mm +++ b/toolkit/mozapps/update/updater/launchchild_osx.mm @@ -280,8 +280,9 @@ void CleanupElevatedMacUpdate(bool aFailureOccurred) { LaunchChild(3, launchctlArgs); } -// Note: Caller is responsible for freeing argv. -bool ObtainUpdaterArguments(int* argc, char*** argv) { +// Note: Caller is responsible for freeing aArgv. +bool ObtainUpdaterArguments(int* aArgc, char*** aArgv, + MARChannelStringTable* aMARStrings) { MacAutoreleasePool pool; id updateServer = ConnectToUpdateServer(); @@ -294,15 +295,22 @@ bool ObtainUpdaterArguments(int* argc, char*** argv) { @try { NSArray* updaterArguments = [updateServer performSelector:@selector(getArguments)]; - *argc = [updaterArguments count]; - char** tempArgv = (char**)malloc(sizeof(char*) * (*argc)); - for (int i = 0; i < *argc; i++) { + *aArgc = [updaterArguments count]; + char** tempArgv = (char**)malloc(sizeof(char*) * (*aArgc)); + for (int i = 0; i < *aArgc; i++) { int argLen = [[updaterArguments objectAtIndex:i] length] + 1; tempArgv[i] = (char*)malloc(argLen); strncpy(tempArgv[i], [[updaterArguments objectAtIndex:i] UTF8String], argLen); } - *argv = tempArgv; + *aArgv = tempArgv; + + NSString* channelID = + [updateServer performSelector:@selector(getMARChannelID)]; + const char* channelIDStr = [channelID UTF8String]; + aMARStrings->MARChannelID = + mozilla::MakeUnique<char[]>(strlen(channelIDStr) + 1); + strcpy(aMARStrings->MARChannelID.get(), channelIDStr); } @catch (NSException* e) { // Let's try our best and clean up. CleanupElevatedMacUpdate(true); @@ -321,10 +329,12 @@ bool ObtainUpdaterArguments(int* argc, char*** argv) { NSArray* mUpdaterArguments; BOOL mShouldKeepRunning; BOOL mAborted; + NSString* mMARChannelID; } -- (id)initWithArgs:(NSArray*)args; +- (id)initWithArgs:(NSArray*)aArgs marChannelID:(NSString*)aMARChannelID; - (BOOL)runServer; - (NSArray*)getArguments; +- (NSString*)getMARChannelID; - (void)abort; - (BOOL)wasAborted; - (void)shutdown; @@ -333,12 +343,13 @@ bool ObtainUpdaterArguments(int* argc, char*** argv) { @implementation ElevatedUpdateServer -- (id)initWithArgs:(NSArray*)args { +- (id)initWithArgs:(NSArray*)aArgs marChannelID:(NSString*)aMARChannelID { self = [super init]; if (!self) { return nil; } - mUpdaterArguments = args; + mUpdaterArguments = aArgs; + mMARChannelID = aMARChannelID; mShouldKeepRunning = YES; mAborted = NO; return self; @@ -367,6 +378,20 @@ bool ObtainUpdaterArguments(int* argc, char*** argv) { return mUpdaterArguments; } +/** + * The MAR channel ID(s) are stored in the UpdateSettings.framework that ships + * with the updater.app bundle. When an elevated update is occurring, the + * org.mozilla.updater binary is extracted and installed individually as a + * Privileged Helper Tool. This Privileged Helper Tool does not have access to + * the UpdateSettings.framework and we therefore rely on the unelevated updater + * process to pass this information to the elevated updater process in the same + * fashion that the command line arguments are passed to the elevated updater + * process by `getArguments`. + */ +- (NSString*)getMARChannelID { + return mMARChannelID; +} + - (void)abort { mAborted = YES; [self shutdown]; @@ -386,16 +411,19 @@ bool ObtainUpdaterArguments(int* argc, char*** argv) { @end -bool ServeElevatedUpdate(int argc, const char** argv) { +bool ServeElevatedUpdate(int aArgc, const char** aArgv, + const char* aMARChannelID) { MacAutoreleasePool pool; - NSMutableArray* updaterArguments = [NSMutableArray arrayWithCapacity:argc]; - for (int i = 0; i < argc; i++) { - [updaterArguments addObject:[NSString stringWithUTF8String:argv[i]]]; + NSMutableArray* updaterArguments = [NSMutableArray arrayWithCapacity:aArgc]; + for (int i = 0; i < aArgc; i++) { + [updaterArguments addObject:[NSString stringWithUTF8String:aArgv[i]]]; } + NSString* channelID = [NSString stringWithUTF8String:aMARChannelID]; ElevatedUpdateServer* updater = - [[ElevatedUpdateServer alloc] initWithArgs:[updaterArguments copy]]; + [[ElevatedUpdateServer alloc] initWithArgs:updaterArguments + marChannelID:channelID]; bool didSucceed = [updater runServer]; [updater release]; diff --git a/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettings-WrongChannel/moz.build b/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettings-WrongChannel/moz.build new file mode 100644 index 0000000000..e1f4ad0981 --- /dev/null +++ b/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettings-WrongChannel/moz.build @@ -0,0 +1,18 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +Framework("UpdateSettings-WrongChannel") +FINAL_TARGET = "_tests/xpcshell/toolkit/mozapps/update/tests" + +DEFINES["ACCEPTED_MAR_CHANNEL_IDS"] = '"wrong-channel"' + +UNIFIED_SOURCES += [ + "../UpdateSettings/UpdateSettings.mm", +] + +OS_LIBS += [ + "-framework Foundation", +] diff --git a/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettings-xpcshell/moz.build b/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettings-xpcshell/moz.build new file mode 100644 index 0000000000..6c9b43b146 --- /dev/null +++ b/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettings-xpcshell/moz.build @@ -0,0 +1,18 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +Framework("UpdateSettings-xpcshell") +FINAL_TARGET = "_tests/xpcshell/toolkit/mozapps/update/tests" + +DEFINES["ACCEPTED_MAR_CHANNEL_IDS"] = '"xpcshell-test"' + +UNIFIED_SOURCES += [ + "../UpdateSettings/UpdateSettings.mm", +] + +OS_LIBS += [ + "-framework Foundation", +] diff --git a/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettings/Info.plist b/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettings/Info.plist new file mode 100644 index 0000000000..65777a475f --- /dev/null +++ b/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettings/Info.plist @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleExecutable</key> + <string>UpdateSettings</string> + <key>CFBundleIdentifier</key> + <string>org.mozilla.updatesettings</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>UpdateSettings</string> + <key>CFBundlePackageType</key> + <string>FMWK</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleShortVersionString</key> + <string>1.0</string> + <key>CFBundleVersion</key> + <string>1.0</string> +</dict> +</plist> diff --git a/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettings/README.md b/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettings/README.md new file mode 100644 index 0000000000..32b9d2ea48 --- /dev/null +++ b/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettings/README.md @@ -0,0 +1,77 @@ +# UpdateSettings macOS Framework + +## Summary + +The UpdateSettings macOS Framework is used to set the accepted MAR download +channels. + +## What are MAR update channels and what are they used for? + +As the name implies, MAR update channels are the channels where MAR update files +are served from and we want to ensure that the updater only applies MAR files +from accepted channels. + +## Why do we need a Framework instead of compiling the accepted MAR update channels directly into the executable? + +There are three main use cases that make it necessary for the accepted MAR +update channels to be set by external means, such as a macOS Framework: + + 1. Allowing users on the Beta channel to test RC builds + +Our beta users test release candidate builds before they are released to the +release population. The MAR files related to release candidates have their MAR +channel set to `release`. We make it possible for beta users to test these +release candidate MAR files by having beta Firefox installs accept MAR files +with their internal update channel set to either `release` or `beta`. + + 2. Switching users to another channel, such as ESR + +In contrast to the Beta use case outlined above, there are times where we +explicitly WANT to switch users to a different channel. An example of this is +when hardware or a particular macOS version have reached their EOL. In this +case, we usually switch users to our ESR channel for extended support. We switch +users to a different channel by serving a MAR file that forces a change to the +update channels that will be accepted for future updates. In other words, while +users may have previously accepted MAR update files from the `release` channel, +they now only accept MAR files from the `esr` channel. + + 3. QA update testing + +QA requires a way to temporarily switch the MAR update channel to a test channel +in order to test MAR updates before new releases. + +## How does the UpdateSettings macOS Framework address these use cases? + +We are able to accommodate all three use cases above by enabling the updater to +ignore certain files on disk if they are already present, but continue to force +update them if so desired. + +In the case of a Beta user updating to an RC build, the updater would encounter +an UpdateSettings macOS Framework inside the .app bundle that has the accepted +MAR update channels set to `beta` and `release`. In this case, the updater will +not update the Framework, but update everything else. This beta user is now able +to run the RC build with the update channel still set to `beta` and `release` +and will be able to apply MAR files related to the next beta cycle once the end +of RC builds is reached. + +In the case of switching users to the ESR channel, the updater will be set to +forcefully update the UpdateSettings macOS Framework, even if already present on +disk. After the update, the user will now be set to accept MAR updates from the +`esr` channel only. + +Before releases, QA replaces the UpdateSettings macOS Framework within the .app +bundle and set the accepted MAR update channels to a test channel in order to +test MAR updates. During testing, the new Framework file would remain in place +for typical update testing, but gets replaced in case QA was testing channel +switching. + +## Why is a macOS Framework the best solution to store the accepted MAR update channels? + +Apple has started strengthening code signature checks and the requirements on +developers such as ourselves on how their apps are signed. In particular, +most files in the .app bundle are now included in signature verifications. + +A macOS Framework is the ideal solution to store the accepted MAR update +channels because Frameworks are the only component within a .app bundle that can +be replaced without invalidating the code signature on the .app bundle, as long +as both the previous and the new Framework are signed. diff --git a/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettings/UpdateSettings.h b/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettings/UpdateSettings.h new file mode 100644 index 0000000000..65a7ba3e00 --- /dev/null +++ b/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettings/UpdateSettings.h @@ -0,0 +1,17 @@ +/* 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/. */ + +#ifndef UpdateSettings_h_ +#define UpdateSettings_h_ + +#import <Foundation/Foundation.h> + +extern "C" { + +// Returns the accepted MAR channels, as an autoreleased string. +extern NSString* UpdateSettingsGetAcceptedMARChannels(void) + __attribute__((weak_import)) __attribute__((visibility("default"))); +} + +#endif // UpdateSettings_h_ diff --git a/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettings/UpdateSettings.mm b/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettings/UpdateSettings.mm new file mode 100644 index 0000000000..2c39f13f3b --- /dev/null +++ b/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettings/UpdateSettings.mm @@ -0,0 +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/. */ + +#include "UpdateSettings.h" + +#include "mozilla/HelperMacros.h" + +NSString* UpdateSettingsGetAcceptedMARChannels(void) { + return + [NSString stringWithFormat:@"[Settings]\nACCEPTED_MAR_CHANNEL_IDS=%s\n", + ACCEPTED_MAR_CHANNEL_IDS]; +} diff --git a/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettings/moz.build b/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettings/moz.build new file mode 100644 index 0000000000..67de1d68f7 --- /dev/null +++ b/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettings/moz.build @@ -0,0 +1,24 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +Framework("UpdateSettings") + +if CONFIG["ACCEPTED_MAR_CHANNEL_IDS"]: + DEFINES["ACCEPTED_MAR_CHANNEL_IDS"] = '"%s"' % CONFIG["ACCEPTED_MAR_CHANNEL_IDS"] +else: + DEFINES["ACCEPTED_MAR_CHANNEL_IDS"] = '""' + +EXPORTS += [ + "UpdateSettings.h", +] + +UNIFIED_SOURCES += [ + "UpdateSettings.mm", +] + +OS_LIBS += [ + "-framework Foundation", +] diff --git a/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettingsUtil.h b/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettingsUtil.h new file mode 100644 index 0000000000..5964d9fb18 --- /dev/null +++ b/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettingsUtil.h @@ -0,0 +1,17 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef UpdateSettingsUtil_h_ +#define UpdateSettingsUtil_h_ + +#include <optional> +#include <string> + +class UpdateSettingsUtil { + public: + static std::optional<std::string> GetAcceptedMARChannelsValue(); +}; + +#endif // UpdateSettingsUtil_h_ diff --git a/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettingsUtil.mm b/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettingsUtil.mm new file mode 100644 index 0000000000..6555fd1350 --- /dev/null +++ b/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettingsUtil.mm @@ -0,0 +1,21 @@ +/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: + * 2 -*- */ +/* vim: set ts=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/. */ + +#import "UpdateSettings/UpdateSettings.h" + +#include "UpdateSettingsUtil.h" + +/* static */ +std::optional<std::string> UpdateSettingsUtil::GetAcceptedMARChannelsValue() { + // `UpdateSettingsGetAcceptedMARChannels` is resolved at runtime and requires + // the UpdateSettings framework to be loaded. + if (UpdateSettingsGetAcceptedMARChannels) { + NSString* marChannels = UpdateSettingsGetAcceptedMARChannels(); + return [marChannels UTF8String]; + } + return {}; +} diff --git a/toolkit/mozapps/update/updater/macos-frameworks/moz.build b/toolkit/mozapps/update/updater/macos-frameworks/moz.build new file mode 100644 index 0000000000..19fa11942e --- /dev/null +++ b/toolkit/mozapps/update/updater/macos-frameworks/moz.build @@ -0,0 +1,18 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Toolkit", "Application Update") + +DIRS += ["UpdateSettings", "UpdateSettings-xpcshell", "UpdateSettings-WrongChannel"] + +EXPORTS += [ + "UpdateSettingsUtil.h", +] + +UNIFIED_SOURCES += [ + "UpdateSettingsUtil.mm", +] diff --git a/toolkit/mozapps/update/updater/moz.build b/toolkit/mozapps/update/updater/moz.build index eede9cd723..9620dde22c 100644 --- a/toolkit/mozapps/update/updater/moz.build +++ b/toolkit/mozapps/update/updater/moz.build @@ -26,6 +26,9 @@ if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": "__launchd_plist", SRCDIR + "/Launchd.plist", ] + DIRS += [ + "macos-frameworks", + ] GENERATED_FILES = [ "dep1Cert.h", diff --git a/toolkit/mozapps/update/updater/updater-common.build b/toolkit/mozapps/update/updater/updater-common.build index fe0b4a85fa..6c6d0adf6f 100644 --- a/toolkit/mozapps/update/updater/updater-common.build +++ b/toolkit/mozapps/update/updater/updater-common.build @@ -22,6 +22,17 @@ if CONFIG["MOZ_VERIFY_MAR_SIGNATURE"]: "verifymar", ] + if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": + srcs += [ + "macos-frameworks/UpdateSettingsUtil.mm", + ] + USE_LIBS += [ + "UpdateSettings", + ] + LDFLAGS += [ + "-Wl,-rpath,@executable_path/../Frameworks/UpdateSettings.framework" + ] + if CONFIG["OS_ARCH"] == "WINNT": have_progressui = 1 srcs += [ @@ -114,7 +125,7 @@ if CONFIG["MOZ_TSAN"]: DEFINES["SPRINTF_H_USES_VSNPRINTF"] = True DEFINES["NS_NO_XPCOM"] = True DisableStlWrapping() -for var in ("MAR_CHANNEL_ID", "MOZ_APP_VERSION"): +for var in ("MAR_CHANNEL_ID", "MOZ_APP_VERSION", "ACCEPTED_MAR_CHANNEL_IDS"): DEFINES[var] = '"%s"' % CONFIG[var] LOCAL_INCLUDES += [ diff --git a/toolkit/mozapps/update/updater/updater-xpcshell/Makefile.in b/toolkit/mozapps/update/updater/updater-xpcshell/Makefile.in index 533533c4d9..7ef90f9ce5 100644 --- a/toolkit/mozapps/update/updater/updater-xpcshell/Makefile.in +++ b/toolkit/mozapps/update/updater/updater-xpcshell/Makefile.in @@ -30,6 +30,10 @@ ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT)) $(call py_action,preprocessor updater-xpcshell.app/Contents/Resources/English.lproj/InfoPlist.strings,-Fsubstitution --output-encoding utf-16 -DAPP_NAME='$(MOZ_APP_DISPLAYNAME)' $(srcdir)/../macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in -o $(XPCSHELLTESTDIR)/data/updater-xpcshell.app/Contents/Resources/English.lproj/InfoPlist.strings) $(NSINSTALL) -D $(XPCSHELLTESTDIR)/data/updater-xpcshell.app/Contents/MacOS $(NSINSTALL) $(FINAL_TARGET)/updater-xpcshell $(XPCSHELLTESTDIR)/data/updater-xpcshell.app/Contents/MacOS + $(NSINSTALL) -D $(XPCSHELLTESTDIR)/data/updater-xpcshell.app/Contents/Frameworks + $(NSINSTALL) -D $(XPCSHELLTESTDIR)/data/updater-xpcshell.app/Contents/Frameworks/UpdateSettings.framework + cp $(XPCSHELLTESTDIR)/UpdateSettings-xpcshell $(XPCSHELLTESTDIR)/data/updater-xpcshell.app/Contents/Frameworks/UpdateSettings.framework/UpdateSettings + $(NSINSTALL) $(srcdir)/../macos-frameworks/UpdateSettings/Info.plist $(XPCSHELLTESTDIR)/data/updater-xpcshell.app/Contents/Frameworks/UpdateSettings.framework/Resources rm -Rf $(XPCSHELLTESTDIR)/data/updater.app mv $(XPCSHELLTESTDIR)/data/updater-xpcshell.app $(XPCSHELLTESTDIR)/data/updater.app mv $(XPCSHELLTESTDIR)/data/updater.app/Contents/MacOS/updater-xpcshell $(XPCSHELLTESTDIR)/data/updater.app/Contents/MacOS/org.mozilla.updater diff --git a/toolkit/mozapps/update/updater/updater.cpp b/toolkit/mozapps/update/updater/updater.cpp index 6b01450b31..084945dc44 100644 --- a/toolkit/mozapps/update/updater/updater.cpp +++ b/toolkit/mozapps/update/updater/updater.cpp @@ -46,6 +46,7 @@ #include "updatecommon.h" #ifdef XP_MACOSX +# include "UpdateSettingsUtil.h" # include "updaterfileutils_osx.h" #endif // XP_MACOSX @@ -75,13 +76,16 @@ bool IsOwnedByGroupAdmin(const char* aAppBundle); bool IsRecursivelyWritable(const char* aPath); void LaunchChild(int argc, const char** argv); void LaunchMacPostProcess(const char* aAppBundle); -bool ObtainUpdaterArguments(int* argc, char*** argv); -bool ServeElevatedUpdate(int argc, const char** argv); +bool ObtainUpdaterArguments(int* aArgc, char*** aArgv, + MARChannelStringTable* aMARStrings); +bool ServeElevatedUpdate(int aArgc, const char** aArgv, + const char* aMARChannelID); void SetGroupOwnershipAndPermissions(const char* aAppBundle); bool PerformInstallationFromDMG(int argc, char** argv); struct UpdateServerThreadArgs { int argc; const NS_tchar** argv; + const char* marChannelID; }; #endif @@ -187,15 +191,6 @@ class AutoFile { } }; -struct MARChannelStringTable { - MARChannelStringTable() { - MARChannelID = mozilla::MakeUnique<char[]>(1); - MARChannelID[0] = '\0'; - } - - mozilla::UniquePtr<char[]> MARChannelID; -}; - //----------------------------------------------------------------------------- #ifdef XP_MACOSX @@ -292,6 +287,10 @@ static bool sUsingService = false; // that iteration will run with `gIsElevated == true`. static bool gIsElevated = false; +// This string contains the MAR channel IDs that are later extracted by one of +// the `ReadMARChannelIDsFrom` variants. +static MARChannelStringTable gMARStrings; + // Normally, we run updates as a result of user action (the user started Firefox // or clicked a "Restart to Update" button). But there are some cases when // we are not: @@ -2659,23 +2658,67 @@ static void WaitForServiceFinishThread(void* param) { #endif #ifdef MOZ_VERIFY_MAR_SIGNATURE +# ifndef XP_MACOSX /** * This function reads in the ACCEPTED_MAR_CHANNEL_IDS from update-settings.ini * - * @param path The path to the ini file that is to be read - * @param results A pointer to the location to store the read strings + * @param aPath The path to the ini file that is to be read + * @param aResults A pointer to the location to store the read strings + * @return OK on success + */ +static int ReadMARChannelIDsFromPath(const NS_tchar* aPath, + MARChannelStringTable* aResults) { + const unsigned int kNumStrings = 1; + const char* kUpdaterKeys = "ACCEPTED_MAR_CHANNEL_IDS\0"; + return ReadStrings(aPath, kUpdaterKeys, kNumStrings, &aResults->MARChannelID, + "Settings"); +} +# else // XP_MACOSX +/** + * This function reads in the ACCEPTED_MAR_CHANNEL_IDS from a string buffer. + * + * @param aChannels A string buffer containing the MAR channel(s). + * @param aResults A pointer to the location to store the read strings. * @return OK on success */ -static int ReadMARChannelIDs(const NS_tchar* path, - MARChannelStringTable* results) { +static int ReadMARChannelIDsFromBuffer(char* aChannels, + MARChannelStringTable* aResults) { const unsigned int kNumStrings = 1; const char* kUpdaterKeys = "ACCEPTED_MAR_CHANNEL_IDS\0"; - int result = ReadStrings(path, kUpdaterKeys, kNumStrings, - &results->MARChannelID, "Settings"); + return ReadStringsFromBuffer(aChannels, kUpdaterKeys, kNumStrings, + &aResults->MARChannelID, "Settings"); +} +# endif // XP_MACOSX - return result; +/** + * This function reads in the `ACCEPTED_MAR_CHANNEL_IDS` from the appropriate + * (platform-dependent) source and populates `gMARStrings`. + * + * @return + * `OK` on success, `UPDATE_SETTINGS_FILE_CHANNEL` on failure. + */ +static int PopulategMARStrings() { + int rv = UPDATE_SETTINGS_FILE_CHANNEL; +# ifdef XP_MACOSX + if (gIsElevated) { + // An elevated update process will have already populated gMARStrings when + // it connected to the unelevated update process to obtain the command line + // args. See `ObtainUpdaterArguments`. + rv = OK; + } else if (auto marChannels = + UpdateSettingsUtil::GetAcceptedMARChannelsValue()) { + rv = ReadMARChannelIDsFromBuffer(marChannels->data(), &gMARStrings); + } +# else + NS_tchar updateSettingsPath[MAXPATHLEN]; + NS_tsnprintf(updateSettingsPath, + sizeof(updateSettingsPath) / sizeof(updateSettingsPath[0]), + NS_T("%s/update-settings.ini"), gInstallDirPath); + rv = ReadMARChannelIDsFromPath(updateSettingsPath, &gMARStrings); +# endif + return rv == OK ? OK : UPDATE_SETTINGS_FILE_CHANNEL; } -#endif +#endif // MOZ_VERIFY_MAR_SIGNATURE static int GetUpdateFileName(NS_tchar* fileName, int maxChars) { NS_tsnprintf(fileName, maxChars, NS_T("%s/update.mar"), gPatchDirPath); @@ -2700,21 +2743,10 @@ static void UpdateThreadFunc(void* param) { } if (rv == OK) { - NS_tchar updateSettingsPath[MAXPATHLEN]; - NS_tsnprintf(updateSettingsPath, - sizeof(updateSettingsPath) / sizeof(updateSettingsPath[0]), -# ifdef XP_MACOSX - NS_T("%s/Contents/Resources/update-settings.ini"), -# else - NS_T("%s/update-settings.ini"), -# endif - gInstallDirPath); - MARChannelStringTable MARStrings; - if (ReadMARChannelIDs(updateSettingsPath, &MARStrings) != OK) { - rv = UPDATE_SETTINGS_FILE_CHANNEL; - } else { + rv = PopulategMARStrings(); + if (rv == OK) { rv = gArchiveReader.VerifyProductInformation( - MARStrings.MARChannelID.get(), MOZ_APP_VERSION); + gMARStrings.MARChannelID.get(), MOZ_APP_VERSION); } } #endif @@ -2819,7 +2851,8 @@ static void UpdateThreadFunc(void* param) { #ifdef XP_MACOSX static void ServeElevatedUpdateThreadFunc(void* param) { UpdateServerThreadArgs* threadArgs = (UpdateServerThreadArgs*)param; - gSucceeded = ServeElevatedUpdate(threadArgs->argc, threadArgs->argv); + gSucceeded = ServeElevatedUpdate(threadArgs->argc, threadArgs->argv, + threadArgs->marChannelID); if (!gSucceeded) { WriteStatusFile(ELEVATION_CANCELED); } @@ -2955,6 +2988,21 @@ int NS_main(int argc, NS_tchar** argv) { putenv(const_cast<char*>("MOZ_USING_SERVICE=")); #endif + if (argc == 2 && NS_tstrcmp(argv[1], NS_T("--channels-allowed")) == 0) { +#ifdef MOZ_VERIFY_MAR_SIGNATURE + int rv = PopulategMARStrings(); + if (rv == OK) { + printf("Channels Allowed: %s\n", gMARStrings.MARChannelID.get()); + return 0; + } + printf("Error: %d\n", rv); + return 1; +#else + printf("Not Applicable: No support for signature verification\n"); + return 0; +#endif + } + // The callback is the remaining arguments starting at callbackIndex. // The argument specified by callbackIndex is the callback executable and the // argument prior to callbackIndex is the working directory. @@ -2977,7 +3025,7 @@ int NS_main(int argc, NS_tchar** argv) { strstr(argv[0], "/Library/PrivilegedHelperTools/org.mozilla.updater") != 0; if (isElevated) { - if (!ObtainUpdaterArguments(&argc, &argv)) { + if (!ObtainUpdaterArguments(&argc, &argv, &gMARStrings)) { // Won't actually get here because ObtainUpdaterArguments will terminate // the current process on failure. return 1; @@ -3288,6 +3336,7 @@ int NS_main(int argc, NS_tchar** argv) { UpdateServerThreadArgs threadArgs; threadArgs.argc = argc; threadArgs.argv = const_cast<const NS_tchar**>(argv); + threadArgs.marChannelID = gMARStrings.MARChannelID.get(); Thread t1; if (t1.Run(ServeElevatedUpdateThreadFunc, &threadArgs) == 0) { @@ -4179,7 +4228,7 @@ int NS_main(int argc, NS_tchar** argv) { // Run update process on a background thread. ShowProgressUI may return // before QuitProgressUI has been called, so wait for UpdateThreadFunc to // terminate. Avoid showing the progress UI when staging an update, or if - // this is an elevated process on OSX. + // this is an elevated process on macOS. Thread t; if (t.Run(UpdateThreadFunc, nullptr) == 0) { if (!sStagedUpdate && !sReplaceRequest && !sUpdateSilently diff --git a/toolkit/profile/nsIToolkitProfileService.idl b/toolkit/profile/nsIToolkitProfileService.idl index f8519f1101..66cc5a6977 100644 --- a/toolkit/profile/nsIToolkitProfileService.idl +++ b/toolkit/profile/nsIToolkitProfileService.idl @@ -79,11 +79,11 @@ interface nsIToolkitProfileService : nsISupports * This returns true if a new profile was created. * This method is primarily for testing. It can be called only once. */ - bool selectStartupProfile(in Array<ACString> aArgv, - in boolean aIsResetting, in AUTF8String aUpdateChannel, - in AUTF8String aLegacyInstallHash, - out nsIFile aRootDir, out nsIFile aLocalDir, - out nsIToolkitProfile aProfile); + boolean selectStartupProfile(in Array<ACString> aArgv, + in boolean aIsResetting, in AUTF8String aUpdateChannel, + in AUTF8String aLegacyInstallHash, + out nsIFile aRootDir, out nsIFile aLocalDir, + out nsIToolkitProfile aProfile); /** * Get a profile by name. This is mainly for use by the -P diff --git a/toolkit/system/unixproxy/nsLibProxySettings.cpp b/toolkit/system/unixproxy/nsLibProxySettings.cpp index 4f6f43fb76..f2c7824554 100644 --- a/toolkit/system/unixproxy/nsLibProxySettings.cpp +++ b/toolkit/system/unixproxy/nsLibProxySettings.cpp @@ -99,6 +99,12 @@ nsresult nsUnixSystemProxySettings::GetProxyForURI(const nsACString& aSpec, return NS_OK; } +NS_IMETHODIMP +nsUnixSystemProxySettings::GetSystemWPADSetting(bool* aSystemWPADSetting) { + *aSystemWPADSetting = false; + return NS_OK; +} + NS_IMPL_COMPONENT_FACTORY(nsUnixSystemProxySettings) { return do_AddRef(new nsUnixSystemProxySettings()).downcast<nsISupports>(); } diff --git a/toolkit/themes/linux/global/toolbar.css b/toolkit/themes/linux/global/toolbar.css index 05fc5eec49..9f4550a790 100644 --- a/toolkit/themes/linux/global/toolbar.css +++ b/toolkit/themes/linux/global/toolbar.css @@ -8,11 +8,6 @@ @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); -toolbox { - appearance: auto; - -moz-default-appearance: toolbox; -} - toolbar { background-color: -moz-headerbar; color: -moz-headerbartext; diff --git a/toolkit/themes/moz.build b/toolkit/themes/moz.build index ac0a9798bb..fe4ffb6949 100644 --- a/toolkit/themes/moz.build +++ b/toolkit/themes/moz.build @@ -29,3 +29,5 @@ else: with Files("**"): BUG_COMPONENT = ("Toolkit", "Themes") + +SPHINX_TREES["shared/design-system/docs"] = "shared/design-system/docs" diff --git a/toolkit/themes/osx/global/jar.mn b/toolkit/themes/osx/global/jar.mn index 19bf28220d..6d7b1975b5 100644 --- a/toolkit/themes/osx/global/jar.mn +++ b/toolkit/themes/osx/global/jar.mn @@ -12,7 +12,6 @@ toolkit.jar: skin/classic/global/menu.css skin/classic/global/menulist.css skin/classic/global/richlistbox.css - skin/classic/global/tabprompts.css skin/classic/global/tabbox.css skin/classic/global/toolbar.css skin/classic/global/wizard.css diff --git a/toolkit/themes/osx/global/tabprompts.css b/toolkit/themes/osx/global/tabprompts.css deleted file mode 100644 index 7259cf1829..0000000000 --- a/toolkit/themes/osx/global/tabprompts.css +++ /dev/null @@ -1,62 +0,0 @@ -/* 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/. */ - -/* Tab Modal Prompt boxes */ -.tabModalBackground, -tabmodalprompt { - background-color: hsla(0,0%,10%,.5); -} - -tabmodalprompt { - font-family: sans-serif; /* use content font not system UI font */ - font-size: 110%; -} - -.tabmodalprompt-mainContainer { - color: black; - background-color: hsla(0,0%,100%,.95); - background-clip: padding-box; - border-radius: 2px; - border: 1px solid hsla(0,0%,0%,.5); -} - -.tabmodalprompt-buttonContainer { - background-color: hsla(0,0%,0%,.05); - border-top: 1px solid hsla(0,0%,0%,.05); -} - -.tabmodalprompt-buttonContainer > button { - appearance: none; - padding: 2px 0; - margin: 0; - margin-inline-start: 8px; - border-radius: 2px; - color: black !important; - background-color: hsl(0,0%,90%); - background-image: linear-gradient(hsla(0,0%,100%,.7), transparent); - background-clip: padding-box; - border: 1px solid; - border-color: hsl(0,0%,65%) hsl(0,0%,60%) hsl(0,0%,50%); - box-shadow: 0 1px 0 hsla(0,0%,100%,.9) inset, - 0 1px 2px hsla(0,0%,0%,.1); -} - -.tabmodalprompt-buttonContainer > button[default=true] { - background-color: hsl(0,0%,79%); -} - -.tabmodalprompt-buttonContainer > button:hover { - background-color: hsl(0,0%,96%); -} - -.tabmodalprompt-buttonContainer > button:hover:active { - background-image: linear-gradient(hsla(0,0%,100%,.2), transparent); - background-color: hsl(0,0%,70%); - box-shadow: 0 1px 0 hsla(0,0%,100%,.2) inset, - 0 1px 3px hsla(0,0%,0%,.2); -} - -.tabmodalprompt-buttonContainer > button:focus-visible { - outline: var(--focus-outline); -} diff --git a/toolkit/themes/osx/global/toolbar.css b/toolkit/themes/osx/global/toolbar.css index 0a60ae57d9..4148b1e934 100644 --- a/toolkit/themes/osx/global/toolbar.css +++ b/toolkit/themes/osx/global/toolbar.css @@ -4,22 +4,9 @@ @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); -toolbox { - /* Setting -moz-default-appearance:toolbox causes sheets to attach under the - * toolbox and has no other effects. It doesn't render anything. */ - appearance: auto; - -moz-default-appearance: toolbox; -} - toolbar { min-width: 1px; min-height: 20px; - appearance: auto; - -moz-default-appearance: toolbar; -} - -toolbar:-moz-lwtheme { - appearance: none; } toolbarseparator { diff --git a/toolkit/themes/shared/aboutReader.css b/toolkit/themes/shared/aboutReader.css index 6e4206e293..764dd39702 100644 --- a/toolkit/themes/shared/aboutReader.css +++ b/toolkit/themes/shared/aboutReader.css @@ -39,7 +39,6 @@ body { --tooltip-border: transparent; --popup-background: #fff; --popup-border: rgba(0, 0, 0, 0.12); - --opaque-popup-border: rgb(224, 224, 224); --popup-line: var(--grey-30); --popup-shadow: rgba(49, 49, 49, 0.3); --popup-button-background: rgba(207, 207, 216, 0.33); @@ -47,9 +46,8 @@ body { --popup-button-background-hover: var(--toolbar-button-background-hover); --popup-button-foreground-hover: var(--main-foreground); --popup-button-background-active: var(--toolbar-button-background-active); - --popup-button-border: var(--popup-border); + --popup-button-border: rgba(0, 0, 0, 0.2); --selected-background: rgba(0, 97, 224, 0.3); - --selected-border: var(--primary-color); --outline-focus-color: var(--primary-color); --font-value-background: rgb(240, 240, 244); --font-value-border: var(--grey-30); @@ -61,6 +59,11 @@ body { --link-selected-background: var(--selected-background); --link-selected-foreground: #333; --visited-link-foreground: #b5007f; + --custom-theme-background: var(--color-white); + --custom-theme-foreground: #14151A; + --custom-theme-unvisited-links: var(--color-blue-50); + --custom-theme-visited-links: #321C64; + --custom-theme-selection-highlight: #FFFFCC; /* light colours */ } @@ -72,26 +75,62 @@ body.sepia { --icon-disabled-fill: rgba(91, 70, 54, 0.4); } -body.dark { - --main-background: var(--dark-theme-background); - --main-foreground: var(--dark-theme-foreground); - --primary-color: rgb(0, 221, 255); - --toolbar-border: rgba(249, 249, 250, 0.2); +body.gray { + --main-background: var(--grey-30); + --main-foreground: var(--light-theme-foreground); + --toolbar-border: var(--main-foreground); + --icon-fill: var(--main-foreground); +} + +body.dark, +body.contrast { --toolbar-box-shadow: black; --toolbar-button-background-hover: rgb(82, 82, 94); --toolbar-button-background-active: rgb(91, 91, 102); --popup-background: rgb(66, 65, 77); - --opaque-popup-border: #434146; + --popup-button-border: rgba(255, 255, 255, 0.12); --popup-line: rgba(249, 249, 250, 0.1); --popup-button-background: rgb(43, 42, 51); - --selected-background: rgba(0, 221, 255, 0.3); --font-value-background: rgba(249, 249, 250, 0.15); --font-value-border: #656468; - --icon-fill: rgb(251, 251, 254); --icon-disabled-fill: rgba(251, 251, 254, 0.4); --link-selected-foreground: #fff; --visited-link-foreground: #e675fd; + --selected-background: rgba(0, 221, 255, 0.3); /* dark colours */ + + .moz-reader-block-img { + filter: brightness(0.8) contrast(1.2); + } +} + +body.dark { + --main-background: var(--dark-theme-background); + --main-foreground: var(--dark-theme-foreground); + --primary-color: rgb(0, 221, 255); + --toolbar-border: rgba(249, 249, 250, 0.2); + --icon-fill: rgb(251, 251, 254); +} + +body.contrast { + --main-background: #000000; + --main-foreground: #fff; + --primary-color: #D6B4FD; + --toolbar-border: #FFEE32; + --icon-fill: #FFEE32; +} + +body.custom { + --main-background: var(--custom-theme-background); + --main-foreground: var(--custom-theme-foreground); + --link-foreground: var(--custom-theme-unvisited-links); + --visited-link-foreground: var(--custom-theme-visited-links); + --popup-button-foreground: var(--light-theme-foreground); + --popup-button-foreground-hover: var(--light-theme-foreground); + --tooltip-foreground: var(--light-theme-foreground); + --toolbar-border: var(--main-foreground); + --icon-fill: var(--main-foreground); + --icon-disabled-fill: rgba(91, 91, 102, 0.4); } body.hcm { @@ -116,7 +155,6 @@ body.hcm { --tooltip-border: CanvasText; --popup-background: Canvas; --popup-border: CanvasText; - --opaque-popup-border: CanvasText; --popup-line: CanvasText; --popup-button-background: ButtonFace; --popup-button-foreground: ButtonText; @@ -138,6 +176,11 @@ body.hcm { --visited-link-foreground: VisitedText; } +body.hcm .colors-dropdown { + /* Hide entire colors menu when HCM is on. */ + display: none; +} + body { margin: 0; padding: var(--body-padding); @@ -182,31 +225,29 @@ blockquote { border-inline-start: 2px solid var(--main-foreground) !important; } -.light-button, -.auto-button { - color: var(--light-theme-foreground); - background-color: var(--light-theme-background); -} - -@media (prefers-color-scheme: dark) { +.color-scheme-buttons { + .light-button, .auto-button { - color: var(--dark-theme-foreground); - background-color: var(--dark-theme-background); + color: var(--light-theme-foreground); + background-color: var(--light-theme-background); } - .moz-reader-block-img { - filter: brightness(0.8) contrast(1.2); + @media (prefers-color-scheme: dark) { + .auto-button { + color: var(--dark-theme-foreground); + background-color: var(--dark-theme-background); + } } -} -.dark-button { - color: var(--dark-theme-foreground); - background-color: var(--dark-theme-background); -} + .dark-button { + color: var(--dark-theme-foreground); + background-color: var(--dark-theme-background); + } -.sepia-button { - color: #5b4636; - background-color: #f4ecd8; + .sepia-button { + color: #5b4636; + background-color: #f4ecd8; + } } /* Loading/error message */ @@ -311,7 +352,7 @@ blockquote { */ margin-inline-start: max(calc(50% - 17px - var(--inline-padding)), calc(100% - 96px - 34px - 2 * var(--inline-padding))); - font-family: Helvetica, Arial, sans-serif; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Ubuntu, "Helvetica Neue", sans-serif; list-style: none; user-select: none; @@ -452,40 +493,26 @@ button:disabled { .dropdown .dropdown-popup { text-align: start; position: absolute; - inset-inline-start: 40px; + inset-inline-start: 32px; z-index: 1000; background-color: var(--popup-background); visibility: hidden; border-radius: 4px; border: 1px solid var(--popup-border); box-shadow: 0 0 10px 0 var(--popup-shadow); - top: 0; -} - -.open > .dropdown-popup { - visibility: visible; + top: var(--space-xsmall); } -.dropdown-arrow { - position: absolute; - height: 24px; - width: 16px; - inset-inline-start: -16px; - background-image: url("chrome://global/skin/reader/RM-Type-Controls-Arrow.svg"); - display: block; - -moz-context-properties: fill, stroke; - fill: var(--popup-background); - stroke: var(--opaque-popup-border); - pointer-events: none; -} - -.dropdown-arrow:dir(rtl) { - transform: scaleX(-1); +.dropdown-popup h2 { + font-size: var(--font-size-root); + font-weight: var(--font-weight-bold); + color: var(--popup-button-foreground); + margin-block: var(--space-large); + margin-inline: var(--space-large) 0; } -/* Align the style dropdown arrow (narrate does its own) */ -.style-dropdown .dropdown-arrow { - top: 7px; +.open > .dropdown-popup { + visibility: visible; } /* Font style popup */ @@ -513,13 +540,14 @@ button:disabled { box-sizing: border-box; width: 36px; height: 20px; - line-height: 20px; + line-height: 18px; display: flex; justify-content: center; align-content: center; margin: auto; border-radius: 10px; border: 1px solid var(--font-value-border); + color: var(--popup-button-foreground); background-color: var(--font-value-background); } @@ -547,16 +575,11 @@ button:disabled { outline-offset: -2px; } -.radiorow:not(:last-child), -.buttonrow:not(:last-child) { +.style-dropdown .radiorow:not(:last-child), +.style-dropdown .buttonrow:not(:last-child) { border-bottom: 1px solid var(--popup-line); } -body.hcm .buttonrow.line-height-buttons { - /* On HCM the .color-scheme-buttons row is hidden, so remove the border from the row above it */ - border-bottom: none; -} - .radiorow > label { position: relative; box-sizing: border-box; @@ -565,27 +588,16 @@ body.hcm .buttonrow.line-height-buttons { } .radiorow > label[checked] { - border-color: var(--selected-border); + border: 2px solid var(--link-selected-foreground); } -/* For the hover style, we draw a line under the item by means of a - * pseudo-element. Because these items are variable height, and - * because their contents are variable height, position it absolutely, - * and give it a width of 100% (the content width) + 4px for the 2 * 2px - * border width. - */ -.radiorow > input[type=radio]:focus-visible + label::after, -.radiorow > label:hover::after { - content: ""; - display: block; - border-bottom: 2px solid var(--selected-border); - border-radius: 4px; - width: calc(100% + 4px); - position: absolute; - /* to skip the 2 * 2px border + 2px spacing. */ - bottom: -6px; - /* Match the start of the 2px border of the element: */ - inset-inline-start: -2px; +.radiorow > label:hover { + background-color: var(--toolbar-button-background-hover); +} + +.radiorow > input[type=radio]:focus-visible + label { + outline: 2px solid var(--primary-color); + outline-offset: var(--focus-outline-offset); } .font-type-buttons > label { @@ -608,6 +620,16 @@ body.hcm .buttonrow.line-height-buttons { padding-top: 2px; } +.font-type-buttons { + > label:first-of-type { + margin-inline-start: var(--space-large); + } + + > label:last-of-type { + margin-inline-end: var(--space-large); + } +} + .font-type-buttons > label[checked] { background-color: var(--selected-background); } @@ -642,6 +664,7 @@ body.hcm .color-scheme-buttons { justify-content: center; /* We want 10px between items, but there's no margin collapsing in flexbox. */ margin: 10px 5px; + border-color: var(--popup-border); } .color-scheme-buttons > input:first-child + label { @@ -652,6 +675,108 @@ body.hcm .color-scheme-buttons { margin-inline-end: 10px; } +/* Separate colors menu popup */ + +#color-controls { + padding-block-end: var(--space-large); + width: 400px; +} + +button-group { + display: flex; + font-size: var(--font-size-small); +} + +button[is="named-deck-button"] { + background: none; + color: var(--popup-button-foreground); + border: 1px var(--popup-button-border); + border-style: solid none; + padding: var(--space-small); + flex-basis: 50%; + + &[selected] { + color: var(--primary-color); + border-top: 2px solid var(--primary-color); + } +} + +.custom-colors-selection { + display: flex; + flex-direction: column; + gap: var(--space-small); + padding: var(--space-large); + list-style-type: none; + font-size: var(--font-size-root); + color: var(--popup-button-foreground); +} + +.colors-menu-color-scheme-buttons { + flex-wrap: wrap; + margin-block-start: var(--space-small); +} + +.colors-menu-color-scheme-buttons > label { + height: 48px; + width: calc(50% - 21px); + font-size: var(--font-size-root); + color: var(--popup-button-foreground); + border: 1px solid var(--popup-button-border); + border-radius: var(--border-radius-small); + /* Center content vertically and justify left horizontally */ + display: inline-flex; + align-items: center; + justify-content: start; + margin: var(--space-xsmall); +} + +.colors-menu-color-scheme-buttons > label:before { + content: ""; + display: inline-block; + width: 24px; + height: 24px; + border-radius: var(--border-radius-circle); + outline: 1px solid var(--popup-button-border); + margin: 0 10px 0 12px; +} + +.colors-menu-color-scheme-buttons { + .auto-button:before { + background: linear-gradient(to right, var(--light-theme-background) 50%, var(--dark-theme-background) 50%);; + } + + .light-button:before { + background-color: var(--light-theme-background); + } + + .dark-button:before { + background-color: var(--dark-theme-background); + } + + .sepia-button:before { + background-color: #f4ecd8; + } + + .contrast-button:before { + background-color: #000000; + } + + .gray-button:before { + background-color: var(--grey-30); + } +} + +.custom-colors-reset-button { + display: block; + background: none; + border: none; + padding: 0 var(--space-large); + color: var(--primary-color); + text-decoration: underline; + font-size: var(--font-size-root); + cursor: pointer; +} + /* Toolbar icons */ .close-button { @@ -662,6 +787,10 @@ body.hcm .color-scheme-buttons { background-image: url("chrome://global/skin/reader/RM-Type-Controls-24x24.svg"); } +.colors-button { + background-image: url("chrome://mozapps/skin/extensions/category-themes.svg"); +} + .minus-button { background-size: 18px 18px; background-image: url("chrome://global/skin/reader/RM-Minus-24x24.svg"); diff --git a/toolkit/themes/shared/design-system/design-tokens.json b/toolkit/themes/shared/design-system/design-tokens.json new file mode 100644 index 0000000000..c807335942 --- /dev/null +++ b/toolkit/themes/shared/design-system/design-tokens.json @@ -0,0 +1,944 @@ +{ + "attention": { + "dot": { + "color": { + "value": { + "platform": { + "default": "AccentColor" + }, + "brand": { + "light": "#2ac3a2", + "dark": "#54ffbd" + }, + "prefersContrast": "AccentColor" + } + } + } + }, + "background": { + "color": { + "box": { + "value": { + "light": "{color.white}", + "dark": "{color.gray.80}", + "prefersContrast": "{background.color.canvas}" + } + }, + "canvas": { + "value": { + "prefersContrast": "Canvas", + "brand": { + "light": "{color.white}", + "dark": "{color.gray.90}" + }, + "platform": { + "default": "Canvas" + } + } + }, + "critical": { + "value": { + "light": "{color.red.05}", + "dark": "{color.red.80}", + "prefersContrast": "{background.color.canvas}" + } + }, + "information": { + "value": { + "light": "{color.blue.05}", + "dark": "{color.blue.80}", + "prefersContrast": "{background.color.canvas}" + } + }, + "success": { + "value": { + "light": "{color.green.05}", + "dark": "{color.green.80}", + "prefersContrast": "{background.color.canvas}" + } + }, + "warning": { + "value": { + "light": "{color.yellow.05}", + "dark": "{color.yellow.80}", + "prefersContrast": "{background.color.canvas}" + } + } + } + }, + "border": { + "color": { + "@base": { + "value": { + "prefersContrast": "{text.color.@base}" + } + }, + "interactive": { + "@base": { + "value": { + "prefersContrast": "{text.color.@base}", + "forcedColors": "ButtonText", + "brand": { + "light": "{color.gray.60}", + "dark": "{color.gray.50}" + }, + "platform": { + "default": "color-mix(in srgb, currentColor 15%, {color.gray.60})" + } + } + }, + "hover": { + "value": { + "default": "{border.color.interactive.@base}", + "forcedColors": "SelectedItem" + } + }, + "active": { + "value": { + "default": "{border.color.interactive.@base}", + "forcedColors": "ButtonText" + } + }, + "disabled": { + "value": { + "default": "{border.color.interactive.@base}", + "forcedColors": "GrayText" + } + } + } + }, + "radius": { + "circle": { + "value": "9999px" + }, + "small": { + "value": "4px" + }, + "medium": { + "value": "8px" + } + }, + "width": { + "value": "1px" + } + }, + "box": { + "shadow": { + "10": { + "value": "0 1px 4px {color.black.a10}" + } + } + }, + "button": { + "background": { + "color": { + "@base": { + "comment": "TODO Bug 1821203 - Gray use needs to be consolidated", + "value": { + "forcedColors": "ButtonFace", + "brand": { + "default": "color-mix(in srgb, currentColor 7%, transparent)" + }, + "platform": { + "default": "var(--button-bgcolor)" + } + } + }, + "hover": { + "value": { + "forcedColors": "SelectedItemText", + "brand": { + "default": "color-mix(in srgb, currentColor 14%, transparent)" + }, + "platform": { + "default": "var(--button-hover-bgcolor)" + } + } + }, + "active": { + "value": { + "forcedColors": "SelectedItemText", + "brand": { + "default": "color-mix(in srgb, currentColor 21%, transparent)" + }, + "platform": { + "default": "var(--button-active-bgcolor)" + } + } + }, + "disabled": { + "value": { + "default": "{button.background.color.@base}", + "forcedColors": "ButtonFace" + } + }, + "primary": { + "@base": { + "value": "{color.accent.primary.@base}" + }, + "hover": { + "value": "{color.accent.primary.hover}" + }, + "active": { + "value": "{color.accent.primary.active}" + }, + "disabled": { + "value": { + "default": "{button.background.color.primary.@base}", + "forcedColors": "{button.text.color.disabled}" + } + } + }, + "destructive": { + "@base": { + "value": { + "light": "{color.red.50}", + "dark": "{color.red.30}", + "forcedColors": "{button.background.color.primary.@base}" + } + }, + "active": { + "value": { + "light": "{color.red.70}", + "dark": "{color.red.05}", + "forcedColors": "{button.background.color.primary.active}" + } + }, + "disabled": { + "value": { + "default": "{button.background.color.destructive.@base}", + "forcedColors": "{button.background.color.primary.disabled}" + } + }, + "hover": { + "value": { + "light": "{color.red.60}", + "dark": "{color.red.10}", + "forcedColors": "{button.background.color.primary.hover}" + } + } + }, + "ghost": { + "@base": { + "value": { + "default": "transparent", + "prefersContrast": "{button.background.color.@base}", + "forcedColors": "{button.background.color.@base}" + } + }, + "active": { + "value": "{button.background.color.active}" + }, + "disabled": { + "value": { + "default": "{button.background.color.ghost.@base}", + "forcedColors": "{button.background.color.disabled}" + } + }, + "hover": { + "value": "{button.background.color.hover}" + } + } + } + }, + "border": { + "@base": { + "value": "{border.width} solid {button.border.color.@base}" + }, + "color": { + "@base": { + "value": { + "default": "transparent", + "prefersContrast": "{button.text.color.@base}", + "forcedColors": "{border.color.interactive.@base}" + } + }, + "active": { + "value": { + "default": "{button.border.color.@base}", + "forcedColors": "{border.color.interactive.active}" + } + }, + "destructive": { + "@base": { + "value": { + "default": "transparent", + "forcedColors": "{button.border.color.primary.@base}" + } + }, + "active": { + "value": { + "default": "{button.border.color.destructive.@base}", + "forcedColors": "{button.border.color.primary.active}" + } + }, + "disabled": { + "value": { + "default": "{button.border.color.destructive.@base}", + "forcedColors": "{button.border.color.primary.disabled}" + } + }, + "hover": { + "value": { + "default": "{button.border.color.destructive.@base}", + "forcedColors": "{button.border.color.primary.hover}" + } + } + }, + "disabled": { + "value": { + "default": "{button.border.color.@base}", + "forcedColors": "{border.color.interactive.disabled}" + } + }, + "ghost": { + "@base": { + "value": { + "default": "{button.border.color.@base}" + } + }, + "active": { + "value": { + "default": "{button.border.color.active}" + } + }, + "disabled": { + "value": { + "default": "{button.border.color.disabled}" + } + }, + "hover": { + "value": { + "default": "{button.border.color.hover}" + } + } + }, + "hover": { + "value": { + "default": "{button.border.color.@base}", + "forcedColors": "{border.color.interactive.hover}" + } + }, + "primary": { + "@base": { + "value": { + "default": "transparent", + "forcedColors": "ButtonFace" + } + }, + "active": { + "value": { + "default": "{button.border.color.primary.@base}", + "forcedColors": "ButtonText" + } + }, + "disabled": { + "value": "{button.border.color.primary.@base}" + }, + "hover": { + "value": { + "default": "{button.border.color.primary.@base}", + "forcedColors": "SelectedItemText" + } + } + } + }, + "radius": { + "value": "{border.radius.small}" + } + }, + "font": { + "size": { + "@base": { + "value": "{font.size.root}" + }, + "small": { + "value": "{font.size.small}" + } + }, + "weight": { + "value": "{font.weight.bold}" + } + }, + "min": { + "height": { + "@base": { + "value": "{size.item.large}" + }, + "small": { + "value": "{size.item.medium}" + } + } + }, + "opacity": { + "disabled": { + "value": { + "default": 0.5, + "forcedColors": 1 + } + } + }, + "padding": { + "@base": { + "value": "{space.xsmall} {space.large}" + }, + "icon": { + "value": 0 + } + }, + "size": { + "icon": { + "@base": { + "value": "{button.min.height.@base}" + }, + "small": { + "value": "{button.min.height.small}" + } + } + }, + "text": { + "color": { + "@base": { + "value": { + "forcedColors": "ButtonText", + "brand": { + "light": "{color.gray.100}", + "dark": "{color.gray.05}" + }, + "platform": { + "default": "var(--button-color)" + } + } + }, + "active": { + "value": { + "default": "{button.text.color.@base}", + "forcedColors": "SelectedItem" + } + }, + "destructive": { + "@base": { + "value": { + "light": "{color.gray.05}", + "dark": "{color.gray.100}", + "forcedColors": "{button.text.color.primary.@base}" + } + }, + "active": { + "value": { + "default": "{button.text.color.destructive.@base}", + "forcedColors": "{button.text.color.primary.active}" + } + }, + "disabled": { + "value": { + "default": "{button.text.color.destructive.@base}", + "forcedColors": "{button.text.color.primary.disabled}" + } + }, + "hover": { + "value": { + "default": "{button.text.color.destructive.@base}", + "forcedColors": "{button.text.color.primary.hover}" + } + } + }, + "disabled": { + "value": { + "default": "{button.text.color.@base}", + "forcedColors": "GrayText" + } + }, + "ghost": { + "@base": { + "value": { + "default": "{button.text.color.@base}" + } + }, + "active": { + "value": { + "default": "{button.text.color.active}" + } + }, + "disabled": { + "value": { + "default": "{button.text.color.disabled}" + } + }, + "hover": { + "value": { + "default": "{button.text.color.hover}" + } + } + }, + "hover": { + "value": { + "default": "{button.text.color.@base}", + "forcedColors": "SelectedItem" + } + }, + "primary": { + "@base": { + "value": { + "forcedColors": "ButtonFace", + "brand": { + "light": "{color.gray.05}", + "dark": "{color.gray.100}" + }, + "platform": { + "default": "var(--button-primary-color)" + } + } + }, + "active": { + "value": "{button.text.color.primary.hover}" + }, + "disabled": { + "value": "{button.text.color.primary.@base}" + }, + "hover": { + "value": { + "default": "{button.text.color.primary.@base}", + "forcedColors": "SelectedItemText" + } + } + } + } + } + }, + "checkbox": { + "margin": { + "inline": { + "value": "{space.small}" + } + }, + "size": { + "comment": "TODO Bug 1876537: Make this em-based, probably?", + "value": "{size.item.small}" + } + }, + "color": { + "black": { + "a10": { + "value": "rgba(0, 0, 0, 0.1)" + } + }, + "blue": { + "05": { + "value": "#deeafc" + }, + "30": { + "value": "#73a7f3" + }, + "50": { + "value": "#0060df" + }, + "60": { + "value": "#0250bb" + }, + "70": { + "value": "#054096" + }, + "80": { + "value": "#003070" + } + }, + "cyan": { + "20": { + "value": "#aaf2ff" + }, + "30": { + "value": "#80ebff" + }, + "50": { + "value": "#00ddff" + } + }, + "gray": { + "05": { + "value": "#fbfbfe" + }, + "50": { + "value": "#bfbfc9" + }, + "60": { + "value": "#8f8f9d" + }, + "70": { + "value": "#5b5b66" + }, + "80": { + "value": "#23222b" + }, + "90": { + "value": "#1c1b22" + }, + "100": { + "value": "#15141a" + } + }, + "green": { + "05": { + "value": "#d8eedc" + }, + "30": { + "value": "#4dbc87" + }, + "50": { + "value": "#017a40" + }, + "80": { + "value": "#004725" + } + }, + "red": { + "05": { + "value": "#ffe8e8" + }, + "10": { + "value": "#ffbdc5" + }, + "20": { + "value": "#ff9aa2" + }, + "30": { + "value": "#f37f98" + }, + "50": { + "value": "#d7264c" + }, + "60": { + "value": "#ac1e3d" + }, + "70": { + "value": "#8a1831" + }, + "80": { + "value": "#690f22" + } + }, + "yellow": { + "05": { + "value": "#ffebcd" + }, + "30": { + "value": "#e49c49" + }, + "50": { + "value": "#cd411e" + }, + "80": { + "value": "#5a3100" + } + }, + "white": { + "value": "#ffffff" + }, + "accent": { + "primary": { + "@base": { + "value": { + "forcedColors": "ButtonText", + "brand": { + "light": "{color.blue.50}", + "dark": "{color.cyan.50}" + }, + "platform": { + "default": "var(--button-primary-bgcolor, AccentColor)" + } + } + }, + "hover": { + "value": { + "forcedColors": "SelectedItem", + "brand": { + "light": "{color.blue.60}", + "dark": "{color.cyan.30}" + }, + "platform": { + "default": "var(--button-primary-hover-bgcolor)" + } + } + }, + "active": { + "value": { + "forcedColors": "{color.accent.primary.hover}", + "brand": { + "light": "{color.blue.70}", + "dark": "{color.cyan.20}" + }, + "platform": { + "default": "var(--button-primary-active-bgcolor)" + } + } + } + } + } + }, + "focus": { + "outline": { + "@base": { + "value": "{focus.outline.width} solid {focus.outline.color}" + }, + "color": { + "value": "{color.accent.primary.@base}" + }, + "inset": { + "value": "calc(-1 * {focus.outline.width})" + }, + "offset": { + "value": "2px" + }, + "width": { + "value": "2px" + } + } + }, + "font": { + "size": { + "root": { + "value": { + "brand": { + "default": "15px" + }, + "platform": { + "default": "unset" + } + } + }, + "small": { + "value": { + "brand": { + "default": "0.867rem" + }, + "platform": { + "default": "unset" + } + } + }, + "large": { + "value": { + "brand": { + "default": "1.133rem" + }, + "platform": { + "default": "unset" + } + } + }, + "xlarge": { + "value": { + "brand": { + "default": "1.467rem" + }, + "platform": { + "default": "unset" + } + } + }, + "xxlarge": { + "value": { + "brand": { + "default": "1.6rem" + }, + "platform": { + "default": "unset" + } + } + } + }, + "weight": { + "@base": { + "value": "normal" + }, + "bold": { + "value": 600 + } + } + }, + "icon": { + "color": { + "@base": { + "value": { + "light": "{color.gray.70}", + "dark": "{color.gray.05}", + "prefersContrast": "{text.color.@base}" + } + }, + "information": { + "value": { + "light": "{color.blue.50}", + "dark": "{color.blue.30}", + "prefersContrast": "{icon.color.@base}" + } + }, + "success": { + "value": { + "light": "{color.green.50}", + "dark": "{color.green.30}", + "prefersContrast": "{icon.color.@base}" + } + }, + "warning": { + "value": { + "light": "{color.yellow.50}", + "dark": "{color.yellow.30}", + "prefersContrast": "{icon.color.@base}" + } + }, + "critical": { + "value": { + "light": "{color.red.50}", + "dark": "{color.red.30}", + "prefersContrast": "{icon.color.@base}" + } + } + }, + "size": { + "default": { + "value": "{size.item.small}" + } + } + }, + "input": { + "text": { + "min": { + "height": { + "value": "{button.min.height.@base}" + } + } + } + }, + "link": { + "color": { + "@base": { + "value": { + "prefersContrast": "LinkText", + "brand": { + "default": "{color.accent.primary.@base}" + }, + "platform": { + "default": "LinkText" + } + } + }, + "hover": { + "value": { + "prefersContrast": "LinkText", + "brand": { + "default": "{color.accent.primary.hover}" + }, + "platform": { + "default": "LinkText" + } + } + }, + "active": { + "value": { + "prefersContrast": "ActiveText", + "brand": { + "default": "{color.accent.primary.active}" + }, + "platform": { + "default": "ActiveText" + } + } + }, + "visited": { + "value": { + "prefersContrast": "{link.color.@base}", + "brand": { + "default": "{link.color.@base}" + }, + "platform": { + "default": "{link.color.@base}" + } + } + } + }, + "focus": { + "outline": { + "offset": { + "comment": "Not using --force-outline-offset for links because that's intended for\nelements with a background, and we only want a slight offset here while\nnot overlapping adjacent text", + "value": "1px" + } + } + } + }, + "outline": { + "color": { + "error": { + "value": { + "light": "{color.red.50}", + "dark": "{color.red.20}", + "prefersContrast": "{border.color.@base}" + } + } + } + }, + "size": { + "item": { + "small": { + "value": "16px" + }, + "medium": { + "value": "28px" + }, + "large": { + "value": "32px" + } + } + }, + "space": { + "xxsmall": { + "value": "calc(0.5 * {space.xsmall})" + }, + "xsmall": { + "value": "0.267rem" + }, + "small": { + "value": "calc(2 * {space.xsmall})" + }, + "medium": { + "value": "calc(3 * {space.xsmall})" + }, + "large": { + "value": "calc(4 * {space.xsmall})" + }, + "xlarge": { + "value": "calc(6 * {space.xsmall})" + }, + "xxlarge": { + "value": "calc(8 * {space.xsmall})" + } + }, + "text": { + "color": { + "@base": { + "value": { + "prefersContrast": "CanvasText", + "brand": { + "light": "{color.gray.100}", + "dark": "{color.gray.05}" + }, + "platform": { + "default": "currentColor" + } + } + }, + "deemphasized": { + "value": { + "default": "color-mix(in srgb, currentColor 69%, transparent)", + "prefersContrast": "inherit" + } + }, + "error": { + "value": { + "light": "{color.red.50}", + "dark": "{color.red.20}", + "prefersContrast": "inherit" + } + } + } + } +} diff --git a/toolkit/themes/shared/design-system/README.design-tokens.stories.md b/toolkit/themes/shared/design-system/docs/README.design-tokens.stories.md index 156ab35e19..52651c1d59 100644 --- a/toolkit/themes/shared/design-system/README.design-tokens.stories.md +++ b/toolkit/themes/shared/design-system/docs/README.design-tokens.stories.md @@ -41,173 +41,15 @@ _The following documentation borrows from Nathan Curtis' essay on [Naming Design Design tokens' variable names follow a taxonomy with distinct classification levels and sublevels, forming a prescriptive vocabulary of descriptive terms which are classified by category. -<div class="box container-width-large"> -<div class="box"> - <div class="box vertical"> - <b>Ecosystem</b> - <div class="post-it"><b>Domain</b></div> - </div> - <div class="box vertical"> - <b>Object</b> - <div class="box"> - <div class="post-it green"><b>Pattern</b></div> - <div class="post-it green"><b>Component</b></div> - <div class="post-it green"><b>Element</b></div> - </div> - </div> - <div class="box vertical"> - <b>Category</b> - <div class="box"> - <div class="post-it orange"><b>Type</b></div> - <div class="post-it orange"><b>Concept</b></div> - <div class="post-it orange"><b>Property</b></div> - </div> - </div> - <div class="box vertical"> - <b>Modifier</b> - <div class="box"> - <div class="post-it blue"><b>Variant</b></div> - <div class="post-it blue"><b>State</b></div> - <div class="post-it blue"><b>Scale</b></div> - </div> - </div> -</div> - -<div class="box"> - <div class="box vertical"> - <div class="post-it disabled"></div> - </div> - <div class="box vertical"> - <div class="box"> - <div class="post-it green disabled"></div> - <div class="post-it green disabled"></div> - <div class="post-it green disabled"></div> - </div> - </div> - <div class="box vertical"> - <div class="box"> - <div class="post-it orange">Color</div> - <div class="post-it orange disabled"></div> - <div class="post-it orange disabled"></div> - </div> - </div> - <div class="box vertical"> - <div class="box"> - <div class="post-it blue">Blue</div> - <div class="post-it blue disabled"></div> - <div class="post-it blue">50</div> - </div> - </div> -</div> - -<div class="box"> - <div class="box vertical"> - <div class="post-it disabled"></div> - </div> - <div class="box vertical"> - <div class="box"> - <div class="post-it green disabled"></div> - <div class="post-it green disabled"></div> - <div class="post-it green disabled"></div> - </div> - </div> - <div class="box vertical"> - <div class="box"> - <div class="post-it orange">Size</div> - <div class="post-it orange">Item</div> - <div class="post-it orange disabled"></div> - </div> - </div> - <div class="box vertical"> - <div class="box"> - <div class="post-it blue disabled"></div> - <div class="post-it blue disabled"></div> - <div class="post-it blue">Small</div> - </div> - </div> -</div> - -<div class="box"> - <div class="box vertical"> - <div class="post-it disabled"></div> - </div> - <div class="box vertical"> - <div class="box"> - <div class="post-it green disabled"></div> - <div class="post-it green">Link</div> - <div class="post-it green disabled"></div> - </div> - </div> - <div class="box vertical"> - <div class="box"> - <div class="post-it orange disabled"></div> - <div class="post-it orange disabled"></div> - <div class="post-it orange">Color</div> - </div> - </div> - <div class="box vertical"> - <div class="box"> - <div class="post-it blue disabled"></div> - <div class="post-it blue disabled"></div> - <div class="post-it blue disabled"></div> - </div> - </div> -</div> - -<div class="box"> - <div class="box vertical"> - <div class="post-it disabled"></div> - </div> - <div class="box vertical"> - <div class="box"> - <div class="post-it green disabled"></div> - <div class="post-it green">Link</div> - <div class="post-it green">Focus Outline</div> - </div> - </div> - <div class="box vertical"> - <div class="box"> - <div class="post-it orange disabled"></div> - <div class="post-it orange disabled"></div> - <div class="post-it orange">Color</div> - </div> - </div> - <div class="box vertical"> - <div class="box"> - <div class="post-it blue disabled"></div> - <div class="post-it blue disabled"></div> - <div class="post-it blue disabled"></div> - </div> - </div> -</div> - -<div class="box"> - <div class="box vertical"> - <div class="post-it">Shopping</div> - </div> - <div class="box vertical"> - <div class="box"> - <div class="post-it green disabled"></div> - <div class="post-it green">Card</div> - <div class="post-it green disabled"></div> - </div> - </div> - <div class="box vertical"> - <div class="box"> - <div class="post-it orange disabled"></div> - <div class="post-it orange disbaled"></div> - <div class="post-it orange">Border Color</div> - </div> - </div> - <div class="box vertical"> - <div class="box"> - <div class="post-it blue disabled"></div> - <div class="post-it blue disabled"></div> - <div class="post-it blue disabled"></div> - </div> - </div> -</div> -</div> +![ + Our design system taxonomy has distinct classifiction levels and sublevels. \ + There are four high level buckets: Ecosystem, Object, Category, and Modifier. \ + Within the Ecosystem bucket is one sublevel: Domain. \ + Within the Object bucket are three sublevels: Pattern, Component, and Element. \ + Within the Category bucket are three sublevels: Type, Concept, and Property. \ + Within the Modifier bucket are three sublevels: Variant, State, and Scale. \ + Each of these levels and sublevels are explained in further detail after this graphic. \ + ](./img/taxonomy-overview.png) ### Ecosystem The ecosystem level helps describe the context that a token is scoped to. @@ -219,17 +61,7 @@ For example, if a token is specific to a certain feature, you can use the domain Example from [browser/components/shopping/content/shopping-container.css](https://searchfox.org/mozilla-central/rev/02841791400cf7cf5760c0cfaf31f5d772624253/browser/components/shopping/content/shopping-container.css#7): -<div class="box"> - <div class="post-it big"> - <strong>shopping</strong> - </div> - <div class="post-it blue big"> - header - </div> - <div class="post-it blue big"> - font-size - </div> -</div> +![Example showing "shopping" as the 'Domain' part of the "shopping-header-font-size" token](./img/ecosystem-domain.png) ### Objects The object level helps define the object (or objects) that the token applies to. @@ -237,44 +69,17 @@ The object level helps define the object (or objects) that the token applies to. #### Pattern A design pattern that is composed of, or represents, multiple related components. -<div class="box"> - <div class="post-it big"> - <strong>input</strong> - </div> - <div class="post-it blue big"> - text - </div> - <div class="post-it blue big"> - min-height - </div> -</div> +![Example showing "input" as the 'Pattern' part of the "input-text-min-height" token](./img/objects-pattern-example.png) #### Component The component name. -<div class="box"> - <div class="post-it big"> - <strong>toggle</strong> - </div> - <div class="post-it blue big"> - background-color - </div> -</div> +![Example showing "toggle" as the 'Component' part of the "toggle-background-color" token](./img/component-name-example.png) #### Nested element Any element that may be nested within a component (e.g. an icon). -<div class="box"> - <div class="post-it blue big"> - message-bar - </div> - <div class="post-it big"> - <strong>icon</strong> - </div> - <div class="post-it blue big"> - color - </div> -</div> +![Example showing "icon" as the 'Nested Element' part of the "message-bar-icon-color" token](./img/nested-element-example.png) ### Categories The category level helps define the visual style that apply to the token. @@ -282,32 +87,12 @@ The category level helps define the visual style that apply to the token. #### Type The type of style category a design token belongs to. -<div class="box"> - <div class="post-it big"> - <strong>color</strong> - </div> - <div class="post-it blue big"> - blue - </div> - <div class="post-it blue big"> - 50 - </div> -</div> +![Example showing "color" as the 'Type' part of the "color-blue-50" token](./img/categories-type-example.png) #### Concept A concept further describes user interface styles. They are either industry-wide patterns, or they are terms determined by our team based on specific user interface style needs. For example, "accent" is a common design industry term used for deliniating a brand's or product's accent color(s) that we happen to use for our color tokens. -<div class="box"> - <div class="post-it blue big"> - color - </div> - <div class="post-it big"> - <strong>accent</strong> - </div> - <div class="post-it blue big"> - primary - </div> -</div> +![Example showing "accent" as the 'Concept" part of the "color-accent-primary" token](./img/categories-concept-example.png) To further illustrate this taxonomy level, here are detailed explanations and definitions of existing concepts: @@ -315,11 +100,11 @@ To further illustrate this taxonomy level, here are detailed explanations and de We use the "accent" color concept for referring to our brand and the operating system (platform) accent colors. The brand and platform accent colors are used as the primary color for accentuating and characterizing several Firefox UI elements' (e.g. buttons, focus outlines, links, icons, and more). ##### Interactive -We use the "interactive" concept to describe design tokens that pertain to interactive elements. For example, `--border-interactive-color` is used on [moz-toggle](https://searchfox.org/mozilla-central/rev/956e25260926097a4d54d5aeb0e06347841616bf/toolkit/content/widgets/moz-toggle/moz-toggle.css#40) since interactive elements such as toggles, radios, and checkboxes share the same border color pattern that is different from our default border color. +We use the "interactive" concept to describe design tokens that pertain to interactive elements. For example, `--border-color-interactive` is used on [moz-toggle](https://searchfox.org/mozilla-central/rev/956e25260926097a4d54d5aeb0e06347841616bf/toolkit/content/widgets/moz-toggle/moz-toggle.css#40) since interactive elements such as toggles, radios, and checkboxes share the same border color pattern that is different from our default border color. ```css /* moz-toggle.css */ ---toggle-border-color: var(--border-interactive-color); +--toggle-border-color: var(--border-color-interactive); ``` ##### Item @@ -334,14 +119,7 @@ We use the "item" concept as a modifier on top of the "size" type tokens group t #### Property A property (e.g. size, width, color, fill) further describes a design tokens' style, although this is not to be confused with the categorical type of token mentioned above, albeit they often use similar terms. Note that sometimes properties are double-worded, and that's totally fine (e.g. min-width, background-color) -<div class="box"> - <div class="post-it big"> - <strong>border-radius</strong> - </div> - <div class="post-it blue big"> - circle - </div> -</div> +![Example showing "border-radius" as the 'Property' part of the "border-radius-circle" token](./img/categories-property-example.png) ### Modifiers The modifier level helps further classify a design token's characteristic with further specification. @@ -349,61 +127,21 @@ The modifier level helps further classify a design token's characteristic with f #### Variant A variant specifies a token from a group of tokens related by a common meaning but that have varying purpose. -<div class="box vertical"> -<div class="box"> - <div class="post-it blue big"> - icon - </div> - <div class="post-it blue big"> - color - </div> - <div class="post-it big"> - <strong>success</strong> - </div> -</div> - -<div class="box"> - <div class="post-it blue big"> - icon - </div> - <div class="post-it blue big"> - color - </div> - <div class="post-it big"> - <strong>critical</strong> - </div> -</div> -</div> +![Two examples: + one showing "success" as the 'Variant' part of the "icon-color-success" token. \ + another showing "critical" as the 'Variant' part of the "icon-color-critical" token. \ +](./img/modifiers-variant-example.png) #### State A state defines possible intereactive states of a design token. (e.g. hover, active, focus, disabled) -<div class="box"> - <div class="post-it blue big"> - button - </div> - <div class="post-it blue big"> - background-color - </div> - <div class="post-it big"> - <strong>hover</strong> - </div> -</div> +![Example showing "hover" as the 'State' part of the "button-background-color-hover" token](./img/modifiers-state-example.png) #### Scale A scale defines a collection of tokens that relate to one another's but vary by their type, such as a collection of size units, or any other relationship that requires differentiating tokens by a determined scale. -<div class="box vertical"> -<div class="box"> - <div class="post-it blue big"> - font-size - </div> - <div class="post-it big"> - <strong>small</strong> - </div> -</div> -</div> +![Example showing "small" as the 'Scale' part of the "font-size-small" token](./img/modifiers-scale-example.png) Today we have scales based off a sequence of numbers or t-shirt sizing. @@ -442,52 +180,20 @@ You will see that the font size scale is missing what would be a logical "medium The `--font-size-root` token was created for specific use under the document's `:root` in order to set the default font size for our relative typography scale. We label our default font size token as `root` in order to be specific **and** intentional. -<div class="box"> - <div class="post-it blue big"> - font-size - </div> - <div class="post-it big"> - <strong>root</strong> - </div> -</div> +![Example showing "root" being the 'Scale' part of the "font-size-root" token](./img/modifiers-font-size-example.png) It's okay to include intentional terms within scales that better represent the meaning of a value and when to use it. For example, our border radius collection, which uses t-shirt sizing, also mixes the 'circle' option within its scale in order to describe a border radius that will create a circular effect: -<div class="box"> - <div class="post-it blue big"> - border-radius - </div> - <div class="post-it big"> - <strong>circle</strong> - </div> -</div> +![Example showing "circle" being the 'Scale' part of the "border-radius-circle" token](./img/modifiers-border-radius-example.png) ## Naming guidelines The goal of design tokens naming is modularity and legibility. -<div class="box container-width-large"> - <div class="box vertical"> - <b>Ecosystem</b> - Domain - <div class="post-it big green">brand</div> - </div> - <div class="box"> - <div class="box vertical"> - <b>Category</b> - <div class="box"> - <div class="box vertical"> - Type - <div class="post-it big orange">color</div> - </div> - <div class="box vertical"> - Concept - <div class="post-it big orange">accent</div> - </div> - </div> - </div> - </div> -</div> +![ + An example showing the "brand-color-accent" token. \ + "brand" is part of the 'Domain' ecosystem, while "color" and "accent" belong to 'Type' and 'Concept' sublevel of Category respectively. + ](./img/modifiers-naming-guidelines.png) Meanings and the relationship between meanings can be complex, therefore taxonomy levels are chained to provide clarity. (see example above) @@ -521,62 +227,13 @@ Shared tokens ([tokens-shared.css](https://searchfox.org/mozilla-central/source/ [common-shared.css](https://searchfox.org/mozilla-central/source/toolkit/themes/shared/in-content/common-shared.css) imports `tokens-brand.css` so that in-content/about: pages can make use of our brand values, while [global-shared.css](https://searchfox.org/mozilla-central/source/toolkit/themes/shared/global-shared.css), which styles the chrome, imports `tokens-platform.css` so that the chrome can access operating system and themeable values. -<div class="box"> - <div class="box vertical"> - <div class="box vertical align-center"> - <div class="post-it big"> - <a title="tokens-shared.css" href="https://searchfox.org/mozilla-central/source/toolkit/themes/shared/design-system/tokens-shared.css">tokens-shared.css</a> - </div> - </div> - <div class="box justify-center"> - ↙ ↘ - </div> - <div class="box justify-center"> - <div class="box vertical"> - <div class="post-it big"> - <a title="tokens-brand.css" href="https://searchfox.org/mozilla-central/source/toolkit/themes/shared/design-system/tokens-brand.css">tokens-brand.css</a> - </div> - <div class="box justify-center"> - ↓ - </div> - </div> - <div class="box vertical"> - <div class="post-it big"> - <a title="tokens-platform.css" href="https://searchfox.org/mozilla-central/source/toolkit/themes/shared/design-system/tokens-platform.css">tokens-platform.css</a> - </div> - <div class="box justify-center"> - ↓ - </div> - </div> - </div> - <div class="box justify-center"> - <div class="box vertical"> - <div class="post-it orange big"> - <a title="common-shared.css" href="https://searchfox.org/mozilla-central/source/toolkit/themes/shared/in-content/common-shared.css">common-shared.css</a> - </div> - <div class="box justify-center"> - ↓ - </div> - </div> - <div class="box vertical"> - <div class="post-it orange big"> - <a title="global-shared.css" href="https://searchfox.org/mozilla-central/source/toolkit/themes/shared/global-shared.css">global-shared.css</a> - </div> - <div class="box justify-center"> - ↓ - </div> - </div> - </div> - <div class="box justify-center"> - <div class="post-it blue big"> - Styles in-content (about:) pages - </div> - <div class="post-it blue big"> - Styles the chrome - </div> - </div> - </div> -</div> +![ + A diagram of the token files' hierarchy. \ + Tokens-shared.css is imported by both tokens-brand.css and tokens-platform.css. \ + There are two branches now, tokens-brand and tokens-platform. \ + For the tokens-brand branch, these tokens are then imported by common-shared.css which then styles in-content pages. \ + For the tokens-platform branch, these tokens are imported by global-shared.css which then styles the chrome. \ +](./img/token-files-hierarchy.png) #### `tokens-brand.css` This file is for token values specific to the brand, such as colors and @@ -612,7 +269,7 @@ For example, both the chrome and in-content pages make use of the same border-ra ### Tiers #### Base -Base design tokens act as the foundation for the design tokens collection. This tier defines our collection of raw styles that are used to form our semantic design token names. These tokens should not be used directly for styling UI as they don’t carry any meaning and just serve as a base for the design tokens structure. +Base design tokens represent the most basic, or foundational, groups of design tokens that point to the actual hard-coded values of the design system. They can be referenced to create more meaningful tokens. ```css /* tokens-shared.css */ @@ -622,7 +279,7 @@ Base design tokens act as the foundation for the design tokens collection. This ``` #### Application -Application design tokens represent the collection of semantic design tokens that actually give proper meaning to style choices. This tier relies on the agreement of the name and meaning behind these styles and their values. +Application design tokens represent the more semantic groups of design tokens that give meaning to base values based on their purpose or how/where they are applied. ```css /* tokens-brand.css */ @@ -630,9 +287,9 @@ Application design tokens represent the collection of semantic design tokens tha ``` #### Component -Component is the final tier. This tier is used in order to give meaning to identified style choices made within user interface components. While the "Application" tier can handle most if not all styling use cases, tier 3 helps encapsulate style decisions at the component level. +Component design tokens represent design tokens scoped to a specific component or element. While the "Application" tier can handle most if not all styling use cases, tier 3 helps encapsulate style decisions at the component level. -Component-level tokens should live at the component-level file (e.g. [moz-toggle.css](https://searchfox.org/mozilla-central/rev/02841791400cf7cf5760c0cfaf31f5d772624253/toolkit/content/widgets/moz-toggle/moz-toggle.css#34-50)), so that they can't be used outside of that specific component's domain. +Although some component-specific tokens for basic HTML elements (e.g. button) live in `tokens-shared.css` today, component-specific tokens should live at the component-level file (e.g. [moz-toggle.css](https://searchfox.org/mozilla-central/rev/02841791400cf7cf5760c0cfaf31f5d772624253/toolkit/content/widgets/moz-toggle/moz-toggle.css#34-50)) so that they can't be used outside of that specific component's domain. ```css /* moz-toggle.css */ @@ -676,29 +333,7 @@ Any semantic tokens within a scale, such as the `--font-size-root` example below --font-size-xxlarge: 2.2rem; /* 33px */ ``` -[Base](#base) tokens should be listed at the top of the file, with a heading indicating that they are Base: -```css -/* Base tokens */ -/* Do not use base tokens directly. Their names don't carry any specific meaning, and they are simply used to set foundations. Refer to Application tokens below. */ -/** Color **/ ---color-white: #ffffff; ---color-blue-05: #deeafc; ---color-blue-30: #73a7f3; -... -``` - -[Application](#application) tokens should be added after Base tokens, accompanied by its own heading as well: -```css -/* Application tokens */ -/** Border **/ ---border-radius-circle: 9999px; ---border-radius-small: 4px; ---border-width: 1px; -... -``` - ## Theming - For certain components, their high-contrast mode design will need styles that other modes do not (e.g. HCM relies on borders that do not exist in non-HCM). In those instances, we just add tokens under the high contrast mode media query rules. On the other hand, if something such as a color, does not apply to HCM contexts, then we add those design tokens under a "@media not (prefers-contrast)" query. ### Light and dark diff --git a/toolkit/themes/shared/design-system/docs/README.json-design-tokens.stories.md b/toolkit/themes/shared/design-system/docs/README.json-design-tokens.stories.md new file mode 100644 index 0000000000..1081481f9f --- /dev/null +++ b/toolkit/themes/shared/design-system/docs/README.json-design-tokens.stories.md @@ -0,0 +1,290 @@ +# JSON design tokens +## Background +The benefit of storing design tokens with a platform-agnostic format such as JSON is that it can be converted or translated into other languages or tools (e.g CSS, Swift, Kotlin, Figma). + +## Quick start +`design-tokens.json` holds our source of truth for design tokens in `mozilla-central` under the [design-system](https://searchfox.org/mozilla-central/source/toolkit/themes/shared/design-system) folder in `toolkit/themes/shared`. The CSS design token files in that folder come from the JSON file. If you need to modify a design token file, you should be editing the JSON. + +In order for us to be able to define design tokens in one place (the JSON file) and allow all platforms to consume design tokens in their specific format, we use a build system called [Style Dictionary](https://amzn.github.io/style-dictionary/#/). + +Here's how to build design tokens for desktop: + +```sh +$ ./mach buildtokens +``` + +If successful, you should see Style Dictionary building all of our tokens files within the `design-system` folder. Otherwise, Style Dictionary can also generate helpful errors to help you debug. + +At the end, we're capable of transforming JSON notation into CSS: + +```json +{ + "color": { + "blue": { + "05": { + "value": "#deeafc" + }, + "30": { + "value": "#73a7f3" + }, + "50": { + "value": "#0060df" + }, + "60": { + "value": "#0250bb" + }, + "70": { + "value": "#054096" + }, + "80": { + "value": "#003070" + } + }, + }, +} +``` + +```css +--color-blue-05: #deeafc; +--color-blue-30: #73a7f3; +--color-blue-50: #0060df; +--color-blue-60: #0250bb; +--color-blue-70: #054096; +--color-blue-80: #003070; +``` + +Neat! + +## Testing + +We have basic tests in place to ensure that our built CSS files stay up to date +and that new tokens are properly categorized. These tests will fail if the JSON +is modified but the command to build the tokens CSS files doesn't run, or if the +tokens CSS files are modified directly without changing the JSON. + +Our tests are Node-based to allow us to install and work with the +`style-dictionary` library, and follow a format that has been used previously +for Devtools and New Tab tests. + +You can run the tests locally using the following command: + +```sh +$ ./mach npm test --prefix=toolkit/themes/shared/design-system +``` + +## JSON format deep dive + +Style Dictionary works by ingesting JSON files with tokens data and performing various platform-specific transformations to output token files formatted for different languages. The library has certain quirks and limitations that we had to take into consideration when coming up with a JSON format for representing our tokens. The following is a bit of a "how to" guide for reading and adding to [`design-tokens.json`](https://searchfox.org/mozilla-central/source/toolkit/themes/shared/design-system/design-tokens.json) for anyone who needs to consume our tokens or add new tokens. + +### Naming + +When going from token name to JSON, each place we would insert a hyphen, underscore, or case change in a variable name translates to a layer of nesting in our JSON. That means a token with the CSS variable name `--border-radius-circle` would be represented as: + +```json +"border": { + "radius": { + "circle": { + "value": "9999px" + } + } +} +``` + +The `value` key is required as this is the indicator [Style Dictionary looks for](https://amzn.github.io/style-dictionary/#/architecture?id=_4a-transform-the-tokens) to know what sections of the JSON to parse into distinct tokens. It gets omitted from the final variable name. + +For simple cases `value` will be either a string or a number, but when we have to take media queries or themes into account `value` will be an object. Examples of these cases will be explained in more detail below. + +### `@base` + +When reading through our JSON you will see many instances where we have used an `@base` key, always appearing right before a `value` key indicating that everything up to that point should be parsed as a token. For example our `--font-weight` CSS token is represented like this in our JSON: + +```json +"font": { + "weight": { + "@base": { + "value": "normal" + } + } +} +``` +This is a workaround for a [known](https://github.com/amzn/style-dictionary/issues/119) [limitation](https://github.com/amzn/style-dictionary/issues/366) of Style Dictionary where it doesn't support nested token names that appear after a `value` key. If we want to have both `--font-weight` and `--font-weight-bold` CSS tokens they need to be represented as distinct objects with their own `value`s: + +```json +"font": { + "weight": { + "@base": { + "value": "normal" + }, + "bold": { + "value": 600 + } + } +} +``` +`@base` is a special indicator we chose to enable Style Dictionary to parse our tokens correctly while still generating our desired variable names. Much like `value`, it ultimately [gets removed](https://searchfox.org/mozilla-central/rev/159929cd10b8fba135c72a497d815ab2dd5a521c/toolkit/themes/shared/design-system/tokens-config.js#94-96) from the final token name. + +You will also see many places where `@base` is referenced in a value definition. This is a bit of a gotcha - even though `@base` isn't part of the token name, we still need to include it when using Style Dictionary's syntax for [variable references/aliases](https://amzn.github.io/style-dictionary/#/tokens?id=referencing-aliasing). That means the following CSS: + +```css +--input-text-min-height: var(--button-min-height); +``` + +Will look like this in our JSON: + +```json +"input": { + "text": { + "min": { + "height": { + "value": "{button.min.height.@base}" + } + } + } +} +``` + +### High Contrast Mode (HCM) media queries + +We need to be able to change our token values to support both the `prefers-contrast` and `forced-colors` [media queries](https://firefox-source-docs.mozilla.org/accessible/HCMMediaQueries.html). We employ `prefersContrast` and `forcedColors` keys nested in a token's `value` key to indicate that we need to generate a media query entry for that token. For example this JSON: + +```json +"border": { + "color": { + "interactive": { + "@base": { + "value": { + "prefersContrast": "{text.color.@base}", + "forcedColors": "ButtonText", + } + } + }, + } +} +``` +results in the following CSS: + +```css +/* tokens-shared.css */ + +@layer tokens-prefers-contrast { + @media (prefers-contrast) { + :root, + :host(.anonymous-content-host) { + /** Border **/ + --border-color-interactive: var(--text-color); + } + } +} + +@layer tokens-forced-colors { + @media (forced-colors) { + :root, + :host(.anonymous-content-host) { + /** Border **/ + --border-color-interactive: ButtonText; + } + } +} +``` + +### Theming + +Our JSON supports two dimensions of theming designed around the needs of the Firefox desktop client; light and dark themes, and brand and platform themes. + +#### Light and dark themes + +We employ `light` and `dark` keys in our `value` objects to indicate when a given token should have different values in our light and dark themes: + +```json +"background": { + "color": { + "box": { + "value": { + "light": "{color.white}", + "dark": "{color.gray.80}", + } + }, + } +} +``` + +The above JSON communicates that `--background-color-box` should have the value of `var(--color-white)` in our light theme and `var(--color-gray-80)` in our dark theme. + +If a token has the same value for both the light and dark themes it will either have a string or number as its `value` or it will use a `default` key: + +```json +"button": { + "background": { + "color": { + "disabled": { + "value": { + "default": "{button.background.color.@base}", + } + } + } + } +} +``` + +The above JSON indicates that `--button-background-color-disabled` will have the value of `var(--button-background-color)` regardless of theme. + +#### Brand and platform themes + +The Firefox desktop client consists of [two distinct surfaces](https://acorn.firefox.com/latest/resources/browser-anatomy/desktop-ZaxCgqkt#section-anatomy-fd); "the chrome," or the UI of the browser application that surrounds web pages, and the web content itself which is often referred to as "in-content." Firefox UI development spans both surfaces since our `about:` pages are "in-content" pages. In our design system we distinguish between these surfaces by using the terminology of `platform` vs `brand`. Chrome specific token values live in [`tokens-platform.css`](https://searchfox.org/mozilla-central/source/toolkit/themes/shared/design-system/tokens-platform.css) and in-content specific token values live in [`tokens-brand.css`](https://searchfox.org/mozilla-central/source/toolkit/themes/shared/design-system/tokens-brand.css). + +We use `platform` and `brand` keys in our JSON to indicate when a token has a surface-specific value. For example this token definition: + +```json +"text": { + "color": { + "@base": { + "value": { + "brand": { + "light": "{color.gray.100}", + "dark": "{color.gray.05}" + }, + "platform": { + "default": "currentColor" + } + } + }, + } +} +``` +communicates that `--text-color` should have the value `currentColor` in `tokens-platform.css` for chrome surfaces, and the value `light-dark(var(--color-gray-100), var(--color-gray-05))` in `tokens-brand.css` for in-content pages. The resulting CSS spans multiple files: + +```css +/* tokens-platform.css */ +@layer tokens-foundation { + :root, + :host(.anonymous-content-host) { + /** Text **/ + --text-color: currentColor; + } +} +``` + +```css +/* tokens-brand.css */ +@layer tokens-foundation { + :root, + :host(.anonymous-content-host) { + /** Text **/ + --text-color: light-dark(var(--color-gray-100), var(--color-gray-05)); + } +} +``` + +### Adding new tokens + +In order to add a new token you will need to make changes to `design-tokens.json` - any files generated based on our JSON token definitions should never be modified directly. You should be able to work backwards from the variable name in whatever language you're working with to figure out the correct JSON structure. + +Once you've made your changes, you can generate the new tokens CSS files by running `./mach buildtokens`. + +If you encounter errors or get unexpected results when running the build command it may be because: + +* You forgot to nest everything in a `value` key and consequently Style Dictionary is not parsing your tokens correctly, or; +* You omitted `@base` when referencing another token, or; +* You are referencing another token that you haven't defined in the JSON yet - Style Dictionary should give descriptive error messages in this case. + +If you need any help feel free to reach out in #firefox-reusable-components on Slack or #reusable-components on Matrix. diff --git a/toolkit/themes/shared/design-system/docs/img/categories-concept-example.png b/toolkit/themes/shared/design-system/docs/img/categories-concept-example.png Binary files differnew file mode 100644 index 0000000000..6fc7681898 --- /dev/null +++ b/toolkit/themes/shared/design-system/docs/img/categories-concept-example.png diff --git a/toolkit/themes/shared/design-system/docs/img/categories-property-example.png b/toolkit/themes/shared/design-system/docs/img/categories-property-example.png Binary files differnew file mode 100644 index 0000000000..0304beb049 --- /dev/null +++ b/toolkit/themes/shared/design-system/docs/img/categories-property-example.png diff --git a/toolkit/themes/shared/design-system/docs/img/categories-type-example.png b/toolkit/themes/shared/design-system/docs/img/categories-type-example.png Binary files differnew file mode 100644 index 0000000000..6a8cba0377 --- /dev/null +++ b/toolkit/themes/shared/design-system/docs/img/categories-type-example.png diff --git a/toolkit/themes/shared/design-system/docs/img/component-name-example.png b/toolkit/themes/shared/design-system/docs/img/component-name-example.png Binary files differnew file mode 100644 index 0000000000..14aaf57516 --- /dev/null +++ b/toolkit/themes/shared/design-system/docs/img/component-name-example.png diff --git a/toolkit/themes/shared/design-system/docs/img/ecosystem-domain.png b/toolkit/themes/shared/design-system/docs/img/ecosystem-domain.png Binary files differnew file mode 100644 index 0000000000..a2024fd6bc --- /dev/null +++ b/toolkit/themes/shared/design-system/docs/img/ecosystem-domain.png diff --git a/toolkit/themes/shared/design-system/docs/img/modifiers-border-radius-example.png b/toolkit/themes/shared/design-system/docs/img/modifiers-border-radius-example.png Binary files differnew file mode 100644 index 0000000000..fe94e2855e --- /dev/null +++ b/toolkit/themes/shared/design-system/docs/img/modifiers-border-radius-example.png diff --git a/toolkit/themes/shared/design-system/docs/img/modifiers-font-size-example.png b/toolkit/themes/shared/design-system/docs/img/modifiers-font-size-example.png Binary files differnew file mode 100644 index 0000000000..8df20aa7b6 --- /dev/null +++ b/toolkit/themes/shared/design-system/docs/img/modifiers-font-size-example.png diff --git a/toolkit/themes/shared/design-system/docs/img/modifiers-naming-guidelines.png b/toolkit/themes/shared/design-system/docs/img/modifiers-naming-guidelines.png Binary files differnew file mode 100644 index 0000000000..3197e53cc5 --- /dev/null +++ b/toolkit/themes/shared/design-system/docs/img/modifiers-naming-guidelines.png diff --git a/toolkit/themes/shared/design-system/docs/img/modifiers-scale-example.png b/toolkit/themes/shared/design-system/docs/img/modifiers-scale-example.png Binary files differnew file mode 100644 index 0000000000..6d39a1a740 --- /dev/null +++ b/toolkit/themes/shared/design-system/docs/img/modifiers-scale-example.png diff --git a/toolkit/themes/shared/design-system/docs/img/modifiers-state-example.png b/toolkit/themes/shared/design-system/docs/img/modifiers-state-example.png Binary files differnew file mode 100644 index 0000000000..2d24c1af28 --- /dev/null +++ b/toolkit/themes/shared/design-system/docs/img/modifiers-state-example.png diff --git a/toolkit/themes/shared/design-system/docs/img/modifiers-variant-example.png b/toolkit/themes/shared/design-system/docs/img/modifiers-variant-example.png Binary files differnew file mode 100644 index 0000000000..bd854d923e --- /dev/null +++ b/toolkit/themes/shared/design-system/docs/img/modifiers-variant-example.png diff --git a/toolkit/themes/shared/design-system/docs/img/nested-element-example.png b/toolkit/themes/shared/design-system/docs/img/nested-element-example.png Binary files differnew file mode 100644 index 0000000000..8f6db5c329 --- /dev/null +++ b/toolkit/themes/shared/design-system/docs/img/nested-element-example.png diff --git a/toolkit/themes/shared/design-system/docs/img/objects-pattern-example.png b/toolkit/themes/shared/design-system/docs/img/objects-pattern-example.png Binary files differnew file mode 100644 index 0000000000..f26500d131 --- /dev/null +++ b/toolkit/themes/shared/design-system/docs/img/objects-pattern-example.png diff --git a/toolkit/themes/shared/design-system/docs/img/taxonomy-overview.png b/toolkit/themes/shared/design-system/docs/img/taxonomy-overview.png Binary files differnew file mode 100644 index 0000000000..d5df5622e4 --- /dev/null +++ b/toolkit/themes/shared/design-system/docs/img/taxonomy-overview.png diff --git a/toolkit/themes/shared/design-system/docs/img/token-files-hierarchy.png b/toolkit/themes/shared/design-system/docs/img/token-files-hierarchy.png Binary files differnew file mode 100644 index 0000000000..423fc4ccf5 --- /dev/null +++ b/toolkit/themes/shared/design-system/docs/img/token-files-hierarchy.png diff --git a/toolkit/themes/shared/design-system/package-lock.json b/toolkit/themes/shared/design-system/package-lock.json new file mode 100644 index 0000000000..b45ca87094 --- /dev/null +++ b/toolkit/themes/shared/design-system/package-lock.json @@ -0,0 +1,1405 @@ +{ + "name": "design-system", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "design-system", + "version": "1.0.0", + "license": "MPL-2.0", + "devDependencies": { + "style-dictionary": "^3.9.2" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/capital-case": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", + "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/change-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", + "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", + "dev": true, + "dependencies": { + "camel-case": "^4.1.2", + "capital-case": "^1.0.4", + "constant-case": "^3.0.4", + "dot-case": "^3.0.4", + "header-case": "^2.0.4", + "no-case": "^3.0.4", + "param-case": "^3.0.4", + "pascal-case": "^3.1.2", + "path-case": "^3.0.4", + "sentence-case": "^3.0.4", + "snake-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/constant-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", + "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case": "^2.0.2" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/header-case": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", + "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", + "dev": true, + "dependencies": { + "capital-case": "^1.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", + "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sentence-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", + "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/style-dictionary": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/style-dictionary/-/style-dictionary-3.9.2.tgz", + "integrity": "sha512-M2pcQ6hyRtqHOh+NyT6T05R3pD/gwNpuhREBKvxC1En0vyywx+9Wy9nXWT1SZ9ePzv1vAo65ItnpA16tT9ZUCg==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "change-case": "^4.1.2", + "commander": "^8.3.0", + "fs-extra": "^10.0.0", + "glob": "^10.3.10", + "json5": "^2.2.2", + "jsonc-parser": "^3.0.0", + "lodash": "^4.17.15", + "tinycolor2": "^1.4.1" + }, + "bin": { + "style-dictionary": "bin/style-dictionary" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "dev": true + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/upper-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", + "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/upper-case-first": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", + "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + } + }, + "dependencies": { + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + } + }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "requires": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "capital-case": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", + "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", + "dev": true, + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "change-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", + "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", + "dev": true, + "requires": { + "camel-case": "^4.1.2", + "capital-case": "^1.0.4", + "constant-case": "^3.0.4", + "dot-case": "^3.0.4", + "header-case": "^2.0.4", + "no-case": "^3.0.4", + "param-case": "^3.0.4", + "pascal-case": "^3.1.2", + "path-case": "^3.0.4", + "sentence-case": "^3.0.4", + "snake-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true + }, + "constant-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", + "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", + "dev": true, + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case": "^2.0.2" + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + } + }, + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "header-case": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", + "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", + "dev": true, + "requires": { + "capital-case": "^1.0.4", + "tslib": "^2.0.3" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "requires": { + "tslib": "^2.0.3" + } + }, + "lru-cache": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", + "dev": true + }, + "minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true + }, + "no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "requires": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "requires": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "path-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", + "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", + "dev": true, + "requires": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "requires": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + } + }, + "sentence-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", + "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", + "dev": true, + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + }, + "snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dev": true, + "requires": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + } + } + }, + "style-dictionary": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/style-dictionary/-/style-dictionary-3.9.2.tgz", + "integrity": "sha512-M2pcQ6hyRtqHOh+NyT6T05R3pD/gwNpuhREBKvxC1En0vyywx+9Wy9nXWT1SZ9ePzv1vAo65ItnpA16tT9ZUCg==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "change-case": "^4.1.2", + "commander": "^8.3.0", + "fs-extra": "^10.0.0", + "glob": "^10.3.10", + "json5": "^2.2.2", + "jsonc-parser": "^3.0.0", + "lodash": "^4.17.15", + "tinycolor2": "^1.4.1" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "dev": true + }, + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true + }, + "upper-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", + "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", + "dev": true, + "requires": { + "tslib": "^2.0.3" + } + }, + "upper-case-first": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", + "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", + "dev": true, + "requires": { + "tslib": "^2.0.3" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + } + } + }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + } + } +} diff --git a/toolkit/themes/shared/design-system/package.json b/toolkit/themes/shared/design-system/package.json new file mode 100644 index 0000000000..2ced9f47bb --- /dev/null +++ b/toolkit/themes/shared/design-system/package.json @@ -0,0 +1,13 @@ +{ + "name": "design-system", + "version": "1.0.0", + "description": "Package file for node modules used to create our CSS tokens from a JSON source of truth.", + "license": "MPL-2.0", + "scripts": { + "build": "(npm ls || npm ci) && style-dictionary build --config ./tokens-config.js && prettier ./tokens-storybook.mjs --write", + "test": "(npm ls || npm ci) && node tests/try-runner.js" + }, + "devDependencies": { + "style-dictionary": "^3.9.2" + } +} diff --git a/toolkit/themes/shared/design-system/tests/try-runner.js b/toolkit/themes/shared/design-system/tests/try-runner.js new file mode 100644 index 0000000000..67ddfe3733 --- /dev/null +++ b/toolkit/themes/shared/design-system/tests/try-runner.js @@ -0,0 +1,119 @@ +/* eslint-disable no-console */ +/* 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/. */ + +/* eslint-env node */ + +/* + * A small test runner/reporter for node-based tests, + * which are run via taskcluster node(debugger). + * + * Adapted from: + * https://searchfox.org/mozilla-central/rev/9cd4ea81e27db6b767f1d9bbbcf47da238dd64fa/browser/components/newtab/bin/try-runner.js + */ + +const { readFileSync, rmSync } = require("fs"); +const chalk = require("chalk"); +const path = require("path"); +const StyleDictionary = require("style-dictionary"); +const config = require("../tokens-config.js"); + +const TEST_BUILD_PATH = "tests/build/css/"; + +function buildFilesWithTestConfig() { + // Use our real config, just modify some values for the test. This prevents us + // from re-building the CSS files that get checked in when we run the tests. + let testConfig = Object.assign({}, config); + testConfig.source = [path.join(__dirname, "../design-tokens.json")]; + testConfig.platforms.css.buildPath = TEST_BUILD_PATH; + + // This is effectively the same as running `npm run build` and allows us to + // use the modified config. + StyleDictionary.extend(testConfig).buildAllPlatforms(); +} + +function logErrors(tool, errors) { + for (const error of errors) { + console.log(`TEST-UNEXPECTED-FAIL | ${tool} | ${error}`); + } + return errors; +} + +function logStart(name) { + console.log(`TEST-START | ${name}`); +} + +const FILE_PATHS = { + "tokens-brand.css": { + path: path.join("tokens-brand.css"), + testPath: path.join(TEST_BUILD_PATH, "tokens-brand.css"), + }, + "tokens-platform.css": { + path: path.join("tokens-platform.css"), + testPath: path.join(TEST_BUILD_PATH, "tokens-platform.css"), + }, + "tokens-shared.css": { + path: path.join("tokens-shared.css"), + testPath: path.join(TEST_BUILD_PATH, "tokens-shared.css"), + }, +}; + +const tests = { + // Verify the CSS files build successfully and are up to date. + buildCSS() { + logStart("build CSS"); + + let errors = []; + let currentCSS = {}; + + // Read the contents of our built CSS files. + for (let [fileName, { path }] of Object.entries(FILE_PATHS)) { + currentCSS[fileName] = readFileSync(path, "utf8"); + } + + try { + buildFilesWithTestConfig(); + } catch { + errors.push("CSS build did not run successfully"); + } + + // Build CSS files to the test directory and compare them to the current CSS + // files that get checked in. If the contents don't match we either forgot + // to build the files after making a change, or edited the CSS files directly. + for (let [fileName, { testPath }] of Object.entries(FILE_PATHS)) { + let builtCSS = readFileSync(testPath, "utf8"); + + if (builtCSS !== currentCSS[fileName]) { + errors.push(`${fileName} is out of date`); + } + + if (builtCSS.includes("/** Unspecified **/")) { + errors.push( + "Tokens present in the 'Unspecified' section. Please update TOKEN_SECTIONS in tokens-config.js" + ); + } + } + + logErrors("build CSS", errors); + rmSync("tests/build", { recursive: true, force: true }); + return errors.length === 0; + }, +}; + +(function runTests() { + let results = []; + + for (let testName of Object.keys(tests)) { + results.push([testName, tests[testName]()]); + } + + for (const [name, result] of results) { + // Colorize output based on result. + console.log(result ? chalk.green(`✓ ${name}`) : chalk.red(`✗ ${name}`)); + } + + const success = results.every(([, result]) => result); + process.exitCode = success ? 0 : 1; + console.log("CODE", process.exitCode); +})(); diff --git a/toolkit/themes/shared/design-system/tokens-brand.css b/toolkit/themes/shared/design-system/tokens-brand.css index 0ab73834d0..4574cf405a 100644 --- a/toolkit/themes/shared/design-system/tokens-brand.css +++ b/toolkit/themes/shared/design-system/tokens-brand.css @@ -2,56 +2,49 @@ * 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/. */ +/* DO NOT EDIT this file directly, instead modify design-tokens.json + * and run `npm run build` to see your changes. */ + @import url("chrome://global/skin/design-system/tokens-shared.css"); @layer tokens-foundation { :root, :host(.anonymous-content-host) { + /** Attention Dot **/ + --attention-dot-color: light-dark(#2ac3a2, #54ffbd); + + /** Background Color **/ + --background-color-canvas: light-dark(var(--color-white), var(--color-gray-90)); + /** Border **/ - --border-interactive-color: light-dark(var(--color-gray-60), var(--color-gray-50)); + --border-color-interactive: light-dark(var(--color-gray-60), var(--color-gray-50)); /** Button **/ - /* TODO Bug 1821203 - Gray use needs to be consolidated */ - --button-background-color: color-mix(in srgb, currentColor 7%, transparent); + --button-background-color: color-mix(in srgb, currentColor 7%, transparent); /* TODO Bug 1821203 - Gray use needs to be consolidated */ --button-background-color-hover: color-mix(in srgb, currentColor 14%, transparent); --button-background-color-active: color-mix(in srgb, currentColor 21%, transparent); - --button-border-color-primary: transparent; --button-text-color: light-dark(var(--color-gray-100), var(--color-gray-05)); --button-text-color-primary: light-dark(var(--color-gray-05), var(--color-gray-100)); - /** Link **/ - --link-color: var(--color-accent-primary); - --link-color-hover: var(--color-accent-primary-hover); - --link-color-active: var(--color-accent-primary-active); - --link-color-visited: var(--link-color); - /** Color **/ --color-accent-primary: light-dark(var(--color-blue-50), var(--color-cyan-50)); --color-accent-primary-hover: light-dark(var(--color-blue-60), var(--color-cyan-30)); --color-accent-primary-active: light-dark(var(--color-blue-70), var(--color-cyan-20)); - --color-canvas: light-dark(var(--color-white), var(--color-gray-90)); - /** Font size **/ - --font-size-root: 15px; /* Set at the `:root`. Do not use */ - --font-size-small: 0.867rem; /* 13px */ - --font-size-large: 1.133rem; /* 17px */ - --font-size-xlarge: 1.467rem; /* 22px */ - --font-size-xxlarge: 1.6rem; /* 24px */ + /** Font Size **/ + --font-size-root: 15px; + --font-size-small: 0.867rem; + --font-size-large: 1.133rem; + --font-size-xlarge: 1.467rem; + --font-size-xxlarge: 1.6rem; + + /** Link **/ + --link-color: var(--color-accent-primary); + --link-color-hover: var(--color-accent-primary-hover); + --link-color-active: var(--color-accent-primary-active); + --link-color-visited: var(--link-color); /** Text **/ --text-color: light-dark(var(--color-gray-100), var(--color-gray-05)); } } - -@layer tokens-prefers-contrast { - @media (prefers-contrast) { - :root, - :host(.anonymous-content-host) { - /* Border */ - --border-interactive-color: var(--text-color); - --border-interactive-color-hover: var(--border-interactive-color); - --border-interactive-color-active: var(--border-interactive-color); - --border-interactive-color-disabled: var(--border-interactive-color); - } - } -} diff --git a/toolkit/themes/shared/design-system/tokens-config.js b/toolkit/themes/shared/design-system/tokens-config.js new file mode 100644 index 0000000000..81d92d8faa --- /dev/null +++ b/toolkit/themes/shared/design-system/tokens-config.js @@ -0,0 +1,547 @@ +/* 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/. */ + +/* eslint-env node */ + +const StyleDictionary = require("style-dictionary"); +const { createPropertyFormatter } = StyleDictionary.formatHelpers; +const TOKEN_SECTIONS = { + "Attention Dot": "attention-dot", + "Background Color": "background-color", + Border: "border", + "Box Shadow": "box-shadow", + Button: "button", + Checkbox: "checkbox", + Color: ["brand-color", "color", "platform-color"], + "Focus Outline": "focus-outline", + "Font Size": "font-size", + "Font Weight": "font-weight", + Icon: "icon", + "Input - Text": "input-text", + Link: "link", + "Outline Color": "outline-color", + Size: "size", + Space: "space", + Text: "text", + Unspecified: "", +}; +const TSHIRT_ORDER = [ + "circle", + "xxxsmall", + "xxsmall", + "xsmall", + "small", + "medium", + "large", + "xlarge", + "xxlarge", + "xxxlarge", +]; +const STATE_ORDER = [ + "base", + "default", + "root", + "hover", + "active", + "focus", + "disabled", +]; + +/** + * Adds the Mozilla Public License header in one comment and + * how to make changes in the generated output files via the + * design-tokens.json file in another comment. Also imports + * tokens-shared.css when applicable. + * + * @param {string} surface + * Desktop surface, either "brand" or "platform". Determines + * whether or not we need to import tokens-shared.css. + * @returns {string} Formatted comment header string + */ +let customFileHeader = ({ surface, platform }) => { + let licenseString = [ + "/* 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/. */", + ].join("\n"); + + let commentString = [ + "/* DO NOT EDIT this file directly, instead modify design-tokens.json", + " * and run `npm run build` to see your changes. */", + ].join("\n"); + + let cssImport = surface + ? `@import url("chrome://global/skin/design-system/tokens-shared.css");\n\n` + : ""; + let layerString = + !surface && !platform + ? `@layer tokens-foundation, tokens-prefers-contrast, tokens-forced-colors;\n\n` + : ""; + + return [ + licenseString + "\n\n" + commentString + "\n\n" + cssImport + layerString, + ]; +}; + +const NEST_MEDIA_QUERIES_COMMENT = `/* Bug 1879900: Can't nest media queries inside of :host, :root selector + until Bug 1879349 lands */`; + +const MEDIA_QUERY_PROPERTY_MAP = { + "forced-colors": "forcedColors", + "prefers-contrast": "prefersContrast", +}; + +function formatBaseTokenNames(str) { + return str.replaceAll(/(?<tokenName>\w+)-base(?=\b)/g, "$<tokenName>"); +} + +/** + * Creates a surface-specific formatter. The formatter is used to build + * our different CSS files, including "prefers-contrast" and "forced-colors" + * media queries. See more at + * https://amzn.github.io/style-dictionary/#/formats?id=formatter + * + * @param {string} surface + * Which desktop area we are generating CSS for. + * Either "brand" (i.e. in-content) or "platform" (i.e. chrome). + * @returns {Function} - Formatter function that returns a CSS string. + */ +const createDesktopFormat = surface => args => { + return formatBaseTokenNames( + customFileHeader({ surface }) + + formatTokens({ + surface, + args, + }) + + formatTokens({ + mediaQuery: "prefers-contrast", + surface, + args, + }) + + formatTokens({ + mediaQuery: "forced-colors", + surface, + args, + }) + ); +}; + +/** + * Formats a subset of tokens into CSS. Wraps token CSS in a media query when + * applicable. + * + * @param {object} tokenArgs + * @param {string} [tokenArgs.mediaQuery] + * Media query formatted CSS should be wrapped in. This is used + * to determine what property we are parsing from the token values. + * @param {string} [tokenArgs.surface] + * Specifies a desktop surface, either "brand" or "platform". + * @param {object} tokenArgs.args + * Formatter arguments provided by style-dictionary. See more at + * https://amzn.github.io/style-dictionary/#/formats?id=formatter + * @returns {string} Tokens formatted into a CSS string. + */ +function formatTokens({ mediaQuery, surface, args }) { + let prop = MEDIA_QUERY_PROPERTY_MAP[mediaQuery] ?? "default"; + let dictionary = Object.assign({}, args.dictionary); + let tokens = []; + + dictionary.allTokens.forEach(token => { + let originalVal = getOriginalTokenValue(token, prop, surface); + if (originalVal != undefined) { + let formattedToken = transformTokenValue(token, originalVal, dictionary); + tokens.push(formattedToken); + } + }); + + if (!tokens.length) { + return ""; + } + + dictionary.allTokens = dictionary.allProperties = tokens; + + let formattedVars = formatVariables({ + format: "css", + dictionary, + outputReferences: args.options.outputReferences, + formatting: { + indentation: mediaQuery ? " " : " ", + }, + }); + + let layer = `tokens-${mediaQuery ?? "foundation"}`; + // Weird spacing below is unfortunately necessary for formatting the built CSS. + if (mediaQuery) { + return ` +${NEST_MEDIA_QUERIES_COMMENT} +@layer ${layer} { + @media (${mediaQuery}) { + :root, + :host(.anonymous-content-host) { +${formattedVars} + } + } +} +`; + } + + return `@layer ${layer} { + :root, + :host(.anonymous-content-host) { +${formattedVars} + } +} +`; +} + +/** + * Finds the original value of a token for a given media query and surface. + * + * @param {object} token - Token object parsed by style-dictionary. + * @param {string} prop - Name of the property we're querying for. + * @param {string} surface + * The desktop surface we're generating CSS for, either "brand" or "platform". + * @returns {string} The original token value based on our parameters. + */ +function getOriginalTokenValue(token, prop, surface) { + if (surface) { + return token.original.value[surface]?.[prop]; + } else if (prop == "default" && typeof token.original.value != "object") { + return token.original.value; + } + return token.original.value?.[prop]; +} + +/** + * Updates a token's value to the relevant original value after resolving + * variable references. + * + * @param {object} token - Token object parsed from JSON by style-dictionary. + * @param {string} originalVal + * Original value of the token for the combination of surface and media query. + * @param {object} dictionary + * Object of transformed tokens and helper fns provided by style-dictionary. + * @returns {object} Token object with an updated value. + */ +function transformTokenValue(token, originalVal, dictionary) { + let value = originalVal; + if (dictionary.usesReference(value)) { + dictionary.getReferences(value).forEach(ref => { + value = value.replace(`{${ref.path.join(".")}}`, `var(--${ref.name})`); + }); + } + return { ...token, value }; +} + +/** + * Creates a light-dark transform that works for a given surface. Registers + * the transform with style-dictionary and returns the transform's name. + * + * @param {string} surface + * The desktop surface we're generating CSS for, either "brand", "platform", + * or "shared". + * @returns {string} Name of the transform that was registered. + */ +const createLightDarkTransform = surface => { + let name = `lightDarkTransform/${surface}`; + + // Matcher function for determining if a token's value needs to undergo + // a light-dark transform. + let matcher = token => { + if (surface != "shared") { + return ( + token.original.value[surface]?.light && + token.original.value[surface]?.dark + ); + } + return token.original.value.light && token.original.value.dark; + }; + + // Function that uses the token's original value to create a new "default" + // light-dark value and updates the original value object. + let transformer = token => { + if (surface != "shared") { + let lightDarkVal = `light-dark(${token.original.value[surface].light}, ${token.original.value[surface].dark})`; + token.original.value[surface].default = lightDarkVal; + return token.value; + } + let value = `light-dark(${token.original.value.light}, ${token.original.value.dark})`; + token.original.value.default = value; + return value; + }; + + StyleDictionary.registerTransform({ + type: "value", + transitive: true, + name, + matcher, + transformer, + }); + + return name; +}; + +/** + * Format the tokens dictionary to a string. This mostly defers to + * StyleDictionary.createPropertyFormatter but first it sorts the tokens based + * on the groupings in TOKEN_SECTIONS and adds comment headers to CSS output. + * + * @param {object} options + * Options for tokens to format. + * @param {string} options.format + * The format to output. Supported: "css" + * @param {object} options.dictionary + * The tokens dictionary. + * @param {string} options.outputReferences + * Whether to output variable references. + * @param {object} options.formatting + * The formatting settings to be passed to createPropertyFormatter. + * @returns {string} The formatted tokens. + */ +function formatVariables({ format, dictionary, outputReferences, formatting }) { + let lastSection = []; + let propertyFormatter = createPropertyFormatter({ + outputReferences, + dictionary, + format, + formatting, + }); + + let outputParts = []; + let remainingTokens = [...dictionary.allTokens]; + let isFirst = true; + + function tokenParts(name) { + let lastDash = name.lastIndexOf("-"); + let suffix = name.substring(lastDash + 1); + if (TSHIRT_ORDER.includes(suffix) || STATE_ORDER.includes(suffix)) { + return [name.substring(0, lastDash), suffix]; + } + return [name, ""]; + } + + for (let [label, selector] of Object.entries(TOKEN_SECTIONS)) { + let sectionMatchers = Array.isArray(selector) ? selector : [selector]; + let sectionParts = []; + + remainingTokens = remainingTokens.filter(token => { + if ( + sectionMatchers.some(m => + m.test ? m.test(token.name) : token.name.startsWith(m) + ) + ) { + sectionParts.push(token); + return false; + } + return true; + }); + + if (sectionParts.length) { + sectionParts.sort((a, b) => { + let aName = formatBaseTokenNames(a.name); + let bName = formatBaseTokenNames(b.name); + let [aToken, aSuffix] = tokenParts(aName); + let [bToken, bSuffix] = tokenParts(bName); + if (aSuffix || bSuffix) { + if (aToken == bToken) { + let aSize = TSHIRT_ORDER.indexOf(aSuffix); + let bSize = TSHIRT_ORDER.indexOf(bSuffix); + if (aSize != -1 && bSize != -1) { + return aSize - bSize; + } + let aState = STATE_ORDER.indexOf(aSuffix); + let bState = STATE_ORDER.indexOf(bSuffix); + if (aState != -1 && bState != -1) { + return aState - bState; + } + } + } + return aToken.localeCompare(bToken, undefined, { numeric: true }); + }); + + let headingParts = []; + if (!isFirst) { + headingParts.push(""); + } + isFirst = false; + + let sectionLevel = "**"; + let labelParts = label.split("/"); + for (let i = 0; i < labelParts.length; i++) { + if (labelParts[i] != lastSection[i]) { + headingParts.push( + `${formatting.indentation}/${sectionLevel} ${labelParts[i]} ${sectionLevel}/` + ); + } + sectionLevel += "*"; + } + lastSection = labelParts; + + outputParts = outputParts.concat( + headingParts.concat(sectionParts.map(propertyFormatter)) + ); + } + } + + return outputParts.join("\n"); +} + +// Easy way to grab variable values later for display. +let variableLookupTable = {}; + +function storybookJSFormat(args) { + let dictionary = Object.assign({}, args.dictionary); + let resolvedTokens = dictionary.allTokens.map(token => { + let tokenVal = resolveReferences(dictionary, token.original); + return { + name: token.name, + ...tokenVal, + }; + }); + dictionary.allTokens = dictionary.allProperties = resolvedTokens; + + let parsedData = JSON.parse( + formatBaseTokenNames( + StyleDictionary.format["javascript/module-flat"]({ + ...args, + dictionary, + }) + ) + .trim() + .replaceAll(/(^module\.exports\s*=\s*|\;$)/g, "") + ); + let storybookTables = formatTokensTablesData(parsedData); + + return `${customFileHeader({ platform: "storybook" })} + export const storybookTables = ${JSON.stringify(storybookTables)}; + + export const variableLookupTable = ${JSON.stringify(variableLookupTable)}; + `; +} + +function resolveReferences(dictionary, originalVal) { + let resolvedValues = {}; + Object.entries(originalVal).forEach(([key, value]) => { + if (typeof value === "object" && value != null) { + resolvedValues[key] = resolveReferences(dictionary, value); + } else { + let resolvedVal = getValueWithReferences(dictionary, value); + resolvedValues[key] = resolvedVal; + } + }); + return resolvedValues; +} + +function getValueWithReferences(dictionary, value) { + let valWithRefs = value; + if (dictionary.usesReference(value)) { + dictionary.getReferences(value).forEach(ref => { + valWithRefs = valWithRefs.replace( + `{${ref.path.join(".")}}`, + `var(--${ref.name})` + ); + }); + } + return valWithRefs; +} + +function formatTokensTablesData(tokensData) { + let tokensTables = {}; + Object.entries(tokensData).forEach(([key, value]) => { + variableLookupTable[key] = value; + let formattedToken = { + value, + name: `--${key}`, + }; + + let tableName = getTableName(key); + if (tokensTables[tableName]) { + tokensTables[tableName].push(formattedToken); + } else { + tokensTables[tableName] = [formattedToken]; + } + }); + return tokensTables; +} + +const SINGULAR_TABLE_CATEGORIES = [ + "button", + "color", + "link", + "size", + "space", + "opacity", + "outline", + "padding", + "margin", +]; + +function getTableName(tokenName) { + let replacePattern = /^(button-|input-text-|focus-|checkbox-)/; + if (tokenName.match(replacePattern)) { + tokenName = tokenName.replace(replacePattern, ""); + } + let [category, type] = tokenName.split("-"); + return SINGULAR_TABLE_CATEGORIES.includes(category) || !type + ? category + : `${category}-${type}`; +} + +module.exports = { + source: ["design-tokens.json"], + format: { + "css/variables/shared": createDesktopFormat(), + "css/variables/brand": createDesktopFormat("brand"), + "css/variables/platform": createDesktopFormat("platform"), + "javascript/storybook": storybookJSFormat, + }, + platforms: { + css: { + options: { + outputReferences: true, + showFileHeader: false, + }, + transforms: [ + ...StyleDictionary.transformGroup.css, + ...["shared", "platform", "brand"].map(createLightDarkTransform), + ], + files: [ + { + destination: "tokens-shared.css", + format: "css/variables/shared", + }, + { + destination: "tokens-brand.css", + format: "css/variables/brand", + filter: token => + typeof token.original.value == "object" && + token.original.value.brand, + }, + { + destination: "tokens-platform.css", + format: "css/variables/platform", + filter: token => + typeof token.original.value == "object" && + token.original.value.platform, + }, + ], + }, + storybook: { + options: { + outputReferences: true, + showFileHeader: false, + }, + transforms: [ + ...StyleDictionary.transformGroup.css, + ...["shared", "platform", "brand"].map(createLightDarkTransform), + ], + files: [ + { + destination: "tokens-storybook.mjs", + format: "javascript/storybook", + }, + ], + }, + }, +}; diff --git a/toolkit/themes/shared/design-system/tokens-platform.css b/toolkit/themes/shared/design-system/tokens-platform.css index 921f5fd860..d3a48f8047 100644 --- a/toolkit/themes/shared/design-system/tokens-platform.css +++ b/toolkit/themes/shared/design-system/tokens-platform.css @@ -2,16 +2,25 @@ * 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/. */ +/* DO NOT EDIT this file directly, instead modify design-tokens.json + * and run `npm run build` to see your changes. */ + @import url("chrome://global/skin/design-system/tokens-shared.css"); @layer tokens-foundation { :root, :host(.anonymous-content-host) { + /** Attention Dot **/ + --attention-dot-color: AccentColor; + + /** Background Color **/ + --background-color-canvas: Canvas; + /** Border **/ - --border-interactive-color: color-mix(in srgb, currentColor 15%, var(--color-gray-60)); + --border-color-interactive: color-mix(in srgb, currentColor 15%, var(--color-gray-60)); /** Button **/ - --button-background-color: var(--button-bgcolor); + --button-background-color: var(--button-bgcolor); /* TODO Bug 1821203 - Gray use needs to be consolidated */ --button-background-color-hover: var(--button-hover-bgcolor); --button-background-color-active: var(--button-active-bgcolor); --button-text-color: var(--button-color); @@ -21,9 +30,8 @@ --color-accent-primary: var(--button-primary-bgcolor, AccentColor); --color-accent-primary-hover: var(--button-primary-hover-bgcolor); --color-accent-primary-active: var(--button-primary-active-bgcolor); - --color-canvas: Canvas; - /** Font size **/ + /** Font Size **/ --font-size-root: unset; --font-size-small: unset; --font-size-large: unset; diff --git a/toolkit/themes/shared/design-system/tokens-shared.css b/toolkit/themes/shared/design-system/tokens-shared.css index 0c074ca20e..42b33f5e0d 100644 --- a/toolkit/themes/shared/design-system/tokens-shared.css +++ b/toolkit/themes/shared/design-system/tokens-shared.css @@ -2,15 +2,95 @@ * 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/. */ +/* DO NOT EDIT this file directly, instead modify design-tokens.json + * and run `npm run build` to see your changes. */ + @layer tokens-foundation, tokens-prefers-contrast, tokens-forced-colors; @layer tokens-foundation { :root, :host(.anonymous-content-host) { - /* Base tokens */ - /* Do not use base tokens directly as they don't carry any meaning and are used to set our base. Refer to Application tokens below. */ + /** Background Color **/ + --background-color-box: light-dark(var(--color-white), var(--color-gray-80)); + --background-color-critical: light-dark(var(--color-red-05), var(--color-red-80)); + --background-color-information: light-dark(var(--color-blue-05), var(--color-blue-80)); + --background-color-success: light-dark(var(--color-green-05), var(--color-green-80)); + --background-color-warning: light-dark(var(--color-yellow-05), var(--color-yellow-80)); + + /** Border **/ + --border-color-interactive-hover: var(--border-color-interactive); + --border-color-interactive-active: var(--border-color-interactive); + --border-color-interactive-disabled: var(--border-color-interactive); + --border-radius-circle: 9999px; + --border-radius-small: 4px; + --border-radius-medium: 8px; + --border-width: 1px; + + /** Box Shadow **/ + --box-shadow-10: 0 1px 4px var(--color-black-a10); + + /** Button **/ + --button-background-color-disabled: var(--button-background-color); + --button-background-color-destructive: light-dark(var(--color-red-50), var(--color-red-30)); + --button-background-color-destructive-hover: light-dark(var(--color-red-60), var(--color-red-10)); + --button-background-color-destructive-active: light-dark(var(--color-red-70), var(--color-red-05)); + --button-background-color-destructive-disabled: var(--button-background-color-destructive); + --button-background-color-ghost: transparent; + --button-background-color-ghost-hover: var(--button-background-color-hover); + --button-background-color-ghost-active: var(--button-background-color-active); + --button-background-color-ghost-disabled: var(--button-background-color-ghost); + --button-background-color-primary: var(--color-accent-primary); + --button-background-color-primary-hover: var(--color-accent-primary-hover); + --button-background-color-primary-active: var(--color-accent-primary-active); + --button-background-color-primary-disabled: var(--button-background-color-primary); + --button-border: var(--border-width) solid var(--button-border-color); + --button-border-color: transparent; + --button-border-color-hover: var(--button-border-color); + --button-border-color-active: var(--button-border-color); + --button-border-color-disabled: var(--button-border-color); + --button-border-color-destructive: transparent; + --button-border-color-destructive-hover: var(--button-border-color-destructive); + --button-border-color-destructive-active: var(--button-border-color-destructive); + --button-border-color-destructive-disabled: var(--button-border-color-destructive); + --button-border-color-ghost: var(--button-border-color); + --button-border-color-ghost-hover: var(--button-border-color-hover); + --button-border-color-ghost-active: var(--button-border-color-active); + --button-border-color-ghost-disabled: var(--button-border-color-disabled); + --button-border-color-primary: transparent; + --button-border-color-primary-hover: var(--button-border-color-primary); + --button-border-color-primary-active: var(--button-border-color-primary); + --button-border-color-primary-disabled: var(--button-border-color-primary); + --button-border-radius: var(--border-radius-small); + --button-font-size: var(--font-size-root); + --button-font-size-small: var(--font-size-small); + --button-font-weight: var(--font-weight-bold); + --button-min-height: var(--size-item-large); + --button-min-height-small: var(--size-item-medium); + --button-opacity-disabled: 0.5; + --button-padding: var(--space-xsmall) var(--space-large); + --button-padding-icon: 0; + --button-size-icon: var(--button-min-height); + --button-size-icon-small: var(--button-min-height-small); + --button-text-color-hover: var(--button-text-color); + --button-text-color-active: var(--button-text-color); + --button-text-color-disabled: var(--button-text-color); + --button-text-color-destructive: light-dark(var(--color-gray-05), var(--color-gray-100)); + --button-text-color-destructive-hover: var(--button-text-color-destructive); + --button-text-color-destructive-active: var(--button-text-color-destructive); + --button-text-color-destructive-disabled: var(--button-text-color-destructive); + --button-text-color-ghost: var(--button-text-color); + --button-text-color-ghost-hover: var(--button-text-color-hover); + --button-text-color-ghost-active: var(--button-text-color-active); + --button-text-color-ghost-disabled: var(--button-text-color-disabled); + --button-text-color-primary-hover: var(--button-text-color-primary); + --button-text-color-primary-active: var(--button-text-color-primary-hover); + --button-text-color-primary-disabled: var(--button-text-color-primary); + + /** Checkbox **/ + --checkbox-margin-inline: var(--space-small); + --checkbox-size: var(--size-item-small); /* TODO Bug 1876537: Make this em-based, probably? */ + /** Color **/ - --color-white: #ffffff; --color-black-a10: rgba(0, 0, 0, 0.1); --color-blue-05: #deeafc; --color-blue-30: #73a7f3; @@ -38,133 +118,46 @@ --color-red-30: #f37f98; --color-red-50: #d7264c; --color-red-60: #ac1e3d; - --color-red-70: #8A1831; + --color-red-70: #8a1831; --color-red-80: #690f22; + --color-white: #ffffff; --color-yellow-05: #ffebcd; --color-yellow-30: #e49c49; --color-yellow-50: #cd411e; --color-yellow-80: #5a3100; - /* Application tokens */ - /** Border **/ - --border-radius-circle: 9999px; - --border-radius-small: 4px; - --border-radius-medium: 8px; - --border-width: 1px; - --border-interactive-color-hover: var(--border-interactive-color); - --border-interactive-color-active: var(--border-interactive-color); - --border-interactive-color-disabled: var(--border-interactive-color); - - /** Box **/ - --box-background-color: light-dark(var(--color-white), var(--color-gray-80)); - --box-shadow-10: 0 1px 4px var(--color-black-a10); - - /** Color **/ - --color-background-information: light-dark(var(--color-blue-05), var(--color-blue-80)); - --color-background-success: light-dark(var(--color-green-05), var(--color-green-80)); - --color-background-warning: light-dark(var(--color-yellow-05), var(--color-yellow-80)); - --color-background-critical: light-dark(var(--color-red-05), var(--color-red-80)); - --color-error-outline: light-dark(var(--color-red-50), var(--color-red-20)); - - /** Font weight **/ - --font-weight: normal; - --font-weight-bold: 600; - - /** Focus outline **/ + /** Focus Outline **/ --focus-outline: var(--focus-outline-width) solid var(--focus-outline-color); --focus-outline-color: var(--color-accent-primary); --focus-outline-inset: calc(-1 * var(--focus-outline-width)); --focus-outline-offset: 2px; --focus-outline-width: 2px; + /** Font Weight **/ + --font-weight: normal; + --font-weight-bold: 600; + /** Icon **/ --icon-color: light-dark(var(--color-gray-70), var(--color-gray-05)); + --icon-color-critical: light-dark(var(--color-red-50), var(--color-red-30)); --icon-color-information: light-dark(var(--color-blue-50), var(--color-blue-30)); --icon-color-success: light-dark(var(--color-green-50), var(--color-green-30)); --icon-color-warning: light-dark(var(--color-yellow-50), var(--color-yellow-30)); - --icon-color-critical: light-dark(var(--color-red-50), var(--color-red-30)); --icon-size-default: var(--size-item-small); - /** Input **/ - /*** Button ***/ - --button-border: var(--border-width) solid var(--button-border-color); - --button-border-radius: var(--border-radius-small); - --button-font-weight: var(--font-weight-bold); - --button-font-size: var(--font-size-root); - --button-font-size-small: var(--font-size-small); - --button-min-height: var(--size-item-large); - --button-min-height-small: var(--size-item-medium); - --button-size-icon: var(--button-min-height); - --button-size-icon-small: var(--button-min-height-small); - --button-padding: var(--space-xsmall) var(--space-large); - --button-padding-icon: 0; - - --button-text-color: var(--text-color); - --button-text-color-hover: var(--button-text-color); - --button-text-color-active: var(--button-text-color); - --button-text-color-disabled: var(--button-text-color); - --button-border-color: transparent; - --button-border-color-hover: var(--button-border-color); - --button-border-color-active: var(--button-border-color); - --button-border-color-disabled: var(--button-border-color); - --button-background-color-disabled: var(--button-background-color); - --button-opacity-disabled: 0.5; - - --button-background-color-primary: var(--color-accent-primary); - --button-background-color-primary-hover: var(--color-accent-primary-hover); - --button-background-color-primary-active: var(--color-accent-primary-active); - --button-background-color-primary-disabled: var(--button-background-color-primary); - --button-text-color-primary-hover: var(--button-text-color-primary); - --button-text-color-primary-active: var(--button-text-color-primary-hover); - --button-text-color-primary-disabled: var(--button-text-color-primary); - --button-border-color-primary: transparent; - --button-border-color-primary-hover: var(--button-border-color-primary); - --button-border-color-primary-active: var(--button-border-color-primary); - --button-border-color-primary-disabled: var(--button-border-color-primary); - - --button-background-color-destructive: light-dark(var(--color-red-50), var(--color-red-30)); - --button-background-color-destructive-hover: light-dark(var(--color-red-60), var(--color-red-10)); - --button-background-color-destructive-active: light-dark(var(--color-red-70), var(--color-red-05)); - --button-background-color-destructive-disabled: var(--button-background-color-destructive); - --button-text-color-destructive: light-dark(var(--color-gray-05), var(--color-gray-100)); - --button-text-color-destructive-hover: var(--button-text-color-destructive); - --button-text-color-destructive-active: var(--button-text-color-destructive); - --button-text-color-destructive-disabled: var(--button-text-color-destructive); - --button-border-color-destructive: transparent; - --button-border-color-destructive-hover: var(--button-border-color-destructive); - --button-border-color-destructive-active: var(--button-border-color-destructive); - --button-border-color-destructive-disabled: var(--button-border-color-destructive); - - --button-background-color-ghost: transparent; - --button-background-color-ghost-hover: var(--button-background-color-hover); - --button-background-color-ghost-active: var(--button-background-color-active); - --button-background-color-ghost-disabled: var(--button-background-color-ghost); - --button-text-color-ghost: var(--button-text-color); - --button-text-color-ghost-hover: var(--button-text-color-hover); - --button-text-color-ghost-active: var(--button-text-color-ghost-active); - --button-text-color-ghost-disabled: var(--button-text-color); - --button-border-color-ghost: var(--button-border-color); - --button-border-color-ghost-hover: var(--button-border-color-hover); - --button-border-color-ghost-active: var(--button-border-color-active); - --button-border-color-ghost-disabled: var(--button-border-color); - - /*** Checkbox ***/ - --checkbox-margin-inline: var(--space-small); - /* TODO Bug 1876537: Make this em-based, probably? */ - --checkbox-size: var(--size-item-small); - - /*** Text ***/ + /** Input - Text **/ --input-text-min-height: var(--button-min-height); /** Link **/ - /* Not using --focus-outline-offset for links because that's intended for - elements with a background, and we only want a slight offset here while not - overlapping adjacent text. */ + /** + * Not using --force-outline-offset for links because that's intended for + * elements with a background, and we only want a slight offset here while + * not overlapping adjacent text + */ --link-focus-outline-offset: 1px; - /** Text **/ - --text-color-deemphasized: color-mix(in srgb, currentColor 69%, transparent); - --text-color-error: light-dark(var(--color-red-50), var(--color-red-20)); + /** Outline Color **/ + --outline-color-error: light-dark(var(--color-red-50), var(--color-red-20)); /** Size **/ --size-item-small: 16px; @@ -179,6 +172,10 @@ --space-large: calc(4 * var(--space-xsmall)); --space-xlarge: calc(6 * var(--space-xsmall)); --space-xxlarge: calc(8 * var(--space-xsmall)); + + /** Text **/ + --text-color-deemphasized: color-mix(in srgb, currentColor 69%, transparent); + --text-color-error: light-dark(var(--color-red-50), var(--color-red-20)); } } @@ -188,19 +185,31 @@ @media (prefers-contrast) { :root, :host(.anonymous-content-host) { - /* Border */ - --border-color: var(--text-color); - --border-interactive-color: AccentColor; - --border-interactive-color-hover: ButtonText; - --border-interactive-color-active: AccentColor; - --border-interactive-color-disabled: GrayText; + /** Attention Dot **/ + --attention-dot-color: AccentColor; - /** Box **/ - --box-background-color: var(--color-canvas); + /** Background Color **/ + --background-color-box: var(--background-color-canvas); + --background-color-canvas: Canvas; + --background-color-critical: var(--background-color-canvas); + --background-color-information: var(--background-color-canvas); + --background-color-success: var(--background-color-canvas); + --background-color-warning: var(--background-color-canvas); - /* Button */ - --button-border-color: var(--button-text-color); + /** Border **/ + --border-color: var(--text-color); + --border-color-interactive: var(--text-color); + + /** Button **/ --button-background-color-ghost: var(--button-background-color); + --button-border-color: var(--button-text-color); + + /** Icon **/ + --icon-color: var(--text-color); + --icon-color-critical: var(--icon-color); + --icon-color-information: var(--icon-color); + --icon-color-success: var(--icon-color); + --icon-color-warning: var(--icon-color); /** Link **/ --link-color: LinkText; @@ -208,93 +217,68 @@ --link-color-active: ActiveText; --link-color-visited: var(--link-color); - /** Color **/ - --color-canvas: Canvas; - --color-background-information: var(--color-canvas); - --color-background-success: var(--color-canvas); - --color-background-warning: var(--color-canvas); - --color-background-critical: var(--color-canvas); - --color-error-outline: var(--border-color); - - /** Icon **/ - --icon-color: var(--text-color); - --icon-color-information: var(--icon-color); - --icon-color-success: var(--icon-color); - --icon-color-warning: var(--icon-color); - --icon-color-critical: var(--icon-color); + /** Outline Color **/ + --outline-color-error: var(--border-color); /** Text **/ --text-color: CanvasText; - --text-color-error: inherit; --text-color-deemphasized: inherit; + --text-color-error: inherit; } } } /* Bug 1879900: Can't nest media queries inside of :host, :root selector until Bug 1879349 lands */ -/* NOTE: These do not apply in the browser chrome (bug 1878919). */ @layer tokens-forced-colors { @media (forced-colors) { :root, :host(.anonymous-content-host) { /** Border **/ - --border-interactive-color: ButtonText; - --border-interactive-color-hover: SelectedItem; - --border-interactive-color-active: ButtonText; - --border-interactive-color-disabled: GrayText; + --border-color-interactive: ButtonText; + --border-color-interactive-hover: SelectedItem; + --border-color-interactive-active: ButtonText; + --border-color-interactive-disabled: GrayText; /** Button **/ - --button-border-color: var(--border-interactive-color); - --button-border-color-hover: var(--border-interactive-color-hover); - --button-border-color-active: var(--border-interactive-color-active); - --button-border-color-disabled: var(--border-interactive-color-disabled); - --button-background-color: ButtonFace; + --button-background-color: ButtonFace; /* TODO Bug 1821203 - Gray use needs to be consolidated */ --button-background-color-hover: SelectedItemText; --button-background-color-active: SelectedItemText; --button-background-color-disabled: ButtonFace; - --button-text-color: ButtonText; - --button-text-color-hover: SelectedItem; - --button-text-color-active: SelectedItem; - --button-text-color-disabled: GrayText; - --button-opacity-disabled: 1; - + --button-background-color-destructive: var(--button-background-color-primary); + --button-background-color-destructive-hover: var(--button-background-color-primary-hover); + --button-background-color-destructive-active: var(--button-background-color-primary-active); + --button-background-color-destructive-disabled: var(--button-background-color-primary-disabled); + --button-background-color-ghost: var(--button-background-color); + --button-background-color-ghost-disabled: var(--button-background-color-disabled); --button-background-color-primary-disabled: var(--button-text-color-disabled); - --button-border-color-primary: ButtonFace; - --button-border-color-primary-hover: SelectedItemText; - --button-border-color-primary-active: ButtonText; - --button-text-color-primary: ButtonFace; - --button-text-color-primary-hover: SelectedItemText; - + --button-border-color: var(--border-color-interactive); + --button-border-color-hover: var(--border-color-interactive-hover); + --button-border-color-active: var(--border-color-interactive-active); + --button-border-color-disabled: var(--border-color-interactive-disabled); --button-border-color-destructive: var(--button-border-color-primary); --button-border-color-destructive-hover: var(--button-border-color-primary-hover); --button-border-color-destructive-active: var(--button-border-color-primary-active); --button-border-color-destructive-disabled: var(--button-border-color-primary-disabled); - --button-background-color-destructive: var(--button-background-color-primary); - --button-background-color-destructive-hover: var(--button-background-color-primary-hover); - --button-background-color-destructive-active: var(--button-background-color-primary-active); - --button-background-color-destructive-disabled: var(--button-background-color-primary-disabled); + --button-border-color-primary: ButtonFace; + --button-border-color-primary-hover: SelectedItemText; + --button-border-color-primary-active: ButtonText; + --button-opacity-disabled: 1; + --button-text-color: ButtonText; + --button-text-color-hover: SelectedItem; + --button-text-color-active: SelectedItem; + --button-text-color-disabled: GrayText; --button-text-color-destructive: var(--button-text-color-primary); --button-text-color-destructive-hover: var(--button-text-color-primary-hover); --button-text-color-destructive-active: var(--button-text-color-primary-active); --button-text-color-destructive-disabled: var(--button-text-color-primary-disabled); - - --button-border-color-ghost: var(--button-border-color); - --button-border-color-ghost-hover: var(--button-border-color-hover); - --button-border-color-ghost-active: var(--button-border-color-active); - --button-border-color-ghost-disabled: var(--button-border-color-disabled); - --button-background-color-ghost: var(--button-background-color); - --button-background-color-ghost-disabled: var(--button-background-color-disabled); - --button-text-color-ghost: var(--button-text-color); - --button-text-color-ghost-hover: var(--button-text-color-hover); - --button-text-color-ghost-active: var(--button-text-color-active); - --button-text-color-ghost-disabled: var(--button-text-color-disabled); + --button-text-color-primary: ButtonFace; + --button-text-color-primary-hover: SelectedItemText; /** Color **/ --color-accent-primary: ButtonText; --color-accent-primary-hover: SelectedItem; --color-accent-primary-active: var(--color-accent-primary-hover); - --color-error-outline: var(--border-color); } } } diff --git a/toolkit/themes/shared/design-system/tokens-storybook.mjs b/toolkit/themes/shared/design-system/tokens-storybook.mjs new file mode 100644 index 0000000000..fd2ea174c6 --- /dev/null +++ b/toolkit/themes/shared/design-system/tokens-storybook.mjs @@ -0,0 +1,1172 @@ +/* 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/. */ + +/* DO NOT EDIT this file directly, instead modify design-tokens.json + * and run `npm run build` to see your changes. */ + +export const storybookTables = { + "attention-dot": [ + { + value: { + platform: { default: "AccentColor" }, + brand: { + light: "#2ac3a2", + dark: "#54ffbd", + default: "light-dark(#2ac3a2, #54ffbd)", + }, + prefersContrast: "AccentColor", + }, + name: "--attention-dot-color", + }, + ], + "background-color": [ + { + value: { + light: "var(--color-white)", + dark: "var(--color-gray-80)", + prefersContrast: "var(--background-color-canvas)", + default: "light-dark(var(--color-white), var(--color-gray-80))", + }, + name: "--background-color-box", + }, + { + value: { + prefersContrast: "Canvas", + brand: { + light: "var(--color-white)", + dark: "var(--color-gray-90)", + default: "light-dark(var(--color-white), var(--color-gray-90))", + }, + platform: { default: "Canvas" }, + }, + name: "--background-color-canvas", + }, + { + value: { + light: "var(--color-red-05)", + dark: "var(--color-red-80)", + prefersContrast: "var(--background-color-canvas)", + default: "light-dark(var(--color-red-05), var(--color-red-80))", + }, + name: "--background-color-critical", + }, + { + value: { + light: "var(--color-blue-05)", + dark: "var(--color-blue-80)", + prefersContrast: "var(--background-color-canvas)", + default: "light-dark(var(--color-blue-05), var(--color-blue-80))", + }, + name: "--background-color-information", + }, + { + value: { + light: "var(--color-green-05)", + dark: "var(--color-green-80)", + prefersContrast: "var(--background-color-canvas)", + default: "light-dark(var(--color-green-05), var(--color-green-80))", + }, + name: "--background-color-success", + }, + { + value: { + light: "var(--color-yellow-05)", + dark: "var(--color-yellow-80)", + prefersContrast: "var(--background-color-canvas)", + default: "light-dark(var(--color-yellow-05), var(--color-yellow-80))", + }, + name: "--background-color-warning", + }, + { + value: { + forcedColors: "ButtonFace", + brand: { default: "color-mix(in srgb, currentColor 7%, transparent)" }, + platform: { default: "var(--button-bgcolor)" }, + }, + name: "--button-background-color", + }, + { + value: { + forcedColors: "SelectedItemText", + brand: { default: "color-mix(in srgb, currentColor 14%, transparent)" }, + platform: { default: "var(--button-hover-bgcolor)" }, + }, + name: "--button-background-color-hover", + }, + { + value: { + forcedColors: "SelectedItemText", + brand: { default: "color-mix(in srgb, currentColor 21%, transparent)" }, + platform: { default: "var(--button-active-bgcolor)" }, + }, + name: "--button-background-color-active", + }, + { + value: { + default: "var(--button-background-color)", + forcedColors: "ButtonFace", + }, + name: "--button-background-color-disabled", + }, + { + value: "var(--color-accent-primary)", + name: "--button-background-color-primary", + }, + { + value: "var(--color-accent-primary-hover)", + name: "--button-background-color-primary-hover", + }, + { + value: "var(--color-accent-primary-active)", + name: "--button-background-color-primary-active", + }, + { + value: { + default: "var(--button-background-color-primary)", + forcedColors: "var(--button-text-color-disabled)", + }, + name: "--button-background-color-primary-disabled", + }, + { + value: { + light: "var(--color-red-50)", + dark: "var(--color-red-30)", + forcedColors: "var(--button-background-color-primary)", + default: "light-dark(var(--color-red-50), var(--color-red-30))", + }, + name: "--button-background-color-destructive", + }, + { + value: { + light: "var(--color-red-70)", + dark: "var(--color-red-05)", + forcedColors: "var(--button-background-color-primary-active)", + default: "light-dark(var(--color-red-70), var(--color-red-05))", + }, + name: "--button-background-color-destructive-active", + }, + { + value: { + default: "var(--button-background-color-destructive)", + forcedColors: "var(--button-background-color-primary-disabled)", + }, + name: "--button-background-color-destructive-disabled", + }, + { + value: { + light: "var(--color-red-60)", + dark: "var(--color-red-10)", + forcedColors: "var(--button-background-color-primary-hover)", + default: "light-dark(var(--color-red-60), var(--color-red-10))", + }, + name: "--button-background-color-destructive-hover", + }, + { + value: { + default: "transparent", + prefersContrast: "var(--button-background-color)", + forcedColors: "var(--button-background-color)", + }, + name: "--button-background-color-ghost", + }, + { + value: "var(--button-background-color-active)", + name: "--button-background-color-ghost-active", + }, + { + value: { + default: "var(--button-background-color-ghost)", + forcedColors: "var(--button-background-color-disabled)", + }, + name: "--button-background-color-ghost-disabled", + }, + { + value: "var(--button-background-color-hover)", + name: "--button-background-color-ghost-hover", + }, + ], + "border-color": [ + { value: { prefersContrast: "var(--text-color)" }, name: "--border-color" }, + { + value: { + prefersContrast: "var(--text-color)", + forcedColors: "ButtonText", + brand: { + light: "var(--color-gray-60)", + dark: "var(--color-gray-50)", + default: "light-dark(var(--color-gray-60), var(--color-gray-50))", + }, + platform: { + default: "color-mix(in srgb, currentColor 15%, var(--color-gray-60))", + }, + }, + name: "--border-color-interactive", + }, + { + value: { + default: "var(--border-color-interactive)", + forcedColors: "SelectedItem", + }, + name: "--border-color-interactive-hover", + }, + { + value: { + default: "var(--border-color-interactive)", + forcedColors: "ButtonText", + }, + name: "--border-color-interactive-active", + }, + { + value: { + default: "var(--border-color-interactive)", + forcedColors: "GrayText", + }, + name: "--border-color-interactive-disabled", + }, + { + value: { + default: "transparent", + prefersContrast: "var(--button-text-color)", + forcedColors: "var(--border-color-interactive)", + }, + name: "--button-border-color", + }, + { + value: { + default: "var(--button-border-color)", + forcedColors: "var(--border-color-interactive-active)", + }, + name: "--button-border-color-active", + }, + { + value: { + default: "transparent", + forcedColors: "var(--button-border-color-primary)", + }, + name: "--button-border-color-destructive", + }, + { + value: { + default: "var(--button-border-color-destructive)", + forcedColors: "var(--button-border-color-primary-active)", + }, + name: "--button-border-color-destructive-active", + }, + { + value: { + default: "var(--button-border-color-destructive)", + forcedColors: "var(--button-border-color-primary-disabled)", + }, + name: "--button-border-color-destructive-disabled", + }, + { + value: { + default: "var(--button-border-color-destructive)", + forcedColors: "var(--button-border-color-primary-hover)", + }, + name: "--button-border-color-destructive-hover", + }, + { + value: { + default: "var(--button-border-color)", + forcedColors: "var(--border-color-interactive-disabled)", + }, + name: "--button-border-color-disabled", + }, + { + value: { default: "var(--button-border-color)" }, + name: "--button-border-color-ghost", + }, + { + value: { default: "var(--button-border-color-active)" }, + name: "--button-border-color-ghost-active", + }, + { + value: { default: "var(--button-border-color-disabled)" }, + name: "--button-border-color-ghost-disabled", + }, + { + value: { default: "var(--button-border-color-hover)" }, + name: "--button-border-color-ghost-hover", + }, + { + value: { + default: "var(--button-border-color)", + forcedColors: "var(--border-color-interactive-hover)", + }, + name: "--button-border-color-hover", + }, + { + value: { default: "transparent", forcedColors: "ButtonFace" }, + name: "--button-border-color-primary", + }, + { + value: { + default: "var(--button-border-color-primary)", + forcedColors: "ButtonText", + }, + name: "--button-border-color-primary-active", + }, + { + value: "var(--button-border-color-primary)", + name: "--button-border-color-primary-disabled", + }, + { + value: { + default: "var(--button-border-color-primary)", + forcedColors: "SelectedItemText", + }, + name: "--button-border-color-primary-hover", + }, + ], + "border-radius": [ + { value: "9999px", name: "--border-radius-circle" }, + { value: "4px", name: "--border-radius-small" }, + { value: "8px", name: "--border-radius-medium" }, + { value: "var(--border-radius-small)", name: "--button-border-radius" }, + ], + "border-width": [{ value: "1px", name: "--border-width" }], + "box-shadow": [ + { value: "0 1px 4px var(--color-black-a10)", name: "--box-shadow-10" }, + ], + border: [ + { + value: "var(--border-width) solid var(--button-border-color)", + name: "--button-border", + }, + ], + "font-size": [ + { value: "var(--font-size-root)", name: "--button-font-size" }, + { value: "var(--font-size-small)", name: "--button-font-size-small" }, + { + value: { brand: { default: "15px" }, platform: { default: "unset" } }, + name: "--font-size-root", + }, + { + value: { brand: { default: "0.867rem" }, platform: { default: "unset" } }, + name: "--font-size-small", + }, + { + value: { brand: { default: "1.133rem" }, platform: { default: "unset" } }, + name: "--font-size-large", + }, + { + value: { brand: { default: "1.467rem" }, platform: { default: "unset" } }, + name: "--font-size-xlarge", + }, + { + value: { brand: { default: "1.6rem" }, platform: { default: "unset" } }, + name: "--font-size-xxlarge", + }, + ], + "font-weight": [ + { value: "var(--font-weight-bold)", name: "--button-font-weight" }, + { value: "normal", name: "--font-weight" }, + { value: 600, name: "--font-weight-bold" }, + ], + "min-height": [ + { value: "var(--size-item-large)", name: "--button-min-height" }, + { value: "var(--size-item-medium)", name: "--button-min-height-small" }, + { value: "var(--button-min-height)", name: "--input-text-min-height" }, + ], + opacity: [ + { + value: { default: 0.5, forcedColors: 1 }, + name: "--button-opacity-disabled", + }, + ], + padding: [ + { + value: "var(--space-xsmall) var(--space-large)", + name: "--button-padding", + }, + { value: 0, name: "--button-padding-icon" }, + ], + size: [ + { value: "var(--button-min-height)", name: "--button-size-icon" }, + { + value: "var(--button-min-height-small)", + name: "--button-size-icon-small", + }, + { value: "var(--size-item-small)", name: "--checkbox-size" }, + { value: "16px", name: "--size-item-small" }, + { value: "28px", name: "--size-item-medium" }, + { value: "32px", name: "--size-item-large" }, + ], + "text-color": [ + { + value: { + forcedColors: "ButtonText", + brand: { + light: "var(--color-gray-100)", + dark: "var(--color-gray-05)", + default: "light-dark(var(--color-gray-100), var(--color-gray-05))", + }, + platform: { default: "var(--button-color)" }, + }, + name: "--button-text-color", + }, + { + value: { + default: "var(--button-text-color)", + forcedColors: "SelectedItem", + }, + name: "--button-text-color-active", + }, + { + value: { + light: "var(--color-gray-05)", + dark: "var(--color-gray-100)", + forcedColors: "var(--button-text-color-primary)", + default: "light-dark(var(--color-gray-05), var(--color-gray-100))", + }, + name: "--button-text-color-destructive", + }, + { + value: { + default: "var(--button-text-color-destructive)", + forcedColors: "var(--button-text-color-primary-active)", + }, + name: "--button-text-color-destructive-active", + }, + { + value: { + default: "var(--button-text-color-destructive)", + forcedColors: "var(--button-text-color-primary-disabled)", + }, + name: "--button-text-color-destructive-disabled", + }, + { + value: { + default: "var(--button-text-color-destructive)", + forcedColors: "var(--button-text-color-primary-hover)", + }, + name: "--button-text-color-destructive-hover", + }, + { + value: { default: "var(--button-text-color)", forcedColors: "GrayText" }, + name: "--button-text-color-disabled", + }, + { + value: { default: "var(--button-text-color)" }, + name: "--button-text-color-ghost", + }, + { + value: { default: "var(--button-text-color-active)" }, + name: "--button-text-color-ghost-active", + }, + { + value: { default: "var(--button-text-color-disabled)" }, + name: "--button-text-color-ghost-disabled", + }, + { + value: { default: "var(--button-text-color-hover)" }, + name: "--button-text-color-ghost-hover", + }, + { + value: { + default: "var(--button-text-color)", + forcedColors: "SelectedItem", + }, + name: "--button-text-color-hover", + }, + { + value: { + forcedColors: "ButtonFace", + brand: { + light: "var(--color-gray-05)", + dark: "var(--color-gray-100)", + default: "light-dark(var(--color-gray-05), var(--color-gray-100))", + }, + platform: { default: "var(--button-primary-color)" }, + }, + name: "--button-text-color-primary", + }, + { + value: "var(--button-text-color-primary-hover)", + name: "--button-text-color-primary-active", + }, + { + value: "var(--button-text-color-primary)", + name: "--button-text-color-primary-disabled", + }, + { + value: { + default: "var(--button-text-color-primary)", + forcedColors: "SelectedItemText", + }, + name: "--button-text-color-primary-hover", + }, + { + value: { + prefersContrast: "CanvasText", + brand: { + light: "var(--color-gray-100)", + dark: "var(--color-gray-05)", + default: "light-dark(var(--color-gray-100), var(--color-gray-05))", + }, + platform: { default: "currentColor" }, + }, + name: "--text-color", + }, + { + value: { + default: "color-mix(in srgb, currentColor 69%, transparent)", + prefersContrast: "inherit", + }, + name: "--text-color-deemphasized", + }, + { + value: { + light: "var(--color-red-50)", + dark: "var(--color-red-20)", + prefersContrast: "inherit", + default: "light-dark(var(--color-red-50), var(--color-red-20))", + }, + name: "--text-color-error", + }, + ], + margin: [{ value: "var(--space-small)", name: "--checkbox-margin-inline" }], + color: [ + { value: "rgba(0, 0, 0, 0.1)", name: "--color-black-a10" }, + { value: "#73a7f3", name: "--color-blue-30" }, + { value: "#0060df", name: "--color-blue-50" }, + { value: "#0250bb", name: "--color-blue-60" }, + { value: "#054096", name: "--color-blue-70" }, + { value: "#003070", name: "--color-blue-80" }, + { value: "#deeafc", name: "--color-blue-05" }, + { value: "#aaf2ff", name: "--color-cyan-20" }, + { value: "#80ebff", name: "--color-cyan-30" }, + { value: "#00ddff", name: "--color-cyan-50" }, + { value: "#bfbfc9", name: "--color-gray-50" }, + { value: "#8f8f9d", name: "--color-gray-60" }, + { value: "#5b5b66", name: "--color-gray-70" }, + { value: "#23222b", name: "--color-gray-80" }, + { value: "#1c1b22", name: "--color-gray-90" }, + { value: "#15141a", name: "--color-gray-100" }, + { value: "#fbfbfe", name: "--color-gray-05" }, + { value: "#4dbc87", name: "--color-green-30" }, + { value: "#017a40", name: "--color-green-50" }, + { value: "#004725", name: "--color-green-80" }, + { value: "#d8eedc", name: "--color-green-05" }, + { value: "#ffbdc5", name: "--color-red-10" }, + { value: "#ff9aa2", name: "--color-red-20" }, + { value: "#f37f98", name: "--color-red-30" }, + { value: "#d7264c", name: "--color-red-50" }, + { value: "#ac1e3d", name: "--color-red-60" }, + { value: "#8a1831", name: "--color-red-70" }, + { value: "#690f22", name: "--color-red-80" }, + { value: "#ffe8e8", name: "--color-red-05" }, + { value: "#e49c49", name: "--color-yellow-30" }, + { value: "#cd411e", name: "--color-yellow-50" }, + { value: "#5a3100", name: "--color-yellow-80" }, + { value: "#ffebcd", name: "--color-yellow-05" }, + { value: "#ffffff", name: "--color-white" }, + { + value: { + forcedColors: "ButtonText", + brand: { + light: "var(--color-blue-50)", + dark: "var(--color-cyan-50)", + default: "light-dark(var(--color-blue-50), var(--color-cyan-50))", + }, + platform: { default: "var(--button-primary-bgcolor, AccentColor)" }, + }, + name: "--color-accent-primary", + }, + { + value: { + forcedColors: "SelectedItem", + brand: { + light: "var(--color-blue-60)", + dark: "var(--color-cyan-30)", + default: "light-dark(var(--color-blue-60), var(--color-cyan-30))", + }, + platform: { default: "var(--button-primary-hover-bgcolor)" }, + }, + name: "--color-accent-primary-hover", + }, + { + value: { + forcedColors: "var(--color-accent-primary-hover)", + brand: { + light: "var(--color-blue-70)", + dark: "var(--color-cyan-20)", + default: "light-dark(var(--color-blue-70), var(--color-cyan-20))", + }, + platform: { default: "var(--button-primary-active-bgcolor)" }, + }, + name: "--color-accent-primary-active", + }, + ], + outline: [ + { + value: "var(--focus-outline-width) solid var(--focus-outline-color)", + name: "--focus-outline", + }, + { value: "var(--color-accent-primary)", name: "--focus-outline-color" }, + { + value: "calc(-1 * var(--focus-outline-width))", + name: "--focus-outline-inset", + }, + { value: "2px", name: "--focus-outline-offset" }, + { value: "2px", name: "--focus-outline-width" }, + { + value: { + light: "var(--color-red-50)", + dark: "var(--color-red-20)", + prefersContrast: "var(--border-color)", + default: "light-dark(var(--color-red-50), var(--color-red-20))", + }, + name: "--outline-color-error", + }, + ], + "icon-color": [ + { + value: { + light: "var(--color-gray-70)", + dark: "var(--color-gray-05)", + prefersContrast: "var(--text-color)", + default: "light-dark(var(--color-gray-70), var(--color-gray-05))", + }, + name: "--icon-color", + }, + { + value: { + light: "var(--color-blue-50)", + dark: "var(--color-blue-30)", + prefersContrast: "var(--icon-color)", + default: "light-dark(var(--color-blue-50), var(--color-blue-30))", + }, + name: "--icon-color-information", + }, + { + value: { + light: "var(--color-green-50)", + dark: "var(--color-green-30)", + prefersContrast: "var(--icon-color)", + default: "light-dark(var(--color-green-50), var(--color-green-30))", + }, + name: "--icon-color-success", + }, + { + value: { + light: "var(--color-yellow-50)", + dark: "var(--color-yellow-30)", + prefersContrast: "var(--icon-color)", + default: "light-dark(var(--color-yellow-50), var(--color-yellow-30))", + }, + name: "--icon-color-warning", + }, + { + value: { + light: "var(--color-red-50)", + dark: "var(--color-red-30)", + prefersContrast: "var(--icon-color)", + default: "light-dark(var(--color-red-50), var(--color-red-30))", + }, + name: "--icon-color-critical", + }, + ], + "icon-size": [ + { value: "var(--size-item-small)", name: "--icon-size-default" }, + ], + link: [ + { + value: { + prefersContrast: "LinkText", + brand: { default: "var(--color-accent-primary)" }, + platform: { default: "LinkText" }, + }, + name: "--link-color", + }, + { + value: { + prefersContrast: "LinkText", + brand: { default: "var(--color-accent-primary-hover)" }, + platform: { default: "LinkText" }, + }, + name: "--link-color-hover", + }, + { + value: { + prefersContrast: "ActiveText", + brand: { default: "var(--color-accent-primary-active)" }, + platform: { default: "ActiveText" }, + }, + name: "--link-color-active", + }, + { + value: { + prefersContrast: "var(--link-color)", + brand: { default: "var(--link-color)" }, + platform: { default: "var(--link-color)" }, + }, + name: "--link-color-visited", + }, + { value: "1px", name: "--link-focus-outline-offset" }, + ], + space: [ + { value: "calc(0.5 * var(--space-xsmall))", name: "--space-xxsmall" }, + { value: "0.267rem", name: "--space-xsmall" }, + { value: "calc(2 * var(--space-xsmall))", name: "--space-small" }, + { value: "calc(3 * var(--space-xsmall))", name: "--space-medium" }, + { value: "calc(4 * var(--space-xsmall))", name: "--space-large" }, + { value: "calc(6 * var(--space-xsmall))", name: "--space-xlarge" }, + { value: "calc(8 * var(--space-xsmall))", name: "--space-xxlarge" }, + ], +}; + +export const variableLookupTable = { + "attention-dot-color": { + platform: { default: "AccentColor" }, + brand: { + light: "#2ac3a2", + dark: "#54ffbd", + default: "light-dark(#2ac3a2, #54ffbd)", + }, + prefersContrast: "AccentColor", + }, + "background-color-box": { + light: "var(--color-white)", + dark: "var(--color-gray-80)", + prefersContrast: "var(--background-color-canvas)", + default: "light-dark(var(--color-white), var(--color-gray-80))", + }, + "background-color-canvas": { + prefersContrast: "Canvas", + brand: { + light: "var(--color-white)", + dark: "var(--color-gray-90)", + default: "light-dark(var(--color-white), var(--color-gray-90))", + }, + platform: { default: "Canvas" }, + }, + "background-color-critical": { + light: "var(--color-red-05)", + dark: "var(--color-red-80)", + prefersContrast: "var(--background-color-canvas)", + default: "light-dark(var(--color-red-05), var(--color-red-80))", + }, + "background-color-information": { + light: "var(--color-blue-05)", + dark: "var(--color-blue-80)", + prefersContrast: "var(--background-color-canvas)", + default: "light-dark(var(--color-blue-05), var(--color-blue-80))", + }, + "background-color-success": { + light: "var(--color-green-05)", + dark: "var(--color-green-80)", + prefersContrast: "var(--background-color-canvas)", + default: "light-dark(var(--color-green-05), var(--color-green-80))", + }, + "background-color-warning": { + light: "var(--color-yellow-05)", + dark: "var(--color-yellow-80)", + prefersContrast: "var(--background-color-canvas)", + default: "light-dark(var(--color-yellow-05), var(--color-yellow-80))", + }, + "border-color": { prefersContrast: "var(--text-color)" }, + "border-color-interactive": { + prefersContrast: "var(--text-color)", + forcedColors: "ButtonText", + brand: { + light: "var(--color-gray-60)", + dark: "var(--color-gray-50)", + default: "light-dark(var(--color-gray-60), var(--color-gray-50))", + }, + platform: { + default: "color-mix(in srgb, currentColor 15%, var(--color-gray-60))", + }, + }, + "border-color-interactive-hover": { + default: "var(--border-color-interactive)", + forcedColors: "SelectedItem", + }, + "border-color-interactive-active": { + default: "var(--border-color-interactive)", + forcedColors: "ButtonText", + }, + "border-color-interactive-disabled": { + default: "var(--border-color-interactive)", + forcedColors: "GrayText", + }, + "border-radius-circle": "9999px", + "border-radius-small": "4px", + "border-radius-medium": "8px", + "border-width": "1px", + "box-shadow-10": "0 1px 4px var(--color-black-a10)", + "button-background-color": { + forcedColors: "ButtonFace", + brand: { default: "color-mix(in srgb, currentColor 7%, transparent)" }, + platform: { default: "var(--button-bgcolor)" }, + }, + "button-background-color-hover": { + forcedColors: "SelectedItemText", + brand: { default: "color-mix(in srgb, currentColor 14%, transparent)" }, + platform: { default: "var(--button-hover-bgcolor)" }, + }, + "button-background-color-active": { + forcedColors: "SelectedItemText", + brand: { default: "color-mix(in srgb, currentColor 21%, transparent)" }, + platform: { default: "var(--button-active-bgcolor)" }, + }, + "button-background-color-disabled": { + default: "var(--button-background-color)", + forcedColors: "ButtonFace", + }, + "button-background-color-primary": "var(--color-accent-primary)", + "button-background-color-primary-hover": "var(--color-accent-primary-hover)", + "button-background-color-primary-active": + "var(--color-accent-primary-active)", + "button-background-color-primary-disabled": { + default: "var(--button-background-color-primary)", + forcedColors: "var(--button-text-color-disabled)", + }, + "button-background-color-destructive": { + light: "var(--color-red-50)", + dark: "var(--color-red-30)", + forcedColors: "var(--button-background-color-primary)", + default: "light-dark(var(--color-red-50), var(--color-red-30))", + }, + "button-background-color-destructive-active": { + light: "var(--color-red-70)", + dark: "var(--color-red-05)", + forcedColors: "var(--button-background-color-primary-active)", + default: "light-dark(var(--color-red-70), var(--color-red-05))", + }, + "button-background-color-destructive-disabled": { + default: "var(--button-background-color-destructive)", + forcedColors: "var(--button-background-color-primary-disabled)", + }, + "button-background-color-destructive-hover": { + light: "var(--color-red-60)", + dark: "var(--color-red-10)", + forcedColors: "var(--button-background-color-primary-hover)", + default: "light-dark(var(--color-red-60), var(--color-red-10))", + }, + "button-background-color-ghost": { + default: "transparent", + prefersContrast: "var(--button-background-color)", + forcedColors: "var(--button-background-color)", + }, + "button-background-color-ghost-active": + "var(--button-background-color-active)", + "button-background-color-ghost-disabled": { + default: "var(--button-background-color-ghost)", + forcedColors: "var(--button-background-color-disabled)", + }, + "button-background-color-ghost-hover": "var(--button-background-color-hover)", + "button-border": "var(--border-width) solid var(--button-border-color)", + "button-border-color": { + default: "transparent", + prefersContrast: "var(--button-text-color)", + forcedColors: "var(--border-color-interactive)", + }, + "button-border-color-active": { + default: "var(--button-border-color)", + forcedColors: "var(--border-color-interactive-active)", + }, + "button-border-color-destructive": { + default: "transparent", + forcedColors: "var(--button-border-color-primary)", + }, + "button-border-color-destructive-active": { + default: "var(--button-border-color-destructive)", + forcedColors: "var(--button-border-color-primary-active)", + }, + "button-border-color-destructive-disabled": { + default: "var(--button-border-color-destructive)", + forcedColors: "var(--button-border-color-primary-disabled)", + }, + "button-border-color-destructive-hover": { + default: "var(--button-border-color-destructive)", + forcedColors: "var(--button-border-color-primary-hover)", + }, + "button-border-color-disabled": { + default: "var(--button-border-color)", + forcedColors: "var(--border-color-interactive-disabled)", + }, + "button-border-color-ghost": { default: "var(--button-border-color)" }, + "button-border-color-ghost-active": { + default: "var(--button-border-color-active)", + }, + "button-border-color-ghost-disabled": { + default: "var(--button-border-color-disabled)", + }, + "button-border-color-ghost-hover": { + default: "var(--button-border-color-hover)", + }, + "button-border-color-hover": { + default: "var(--button-border-color)", + forcedColors: "var(--border-color-interactive-hover)", + }, + "button-border-color-primary": { + default: "transparent", + forcedColors: "ButtonFace", + }, + "button-border-color-primary-active": { + default: "var(--button-border-color-primary)", + forcedColors: "ButtonText", + }, + "button-border-color-primary-disabled": "var(--button-border-color-primary)", + "button-border-color-primary-hover": { + default: "var(--button-border-color-primary)", + forcedColors: "SelectedItemText", + }, + "button-border-radius": "var(--border-radius-small)", + "button-font-size": "var(--font-size-root)", + "button-font-size-small": "var(--font-size-small)", + "button-font-weight": "var(--font-weight-bold)", + "button-min-height": "var(--size-item-large)", + "button-min-height-small": "var(--size-item-medium)", + "button-opacity-disabled": { default: 0.5, forcedColors: 1 }, + "button-padding": "var(--space-xsmall) var(--space-large)", + "button-padding-icon": 0, + "button-size-icon": "var(--button-min-height)", + "button-size-icon-small": "var(--button-min-height-small)", + "button-text-color": { + forcedColors: "ButtonText", + brand: { + light: "var(--color-gray-100)", + dark: "var(--color-gray-05)", + default: "light-dark(var(--color-gray-100), var(--color-gray-05))", + }, + platform: { default: "var(--button-color)" }, + }, + "button-text-color-active": { + default: "var(--button-text-color)", + forcedColors: "SelectedItem", + }, + "button-text-color-destructive": { + light: "var(--color-gray-05)", + dark: "var(--color-gray-100)", + forcedColors: "var(--button-text-color-primary)", + default: "light-dark(var(--color-gray-05), var(--color-gray-100))", + }, + "button-text-color-destructive-active": { + default: "var(--button-text-color-destructive)", + forcedColors: "var(--button-text-color-primary-active)", + }, + "button-text-color-destructive-disabled": { + default: "var(--button-text-color-destructive)", + forcedColors: "var(--button-text-color-primary-disabled)", + }, + "button-text-color-destructive-hover": { + default: "var(--button-text-color-destructive)", + forcedColors: "var(--button-text-color-primary-hover)", + }, + "button-text-color-disabled": { + default: "var(--button-text-color)", + forcedColors: "GrayText", + }, + "button-text-color-ghost": { default: "var(--button-text-color)" }, + "button-text-color-ghost-active": { + default: "var(--button-text-color-active)", + }, + "button-text-color-ghost-disabled": { + default: "var(--button-text-color-disabled)", + }, + "button-text-color-ghost-hover": { + default: "var(--button-text-color-hover)", + }, + "button-text-color-hover": { + default: "var(--button-text-color)", + forcedColors: "SelectedItem", + }, + "button-text-color-primary": { + forcedColors: "ButtonFace", + brand: { + light: "var(--color-gray-05)", + dark: "var(--color-gray-100)", + default: "light-dark(var(--color-gray-05), var(--color-gray-100))", + }, + platform: { default: "var(--button-primary-color)" }, + }, + "button-text-color-primary-active": "var(--button-text-color-primary-hover)", + "button-text-color-primary-disabled": "var(--button-text-color-primary)", + "button-text-color-primary-hover": { + default: "var(--button-text-color-primary)", + forcedColors: "SelectedItemText", + }, + "checkbox-margin-inline": "var(--space-small)", + "checkbox-size": "var(--size-item-small)", + "color-black-a10": "rgba(0, 0, 0, 0.1)", + "color-blue-30": "#73a7f3", + "color-blue-50": "#0060df", + "color-blue-60": "#0250bb", + "color-blue-70": "#054096", + "color-blue-80": "#003070", + "color-blue-05": "#deeafc", + "color-cyan-20": "#aaf2ff", + "color-cyan-30": "#80ebff", + "color-cyan-50": "#00ddff", + "color-gray-50": "#bfbfc9", + "color-gray-60": "#8f8f9d", + "color-gray-70": "#5b5b66", + "color-gray-80": "#23222b", + "color-gray-90": "#1c1b22", + "color-gray-100": "#15141a", + "color-gray-05": "#fbfbfe", + "color-green-30": "#4dbc87", + "color-green-50": "#017a40", + "color-green-80": "#004725", + "color-green-05": "#d8eedc", + "color-red-10": "#ffbdc5", + "color-red-20": "#ff9aa2", + "color-red-30": "#f37f98", + "color-red-50": "#d7264c", + "color-red-60": "#ac1e3d", + "color-red-70": "#8a1831", + "color-red-80": "#690f22", + "color-red-05": "#ffe8e8", + "color-yellow-30": "#e49c49", + "color-yellow-50": "#cd411e", + "color-yellow-80": "#5a3100", + "color-yellow-05": "#ffebcd", + "color-white": "#ffffff", + "color-accent-primary": { + forcedColors: "ButtonText", + brand: { + light: "var(--color-blue-50)", + dark: "var(--color-cyan-50)", + default: "light-dark(var(--color-blue-50), var(--color-cyan-50))", + }, + platform: { default: "var(--button-primary-bgcolor, AccentColor)" }, + }, + "color-accent-primary-hover": { + forcedColors: "SelectedItem", + brand: { + light: "var(--color-blue-60)", + dark: "var(--color-cyan-30)", + default: "light-dark(var(--color-blue-60), var(--color-cyan-30))", + }, + platform: { default: "var(--button-primary-hover-bgcolor)" }, + }, + "color-accent-primary-active": { + forcedColors: "var(--color-accent-primary-hover)", + brand: { + light: "var(--color-blue-70)", + dark: "var(--color-cyan-20)", + default: "light-dark(var(--color-blue-70), var(--color-cyan-20))", + }, + platform: { default: "var(--button-primary-active-bgcolor)" }, + }, + "focus-outline": + "var(--focus-outline-width) solid var(--focus-outline-color)", + "focus-outline-color": "var(--color-accent-primary)", + "focus-outline-inset": "calc(-1 * var(--focus-outline-width))", + "focus-outline-offset": "2px", + "focus-outline-width": "2px", + "font-size-root": { + brand: { default: "15px" }, + platform: { default: "unset" }, + }, + "font-size-small": { + brand: { default: "0.867rem" }, + platform: { default: "unset" }, + }, + "font-size-large": { + brand: { default: "1.133rem" }, + platform: { default: "unset" }, + }, + "font-size-xlarge": { + brand: { default: "1.467rem" }, + platform: { default: "unset" }, + }, + "font-size-xxlarge": { + brand: { default: "1.6rem" }, + platform: { default: "unset" }, + }, + "font-weight": "normal", + "font-weight-bold": 600, + "icon-color": { + light: "var(--color-gray-70)", + dark: "var(--color-gray-05)", + prefersContrast: "var(--text-color)", + default: "light-dark(var(--color-gray-70), var(--color-gray-05))", + }, + "icon-color-information": { + light: "var(--color-blue-50)", + dark: "var(--color-blue-30)", + prefersContrast: "var(--icon-color)", + default: "light-dark(var(--color-blue-50), var(--color-blue-30))", + }, + "icon-color-success": { + light: "var(--color-green-50)", + dark: "var(--color-green-30)", + prefersContrast: "var(--icon-color)", + default: "light-dark(var(--color-green-50), var(--color-green-30))", + }, + "icon-color-warning": { + light: "var(--color-yellow-50)", + dark: "var(--color-yellow-30)", + prefersContrast: "var(--icon-color)", + default: "light-dark(var(--color-yellow-50), var(--color-yellow-30))", + }, + "icon-color-critical": { + light: "var(--color-red-50)", + dark: "var(--color-red-30)", + prefersContrast: "var(--icon-color)", + default: "light-dark(var(--color-red-50), var(--color-red-30))", + }, + "icon-size-default": "var(--size-item-small)", + "input-text-min-height": "var(--button-min-height)", + "link-color": { + prefersContrast: "LinkText", + brand: { default: "var(--color-accent-primary)" }, + platform: { default: "LinkText" }, + }, + "link-color-hover": { + prefersContrast: "LinkText", + brand: { default: "var(--color-accent-primary-hover)" }, + platform: { default: "LinkText" }, + }, + "link-color-active": { + prefersContrast: "ActiveText", + brand: { default: "var(--color-accent-primary-active)" }, + platform: { default: "ActiveText" }, + }, + "link-color-visited": { + prefersContrast: "var(--link-color)", + brand: { default: "var(--link-color)" }, + platform: { default: "var(--link-color)" }, + }, + "link-focus-outline-offset": "1px", + "outline-color-error": { + light: "var(--color-red-50)", + dark: "var(--color-red-20)", + prefersContrast: "var(--border-color)", + default: "light-dark(var(--color-red-50), var(--color-red-20))", + }, + "size-item-small": "16px", + "size-item-medium": "28px", + "size-item-large": "32px", + "space-xxsmall": "calc(0.5 * var(--space-xsmall))", + "space-xsmall": "0.267rem", + "space-small": "calc(2 * var(--space-xsmall))", + "space-medium": "calc(3 * var(--space-xsmall))", + "space-large": "calc(4 * var(--space-xsmall))", + "space-xlarge": "calc(6 * var(--space-xsmall))", + "space-xxlarge": "calc(8 * var(--space-xsmall))", + "text-color": { + prefersContrast: "CanvasText", + brand: { + light: "var(--color-gray-100)", + dark: "var(--color-gray-05)", + default: "light-dark(var(--color-gray-100), var(--color-gray-05))", + }, + platform: { default: "currentColor" }, + }, + "text-color-deemphasized": { + default: "color-mix(in srgb, currentColor 69%, transparent)", + prefersContrast: "inherit", + }, + "text-color-error": { + light: "var(--color-red-50)", + dark: "var(--color-red-20)", + prefersContrast: "inherit", + default: "light-dark(var(--color-red-50), var(--color-red-20))", + }, +}; diff --git a/toolkit/themes/shared/design-system/tokens-table.css b/toolkit/themes/shared/design-system/tokens-table.css new file mode 100644 index 0000000000..54f01a962a --- /dev/null +++ b/toolkit/themes/shared/design-system/tokens-table.css @@ -0,0 +1,333 @@ +/* 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/. */ + +:host { + --table-border: 2px solid var(--color-gray-50); + --table-background-color: color-mix(in srgb, + var(--color-gray-50) 20%, + transparent); +} + +/* Wrapper and filter styles */ + +.page-wrapper { + margin: 3rem; +} + +.filters-wrapper { + position: sticky; + top: 0; + z-index: 1; + display: flex; + align-items: center; + gap: var(--space-large); + background: var(--background-color-canvas); + padding: var(--space-small); +} + +fieldset { + flex: 1; + margin: 0; + padding-block: var(--space-small); + height: var(--input-text-min-height); + box-sizing: border-box; + gap: var(--space-medium); +} + +fieldset, +.search-wrapper { + border: 1px solid var(--border-color-interactive); + border-radius: var(--border-radius-small); + height: var(--input-text-min-height); + display: inline-flex; + align-items: center; +} + +.search-wrapper { + position: relative; +} + +.search-icon, +.clear-icon { + background-position: center; + background-repeat: no-repeat; + background-size: var(--size-item-small); + fill: currentColor; + -moz-context-properties: fill; + height: var(--size-item-small); + width: var(--size-item-small); + position: absolute; + inset-block: 0; + margin: auto 0; + padding: var(--space-xxsmall); +} + +.search-icon { + background-image: url(chrome://global/skin/icons/search-textbox.svg); + inset-inline-start: var(--space-small); +} + +.clear-icon { + background-image: url(chrome://global/skin/icons/close-12.svg); + inset-inline-end: var(--space-small); +} + +input[type="search"] { + border: none; + padding-block: var(--space-small); + padding-inline: var(--space-xxlarge); + border-radius: var(--border-radius-small); +} + +/* Table styles */ + +.table-wrapper { + box-sizing: border-box; + border-radius: var(--border-radius-small); + border: var(--table-border); + margin-block: 1em; + width: 100%; + + & > summary { + list-style-image: url("chrome://global/skin/icons/arrow-down.svg"); + display: flex; + align-items: center; + position: relative; + + &::before { + content: ""; + background-image: url("chrome://global/skin/icons/arrow-down.svg"); + background-position: center; + background-repeat: no-repeat; + height: 16px; + width: 16px; + position: absolute; + inset-inline-start: 8px; + } + } + + &[open] > summary::before { + background-image: url("chrome://global/skin/icons/arrow-up.svg"); + } +} + +.table-heading { + background-color: var(--table-background-color); + border-radius: var(---border-radius-small); + padding: 0 24px; + + & h3 { + margin: 0; + padding: 0.5em; + font-size: var(--font-size-large); + font-weight: var(--font-weight-bold); + text-transform: uppercase; + color: var(--text-color); + display: inline-block; + } +} + +table { + border-collapse: collapse; + border-spacing: 0; + text-align: left; + width: 100%; + table-layout: fixed; +} + +thead { + background-color: var(--table-background-color); + border-bottom: 1px solid var(--color-gray-50); + + & tr { + border-block-end: var(--table-border); + } +} + +tbody td { + vertical-align: top; + color: var(--color-gray-100); + border-bottom: var(--border-width) solid var(--color-gray-50); + + &.hcm-theme { + border-inline-start: var(--border-width) solid var(--color-gray-50); + } +} + +tr td:last-of-type { + border-inline-end: 0; +} + +tr td:first-of-type { + border-inline-start: 0; +} + +tbody tr:first-of-type td { + border-block-start: 2px solid var(--color-gray-50); +} + +tbody tr:last-of-type { + & td { + border-block-end: 0; + } + + & td:first-of-type { + border-radius: 0 0 0 2px; + } + + & td:last-of-type { + border-radius: 0 0 2px 0; + } +} + +th { + font-size: var(--font-size-small); + text-transform: uppercase; + font-weight: var(--font-weight); + text-align: center; +} + +tr td, +tr th { + padding: 8px; +} + +td { + background: var(--color-white); + text-align: center; +} + +th:first-of-type, +td:first-of-type { + text-align: start; +} + +.light-theme { + background: var(--color-white); + color: var(--color-gray-100); +} + +.dark-theme { + background-color: var(--color-gray-90); + color: var(--color-gray-05); +} + +.hcm-theme { + background-color: #000000; + color: #ffffff; +} + +.preview-wrapper { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 8px; + height: 100%; +} + +.value { + width: 100%; + margin: 0; +} + +/* Preview cell styles */ + +/** Default **/ + +.default-preview { + height: 50px; + width: 100px; + background-color: var(--button-background-color); + border: 1px solid var(--color-gray-60); + border-radius: 4px; +} + +/** Outline **/ + +.outline-preview { + height: 50px; + width: 100px; + background-color: color-mix(in srgb, currentColor 20%, transparent); + outline: 2px solid var(--color-gray-60); +} + +/** Font **/ + +.text-wrapper { + backdrop-filter: contrast(0.4); + padding: var(--space-small); + width: 100%; +} + +/** Icon **/ + +.icon-preview { + background-color: var(--color-gray-60); + height: var(--size-item-large); + width: var(--size-item-large); + /* FIXME: our icons don't seem to work when used as a mask */ + mask: url(https://upload.wikimedia.org/wikipedia/commons/c/c4/Globe_icon.svg) no-repeat center / contain; +} + +/** Link **/ + +.link-preview { + text-decoration: underline; + + &.outline { + outline: 2px solid var(--color-blue-50); + outline-offset: var(--link-focus-outline-offset); + } +} + +/** Space and size **/ + +.space-size-preview { + display: flex; + height: 50px; + width: 75%; + flex-wrap: wrap; + align-items: center; + justify-content: center; + + & .item { + height: 50%; + width: 40%; + background-color: var(--color-blue-60); + border-radius: var(--border-radius-small); + } +} + +.space-size-background { + background-color: var(--color-blue-05); + background-image: linear-gradient(135deg, + var(--color-blue-30) 10%, + #0000 0, + #0000 50%, + var(--color-blue-30) 0, + var(--color-blue-30) 60%, + #0000 0, + #0000); + background-size: 8px 8px; + border-radius: var(--border-radius-small); + border: var(--border-width) solid var(--color-gray-50); +} + +/** Padding **/ + +.padding-item { + min-height: calc(1.5 * var(--size-item-large)); + width: calc(3 * var(--size-item-large)); + border-radius: var(--border-radius-small); + display: flex; + justify-content: center; + align-items: center; + + &.inner { + background-color: var(--color-blue-60); + opacity: 0.5; + border-radius: 3px; + } +} diff --git a/toolkit/themes/shared/design-system/tokens-table.stories.mjs b/toolkit/themes/shared/design-system/tokens-table.stories.mjs new file mode 100644 index 0000000000..8e55d85d7c --- /dev/null +++ b/toolkit/themes/shared/design-system/tokens-table.stories.mjs @@ -0,0 +1,476 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { + html, + LitElement, + classMap, +} from "chrome://global/content/vendor/lit.all.mjs"; +import { storybookTables, variableLookupTable } from "./tokens-storybook.mjs"; +import styles from "./tokens-table.css"; + +export default { + title: "Docs/Tokens Table", + parameters: { + options: { + showPanel: false, + }, + }, +}; + +const HCM_MAP = { + ActiveText: "#8080FF", + ButtonBorder: "#000000", + ButtonFace: "#000000", + ButtonText: "#FFEE32", + Canvas: "#000000", + CanvasText: "#ffffff", + Field: "#000000", + FieldText: "#ffffff", + GrayText: "#A6A6A6", + Highlight: "#D6B4FD", + HighlightText: "#2B2B2B", + LinkText: "#8080FF", + Mark: "#000000", + MarkText: "#000000", + SelectedItem: "#D6B4FD", + SelectedItemText: "#2B2B2B", + AccentColor: "8080FF", + AccentColorText: "#2B2B2B", + VisitedText: "#8080FF", +}; + +const THEMED_TABLES = [ + "attention-dot", + "background-color", + "border", + "border-color", + "opacity", + "text-color", + "color", + "outline", + "icon-color", + "link", +]; + +/** + * + */ +class TablesPage extends LitElement { + #query = ""; + + static properties = { + surface: { type: String, state: true }, + tokensData: { type: Object, state: true }, + filteredTokens: { type: Object, state: true }, + }; + + constructor() { + super(); + this.surface = "brand"; + this.tokensData = storybookTables; + } + + handleSurfaceChange(e) { + this.surface = e.target.value; + } + + handleInput(e) { + this.#query = e.originalTarget.value.trim().toLowerCase(); + e.preventDefault(); + this.handleSearch(); + } + + debounce(fn, delayMs = 300) { + let timeout; + return function () { + clearTimeout(timeout); + timeout = setTimeout(() => { + fn.apply(this, arguments); + }, delayMs); + }; + } + + handleSearch() { + // Clear filteredTokens to show all data. + if (!this.#query) { + this.filteredTokens = null; + } + + let filteredTokens = Object.entries(this.tokensData).reduce( + (acc, [key, tokens]) => { + if (key.includes(this.#query)) { + return { ...acc, [key]: tokens }; + } + let filteredItems = tokens.filter(({ name: tokenName, value }) => + this.filterTokens(this.#query, tokenName, value) + ); + if (filteredItems.length) { + return { ...acc, [key]: filteredItems }; + } + return acc; + }, + {} + ); + this.filteredTokens = filteredTokens; + } + + filterTokens(searchTerm, tokenName, tokenVal) { + if ( + tokenName.includes(searchTerm) || + (typeof tokenVal == "string" && tokenVal.includes(searchTerm)) + ) { + return true; + } + if (typeof tokenVal == "object") { + return Object.entries(tokenVal).some(([key, val]) => + this.filterTokens(searchTerm, key, val) + ); + } + return false; + } + + handleClearSearch(e) { + this.#query = ""; + e.preventDefault(); + this.handleSearch(); + } + + render() { + if (!this.tokensData) { + return ""; + } + + return html` + <link rel="stylesheet" href=${styles} /> + <div class="page-wrapper"> + <div class="filters-wrapper"> + <div class="search-wrapper"> + <div class="search-icon"></div> + <input + type="search" + placeholder="Filter tokens" + @input=${this.debounce(this.handleInput)} + .value=${this.#query} + /> + <div + class="clear-icon" + role="button" + title="Clear" + ?hidden=${!this.#query} + @click=${this.handleClearSearch} + ></div> + </div> + <fieldset id="surface" @change=${this.handleSurfaceChange}> + <label> + <input + type="radio" + name="surface" + value="brand" + ?checked=${this.surface == "brand"} + /> + In-content + </label> + <label> + <input + type="radio" + name="surface" + value="platform" + ?checked=${this.surface == "platform"} + /> + Chrome + </label> + </fieldset> + </div> + <div class="tables-wrapper"> + ${Object.entries(this.filteredTokens ?? this.tokensData).map( + ([tableName, tableEntries]) => { + return html` + <tokens-table + name=${tableName} + surface=${this.surface} + .tokens=${tableEntries} + > + </tokens-table> + `; + } + )} + </div> + </div> + `; + } +} +customElements.define("tables-page", TablesPage); + +/** + * + */ +class TokensTable extends LitElement { + TEMPLATES = { + "font-size": this.fontTemplate, + "font-weight": this.fontTemplate, + "icon-color": this.iconTemplate, + "icon-size": this.iconTemplate, + link: this.linkTemplate, + margin: this.spaceAndSizeTemplate, + "min-height": this.spaceAndSizeTemplate, + outline: this.outlineTemplate, + padding: this.paddingTemplate, + size: this.spaceAndSizeTemplate, + space: this.spaceAndSizeTemplate, + "text-color": this.fontTemplate, + }; + + static properties = { + name: { type: String }, + tokens: { type: Array }, + surface: { type: String }, + }; + + createRenderRoot() { + return this; + } + + getDisplayValues(theme, token) { + let value = this.getResolvedValue(theme, token); + let raw = this.getRawValue(theme, value); + return { value, ...(raw !== value && { raw }) }; + } + + // Return the value with variable references. + // e.g. var(--color-white) + getResolvedValue(theme, token) { + if (typeof token == "string" || typeof token == "number") { + return token; + } + + if (theme == "hcm") { + return ( + token.forcedColors || + token.prefersContrast || + token[this.surface]?.default || + token.default + ); + } + + if (token[this.surface]) { + return this.getResolvedValue(theme, token[this.surface]); + } + + if (token[theme]) { + return token[theme]; + } + + return token.default; + } + + // Return the value with any variables replaced by what they represent. + // e.g. #ffffff + getRawValue(theme, token) { + let cssRegex = /(?<var>var\(--(?<lookupKey>[a-z-0-9,\s]+)\))/; + let matches = cssRegex.exec(token); + if (matches) { + let variableValue = variableLookupTable[matches.groups?.lookupKey]; + if (typeof variableValue == "undefined") { + return token; + } + if (typeof variableValue == "object") { + variableValue = this.getResolvedValue(theme, variableValue); + } + let rawValue = token.replace(matches.groups?.var, variableValue); + if (rawValue.match(cssRegex)) { + return this.getRawValue(theme, rawValue); + } + return rawValue; + } + return token; + } + + getTemplate(value, tokenName, category = this.name) { + // 0 is a valid value + if (value == undefined) { + return "N/A"; + } + + let templateFn = this.TEMPLATES[category]?.bind(this); + if (templateFn) { + return templateFn(category, value, tokenName); + } + + return html` + <div + class="default-preview" + style="${this.getDisplayProperty(category)}: ${HCM_MAP[value] ?? value}" + ></div> + `; + } + + outlineTemplate(_, value, tokenName) { + let property = tokenName.replaceAll(/--|focus-|-error/g, ""); + if (property == "outline-inset") { + property = "outline-offset"; + } + return html` + <div + class="outline-preview" + style="${property}: ${HCM_MAP[value] ?? value};" + ></div> + `; + } + + fontTemplate(category, value) { + return html` + <div class="text-wrapper"> + <p + style="${this.getDisplayProperty(category)}: ${HCM_MAP[value] ?? + value};" + > + The quick brown fox jumps over the lazy dog + </p> + </div> + `; + } + + iconTemplate(_, value, tokenName) { + let property = tokenName.includes("color") ? "background-color" : "height"; + return html` + <div + class="icon-preview" + style="${property}: ${HCM_MAP[value] ?? value};" + ></div> + `; + } + + linkTemplate(_, value, tokenName) { + let hasOutline = tokenName.includes("outline"); + return html` + <a + class=${classMap({ "link-preview": true, outline: hasOutline })} + style="color: ${HCM_MAP[value] ?? value};" + > + Click me! + </a> + `; + } + + spaceAndSizeTemplate(_, value, tokenName) { + let isSize = tokenName.includes("size") || tokenName.includes("height"); + let items = isSize + ? html` + <div class="item" style="height: ${value};width: ${value};"></div> + ` + : html` + <div class="item"></div> + <div class="item"></div> + `; + + return html` + <div + class="space-size-preview space-size-background" + style="gap: ${value};" + > + ${items} + </div> + `; + } + + paddingTemplate(_, value) { + return html` + <div class="space-size-background"> + <div class="padding-item outer" style="padding: ${value}"> + <div class="padding-item inner"></div> + </div> + </div> + `; + } + + getDisplayProperty(category) { + switch (category) { + case "attention-dot": + case "color": + return "background-color"; + case "text-color": + return "color"; + default: + return category.replace("button", ""); + } + } + + cellTemplate(type, tokenValue, tokenName) { + let { value, raw } = this.getDisplayValues("default", tokenValue); + return html` + <td> + <div class="preview-wrapper"> + ${type == "preview" + ? html`${this.getTemplate(raw ?? value, tokenName)}` + : html`<p class="value">${value}</p> + ${raw ? html`<p class="value">${raw}</p>` : ""}`} + </div> + </td> + `; + } + + themeCellTemplate(theme, tokenValue, tokenName) { + let { value, raw } = this.getDisplayValues(theme, tokenValue); + return html` + <td class="${theme}-theme"> + <div class="preview-wrapper"> + ${this.getTemplate(raw ?? value, tokenName)} + <p class="value">${value}</p> + ${raw ? html`<p class="value">${raw}</p>` : ""} + </div> + </td> + `; + } + + render() { + let themedTable = THEMED_TABLES.includes(this.name); + return html` + <details class="table-wrapper" open=""> + <summary class="table-heading"> + <h3>${this.name}</h3> + </summary> + <table> + <thead> + <tr> + <th>Name</th> + ${themedTable + ? html` + <th>Light</th> + <th>Dark</th> + <th>High contrast</th> + ` + : html` + <th>Value</th> + <th>Preview</th> + `} + </tr> + </thead> + <tbody> + ${this.tokens.map(({ name: tokenName, value }) => { + return html`<tr id=${tokenName}> + <td>${tokenName}</td> + ${themedTable + ? html` + ${this.themeCellTemplate("light", value, tokenName)} + ${this.themeCellTemplate("dark", value, tokenName)} + ${this.themeCellTemplate("hcm", value, tokenName)} + ` + : html` + ${this.cellTemplate("value", value, tokenName)} + ${this.cellTemplate("preview", value, tokenName)} + `} + </tr>`; + })} + </tbody> + </table> + </details> + `; + } +} +customElements.define("tokens-table", TokensTable); + +export const Default = () => { + return html`<tables-page></tables-page>`; +}; diff --git a/toolkit/themes/shared/desktop-jar.inc.mn b/toolkit/themes/shared/desktop-jar.inc.mn index 6afb075ea2..226faf26ff 100644 --- a/toolkit/themes/shared/desktop-jar.inc.mn +++ b/toolkit/themes/shared/desktop-jar.inc.mn @@ -32,8 +32,8 @@ skin/classic/global/datetimeinputpickers.css (../../shared/datetimeinputpickers.css) skin/classic/global/design-system/text-and-typography.css(../../shared/design-system/text-and-typography.css) skin/classic/global/design-system/tokens-brand.css (../../shared/design-system/tokens-brand.css) - skin/classic/global/design-system/tokens-shared.css (../../shared/design-system/tokens-shared.css) skin/classic/global/design-system/tokens-platform.css (../../shared/design-system/tokens-platform.css) + skin/classic/global/design-system/tokens-shared.css (../../shared/design-system/tokens-shared.css) skin/classic/global/error-pages.css (../../shared/error-pages.css) skin/classic/global/findbar.css (../../shared/findbar.css) skin/classic/global/global-shared.css (../../shared/global-shared.css) @@ -79,6 +79,7 @@ skin/classic/global/icons/developer.svg (../../shared/icons/developer.svg) skin/classic/global/icons/edit.svg (../../shared/icons/edit.svg) skin/classic/global/icons/edit-copy.svg (../../shared/icons/edit-copy.svg) + skin/classic/global/icons/edit-outline.svg (../../shared/icons/edit-outline.svg) skin/classic/global/icons/error.svg (../../shared/icons/error.svg) skin/classic/global/icons/experiments.svg (../../shared/icons/experiments.svg) skin/classic/global/icons/folder.svg (../../shared/icons/folder.svg) @@ -91,6 +92,7 @@ skin/classic/global/icons/lightbulb.svg (../../shared/icons/lightbulb.svg) skin/classic/global/icons/link.svg (../../shared/icons/link.svg) skin/classic/global/icons/loading.png (../../shared/icons/loading.png) + skin/classic/global/icons/loading.svg (../../shared/icons/loading.svg) skin/classic/global/icons/loading@2x.png (../../shared/icons/loading@2x.png) skin/classic/global/icons/mdn.svg (../../shared/icons/mdn.svg) skin/classic/global/icons/more.svg (../../shared/icons/more.svg) @@ -124,7 +126,6 @@ skin/classic/global/icons/arrow-up.svg (../../shared/icons/arrow-up.svg) skin/classic/global/icons/warning.svg (../../shared/icons/warning.svg) skin/classic/global/icons/warning-fill-12.svg (../../shared/icons/warning-fill-12.svg) - skin/classic/global/icons/whatsnew.svg (../../shared/icons/whatsnew.svg) skin/classic/global/illustrations/about-rights.svg (../../shared/illustrations/about-rights.svg) skin/classic/global/illustrations/about-license.svg (../../shared/illustrations/about-license.svg) skin/classic/global/illustrations/error-malformed-url.svg (../../shared/illustrations/error-malformed-url.svg) @@ -147,7 +148,6 @@ skin/classic/global/reader/RM-Minus-24x24.svg (../../shared/reader/RM-Minus-24x24.svg) skin/classic/global/reader/RM-Plus-24x24.svg (../../shared/reader/RM-Plus-24x24.svg) skin/classic/global/reader/RM-Type-Controls-24x24.svg (../../shared/reader/RM-Type-Controls-24x24.svg) - skin/classic/global/reader/RM-Type-Controls-Arrow.svg (../../shared/reader/RM-Type-Controls-Arrow.svg) skin/classic/global/reader/RM-Content-Width-Minus-42x16.svg (../../shared/reader/RM-Content-Width-Minus-42x16.svg) skin/classic/global/reader/RM-Content-Width-Plus-44x16.svg (../../shared/reader/RM-Content-Width-Plus-44x16.svg) skin/classic/global/reader/RM-Line-Height-Minus-38x14.svg (../../shared/reader/RM-Line-Height-Minus-38x14.svg) diff --git a/toolkit/themes/shared/desktop-non-mac.jar.inc.mn b/toolkit/themes/shared/desktop-non-mac.jar.inc.mn index 072678face..c976798df5 100644 --- a/toolkit/themes/shared/desktop-non-mac.jar.inc.mn +++ b/toolkit/themes/shared/desktop-non-mac.jar.inc.mn @@ -11,7 +11,6 @@ #include desktop-jar.inc.mn skin/classic/global/dialog.css (../../windows/global/dialog.css) - skin/classic/global/tabprompts.css (../../windows/global/tabprompts.css) skin/classic/global/wizard.css (../../windows/global/wizard.css) skin/classic/global/arrow/panelarrow-vertical.svg (../../windows/global/arrow/panelarrow-vertical.svg) diff --git a/toolkit/themes/shared/findbar.css b/toolkit/themes/shared/findbar.css index 714324929e..a3c2ca09b7 100644 --- a/toolkit/themes/shared/findbar.css +++ b/toolkit/themes/shared/findbar.css @@ -2,10 +2,10 @@ * 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/. */ -@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); +@namespace xul url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); @namespace html url("http://www.w3.org/1999/xhtml"); -findbar { +xul|findbar { border-top: 1px solid ThreeDShadow; min-width: 1px; transition-property: margin-bottom, visibility; @@ -14,29 +14,31 @@ findbar { padding-block: 6px; background-color: -moz-dialog; color: -moz-dialogtext; -} -findbar[hidden] { - /* Override display:none to make the transition work. */ - display: flex; - margin-bottom: calc(-1 * (28px + 12px + 1px)); /* findbar-container's height + padding-block + top border */ - visibility: collapse; - transition-delay: 0s, 150ms; -} + &:where([hidden]) { + /* Override display:none to make the transition work. */ + display: flex; + margin-bottom: calc(-1 * (28px + 12px + 1px)); /* findbar-container's height + padding-block + top border */ + visibility: collapse; + transition-delay: 0s, 150ms; + + > .findbar-container, + > .findbar-closebutton { + opacity: 0; + } + } -@media (prefers-reduced-motion: reduce) { - findbar, - findbar[hidden] { + @media (prefers-reduced-motion) { transition-duration: 0s; transition-delay: 0s; } -} -findbar[noanim], -findbar[noanim] > .findbar-container, -findbar[noanim] > .findbar-closebutton { - transition-duration: 0s; - transition-delay: 0s; + &[noanim], + &[noanim] > .findbar-container, + &[noanim] > .findbar-closebutton { + transition-duration: 0s; + transition-delay: 0s; + } } .findbar-container { @@ -46,10 +48,6 @@ findbar[noanim] > .findbar-closebutton { transition: opacity 150ms ease-in-out; } -findbar[hidden] > .findbar-container { - opacity: 0; -} - /* Remove start margin when close button is on the left side (on macOS) */ .findbar-closebutton + .findbar-container { margin-inline-start: 0; @@ -57,7 +55,7 @@ findbar[hidden] > .findbar-container { /* Search field */ -html|input.findbar-textbox { +.findbar-textbox { appearance: none; background-color: var(--input-bgcolor, var(--toolbar-field-background-color)); color: var(--input-color, var(--toolbar-field-color)); @@ -69,39 +67,39 @@ html|input.findbar-textbox { padding-inline-start: 8px; width: 18em; box-sizing: border-box; -} -html|input.findbar-textbox::placeholder { - opacity: 0.69; -} + &::placeholder { + opacity: 0.69; + } -html|input.findbar-textbox:focus { - background-color: var(--toolbar-field-focus-background-color); - color: var(--toolbar-field-focus-color); - border-color: transparent; - outline: var(--focus-outline); - outline-offset: var(--focus-outline-inset); - outline-color: var(--toolbar-field-focus-border-color); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.23); -} + &:focus { + background-color: var(--toolbar-field-focus-background-color); + color: var(--toolbar-field-focus-color); + border-color: transparent; + outline: var(--focus-outline); + outline-offset: var(--focus-outline-inset); + outline-color: var(--toolbar-field-focus-border-color); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.23); + } -html|input.findbar-textbox:-moz-lwtheme::selection { - background-color: var(--lwt-toolbar-field-highlight, Highlight); - color: var(--lwt-toolbar-field-highlight-text, HighlightText); -} + :root[lwtheme] &::selection { + background-color: var(--lwt-toolbar-field-highlight, Highlight); + color: var(--lwt-toolbar-field-highlight-text, HighlightText); + } -html|input.findbar-textbox:not(:focus):-moz-lwtheme::selection { - background-color: var(--lwt-toolbar-field-highlight, text-select-background-disabled); -} + :root[lwtheme] &:not(:focus)::selection { + background-color: var(--lwt-toolbar-field-highlight, text-select-background-disabled); + } -html|input.findbar-textbox[status="notfound"] { - background-color: rgba(226,40,80,.3); - color: inherit; -} + &[status="notfound"] { + background-color: rgba(226,40,80,.3); + color: inherit; + } -html|input.findbar-textbox[flash="true"] { - background-color: rgba(255,233,0,.3); - color: inherit; + &[flash="true"] { + background-color: rgba(255,233,0,.3); + color: inherit; + } } /* Previous/next buttons */ @@ -119,16 +117,18 @@ html|input.findbar-textbox[flash="true"] { outline: var(--focus-outline); outline-offset: var(--focus-outline-inset); } -} -.findbar-find-previous:not([disabled]):hover, -.findbar-find-next:not([disabled]):hover { - background-color: var(--toolbarbutton-hover-background, rgba(190,190,190,.2)); -} + &:not([disabled]):hover { + background-color: var(--toolbarbutton-hover-background, rgba(190,190,190,.2)); + } + + &:not([disabled]):hover:active { + background-color: var(--toolbarbutton-active-background, rgba(190,190,190,.4)); + } -.findbar-find-previous:not([disabled]):hover:active, -.findbar-find-next:not([disabled]):hover:active { - background-color: var(--toolbarbutton-active-background, rgba(190,190,190,.4)); + &[disabled="true"] > .toolbarbutton-icon { + opacity: var(--toolbarbutton-disabled-opacity); + } } .findbar-find-previous { @@ -141,11 +141,6 @@ html|input.findbar-textbox[flash="true"] { margin-inline: 0 8px; } -.findbar-find-previous[disabled="true"] > .toolbarbutton-icon, -.findbar-find-next[disabled="true"] > .toolbarbutton-icon { - opacity: var(--toolbarbutton-disabled-opacity); -} - /* Checkboxes & Labels */ .findbar-container > checkbox, @@ -175,17 +170,13 @@ html|input.findbar-textbox[flash="true"] { width: 24px; fill: var(--toolbarbutton-icon-fill); transition: opacity 150ms ease-in-out; -} - -findbar[hidden] > .findbar-closebutton { - opacity: 0; -} -.close-icon.findbar-closebutton:hover { - background-color: var(--toolbarbutton-hover-background, rgba(190,190,190,.2)); - outline: none; -} + &:hover { + background-color: var(--toolbarbutton-hover-background, rgba(190,190,190,.2)); + outline: none; + } -.close-icon.findbar-closebutton:hover:active { - background-color: var(--toolbarbutton-active-background, rgba(190,190,190,.4)); + &:hover:active { + background-color: var(--toolbarbutton-active-background, rgba(190,190,190,.4)); + } } diff --git a/toolkit/themes/shared/global-shared.css b/toolkit/themes/shared/global-shared.css index 320245afbe..f5b5aa9fbb 100644 --- a/toolkit/themes/shared/global-shared.css +++ b/toolkit/themes/shared/global-shared.css @@ -48,7 +48,7 @@ --toolbar-bgcolor: var(--toolbar-non-lwt-bgcolor); --toolbar-color: var(--toolbar-non-lwt-textcolor); - &:-moz-lwtheme { + &[lwtheme] { --toolbar-bgcolor: rgba(255,255,255,.4); --toolbar-color: var(--lwt-text-color, inherit); } @@ -72,7 +72,7 @@ --panel-separator-color: currentColor; --toolbar-field-focus-border-color: var(--focus-outline-color); - &:not(:-moz-lwtheme) { + &:not([lwtheme]) { --panel-disabled-color: GrayText; } } @@ -96,7 +96,7 @@ /* Lightweight theme roots */ -:root:-moz-lwtheme { +:root[lwtheme] { toolbar, &[lwt-popup="light"] panel { color-scheme: light; @@ -151,7 +151,7 @@ html|textarea { color: var(--input-color, FieldText); &:where(:user-invalid) { - border-color: var(--color-error-outline); + border-color: var(--outline-color-error); } &:where(:focus-visible) { diff --git a/toolkit/themes/shared/icons/edit-outline.svg b/toolkit/themes/shared/icons/edit-outline.svg new file mode 100644 index 0000000000..aef1625941 --- /dev/null +++ b/toolkit/themes/shared/icons/edit-outline.svg @@ -0,0 +1,6 @@ +<!-- 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/. --> +<svg width="16" height="16" viewBox="0 0 16 16" fill="context-fill" fill-opacity="context-fill-opacity" xmlns="http://www.w3.org/2000/svg"> +<path d="M0.0184994 13.6645L0.612501 10.4635C0.687501 10.0545 0.884501 9.6805 1.1805 9.3825L9.9815 0.5805C10.7555 -0.1925 12.0145 -0.1945 12.7915 0.5805L14.4195 2.2085C15.1935 2.9835 15.1935 4.2435 14.4195 5.0185L5.6155 13.8215C5.3195 14.1165 4.9455 14.3125 4.5375 14.3875L1.3355 14.9815C1.2655 14.9935 1.1975 15.0005 1.1295 15.0005C0.8325 15.0005 0.544499 14.8835 0.3305 14.6695C0.0674992 14.4055 -0.0495005 14.0305 0.0184994 13.6645ZM12.4715 5.1965L13.6315 4.0365L13.6305 3.1885L11.8105 1.3675L10.9625 1.3685L9.8025 2.5285L12.4715 5.1965ZM4.3105 13.1585C4.4705 13.1285 4.6175 13.0515 4.7335 12.9345L11.5865 6.0815L8.9185 3.4135L2.0655 10.2655C1.9485 10.3835 1.8715 10.5305 1.8405 10.6915L1.3665 13.2485L1.7515 13.6335L4.3105 13.1585Z"/> +</svg> diff --git a/toolkit/themes/shared/icons/loading.svg b/toolkit/themes/shared/icons/loading.svg new file mode 100644 index 0000000000..1a10324c5a --- /dev/null +++ b/toolkit/themes/shared/icons/loading.svg @@ -0,0 +1,16 @@ +<!-- 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/. --> +<svg id="loading-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" width="12" height="12" fill="context-fill"> + <style> + @keyframes loadingSVGRotate { + from { rotate: 0; } to { rotate: 360deg } + } + #loading-svg { + animation: loadingSVGRotate 1.2s linear infinite; + transform-origin: 50% 50%; + } + </style> + <path d="M8.9 3.8c-.2-.2-.1-.5.1-.7.2-.1.6-.1.7.2.5.7.8 1.6.8 2.5 0 2.5-2 4.5-4.5 4.5l0 1.5c0 .2-.2.3-.3.1l-2-1.9 0-.4 1.9-1.9c.2-.2.4-.1.4.1l0 1.5c1.9 0 3.5-1.6 3.5-3.5 0-.7-.2-1.4-.6-2z"/> + <path d="M3.1 8.2c.2.2.1.5-.1.7-.2.1-.6.1-.7-.2-.5-.7-.8-1.6-.8-2.5 0-2.5 2-4.5 4.5-4.5L6 .2c0-.2.2-.3.3-.1l2 1.9 0 .4-2 2c-.1.1-.3 0-.3-.2l0-1.5c-1.9 0-3.5 1.6-3.5 3.5 0 .7.2 1.4.6 2z"/> +</svg> diff --git a/toolkit/themes/shared/icons/whatsnew.svg b/toolkit/themes/shared/icons/whatsnew.svg deleted file mode 100644 index b77d0165a6..0000000000 --- a/toolkit/themes/shared/icons/whatsnew.svg +++ /dev/null @@ -1,10 +0,0 @@ -<!-- 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/. --> -<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"> - <path fill="context-fill" fill-opacity="context-fill-opacity" d="M2 13.76A1.23 1.23 0 0 0 3.24 15H7V9H2z"/> - <path fill="context-fill" fill-opacity="context-fill-opacity" d="M9 9v6h3.76A1.23 1.23 0 0 0 14 13.76V9H9z"/> - <path fill="context-fill" fill-opacity="context-fill-opacity" d="M1 5v3h6V4H2a1 1 0 0 0-1 1z"/> - <path fill="context-fill" fill-opacity="context-fill-opacity" d="M14 4H9v4h6V5a1 1 0 0 0-1-1z"/> - <path fill="context-fill" fill-opacity="context-fill-opacity" d="M10.05.41A1.34 1.34 0 0 0 8 1a1.35 1.35 0 0 0-2-.59C5.2 1.08 4.91 2.48 8 4c3.09-1.52 2.8-2.92 2.05-3.59z"/> -</svg> diff --git a/toolkit/themes/shared/in-content/common-shared.css b/toolkit/themes/shared/in-content/common-shared.css index c8f4f3efd2..86c6d36210 100644 --- a/toolkit/themes/shared/in-content/common-shared.css +++ b/toolkit/themes/shared/in-content/common-shared.css @@ -14,9 +14,9 @@ :host(:is(.anonymous-content-host, notification-message)), :root { --in-content-page-color: rgb(21, 20, 26); - --in-content-page-background: #fff; + --in-content-page-background: var(--background-color-canvas); --in-content-text-color: var(--in-content-page-color); - --in-content-box-background: var(--box-background-color); + --in-content-box-background: var(--background-color-box); --in-content-box-background-odd: rgba(12, 12, 13, 0.05); /* grey 90 a05 */ --in-content-box-border-color: color-mix(in srgb, currentColor 41%, transparent); --in-content-box-info-background: #f0f0f4; @@ -122,7 +122,6 @@ * just replace most of these for system colors and remove all this * duplication (assuming we honor the preferred color scheme for * in-content privileged pages and plain-text documents). */ - --in-content-page-background: rgb(28,27,34); --in-content-page-color: rgb(251,251,254); --in-content-box-background-odd: rgba(249,249,250,0.05); @@ -156,20 +155,19 @@ } /* For dialogs, use a different background colour. We don't do - * this in High Contrast mode, as we should be using system colours then. + * this in forced colors mode, as we should be using system colours then. */ - @media not (prefers-contrast) { + @media not (forced-colors) { :root[dialogroot] { --in-content-page-background: #42414d; } } } -@media (prefers-contrast) { +@media (forced-colors) { :host(:is(.anonymous-content-host, notification-message)), :root { --in-content-page-color: CanvasText; - --in-content-page-background: Canvas; --in-content-box-background-odd: var(--in-content-box-background); --in-content-box-border-color: -moz-DialogText; @@ -304,7 +302,7 @@ xul|tab[selected]:hover:active { background-color: var(--in-content-button-background-active); } -@media (prefers-contrast) { +@media (forced-colors) { xul|tab:hover, xul|tab:hover:active { border-bottom-color: currentColor; @@ -483,7 +481,7 @@ xul|button.primary:not([disabled="true"]):hover:active { border-color: var(--in-content-primary-button-border-active); } -@media not (prefers-contrast) { +@media not (forced-colors) { html|button.semi-transparent:not(.ghost-button, .primary):enabled { background-color: color-mix(in srgb, currentColor 10%, transparent); } @@ -504,7 +502,7 @@ xul|button.primary:not([disabled="true"]):hover:active { --in-content-focus-outline-color: var(--in-content-danger-button-background); } -@media not (prefers-contrast) { +@media not (forced-colors) { html|button.ghost-button { background-color: transparent; } @@ -798,7 +796,7 @@ html|*#categories > html|*.category:dir(rtl) { border-color: var(--in-content-button-border-color-active); } -@media not (prefers-contrast) { +@media not (forced-colors) { #categories > .category[selected], #categories > .category.selected { color: var(--in-content-accent-color); @@ -815,7 +813,7 @@ html|*#categories > html|*.category:dir(rtl) { } } -@media (prefers-contrast) { +@media (forced-colors) { #categories > .category { /* The transition causes issues with the text getting a background while * transitioning and it looks weird. */ @@ -963,7 +961,7 @@ xul|treechildren::-moz-tree-row(selected) { color: var(--in-content-item-selected-text); } -@media not (prefers-contrast) { +@media not (forced-colors) { xul|richlistbox:not(#categories) > xul|richlistitem[selected] { /* Ensure buttons/menulists inside richlistitems (containers, applications) look OK */ --in-content-button-background: color-mix(in srgb, currentColor 15%, transparent); @@ -981,8 +979,8 @@ xul|richlistitem[selected] xul|menulist:focus-visible { } /* Use a 2px border so that selected row highlight is still visible behind - an existing high-contrast border that uses the background color */ -@media (prefers-contrast) { + an existing forced colors border that uses the background color */ +@media (forced-colors) { xul|treechildren::-moz-tree-row(selected) { border: 2px solid currentColor; border-radius: 4px; @@ -1046,7 +1044,7 @@ xul|treecol:not([hideheader="true"], :first-child), border-image: linear-gradient(transparent 0%, transparent 20%, var(--in-content-box-border-color) 20%, var(--in-content-box-border-color) 80%, transparent 80%, transparent 100%) 1 1; } -@media (prefers-contrast) { +@media (forced-colors) { xul|treecol:not([hideheader="true"], :first-child), xul|treecolpicker { --in-content-box-border-color: var(--in-content-button-border-color); @@ -1191,7 +1189,7 @@ input[type="text"][warning]:enabled:not(:focus) { align-items: center; } -@media (prefers-contrast) { +@media (forced-colors) { .sidebar-footer-link { /* We need a true transparent but in HCM this would compute to an actual color, * so select the page's background color instead: */ diff --git a/toolkit/themes/shared/narrate.css b/toolkit/themes/shared/narrate.css index fbda43841e..9ba7015215 100644 --- a/toolkit/themes/shared/narrate.css +++ b/toolkit/themes/shared/narrate.css @@ -16,12 +16,17 @@ body.sepia { --narrating-paragraph-background-color: #e0d7c5; } -body.dark { +body.dark, +body.contrast { --current-voice: #a09eac; --narrate-word-highlight-color: #6f6f6f; --narrating-paragraph-background-color: #242424; } +body.custom { + --narrating-paragraph-background-color: var(--custom-theme-selection-highlight); +} + body.hcm { --current-voice: inherit; --narrate-word-highlight-color: SelectedItem; @@ -100,14 +105,6 @@ body.hcm .speaking:not(.open) .narrate-toggle:hover { outline-offset: -2px; } -.narrate-dropdown > .dropdown-popup { - top: -34px; -} - -.narrate-dropdown .dropdown-arrow { - top: 40px; -} - .narrate-row { display: flex; align-items: center; @@ -168,7 +165,7 @@ body.hcm .speaking:not(.open) .narrate-toggle:hover { background-repeat: no-repeat; background-size: 24px auto; -moz-context-properties: fill; - fill: var(--icon-fill); + fill: var(--popup-button-foreground); } .narrate-rate::before { @@ -193,7 +190,7 @@ body.hcm .speaking:not(.open) .narrate-toggle:hover { } .narrate-rate-input::-moz-range-track { - background-color: var(--icon-fill); + background-color: var(--popup-button-foreground); height: 2px; } @@ -203,7 +200,7 @@ body.hcm .speaking:not(.open) .narrate-toggle:hover { } .narrate-rate-input::-moz-range-thumb { - background-color: var(--icon-fill); + background-color: var(--popup-button-foreground); height: 16px; width: 16px; border-radius: 8px; @@ -244,6 +241,10 @@ body.hcm .speaking:not(.open) .narrate-toggle:hover { color: var(--current-voice); } +.voiceselect .label { + color: var(--popup-button-foreground); +} + .voiceselect > .options { display: none; overflow-y: auto; @@ -256,6 +257,7 @@ body.hcm .speaking:not(.open) .narrate-toggle:hover { .voiceselect > .options > button.option { box-sizing: border-box; + color: var(--popup-button-foreground); } .voiceselect > .options > button.option:not(:first-child) { diff --git a/toolkit/themes/shared/notification.css b/toolkit/themes/shared/notification.css index b94918cce8..8ddab7fff2 100644 --- a/toolkit/themes/shared/notification.css +++ b/toolkit/themes/shared/notification.css @@ -41,15 +41,15 @@ notification { } @media (prefers-color-scheme: dark) { - notification[type="info"]:-moz-lwtheme { - --notification-background: #38383d; - --notification-text: rgb(249, 249, 250); - } - notification[type="info"] { --notification-button-background: rgba(249,249,250,.1); --notification-button-background-hover: rgba(249,249,250,.2); --notification-button-background-active: rgba(249,249,250,.3); + + *|*:root[lwtheme] & { + --notification-background: #38383d; + --notification-text: rgb(249, 249, 250); + } } } diff --git a/toolkit/themes/shared/pictureinpicture/player.css b/toolkit/themes/shared/pictureinpicture/player.css index f29d68e429..0395408191 100644 --- a/toolkit/themes/shared/pictureinpicture/player.css +++ b/toolkit/themes/shared/pictureinpicture/player.css @@ -326,31 +326,29 @@ body:fullscreen #controls[showing]:hover { background-color: transparent; padding: 6px 2px; margin: 0; -} -#audio-scrubber::-moz-range-thumb { - border-radius: 8px; - background-color: #FFFFFF; - position: relative; - width: 8px; - height: 8px; - bottom: 24px; - padding: 0; -} + &::-moz-range-thumb { + border-radius: 8px; + background-color: #FFFFFF; + width: 8px; + height: 8px; + padding: 0; + } -#audio-scrubber::-moz-range-track { - background-color: #969696; -} + &::-moz-range-track { + background-color: #969696; + } -#audio-scrubber::-moz-range-progress { - background-color: #FFFFFF; -} + &::-moz-range-progress { + background-color: #FFFFFF; + } -#audio-scrubber, -#audio-scrubber::-moz-range-track, -#audio-scrubber::-moz-range-progress { - height: 2px; - border-radius: 10px; + &, + &::-moz-range-track, + &::-moz-range-progress { + height: 2px; + border-radius: 10px; + } } #fullscreen { @@ -594,32 +592,30 @@ input:checked + .slider::before { width: 100%; background-color: transparent; padding: 6px 2px; -} -#scrubber::-moz-range-thumb { - border-radius: 14px; - background-color: #BFBFC9; - position: relative; - width: 8px; - height: 8px; - border: 3px solid #FFFFFF; - bottom: 24px; - padding: 0; -} + &::-moz-range-thumb { + border-radius: 14px; + background-color: #BFBFC9; + width: 8px; + height: 8px; + border: 3px solid #FFFFFF; + padding: 0; + } -#scrubber::-moz-range-track { - background-color: #969696; -} + &::-moz-range-track { + background-color: #969696; + } -#scrubber::-moz-range-progress { - background-color: #FFFFFF; -} + &::-moz-range-progress { + background-color: #FFFFFF; + } -#scrubber, -#scrubber::-moz-range-track, -#scrubber::-moz-range-progress { - height: 4px; - border-radius: 10px; + &, + &::-moz-range-track, + &::-moz-range-progress { + height: 4px; + border-radius: 10px; + } } #timestamp { diff --git a/toolkit/themes/shared/popup.css b/toolkit/themes/shared/popup.css index 1426f9c4f6..ed23086021 100644 --- a/toolkit/themes/shared/popup.css +++ b/toolkit/themes/shared/popup.css @@ -27,7 +27,7 @@ panel { margin: calc(-1 * var(--panel-shadow-margin)); /* Panel design token theming */ - --color-canvas: var(--panel-background); + --background-color-canvas: var(--panel-background); @media (-moz-platform: linux) { --panel-border-radius: 8px; diff --git a/toolkit/themes/shared/reader/RM-Type-Controls-Arrow.svg b/toolkit/themes/shared/reader/RM-Type-Controls-Arrow.svg deleted file mode 100644 index 235f186376..0000000000 --- a/toolkit/themes/shared/reader/RM-Type-Controls-Arrow.svg +++ /dev/null @@ -1,7 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- 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/. --> -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 24"> - <polygon points="16.58 0.01 16.57 0 4.58 12 16.57 24 16.58 23.98" fill="context-fill" stroke="context-stroke"/> -</svg> diff --git a/toolkit/themes/windows/global/menu.css b/toolkit/themes/windows/global/menu.css index 4868f41ed1..4889de0003 100644 --- a/toolkit/themes/windows/global/menu.css +++ b/toolkit/themes/windows/global/menu.css @@ -51,20 +51,22 @@ menubar > menu[_moz-menuactive="true"]:not([disabled="true"]) { color: -moz-menubarhovertext; } -menubar > menu:-moz-lwtheme { - appearance: none; - border-color: transparent; +*|*:root[lwtheme] { + menubar > menu { + appearance: none; + border-color: transparent; + + &:not([disabled="true"]) { + color: inherit; + } + &[_moz-menuactive="true"]:not([disabled="true"]) { + background-color: SelectedItem; + color: SelectedItemText; + text-shadow: none; + } + } } -menubar > menu:-moz-lwtheme:not([disabled="true"]) { - color: inherit; -} - -menubar > menu:-moz-lwtheme[_moz-menuactive="true"]:not([disabled="true"]) { - background-color: SelectedItem; - color: SelectedItemText; - text-shadow: none; -} /* ..... internal content .... */ diff --git a/toolkit/themes/windows/global/tabprompts.css b/toolkit/themes/windows/global/tabprompts.css deleted file mode 100644 index 2b5fbdf55f..0000000000 --- a/toolkit/themes/windows/global/tabprompts.css +++ /dev/null @@ -1,25 +0,0 @@ -/* 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/. */ - -/* Tab Modal Prompt boxes */ -.tabModalBackground, -tabmodalprompt { - background-color: hsla(0,0%,10%,.5); -} - -tabmodalprompt { - font-family: sans-serif; /* use content font not system UI font */ -} - -.tabmodalprompt-mainContainer { - color: FieldText; - background-color: Field; - border-radius: 2px; - border: 1px solid threeDDarkShadow; -} - -.tabmodalprompt-buttonContainer { - background-color: hsla(0,0%,0%,.05); - border-top: 1px solid hsla(0,0%,0%,.05); -} diff --git a/toolkit/themes/windows/global/toolbar.css b/toolkit/themes/windows/global/toolbar.css index 45b79eafdd..ada6f77f9c 100644 --- a/toolkit/themes/windows/global/toolbar.css +++ b/toolkit/themes/windows/global/toolbar.css @@ -8,14 +8,7 @@ @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); -toolbox { - appearance: auto; - -moz-default-appearance: toolbox; -} - toolbar { - appearance: auto; - -moz-default-appearance: toolbar; min-width: 1px; min-height: 19px; } @@ -24,11 +17,6 @@ toolbar:first-child { min-width: 1px; } -toolbox:-moz-lwtheme, -toolbar:-moz-lwtheme { - appearance: none; -} - toolbarseparator { appearance: auto; -moz-default-appearance: separator; diff --git a/toolkit/xre/nsAppRunner.cpp b/toolkit/xre/nsAppRunner.cpp index c1b712984b..35c58d9b36 100644 --- a/toolkit/xre/nsAppRunner.cpp +++ b/toolkit/xre/nsAppRunner.cpp @@ -285,8 +285,6 @@ static const char kPrefHealthReportUploadEnabled[] = static const char kPrefDefaultAgentEnabled[] = "default-browser-agent.enabled"; static const char kPrefServicesSettingsServer[] = "services.settings.server"; -static const char kPrefSecurityContentSignatureRootHash[] = - "security.content.signature.root_hash"; static const char kPrefSetDefaultBrowserUserChoicePref[] = "browser.shell.setDefaultBrowserUserChoice"; #endif // defined(MOZ_DEFAULT_BROWSER_AGENT) @@ -577,7 +575,7 @@ bool BrowserTabsRemoteAutostart() { // then we could remove this automation-only env variable. if (gBrowserTabsRemoteAutostart && xpc::AreNonLocalConnectionsDisabled()) { const char* forceDisable = PR_GetEnv("MOZ_FORCE_DISABLE_E10S"); - if (forceDisable && *forceDisable == '1') { + if (forceDisable && !strcmp(forceDisable, "1")) { gBrowserTabsRemoteAutostart = false; status = kE10sForceDisabled; } @@ -2441,8 +2439,6 @@ static void OnDefaultAgentRemoteSettingsPrefChanged(const char* aPref, nsAutoString valueName; if (strcmp(aPref, kPrefServicesSettingsServer) == 0) { valueName.AssignLiteral("ServicesSettingsServer"); - } else if (strcmp(aPref, kPrefSecurityContentSignatureRootHash) == 0) { - valueName.AssignLiteral("SecurityContentSignatureRootHash"); } else { return; } @@ -5595,9 +5591,6 @@ nsresult XREMain::XRE_mainRun() { Preferences::RegisterCallbackAndCall( &OnDefaultAgentRemoteSettingsPrefChanged, kPrefServicesSettingsServer); - Preferences::RegisterCallbackAndCall( - &OnDefaultAgentRemoteSettingsPrefChanged, - kPrefSecurityContentSignatureRootHash); Preferences::RegisterCallbackAndCall( &OnSetDefaultBrowserUserChoicePrefChanged, diff --git a/toolkit/xre/test/browser_checkdllblockliststate.js b/toolkit/xre/test/browser_checkdllblockliststate.js index 759c9b3635..cddd086991 100644 --- a/toolkit/xre/test/browser_checkdllblockliststate.js +++ b/toolkit/xre/test/browser_checkdllblockliststate.js @@ -5,7 +5,7 @@ add_task(async function test() { await BrowserTestUtils.withNewTab( { gBrowser, url: "about:blank" }, - function (browser) { + function () { ok( Services.appinfo.windowsDLLBlocklistStatus, "Windows dll blocklist status should be true, indicating it is " + diff --git a/toolkit/xre/test/marionette/gen_win32k_tests.py b/toolkit/xre/test/marionette/gen_win32k_tests.py index e0a39776db..2b90cdcefe 100644 --- a/toolkit/xre/test/marionette/gen_win32k_tests.py +++ b/toolkit/xre/test/marionette/gen_win32k_tests.py @@ -58,8 +58,7 @@ def set_e10s(enable): if enable: output.write( """ - app_version = self.execute_script("return Services.appinfo.version") - self.restart(env={ENV_DISABLE_E10S: app_version}) + self.restart(env={ENV_DISABLE_E10S: "1"}) self.set_env(ENV_DISABLE_E10S, "null")\n""" ) else: diff --git a/toolkit/xre/test/marionette/test_fission_autostart.py b/toolkit/xre/test/marionette/test_fission_autostart.py index e34087aca9..40c083d00c 100644 --- a/toolkit/xre/test/marionette/test_fission_autostart.py +++ b/toolkit/xre/test/marionette/test_fission_autostart.py @@ -238,8 +238,7 @@ class TestFissionAutostart(MarionetteTestCase): decision="enabledByDefault", ) - app_version = self.execute_script("return Services.appinfo.version") - self.restart(env={ENV_DISABLE_E10S: app_version}) + self.restart(env={ENV_DISABLE_E10S: "1"}) self.check_fission_status( enabled=False, decision="disabledByE10sEnv", diff --git a/toolkit/xre/test/marionette/test_win32k_enrollment.py b/toolkit/xre/test/marionette/test_win32k_enrollment.py index d09331319b..7aaad1b0cd 100644 --- a/toolkit/xre/test/marionette/test_win32k_enrollment.py +++ b/toolkit/xre/test/marionette/test_win32k_enrollment.py @@ -389,8 +389,7 @@ class TestWin32kAutostart(MarionetteTestCase): self.marionette.set_pref(Prefs.WIN32K, True) - app_version = self.execute_script("return Services.appinfo.version") - self.restart(env={ENV_DISABLE_E10S: app_version}) + self.restart(env={ENV_DISABLE_E10S: "1"}) self.set_env(ENV_DISABLE_E10S, "null") self.check_win32k_status( @@ -623,8 +622,7 @@ class TestWin32kAutostart(MarionetteTestCase): self.marionette.set_pref(Prefs.WIN32K, True) - app_version = self.execute_script("return Services.appinfo.version") - self.restart(env={ENV_DISABLE_E10S: app_version}) + self.restart(env={ENV_DISABLE_E10S: "1"}) self.set_env(ENV_DISABLE_E10S, "null") self.check_win32k_status( @@ -869,8 +867,7 @@ class TestWin32kAutostart(MarionetteTestCase): enrollmentStatusPref=ExperimentStatus.UNENROLLED, ) - app_version = self.execute_script("return Services.appinfo.version") - self.restart(env={ENV_DISABLE_E10S: app_version}) + self.restart(env={ENV_DISABLE_E10S: "1"}) self.set_env(ENV_DISABLE_E10S, "null") self.check_win32k_status( @@ -1137,8 +1134,7 @@ class TestWin32kAutostart(MarionetteTestCase): enrollmentStatusPref=ExperimentStatus.UNENROLLED, ) - app_version = self.execute_script("return Services.appinfo.version") - self.restart(env={ENV_DISABLE_E10S: app_version}) + self.restart(env={ENV_DISABLE_E10S: "1"}) self.set_env(ENV_DISABLE_E10S, "null") self.check_win32k_status( @@ -1366,8 +1362,7 @@ class TestWin32kAutostart(MarionetteTestCase): # Re-set enrollment pref, like Normandy would do self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) - app_version = self.execute_script("return Services.appinfo.version") - self.restart(env={ENV_DISABLE_E10S: app_version}) + self.restart(env={ENV_DISABLE_E10S: "1"}) self.set_env(ENV_DISABLE_E10S, "null") self.check_win32k_status( @@ -1603,8 +1598,7 @@ class TestWin32kAutostart(MarionetteTestCase): # Re-set enrollment pref, like Normandy would do self.set_enrollment_status(ExperimentStatus.ENROLLED_CONTROL) - app_version = self.execute_script("return Services.appinfo.version") - self.restart(env={ENV_DISABLE_E10S: app_version}) + self.restart(env={ENV_DISABLE_E10S: "1"}) self.set_env(ENV_DISABLE_E10S, "null") self.check_win32k_status( @@ -1853,8 +1847,7 @@ class TestWin32kAutostart(MarionetteTestCase): # Re-set enrollment pref, like Normandy would do self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) - app_version = self.execute_script("return Services.appinfo.version") - self.restart(env={ENV_DISABLE_E10S: app_version}) + self.restart(env={ENV_DISABLE_E10S: "1"}) self.set_env(ENV_DISABLE_E10S, "null") self.check_win32k_status( @@ -2103,8 +2096,7 @@ class TestWin32kAutostart(MarionetteTestCase): # Re-set enrollment pref, like Normandy would do self.set_enrollment_status(ExperimentStatus.ENROLLED_TREATMENT) - app_version = self.execute_script("return Services.appinfo.version") - self.restart(env={ENV_DISABLE_E10S: app_version}) + self.restart(env={ENV_DISABLE_E10S: "1"}) self.set_env(ENV_DISABLE_E10S, "null") self.check_win32k_status( diff --git a/toolkit/xre/test/test_launch_without_hang.js b/toolkit/xre/test/test_launch_without_hang.js index feacae36d4..eef1ece226 100644 --- a/toolkit/xre/test/test_launch_without_hang.js +++ b/toolkit/xre/test/test_launch_without_hang.js @@ -100,7 +100,7 @@ function terminateFirefox(completion) { process.init(file); let processObserver = { - observe: function PO_observe(aSubject, aTopic, aData) { + observe: function PO_observe(aSubject, aTopic) { info("topic: " + aTopic + ", process exitValue: " + process.exitValue); Assert.equal( @@ -135,7 +135,7 @@ function launchProcess(file, args, env, timeoutMS, handler, attemptCount) { state.attempt = attemptCount; state.processObserver = { - observe: function PO_observe(aSubject, aTopic, aData) { + observe: function PO_observe(aSubject, aTopic) { if (!state.appTimer) { // the app timer has been canceled; this process has timed out already so don't process further. handler(false); @@ -170,7 +170,7 @@ function launchProcess(file, args, env, timeoutMS, handler, attemptCount) { // The timer callback to kill the process if it takes too long. state.appTimerCallback = { - notify: function TC_notify(aTimer) { + notify: function TC_notify() { state.appTimer = null; info("Restoring environment variables"); diff --git a/toolkit/xre/test/win/mochitest/browser_env_path_long.js b/toolkit/xre/test/win/mochitest/browser_env_path_long.js index 9bc63214b0..33832bba77 100644 --- a/toolkit/xre/test/win/mochitest/browser_env_path_long.js +++ b/toolkit/xre/test/win/mochitest/browser_env_path_long.js @@ -6,7 +6,7 @@ add_task(async function test() { await BrowserTestUtils.withNewTab( { gBrowser, url: "about:blank" }, - function (browser) { + function () { ok( true, "Browser should start even with potentially pathologically long PATH." |