Compare commits

..

1 Commits

Author SHA1 Message Date
mr. m
c3aba894bb no-bug: Open boosts editor with util window animations 2026-06-08 12:22:58 +02:00
60 changed files with 2992 additions and 977 deletions

View File

@@ -60,7 +60,7 @@ jobs:
brew install watchman brew install watchman
cargo install apple-codesign --locked --force cargo install apple-codesign
- name: Force usage of gnu-tar - name: Force usage of gnu-tar
run: | run: |

View File

@@ -34,8 +34,8 @@ Zen is a firefox-based browser with the aim of pushing your productivity to a ne
### Firefox Versions ### Firefox Versions
- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `151.0.4`! 🚀 - [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `151.0.3`! 🚀
- [`Twilight`](https://zen-browser.app/download?twilight) - Is currently built using Firefox version `RC 151.0.4`! - [`Twilight`](https://zen-browser.app/download?twilight) - Is currently built using Firefox version `RC 151.0.3`!
### Contributing ### Contributing

View File

@@ -1 +1 @@
9a6aa4c359d1fb6ac60decc82402f82d49a17cea 5c4d14a559bf26eb4ab3e136d2084310ebe51ac0

View File

@@ -94,8 +94,3 @@
# See gh-12985 for details on the following preferences # See gh-12985 for details on the following preferences
- name: browser.search.widget.new - name: browser.search.widget.new
value: true value: true
# Disabled from https://searchfox.org/firefox-main/rev/d6bfff43852356ca98af848b4705d37f8d41856f/modules/libpref/init/StaticPrefList.yaml#2008
# Only enabled for windows, doesn't really fit inside Zen.
- name: browser.startup.preXulSkeletonUI
value: false

View File

@@ -945,7 +945,6 @@ var gZenCKSSettings = {
}); });
input.addEventListener("blur", (event) => { input.addEventListener("blur", (event) => {
this._currentActionID = null;
const target = event.target; const target = event.target;
target.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-editing`); target.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-editing`);
if (!this._hasSafed) { if (!this._hasSafed) {
@@ -1050,7 +1049,6 @@ var gZenCKSSettings = {
input.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-not-set`); input.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-not-set`);
input.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-editing`); input.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-editing`);
this._latestValidKey = null; this._latestValidKey = null;
this._currentActionID = null;
return; return;
} else if (shortcut == "Escape" && !modifiersActive) { } else if (shortcut == "Escape" && !modifiersActive) {
const { hasConflicts, conflictShortcut } = gZenKeyboardShortcutsManager.checkForConflicts( const { hasConflicts, conflictShortcut } = gZenKeyboardShortcutsManager.checkForConflicts(

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js
index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008ede72076 100644 index 43fb79a3060e20f671ae6ffc26350c7abf497702..2da16e06541438ece4a3ae3a1663a1559780fe22 100644
--- a/browser/components/tabbrowser/content/tabbrowser.js --- a/browser/components/tabbrowser/content/tabbrowser.js
+++ b/browser/components/tabbrowser/content/tabbrowser.js +++ b/browser/components/tabbrowser/content/tabbrowser.js
@@ -502,6 +502,7 @@ @@ -502,6 +502,7 @@
@@ -79,17 +79,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
set selectedTab(val) { set selectedTab(val) {
if ( if (
gSharedTabWarning.willShowSharedTabWarning(val) || gSharedTabWarning.willShowSharedTabWarning(val) ||
@@ -592,6 +644,9 @@ @@ -659,6 +711,10 @@
) {
return;
}
+ if (gZenWorkspaces.onBeforeTabSelect(val)) {
+ return;
+ }
// Update the tab
this.tabbox.selectedTab = val;
}
@@ -659,6 +714,10 @@
userContextId = parseInt(tabArgument.getAttribute("usercontextid"), 10); userContextId = parseInt(tabArgument.getAttribute("usercontextid"), 10);
} }
@@ -100,7 +90,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if (tabArgument && tabArgument.linkedBrowser) { if (tabArgument && tabArgument.linkedBrowser) {
remoteType = tabArgument.linkedBrowser.remoteType; remoteType = tabArgument.linkedBrowser.remoteType;
initialBrowsingContextGroupId = initialBrowsingContextGroupId =
@@ -751,6 +810,8 @@ @@ -751,6 +807,8 @@
this.tabpanels.appendChild(panel); this.tabpanels.appendChild(panel);
let tab = this.tabs[0]; let tab = this.tabs[0];
@@ -109,7 +99,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
tab.linkedPanel = uniqueId; tab.linkedPanel = uniqueId;
this._selectedTab = tab; this._selectedTab = tab;
this._selectedBrowser = browser; this._selectedBrowser = browser;
@@ -1121,13 +1182,18 @@ @@ -1121,13 +1179,18 @@
} }
this.showTab(aTab); this.showTab(aTab);
@@ -129,7 +119,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
aTab.setAttribute("pinned", "true"); aTab.setAttribute("pinned", "true");
this._updateTabBarForPinnedTabs(); this._updateTabBarForPinnedTabs();
@@ -1140,11 +1206,19 @@ @@ -1140,11 +1203,19 @@
} }
this.#handleTabMove(aTab, () => { this.#handleTabMove(aTab, () => {
@@ -150,7 +140,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
}); });
aTab.style.marginInlineStart = ""; aTab.style.marginInlineStart = "";
@@ -1321,6 +1395,9 @@ @@ -1321,6 +1392,9 @@
let LOCAL_PROTOCOLS = ["chrome:", "about:", "resource:", "data:"]; let LOCAL_PROTOCOLS = ["chrome:", "about:", "resource:", "data:"];
@@ -160,7 +150,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if ( if (
aIconURL && aIconURL &&
!LOCAL_PROTOCOLS.some(protocol => aIconURL.startsWith(protocol)) !LOCAL_PROTOCOLS.some(protocol => aIconURL.startsWith(protocol))
@@ -1330,6 +1407,9 @@ @@ -1330,6 +1404,9 @@
); );
return; return;
} }
@@ -170,7 +160,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
let browser = this.getBrowserForTab(aTab); let browser = this.getBrowserForTab(aTab);
browser.mIconURL = aIconURL; browser.mIconURL = aIconURL;
@@ -1652,7 +1732,6 @@ @@ -1652,7 +1729,6 @@
// Preview mode should not reset the owner // Preview mode should not reset the owner
if (!this._previewMode && !oldTab.selected) { if (!this._previewMode && !oldTab.selected) {
@@ -178,7 +168,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
} }
let lastRelatedTab = this._lastRelatedTabMap.get(oldTab); let lastRelatedTab = this._lastRelatedTabMap.get(oldTab);
@@ -1743,6 +1822,7 @@ @@ -1743,6 +1819,7 @@
if (!this._previewMode) { if (!this._previewMode) {
newTab.recordTimeFromUnloadToReload(); newTab.recordTimeFromUnloadToReload();
newTab.updateLastAccessed(); newTab.updateLastAccessed();
@@ -186,7 +176,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
oldTab.updateLastAccessed(); oldTab.updateLastAccessed();
// if this is the foreground window, update the last-seen timestamps. // if this is the foreground window, update the last-seen timestamps.
if (this.ownerGlobal == BrowserWindowTracker.getTopWindow()) { if (this.ownerGlobal == BrowserWindowTracker.getTopWindow()) {
@@ -1957,6 +2037,9 @@ @@ -1957,6 +2034,9 @@
} }
let activeEl = document.activeElement; let activeEl = document.activeElement;
@@ -196,7 +186,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
// If focus is on the old tab, move it to the new tab. // If focus is on the old tab, move it to the new tab.
if (activeEl == oldTab) { if (activeEl == oldTab) {
newTab.focus(); newTab.focus();
@@ -1995,7 +2078,7 @@ @@ -1995,7 +2075,7 @@
// Focus the location bar if it was previously focused for that tab. // Focus the location bar if it was previously focused for that tab.
// In full screen mode, only bother making the location bar visible // In full screen mode, only bother making the location bar visible
// if the tab is a blank one. // if the tab is a blank one.
@@ -205,7 +195,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
let selectURL = () => { let selectURL = () => {
if (this._asyncTabSwitching) { if (this._asyncTabSwitching) {
// Set _awaitingSetURI flag to suppress popup notification // Set _awaitingSetURI flag to suppress popup notification
@@ -2283,7 +2366,12 @@ @@ -2283,7 +2363,12 @@
return this._setTabLabel(aTab, aLabel); return this._setTabLabel(aTab, aLabel);
} }
@@ -219,7 +209,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if (!aLabel || (isURL && /^about:reader\?url=/.test(aLabel))) { if (!aLabel || (isURL && /^about:reader\?url=/.test(aLabel))) {
return false; return false;
} }
@@ -2408,7 +2496,7 @@ @@ -2408,7 +2493,7 @@
newIndex = this.selectedTab._tPos + 1; newIndex = this.selectedTab._tPos + 1;
} }
@@ -228,7 +218,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if (this.isTabGroupLabel(targetTab)) { if (this.isTabGroupLabel(targetTab)) {
throw new Error( throw new Error(
"Replacing a tab group label with a tab is not supported" "Replacing a tab group label with a tab is not supported"
@@ -2685,6 +2773,7 @@ @@ -2685,6 +2770,7 @@
uriIsAboutBlank, uriIsAboutBlank,
userContextId, userContextId,
skipLoad, skipLoad,
@@ -236,7 +226,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
} = {}) { } = {}) {
let b = document.createXULElement("browser"); let b = document.createXULElement("browser");
// Use the JSM global to create the permanentKey, so that if the // Use the JSM global to create the permanentKey, so that if the
@@ -2758,8 +2847,7 @@ @@ -2758,8 +2844,7 @@
// we use a different attribute name for this? // we use a different attribute name for this?
b.setAttribute("name", name); b.setAttribute("name", name);
} }
@@ -246,7 +236,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
b.setAttribute("transparent", "true"); b.setAttribute("transparent", "true");
} }
@@ -2929,7 +3017,7 @@ @@ -2929,7 +3014,7 @@
let panel = this.getPanel(browser); let panel = this.getPanel(browser);
let uniqueId = this._generateUniquePanelID(); let uniqueId = this._generateUniquePanelID();
@@ -255,7 +245,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
aTab.linkedPanel = uniqueId; aTab.linkedPanel = uniqueId;
// Inject the <browser> into the DOM if necessary. // Inject the <browser> into the DOM if necessary.
@@ -2989,8 +3077,8 @@ @@ -2989,8 +3074,8 @@
// If we transitioned from one browser to two browsers, we need to set // If we transitioned from one browser to two browsers, we need to set
// hasSiblings=false on both the existing browser and the new browser. // hasSiblings=false on both the existing browser and the new browser.
if (this.tabs.length == 2) { if (this.tabs.length == 2) {
@@ -266,7 +256,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
} else { } else {
aTab.linkedBrowser.browsingContext.hasSiblings = this.tabs.length > 1; aTab.linkedBrowser.browsingContext.hasSiblings = this.tabs.length > 1;
} }
@@ -3175,7 +3263,6 @@ @@ -3175,7 +3260,6 @@
this.selectedTab = this.addTrustedTab(BROWSER_NEW_TAB_URL, { this.selectedTab = this.addTrustedTab(BROWSER_NEW_TAB_URL, {
tabIndex: tab._tPos + 1, tabIndex: tab._tPos + 1,
userContextId: tab.userContextId, userContextId: tab.userContextId,
@@ -274,7 +264,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
focusUrlBar: true, focusUrlBar: true,
}); });
resolve(this.selectedBrowser); resolve(this.selectedBrowser);
@@ -3285,6 +3372,10 @@ @@ -3285,6 +3369,10 @@
schemelessInput, schemelessInput,
hasValidUserGestureActivation = false, hasValidUserGestureActivation = false,
textDirectiveUserActivation = false, textDirectiveUserActivation = false,
@@ -285,7 +275,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
} = {} } = {}
) { ) {
// all callers of addTab that pass a params object need to pass // all callers of addTab that pass a params object need to pass
@@ -3295,10 +3386,25 @@ @@ -3295,10 +3383,25 @@
); );
} }
@@ -296,7 +286,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
+ +
+ let hasZenDefaultUserContextId = false; + let hasZenDefaultUserContextId = false;
+ let zenForcedWorkspaceId = undefined; + let zenForcedWorkspaceId = undefined;
+ if (beforeRouteResult.isRouteFound) { + if (beforeRouteResult.userContextId) {
+ userContextId = beforeRouteResult.userContextId; + userContextId = beforeRouteResult.userContextId;
+ hasZenDefaultUserContextId = true; + hasZenDefaultUserContextId = true;
+ } else if (typeof gZenWorkspaces !== "undefined" && !_forZenEmptyTab) { + } else if (typeof gZenWorkspaces !== "undefined" && !_forZenEmptyTab) {
@@ -311,7 +301,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
// If we're opening a foreground tab, set the owner by default. // If we're opening a foreground tab, set the owner by default.
ownerTab ??= inBackground ? null : this.selectedTab; ownerTab ??= inBackground ? null : this.selectedTab;
@@ -3306,6 +3412,7 @@ @@ -3306,6 +3409,7 @@
if (this.selectedTab.owner) { if (this.selectedTab.owner) {
this.selectedTab.owner = null; this.selectedTab.owner = null;
} }
@@ -319,7 +309,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
// Find the tab that opened this one, if any. This is used for // Find the tab that opened this one, if any. This is used for
// determining positioning, and inherited attributes such as the // determining positioning, and inherited attributes such as the
@@ -3358,6 +3465,22 @@ @@ -3358,6 +3462,22 @@
noInitialLabel, noInitialLabel,
skipBackgroundNotify, skipBackgroundNotify,
}); });
@@ -342,7 +332,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if (insertTab) { if (insertTab) {
// Insert the tab into the tab container in the correct position. // Insert the tab into the tab container in the correct position.
this.#insertTabAtIndex(t, { this.#insertTabAtIndex(t, {
@@ -3366,6 +3489,7 @@ @@ -3366,6 +3486,7 @@
ownerTab, ownerTab,
openerTab, openerTab,
pinned, pinned,
@@ -350,7 +340,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
bulkOrderedOpen, bulkOrderedOpen,
tabGroup: tabGroup ?? openerTab?.group, tabGroup: tabGroup ?? openerTab?.group,
}); });
@@ -3384,6 +3508,7 @@ @@ -3384,6 +3505,7 @@
openWindowInfo, openWindowInfo,
skipLoad, skipLoad,
triggeringRemoteType, triggeringRemoteType,
@@ -358,7 +348,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
})); }));
if (focusUrlBar) { if (focusUrlBar) {
@@ -3508,6 +3633,12 @@ @@ -3508,6 +3630,12 @@
} }
} }
@@ -371,7 +361,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
// Additionally send pinned tab events // Additionally send pinned tab events
if (pinned) { if (pinned) {
this.#notifyPinnedStatus(t); this.#notifyPinnedStatus(t);
@@ -3518,6 +3649,9 @@ @@ -3518,6 +3646,9 @@
if (!inBackground) { if (!inBackground) {
this.selectedTab = t; this.selectedTab = t;
} }
@@ -381,7 +371,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
return t; return t;
} }
@@ -3750,6 +3884,7 @@ @@ -3750,6 +3881,7 @@
isAdoptingGroup = false, isAdoptingGroup = false,
isUserTriggered = false, isUserTriggered = false,
telemetryUserCreateSource = "unknown", telemetryUserCreateSource = "unknown",
@@ -389,7 +379,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
} = {} } = {}
) { ) {
if ( if (
@@ -3760,9 +3895,6 @@ @@ -3760,9 +3892,6 @@
!this.isSplitViewWrapper(tabOrSplitView) !this.isSplitViewWrapper(tabOrSplitView)
) )
) { ) {
@@ -399,7 +389,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
} }
if (!color) { if (!color) {
@@ -3783,9 +3915,14 @@ @@ -3783,9 +3912,14 @@
label, label,
isAdoptingGroup isAdoptingGroup
); );
@@ -416,7 +406,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
); );
group.addTabs(tabsAndSplitViews); group.addTabs(tabsAndSplitViews);
@@ -3906,7 +4043,7 @@ @@ -3906,7 +4040,7 @@
} }
this.#handleTabMove(tab, () => this.#handleTabMove(tab, () =>
@@ -425,7 +415,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
); );
} }
@@ -3990,6 +4127,7 @@ @@ -3990,6 +4124,7 @@
color: group.color, color: group.color,
insertBefore: newTabs[0], insertBefore: newTabs[0],
isAdoptingGroup: true, isAdoptingGroup: true,
@@ -433,7 +423,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
}); });
} }
@@ -4200,6 +4338,7 @@ @@ -4200,6 +4335,7 @@
openWindowInfo, openWindowInfo,
skipLoad, skipLoad,
triggeringRemoteType, triggeringRemoteType,
@@ -441,7 +431,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
} }
) { ) {
// If we don't have a preferred remote type (or it is `NOT_REMOTE`), and // If we don't have a preferred remote type (or it is `NOT_REMOTE`), and
@@ -4269,6 +4408,7 @@ @@ -4269,6 +4405,7 @@
openWindowInfo, openWindowInfo,
name, name,
skipLoad, skipLoad,
@@ -449,7 +439,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
}); });
} }
@@ -4482,9 +4622,9 @@ @@ -4482,9 +4619,9 @@
} }
// Add a new tab if needed. // Add a new tab if needed.
@@ -461,7 +451,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
let url = "about:blank"; let url = "about:blank";
if (tabData.entries?.length) { if (tabData.entries?.length) {
@@ -4521,8 +4661,10 @@ @@ -4521,8 +4658,10 @@
insertTab: false, insertTab: false,
skipLoad: true, skipLoad: true,
preferredRemoteType, preferredRemoteType,
@@ -473,7 +463,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if (select) { if (select) {
tabToSelect = tab; tabToSelect = tab;
} }
@@ -4544,7 +4686,8 @@ @@ -4544,7 +4683,8 @@
this.pinTab(tab); this.pinTab(tab);
// Then ensure all the tab open/pinning information is sent. // Then ensure all the tab open/pinning information is sent.
this._fireTabOpen(tab, {}); this._fireTabOpen(tab, {});
@@ -483,7 +473,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
let { groupId } = tabData; let { groupId } = tabData;
const tabGroup = tabGroupWorkingData.get(groupId); const tabGroup = tabGroupWorkingData.get(groupId);
// if a tab refers to a tab group we don't know, skip any group // if a tab refers to a tab group we don't know, skip any group
@@ -4564,7 +4707,10 @@ @@ -4564,7 +4704,10 @@
tabGroup.stateData.id, tabGroup.stateData.id,
tabGroup.stateData.color, tabGroup.stateData.color,
tabGroup.stateData.collapsed, tabGroup.stateData.collapsed,
@@ -495,7 +485,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
); );
tabsFragment.appendChild(tabGroup.node); tabsFragment.appendChild(tabGroup.node);
} }
@@ -4619,9 +4765,21 @@ @@ -4619,9 +4762,21 @@
// to remove the old selected tab. // to remove the old selected tab.
if (tabToSelect) { if (tabToSelect) {
let leftoverTab = this.selectedTab; let leftoverTab = this.selectedTab;
@@ -517,7 +507,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if (tabs.length > 1 || !tabs[0].selected) { if (tabs.length > 1 || !tabs[0].selected) {
this._updateTabsAfterInsert(); this._updateTabsAfterInsert();
@@ -4812,11 +4970,14 @@ @@ -4812,11 +4967,14 @@
if (ownerTab) { if (ownerTab) {
tab.owner = ownerTab; tab.owner = ownerTab;
} }
@@ -533,7 +523,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if ( if (
!bulkOrderedOpen && !bulkOrderedOpen &&
((openerTab && ((openerTab &&
@@ -4828,7 +4989,7 @@ @@ -4828,7 +4986,7 @@
let lastRelatedTab = let lastRelatedTab =
openerTab && this._lastRelatedTabMap.get(openerTab); openerTab && this._lastRelatedTabMap.get(openerTab);
let previousTab = lastRelatedTab || openerTab || this.selectedTab; let previousTab = lastRelatedTab || openerTab || this.selectedTab;
@@ -542,7 +532,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
tabGroup = previousTab.group; tabGroup = previousTab.group;
} }
if ( if (
@@ -4844,7 +5005,7 @@ @@ -4844,7 +5002,7 @@
previousTab.splitview previousTab.splitview
) + 1; ) + 1;
} else if (previousTab.visible) { } else if (previousTab.visible) {
@@ -551,7 +541,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
} else if (previousTab == FirefoxViewHandler.tab) { } else if (previousTab == FirefoxViewHandler.tab) {
elementIndex = 0; elementIndex = 0;
} }
@@ -4872,14 +5033,14 @@ @@ -4872,14 +5030,14 @@
} }
// Ensure index is within bounds. // Ensure index is within bounds.
if (tab.pinned) { if (tab.pinned) {
@@ -570,7 +560,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if (pinned && !itemAfter?.pinned) { if (pinned && !itemAfter?.pinned) {
itemAfter = null; itemAfter = null;
@@ -4896,7 +5057,7 @@ @@ -4896,7 +5054,7 @@
this.tabContainer._invalidateCachedTabs(); this.tabContainer._invalidateCachedTabs();
@@ -579,7 +569,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if ( if (
(this.isTab(itemAfter) && itemAfter.group == tabGroup) || (this.isTab(itemAfter) && itemAfter.group == tabGroup) ||
this.isSplitViewWrapper(itemAfter) this.isSplitViewWrapper(itemAfter)
@@ -4927,7 +5088,11 @@ @@ -4927,7 +5085,11 @@
const tabContainer = pinned const tabContainer = pinned
? this.tabContainer.pinnedTabsContainer ? this.tabContainer.pinnedTabsContainer
: this.tabContainer; : this.tabContainer;
@@ -591,7 +581,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
} }
if (tab.group?.collapsed) { if (tab.group?.collapsed) {
@@ -4942,6 +5107,7 @@ @@ -4942,6 +5104,7 @@
if (pinned) { if (pinned) {
this._updateTabBarForPinnedTabs(); this._updateTabBarForPinnedTabs();
} }
@@ -599,7 +589,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
TabBarVisibility.update(); TabBarVisibility.update();
} }
@@ -5490,6 +5656,7 @@ @@ -5490,6 +5653,7 @@
telemetrySource, telemetrySource,
} = {} } = {}
) { ) {
@@ -607,7 +597,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
// When 'closeWindowWithLastTab' pref is enabled, closing all tabs // When 'closeWindowWithLastTab' pref is enabled, closing all tabs
// can be considered equivalent to closing the window. // can be considered equivalent to closing the window.
if ( if (
@@ -5579,6 +5746,7 @@ @@ -5579,6 +5743,7 @@
if (lastToClose) { if (lastToClose) {
this.removeTab(lastToClose, aParams); this.removeTab(lastToClose, aParams);
} }
@@ -615,7 +605,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
@@ -5624,6 +5792,14 @@ @@ -5624,6 +5789,14 @@
return; return;
} }
@@ -630,7 +620,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
let isVisibleTab = aTab.visible; let isVisibleTab = aTab.visible;
// We have to sample the tab width now, since _beginRemoveTab might // We have to sample the tab width now, since _beginRemoveTab might
// end up modifying the DOM in such a way that aTab gets a new // end up modifying the DOM in such a way that aTab gets a new
@@ -5631,6 +5807,9 @@ @@ -5631,6 +5804,9 @@
// state). // state).
let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width; let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width;
let isLastTab = this.#isLastTabInWindow(aTab); let isLastTab = this.#isLastTabInWindow(aTab);
@@ -640,7 +630,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if ( if (
!this._beginRemoveTab(aTab, { !this._beginRemoveTab(aTab, {
closeWindowFastpath: true, closeWindowFastpath: true,
@@ -5642,13 +5821,14 @@ @@ -5642,13 +5818,14 @@
telemetrySource, telemetrySource,
}) })
) { ) {
@@ -656,7 +646,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
let lockTabSizing = let lockTabSizing =
!this.tabContainer.verticalMode && !this.tabContainer.verticalMode &&
!aTab.pinned && !aTab.pinned &&
@@ -5679,7 +5859,13 @@ @@ -5679,7 +5856,13 @@
// We're not animating, so we can cancel the animation stopwatch. // We're not animating, so we can cancel the animation stopwatch.
Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId); Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId);
aTab._closeTimeAnimTimerId = null; aTab._closeTimeAnimTimerId = null;
@@ -671,7 +661,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
return; return;
} }
@@ -5813,7 +5999,7 @@ @@ -5813,7 +5996,7 @@
closeWindowWithLastTab != null closeWindowWithLastTab != null
? closeWindowWithLastTab ? closeWindowWithLastTab
: !window.toolbar.visible || : !window.toolbar.visible ||
@@ -680,7 +670,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if (closeWindow) { if (closeWindow) {
// We've already called beforeunload on all the relevant tabs if we get here, // We've already called beforeunload on all the relevant tabs if we get here,
@@ -5837,6 +6023,7 @@ @@ -5837,6 +6020,7 @@
newTab = true; newTab = true;
} }
@@ -688,7 +678,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
aTab._endRemoveArgs = [closeWindow, newTab]; aTab._endRemoveArgs = [closeWindow, newTab];
// swapBrowsersAndCloseOther will take care of closing the window without animation. // swapBrowsersAndCloseOther will take care of closing the window without animation.
@@ -5877,13 +6064,7 @@ @@ -5877,13 +6061,7 @@
aTab._mouseleave(); aTab._mouseleave();
if (newTab) { if (newTab) {
@@ -703,7 +693,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
} else { } else {
TabBarVisibility.update(); TabBarVisibility.update();
} }
@@ -6016,6 +6197,7 @@ @@ -6016,6 +6194,7 @@
this.tabs[i]._tPos = i; this.tabs[i]._tPos = i;
} }
@@ -711,7 +701,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if (!this._windowIsClosing) { if (!this._windowIsClosing) {
// update tab close buttons state // update tab close buttons state
this.tabContainer._updateCloseButtons(); this.tabContainer._updateCloseButtons();
@@ -6201,6 +6383,7 @@ @@ -6201,6 +6380,7 @@
memory_after: await getTotalMemoryUsage(), memory_after: await getTotalMemoryUsage(),
time_to_unload_in_ms: timeElapsed, time_to_unload_in_ms: timeElapsed,
}); });
@@ -719,7 +709,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
} }
/** /**
@@ -6246,6 +6429,7 @@ @@ -6246,6 +6426,7 @@
} }
let excludeTabs = new Set(aExcludeTabs); let excludeTabs = new Set(aExcludeTabs);
@@ -727,7 +717,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
// If this tab has a successor, it should be selectable, since // If this tab has a successor, it should be selectable, since
// hiding or closing a tab removes that tab as a successor. // hiding or closing a tab removes that tab as a successor.
@@ -6258,15 +6442,22 @@ @@ -6258,15 +6439,22 @@
!excludeTabs.has(aTab.owner) && !excludeTabs.has(aTab.owner) &&
Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose") Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")
) { ) {
@@ -752,7 +742,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
let tab = this.tabContainer.findNextTab(aTab, { let tab = this.tabContainer.findNextTab(aTab, {
direction: 1, direction: 1,
filter: _tab => remainingTabs.includes(_tab), filter: _tab => remainingTabs.includes(_tab),
@@ -6280,7 +6471,7 @@ @@ -6280,7 +6468,7 @@
} }
if (tab) { if (tab) {
@@ -761,7 +751,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
} }
// If no qualifying visible tab was found, see if there is a tab in // If no qualifying visible tab was found, see if there is a tab in
@@ -6301,7 +6492,7 @@ @@ -6301,7 +6489,7 @@
}); });
} }
@@ -770,7 +760,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
} }
_blurTab(aTab) { _blurTab(aTab) {
@@ -6312,7 +6503,7 @@ @@ -6312,7 +6500,7 @@
* @returns {boolean} * @returns {boolean}
* False if swapping isn't permitted, true otherwise. * False if swapping isn't permitted, true otherwise.
*/ */
@@ -779,7 +769,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
// Do not allow transfering a private tab to a non-private window // Do not allow transfering a private tab to a non-private window
// and vice versa. // and vice versa.
if ( if (
@@ -6366,6 +6557,7 @@ @@ -6366,6 +6554,7 @@
// fire the beforeunload event in the process. Close the other // fire the beforeunload event in the process. Close the other
// window if this was its last tab. // window if this was its last tab.
if ( if (
@@ -787,7 +777,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
!remoteBrowser._beginRemoveTab(aOtherTab, { !remoteBrowser._beginRemoveTab(aOtherTab, {
adoptedByTab: aOurTab, adoptedByTab: aOurTab,
closeWindowWithLastTab: true, closeWindowWithLastTab: true,
@@ -6377,7 +6569,7 @@ @@ -6377,7 +6566,7 @@
// If this is the last tab of the window, hide the window // If this is the last tab of the window, hide the window
// immediately without animation before the docshell swap, to avoid // immediately without animation before the docshell swap, to avoid
// about:blank being painted. // about:blank being painted.
@@ -796,7 +786,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if (closeWindow) { if (closeWindow) {
let win = aOtherTab.ownerGlobal; let win = aOtherTab.ownerGlobal;
win.windowUtils.suppressAnimation(true); win.windowUtils.suppressAnimation(true);
@@ -6511,11 +6703,13 @@ @@ -6511,11 +6700,13 @@
} }
// Finish tearing down the tab that's going away. // Finish tearing down the tab that's going away.
@@ -810,7 +800,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
this.setTabTitle(aOurTab); this.setTabTitle(aOurTab);
@@ -6717,10 +6911,10 @@ @@ -6717,10 +6908,10 @@
SessionStore.deleteCustomTabValue(aTab, "hiddenBy"); SessionStore.deleteCustomTabValue(aTab, "hiddenBy");
} }
@@ -823,7 +813,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
aTab.selected || aTab.selected ||
aTab.closing || aTab.closing ||
// Tabs that are sharing the screen, microphone or camera cannot be hidden. // Tabs that are sharing the screen, microphone or camera cannot be hidden.
@@ -6780,7 +6974,8 @@ @@ -6780,7 +6971,8 @@
* @param {object} [aOptions={}] * @param {object} [aOptions={}]
* Key-value pairs that will be serialized into the features string. * Key-value pairs that will be serialized into the features string.
*/ */
@@ -833,7 +823,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if (this.tabs.length == 1) { if (this.tabs.length == 1) {
return null; return null;
} }
@@ -6797,7 +6992,7 @@ @@ -6797,7 +6989,7 @@
// tell a new window to take the "dropped" tab // tell a new window to take the "dropped" tab
let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
args.appendElement(aTab.splitview ?? aTab); args.appendElement(aTab.splitview ?? aTab);
@@ -842,7 +832,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
private: PrivateBrowsingUtils.isWindowPrivate(window), private: PrivateBrowsingUtils.isWindowPrivate(window),
features: Object.entries(aOptions) features: Object.entries(aOptions)
.map(([key, value]) => `${key}=${value}`) .map(([key, value]) => `${key}=${value}`)
@@ -6805,6 +7000,8 @@ @@ -6805,6 +6997,8 @@
openerWindow: window, openerWindow: window,
args, args,
}); });
@@ -851,7 +841,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
} }
/** /**
@@ -6917,7 +7114,7 @@ @@ -6917,7 +7111,7 @@
* `true` if element is a `<tab-group>` * `true` if element is a `<tab-group>`
*/ */
isTabGroup(element) { isTabGroup(element) {
@@ -860,7 +850,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
} }
/** /**
@@ -7002,8 +7199,8 @@ @@ -7002,8 +7196,8 @@
} }
// Don't allow mixing pinned and unpinned tabs. // Don't allow mixing pinned and unpinned tabs.
@@ -871,7 +861,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
} else { } else {
tabIndex = Math.max(tabIndex, this.pinnedTabCount); tabIndex = Math.max(tabIndex, this.pinnedTabCount);
} }
@@ -7049,8 +7246,8 @@ @@ -7049,8 +7243,8 @@
this.#handleTabMove( this.#handleTabMove(
element, element,
() => { () => {
@@ -882,7 +872,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
neighbor = neighbor.group; neighbor = neighbor.group;
} }
if (neighbor?.splitview) { if (neighbor?.splitview) {
@@ -7061,6 +7258,12 @@ @@ -7061,6 +7255,12 @@
return; return;
} }
} }
@@ -895,7 +885,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if (movingForwards && neighbor) { if (movingForwards && neighbor) {
neighbor.after(element); neighbor.after(element);
@@ -7119,23 +7322,31 @@ @@ -7119,23 +7319,31 @@
#moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) { #moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) {
if (this.isTabGroupLabel(targetElement)) { if (this.isTabGroupLabel(targetElement)) {
targetElement = targetElement.group; targetElement = targetElement.group;
@@ -933,7 +923,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
} else if (!element.pinned && targetElement && targetElement.pinned) { } else if (!element.pinned && targetElement && targetElement.pinned) {
// If the caller asks to move an unpinned element next to a pinned // If the caller asks to move an unpinned element next to a pinned
// tab, move the unpinned element to be the first unpinned element // tab, move the unpinned element to be the first unpinned element
@@ -7148,12 +7359,35 @@ @@ -7148,12 +7356,35 @@
// move the tab group right before the first unpinned tab. // move the tab group right before the first unpinned tab.
// 4. Moving a tab group and the first unpinned tab is grouped: // 4. Moving a tab group and the first unpinned tab is grouped:
// move the tab group right before the first unpinned tab's tab group. // move the tab group right before the first unpinned tab's tab group.
@@ -970,7 +960,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
// We want to include the splitview wrapper if it's the targetElement, but // We want to include the splitview wrapper if it's the targetElement, but
// not in the case where we want to reverse tabs within the same splitview. // not in the case where we want to reverse tabs within the same splitview.
@@ -7162,6 +7396,7 @@ @@ -7162,6 +7393,7 @@
} }
let getContainer = () => let getContainer = () =>
@@ -978,7 +968,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
element.pinned element.pinned
? this.tabContainer.pinnedTabsContainer ? this.tabContainer.pinnedTabsContainer
: this.tabContainer; : this.tabContainer;
@@ -7170,11 +7405,15 @@ @@ -7170,11 +7402,15 @@
element, element,
() => { () => {
if (moveBefore) { if (moveBefore) {
@@ -995,7 +985,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
} }
}, },
metricsContext metricsContext
@@ -7248,11 +7487,15 @@ @@ -7248,11 +7484,15 @@
* @param {TabMetricsContext} [metricsContext] * @param {TabMetricsContext} [metricsContext]
*/ */
moveTabToExistingGroup(aTab, aGroup, metricsContext) { moveTabToExistingGroup(aTab, aGroup, metricsContext) {
@@ -1014,7 +1004,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
} }
if (aTab.group && aTab.group.id === aGroup.id) { if (aTab.group && aTab.group.id === aGroup.id) {
return; return;
@@ -7324,6 +7567,7 @@ @@ -7324,6 +7564,7 @@
let state = { let state = {
tabIndex: tab._tPos, tabIndex: tab._tPos,
@@ -1022,7 +1012,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
}; };
if (tab.visible) { if (tab.visible) {
state.elementIndex = tab.elementIndex; state.elementIndex = tab.elementIndex;
@@ -7355,7 +7599,7 @@ @@ -7355,7 +7596,7 @@
let changedSplitView = let changedSplitView =
previousTabState.splitViewId != currentTabState.splitViewId; previousTabState.splitViewId != currentTabState.splitViewId;
@@ -1031,7 +1021,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
tab.dispatchEvent( tab.dispatchEvent(
new CustomEvent("TabMove", { new CustomEvent("TabMove", {
bubbles: true, bubbles: true,
@@ -7402,6 +7646,10 @@ @@ -7402,6 +7643,10 @@
moveActionCallback(); moveActionCallback();
@@ -1042,7 +1032,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
// Clear tabs cache after moving nodes because the order of tabs may have // Clear tabs cache after moving nodes because the order of tabs may have
// changed. // changed.
this.tabContainer._invalidateCachedTabs(); this.tabContainer._invalidateCachedTabs();
@@ -7452,7 +7700,22 @@ @@ -7452,7 +7697,22 @@
* @returns {object} * @returns {object}
* The new tab in the current window, null if the tab couldn't be adopted. * The new tab in the current window, null if the tab couldn't be adopted.
*/ */
@@ -1066,7 +1056,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
// Swap the dropped tab with a new one we create and then close // Swap the dropped tab with a new one we create and then close
// it in the other window (making it seem to have moved between // it in the other window (making it seem to have moved between
// windows). We also ensure that the tab we create to swap into has // windows). We also ensure that the tab we create to swap into has
@@ -7495,6 +7758,8 @@ @@ -7495,6 +7755,8 @@
} }
params.skipLoad = true; params.skipLoad = true;
let newTab = this.addWebTab("about:blank", params); let newTab = this.addWebTab("about:blank", params);
@@ -1075,7 +1065,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
aTab.container.tabDragAndDrop.finishAnimateTabMove(); aTab.container.tabDragAndDrop.finishAnimateTabMove();
@@ -8205,7 +8470,7 @@ @@ -8205,7 +8467,7 @@
// preventDefault(). It will still raise the window if appropriate. // preventDefault(). It will still raise the window if appropriate.
return; return;
} }
@@ -1084,7 +1074,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
window.focus(); window.focus();
aEvent.preventDefault(); aEvent.preventDefault();
} }
@@ -8222,7 +8487,6 @@ @@ -8222,7 +8484,6 @@
on_TabGroupCollapse(aEvent) { on_TabGroupCollapse(aEvent) {
aEvent.target.tabs.forEach(tab => { aEvent.target.tabs.forEach(tab => {
@@ -1092,7 +1082,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
}); });
} }
@@ -8556,7 +8820,9 @@ @@ -8556,7 +8817,9 @@
let filter = this._tabFilters.get(tab); let filter = this._tabFilters.get(tab);
if (filter) { if (filter) {
@@ -1102,7 +1092,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
let listener = this._tabListeners.get(tab); let listener = this._tabListeners.get(tab);
if (listener) { if (listener) {
@@ -9359,6 +9625,7 @@ @@ -9359,6 +9622,7 @@
aWebProgress.isTopLevel aWebProgress.isTopLevel
) { ) {
this.mTab.setAttribute("busy", "true"); this.mTab.setAttribute("busy", "true");
@@ -1110,7 +1100,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
gBrowser._tabAttrModified(this.mTab, ["busy"]); gBrowser._tabAttrModified(this.mTab, ["busy"]);
this.mTab._notselectedsinceload = !this.mTab.selected; this.mTab._notselectedsinceload = !this.mTab.selected;
} }
@@ -9439,6 +9706,7 @@ @@ -9439,6 +9703,7 @@
// known defaults. Note we use the original URL since about:newtab // known defaults. Note we use the original URL since about:newtab
// redirects to a prerendered page. // redirects to a prerendered page.
const shouldRemoveFavicon = const shouldRemoveFavicon =
@@ -1118,7 +1108,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
!this.mBrowser.mIconURL && !this.mBrowser.mIconURL &&
!ignoreBlank && !ignoreBlank &&
!(originalLocation.spec in FAVICON_DEFAULTS); !(originalLocation.spec in FAVICON_DEFAULTS);
@@ -9613,13 +9881,6 @@ @@ -9613,13 +9878,6 @@
this.mBrowser.originalURI = aRequest.originalURI; this.mBrowser.originalURI = aRequest.originalURI;
} }
@@ -1132,7 +1122,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
} }
let userContextId = this.mBrowser.getAttribute("usercontextid") || 0; let userContextId = this.mBrowser.getAttribute("usercontextid") || 0;
@@ -10507,7 +10768,8 @@ var TabContextMenu = { @@ -10507,7 +10765,8 @@ var TabContextMenu = {
); );
contextUnpinSelectedTabs.hidden = contextUnpinSelectedTabs.hidden =
!this.contextTab.pinned || !this.multiselected; !this.contextTab.pinned || !this.multiselected;

View File

@@ -0,0 +1,172 @@
diff --git a/gfx/webrender_bindings/WebRenderAPI.cpp b/gfx/webrender_bindings/WebRenderAPI.cpp
--- a/gfx/webrender_bindings/WebRenderAPI.cpp
+++ b/gfx/webrender_bindings/WebRenderAPI.cpp
@@ -298,11 +298,13 @@
panic_on_gl_error, picTileWidth, picTileHeight,
gfx::gfxVars::WebRenderRequiresHardwareDriver(),
StaticPrefs::gfx_webrender_low_quality_pinch_zoom_AtStartup(),
StaticPrefs::gfx_webrender_max_shared_surface_size_AtStartup(),
StaticPrefs::gfx_webrender_enable_subpixel_aa_AtStartup(),
- compositor->ShouldUseLayerCompositor())) {
+ compositor->ShouldUseLayerCompositor(),
+ StaticPrefs::
+ gfx_webrender_opaque_backdrop_fallback_AtStartup())) {
// wr_window_new puts a message into gfxCriticalNote if it returns
// false
MOZ_ASSERT(errorMessage);
error.AssignASCII(errorMessage);
wr_api_free_error_msg(errorMessage);
diff --git a/gfx/webrender_bindings/src/bindings.rs b/gfx/webrender_bindings/src/bindings.rs
--- a/gfx/webrender_bindings/src/bindings.rs
+++ b/gfx/webrender_bindings/src/bindings.rs
@@ -1998,10 +1998,11 @@
reject_software_rasterizer: bool,
low_quality_pinch_zoom: bool,
max_shared_surface_size: i32,
enable_subpixel_aa: bool,
use_layer_compositor: bool,
+ opaque_backdrop_fallback: bool,
) -> bool {
assert!(unsafe { is_in_render_thread() });
// Ensure the WR profiler callbacks are hooked up to the Gecko profiler.
set_profiler_hooks(Some(&PROFILER_HOOKS));
@@ -2164,10 +2165,11 @@
texture_cache_config,
reject_software_rasterizer,
low_quality_pinch_zoom,
max_shared_surface_size,
enable_dithering,
+ opaque_backdrop_fallback,
..Default::default()
};
let window_size = DeviceIntSize::new(window_width, window_height);
let notifier = Box::new(CppNotifier { window_id });
diff --git a/gfx/wr/webrender/src/device/gl.rs b/gfx/wr/webrender/src/device/gl.rs
--- a/gfx/wr/webrender/src/device/gl.rs
+++ b/gfx/wr/webrender/src/device/gl.rs
@@ -3982,10 +3982,14 @@
pub fn disable_color_write(&self) {
self.gl.color_mask(false, false, false, false);
}
+ pub fn set_color_mask(&self, r: bool, g: bool, b: bool, a: bool) {
+ self.gl.color_mask(r, g, b, a);
+ }
+
pub fn set_blend(&mut self, enable: bool) {
if enable {
self.gl.enable(gl::BLEND);
} else {
self.gl.disable(gl::BLEND);
diff --git a/gfx/wr/webrender/src/renderer/init.rs b/gfx/wr/webrender/src/renderer/init.rs
--- a/gfx/wr/webrender/src/renderer/init.rs
+++ b/gfx/wr/webrender/src/renderer/init.rs
@@ -204,10 +204,12 @@
pub low_quality_pinch_zoom: bool,
pub max_shared_surface_size: i32,
/// If true, open a debug socket to listen for remote debugger.
/// Relies on `debugger` cargo feature being enabled.
pub enable_debugger: bool,
+ /// See explanation of `gfx.webrender.opaque-backdrop-fallback`.
+ pub opaque_backdrop_fallback: bool,
/// Use the new quad primitive path for box-shadow blur rendering.
pub use_quad_box_shadow: bool,
}
@@ -277,10 +279,11 @@
enable_instancing: true,
reject_software_rasterizer: false,
low_quality_pinch_zoom: false,
max_shared_surface_size: 2048,
enable_debugger: true,
+ opaque_backdrop_fallback: false,
use_quad_box_shadow: true,
}
}
}
@@ -802,10 +805,11 @@
allocated_native_surfaces: FastHashSet::default(),
debug_overlay_state: DebugOverlayState::new(),
buffer_damage_tracker: BufferDamageTracker::default(),
max_primitive_instance_count,
enable_instancing: options.enable_instancing,
+ opaque_backdrop_fallback: options.opaque_backdrop_fallback,
consecutive_oom_frames: 0,
target_frame_publish_id: None,
pending_result_msg: None,
layer_compositor_frame_state_in_prev_frame: None,
external_composite_debug_items: Vec::new(),
diff --git a/gfx/wr/webrender/src/renderer/mod.rs b/gfx/wr/webrender/src/renderer/mod.rs
--- a/gfx/wr/webrender/src/renderer/mod.rs
+++ b/gfx/wr/webrender/src/renderer/mod.rs
@@ -867,10 +867,12 @@
buffer_damage_tracker: BufferDamageTracker,
max_primitive_instance_count: usize,
enable_instancing: bool,
+ opaque_backdrop_fallback: bool,
+
/// Count consecutive oom frames to detectif we are stuck unable to render
/// in a loop.
consecutive_oom_frames: u32,
/// update() defers processing of ResultMsg, if frame_publish_id of
@@ -2787,18 +2789,29 @@
let read_target = ReadTarget::from_texture(cache_texture);
// Should always be drawing to picture cache tiles or off-screen surface!
debug_assert!(!draw_target.is_default());
let device_to_framebuffer = Scale::new(1i32);
+ let dest_fb_rect = dest * device_to_framebuffer;
self.device.blit_render_target(
read_target,
src * device_to_framebuffer,
draw_target,
- dest * device_to_framebuffer,
+ dest_fb_rect,
TextureFilter::Linear,
);
+
+ if self.opaque_backdrop_fallback {
+ self.device.set_color_mask(false, false, false, true);
+ self.device.clear_target(
+ Some([0.0, 0.0, 0.0, 1.0]),
+ None,
+ Some(dest_fb_rect),
+ );
+ self.device.set_color_mask(true, true, true, true);
+ }
}
}
}
fn draw_picture_cache_target(
diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml
--- a/modules/libpref/init/StaticPrefList.yaml
+++ b/modules/libpref/init/StaticPrefList.yaml
@@ -8439,10 +8439,17 @@
#else
value: false
#endif
mirror: once
+# Make backdrop-filter treat its captured backdrop as if it had been
+# composited over an opaque-black background. (See bug 2036640)
+- name: gfx.webrender.opaque-backdrop-fallback
+ type: bool
+ value: true
+ mirror: once
+
# Disable wait of GPU execution completion
- name: gfx.webrender.wait-gpu-finished.disabled
type: bool
value: false
mirror: once

View File

@@ -0,0 +1,14 @@
diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
index 3e58a40d747fb3e05bd67518b41b6467725cfe42..995419b6390e8abe663f71f8fb26281eae56aa84 100644
--- a/widget/cocoa/nsCocoaWindow.mm
+++ b/widget/cocoa/nsCocoaWindow.mm
@@ -5329,6 +5329,9 @@ static unsigned int WindowMaskForBorderStyle(BorderStyle aBorderStyle) {
case nsIWidget::eDocumentWindowAnimation:
behavior = NSWindowAnimationBehaviorDocumentWindow;
break;
+ case nsIWidget::eZenUtilityWindowAnimation:
+ behavior = NSWindowAnimationBehaviorUtilityWindow;
+ break;
default:
MOZ_FALLTHROUGH_ASSERT("unexpected mAnimationType value");
case nsIWidget::eGenericWindowAnimation:

View File

@@ -0,0 +1,12 @@
diff --git a/widget/nsIWidget.h b/widget/nsIWidget.h
index 4a5d933d865585ea772bafd233f7d4ebce3ac02b..3eae20d579330a71196c83eeb20d8fb2cd9f5fae 100644
--- a/widget/nsIWidget.h
+++ b/widget/nsIWidget.h
@@ -1118,6 +1118,7 @@ class nsIWidget : public nsSupportsWeakReference {
virtual void SetSupportsNativeFullscreen(bool aSupportsNativeFullscreen) {}
enum WindowAnimationType {
+ eZenUtilityWindowAnimation,
eGenericWindowAnimation,
eDocumentWindowAnimation
};

View File

@@ -1,8 +1,17 @@
diff --git a/xpfe/appshell/AppWindow.cpp b/xpfe/appshell/AppWindow.cpp diff --git a/xpfe/appshell/AppWindow.cpp b/xpfe/appshell/AppWindow.cpp
index d980bca7b42bb8d81817756215067771d2793bfe..94992e9d96348e4c88e089e4cdc3234076efb656 100644 index d980bca7b42bb8d81817756215067771d2793bfe..06818048ce6752a8b55faa3144c934fadbc46a68 100644
--- a/xpfe/appshell/AppWindow.cpp --- a/xpfe/appshell/AppWindow.cpp
+++ b/xpfe/appshell/AppWindow.cpp +++ b/xpfe/appshell/AppWindow.cpp
@@ -1847,7 +1847,7 @@ nsresult AppWindow::MaybeSaveEarlyWindowPersistentValues( @@ -1564,6 +1564,8 @@ void AppWindow::SyncAttributesToWidget() {
windowElement->GetAttribute(u"macanimationtype"_ns, attr);
if (attr.EqualsLiteral("document")) {
mWindow->SetWindowAnimationType(nsIWidget::eDocumentWindowAnimation);
+ } else if (attr.EqualsLiteral("zen:utilities")) {
+ mWindow->SetWindowAnimationType(nsIWidget::eZenUtilityWindowAnimation);
}
// Check if the client size did change and if we want to restore it.
@@ -1847,7 +1849,7 @@ nsresult AppWindow::MaybeSaveEarlyWindowPersistentValues(
} }
} }

View File

@@ -59,18 +59,6 @@ export class nsZenBoostEditor {
this.loadBoost(domain); this.loadBoost(domain);
} }
/**
* Returns the ZenBoosts JSWindowActor child for the currently selected tab.
*
* @returns {ZenBoostsChild} zenBoostsChild Boost JSActor child
*/
get zenBoostsChild() {
const linkedBrowser = this.openerWindow.gBrowser.selectedTab.linkedBrowser;
const actor =
linkedBrowser.browsingContext.currentWindowGlobal.getActor("ZenBoosts");
return actor;
}
/** /**
* Initializes the boost editor by setting up event listeners for all UI controls. * Initializes the boost editor by setting up event listeners for all UI controls.
*/ */
@@ -230,7 +218,7 @@ export class nsZenBoostEditor {
const editor = new Editor({ const editor = new Editor({
mode: Editor.modes.css, mode: Editor.modes.css,
lineNumbers: true, lineNumbers: true,
theme: "mozilla", theme: this.isDarkMode ? "mozilla" : "default",
readOnly: false, readOnly: false,
gutters: ["CodeMirror-linenumbers"], gutters: ["CodeMirror-linenumbers"],
}); });
@@ -239,9 +227,6 @@ export class nsZenBoostEditor {
editor.refresh(); editor.refresh();
editor.on("change", this.onCodeEditorChange.bind(this)); editor.on("change", this.onCodeEditorChange.bind(this));
const editorEl =
container.querySelector("iframe").contentDocument.documentElement;
editorEl.className = "theme-" + (this.isDarkMode ? "dark" : "light");
this.editorWindow._editor = editor; this.editorWindow._editor = editor;
this.codeEditorReady = true; this.codeEditorReady = true;
} }
@@ -444,13 +429,20 @@ export class nsZenBoostEditor {
} }
async onZapButtonPressed() { async onZapButtonPressed() {
this.zenBoostsChild.sendQuery("ZenBoost:ToggleZapMode"); const linkedBrowser = this.openerWindow.gBrowser.selectedTab.linkedBrowser;
const actor =
linkedBrowser.browsingContext.currentWindowGlobal.getActor("ZenBoosts");
actor.sendQuery("ZenBoost:ToggleZapMode");
// Focus the parent browser window // Focus the parent browser window
this.openerWindow.focus(); this.openerWindow.focus();
} }
async onPickerButtonPressed() { async onPickerButtonPressed() {
this.zenBoostsChild.sendQuery("ZenBoost:TogglePickerMode"); const linkedBrowser = this.openerWindow.gBrowser.selectedTab.linkedBrowser;
const actor =
linkedBrowser.browsingContext.currentWindowGlobal.getActor("ZenBoosts");
actor.sendQuery("ZenBoost:TogglePickerMode");
this.openerWindow.focus(); this.openerWindow.focus();
} }
@@ -475,11 +467,16 @@ ${cssSelector} {
} }
onInspectorButtonPressed() { onInspectorButtonPressed() {
this.zenBoostsChild.sendQuery("ZenBoost:OpenInspector"); const linkedBrowser = this.openerWindow.gBrowser.selectedTab.linkedBrowser;
const actor =
linkedBrowser.browsingContext.currentWindowGlobal.getActor("ZenBoosts");
actor.sendQuery("ZenBoost:OpenInspector");
} }
async onUpdateZapButtonVisual() { async onUpdateZapButtonVisual() {
const actor = this.zenBoostsChild; const linkedBrowser = this.openerWindow.gBrowser.selectedTab.linkedBrowser;
const actor =
linkedBrowser.browsingContext.currentWindowGlobal.getActor("ZenBoosts");
const zapButton = this.doc.getElementById("zen-boost-zap"); const zapButton = this.doc.getElementById("zen-boost-zap");
const zapEnabled = await actor.sendQuery("ZenBoost:ZapModeEnabled"); const zapEnabled = await actor.sendQuery("ZenBoost:ZapModeEnabled");
@@ -490,8 +487,12 @@ ${cssSelector} {
} }
async onUpdatePickerButtonVisual() { async onUpdatePickerButtonVisual() {
const linkedBrowser = this.openerWindow.gBrowser.selectedTab.linkedBrowser;
const actor =
linkedBrowser.browsingContext.currentWindowGlobal.getActor("ZenBoosts");
const pickerButton = this.doc.getElementById("zen-boost-css-picker"); const pickerButton = this.doc.getElementById("zen-boost-css-picker");
const selectEnabled = await this.zenBoostsChild.sendQuery( const selectEnabled = await actor.sendQuery(
"ZenBoost:SelectorPickerModeEnabled" "ZenBoost:SelectorPickerModeEnabled"
); );
@@ -630,7 +631,6 @@ ${cssSelector} {
this.currentBoostData.textCaseOverride = "uppercase"; this.currentBoostData.textCaseOverride = "uppercase";
} }
this.currentBoostData.changeWasMade = true;
this.updateCaseButtonVisuals(); this.updateCaseButtonVisuals();
this.updateCurrentBoost(); this.updateCurrentBoost();
} }
@@ -638,7 +638,7 @@ ${cssSelector} {
/** /**
* Handles the size toggle button press, cycling through size override options * Handles the size toggle button press, cycling through size override options
*/ */
async onBoostSizePressed() { onBoostSizePressed() {
if (this.currentBoostData.sizeOverride == 1) { if (this.currentBoostData.sizeOverride == 1) {
this.currentBoostData.sizeOverride = 1.1; this.currentBoostData.sizeOverride = 1.1;
} else if (this.currentBoostData.sizeOverride == 1.1) { } else if (this.currentBoostData.sizeOverride == 1.1) {
@@ -649,10 +649,8 @@ ${cssSelector} {
this.currentBoostData.sizeOverride = 0.9; this.currentBoostData.sizeOverride = 0.9;
} else if (this.currentBoostData.sizeOverride == 0.9) { } else if (this.currentBoostData.sizeOverride == 0.9) {
this.currentBoostData.sizeOverride = 1; this.currentBoostData.sizeOverride = 1;
await this.zenBoostsChild.sendQuery("ZenBoost:DisableSizeOverride");
} }
this.currentBoostData.changeWasMade = true;
this.updateSizeButtonVisuals(); this.updateSizeButtonVisuals();
this.updateCurrentBoost(); this.updateCurrentBoost();
} }

View File

@@ -640,21 +640,10 @@ class nsZenBoostsManager {
} }
} }
const editor = Services.ww.openWindow( const editor = parentWindow.openDialog(
parentWindow,
"chrome://browser/content/zen-components/windows/zen-boost-editor.xhtml", "chrome://browser/content/zen-components/windows/zen-boost-editor.xhtml",
null, "",
`left=${left},top=${top},chrome,alwaysontop,resizable=no,minimizable=no,dependent,dialog=yes`, `left=${left},top=${top},chrome,alwaysontop,resizable=no,minimizable=no,dependent,dialog=yes`
null
);
// Close the editor if the tab is switched
parentWindow.gBrowser.tabContainer.addEventListener(
"TabSelect",
editor.close.bind(editor),
{
once: true,
}
); );
const progressListener = { const progressListener = {
@@ -671,7 +660,6 @@ class nsZenBoostsManager {
// Give the domain // Give the domain
editor.domain = domain; editor.domain = domain;
editor.openerWindow = parentWindow; editor.openerWindow = parentWindow;
editor.focus();
// Make boost active // Make boost active
this.makeBoostActiveForDomain(domain, boost.id); this.makeBoostActiveForDomain(domain, boost.id);

View File

@@ -280,9 +280,6 @@ export class ZenBoostsChild extends JSWindowActorChild {
case "ZenBoost:OpenInspector": case "ZenBoost:OpenInspector":
this.sendAsyncMessage("ZenBoost:OpenInspector"); this.sendAsyncMessage("ZenBoost:OpenInspector");
break; break;
case "ZenBoost:DisableSizeOverride":
this.disableSizeOverride();
break;
} }
return null; return null;
} }
@@ -552,14 +549,6 @@ export class ZenBoostsChild extends JSWindowActorChild {
this.sendNotify("selector-picker-state-update", "ondisable"); this.sendNotify("selector-picker-state-update", "ondisable");
} }
disableSizeOverride() {
const browsingContext = this.browsingContext;
if (!browsingContext || browsingContext.parent !== null) {
return;
}
browsingContext.fullZoom = 1;
}
sendNotify(topic, msg = null) { sendNotify(topic, msg = null) {
this.sendAsyncMessage("ZenBoost:Notify", { topic, msg }); this.sendAsyncMessage("ZenBoost:Notify", { topic, msg });
} }

View File

@@ -18,7 +18,7 @@
customtitlebar="true" customtitlebar="true"
sizemode="normal" sizemode="normal"
scrolling="false" scrolling="false"
macanimationtype="document" macanimationtype="zen:utilities"
windowsmica="true" windowsmica="true"
data-l10n-sync="true"> data-l10n-sync="true">
<head> <head>

View File

@@ -100,16 +100,7 @@ class ZenStartup {
delete this.promiseInitializedResolve; delete this.promiseInitializedResolve;
setTimeout(() => { setTimeout(() => {
// Wait for the natural PlacesToolbar rebuild before invalidating, so gZenWorkspaces._invalidateBookmarkContainers();
// the two async rebuilds don't interleave and duplicate bookmarks.
// promiseRebuilt() returns undefined when no rebuild is in flight.
const rebuilt =
document
.getElementById("PlacesToolbar")
?._placesView?.promiseRebuilt() ?? Promise.resolve();
rebuilt
.catch(console.error)
.then(() => gZenWorkspaces._invalidateBookmarkContainers());
}); });
}); });
} }
@@ -156,7 +147,7 @@ class ZenStartup {
} }
#initUIComponents() { #initUIComponents() {
const kUIComponents = ["ZenProgressBar", "ZenSpaceRoutingNavigation"]; const kUIComponents = ["ZenProgressBar"];
for (let component of kUIComponents) { for (let component of kUIComponents) {
const module = ChromeUtils.importESModule( const module = ChromeUtils.importESModule(
"resource:///modules/zen/ui/" + component + ".sys.mjs" "resource:///modules/zen/ui/" + component + ".sys.mjs"

View File

@@ -10,6 +10,5 @@ EXTRA_JS_MODULES += [
EXTRA_JS_MODULES.zen.ui += [ EXTRA_JS_MODULES.zen.ui += [
"sys/ui/ZenProgressBar.sys.mjs", "sys/ui/ZenProgressBar.sys.mjs",
"sys/ui/ZenSpaceRoutingNavigation.sys.mjs",
"sys/ui/ZenUIComponent.sys.mjs", "sys/ui/ZenUIComponent.sys.mjs",
] ]

View File

@@ -1,123 +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 { ZenUIComponent } from "resource:///modules/zen/ui/ZenUIComponent.sys.mjs";
/**
* Per-window listener that re-routes in-place navigations for Space Routing.
*
* When any top-level navigation (link click, address bar, JS redirect, form
* submit, ...) targets a URL whose rule points at a *different* space than the
* one the tab currently lives in, the load is cancelled and re-opened in a new
* tab. The new tab flows through tabbrowser's addTab() routing, which moves it
* into the matching space.
*/
export class ZenSpaceRoutingNavigation extends ZenUIComponent {
init() {
this.listenBrowserTabsProgress();
}
/**
* @param {MozBrowser} aBrowser - The browser the state change happened in
* @param {nsIWebProgress} aWebProgress - The web progress
* @param {nsIRequest} aRequest - The request driving the state change
* @param {number} aStateFlags - The nsIWebProgressListener state flags
*/
onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags) {
const wpl = Ci.nsIWebProgressListener;
if (
!aWebProgress?.isTopLevel ||
!(aStateFlags & wpl.STATE_START) ||
!(aStateFlags & wpl.STATE_IS_DOCUMENT) ||
aStateFlags & wpl.STATE_RESTORING
) {
return;
}
// The tab we spawn for a route must be allowed to load once without being
// redirected again, regardless of when its workspace attribute lands.
if (aBrowser._zenSkipNavRouteOnce) {
aBrowser._zenSkipNavRouteOnce = false;
return;
}
let uri;
try {
uri = aRequest.QueryInterface(Ci.nsIChannel).URI;
} catch (e) {
return;
}
if (!uri || !(uri.schemeIs("http") || uri.schemeIs("https"))) {
return;
}
// Don't disturb a tab that is merely (re)loading the page it already shows:
// a reload, a session restore, or a tab that was already sitting on this URL
// before the rule was set. At STATE_START the browser's currentURI still
// points at the existing document, so an equal target means this isn't a
// new navigation worth routing.
let currentURI = null;
try {
currentURI = aBrowser.currentURI;
} catch (e) {
currentURI = null;
}
if (currentURI?.equals(uri)) {
return;
}
const win = this.window;
const gBrowser = win.gBrowser;
const tab = gBrowser.getTabForBrowser(aBrowser);
if (
!tab ||
tab.pinned ||
tab.hasAttribute("zen-empty-tab") ||
tab.hasAttribute("zen-glance-tab")
) {
return;
}
const currentWorkspaceId = tab.getAttribute("zen-workspace-id");
if (
!win.gZenSpaceRoutingManager.shouldRedirectNavigation(
uri.spec,
currentWorkspaceId,
win
)
) {
return;
}
// Under Fission the parent-side aRequest is a RemoteWebProgress stand-in
// whose cancel()/loadInfo throw NS_ERROR_NOT_IMPLEMENTED (the real channel
// lives in the content process). Stop the in-place load through the browser,
// which proxies the request to the content process.
try {
aBrowser.stop();
} catch (e) {
return;
}
const urlToOpen = uri.spec;
// loadInfo isn't reachable on the remote request, so use the navigating
// page as the triggering principal (correct for link clicks), with a null
// principal as the safe last resort.
const principal =
aBrowser.contentPrincipal ||
Services.scriptSecurityManager.createNullPrincipal({});
// Defer so we don't mutate the tab strip from inside a progress notification.
win.setTimeout(() => {
const newTab = gBrowser.addTab(urlToOpen, {
triggeringPrincipal: principal,
ownerTab: tab.isConnected ? tab : null,
});
if (newTab?.linkedBrowser) {
newTab.linkedBrowser._zenSkipNavRouteOnce = true;
}
}, 0);
}
}

View File

@@ -82,40 +82,6 @@ class nsZenSpaceRoutingManager {
this.#routeToWorkspace(targetRoute, newTab, win); this.#routeToWorkspace(targetRoute, newTab, win);
} }
/**
* Decides whether an in-place top-level navigation should be pulled out of
* the current tab and re-opened in a new tab, so that addTab()'s routing can
* move it into the space its rule points at.
*
* Only navigations whose rule targets a *different* space than the one the
* navigating tab already lives in are redirected. Staying put when the tab is
* already in the destination space keeps normal browsing in place and also
* prevents the freshly routed tab from being redirected again (infinite loop).
*
* @param {string} uriString - The destination URI
* @param {string|null} currentWorkspaceId - The zen-workspace-id of the navigating tab
* @param {Window} win - The owning browser window
* @returns {boolean} True when the navigation should open in a new routed tab
*/
shouldRedirectNavigation(uriString, currentWorkspaceId, win) {
if (!win?.gZenWorkspaces?.workspaceEnabled) {
return false;
}
const targetRoute = this.routeUri(uriString, { fromExternal: false });
// No specific destination, or the tab is already where the rule points.
if (
targetRoute === "most-recent-space" ||
targetRoute === currentWorkspaceId
) {
return false;
}
// Only redirect when the destination space actually exists.
return !!win.gZenWorkspaces.getWorkspaceFromId(targetRoute);
}
/** /**
* Checks if the tab should be processed or not * Checks if the tab should be processed or not
* *
@@ -389,14 +355,9 @@ class nsZenSpaceRoutingManager {
} }
/** /**
* Saves all routes. The list of * Saves all routes
* routes is stripped of empty routes
* before being saved
*/ */
saveRoutes() { saveRoutes() {
this.#file.data.routes = this.#file.data.routes.filter(
route => route.reference.trim() !== ""
);
this.#writeToDisk(); this.#writeToDisk();
} }

View File

@@ -27,6 +27,11 @@
--sr-border-radius: 12px; --sr-border-radius: 12px;
} }
.dialog-button-box {
/* Remove default dialog buttons */
display: none !important;
}
window { window {
height: var(--sr-height); height: var(--sr-height);
max-height: var(--sr-height); max-height: var(--sr-height);
@@ -118,6 +123,10 @@ p {
min-height: unset; min-height: unset;
height: 26px; height: 26px;
padding: 4px; padding: 4px;
}
.select,
.select * {
background-color: var(--select-background-color) !important; background-color: var(--select-background-color) !important;
color: var(--text-color); color: var(--text-color);
margin: 0; margin: 0;
@@ -130,6 +139,7 @@ p {
} }
} }
menulist[image] .menulist-icon,
menulist[image]::part(icon) { menulist[image]::part(icon) {
width: 16px; width: 16px;
height: 16px; height: 16px;
@@ -141,6 +151,10 @@ menulist[image]::part(icon) {
padding-left: 2px; padding-left: 2px;
} }
menulist {
-moz-box-align: center;
}
.sr-rule-row { .sr-rule-row {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -162,7 +176,8 @@ menulist[image]::part(icon) {
margin-left: 87px; margin-left: 87px;
} }
.input { .input,
.input * {
background-color: var(--input-background-color); background-color: var(--input-background-color);
color: var(--text-color); color: var(--text-color);
margin: 0; margin: 0;
@@ -170,7 +185,8 @@ menulist[image]::part(icon) {
flex-grow: 1; flex-grow: 1;
} }
.invalid { .invalid,
.invalid * {
color: var(--text-color-error) !important; color: var(--text-color-error) !important;
text-decoration-line: underline; text-decoration-line: underline;

View File

@@ -1494,9 +1494,12 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
return color; return color;
} }
getToolbarColor(isDarkMode = false) { getToolbarColor(isDarkMode = false, accentColor = null) {
const opacity = 0.8; const opacity = 0.8;
let baseColor = isDarkMode ? [255, 255, 255, opacity] : [0, 0, 0, opacity]; // Default toolbar let baseColor = isDarkMode ? [255, 255, 255, opacity] : [0, 0, 0, opacity]; // Default toolbar
if (accentColor) {
return this.blendColors(baseColor.slice(0, 3), accentColor, 75).concat(1);
}
return baseColor; return baseColor;
} }
@@ -1762,7 +1765,7 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
docElement.style.setProperty("--zen-primary-color", primaryColor); docElement.style.setProperty("--zen-primary-color", primaryColor);
// Set `--toolbox-textcolor` to have a contrast with the primary color // Set `--toolbox-textcolor` to have a contrast with the primary color
let textColor = this.getToolbarColor(isDarkMode); let textColor = this.getToolbarColor(isDarkMode, dominantColor);
docElement.style.setProperty( docElement.style.setProperty(
"--toolbox-textcolor", "--toolbox-textcolor",
`rgba(${textColor[0]}, ${textColor[1]}, ${textColor[2]}, ${textColor[3]})` `rgba(${textColor[0]}, ${textColor[1]}, ${textColor[2]}, ${textColor[3]})`
@@ -2008,7 +2011,7 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
grain: theme.texture ?? 0, grain: theme.texture ?? 0,
isDarkMode, isDarkMode,
isExplicitMode, isExplicitMode,
toolbarColor: this.getToolbarColor(isDarkMode), toolbarColor: this.getToolbarColor(isDarkMode, dominantColor),
primaryColor: this.getAccentColorForUI(dominantColor, isDarkMode), primaryColor: this.getAccentColorForUI(dominantColor, isDarkMode),
}; };
this.currentOpacity = previousOpacity; this.currentOpacity = previousOpacity;

View File

@@ -15,12 +15,12 @@ class nsZenWorkspaceIcons extends MozXULElement {
this.initDragAndDrop(); this.initDragAndDrop();
this.addEventListener("mouseover", e => { this.addEventListener("mouseover", e => {
if (e.shiftKey || this.isReorderMode) { if (this.isReorderMode) {
return; return;
} }
const target = e.target.closest("toolbarbutton[zen-workspace-id]"); const target = e.target.closest("toolbarbutton[zen-workspace-id]");
if (target) { if (target) {
target.scrollIntoView({ behavior: "smooth", inline: "nearest" }); this.scrollLeft = target.offsetLeft - 10;
} }
}); });
} }
@@ -178,7 +178,7 @@ class nsZenWorkspaceIcons extends MozXULElement {
return; return;
} }
buttons[selected].setAttribute("active", true); buttons[selected].setAttribute("active", true);
buttons[selected].scrollIntoView({ behavior: "smooth", inline: "nearest" }); this.scrollLeft = buttons[selected].offsetLeft - 10;
this.setAttribute("selected", selected); this.setAttribute("selected", selected);
} }

View File

@@ -1010,14 +1010,6 @@ class nsZenWorkspaces {
return null; return null;
} }
// Closing a glance tab tears down the overlay and restores selection
// to its parent tab. Don't run the last-tab handling here:
// in a pinned-only window the glance child is the only unpinned tab,
// so this would switch to an empty tab and clobber the restore-to-parent.
if (tab.hasAttribute("glance-id")) {
return null;
}
let workspaceID = tab.getAttribute("zen-workspace-id"); let workspaceID = tab.getAttribute("zen-workspace-id");
if (!workspaceID) { if (!workspaceID) {
return null; return null;
@@ -1506,6 +1498,7 @@ class nsZenWorkspaces {
continue; continue;
} }
tab.owner = null;
if (container) { if (container) {
if (tab.group?.hasAttribute("split-view-group")) { if (tab.group?.hasAttribute("split-view-group")) {
gBrowser.zenHandleTabMove(tab.group, () => { gBrowser.zenHandleTabMove(tab.group, () => {
@@ -2289,27 +2282,6 @@ class nsZenWorkspaces {
); );
} }
onBeforeTabSelect(aTab) {
if (this.#inChangingWorkspace) {
// Just in case, Let's not do these checks while we are
// in the middle of changing workspace,
return false;
}
const tabSpace = aTab?.getAttribute("zen-workspace-id");
if (
tabSpace &&
tabSpace !== this.activeWorkspace &&
!aTab.hasAttribute("zen-empty-tab") &&
!aTab.hasAttribute("zen-essential")
) {
this.lastSelectedWorkspaceTabs[tabSpace] =
gZenGlanceManager.getTabOrGlanceParent(aTab);
this.changeWorkspaceWithID(tabSpace);
return true;
}
return false;
}
_shouldShowTab(tab, workspaceUuid, containerId, workspaces) { _shouldShowTab(tab, workspaceUuid, containerId, workspaces) {
const isEssential = tab.getAttribute("zen-essential") === "true"; const isEssential = tab.getAttribute("zen-essential") === "true";
const tabWorkspaceId = tab.getAttribute("zen-workspace-id"); const tabWorkspaceId = tab.getAttribute("zen-workspace-id");
@@ -3014,8 +2986,7 @@ class nsZenWorkspaces {
if ( if (
triggeringPrincipal && triggeringPrincipal &&
triggeringPrincipal.isAddonOrExpandedAddonPrincipal && triggeringPrincipal.isAddonOrExpandedAddonPrincipal
typeof userContextId === "undefined"
) { ) {
return [userContextId, false, undefined]; return [userContextId, false, undefined];
} }

View File

@@ -20,6 +20,7 @@
display: none !important; display: none !important;
} }
border-radius: var(--button-border-radius) !important;
background: transparent; background: transparent;
appearance: unset !important; appearance: unset !important;
height: fit-content; height: fit-content;
@@ -40,7 +41,6 @@
fill-opacity: 0.6; fill-opacity: 0.6;
-moz-context-properties: fill-opacity, fill; -moz-context-properties: fill-opacity, fill;
fill: currentColor; fill: currentColor;
scroll-margin: 0 20px;
& .zen-workspace-icon { & .zen-workspace-icon {
pointer-events: none; pointer-events: none;
@@ -105,7 +105,7 @@
&[icons-overflow] { &[icons-overflow] {
gap: 0 !important; gap: 0 !important;
justify-content: safe center; justify-content: center;
& toolbarbutton { & toolbarbutton {
margin: 0; margin: 0;
@@ -321,11 +321,7 @@ zen-workspace {
position: absolute; position: absolute;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
color: color-mix(in srgb, var(--toolbox-textcolor) 95%, var(--zen-primary-color)); color: var(--toolbox-textcolor);
--tab-selected-bgcolor: color-mix(in srgb, light-dark(rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.18)) 95%, var(--zen-primary-color)) !important;
--tab-selected-shadow: 0 0.8px 1.5px 0px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.05)) !important;
--tab-selected-textcolor: color-mix(in srgb, var(--toolbox-textcolor) 95%, var(--zen-primary-color)) !important;
@media not (prefers-reduced-motion: reduce) { @media not (prefers-reduced-motion: reduce) {
transition: padding-top 0.1s; transition: padding-top 0.1s;

View File

@@ -303,6 +303,8 @@
border-bottom: 0 solid transparent !important; border-bottom: 0 solid transparent !important;
--tab-block-margin: 2px; --tab-block-margin: 2px;
--tab-selected-bgcolor: light-dark(rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.18));
--tab-selected-shadow: 0 0.8px 1.5px 0px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.05)) !important;
grid-gap: 0 !important; grid-gap: 0 !important;
&[overflow]::after, &[overflow]::after,

View File

@@ -20,5 +20,3 @@ support-files = [
["browser_glance_prev_tab.js"] ["browser_glance_prev_tab.js"]
["browser_glance_select_parent.js"] ["browser_glance_select_parent.js"]
["browser_issue_14049.js"]

View File

@@ -1,101 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Regression test for gh-14049: closing a Glance ("preview") opened from a
// pinned tab in a pinned-only window used to drop the user on a blank new tab
// instead of restoring the pinned parent. The glance child is the last unpinned
// tab, so removing it tripped `handleTabBeforeClose`'s "last unpinned tab
// closed" handling, which switched to an empty tab and clobbered the
// restore-to-parent.
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [["zen.workspaces.open-new-tab-if-last-unpinned-tab-is-closed", true]],
});
registerCleanupFunction(async () => {
await SpecialPowers.popPrefEnv();
});
});
add_task(async function test_Glance_Close_Pinned_Parent() {
if (!gZenWorkspaces.workspaceEnabled) {
ok(true, "Workspaces disabled; the regression cannot occur. Skipping.");
return;
}
// Recreate the "only pinned tabs open" state by pinning every existing tab.
// The glance child opened below is then guaranteed to be the sole unpinned
// tab, which is the precondition the regression depends on.
const pinnedByTest = gBrowser.visibleTabs.filter(t => !t.pinned);
for (const tab of pinnedByTest) {
gBrowser.pinTab(tab);
}
registerCleanupFunction(() => {
for (const tab of pinnedByTest) {
if (tab.pinned && !tab.closing) {
gBrowser.unpinTab(tab);
}
}
});
const parentTab = gBrowser.selectedTab;
ok(parentTab.pinned, "Parent tab should be pinned");
// selectEmptyTab() is a no-op while Zen's testing mode is enabled, so the
// regression cannot be observed through the resulting selection alone. Spy on
// it instead: the bug is "selectEmptyTab gets called when a glance is closed".
let selectEmptyTabCalled = false;
const originalSelectEmptyTab = gZenWorkspaces.selectEmptyTab;
gZenWorkspaces.selectEmptyTab = function (...args) {
selectEmptyTabCalled = true;
return originalSelectEmptyTab.apply(this, args);
};
registerCleanupFunction(() => {
gZenWorkspaces.selectEmptyTab = originalSelectEmptyTab;
});
await openGlanceOnTab(async glanceTab => {
ok(
glanceTab.hasAttribute("glance-id"),
"The glance tab should have a glance-id"
);
ok(!glanceTab.pinned, "The glance child should be unpinned");
// `handleTabBeforeClose` bails early without a workspace id, so make sure
// the glance child carries one (as it does at teardown time).
if (!glanceTab.getAttribute("zen-workspace-id")) {
glanceTab.setAttribute(
"zen-workspace-id",
gZenWorkspaces.activeWorkspace
);
}
Assert.deepEqual(
gBrowser.visibleTabs.filter(t => !t.pinned),
[glanceTab],
"The glance child should be the only unpinned visible tab"
);
// Close the glance through the real tab-removal flow, which is what runs
// handleTabBeforeClose and the glance teardown.
await BrowserTestUtils.removeTab(glanceTab);
}, false);
ok(
!selectEmptyTabCalled,
"Closing a glance tab must not switch to an empty tab"
);
await TestUtils.waitForCondition(
() => gBrowser.selectedTab === parentTab && parentTab.selected,
"The pinned parent tab should be selected after closing the glance"
);
Assert.equal(
gBrowser.selectedTab,
parentTab,
"The pinned parent tab should be selected after closing the glance"
);
ok(parentTab.selected, "The pinned parent tab should be visually selected");
});

View File

@@ -17,6 +17,16 @@ disable = [
source = "browser/components/safebrowsing/content/test" source = "browser/components/safebrowsing/content/test"
is_direct_path = true is_direct_path = true
[sandbox]
source = "security/sandbox/test"
is_direct_path = true
disable = [
"browser_bug1393259.js",
]
[sandbox.replace-manifest]
"../../../" = "../../../../"
[shell] [shell]
source = "browser/components/shell/test" source = "browser/components/shell/test"
is_direct_path = true is_direct_path = true

View File

@@ -10,6 +10,7 @@
BROWSER_CHROME_MANIFESTS += [ BROWSER_CHROME_MANIFESTS += [
"readermode/browser.toml", "readermode/browser.toml",
"safebrowsing/browser.toml", "safebrowsing/browser.toml",
"sandbox/browser.toml",
"shell/browser.toml", "shell/browser.toml",
"tabMediaIndicator/browser.toml", "tabMediaIndicator/browser.toml",
"tooltiptext/browser.toml", "tooltiptext/browser.toml",

View File

@@ -0,0 +1,44 @@
[DEFAULT]
skip-if = [
"ccov",
"os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && asan", # bug 1784517
"os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && tsan", # bug 1784517
]
tags = "contentsandbox"
support-files = [
"browser_content_sandbox_utils.js",
"browser_content_sandbox_fs_tests.js",
"mac_register_font.py",
"../../../../layout/reftests/fonts/fira/FiraSans-Regular.otf"
]
["browser_bug1393259.js"]
disabled="Disabled by import_external_tests.py"
support-files = ["bug1393259.html"]
run-if = [
"os == 'mac'", # This is a Mac-specific test
]
skip-if = [
"os == 'mac' && os_version == '14.70' && arch == 'x86_64'", # Bug 1929424
]
tags = "os_integration"
["browser_content_sandbox_fs.js"]
skip-if = [
"os == 'mac' && os_version == '15.30' && arch == 'aarch64'", # Bug 2023967
"os == 'win' && os_version == '11.26100' && arch == 'x86' && debug", # bug 1379635
"os == 'win' && os_version == '11.26100' && arch == 'x86_64' && debug", # bug 1379635
"os == 'win' && os_version == '11.26200' && arch == 'x86' && debug", # bug 1379635
"os == 'win' && os_version == '11.26200' && arch == 'x86_64' && debug", # bug 1379635
]
["browser_content_sandbox_syscalls.js"]
["browser_sandbox_test.js"]
skip-if = [
"os == 'win' && os_version == '11.26200' && arch == 'x86' && debug", # bug 2028636
"os == 'win' && os_version == '11.26200' && arch == 'x86_64' && debug", # bug 2028636
]
run-if = [
"debug",
]

View File

@@ -0,0 +1,203 @@
/* 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 strict";
/*
* This test validates that an OTF font installed in a directory not
* accessible to content processes is rendered correctly by checking that
* content displayed never uses the OS fallback font "LastResort". When
* a content process renders a page with the fallback font, that is an
* indication the content process failed to read or load the computed font.
* The test uses a version of the Fira Sans font and depends on the font
* not being already installed and enabled.
*/
const kPageURL =
"http://example.com/browser/security/sandbox/test/bug1393259.html";
// Parameters for running the python script that registers/unregisters fonts.
let kPythonPath = "/usr/bin/python";
if (AppConstants.isPlatformAndVersionAtLeast("macosx", 23.0)) {
kPythonPath = "/usr/local/bin/python3";
}
const kFontInstallerPath = "browser/security/sandbox/test/mac_register_font.py";
const kUninstallFlag = "-u";
const kVerboseFlag = "-v";
// Where to find the font in the test environment.
const kRepoFontPath = "browser/security/sandbox/test/FiraSans-Regular.otf";
// Font name strings to check for.
const kLastResortFontName = "LastResort";
const kTestFontName = "Fira Sans";
// Home-relative path to install a private font. Where a private font is
// a font at a location not readable by content processes.
const kPrivateFontSubPath = "/FiraSans-Regular.otf";
add_task(async function () {
await new Promise(resolve => waitForFocus(resolve, window));
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: kPageURL,
},
async function (aBrowser) {
function runProcess(aCmd, aArgs, blocking = true) {
let cmdFile = Cc["@mozilla.org/file/local;1"].createInstance(
Ci.nsIFile
);
cmdFile.initWithPath(aCmd);
let process = Cc["@mozilla.org/process/util;1"].createInstance(
Ci.nsIProcess
);
process.init(cmdFile);
process.run(blocking, aArgs, aArgs.length);
return process.exitValue;
}
// Register the font at path |fontPath| and wait
// for the browser to detect the change.
async function registerFont(fontPath) {
let fontRegistered = getFontNotificationPromise();
let exitCode = runProcess(kPythonPath, [
kFontInstallerPath,
kVerboseFlag,
fontPath,
]);
Assert.equal(exitCode, 0, "registering font" + fontPath);
if (exitCode == 0) {
// Wait for the font registration to be detected by the browser.
await fontRegistered;
}
}
// Unregister the font at path |fontPath|. If |waitForUnreg| is true,
// don't wait for the browser to detect the change and don't use
// the verbose arg for the unregister command.
async function unregisterFont(fontPath, waitForUnreg = true) {
let args = [kFontInstallerPath, kUninstallFlag];
let fontUnregistered;
if (waitForUnreg) {
args.push(kVerboseFlag);
fontUnregistered = getFontNotificationPromise();
}
let exitCode = runProcess(kPythonPath, args.concat(fontPath));
if (waitForUnreg) {
Assert.equal(exitCode, 0, "unregistering font" + fontPath);
if (exitCode == 0) {
await fontUnregistered;
}
}
}
// Returns a promise that resolves when font info is changed.
let getFontNotificationPromise = () =>
new Promise(resolve => {
const kTopic = "font-info-updated";
function observe() {
Services.obs.removeObserver(observe, kTopic);
resolve();
}
Services.obs.addObserver(observe, kTopic);
});
let homeDir = Services.dirsvc.get("Home", Ci.nsIFile);
let privateFontPath = homeDir.path + kPrivateFontSubPath;
registerCleanupFunction(function () {
unregisterFont(privateFontPath, /* waitForUnreg = */ false);
runProcess("/bin/rm", [privateFontPath], /* blocking = */ false);
});
// Copy the font file to the private path.
runProcess("/bin/cp", [kRepoFontPath, privateFontPath]);
// Cleanup previous aborted tests.
unregisterFont(privateFontPath, /* waitForUnreg = */ false);
// Get the original width, using the fallback monospaced font
let origWidth = await SpecialPowers.spawn(
aBrowser,
[],
async function () {
let window = content.window.wrappedJSObject;
let contentDiv = window.document.getElementById("content");
return contentDiv.offsetWidth;
}
);
// Activate the font we want to test at a non-standard path.
await registerFont(privateFontPath);
// Assign the new font to the content.
await SpecialPowers.spawn(aBrowser, [], async function () {
let window = content.window.wrappedJSObject;
let contentDiv = window.document.getElementById("content");
contentDiv.style.fontFamily = "'Fira Sans', monospace";
});
// Wait until the width has changed, indicating the content process
// has recognized the newly-activated font.
while (true) {
let width = await SpecialPowers.spawn(aBrowser, [], async function () {
let window = content.window.wrappedJSObject;
let contentDiv = window.document.getElementById("content");
return contentDiv.offsetWidth;
});
if (width != origWidth) {
break;
}
// If the content wasn't ready yet, wait a little before re-checking.
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(c => setTimeout(c, 100));
}
// Get a list of fonts now being used to display the web content.
let fontList = await SpecialPowers.spawn(aBrowser, [], async function () {
let window = content.window.wrappedJSObject;
let range = window.document.createRange();
let contentDiv = window.document.getElementById("content");
range.selectNode(contentDiv);
let fonts = InspectorUtils.getUsedFontFaces(range);
let fontList = [];
for (let i = 0; i < fonts.length; i++) {
fontList.push({ name: fonts[i].name });
}
return fontList;
});
let lastResortFontUsed = false;
let testFontUsed = false;
for (let font of fontList) {
// Did we fall back to the "LastResort" font?
if (!lastResortFontUsed && font.name.includes(kLastResortFontName)) {
lastResortFontUsed = true;
continue;
}
// Did we render using our test font as expected?
if (!testFontUsed && font.name.includes(kTestFontName)) {
testFontUsed = true;
continue;
}
}
Assert.ok(
!lastResortFontUsed,
`The ${kLastResortFontName} fallback font was not used`
);
Assert.ok(testFontUsed, `The test font "${kTestFontName}" was used`);
await unregisterFont(privateFontPath);
}
);
});

View File

@@ -0,0 +1,15 @@
# Any copyright is dedicated to the Public Domain.
# http://creativecommons.org/publicdomain/zero/1.0/
[DEFAULT]
skip-if = [
"ccov",
"os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && asan", # bug 1784517
"os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && tsan", # bug 1784517
]
tags = "contentsandbox"
environment = "XDG_CONFIG_DIRS=:/opt"
["browser_content_sandbox_bug1717599_XDG-CONFIG-DIRS.js"]
run-if = [
"os == 'linux'",
]

View File

@@ -0,0 +1,13 @@
[DEFAULT]
skip-if = [
"ccov",
"os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && asan", # bug 1784517
"os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && tsan", # bug 1784517
]
tags = "contentsandbox"
environment = "XDG_CONFIG_HOME="
["browser_content_sandbox_bug1717599_XDG-CONFIG-HOME.js"]
run-if = [
"os == 'linux'",
]

View File

@@ -0,0 +1,11 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* import-globals-from browser_content_sandbox_utils.js */
"use strict";
//
// Just test that browser does not die on empty env var
//
add_task(async function () {
ok(true, "Process can run");
});

View File

@@ -0,0 +1,11 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* import-globals-from browser_content_sandbox_utils.js */
"use strict";
//
// Just test that browser does not die on empty env var
//
add_task(async function () {
ok(true, "Process can run");
});

View File

@@ -0,0 +1,56 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* import-globals-from browser_content_sandbox_utils.js */
"use strict";
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/" +
"security/sandbox/test/browser_content_sandbox_utils.js",
this
);
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/" +
"security/sandbox/test/browser_content_sandbox_fs_tests.js",
this
);
/*
* This test exercises file I/O from web and file content processes using
* nsIFile etc. methods to validate that calls that are meant to be blocked by
* content sandboxing are blocked.
*/
//
// Checks that sandboxing is enabled and at the appropriate level
// setting before triggering tests that do the file I/O.
//
// Tests attempting to write to a file in the home directory from the
// content process--expected to fail.
//
// Tests attempting to write to a file in the content temp directory
// from the content process--expected to succeed. Uses "ContentTmpD".
//
// Tests reading various files and directories from file and web
// content processes.
//
add_task(async function () {
sanityChecks();
// Test creating a file in the home directory from a web content process
add_task(createFileInHome); // eslint-disable-line no-undef
// Test creating a file content temp from a web content process
add_task(createTempFile); // eslint-disable-line no-undef
// Test reading files/dirs from web and file content processes
add_task(testFileAccessAllPlatforms); // eslint-disable-line no-undef
add_task(testFileAccessMacOnly); // eslint-disable-line no-undef
add_task(testFileAccessLinuxOnly); // eslint-disable-line no-undef
add_task(testFileAccessWindowsOnly); // eslint-disable-line no-undef
add_task(cleanupBrowserTabs); // eslint-disable-line no-undef
});

View File

@@ -0,0 +1,31 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* import-globals-from browser_content_sandbox_utils.js */
"use strict";
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/" +
"security/sandbox/test/browser_content_sandbox_utils.js",
this
);
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/" +
"security/sandbox/test/browser_content_sandbox_fs_tests.js",
this
);
add_task(async function () {
// Ensure that SNAP is there
const snap = Services.env.get("SNAP");
Assert.greater(snap.length, 1, "SNAP is defined");
// If it is there, do actual testing
sanityChecks();
add_task(testFileAccessLinuxOnly); // eslint-disable-line no-undef
add_task(testFileAccessLinuxSnap); // eslint-disable-line no-undef
add_task(cleanupBrowserTabs); // eslint-disable-line no-undef
});

View File

@@ -0,0 +1,719 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* import-globals-from browser_content_sandbox_utils.js */
"use strict";
const lazy = {};
/* getLibcConstants is only present on *nix */
ChromeUtils.defineLazyGetter(lazy, "LIBC", () =>
ChromeUtils.getLibcConstants()
);
// Test if the content process can create in $HOME, this should fail
async function createFileInHome() {
let browser = gBrowser.selectedBrowser;
let homeFile = fileInHomeDir();
let path = homeFile.path;
let fileCreated = await SpecialPowers.spawn(browser, [path], createFile);
ok(!fileCreated.ok, "creating a file in home dir failed");
is(
fileCreated.code,
Cr.NS_ERROR_FILE_ACCESS_DENIED,
"creating a file in home dir failed with access denied"
);
if (fileCreated.ok) {
// content process successfully created the file, now remove it
homeFile.remove(false);
}
}
// Test if the content process can create a temp file, this is forbidden on all
// platforms. Also test that the content process cannot create symlinks on
// macOS/Linux or delete files.
async function createTempFile() {
// On Windows we allow access to the temp dir for DEBUG builds, because of
// logging that uses that dir.
let isDbgWin = isWin() && SpecialPowers.isDebugBuild;
let browser = gBrowser.selectedBrowser;
let path = fileInTempDir().path;
let fileCreated = await SpecialPowers.spawn(browser, [path], createFile);
if (isDbgWin) {
ok(fileCreated.ok, "creating a file in temp suceeded");
} else {
ok(!fileCreated.ok, "creating a file in temp failed");
is(
fileCreated.code,
Cr.NS_ERROR_FILE_ACCESS_DENIED,
"creating a file in temp failed with access denied"
);
}
// now delete the file
let fileDeleted = await SpecialPowers.spawn(browser, [path], deleteFile);
if (isDbgWin) {
ok(fileDeleted.ok, "deleting a file in temp succeeded");
} else {
ok(!fileDeleted.ok, "deleting a file in temp failed");
const expectedError = isLinux()
? Cr.NS_ERROR_FILE_ACCESS_DENIED
: Cr.NS_ERROR_FILE_NOT_FOUND;
is(
fileDeleted.code,
expectedError,
"deleting a file in temp failed with access denied"
);
}
// Test that symlink creation is not allowed on macOS/Linux.
if (isMac() || isLinux()) {
let path = fileInTempDir().path;
let symlinkCreated = await SpecialPowers.spawn(
browser,
[path],
createSymlink
);
ok(!symlinkCreated.ok, "created a symlink in temp failed");
const expectedError = isLinux() ? lazy.LIBC.EACCES : lazy.LIBC.EPERM;
is(
symlinkCreated.code,
expectedError,
"created a symlink in temp failed with access denied"
);
}
}
// Test reading files and dirs from web and file content processes.
async function testFileAccessAllPlatforms() {
let webBrowser = GetWebBrowser();
let fileContentProcessEnabled = isFileContentProcessEnabled();
let fileBrowser = GetFileBrowser();
// Directories/files to test accessing from content processes.
// For directories, we test whether a directory listing is allowed
// or blocked. For files, we test if we can read from the file.
// Each entry in the array represents a test file or directory
// that will be read from either a web or file process.
let tests = [];
let profileDir = GetProfileDir();
tests.push({
desc: "profile dir", // description
ok: false, // expected to succeed?
browser: webBrowser, // browser to run test in
file: profileDir, // nsIFile object
minLevel: minProfileReadSandboxLevel(), // min level to enable test
func: readDir,
});
if (fileContentProcessEnabled) {
tests.push({
desc: "profile dir",
ok: true,
browser: fileBrowser,
file: profileDir,
minLevel: 0,
func: readDir,
});
}
let homeDir = GetHomeDir();
tests.push({
desc: "home dir",
ok: false,
browser: webBrowser,
file: homeDir,
minLevel: minHomeReadSandboxLevel(),
func: readDir,
});
if (fileContentProcessEnabled) {
tests.push({
desc: "home dir",
ok: true,
browser: fileBrowser,
file: homeDir,
minLevel: 0,
func: readDir,
});
}
let extensionsDir = GetProfileEntry("extensions");
if (extensionsDir.exists() && extensionsDir.isDirectory()) {
tests.push({
desc: "extensions dir",
ok: true,
browser: webBrowser,
file: extensionsDir,
minLevel: 0,
func: readDir,
});
} else {
ok(false, `${extensionsDir.path} is a valid dir`);
}
let chromeDir = GetProfileEntry("chrome");
if (chromeDir.exists() && chromeDir.isDirectory()) {
tests.push({
desc: "chrome dir",
ok: true,
browser: webBrowser,
file: chromeDir,
minLevel: 0,
func: readDir,
});
} else {
ok(false, `${chromeDir.path} is valid dir`);
}
let cookiesFile = GetProfileEntry("cookies.sqlite");
if (cookiesFile.exists() && !cookiesFile.isDirectory()) {
tests.push({
desc: "cookies file",
ok: false,
browser: webBrowser,
file: cookiesFile,
minLevel: minProfileReadSandboxLevel(),
func: readFile,
});
if (fileContentProcessEnabled) {
tests.push({
desc: "cookies file",
ok: true,
browser: fileBrowser,
file: cookiesFile,
minLevel: 0,
func: readFile,
});
}
} else {
ok(false, `${cookiesFile.path} is a valid file`);
}
if (isMac() || isLinux()) {
let varDir = GetDir("/var");
if (isMac()) {
// Mac sandbox rules use /private/var because /var is a symlink
// to /private/var on OS X. Make sure that hasn't changed.
varDir.normalize();
Assert.strictEqual(
varDir.path,
"/private/var",
"/var resolves to /private/var"
);
}
tests.push({
desc: "/var",
ok: false,
browser: webBrowser,
file: varDir,
minLevel: minHomeReadSandboxLevel(),
func: readDir,
});
if (fileContentProcessEnabled) {
tests.push({
desc: "/var",
ok: true,
browser: fileBrowser,
file: varDir,
minLevel: 0,
func: readDir,
});
}
}
await runTestsList(tests);
}
async function testFileAccessMacOnly() {
if (!isMac()) {
return;
}
let webBrowser = GetWebBrowser();
let fileContentProcessEnabled = isFileContentProcessEnabled();
let fileBrowser = GetFileBrowser();
let level = GetSandboxLevel();
let tests = [];
// If ~/Library/Caches/TemporaryItems exists, when level <= 2 we
// make sure it's readable. For level 3, we make sure it isn't.
let homeTempDir = GetHomeDir();
homeTempDir.appendRelativePath("Library/Caches/TemporaryItems");
if (homeTempDir.exists()) {
let shouldBeReadable, minLevel;
if (level >= minHomeReadSandboxLevel()) {
shouldBeReadable = false;
minLevel = minHomeReadSandboxLevel();
} else {
shouldBeReadable = true;
minLevel = 0;
}
tests.push({
desc: "home library cache temp dir",
ok: shouldBeReadable,
browser: webBrowser,
file: homeTempDir,
minLevel,
func: readDir,
});
}
// Test if we can read from $TMPDIR because we expect it
// to be within /private/var. Reading from it should be
// prevented in a 'web' process.
let macTempDir = GetDirFromEnvVariable("TMPDIR");
macTempDir.normalize();
Assert.ok(
macTempDir.path.startsWith("/private/var"),
"$TMPDIR is in /private/var"
);
tests.push({
desc: `$TMPDIR (${macTempDir.path})`,
ok: false,
browser: webBrowser,
file: macTempDir,
minLevel: minHomeReadSandboxLevel(),
func: readDir,
});
if (fileContentProcessEnabled) {
tests.push({
desc: `$TMPDIR (${macTempDir.path})`,
ok: true,
browser: fileBrowser,
file: macTempDir,
minLevel: 0,
func: readDir,
});
}
// The font registry directory is in the Darwin user cache dir which is
// accessible with the getconf(1) library call using DARWIN_USER_CACHE_DIR.
// For this test, assume the cache dir is located at $TMPDIR/../C and use
// the $TMPDIR to derive the path to the registry.
let fontRegistryDir = macTempDir.parent.clone();
fontRegistryDir.appendRelativePath("C/com.apple.FontRegistry");
if (fontRegistryDir.exists()) {
tests.push({
desc: `FontRegistry (${fontRegistryDir.path})`,
ok: true,
browser: webBrowser,
file: fontRegistryDir,
minLevel: minHomeReadSandboxLevel(),
func: readDir,
});
// Check that we can read the file named `font` which typically
// exists in the the font registry directory.
let fontFile = fontRegistryDir.clone();
fontFile.appendRelativePath("font");
if (fontFile.exists()) {
tests.push({
desc: `FontRegistry file (${fontFile.path})`,
ok: true,
browser: webBrowser,
file: fontFile,
minLevel: minHomeReadSandboxLevel(),
func: readFile,
});
}
}
// Test that we cannot read from /Volumes at level 3
let volumes = GetDir("/Volumes");
tests.push({
desc: "/Volumes",
ok: false,
browser: webBrowser,
file: volumes,
minLevel: minHomeReadSandboxLevel(),
func: readDir,
});
// Test that we cannot read from /Users at level 3
let users = GetDir("/Users");
tests.push({
desc: "/Users",
ok: false,
browser: webBrowser,
file: users,
minLevel: minHomeReadSandboxLevel(),
func: readDir,
});
// Test that we can stat /Users at level 3
tests.push({
desc: "/Users",
ok: true,
browser: webBrowser,
file: users,
minLevel: minHomeReadSandboxLevel(),
func: statPath,
});
// Test that we can stat /Library at level 3, but can't get a
// directory listing of /Library. This test uses "/Library"
// because it's a path that is expected to always be present.
let libraryDir = GetDir("/Library");
tests.push({
desc: "/Library",
ok: true,
browser: webBrowser,
file: libraryDir,
minLevel: minHomeReadSandboxLevel(),
func: statPath,
});
tests.push({
desc: "/Library",
ok: false,
browser: webBrowser,
file: libraryDir,
minLevel: minHomeReadSandboxLevel(),
func: readDir,
});
// Similarly, test that we can stat /private, but not /private/etc.
let privateDir = GetDir("/private");
tests.push({
desc: "/private",
ok: true,
browser: webBrowser,
file: privateDir,
minLevel: minHomeReadSandboxLevel(),
func: statPath,
});
await runTestsList(tests);
}
async function testFileAccessLinuxOnly() {
if (!isLinux()) {
return;
}
let webBrowser = GetWebBrowser();
let fileContentProcessEnabled = isFileContentProcessEnabled();
let fileBrowser = GetFileBrowser();
let tests = [];
// Test /proc/self/fd, because that can be used to unfreeze
// frozen shared memory.
let selfFdDir = GetDir("/proc/self/fd");
tests.push({
desc: "/proc/self/fd",
ok: false,
browser: webBrowser,
file: selfFdDir,
minLevel: isContentFileIOSandboxed(),
func: readDir,
});
let cacheFontConfigDir = GetHomeSubdir(".cache/fontconfig/");
tests.push({
desc: `$HOME/.cache/fontconfig/ (${cacheFontConfigDir.path})`,
ok: true,
browser: webBrowser,
file: cacheFontConfigDir,
minLevel: minHomeReadSandboxLevel(),
func: readDir,
});
// allows to handle both $HOME/.config/ or $XDG_CONFIG_HOME
let configDir = GetHomeSubdir(".config");
const xdgConfigHome = Services.env.get("XDG_CONFIG_HOME");
if (xdgConfigHome) {
configDir = GetDir(xdgConfigHome);
configDir.normalize();
}
tests.push({
desc: `$XDG_CONFIG_HOME (${configDir.path})`,
ok: true, // access should not be granted outside of XDG support
browser: webBrowser,
file: configDir,
minLevel: minHomeReadSandboxLevel(),
func: readDir,
});
tests.push({
desc: `XDG_CONFIG_HOME=${configDir.path} dir should have rdonly`,
ok: true, // should be allowed only if XDG support is there
browser: webBrowser,
file: configDir,
minLevel: minHomeReadSandboxLevel(),
func: readDir,
});
if (fileContentProcessEnabled) {
tests.push({
desc: `${configDir.path} dir`,
ok: true, // should be allowed only if XDG support is there
browser: fileBrowser,
file: configDir,
minLevel: 0,
func: readDir,
});
}
if (isXdgEnabled() && xdgConfigHome) {
const homeConfigDir = GetHomeSubdir(".config");
tests.push({
desc: `XDG_CONFIG_HOME=${homeConfigDir.path} dir should deny $HOME/.config`,
ok: false,
browser: webBrowser,
file: homeConfigDir,
minLevel: minHomeReadSandboxLevel(),
func: readDir,
});
if (fileContentProcessEnabled) {
tests.push({
desc: `${homeConfigDir.path} dir`,
ok: true,
browser: fileBrowser,
file: homeConfigDir,
minLevel: 0,
func: readDir,
});
}
} else {
// WWhen XDG_CONFIG_HOME is not set, verify we do not allow $HOME/.configlol
// (i.e., check allow the dir and not the prefix)
//
// Checking $HOME/.config is already done above.
const homeConfigPrefix = GetHomeSubdir(".configlol");
tests.push({
desc: `No XDG_CONFIG_HOME we dont allow ${homeConfigPrefix.path} access`,
ok: false,
browser: webBrowser,
file: homeConfigPrefix,
minLevel: minHomeReadSandboxLevel(),
func: readDir,
});
if (fileContentProcessEnabled) {
tests.push({
desc: `No XDG_CONFIG_HOME we dont allow ${homeConfigPrefix.path} access`,
ok: false,
browser: fileBrowser,
file: homeConfigPrefix,
minLevel: 0,
func: readDir,
});
}
}
// Create a file under $HOME/.config/ or $XDG_CONFIG_HOME and ensure we can
// read it
let fileUnderConfig = GetSubdirFile(configDir);
await IOUtils.writeUTF8(fileUnderConfig.path, "TEST FILE DUMMY DATA");
ok(
await IOUtils.exists(fileUnderConfig.path),
`File ${fileUnderConfig.path} was properly created`
);
tests.push({
desc: `${configDir.path}/xxx is readable (${fileUnderConfig.path})`,
ok: true,
browser: webBrowser,
file: fileUnderConfig,
minLevel: minHomeReadSandboxLevel(),
func: readFile,
cleanup: aPath => IOUtils.remove(aPath),
});
let configFile = GetSubdirFile(configDir);
tests.push({
desc: `${configDir.path} file write`,
ok: false,
browser: webBrowser,
file: configFile,
minLevel: minHomeReadSandboxLevel(),
func: createFile,
});
if (fileContentProcessEnabled) {
tests.push({
desc: `${configDir.path} file write`,
ok: false,
browser: fileBrowser,
file: configFile,
minLevel: 0,
func: createFile,
});
}
// Create a $HOME/.config/mozilla/ or $XDG_CONFIG_HOME/mozilla/ if none
// exists and assert content process cannot access it
let configMozilla = GetSubdir(configDir, "mozilla");
const emptyFileName = ".test_run_browser_sandbox.tmp";
let emptyFile = configMozilla.clone();
emptyFile.appendRelativePath(emptyFileName);
let populateFakeConfigMozilla = async aPath => {
// called with configMozilla
await IOUtils.makeDirectory(aPath, { permissions: 0o700 });
await IOUtils.writeUTF8(emptyFile.path, "");
ok(
await IOUtils.exists(emptyFile.path),
`Temp file ${emptyFile.path} was created`
);
};
let unpopulateFakeConfigMozilla = async aPath => {
// called with emptyFile
await IOUtils.remove(aPath);
ok(!(await IOUtils.exists(aPath)), `Temp file ${aPath} was removed`);
const parentDir = PathUtils.parent(aPath);
try {
await IOUtils.remove(parentDir, { recursive: false });
} catch (ex) {
if (
!DOMException.isInstance(ex) ||
ex.name !== "OperationError" ||
/Could not remove the non-empty directory/.test(ex.message)
) {
// If we get here it means the directory was not empty and since we assert
// earlier we removed the temp file we created it means we should not
// worrying about removing this directory ...
throw ex;
}
}
};
await populateFakeConfigMozilla(configMozilla.path);
tests.push({
desc: `stat ${configDir.path}/mozilla (${configMozilla.path})`,
ok: false,
browser: webBrowser,
file: configMozilla,
minLevel: minHomeReadSandboxLevel(),
func: statPath,
});
tests.push({
desc: `read ${configDir.path}/mozilla (${configMozilla.path})`,
ok: false,
browser: webBrowser,
file: configMozilla,
minLevel: minHomeReadSandboxLevel(),
func: readDir,
});
tests.push({
desc: `stat ${configDir.path}/mozilla/${emptyFileName} (${emptyFile.path})`,
ok: false,
browser: webBrowser,
file: emptyFile,
minLevel: minHomeReadSandboxLevel(),
func: statPath,
});
tests.push({
desc: `read ${configDir.path}/mozilla/${emptyFileName} (${emptyFile.path})`,
ok: false,
browser: webBrowser,
file: emptyFile,
minLevel: minHomeReadSandboxLevel(),
func: readFile,
cleanup: unpopulateFakeConfigMozilla,
});
// Only needed to perform cleanup
if (isXdgEnabled()) {
tests.push({
desc: `$XDG_CONFIG_HOME (${configDir.path}) cleanup`,
ok: true,
browser: webBrowser,
file: configDir,
minLevel: minHomeReadSandboxLevel(),
func: readDir,
});
}
await runTestsList(tests);
}
async function testFileAccessLinuxSnap() {
let webBrowser = GetWebBrowser();
let tests = [];
// Assert that if we run with SNAP= env, then we allow access to it in the
// content process
let snap = Services.env.get("SNAP");
let snapExpectedResult = false;
if (snap.length > 1) {
snapExpectedResult = true;
} else {
snap = "/tmp/.snap_firefox_current/";
}
let snapDir = GetDir(snap);
snapDir.normalize();
let snapFile = GetSubdirFile(snapDir);
await createFile(snapFile.path);
ok(await IOUtils.exists(snapFile.path), `SNAP ${snapFile.path} was created`);
info(`SNAP (file) ${snapFile.path} was created`);
tests.push({
desc: `$SNAP (${snapDir.path} => ${snapFile.path})`,
ok: snapExpectedResult,
browser: webBrowser,
file: snapFile,
minLevel: minHomeReadSandboxLevel(),
func: readFile,
});
await runTestsList(tests);
}
async function testFileAccessWindowsOnly() {
if (!isWin()) {
return;
}
let webBrowser = GetWebBrowser();
let tests = [];
let extDir = GetPerUserExtensionDir();
// We used to unconditionally create this directory from Firefox, but that
// was dropped in bug 2001887. The value of this directory is questionable;
// the test was added in Firefox 56 (bug 1403744) to cover legacy add-ons,
// but legacy add-on support was discontinued in Firefox 57, and we stopped
// sideloading add-ons from this directory on all builds except ESR in
// Firefox 74 (bug 1602840).
await IOUtils.makeDirectory(extDir.path);
tests.push({
desc: "per-user extensions dir",
ok: true,
browser: webBrowser,
file: extDir,
minLevel: minHomeReadSandboxLevel(),
func: readDir,
});
await runTestsList(tests);
}
function cleanupBrowserTabs() {
let fileBrowser = GetFileBrowser();
if (fileBrowser.selectedTab) {
gBrowser.removeTab(fileBrowser.selectedTab);
}
let webBrowser = GetWebBrowser();
if (webBrowser.selectedTab) {
gBrowser.removeTab(webBrowser.selectedTab);
}
let tab1 = gBrowser.tabs[1];
if (tab1) {
gBrowser.removeTab(tab1);
}
}

View File

@@ -0,0 +1,40 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* import-globals-from browser_content_sandbox_utils.js */
"use strict";
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/" +
"security/sandbox/test/browser_content_sandbox_utils.js",
this
);
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/" +
"security/sandbox/test/browser_content_sandbox_fs_tests.js",
this
);
SimpleTest.requestCompleteLog();
add_setup(async function setup() {
const xdgConfigHome = Services.env.exists("XDG_CONFIG_HOME");
Assert.equal(xdgConfigHome, false, `XDG_CONFIG_HOME is not set`);
const mozLegacyHome = Services.env.exists("MOZ_LEGACY_HOME");
Assert.equal(mozLegacyHome, false, "MOZ_LEGACY_HOME is not set");
// If it is there, do actual testing
sanityChecks();
});
add_task(async function () {
// Make sure we dont break others.
add_task(testFileAccessAllPlatforms); // eslint-disable-line no-undef
// The linux only tests are the ones that can behave differently based on
// existence of XDG_CONFIG_HOME
add_task(testFileAccessLinuxOnly); // eslint-disable-line no-undef
add_task(cleanupBrowserTabs); // eslint-disable-line no-undef
});

View File

@@ -0,0 +1,42 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* import-globals-from browser_content_sandbox_utils.js */
"use strict";
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/" +
"security/sandbox/test/browser_content_sandbox_utils.js",
this
);
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/" +
"security/sandbox/test/browser_content_sandbox_fs_tests.js",
this
);
SimpleTest.requestCompleteLog();
add_setup(async function setup() {
const xdgConfigHome = Services.env.exists("XDG_CONFIG_HOME");
Assert.equal(xdgConfigHome, true, "XDG_CONFIG_HOME is defined");
if (isXdgEnabled()) {
const mozLegacyHome = Services.env.get("MOZ_LEGACY_HOME");
Assert.equal(mozLegacyHome, 1, "MOZ_LEGACY_HOME is set to 1");
}
// If it is there, do actual testing
sanityChecks();
});
add_task(async function () {
// Make sure we dont break others.
add_task(testFileAccessAllPlatforms); // eslint-disable-line no-undef
// The linux only tests are the ones that can behave differently based on
// existence of XDG_CONFIG_HOME
add_task(testFileAccessLinuxOnly); // eslint-disable-line no-undef
add_task(cleanupBrowserTabs); // eslint-disable-line no-undef
});

View File

@@ -0,0 +1,45 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* import-globals-from browser_content_sandbox_utils.js */
"use strict";
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/" +
"security/sandbox/test/browser_content_sandbox_utils.js",
this
);
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/" +
"security/sandbox/test/browser_content_sandbox_fs_tests.js",
this
);
SimpleTest.requestCompleteLog();
add_setup(async function setup() {
// Ensure that XDG_CONFIG_HOME is there
const xdgConfigHome = Services.env.get("XDG_CONFIG_HOME");
Assert.greater(xdgConfigHome.length, 1, "XDG_CONFIG_HOME is defined");
// Verify the profile directory is inside XDG_CONFIG_HOME
const profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
Assert.ok(
profileDir.path.startsWith(xdgConfigHome),
`Profile directory (${profileDir.path}) should be inside XDG_CONFIG_HOME (${xdgConfigHome})`
);
// If it is there, do actual testing
sanityChecks();
});
add_task(async function () {
// Make sure we dont break others.
add_task(testFileAccessAllPlatforms); // eslint-disable-line no-undef
// The linux only tests are the ones that can behave differently based on
// existence of XDG_CONFIG_HOME
add_task(testFileAccessLinuxOnly); // eslint-disable-line no-undef
add_task(cleanupBrowserTabs); // eslint-disable-line no-undef
});

View File

@@ -0,0 +1,405 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* import-globals-from browser_content_sandbox_utils.js */
"use strict";
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/" +
"security/sandbox/test/browser_content_sandbox_utils.js",
this
);
const lazy = {};
/* getLibcConstants is only present on *nix */
ChromeUtils.defineLazyGetter(lazy, "LIBC", () =>
ChromeUtils.getLibcConstants()
);
/*
* This test is for executing system calls in content processes to validate
* that calls that are meant to be blocked by content sandboxing are blocked.
* We use the term system calls loosely so that any OS API call such as
* fopen could be included.
*/
// Calls the native execv library function. Include imports so this can be
// safely serialized and run remotely by SpecialPowers.spawn.
function callExec(args) {
const { ctypes } = ChromeUtils.importESModule(
"resource://gre/modules/ctypes.sys.mjs"
);
let { lib, cmd } = args;
let libc = ctypes.open(lib);
let exec = libc.declare(
"execv",
ctypes.default_abi,
ctypes.int,
ctypes.char.ptr
);
let rv = exec(cmd);
libc.close();
return rv;
}
// Calls the native fork syscall.
function callFork(args) {
const { ctypes } = ChromeUtils.importESModule(
"resource://gre/modules/ctypes.sys.mjs"
);
let { lib } = args;
let libc = ctypes.open(lib);
let fork = libc.declare("fork", ctypes.default_abi, ctypes.int);
let rv = fork();
libc.close();
return rv;
}
// Calls the native sysctl syscall.
function callSysctl(args) {
const { ctypes } = ChromeUtils.importESModule(
"resource://gre/modules/ctypes.sys.mjs"
);
let { lib, name } = args;
let libc = ctypes.open(lib);
let sysctlbyname = libc.declare(
"sysctlbyname",
ctypes.default_abi,
ctypes.int,
ctypes.char.ptr,
ctypes.voidptr_t,
ctypes.size_t.ptr,
ctypes.voidptr_t,
ctypes.size_t.ptr
);
let rv = sysctlbyname(name, null, null, null, null);
libc.close();
return rv;
}
function callPrctl(args) {
const { ctypes } = ChromeUtils.importESModule(
"resource://gre/modules/ctypes.sys.mjs"
);
let { lib, option } = args;
let libc = ctypes.open(lib);
let prctl = libc.declare(
"prctl",
ctypes.default_abi,
ctypes.int,
ctypes.int, // option
ctypes.unsigned_long, // arg2
ctypes.unsigned_long, // arg3
ctypes.unsigned_long, // arg4
ctypes.unsigned_long // arg5
);
let rv = prctl(option, 0, 0, 0, 0);
if (rv == -1) {
rv = ctypes.errno;
}
libc.close();
return rv;
}
// Calls the native open/close syscalls.
function callOpen(args) {
const { ctypes } = ChromeUtils.importESModule(
"resource://gre/modules/ctypes.sys.mjs"
);
let { lib, path, flags } = args;
let libc = ctypes.open(lib);
let open = libc.declare(
"open",
ctypes.default_abi,
ctypes.int,
ctypes.char.ptr,
ctypes.int
);
let close = libc.declare("close", ctypes.default_abi, ctypes.int, ctypes.int);
let fd = open(path, flags);
close(fd);
libc.close();
return fd;
}
// Verify faccessat2
function callFaccessat2(args) {
const { ctypes } = ChromeUtils.importESModule(
"resource://gre/modules/ctypes.sys.mjs"
);
let { lib, dirfd, path, mode, flag } = args;
let libc = ctypes.open(lib);
let faccessat = libc.declare(
"faccessat",
ctypes.default_abi,
ctypes.int,
ctypes.int, // dirfd
ctypes.char.ptr, // path
ctypes.int, // mode
ctypes.int // flag
);
let rv = faccessat(dirfd, path, mode, flag);
if (rv == -1) {
rv = ctypes.errno;
}
libc.close();
return rv;
}
// Returns the name of the native library needed for native syscalls
function getOSLib() {
switch (Services.appinfo.OS) {
case "WINNT":
return "kernel32.dll";
case "Darwin":
return "libc.dylib";
case "Linux":
return "libc.so.6";
default:
Assert.ok(false, "Unknown OS");
return 0;
}
}
// Reading a header might be weird, but the alternatives to read a stable
// version number we can easily check against are not much more fun
async function getKernelVersion() {
let header = await IOUtils.readUTF8("/usr/include/linux/version.h");
let hr = header.split("\n");
for (let line in hr) {
let hrs = hr[line].split(" ");
if (hrs[0] === "#define" && hrs[1] === "LINUX_VERSION_CODE") {
return Number(hrs[2]);
}
}
throw Error("No LINUX_VERSION_CODE");
}
// This is how it is done in /usr/include/linux/version.h
function computeKernelVersion(major, minor, dot) {
return (major << 16) + (minor << 8) + dot;
}
function getGlibcVersion() {
const { ctypes } = ChromeUtils.importESModule(
"resource://gre/modules/ctypes.sys.mjs"
);
let libc = ctypes.open(getOSLib());
let gnu_get_libc_version = libc.declare(
"gnu_get_libc_version",
ctypes.default_abi,
ctypes.char.ptr
);
let rv = gnu_get_libc_version().readString();
libc.close();
let ar = rv.split(".");
// return a number made of MAJORMINOR
return Number(ar[0] + ar[1]);
}
// Returns a harmless command to execute with execv
function getOSExecCmd() {
Assert.ok(!isWin());
return "/bin/cat";
}
// Returns true if the current content sandbox level, passed in
// the |level| argument, supports syscall sandboxing.
function areContentSyscallsSandboxed(level) {
let syscallsSandboxMinLevel = 0;
// Set syscallsSandboxMinLevel to the lowest level that has
// syscall sandboxing enabled. For now, this varies across
// Windows, Mac, Linux, other.
switch (Services.appinfo.OS) {
case "WINNT":
syscallsSandboxMinLevel = 1;
break;
case "Darwin":
syscallsSandboxMinLevel = 1;
break;
case "Linux":
syscallsSandboxMinLevel = 1;
break;
default:
Assert.ok(false, "Unknown OS");
}
return level >= syscallsSandboxMinLevel;
}
//
// Drive tests for a single content process.
//
// Tests executing OS API calls in the content process. Limited to Mac
// and Linux calls for now.
//
add_task(async function () {
// This test is only relevant in e10s
if (!gMultiProcessBrowser) {
ok(false, "e10s is enabled");
info("e10s is not enabled, exiting");
return;
}
let level = 0;
let prefExists = true;
// Read the security.sandbox.content.level pref.
// If the pref isn't set and we're running on Linux on !isNightly(),
// exit without failing. The Linux content sandbox is only enabled
// on Nightly at this time.
// eslint-disable-next-line mozilla/use-default-preference-values
try {
level = Services.prefs.getIntPref("security.sandbox.content.level");
} catch (e) {
prefExists = false;
}
ok(prefExists, "pref security.sandbox.content.level exists");
if (!prefExists) {
return;
}
info(`security.sandbox.content.level=${level}`);
Assert.greater(level, 0, "content sandbox is enabled.");
let areSyscallsSandboxed = areContentSyscallsSandboxed(level);
// Content sandbox enabled, but level doesn't include syscall sandboxing.
ok(areSyscallsSandboxed, "content syscall sandboxing is enabled.");
if (!areSyscallsSandboxed) {
info("content sandbox level too low for syscall tests, exiting\n");
return;
}
let browser = gBrowser.selectedBrowser;
let lib = getOSLib();
// use execv syscall
// (causes content process to be killed on Linux)
if (isMac()) {
// exec something harmless, this should fail
let cmd = getOSExecCmd();
let rv = await SpecialPowers.spawn(browser, [{ lib, cmd }], callExec);
Assert.equal(rv, -1, `exec(${cmd}) is not permitted`);
}
// use open syscall
if (isLinux() || isMac()) {
// open a file for writing in $HOME, this should fail
let path = fileInHomeDir().path;
let flags = lazy.LIBC.O_CREAT | lazy.LIBC.O_WRONLY;
let fd = await SpecialPowers.spawn(
browser,
[{ lib, path, flags }],
callOpen
);
Assert.less(fd, 0, "opening a file for writing in home is not permitted");
}
// use open syscall
if (isLinux() || isMac()) {
// open a file for writing in the content temp dir, this should fail on
// macOS and Linux. The open handler in the content process closes the file
// for us
let path = fileInTempDir().path;
let flags = lazy.LIBC.O_CREAT | lazy.LIBC.O_WRONLY;
let fd = await SpecialPowers.spawn(
browser,
[{ lib, path, flags }],
callOpen
);
Assert.strictEqual(
fd,
-1,
"opening a file for writing in content temp is not permitted"
);
}
// use fork syscall
if (isLinux() || isMac()) {
let rv = await SpecialPowers.spawn(browser, [{ lib }], callFork);
Assert.equal(rv, -1, "calling fork is not permitted");
}
// On macOS before 10.10 the |sysctl-name| predicate didn't exist for
// filtering |sysctl| access. Check the Darwin version before running the
// tests (Darwin 14.0.0 is macOS 10.10). This branch can be removed when we
// remove support for macOS 10.9.
if (isMac() && Services.sysinfo.getProperty("version") >= "14.0.0") {
let rv = await SpecialPowers.spawn(
browser,
[{ lib, name: "kern.boottime" }],
callSysctl
);
Assert.equal(rv, -1, "calling sysctl('kern.boottime') is not permitted");
rv = await SpecialPowers.spawn(
browser,
[{ lib, name: "net.inet.ip.ttl" }],
callSysctl
);
Assert.equal(rv, -1, "calling sysctl('net.inet.ip.ttl') is not permitted");
rv = await SpecialPowers.spawn(
browser,
[{ lib, name: "hw.ncpu" }],
callSysctl
);
Assert.equal(rv, 0, "calling sysctl('hw.ncpu') is permitted");
}
if (isLinux()) {
// These constants are not portable.
// verify we block PR_CAPBSET_READ with EINVAL
let option = lazy.LIBC.PR_CAPBSET_READ;
let rv = await SpecialPowers.spawn(browser, [{ lib, option }], callPrctl);
Assert.strictEqual(
rv,
lazy.LIBC.EINVAL,
"prctl(PR_CAPBSET_READ) is blocked"
);
const kernelVersion = await getKernelVersion();
const glibcVersion = getGlibcVersion();
// faccessat2 is only used with kernel 5.8+ by glibc 2.33+
if (glibcVersion >= 233 && kernelVersion >= computeKernelVersion(5, 8, 0)) {
info("Linux v5.8+, glibc 2.33+, checking faccessat2");
const dirfd = 0;
const path = "/";
const mode = 0;
// the value 0x01 is just one we know should get rejected
let rv = await SpecialPowers.spawn(
browser,
[{ lib, dirfd, path, mode, flag: 0x01 }],
callFaccessat2
);
Assert.strictEqual(
rv,
lazy.LIBC.ENOSYS,
"faccessat2 (flag=0x01) was blocked with ENOSYS"
);
rv = await SpecialPowers.spawn(
browser,
[{ lib, dirfd, path, mode, flag: lazy.LIBC.AT_EACCESS }],
callFaccessat2
);
Assert.strictEqual(
rv,
lazy.LIBC.EACCES,
"faccessat2 (flag=0x200) was allowed, errno=EACCES"
);
} else {
info(
"Unsupported kernel (" +
kernelVersion +
" )/glibc (" +
glibcVersion +
"), skipping faccessat2"
);
}
}
});

View File

@@ -0,0 +1,502 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const uuidGenerator = Services.uuid;
/*
* Utility functions for the browser content sandbox tests.
*/
function sanityChecks() {
// This test is only relevant in e10s
if (!gMultiProcessBrowser) {
ok(false, "e10s is enabled");
info("e10s is not enabled, exiting");
return;
}
let level = 0;
let prefExists = true;
// Read the security.sandbox.content.level pref.
// eslint-disable-next-line mozilla/use-default-preference-values
try {
level = Services.prefs.getIntPref("security.sandbox.content.level");
} catch (e) {
prefExists = false;
}
ok(prefExists, "pref security.sandbox.content.level exists");
if (!prefExists) {
return;
}
info(`security.sandbox.content.level=${level}`);
Assert.greater(level, 0, "content sandbox is enabled.");
let isFileIOSandboxed = isContentFileIOSandboxed(level);
// Content sandbox enabled, but level doesn't include file I/O sandboxing.
ok(isFileIOSandboxed, "content file I/O sandboxing is enabled.");
if (!isFileIOSandboxed) {
info("content sandbox level too low for file I/O tests, exiting\n");
}
}
function isXdgEnabled() {
try {
return Services.prefs.getBoolPref("widget.support-xdg-config");
} catch (ex) {
// if the pref is not there it means we dont have XDG support
if (ex.name === "NS_ERROR_UNEXPECTED") {
return false;
}
throw ex;
}
}
// Creates file at |path| and returns a promise that resolves with an object
// with .ok boolean to indicate true if the file was successfully created,
// otherwise false. Include imports so this can be safely serialized and run
// remotely by SpecialPowers.spawn.
//
// Report the exception's error code in .code as well.
function createFile(path) {
const { FileUtils } = ChromeUtils.importESModule(
"resource://gre/modules/FileUtils.sys.mjs"
);
try {
const fstream = Cc[
"@mozilla.org/network/file-output-stream;1"
].createInstance(Ci.nsIFileOutputStream);
fstream.init(
new FileUtils.File(path),
-1, // readonly mode
-1, // default permissions
0
); // behaviour flags
const ostream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
Ci.nsIBinaryOutputStream
);
ostream.setOutputStream(fstream);
const data = "TEST FILE DUMMY DATA";
ostream.writeBytes(data, data.length);
ostream.close();
fstream.close();
} catch (e) {
return { ok: false, code: e.result };
}
return { ok: true };
}
// Creates a symlink at |path| and returns a promise that resolves with an
// object with .ok boolean to indicate true if the symlink was successfully
// created, otherwise false. Include imports so this can be safely serialized
// and run remotely by SpecialPowers.spawn.
//
// Report the exception's error code in .code as well.
// Report errno in .code if syscall returns -1.
function createSymlink(path) {
const { ctypes } = ChromeUtils.importESModule(
"resource://gre/modules/ctypes.sys.mjs"
);
try {
// Trying to open "libc.so" on linux will fail with invalid elf header error
// because it would be a linker script. Using libc.so.6 avoids that.
const libc = ctypes.open(
Services.appinfo.OS === "Darwin" ? "libSystem.B.dylib" : "libc.so.6"
);
const symlink = libc.declare(
"symlink",
ctypes.default_abi,
ctypes.int, // return value
ctypes.char.ptr, // target
ctypes.char.ptr //linkpath
);
ctypes.errno = 0;
const rv = symlink("/etc", path);
const _errno = ctypes.errno;
if (rv < 0) {
return { ok: false, code: _errno };
}
} catch (e) {
return { ok: false, code: e.result };
}
return { ok: true };
}
// Deletes file at |path| and returns a promise that resolves with an object
// with .ok boolean to indicate true if the file was successfully deleted,
// otherwise false. Include imports so this can be safely serialized and run
// remotely by SpecialPowers.spawn.
//
// Report the exception's error code in .code as well.
function deleteFile(path) {
const { FileUtils } = ChromeUtils.importESModule(
"resource://gre/modules/FileUtils.sys.mjs"
);
try {
const file = new FileUtils.File(path);
file.remove(false);
} catch (e) {
return { ok: false, code: e.result };
}
return { ok: true };
}
// Reads the directory at |path| and returns a promise that resolves when
// iteration over the directory finishes or encounters an error. The promise
// resolves with an object where .ok indicates success or failure and
// .numEntries is the number of directory entries found.
//
// Report the exception's error code in .code as well.
function readDir(path) {
const { FileUtils } = ChromeUtils.importESModule(
"resource://gre/modules/FileUtils.sys.mjs"
);
let numEntries = 0;
try {
const file = new FileUtils.File(path);
const enumerator = file.directoryEntries;
while (enumerator.hasMoreElements()) {
void enumerator.nextFile;
numEntries++;
}
} catch (e) {
return { ok: false, numEntries, code: e.result };
}
return { ok: true, numEntries };
}
// Reads the file at |path| and returns a promise that resolves when
// reading is completed. Returned object has boolean .ok to indicate
// success or failure.
//
// Report the exception's error code in .code as well.
function readFile(path) {
const { FileUtils } = ChromeUtils.importESModule(
"resource://gre/modules/FileUtils.sys.mjs"
);
try {
const file = new FileUtils.File(path);
const fstream = Cc[
"@mozilla.org/network/file-input-stream;1"
].createInstance(Ci.nsIFileInputStream);
fstream.init(file, -1, -1, 0);
const istream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
Ci.nsIBinaryInputStream
);
istream.setInputStream(fstream);
const available = istream.available();
void istream.readBytes(available);
} catch (e) {
return { ok: false, code: e.result };
}
return { ok: true };
}
// Does a stat of |path| and returns a promise that resolves if the
// stat is successful. Returned object has boolean .ok to indicate
// success or failure.
//
// Report the exception's error code in .code as well.
function statPath(path) {
const { FileUtils } = ChromeUtils.importESModule(
"resource://gre/modules/FileUtils.sys.mjs"
);
try {
const file = new FileUtils.File(path);
void file.lastModifiedTime;
} catch (e) {
return { ok: false, code: e.result };
}
return { ok: true };
}
// Returns true if the current content sandbox level, passed in
// the |level| argument, supports filesystem sandboxing.
function isContentFileIOSandboxed(level) {
let fileIOSandboxMinLevel = 0;
// Set fileIOSandboxMinLevel to the lowest level that has
// content filesystem sandboxing enabled. For now, this
// varies across Windows, Mac, Linux, other.
switch (Services.appinfo.OS) {
case "WINNT":
fileIOSandboxMinLevel = 1;
break;
case "Darwin":
fileIOSandboxMinLevel = 1;
break;
case "Linux":
fileIOSandboxMinLevel = 2;
break;
default:
Assert.ok(false, "Unknown OS");
}
return level >= fileIOSandboxMinLevel;
}
// Returns the lowest sandbox level where blanket reading of the profile
// directory from the content process should be blocked by the sandbox.
function minProfileReadSandboxLevel() {
switch (Services.appinfo.OS) {
case "WINNT":
return 3;
case "Darwin":
return 2;
case "Linux":
return 3;
default:
Assert.ok(false, "Unknown OS");
return 0;
}
}
// Returns the lowest sandbox level where blanket reading of the home
// directory from the content process should be blocked by the sandbox.
function minHomeReadSandboxLevel() {
switch (Services.appinfo.OS) {
case "WINNT":
return 3;
case "Darwin":
return 3;
case "Linux":
return 3;
default:
Assert.ok(false, "Unknown OS");
return 0;
}
}
function isMac() {
return Services.appinfo.OS == "Darwin";
}
function isWin() {
return Services.appinfo.OS == "WINNT";
}
function isLinux() {
return Services.appinfo.OS == "Linux";
}
function isNightly() {
let version = SpecialPowers.Services.appinfo.version;
return version.endsWith("a1");
}
function uuid() {
return uuidGenerator.generateUUID().toString();
}
// Returns a file object for a new file in the home dir ($HOME/<UUID>).
function fileInHomeDir() {
// get home directory, make sure it exists
let homeDir = Services.dirsvc.get("Home", Ci.nsIFile);
Assert.ok(homeDir.exists(), "Home dir exists");
Assert.ok(homeDir.isDirectory(), "Home dir is a directory");
// build a file object for a new file named $HOME/<UUID>
let homeFile = homeDir.clone();
homeFile.appendRelativePath(uuid());
Assert.ok(!homeFile.exists(), homeFile.path + " does not exist");
return homeFile;
}
// Returns a file object for a new file in the content temp dir (.../<UUID>).
function fileInTempDir() {
let contentTempKey = "TmpD";
// get the content temp dir, make sure it exists
let ctmp = Services.dirsvc.get(contentTempKey, Ci.nsIFile);
Assert.ok(ctmp.exists(), "Temp dir exists");
Assert.ok(ctmp.isDirectory(), "Temp dir is a directory");
// build a file object for a new file in content temp
let tempFile = ctmp.clone();
tempFile.appendRelativePath(uuid());
Assert.ok(!tempFile.exists(), tempFile.path + " does not exist");
return tempFile;
}
function GetProfileDir() {
// get profile directory
let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
return profileDir;
}
function GetHomeDir() {
// get home directory
let homeDir = Services.dirsvc.get("Home", Ci.nsIFile);
return homeDir;
}
function GetHomeSubdir(subdir) {
return GetSubdir(GetHomeDir(), subdir);
}
function GetHomeSubdirFile(subdir) {
return GetSubdirFile(GetHomeSubdir(subdir));
}
function GetSubdir(dir, subdir) {
let newSubdir = dir.clone();
newSubdir.appendRelativePath(subdir);
return newSubdir;
}
function GetSubdirFile(dir) {
let newFile = dir.clone();
newFile.appendRelativePath(uuid());
return newFile;
}
function GetPerUserExtensionDir() {
return Services.dirsvc.get("XREUSysExt", Ci.nsIFile);
}
// Returns a file object for the file or directory named |name| in the
// profile directory.
function GetProfileEntry(name) {
let entry = GetProfileDir();
entry.append(name);
return entry;
}
function GetDir(path) {
info(`GetDir(${path})`);
let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
dir.initWithPath(path);
Assert.ok(dir.isDirectory(), `${path} is a directory`);
return dir;
}
function GetDirFromEnvVariable(varName) {
return GetDir(Services.env.get(varName));
}
function GetFile(path) {
let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
file.initWithPath(path);
return file;
}
function GetBrowserType(type) {
let browserType = undefined;
if (!GetBrowserType[type]) {
if (type === "web") {
GetBrowserType[type] = gBrowser.selectedBrowser;
} else {
// open a tab in a `type` content process
gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank", {
preferredRemoteType: type,
allowInheritPrincipal: true,
});
// get the browser for the `type` process tab
GetBrowserType[type] = gBrowser.getBrowserForTab(gBrowser.selectedTab);
}
}
browserType = GetBrowserType[type];
Assert.strictEqual(
browserType.remoteType,
type,
`GetBrowserType(${type}) returns a ${type} process`
);
return browserType;
}
function GetWebBrowser() {
return GetBrowserType("web");
}
function isFileContentProcessEnabled() {
// Ensure that the file content process is enabled.
let fileContentProcessEnabled = Services.prefs.getBoolPref(
"browser.tabs.remote.separateFileUriProcess"
);
ok(fileContentProcessEnabled, "separate file content process is enabled");
return fileContentProcessEnabled;
}
function GetFileBrowser() {
if (!isFileContentProcessEnabled()) {
return undefined;
}
return GetBrowserType("file");
}
function GetSandboxLevel() {
// Current level
return Services.prefs.getIntPref("security.sandbox.content.level");
}
async function runTestsList(tests) {
let level = GetSandboxLevel();
// remove tests not enabled by the current sandbox level
tests = tests.filter(test => test.minLevel <= level);
for (let test of tests) {
let okString = test.ok ? "allowed" : "blocked";
let processType = test.browser.remoteType;
// ensure the file/dir exists before we ask a content process to stat
// it so we know a failure is not due to a nonexistent file/dir
if (test.func === statPath) {
ok(test.file.exists(), `${test.file.path} exists`);
}
let result = await SpecialPowers.spawn(
test.browser,
[test.file.path],
test.func
);
Assert.equal(
result.ok,
test.ok,
`reading ${test.desc} from a ${processType} process ` +
`is ${okString} (${test.file.path})`
);
// if the directory is not expected to be readable,
// ensure the listing has zero entries
if (test.func === readDir && !test.ok) {
Assert.equal(
result.numEntries,
0,
`directory list is empty (${test.file.path})`
);
}
if (test.cleanup != undefined) {
await test.cleanup(test.file.path);
}
}
}

View File

@@ -0,0 +1,21 @@
[DEFAULT]
skip-if = [
"ccov",
"os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && asan", # bug 1784517 for sandbox, bug 1885381 for profiler
"os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && tsan", # bug 1784517 for sandbox, bug 1885381 for profiler
]
tags = "contentsandbox"
# This is here to make sure we will not have prelaunched processes, which will
# mess with sandbox profiling interaction: we will miss launch-related markers
# and this makes the test intermittently fail on TV jobs
prefs = [
"dom.ipc.processPrelaunch.fission.number=0"
]
environment = "MOZ_SANDBOX_LOGGING_FOR_TESTS=1"
["browser_sandbox_profiler.js"]
run-if = [
"os == 'linux'",
]

View File

@@ -0,0 +1,153 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { ProfilerTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/ProfilerTestUtils.sys.mjs"
);
async function addTab() {
const tab = BrowserTestUtils.addTab(gBrowser, "https://example.com/browser", {
forceNewProcess: true,
});
const browser = gBrowser.getBrowserForTab(tab);
await BrowserTestUtils.browserLoaded(browser);
return tab;
}
const sandboxSettingsEnabled = {
entries: 8 * 1024 * 1024, // 8M entries = 64MB
interval: 1, // ms
features: ["stackwalk", "sandbox"],
threads: ["SandboxProfilerEmitter"],
};
const sandboxSettingsDisabled = {
entries: 8 * 1024 * 1024, // 8M entries = 64MB
interval: 1, // ms
features: ["stackwalk"],
threads: ["SandboxProfilerEmitter"],
};
const kNewProcesses = 2;
async function waitForMaybeSandboxProfilerData(
threadName,
name1,
withStacks,
enabled
) {
let tabs = [];
for (let i = 0; i < kNewProcesses; ++i) {
tabs.push(await addTab());
}
let profile;
let intercepted = undefined;
try {
await TestUtils.waitForCondition(
async () => {
profile = await Services.profiler.getProfileDataAsync();
intercepted = profile.processes
.flatMap(ps => {
let sandboxThreads = ps.threads.filter(
th => th.name === threadName
);
return sandboxThreads.flatMap(th => {
let markersData = th.markers.data;
return markersData.flatMap(d => {
let [, , , , , o] = d;
return o;
});
});
})
.filter(x => "name1" in x && name1.includes(x.name1) >= 0);
return !!intercepted.length;
},
`Wait for some samples from ${threadName}`,
/* interval*/ 100,
/* maxTries */ 25
);
Assert.greater(
intercepted.length,
0,
`Should have collected some data from ${threadName}`
);
} catch (ex) {
if (!enabled && ex.includes(`Wait for some samples from ${threadName}`)) {
Assert.equal(
intercepted.length,
0,
`Should have NOT collected data from ${threadName}`
);
} else {
throw ex;
}
}
if (withStacks) {
let stacks = profile.processes.flatMap(ps => {
let sandboxThreads = ps.threads.filter(th => th.name === threadName);
return sandboxThreads.flatMap(th => {
let stackTableData = th.stackTable.data;
return stackTableData.flatMap(d => {
return [d];
});
});
});
if (enabled) {
Assert.greater(stacks.length, 0, "Should have some stack as well");
} else {
Assert.equal(stacks.length, 0, "Should have NO stack as well");
}
}
for (let tab of tabs) {
await BrowserTestUtils.removeTab(tab);
}
}
add_task(async () => {
await ProfilerTestUtils.startProfiler(sandboxSettingsEnabled);
await waitForMaybeSandboxProfilerData(
"SandboxProfilerEmitterSyscalls",
["id", "init"],
/* withStacks */ true,
/* enabled */ true
);
await Services.profiler.StopProfiler();
});
add_task(async () => {
await ProfilerTestUtils.startProfiler(sandboxSettingsEnabled);
await waitForMaybeSandboxProfilerData(
"SandboxProfilerEmitterLogs",
["log"],
/* withStacks */ false,
/* enabled */ true
);
await Services.profiler.StopProfiler();
});
add_task(async () => {
await ProfilerTestUtils.startProfiler(sandboxSettingsDisabled);
await waitForMaybeSandboxProfilerData(
"SandboxProfilerEmitterSyscalls",
["id", "init"],
/* withStacks */ true,
/* enabled */ false
);
await Services.profiler.StopProfiler();
});
add_task(async () => {
await ProfilerTestUtils.startProfiler(sandboxSettingsEnabled);
await waitForMaybeSandboxProfilerData(
"SandboxProfilerEmitterLogs",
["log"],
/* withStacks */ false,
/* enabled */ false
);
await Services.profiler.StopProfiler();
});

View File

@@ -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 https://mozilla.org/MPL/2.0/. */
"use strict";
function test() {
waitForExplicitFinish();
// Types of processes to test, taken from GeckoProcessTypes.h
// GPU process might not run depending on the platform, so we need it to be
// the last one of the list to allow the remainingTests logic below to work
// as expected.
//
// For UtilityProcess, allow constructing a string made of the process type
// and the sandbox variant we want to test, e.g.,
// utility:0 for GENERIC_UTILITY
// utility:1 for AppleMedia/WMF on macOS/Windows
var processTypes = ["tab", "socket", "rdd", "gmplugin", "utility:0", "gpu"];
const platform = SpecialPowers.Services.appinfo.OS;
if (platform === "WINNT" || platform === "Darwin") {
processTypes.push("utility:1");
}
// A callback called after each test-result.
let sandboxTestResult = (subject, topic, data) => {
let { testid, passed, message } = JSON.parse(data);
ok(
passed,
"Test " + testid + (passed ? " passed: " : " failed: ") + message
);
};
Services.obs.addObserver(sandboxTestResult, "sandbox-test-result");
var remainingTests = processTypes.length;
// A callback that is notified when a child process is done running tests.
let sandboxTestDone = () => {
remainingTests = remainingTests - 1;
if (remainingTests == 0) {
// Clean up test file
if (homeTestFile.exists()) {
ok(homeTestFile.isFile(), "homeTestFile should be a file");
if (homeTestFile.isFile()) {
homeTestFile.remove(false);
}
}
Services.obs.removeObserver(sandboxTestResult, "sandbox-test-result");
Services.obs.removeObserver(sandboxTestDone, "sandbox-test-done");
// Notify SandboxTest component that it should terminate the connection
// with the child processes.
comp.finishTests();
// Notify mochitest that all process tests are complete.
finish();
}
};
Services.obs.addObserver(sandboxTestDone, "sandbox-test-done");
var comp = Cc["@mozilla.org/sandbox/sandbox-test;1"].getService(
Ci.mozISandboxTest
);
let homeTestFile;
try {
homeTestFile = Services.dirsvc.get("Home", Ci.nsIFile);
homeTestFile.append(".mozilla_gpu_sandbox_read_test");
if (!homeTestFile.exists()) {
homeTestFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
}
} catch (e) {
ok(false, "Failed to create home test file: " + e);
}
comp.startTests(processTypes);
}

View File

@@ -0,0 +1,20 @@
# Any copyright is dedicated to the Public Domain.
# http://creativecommons.org/publicdomain/zero/1.0/
[DEFAULT]
skip-if = [
"ccov",
"os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && asan", # bug 1784517
"os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && tsan", # bug 1784517
]
tags = "contentsandbox"
support-files = [
"browser_content_sandbox_utils.js",
"browser_content_sandbox_fs_tests.js",
]
test-directories = "/tmp/.snap_firefox_current_real/"
environment = "SNAP=/tmp/.snap_firefox_current_real/"
["browser_content_sandbox_fs_snap.js"]
run-if = [
"os == 'linux'",
]

View File

@@ -0,0 +1,26 @@
# Any copyright is dedicated to the Public Domain.
# http://creativecommons.org/publicdomain/zero/1.0/
[DEFAULT]
skip-if = [
"ccov",
"os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && asan", # bug 1784517
"os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && tsan", # bug 1784517
]
tags = "contentsandbox"
support-files = [
"browser_content_sandbox_utils.js",
"browser_content_sandbox_fs_tests.js",
]
test-directories = [
"/tmp/.xdg_default_test",
"/tmp/.xdg_default_test/.config/mozilla/firefox/xdg_default_profile",
]
environment = [
"HOME=/tmp/.xdg_default_test",
]
profile-path = "/tmp/.xdg_default_test/.config/mozilla/firefox/xdg_default_profile"
["browser_content_sandbox_fs_xdg_default.js"]
run-if = [
"os == 'linux'"
]

View File

@@ -0,0 +1,29 @@
# Any copyright is dedicated to the Public Domain.
# http://creativecommons.org/publicdomain/zero/1.0/
[DEFAULT]
skip-if = [
"ccov",
"os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && asan", # bug 1784517
"os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && tsan", # bug 1784517
]
tags = "contentsandbox"
support-files = [
"browser_content_sandbox_utils.js",
"browser_content_sandbox_fs_tests.js",
]
test-directories = [
"/tmp/.xdg_mozLegacyHome_test/.config",
"/tmp/.xdg_config_home_test",
"/tmp/.xdg_mozLegacyHome_test/.mozilla/firefox/xdg_mozLegacyHome_profile",
]
environment = [
"XDG_CONFIG_HOME=/tmp/.xdg_config_home_test",
"HOME=/tmp/.xdg_mozLegacyHome_test",
"MOZ_LEGACY_HOME=1",
]
profile-path = "/tmp/.xdg_mozLegacyHome_test/.mozilla/firefox/xdg_mozLegacyHome_profile"
["browser_content_sandbox_fs_xdg_mozLegacyHome.js"]
run-if = [
"os == 'linux'"
]

View File

@@ -0,0 +1,27 @@
# Any copyright is dedicated to the Public Domain.
# http://creativecommons.org/publicdomain/zero/1.0/
[DEFAULT]
skip-if = [
"ccov",
"os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && asan", # bug 1784517
"os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && tsan", # bug 1784517
]
tags = "contentsandbox"
support-files = [
"browser_content_sandbox_utils.js",
"browser_content_sandbox_fs_tests.js",
]
test-directories = [
"/tmp/.xdg_config_home_test",
"/tmp/.xdg_config_home_test/mozilla/firefox/xdg_config_home_profile",
]
environment = [
"XDG_CONFIG_HOME=/tmp/.xdg_config_home_test",
"MOZ_LEGACY_HOME=0",
]
profile-path = "/tmp/.xdg_config_home_test/mozilla/firefox/xdg_config_home_profile"
["browser_content_sandbox_fs_xdg_xdgConfigHome.js"]
run-if = [
"os == 'linux'",
]

View File

@@ -0,0 +1,19 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8"/>
</head>
<style>
#content { display: inline-block; }
.monospace_fallback { font: 3em monospace; }
</style>
<body>
<div id="content" class="monospace_fallback">
abcdefghijklmnopqrstuvwxyz<br>
<b>abcdefghijklmnopqrstuvwxyz</b><br>
<i>abcdefghijklmnopqrstuvwxyz</i>
</div>
</body>
</html>

View File

@@ -0,0 +1,85 @@
#!/usr/bin/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/. */
"""
mac_register_font.py
Mac-specific utility command to register a font file with the OS.
"""
import argparse
import sys
import Cocoa
import CoreText
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="print verbose registration failures",
default=False,
)
parser.add_argument(
"file", nargs="*", help="font file to register or unregister", default=[]
)
parser.add_argument(
"-u",
"--unregister",
action="store_true",
help="unregister the provided fonts",
default=False,
)
parser.add_argument(
"-p",
"--persist-user",
action="store_true",
help="permanently register the font",
default=False,
)
args = parser.parse_args()
if args.persist_user:
scope = CoreText.kCTFontManagerScopeUser
scopeDesc = "user"
else:
scope = CoreText.kCTFontManagerScopeSession
scopeDesc = "session"
failureCount = 0
for fontPath in args.file:
fontURL = Cocoa.NSURL.fileURLWithPath_(fontPath)
(result, error) = register_or_unregister_font(fontURL, args.unregister, scope)
if result:
print(
"%sregistered font %s with %s scope"
% (("un" if args.unregister else ""), fontPath, scopeDesc)
)
else:
print(
"Failed to %sregister font %s with %s scope"
% (("un" if args.unregister else ""), fontPath, scopeDesc)
)
if args.verbose:
print(error)
failureCount += 1
sys.exit(failureCount)
def register_or_unregister_font(fontURL, unregister, scope):
return (
CoreText.CTFontManagerUnregisterFontsForURL(fontURL, scope, None)
if unregister
else CoreText.CTFontManagerRegisterFontsForURL(fontURL, scope, None)
)
if __name__ == "__main__":
main()

View File

@@ -11,12 +11,8 @@ support-files = [
["browser_space_routing_dialog.js"] ["browser_space_routing_dialog.js"]
["browser_space_routing_fuzz.js"]
["browser_space_routing_on_add_tab.js"] ["browser_space_routing_on_add_tab.js"]
["browser_space_routing_redirect_navigation.js"]
["browser_space_routing_route_matching.js"] ["browser_space_routing_route_matching.js"]
["browser_space_routing_route_uri.js"] ["browser_space_routing_route_uri.js"]

View File

@@ -199,7 +199,7 @@ add_task(async function test_routes_are_saved_on_close() {
}; };
try { try {
const closed = promiseRoutingDialogClosed(); const closed = BrowserTestUtils.domWindowClosed(dlg);
dlg.close(); dlg.close();
await TestUtils.waitForCondition( await TestUtils.waitForCondition(
() => saveCalls > 0, () => saveCalls > 0,
@@ -241,15 +241,11 @@ add_task(async function test_open_broadcasts_kill_to_other_instances() {
add_task(async function test_kill_notification_closes_dialog() { add_task(async function test_kill_notification_closes_dialog() {
clearAllRoutes(); clearAllRoutes();
await openRoutingDialog(); const dlg = await openRoutingDialog();
const closed = promiseRoutingDialogClosed(); const closed = BrowserTestUtils.domWindowClosed(dlg);
Services.obs.notifyObservers(null, "zen-space-routing-kill"); Services.obs.notifyObservers(null, "zen-space-routing-kill");
await closed; await closed;
const container = document.getElementById("window-modal-dialog"); ok(dlg.closed, "A 'zen-space-routing-kill' notification closes the dialog");
ok(
!container.open && !container.hasChildNodes(),
"A 'zen-space-routing-kill' notification closes the dialog"
);
}); });

View File

@@ -1,239 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Seeded fuzzing for the pure routing decision functions. The point is not to
// assert a particular routing outcome but to prove robustness invariants under
// adversarial input: the functions must never throw, must always return the
// declared type, and routeUri must only ever return a value it is allowed to.
//
// The RNG is seeded so any failure is reproducible: re-run with the logged seed.
const FUZZ_SEED = 0x5eed1234;
// mulberry32 — small, fast, deterministic PRNG.
function makeRng(seed) {
let s = seed >>> 0;
return function rng() {
s = (s + 0x6d2b79f5) | 0;
let t = Math.imul(s ^ (s >>> 15), 1 | s);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
const DOMAIN_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-.";
const REGEX_CHARS = ".*+?^${}()|[]\\" + DOMAIN_CHARS;
const TRICKY_CHARS =
DOMAIN_CHARS + "%/?#:@!$&'()*+,;= []{}<>\"\\^`|~\tünïçødé日本語🚀";
const SCHEMES = [
"http://",
"https://",
"ftp://",
"file://",
"about:",
"data:text/plain,",
"javascript:",
"//",
"",
];
const MATCH_TYPES = ["contains", "equal-to", "regex", "bogus-type", ""];
function randInt(rng, n) {
return Math.floor(rng() * n);
}
function pick(rng, arr) {
return arr[randInt(rng, arr.length)];
}
function randString(rng, maxLen, charset) {
const len = randInt(rng, maxLen + 1);
let out = "";
for (let i = 0; i < len; i++) {
out += charset[randInt(rng, charset.length)];
}
return out;
}
function randomUrl(rng) {
const scheme = pick(rng, SCHEMES);
const host = randString(rng, 30, DOMAIN_CHARS + "ünïçødé");
const port = rng() < 0.2 ? ":" + randInt(rng, 99999) : "";
const path = rng() < 0.7 ? "/" + randString(rng, 40, TRICKY_CHARS) : "";
return scheme + host + port + path;
}
function randomReference(rng) {
switch (randInt(rng, 5)) {
case 0:
return "";
case 1:
return " ";
case 2:
return randString(rng, 30, DOMAIN_CHARS);
case 3:
// Deliberately regex-flavoured to exercise the "regex" match path.
return randString(rng, 20, REGEX_CHARS);
default:
return randString(rng, 50, TRICKY_CHARS);
}
}
function randomRoute(rng, openIn = "most-recent-space") {
return {
id: "fuzz-" + randInt(rng, 1e9),
reference: randomReference(rng),
openIn,
matchType: pick(rng, MATCH_TYPES),
};
}
add_setup(async function () {
clearAllRoutes();
registerCleanupFunction(() => clearAllRoutes());
info(`Space Routing fuzz seed: 0x${FUZZ_SEED.toString(16)}`);
});
add_task(async function fuzz_isRouteMatching_never_throws() {
const rng = makeRng(FUZZ_SEED);
const ITERATIONS = 5000;
for (let i = 0; i < ITERATIONS; i++) {
const url = randomUrl(rng);
const route = randomRoute(rng);
let result;
try {
result = gZenSpaceRoutingManager.isRouteMatching(url, route);
} catch (e) {
ok(
false,
`isRouteMatching threw on url=${JSON.stringify(
url
)} route=${JSON.stringify(route)}: ${e}`
);
continue;
}
is(
typeof result,
"boolean",
`isRouteMatching must return a boolean (iter ${i})`
);
// An empty / whitespace reference can never match.
if (typeof route.reference !== "string" || route.reference.trim() === "") {
ok(!result, "Empty reference never matches");
}
}
});
add_task(async function fuzz_routeUri_returns_only_valid_destinations() {
const rng = makeRng(FUZZ_SEED ^ 0x1111);
clearAllRoutes();
// Populate the manager with a mix of routes pointing at a few destinations.
const destinations = ["most-recent-space", "ws-a", "ws-b", "ws-c"];
for (let i = 0; i < 200; i++) {
const r = randomRoute(rng, pick(rng, destinations));
addRoute({
reference: r.reference,
openIn: r.openIn,
matchType: r.matchType,
});
}
const allowed = new Set(
gZenSpaceRoutingManager.getAllRoutes().map(r => r.openIn)
);
allowed.add("most-recent-space");
const defaultExternal = gZenSpaceRoutingManager.getDefaultExternalRoute();
allowed.add(defaultExternal);
const ITERATIONS = 4000;
for (let i = 0; i < ITERATIONS; i++) {
const url = randomUrl(rng);
const fromExternal = rng() < 0.5;
let result;
try {
result = gZenSpaceRoutingManager.routeUri(url, { fromExternal });
} catch (e) {
ok(false, `routeUri threw on url=${JSON.stringify(url)}: ${e}`);
continue;
}
is(typeof result, "string", `routeUri must return a string (iter ${i})`);
ok(
allowed.has(result),
`routeUri returned an out-of-set destination: ${JSON.stringify(result)}`
);
}
clearAllRoutes();
});
add_task(async function fuzz_shouldRedirectNavigation_invariants() {
const rng = makeRng(FUZZ_SEED ^ 0x2222);
clearAllRoutes();
const workspaces = [
{ uuid: "ws-a", containerTabId: 1 },
{ uuid: "ws-b", containerTabId: 2 },
];
const win = makeFakeWindow({ workspaces });
for (let i = 0; i < 120; i++) {
const r = randomRoute(
rng,
pick(rng, ["ws-a", "ws-b", "most-recent-space"])
);
addRoute({
reference: r.reference,
openIn: r.openIn,
matchType: r.matchType,
});
}
const ITERATIONS = 4000;
const currentChoices = ["ws-a", "ws-b", "ws-other", "", null];
for (let i = 0; i < ITERATIONS; i++) {
const url = randomUrl(rng);
const currentWorkspaceId = pick(rng, currentChoices);
let result;
try {
result = gZenSpaceRoutingManager.shouldRedirectNavigation(
url,
currentWorkspaceId,
win
);
} catch (e) {
ok(
false,
`shouldRedirectNavigation threw on url=${JSON.stringify(url)}: ${e}`
);
continue;
}
is(typeof result, "boolean", "shouldRedirectNavigation returns a boolean");
if (result) {
// If we decided to redirect, the target must be a real, *different* space.
const target = gZenSpaceRoutingManager.routeUri(url, {
fromExternal: false,
});
ok(
target !== "most-recent-space" && target !== currentWorkspaceId,
`Redirect target must differ from current space (url=${url})`
);
ok(
!!win.gZenWorkspaces.getWorkspaceFromId(target),
"Redirect target must be an existing workspace"
);
}
}
clearAllRoutes();
});

View File

@@ -1,127 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Exercises nsZenSpaceRoutingManager.shouldRedirectNavigation: an in-place
// navigation is only redirected into a new tab when its rule points at a space
// that differs from the one the navigating tab already lives in.
const TARGET_WS = { uuid: "ws-target", containerTabId: 7 };
add_setup(async function () {
clearAllRoutes();
registerCleanupFunction(() => clearAllRoutes());
});
add_task(async function test_redirect_when_route_targets_other_space() {
clearAllRoutes();
addRoute({
reference: "github.com",
matchType: "contains",
openIn: TARGET_WS.uuid,
});
const win = makeFakeWindow({ workspaces: [TARGET_WS] });
ok(
gZenSpaceRoutingManager.shouldRedirectNavigation(
"https://github.com/zen",
"ws-current",
win
),
"Navigating to a routed site from a different space redirects"
);
});
add_task(async function test_no_redirect_when_already_in_target_space() {
clearAllRoutes();
addRoute({
reference: "github.com",
matchType: "contains",
openIn: TARGET_WS.uuid,
});
const win = makeFakeWindow({ workspaces: [TARGET_WS] });
ok(
!gZenSpaceRoutingManager.shouldRedirectNavigation(
"https://github.com/zen",
TARGET_WS.uuid,
win
),
"Already in the destination space navigates in place (and avoids a loop)"
);
});
add_task(async function test_no_redirect_when_no_rule_matches() {
clearAllRoutes();
const win = makeFakeWindow({ workspaces: [TARGET_WS] });
ok(
!gZenSpaceRoutingManager.shouldRedirectNavigation(
"https://example.com",
"ws-current",
win
),
"An unmatched URL is never redirected"
);
});
add_task(async function test_no_redirect_when_rule_targets_most_recent() {
clearAllRoutes();
addRoute({
reference: "github.com",
matchType: "contains",
openIn: "most-recent-space",
});
const win = makeFakeWindow({ workspaces: [TARGET_WS] });
ok(
!gZenSpaceRoutingManager.shouldRedirectNavigation(
"https://github.com",
"ws-current",
win
),
"A rule that opens in the most recent space is not redirected"
);
});
add_task(async function test_no_redirect_when_target_workspace_missing() {
clearAllRoutes();
addRoute({
reference: "github.com",
matchType: "contains",
openIn: "ws-does-not-exist",
});
const win = makeFakeWindow({ workspaces: [TARGET_WS] });
ok(
!gZenSpaceRoutingManager.shouldRedirectNavigation(
"https://github.com",
"ws-current",
win
),
"A rule pointing at a missing workspace is not redirected"
);
});
add_task(async function test_no_redirect_when_workspaces_disabled() {
clearAllRoutes();
addRoute({
reference: "github.com",
matchType: "contains",
openIn: TARGET_WS.uuid,
});
const win = makeFakeWindow({
workspaces: [TARGET_WS],
workspaceEnabled: false,
});
ok(
!gZenSpaceRoutingManager.shouldRedirectNavigation(
"https://github.com",
"ws-current",
win
),
"Nothing is redirected when workspaces are disabled"
);
});

View File

@@ -29,15 +29,10 @@ function addRoute({
return route; return route;
} }
function makeFakeWindow({ function makeFakeWindow({ ready = true, workspaces = [] } = {}) {
ready = true,
workspaces = [],
workspaceEnabled = true,
} = {}) {
return { return {
gZenStartup: { isReady: ready }, gZenStartup: { isReady: ready },
gZenWorkspaces: { gZenWorkspaces: {
workspaceEnabled,
moveCalls: [], moveCalls: [],
changeCalls: [], changeCalls: [],
lastSelectedWorkspaceTabs: {}, lastSelectedWorkspaceTabs: {},
@@ -62,17 +57,12 @@ async function flushEventLoop() {
} }
async function openRoutingDialog() { async function openRoutingDialog() {
// openSpaceRoutingDialog() presents an in-window modal through gDialogBox, so const dialogPromise = BrowserTestUtils.domWindowOpenedAndLoaded(null, win =>
// the dialog is a subdialog rather than a separate top-level window. win.document?.documentURI?.includes("zen-space-routing.xhtml")
const dialogPromise = BrowserTestUtils.promiseAlertDialogOpen(
null,
SR_DIALOG_URI,
{ isSubDialog: true }
); );
// gDialogBox.open() only resolves once the dialog is dismissed, so kick it off
// without awaiting and wait on the open notification instead.
executeSoon(() => gZenSpaceRoutingManager.openSpaceRoutingDialog(window)); executeSoon(() => gZenSpaceRoutingManager.openSpaceRoutingDialog(window));
const dialogWin = await dialogPromise; const dialogWin = await dialogPromise;
await SimpleTest.promiseFocus(dialogWin);
await TestUtils.waitForCondition( await TestUtils.waitForCondition(
() => dialogWin.spaceroutingDialog?.initialized, () => dialogWin.spaceroutingDialog?.initialized,
"Space Routing dialog finished initializing" "Space Routing dialog finished initializing"
@@ -80,23 +70,11 @@ async function openRoutingDialog() {
return dialogWin; return dialogWin;
} }
// Resolves once the gDialogBox subdialog has fully torn down. Use this instead
// of BrowserTestUtils.domWindowClosed(), which only fires for separate
// top-level windows and so never resolves for an in-window subdialog.
function promiseRoutingDialogClosed() {
const container = document.getElementById("window-modal-dialog");
if (!container?.open) {
return Promise.resolve();
}
return BrowserTestUtils.waitForMutationCondition(
container,
{ childList: true, attributes: true },
() => !container.hasChildNodes() && !container.open
);
}
async function closeRoutingDialog(dialogWin) { async function closeRoutingDialog(dialogWin) {
const closed = promiseRoutingDialogClosed(); if (dialogWin.closed) {
return;
}
const closed = BrowserTestUtils.domWindowClosed(dialogWin);
dialogWin.close(); dialogWin.close();
await closed; await closed;
} }

View File

@@ -26,8 +26,6 @@ support-files = [
["browser_private_mode_startup.js"] ["browser_private_mode_startup.js"]
["browser_select_tab_switches_space.js"]
["browser_unload_all_other_spaces.js"] ["browser_unload_all_other_spaces.js"]
["browser_workspace_bookmarks.js"] ["browser_workspace_bookmarks.js"]

View File

@@ -1,69 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
function fakeTab(workspaceId) {
return {
getAttribute(name) {
return name === "zen-workspace-id" ? workspaceId : null;
},
};
}
function withRecordedSwitch(fn) {
const calls = [];
gZenWorkspaces.changeWorkspaceWithID = id => {
calls.push(id);
};
try {
fn(calls);
} finally {
// Remove the own property so the prototype method shows through again.
delete gZenWorkspaces.changeWorkspaceWithID;
}
}
add_task(function test_switches_when_tab_in_other_space() {
withRecordedSwitch(calls => {
const otherSpace = gZenWorkspaces.activeWorkspace + "-different";
gZenWorkspaces.onBeforeTabSelect(fakeTab(otherSpace));
Assert.deepEqual(
calls,
[otherSpace],
"Selecting a tab from another space switches to that space"
);
});
});
add_task(function test_no_switch_when_tab_in_active_space() {
withRecordedSwitch(calls => {
const active = gZenWorkspaces.activeWorkspace;
Assert.ok(active, "Test relies on a non-empty active workspace");
gZenWorkspaces.onBeforeTabSelect(fakeTab(active));
Assert.deepEqual(
calls,
[],
"Selecting a tab already in the active space does not switch"
);
});
});
add_task(function test_no_switch_when_tab_has_no_space() {
withRecordedSwitch(calls => {
gZenWorkspaces.onBeforeTabSelect(fakeTab(null));
Assert.deepEqual(
calls,
[],
"A tab with no zen-workspace-id does not switch spaces"
);
});
});
add_task(function test_handles_missing_tab() {
withRecordedSwitch(calls => {
gZenWorkspaces.onBeforeTabSelect(null);
gZenWorkspaces.onBeforeTabSelect(undefined);
Assert.deepEqual(calls, [], "A missing tab is ignored without throwing");
});
});

View File

@@ -142,9 +142,7 @@ export class nsZenSiteDataPanel {
this.anchor.removeAttribute("boosting"); this.anchor.removeAttribute("boosting");
} }
// Force a reflow to ensure the attribute change is applied before any potential animation. // Force a reflow to ensure the attribute change is applied before any potential animation.
if (this.unifiedPanel.state === "open") { this.anchor.getBoundingClientRect();
this.anchor.getBoundingClientRect();
}
} }
#initCopyUrlButton() { #initCopyUrlButton() {
@@ -889,8 +887,8 @@ export class nsZenSiteDataPanel {
domain, domain,
boostId boostId
); );
lazy.gZenBoostsManager.openBoostWindow(this.window, boost, uri);
this.unifiedPanel.hidePopup(); this.unifiedPanel.hidePopup();
lazy.gZenBoostsManager.openBoostWindow(this.window, boost, uri);
break; break;
} }
} }

View File

@@ -5,8 +5,8 @@
"binaryName": "zen", "binaryName": "zen",
"version": { "version": {
"product": "firefox", "product": "firefox",
"version": "151.0.4", "version": "151.0.3",
"candidate": "151.0.4", "candidate": "151.0.3",
"candidateBuild": 1 "candidateBuild": 1
}, },
"buildOptions": { "buildOptions": {
@@ -20,7 +20,7 @@
"brandShortName": "Zen", "brandShortName": "Zen",
"brandFullName": "Zen Browser", "brandFullName": "Zen Browser",
"release": { "release": {
"displayVersion": "1.21.1b", "displayVersion": "1.20.2b",
"github": { "github": {
"repo": "zen-browser/desktop" "repo": "zen-browser/desktop"
}, },
@@ -40,7 +40,7 @@
"brandShortName": "Twilight", "brandShortName": "Twilight",
"brandFullName": "Zen Twilight", "brandFullName": "Zen Twilight",
"release": { "release": {
"displayVersion": "1.22t", "displayVersion": "1.21t",
"github": { "github": {
"repo": "zen-browser/desktop" "repo": "zen-browser/desktop"
} }
@@ -54,4 +54,4 @@
"licenseType": "MPL-2.0" "licenseType": "MPL-2.0"
}, },
"updateHostname": "updates.zen-browser.app" "updateHostname": "updates.zen-browser.app"
} }