Compare commits

..

2 Commits

Author SHA1 Message Date
mr. m
b7a8e79299 no-bug: Apply squircles to elements 2026-06-06 14:08:36 +02:00
mr. m
0048f21a52 no-bug: Squircles support 2026-06-04 01:37:46 +02:00
93 changed files with 8310 additions and 3339 deletions

View File

@@ -60,7 +60,7 @@ jobs:
brew install watchman
cargo install apple-codesign --locked --force
cargo install apple-codesign
- name: Force usage of gnu-tar
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
- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `151.0.4`! 🚀
- [`Twilight`](https://zen-browser.app/download?twilight) - Is currently built using Firefox version `RC 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.3`!
### Contributing

View File

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

View File

@@ -22,5 +22,3 @@ files:
translation: browser/browser/zen-folders.ftl
- source: en-US/browser/browser/zen-boosts.ftl
translation: browser/browser/zen-boosts.ftl
- source: en-US/browser/browser/zen-space-routing.ftl
translation: browser/browser/zen-space-routing.ftl

View File

@@ -1,25 +0,0 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
zen-space-routing-settings =
.label = Space Routing Settings
zen-space-routing-rulepanel-placeholder = Routes let you choose where specific sites open inside Zen. For example, you can route YouTube links to always open inside your Personal space.
zen-space-routing-dialog-title = Space Routing Settings
zen-space-routing-external-default = Default route for external links
zen-space-routing-new-route = New Route
zen-space-routing-open-in-space = Open in Space
zen-space-routing-most-recent-space = Most recent Space
zen-space-routing-close-button =
.aria-label = Close
.tooltiptext = Close
zen-space-routing-contains =
.label = Contains
zen-space-routing-equal-to =
.label = Is Equal To
zen-space-routing-regex =
.label = RegEx
zen-space-routing-open-in = Open In
zen-space-routing-url = URL

View File

@@ -95,7 +95,5 @@
- name: browser.search.widget.new
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
- name: layout.css.corner-shape.enabled
value: true

View File

@@ -20,4 +20,3 @@
#include ../../../zen/fonts/jar.inc.mn
#include ../../../zen/boosts/jar.inc.mn
#include ../../../zen/live-folders/jar.inc.mn
#include ../../../zen/space-routing/jar.inc.mn

View File

@@ -41,7 +41,6 @@
<command id="cmd_zenCtxDeleteWorkspace" />
<command id="cmd_zenUnloadWorkspace" />
<command id="cmd_zenUnloadAllOtherWorkspace" />
<command id="cmd_zenOpenSpaceRoutingSettings" />
<command id="cmd_zenChangeWorkspaceName" />
<command id="cmd_zenChangeWorkspaceIcon" />
<command id="cmd_zenReorderWorkspaces" />

View File

@@ -11,5 +11,4 @@
<link rel="localization" href="browser/zen-folders.ftl"/>
<link rel="localization" href="browser/zen-boosts.ftl"/>
<link rel="localization" href="browser/zen-live-folders.ftl"/>
<link rel="localization" href="browser/zen-space-routing.ftl"/>
</linkset>

View File

@@ -33,8 +33,6 @@
<menuseparator/>
<menuitem data-l10n-id="zen-panel-ui-workspaces-create" command="cmd_zenOpenWorkspaceCreation"/>
<menuitem id="context_zenDeleteWorkspace" data-l10n-id="zen-workspaces-panel-context-delete" command="cmd_zenCtxDeleteWorkspace"/>
<menuseparator/>
<menuitem id="context_zenSpaceRoutingSettings" data-l10n-id="zen-space-routing-settings" command="cmd_zenOpenSpaceRoutingSettings"/>
</menupopup>
<menupopup id="zenFolderActions">

View File

@@ -131,10 +131,10 @@
</box>
<html:input type="range" value="0.4" step="0.001" id="PanelUI-zen-gradient-generator-opacity"
#ifdef XP_MACOSX
max="0.9"
max="0.8"
min="0.30"
#else
max="0.9"
max="0.8"
min="0.25"
#endif
/>

View File

@@ -945,7 +945,6 @@ var gZenCKSSettings = {
});
input.addEventListener("blur", (event) => {
this._currentActionID = null;
const target = event.target;
target.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-editing`);
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}-editing`);
this._latestValidKey = null;
this._currentActionID = null;
return;
} else if (shortcut == "Escape" && !modifiersActive) {
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
index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008ede72076 100644
index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88f4eec282 100644
--- a/browser/components/tabbrowser/content/tabbrowser.js
+++ b/browser/components/tabbrowser/content/tabbrowser.js
@@ -502,6 +502,7 @@
@@ -79,17 +79,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
set selectedTab(val) {
if (
gSharedTabWarning.willShowSharedTabWarning(val) ||
@@ -592,6 +644,9 @@
) {
return;
}
+ if (gZenWorkspaces.onBeforeTabSelect(val)) {
+ return;
+ }
// Update the tab
this.tabbox.selectedTab = val;
}
@@ -659,6 +714,10 @@
@@ -659,6 +711,10 @@
userContextId = parseInt(tabArgument.getAttribute("usercontextid"), 10);
}
@@ -100,7 +90,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if (tabArgument && tabArgument.linkedBrowser) {
remoteType = tabArgument.linkedBrowser.remoteType;
initialBrowsingContextGroupId =
@@ -751,6 +810,8 @@
@@ -751,6 +807,8 @@
this.tabpanels.appendChild(panel);
let tab = this.tabs[0];
@@ -109,7 +99,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
tab.linkedPanel = uniqueId;
this._selectedTab = tab;
this._selectedBrowser = browser;
@@ -1121,13 +1182,18 @@
@@ -1121,13 +1179,18 @@
}
this.showTab(aTab);
@@ -129,7 +119,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
aTab.setAttribute("pinned", "true");
this._updateTabBarForPinnedTabs();
@@ -1140,11 +1206,19 @@
@@ -1140,11 +1203,19 @@
}
this.#handleTabMove(aTab, () => {
@@ -150,7 +140,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
});
aTab.style.marginInlineStart = "";
@@ -1321,6 +1395,9 @@
@@ -1321,6 +1392,9 @@
let LOCAL_PROTOCOLS = ["chrome:", "about:", "resource:", "data:"];
@@ -160,7 +150,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if (
aIconURL &&
!LOCAL_PROTOCOLS.some(protocol => aIconURL.startsWith(protocol))
@@ -1330,6 +1407,9 @@
@@ -1330,6 +1404,9 @@
);
return;
}
@@ -170,7 +160,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
let browser = this.getBrowserForTab(aTab);
browser.mIconURL = aIconURL;
@@ -1652,7 +1732,6 @@
@@ -1652,7 +1729,6 @@
// Preview mode should not reset the owner
if (!this._previewMode && !oldTab.selected) {
@@ -178,7 +168,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
}
let lastRelatedTab = this._lastRelatedTabMap.get(oldTab);
@@ -1743,6 +1822,7 @@
@@ -1743,6 +1819,7 @@
if (!this._previewMode) {
newTab.recordTimeFromUnloadToReload();
newTab.updateLastAccessed();
@@ -186,7 +176,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
oldTab.updateLastAccessed();
// if this is the foreground window, update the last-seen timestamps.
if (this.ownerGlobal == BrowserWindowTracker.getTopWindow()) {
@@ -1957,6 +2037,9 @@
@@ -1957,6 +2034,9 @@
}
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 (activeEl == oldTab) {
newTab.focus();
@@ -1995,7 +2078,7 @@
@@ -1995,7 +2075,7 @@
// Focus the location bar if it was previously focused for that tab.
// In full screen mode, only bother making the location bar visible
// if the tab is a blank one.
@@ -205,7 +195,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
let selectURL = () => {
if (this._asyncTabSwitching) {
// Set _awaitingSetURI flag to suppress popup notification
@@ -2283,7 +2366,12 @@
@@ -2283,7 +2363,12 @@
return this._setTabLabel(aTab, aLabel);
}
@@ -219,7 +209,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if (!aLabel || (isURL && /^about:reader\?url=/.test(aLabel))) {
return false;
}
@@ -2408,7 +2496,7 @@
@@ -2408,7 +2493,7 @@
newIndex = this.selectedTab._tPos + 1;
}
@@ -228,7 +218,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if (this.isTabGroupLabel(targetTab)) {
throw new Error(
"Replacing a tab group label with a tab is not supported"
@@ -2685,6 +2773,7 @@
@@ -2685,6 +2770,7 @@
uriIsAboutBlank,
userContextId,
skipLoad,
@@ -236,7 +226,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
} = {}) {
let b = document.createXULElement("browser");
// 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?
b.setAttribute("name", name);
}
@@ -246,7 +236,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
b.setAttribute("transparent", "true");
}
@@ -2929,7 +3017,7 @@
@@ -2929,7 +3014,7 @@
let panel = this.getPanel(browser);
let uniqueId = this._generateUniquePanelID();
@@ -255,7 +245,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
aTab.linkedPanel = uniqueId;
// 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
// hasSiblings=false on both the existing browser and the new browser.
if (this.tabs.length == 2) {
@@ -266,7 +256,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
} else {
aTab.linkedBrowser.browsingContext.hasSiblings = this.tabs.length > 1;
}
@@ -3175,7 +3263,6 @@
@@ -3175,7 +3260,6 @@
this.selectedTab = this.addTrustedTab(BROWSER_NEW_TAB_URL, {
tabIndex: tab._tPos + 1,
userContextId: tab.userContextId,
@@ -274,32 +264,23 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
focusUrlBar: true,
});
resolve(this.selectedBrowser);
@@ -3285,6 +3372,10 @@
@@ -3285,6 +3369,9 @@
schemelessInput,
hasValidUserGestureActivation = false,
textDirectiveUserActivation = false,
+ _forZenEmptyTab,
+ essential,
+ zenWorkspaceId,
+ skipRoute = false,
} = {}
) {
// all callers of addTab that pass a params object need to pass
@@ -3295,10 +3386,25 @@
@@ -3295,10 +3382,17 @@
);
}
+ const beforeRouteResult = window.gZenSpaceRoutingManager.onBeforeAddTab(uriString, { skipRoute, pinned, tabGroup, fromExternal }, window);
+ if (beforeRouteResult.shouldEarlyExit) {
+ return null;
+ }
+
+ let hasZenDefaultUserContextId = false;
+ let zenForcedWorkspaceId = undefined;
+ if (beforeRouteResult.isRouteFound) {
+ userContextId = beforeRouteResult.userContextId;
+ hasZenDefaultUserContextId = true;
+ } else if (typeof gZenWorkspaces !== "undefined" && !_forZenEmptyTab) {
+ if (typeof gZenWorkspaces !== "undefined" && !_forZenEmptyTab) {
+ [userContextId, hasZenDefaultUserContextId, zenForcedWorkspaceId] = gZenWorkspaces.getContextIdIfNeeded(userContextId, fromExternal, triggeringPrincipal);
+ }
+
@@ -311,7 +292,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
// If we're opening a foreground tab, set the owner by default.
ownerTab ??= inBackground ? null : this.selectedTab;
@@ -3306,6 +3412,7 @@
@@ -3306,6 +3400,7 @@
if (this.selectedTab.owner) {
this.selectedTab.owner = null;
}
@@ -319,7 +300,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
// Find the tab that opened this one, if any. This is used for
// determining positioning, and inherited attributes such as the
@@ -3358,6 +3465,22 @@
@@ -3358,6 +3453,22 @@
noInitialLabel,
skipBackgroundNotify,
});
@@ -342,7 +323,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if (insertTab) {
// Insert the tab into the tab container in the correct position.
this.#insertTabAtIndex(t, {
@@ -3366,6 +3489,7 @@
@@ -3366,6 +3477,7 @@
ownerTab,
openerTab,
pinned,
@@ -350,7 +331,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
bulkOrderedOpen,
tabGroup: tabGroup ?? openerTab?.group,
});
@@ -3384,6 +3508,7 @@
@@ -3384,6 +3496,7 @@
openWindowInfo,
skipLoad,
triggeringRemoteType,
@@ -358,7 +339,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
}));
if (focusUrlBar) {
@@ -3508,6 +3633,12 @@
@@ -3508,6 +3621,12 @@
}
}
@@ -371,17 +352,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
// Additionally send pinned tab events
if (pinned) {
this.#notifyPinnedStatus(t);
@@ -3518,6 +3649,9 @@
if (!inBackground) {
this.selectedTab = t;
}
+
+ window.gZenSpaceRoutingManager.onAfterAddTab(uriString, t, { skipRoute: skipRoute || _forZenEmptyTab, fromExternal, pinned, tabGroup }, window, beforeRouteResult);
+
return t;
}
@@ -3750,6 +3884,7 @@
@@ -3750,6 +3869,7 @@
isAdoptingGroup = false,
isUserTriggered = false,
telemetryUserCreateSource = "unknown",
@@ -389,7 +360,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
} = {}
) {
if (
@@ -3760,9 +3895,6 @@
@@ -3760,9 +3880,6 @@
!this.isSplitViewWrapper(tabOrSplitView)
)
) {
@@ -399,7 +370,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
}
if (!color) {
@@ -3783,9 +3915,14 @@
@@ -3783,9 +3900,14 @@
label,
isAdoptingGroup
);
@@ -416,7 +387,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
);
group.addTabs(tabsAndSplitViews);
@@ -3906,7 +4043,7 @@
@@ -3906,7 +4028,7 @@
}
this.#handleTabMove(tab, () =>
@@ -425,7 +396,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
);
}
@@ -3990,6 +4127,7 @@
@@ -3990,6 +4112,7 @@
color: group.color,
insertBefore: newTabs[0],
isAdoptingGroup: true,
@@ -433,7 +404,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
});
}
@@ -4200,6 +4338,7 @@
@@ -4200,6 +4323,7 @@
openWindowInfo,
skipLoad,
triggeringRemoteType,
@@ -441,7 +412,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
}
) {
// If we don't have a preferred remote type (or it is `NOT_REMOTE`), and
@@ -4269,6 +4408,7 @@
@@ -4269,6 +4393,7 @@
openWindowInfo,
name,
skipLoad,
@@ -449,7 +420,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
});
}
@@ -4482,9 +4622,9 @@
@@ -4482,9 +4607,9 @@
}
// Add a new tab if needed.
@@ -461,7 +432,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
let url = "about:blank";
if (tabData.entries?.length) {
@@ -4521,8 +4661,10 @@
@@ -4521,8 +4646,10 @@
insertTab: false,
skipLoad: true,
preferredRemoteType,
@@ -473,7 +444,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if (select) {
tabToSelect = tab;
}
@@ -4544,7 +4686,8 @@
@@ -4544,7 +4671,8 @@
this.pinTab(tab);
// Then ensure all the tab open/pinning information is sent.
this._fireTabOpen(tab, {});
@@ -483,7 +454,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
let { groupId } = tabData;
const tabGroup = tabGroupWorkingData.get(groupId);
// if a tab refers to a tab group we don't know, skip any group
@@ -4564,7 +4707,10 @@
@@ -4564,7 +4692,10 @@
tabGroup.stateData.id,
tabGroup.stateData.color,
tabGroup.stateData.collapsed,
@@ -495,7 +466,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
);
tabsFragment.appendChild(tabGroup.node);
}
@@ -4619,9 +4765,21 @@
@@ -4619,9 +4750,21 @@
// to remove the old selected tab.
if (tabToSelect) {
let leftoverTab = this.selectedTab;
@@ -517,7 +488,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if (tabs.length > 1 || !tabs[0].selected) {
this._updateTabsAfterInsert();
@@ -4812,11 +4970,14 @@
@@ -4812,11 +4955,14 @@
if (ownerTab) {
tab.owner = ownerTab;
}
@@ -533,7 +504,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if (
!bulkOrderedOpen &&
((openerTab &&
@@ -4828,7 +4989,7 @@
@@ -4828,7 +4974,7 @@
let lastRelatedTab =
openerTab && this._lastRelatedTabMap.get(openerTab);
let previousTab = lastRelatedTab || openerTab || this.selectedTab;
@@ -542,7 +513,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
tabGroup = previousTab.group;
}
if (
@@ -4844,7 +5005,7 @@
@@ -4844,7 +4990,7 @@
previousTab.splitview
) + 1;
} else if (previousTab.visible) {
@@ -551,7 +522,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
} else if (previousTab == FirefoxViewHandler.tab) {
elementIndex = 0;
}
@@ -4872,14 +5033,14 @@
@@ -4872,14 +5018,14 @@
}
// Ensure index is within bounds.
if (tab.pinned) {
@@ -570,7 +541,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if (pinned && !itemAfter?.pinned) {
itemAfter = null;
@@ -4896,7 +5057,7 @@
@@ -4896,7 +5042,7 @@
this.tabContainer._invalidateCachedTabs();
@@ -579,7 +550,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if (
(this.isTab(itemAfter) && itemAfter.group == tabGroup) ||
this.isSplitViewWrapper(itemAfter)
@@ -4927,7 +5088,11 @@
@@ -4927,7 +5073,11 @@
const tabContainer = pinned
? this.tabContainer.pinnedTabsContainer
: this.tabContainer;
@@ -591,7 +562,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
}
if (tab.group?.collapsed) {
@@ -4942,6 +5107,7 @@
@@ -4942,6 +5092,7 @@
if (pinned) {
this._updateTabBarForPinnedTabs();
}
@@ -599,7 +570,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
TabBarVisibility.update();
}
@@ -5490,6 +5656,7 @@
@@ -5490,6 +5641,7 @@
telemetrySource,
} = {}
) {
@@ -607,7 +578,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
// When 'closeWindowWithLastTab' pref is enabled, closing all tabs
// can be considered equivalent to closing the window.
if (
@@ -5579,6 +5746,7 @@
@@ -5579,6 +5731,7 @@
if (lastToClose) {
this.removeTab(lastToClose, aParams);
}
@@ -615,7 +586,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
} catch (e) {
console.error(e);
}
@@ -5624,6 +5792,14 @@
@@ -5624,6 +5777,14 @@
return;
}
@@ -630,7 +601,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
let isVisibleTab = aTab.visible;
// 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
@@ -5631,6 +5807,9 @@
@@ -5631,6 +5792,9 @@
// state).
let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width;
let isLastTab = this.#isLastTabInWindow(aTab);
@@ -640,7 +611,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if (
!this._beginRemoveTab(aTab, {
closeWindowFastpath: true,
@@ -5642,13 +5821,14 @@
@@ -5642,13 +5806,14 @@
telemetrySource,
})
) {
@@ -656,7 +627,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
let lockTabSizing =
!this.tabContainer.verticalMode &&
!aTab.pinned &&
@@ -5679,7 +5859,13 @@
@@ -5679,7 +5844,13 @@
// We're not animating, so we can cancel the animation stopwatch.
Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId);
aTab._closeTimeAnimTimerId = null;
@@ -671,7 +642,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
return;
}
@@ -5813,7 +5999,7 @@
@@ -5813,7 +5984,7 @@
closeWindowWithLastTab != null
? closeWindowWithLastTab
: !window.toolbar.visible ||
@@ -680,7 +651,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if (closeWindow) {
// We've already called beforeunload on all the relevant tabs if we get here,
@@ -5837,6 +6023,7 @@
@@ -5837,6 +6008,7 @@
newTab = true;
}
@@ -688,7 +659,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
aTab._endRemoveArgs = [closeWindow, newTab];
// swapBrowsersAndCloseOther will take care of closing the window without animation.
@@ -5877,13 +6064,7 @@
@@ -5877,13 +6049,7 @@
aTab._mouseleave();
if (newTab) {
@@ -703,7 +674,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
} else {
TabBarVisibility.update();
}
@@ -6016,6 +6197,7 @@
@@ -6016,6 +6182,7 @@
this.tabs[i]._tPos = i;
}
@@ -711,7 +682,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if (!this._windowIsClosing) {
// update tab close buttons state
this.tabContainer._updateCloseButtons();
@@ -6201,6 +6383,7 @@
@@ -6201,6 +6368,7 @@
memory_after: await getTotalMemoryUsage(),
time_to_unload_in_ms: timeElapsed,
});
@@ -719,7 +690,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
}
/**
@@ -6246,6 +6429,7 @@
@@ -6246,6 +6414,7 @@
}
let excludeTabs = new Set(aExcludeTabs);
@@ -727,7 +698,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
// If this tab has a successor, it should be selectable, since
// hiding or closing a tab removes that tab as a successor.
@@ -6258,15 +6442,22 @@
@@ -6258,15 +6427,22 @@
!excludeTabs.has(aTab.owner) &&
Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")
) {
@@ -752,7 +723,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
let tab = this.tabContainer.findNextTab(aTab, {
direction: 1,
filter: _tab => remainingTabs.includes(_tab),
@@ -6280,7 +6471,7 @@
@@ -6280,7 +6456,7 @@
}
if (tab) {
@@ -761,7 +732,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
}
// If no qualifying visible tab was found, see if there is a tab in
@@ -6301,7 +6492,7 @@
@@ -6301,7 +6477,7 @@
});
}
@@ -770,7 +741,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
}
_blurTab(aTab) {
@@ -6312,7 +6503,7 @@
@@ -6312,7 +6488,7 @@
* @returns {boolean}
* False if swapping isn't permitted, true otherwise.
*/
@@ -779,7 +750,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
// Do not allow transfering a private tab to a non-private window
// and vice versa.
if (
@@ -6366,6 +6557,7 @@
@@ -6366,6 +6542,7 @@
// fire the beforeunload event in the process. Close the other
// window if this was its last tab.
if (
@@ -787,7 +758,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
!remoteBrowser._beginRemoveTab(aOtherTab, {
adoptedByTab: aOurTab,
closeWindowWithLastTab: true,
@@ -6377,7 +6569,7 @@
@@ -6377,7 +6554,7 @@
// If this is the last tab of the window, hide the window
// immediately without animation before the docshell swap, to avoid
// about:blank being painted.
@@ -796,7 +767,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if (closeWindow) {
let win = aOtherTab.ownerGlobal;
win.windowUtils.suppressAnimation(true);
@@ -6511,11 +6703,13 @@
@@ -6511,11 +6688,13 @@
}
// Finish tearing down the tab that's going away.
@@ -810,7 +781,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
this.setTabTitle(aOurTab);
@@ -6717,10 +6911,10 @@
@@ -6717,10 +6896,10 @@
SessionStore.deleteCustomTabValue(aTab, "hiddenBy");
}
@@ -823,7 +794,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
aTab.selected ||
aTab.closing ||
// Tabs that are sharing the screen, microphone or camera cannot be hidden.
@@ -6780,7 +6974,8 @@
@@ -6780,7 +6959,8 @@
* @param {object} [aOptions={}]
* Key-value pairs that will be serialized into the features string.
*/
@@ -833,7 +804,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if (this.tabs.length == 1) {
return null;
}
@@ -6797,7 +6992,7 @@
@@ -6797,7 +6977,7 @@
// tell a new window to take the "dropped" tab
let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
args.appendElement(aTab.splitview ?? aTab);
@@ -842,7 +813,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
private: PrivateBrowsingUtils.isWindowPrivate(window),
features: Object.entries(aOptions)
.map(([key, value]) => `${key}=${value}`)
@@ -6805,6 +7000,8 @@
@@ -6805,6 +6985,8 @@
openerWindow: window,
args,
});
@@ -851,7 +822,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
}
/**
@@ -6917,7 +7114,7 @@
@@ -6917,7 +7099,7 @@
* `true` if element is a `<tab-group>`
*/
isTabGroup(element) {
@@ -860,7 +831,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
}
/**
@@ -7002,8 +7199,8 @@
@@ -7002,8 +7184,8 @@
}
// Don't allow mixing pinned and unpinned tabs.
@@ -871,7 +842,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
} else {
tabIndex = Math.max(tabIndex, this.pinnedTabCount);
}
@@ -7049,8 +7246,8 @@
@@ -7049,8 +7231,8 @@
this.#handleTabMove(
element,
() => {
@@ -882,7 +853,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
neighbor = neighbor.group;
}
if (neighbor?.splitview) {
@@ -7061,6 +7258,12 @@
@@ -7061,6 +7243,12 @@
return;
}
}
@@ -895,7 +866,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
if (movingForwards && neighbor) {
neighbor.after(element);
@@ -7119,23 +7322,31 @@
@@ -7119,23 +7307,31 @@
#moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) {
if (this.isTabGroupLabel(targetElement)) {
targetElement = targetElement.group;
@@ -933,7 +904,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
} else if (!element.pinned && targetElement && targetElement.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
@@ -7148,12 +7359,35 @@
@@ -7148,12 +7344,35 @@
// move the tab group right before the first unpinned tab.
// 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.
@@ -970,7 +941,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
// 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.
@@ -7162,6 +7396,7 @@
@@ -7162,6 +7381,7 @@
}
let getContainer = () =>
@@ -978,7 +949,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
element.pinned
? this.tabContainer.pinnedTabsContainer
: this.tabContainer;
@@ -7170,11 +7405,15 @@
@@ -7170,11 +7390,15 @@
element,
() => {
if (moveBefore) {
@@ -995,7 +966,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
}
},
metricsContext
@@ -7248,11 +7487,15 @@
@@ -7248,11 +7472,15 @@
* @param {TabMetricsContext} [metricsContext]
*/
moveTabToExistingGroup(aTab, aGroup, metricsContext) {
@@ -1014,7 +985,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
}
if (aTab.group && aTab.group.id === aGroup.id) {
return;
@@ -7324,6 +7567,7 @@
@@ -7324,6 +7552,7 @@
let state = {
tabIndex: tab._tPos,
@@ -1022,7 +993,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
};
if (tab.visible) {
state.elementIndex = tab.elementIndex;
@@ -7355,7 +7599,7 @@
@@ -7355,7 +7584,7 @@
let changedSplitView =
previousTabState.splitViewId != currentTabState.splitViewId;
@@ -1031,7 +1002,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
tab.dispatchEvent(
new CustomEvent("TabMove", {
bubbles: true,
@@ -7402,6 +7646,10 @@
@@ -7402,6 +7631,10 @@
moveActionCallback();
@@ -1042,7 +1013,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
// Clear tabs cache after moving nodes because the order of tabs may have
// changed.
this.tabContainer._invalidateCachedTabs();
@@ -7452,7 +7700,22 @@
@@ -7452,7 +7685,22 @@
* @returns {object}
* The new tab in the current window, null if the tab couldn't be adopted.
*/
@@ -1066,7 +1037,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
// 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
// windows). We also ensure that the tab we create to swap into has
@@ -7495,6 +7758,8 @@
@@ -7495,6 +7743,8 @@
}
params.skipLoad = true;
let newTab = this.addWebTab("about:blank", params);
@@ -1075,7 +1046,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
aTab.container.tabDragAndDrop.finishAnimateTabMove();
@@ -8205,7 +8470,7 @@
@@ -8205,7 +8455,7 @@
// preventDefault(). It will still raise the window if appropriate.
return;
}
@@ -1084,7 +1055,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
window.focus();
aEvent.preventDefault();
}
@@ -8222,7 +8487,6 @@
@@ -8222,7 +8472,6 @@
on_TabGroupCollapse(aEvent) {
aEvent.target.tabs.forEach(tab => {
@@ -1092,7 +1063,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
});
}
@@ -8556,7 +8820,9 @@
@@ -8556,7 +8805,9 @@
let filter = this._tabFilters.get(tab);
if (filter) {
@@ -1102,7 +1073,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
let listener = this._tabListeners.get(tab);
if (listener) {
@@ -9359,6 +9625,7 @@
@@ -9359,6 +9610,7 @@
aWebProgress.isTopLevel
) {
this.mTab.setAttribute("busy", "true");
@@ -1110,7 +1081,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
gBrowser._tabAttrModified(this.mTab, ["busy"]);
this.mTab._notselectedsinceload = !this.mTab.selected;
}
@@ -9439,6 +9706,7 @@
@@ -9439,6 +9691,7 @@
// known defaults. Note we use the original URL since about:newtab
// redirects to a prerendered page.
const shouldRemoveFavicon =
@@ -1118,7 +1089,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
!this.mBrowser.mIconURL &&
!ignoreBlank &&
!(originalLocation.spec in FAVICON_DEFAULTS);
@@ -9613,13 +9881,6 @@
@@ -9613,13 +9866,6 @@
this.mBrowser.originalURI = aRequest.originalURI;
}
@@ -1132,7 +1103,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
}
let userContextId = this.mBrowser.getAttribute("usercontextid") || 0;
@@ -10507,7 +10768,8 @@ var TabContextMenu = {
@@ -10507,7 +10753,8 @@ var TabContextMenu = {
);
contextUnpinSelectedTabs.hidden =
!this.contextTab.pinned || !this.multiselected;

View File

@@ -51,8 +51,7 @@
}
#PanelUI-zen-gradient-generator-color-remove,
#zen-gradient-generator-color-remove,
.sr-remove {
#zen-gradient-generator-color-remove {
list-style-image: url("unpin.svg") !important;
}
@@ -135,10 +134,6 @@
list-style-image: url("arrow-right.svg");
}
.sr-open-in-icon {
list-style-image: url("arrow-corner-down-right.svg");
}
#PanelUI-menu-button,
#appMenu-more-button2,
.zen-workspaces-actions,
@@ -1011,8 +1006,7 @@
}
}
#zen-copy-url-button image,
.sr-url-icon {
#zen-copy-url-button image {
list-style-image: url("link.svg");
fill-opacity: 0.65;
}
@@ -1096,7 +1090,3 @@
#zen-boost-load {
list-style-image: url("open.svg");
}
.sr-airplane {
list-style-image: url("selectable/airplane.svg");
}

View File

@@ -4,7 +4,6 @@
#ifdef XP_WIN
* skin/classic/browser/zen-icons/algorithm.svg (../shared/zen-icons/nucleo/algorithm.svg)
* skin/classic/browser/zen-icons/arrow-corner-down-right.svg (../shared/zen-icons/nucleo/arrow-corner-down-right.svg)
* skin/classic/browser/zen-icons/arrow-down.svg (../shared/zen-icons/nucleo/arrow-down.svg)
* skin/classic/browser/zen-icons/arrow-left.svg (../shared/zen-icons/nucleo/arrow-left.svg)
* skin/classic/browser/zen-icons/arrow-right.svg (../shared/zen-icons/nucleo/arrow-right.svg)
@@ -154,7 +153,6 @@
#endif
#ifdef XP_MACOSX
* skin/classic/browser/zen-icons/algorithm.svg (../shared/zen-icons/nucleo/algorithm.svg)
* skin/classic/browser/zen-icons/arrow-corner-down-right.svg (../shared/zen-icons/nucleo/arrow-corner-down-right.svg)
* skin/classic/browser/zen-icons/arrow-down.svg (../shared/zen-icons/nucleo/arrow-down.svg)
* skin/classic/browser/zen-icons/arrow-left.svg (../shared/zen-icons/nucleo/arrow-left.svg)
* skin/classic/browser/zen-icons/arrow-right.svg (../shared/zen-icons/nucleo/arrow-right.svg)
@@ -304,7 +302,6 @@
#endif
#ifdef XP_LINUX
* skin/classic/browser/zen-icons/algorithm.svg (../shared/zen-icons/nucleo/algorithm.svg)
* skin/classic/browser/zen-icons/arrow-corner-down-right.svg (../shared/zen-icons/nucleo/arrow-corner-down-right.svg)
* skin/classic/browser/zen-icons/arrow-down.svg (../shared/zen-icons/nucleo/arrow-down.svg)
* skin/classic/browser/zen-icons/arrow-left.svg (../shared/zen-icons/nucleo/arrow-left.svg)
* skin/classic/browser/zen-icons/arrow-right.svg (../shared/zen-icons/nucleo/arrow-right.svg)

View File

@@ -1,5 +0,0 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="context-fill" fill-opacity="context-fill-opacity" viewBox="0 0 18 18"><path d="M2.75 3a.75.75 0 0 1 .75.75v4C3.5 8.44 4.06 9 4.75 9h8.69l-2.97-2.97a.75.75 0 1 1 1.06-1.06l4.25 4.25a.8.8 0 0 1 .118.16q.025.04.044.083a.75.75 0 0 1-.078.715 1 1 0 0 1-.084.102l-4.25 4.25a.75.75 0 0 1-1.06-1.06l2.97-2.97H4.75A2.75 2.75 0 0 1 2 7.75v-4A.75.75 0 0 1 2.75 3"/></svg>

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
diff --git a/servo/components/style/values/generics/border.rs b/servo/components/style/values/generics/border.rs
--- a/servo/components/style/values/generics/border.rs
+++ b/servo/components/style/values/generics/border.rs
@@ -296,9 +296,9 @@
pub fn all(s: S) -> Self {
Self {
top_left: s.clone(),
top_right: s.clone(),
bottom_right: s.clone(),
- bottom_left: s.clone(),
+ bottom_left: s,
}
}
}

View File

@@ -0,0 +1,38 @@
diff --git a/gfx/wr/glsl-to-cxx/src/hir.rs b/gfx/wr/glsl-to-cxx/src/hir.rs
--- a/gfx/wr/glsl-to-cxx/src/hir.rs
+++ b/gfx/wr/glsl-to-cxx/src/hir.rs
@@ -3531,10 +3531,11 @@
None,
Type::new(Float),
vec![Type::new(Vec2)],
);
declare_function(state, "pow", None, Type::new(Vec3), vec![Type::new(Vec3)]);
+ declare_function(state, "pow", None, Type::new(Vec2), vec![Type::new(Vec2)]);
declare_function(state, "pow", None, Type::new(Float), vec![Type::new(Float)]);
declare_function(state, "exp", None, Type::new(Float), vec![Type::new(Float)]);
declare_function(state, "exp2", None, Type::new(Float), vec![Type::new(Float)]);
declare_function(state, "log", None, Type::new(Float), vec![Type::new(Float)]);
declare_function(state, "log2", None, Type::new(Float), vec![Type::new(Float)]);
diff --git a/gfx/wr/swgl/src/glsl.h b/gfx/wr/swgl/src/glsl.h
--- a/gfx/wr/swgl/src/glsl.h
+++ b/gfx/wr/swgl/src/glsl.h
@@ -800,10 +800,18 @@
Float pow(Float x, Float y) {
return if_then_else((x == 0) | (x == 1), x, approx_pow2(approx_log2(x) * y));
}
+vec2 pow(vec2 a, vec2 b) {
+ return vec2(pow(a.x, b.x), pow(a.y, b.y));
+}
+
+vec2_scalar pow(vec2_scalar a, vec2_scalar b) {
+ return vec2_scalar(pow(a.x, b.x), pow(a.y, b.y));
+}
+
#define exp __glsl_exp
SI float exp(float x) { return expf(x); }
Float exp(Float y) {

View File

@@ -2,6 +2,16 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
[
{
"type": "phabricator",
"ids": [
"D296935",
"D303334",
"D297660",
"D304517"
],
"name": "Corner shape support"
},
{
"type": "phabricator",
"ids": [

View File

@@ -59,18 +59,6 @@ export class nsZenBoostEditor {
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.
*/
@@ -239,9 +227,6 @@ export class nsZenBoostEditor {
editor.refresh();
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.codeEditorReady = true;
}
@@ -444,13 +429,20 @@ export class nsZenBoostEditor {
}
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
this.openerWindow.focus();
}
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();
}
@@ -475,11 +467,16 @@ ${cssSelector} {
}
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() {
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 zapEnabled = await actor.sendQuery("ZenBoost:ZapModeEnabled");
@@ -490,8 +487,12 @@ ${cssSelector} {
}
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 selectEnabled = await this.zenBoostsChild.sendQuery(
const selectEnabled = await actor.sendQuery(
"ZenBoost:SelectorPickerModeEnabled"
);
@@ -630,7 +631,6 @@ ${cssSelector} {
this.currentBoostData.textCaseOverride = "uppercase";
}
this.currentBoostData.changeWasMade = true;
this.updateCaseButtonVisuals();
this.updateCurrentBoost();
}
@@ -638,7 +638,7 @@ ${cssSelector} {
/**
* Handles the size toggle button press, cycling through size override options
*/
async onBoostSizePressed() {
onBoostSizePressed() {
if (this.currentBoostData.sizeOverride == 1) {
this.currentBoostData.sizeOverride = 1.1;
} else if (this.currentBoostData.sizeOverride == 1.1) {
@@ -649,10 +649,8 @@ ${cssSelector} {
this.currentBoostData.sizeOverride = 0.9;
} else if (this.currentBoostData.sizeOverride == 0.9) {
this.currentBoostData.sizeOverride = 1;
await this.zenBoostsChild.sendQuery("ZenBoost:DisableSizeOverride");
}
this.currentBoostData.changeWasMade = true;
this.updateSizeButtonVisuals();
this.updateCurrentBoost();
}

View File

@@ -280,9 +280,6 @@ export class ZenBoostsChild extends JSWindowActorChild {
case "ZenBoost:OpenInspector":
this.sendAsyncMessage("ZenBoost:OpenInspector");
break;
case "ZenBoost:DisableSizeOverride":
this.disableSizeOverride();
break;
}
return null;
}
@@ -552,14 +549,6 @@ export class ZenBoostsChild extends JSWindowActorChild {
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) {
this.sendAsyncMessage("ZenBoost:Notify", { topic, msg });
}

View File

@@ -5,11 +5,6 @@
// prettier-ignore
// eslint-disable-next-line no-lone-blocks
{
ChromeUtils.defineESModuleGetters(this, {
gZenSpaceRoutingManager:
"resource:///modules/zen/spacerouting/ZenSpaceRoutingManager.sys.mjs",
});
Services.scriptloader.loadSubScript("chrome://browser/content/zen-components/ZenSpaceBookmarksStorage.js", this);
let scripts = [

View File

@@ -100,16 +100,7 @@ class ZenStartup {
delete this.promiseInitializedResolve;
setTimeout(() => {
// Wait for the natural PlacesToolbar rebuild before invalidating, so
// 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());
gZenWorkspaces._invalidateBookmarkContainers();
});
});
}
@@ -156,7 +147,7 @@ class ZenStartup {
}
#initUIComponents() {
const kUIComponents = ["ZenProgressBar", "ZenSpaceRoutingNavigation"];
const kUIComponents = ["ZenProgressBar"];
for (let component of kUIComponents) {
const module = ChromeUtils.importESModule(
"resource:///modules/zen/ui/" + component + ".sys.mjs"

View File

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

View File

@@ -5,10 +5,10 @@
*/
.dialogBox {
border-radius: 12px !important;
border: 0.5px solid light-dark(rgba(0, 0, 0, 0.4), var(--zen-dialog-background)) !important;
border: 1px solid light-dark(rgba(168, 168, 169, 0.50), var(--zen-dialog-background)) !important;
outline: 1px solid light-dark(transparent, rgba(168, 168, 169, 0.50)) !important;
box-shadow: 0 10px 8px rgba(0, 0 , 0, 0.15) !important;
outline-offset: -2px;
outline-offset: -1.5px;
@media not (prefers-reduced-motion: reduce) {
animation: zen-dialog-fade-in 0.3s ease-out;

View File

@@ -27,7 +27,7 @@
display: flex;
position: relative;
flex: 1;
padding: 16px;
padding: 8px;
border-bottom: 1px solid color-mix(in srgb, currentColor 10%, transparent);
}
@@ -38,12 +38,6 @@
white-space: nowrap;
font-weight: 500;
flex: 1;
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
width: calc(100% - 40px);
padding: 0 16px;
}
.zen-sidebar-notification-close-button {

View File

@@ -185,7 +185,7 @@
.toolbarbutton-1:not(#tabs-newtab-button),
.urlbar-page-action,
.identity-box-button {
--tab-border-radius: 6px;
--tab-border-radius: 8px;
--toolbarbutton-border-radius: var(--tab-border-radius);
--toolbarbutton-inner-padding: 6px;
--toolbarbutton-outer-padding: 1px;

View File

@@ -213,6 +213,8 @@
--toolbarbutton-border-radius: 6px;
--urlbar-margin-inline: 1px !important;
--zen-squircle-value: 1.3;
--tab-icon-overlay-stroke: light-dark(white, black) !important;
--tab-close-button-padding: 4px !important;
@@ -343,3 +345,7 @@
}
%include zen-buttons.css
*:not(.no-squircles) {
corner-shape: superellipse(var(--zen-squircle-value));
}

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

@@ -133,10 +133,6 @@ document.addEventListener(
gZenWorkspaces.unloadAllOtherWorkspaces();
break;
}
case "cmd_zenOpenSpaceRoutingSettings": {
gZenSpaceRoutingManager.openSpaceRoutingDialog(window);
break;
}
case "cmd_zenNewNavigatorUnsynced":
OpenBrowserWindow({ zenSyncedWindow: false });
break;

View File

@@ -214,7 +214,6 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
skipAnimation: true,
ownerTab: currentTab,
triggeringPrincipal: data.triggeringPrincipal,
skipRoute: true,
};
}

View File

@@ -41,11 +41,11 @@
&:hover {
background: light-dark(rgb(41, 41, 41), rgb(204, 204, 204));
scale: 1.05;
scale: 1.02;
}
&:hover:active {
scale: 0.95;
scale: 0.98;
}
& label {

View File

@@ -4,8 +4,8 @@
<html:template id="zen-glance-sidebar-template">
<vbox class="zen-glance-sidebar-container">
<toolbarbutton class="zen-glance-sidebar-close toolbarbutton-1" command="cmd_zenGlanceClose" data-l10n-id="zen-general-confirm" />
<toolbarbutton class="zen-glance-sidebar-open toolbarbutton-1" command="cmd_zenGlanceExpand" />
<toolbarbutton class="zen-glance-sidebar-split toolbarbutton-1" command="cmd_zenGlanceSplit" />
<toolbarbutton class="no-squircles zen-glance-sidebar-close toolbarbutton-1" command="cmd_zenGlanceClose" data-l10n-id="zen-general-confirm" />
<toolbarbutton class="no-squircles zen-glance-sidebar-open toolbarbutton-1" command="cmd_zenGlanceExpand" />
<toolbarbutton class="no-squircles zen-glance-sidebar-split toolbarbutton-1" command="cmd_zenGlanceSplit" />
</vbox>
</html:template>

View File

@@ -19,5 +19,4 @@ DIRS += [
"sessionstore",
"share",
"spaces",
"space-routing",
]

View File

@@ -521,13 +521,10 @@ class nsZenWindowSync {
if (flags & SYNC_FLAG_ICON) {
aTargetItem.zenStaticIcon = aOriginalItem.zenStaticIcon;
if (gBrowser.isTab(aOriginalItem)) {
try {
gBrowser.setIcon(
aTargetItem,
aOriginalItem.getAttribute("image") ||
gBrowser.getIcon(aOriginalItem)
);
} catch {}
gBrowser.setIcon(
aTargetItem,
aOriginalItem.getAttribute("image") || gBrowser.getIcon(aOriginalItem)
);
} else if (aOriginalItem.isZenFolder) {
// Icons are a zen-only feature for tab groups.
gZenFolders.setFolderUserIcon(aTargetItem, aOriginalItem.iconURL);
@@ -1545,7 +1542,6 @@ class nsZenWindowSync {
console.error(`Error moving active tabs to other windows on close:`, e);
}
resolve();
this.#docShellSwitchPromise = null;
}
on_WindowCloseAndBrowserFlushed(aBrowsers) {

View File

@@ -1,462 +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/. */
// eslint-disable-next-line no-shadow
const { gZenSpaceRoutingManager } = ChromeUtils.importESModule(
"resource:///modules/zen/spacerouting/ZenSpaceRoutingManager.sys.mjs"
);
export class nsZenSpaceRoutingDialog {
doc = null;
editorWindow = null;
openerWindow = null;
static OBSERVERS = ["zen-space-routing-kill"];
/**
* Creates a new Space Routing dialog controller.
*
* @param {Document} doc - The document object for the dialog window.
* @param {Window} editorWindow - The Space Routing dialog window.
* @param {Window} openerWindow - The browser window that opened the dialog.
*/
constructor(doc, editorWindow, openerWindow) {
this.doc = doc;
this.editorWindow = editorWindow;
this.openerWindow = openerWindow;
this.killOtherShareInstances();
nsZenSpaceRoutingDialog.OBSERVERS.forEach(observe => {
Services.obs.addObserver(this, observe);
});
this.init();
this.editorWindow.addEventListener("unload", () => this.uninit(), {
once: true,
});
}
/**
* Initializes the boost share instance by setting up event listeners for all UI controls.
*/
init() {
this.editorWindow.addEventListener("unload", () => this.handleClose(), {
once: true,
});
this.doc
.getElementById("sr-close")
.addEventListener("click", this.onClosePressed.bind(this));
this.doc
.getElementById("sr-new-route")
.addEventListener("click", this.onNewRoutePressed.bind(this));
const defaultRouteSelect = this.doc.getElementById(
"sr-default-external-open-in"
);
this.createOpenInList(
defaultRouteSelect,
gZenSpaceRoutingManager.getDefaultExternalRoute()
);
defaultRouteSelect.addEventListener("command", e =>
this.onRouteDefaultExternalChange(e.target.value)
);
this.doc.addEventListener("keydown", event => {
if (
event.key === "Escape" ||
(event.key === "w" && (event.ctrlKey || event.metaKey))
) {
this.onClosePressed();
}
});
this.initRouteList();
this.initialized = true;
}
/**
* Initializes the routes list and loads all current routes from the disk
*/
initRouteList() {
const allRoutes = gZenSpaceRoutingManager.getAllRoutes();
allRoutes.forEach(r => this.createRouteElement(r));
}
/**
* Will create a new route and update the route list
*/
onNewRoutePressed() {
const newRoute = gZenSpaceRoutingManager.createNewRoute();
this.createRouteElement(newRoute);
}
/**
* Will remove a route and update the list
*
* @param {string} routeId - The unique ID of the affected route
* @param {string} containerElement - The container element of the route in the list
*/
onRemoveRoutePressed(routeId, containerElement) {
gZenSpaceRoutingManager.removeRoute(routeId);
containerElement.remove();
this.updateShowNoRouteText();
}
/**
* Will create the rule element content and inject it into the ui
*
* @param {object} route - The target route
* @returns {Element} The created element for the route
*/
createRouteElement(route) {
const container = this.doc.getElementById("sr-content");
const root = this.doc.createXULElement("vbox");
root.setAttribute("routeId", route.id);
root.className = "sr-rule-container";
// ---- Top row
const topRow = this.doc.createXULElement("hbox");
topRow.className = "sr-rule-row sr-rule-top";
const topLabelContainer = this.doc.createXULElement("hbox");
topLabelContainer.className = "sr-label-container";
const urlIcon = this.doc.createXULElement("image");
urlIcon.className = "sr-url-icon";
const urlLabel = this.doc.createXULElement("label");
urlLabel.className = "sr-label";
urlLabel.setAttribute("data-l10n-id", "zen-space-routing-url");
topLabelContainer.append(urlIcon, urlLabel);
// Match type
const matchTypeMenulist = this.doc.createXULElement("menulist");
matchTypeMenulist.className = "select match-type-select";
const matchTypePopup = this.doc.createXULElement("menupopup");
matchTypeMenulist.appendChild(matchTypePopup);
["contains", "equal-to", "regex"].forEach(id => {
const menuItem = this.doc.createXULElement("menuitem");
menuItem.setAttribute("data-l10n-id", `zen-space-routing-${id}`);
menuItem.setAttribute("value", id);
matchTypePopup.appendChild(menuItem);
});
matchTypeMenulist.value = route.matchType;
// Input domain
const input = this.doc.createElement("input");
input.className = "input";
input.value = route.reference;
this.updateInputPlaceholder(route.matchType, input);
const removeButton = this.doc.createXULElement("button");
removeButton.className = "sr-remove";
topRow.append(topLabelContainer, matchTypeMenulist, input, removeButton);
// ---- Bottom row
const bottomRow = this.doc.createXULElement("hbox");
bottomRow.className = "sr-rule-row sr-rule-bottom";
const bottomLabelContainer = this.doc.createXULElement("hbox");
bottomLabelContainer.className = "sr-label-container";
const openInIcon = this.doc.createXULElement("image");
openInIcon.className = "sr-open-in-icon";
const openInLabel = this.doc.createXULElement("label");
openInLabel.className = "sr-label";
openInLabel.setAttribute("data-l10n-id", "zen-space-routing-open-in");
bottomLabelContainer.append(openInIcon, openInLabel);
// Open in
const openInMenulist = this.doc.createXULElement("menulist");
openInMenulist.className = "select open-in-select";
const openInMenupopup = this.doc.createXULElement("menupopup");
openInMenulist.appendChild(openInMenupopup);
this.createOpenInList(openInMenulist, route.openIn);
bottomRow.append(bottomLabelContainer, openInMenulist);
root.append(topRow, bottomRow);
root.style.display = "none";
container.appendChild(root);
// Wait for l10n to catch up and then show the element to avoid flickering.
this.editorWindow.promiseDocumentFlushed(() =>
this.editorWindow.requestAnimationFrame(() => {
root.style.display = "";
input.focus();
})
);
removeButton.addEventListener("click", () => {
this.onRemoveRoutePressed(route.id, root);
});
input.addEventListener("input", e =>
this.onRouteReferenceChange(e.target.value, route.id, input)
);
matchTypeMenulist.addEventListener("command", e =>
this.onRouteMatchTypeChange(e.target.value, route.id, input)
);
openInMenulist.addEventListener("command", e =>
this.onRouteOpenInChange(e.target.value, route.id)
);
this.updateShowNoRouteText();
return root;
}
/**
* Checks if the text for when no routes are
* created should be displayed
*/
updateShowNoRouteText() {
const container = this.doc.getElementById("sr-content");
const noRoutesText = this.doc.getElementById("sr-empty-content");
// One because of the element itself
noRoutesText.style.display =
container.children.length == 1 ? "flex" : "none";
}
/**
* Callback for when the reference text changes
*
* @param {string} value - The new value
* @param {string} routeId - The ID of the affected route
* @param {Element} input - The input element
*/
onRouteReferenceChange(value, routeId, input) {
const route = gZenSpaceRoutingManager.getRoute(routeId);
route.reference = value;
this.updateInputPlaceholder(route.matchType, input);
// Don't update the route if the regex is invalid
if (route.matchType == "regex") {
if (!this.onCheckRegexValid(input)) {
return;
}
}
gZenSpaceRoutingManager.updateRoute(route);
}
/**
* Callback for when the open in attribute changes
*
* @param {string} value - The new value
* @param {string} routeId - The ID of the affected route
*/
onRouteOpenInChange(value, routeId) {
const route = gZenSpaceRoutingManager.getRoute(routeId);
route.openIn = value;
gZenSpaceRoutingManager.updateRoute(route);
}
/**
* Callback for when the route match type changes
*
* @param {string} value - The new value
* @param {string} routeId - The ID of the affected route
* @param {Element} input - The text input
*/
onRouteMatchTypeChange(value, routeId, input) {
const route = gZenSpaceRoutingManager.getRoute(routeId);
route.matchType = value;
this.updateInputPlaceholder(route.matchType, input);
// Don't update the route if the regex is invalid
if (route.matchType == "regex") {
if (!this.onCheckRegexValid(input)) {
return;
}
}
gZenSpaceRoutingManager.updateRoute(route);
}
/**
* Updates the input placeholder based on the
* current route match type
*
* @param {string} matchType - The match type (e.g. "contains", "equal-to", "regex")
* @param {Element} input - The input element
*/
updateInputPlaceholder(matchType, input) {
switch (matchType) {
case "regex":
input.placeholder = "zen-browser\\.app";
break;
default:
input.placeholder = "zen-browser.app";
break;
}
}
/**
* Will validate and return the validity of the
* regex. Applies a tint to the input if an error occurs.
*
* @param {Element} input - The input element for the regex
* @returns {bool} True if regex is valid
*/
onCheckRegexValid(input) {
const reference = input.value;
// Ignore empty
if (reference.trim() == "") {
input.classList.remove("invalid");
return true;
}
try {
new RegExp(reference);
} catch (e) {
input.classList.add("invalid");
return false;
}
input.classList.remove("invalid");
return true;
}
/**
* Callback for when the default external route changes
*
* @param {string} value - The new value
*/
onRouteDefaultExternalChange(value) {
gZenSpaceRoutingManager.setDefaultExternalRoute(value);
}
/**
* Creates the options list selects
*
* @param {Element} selectElement - The menulist element
* @param {string} value - The initial value
*/
async createOpenInList(selectElement, value) {
const popupElement =
selectElement.querySelector("menupopup") || selectElement;
popupElement.replaceChildren(); // Clear existing
const [openInSpace, mostRecentSpace] = await this.doc.l10n.formatMessages([
"zen-space-routing-open-in-space",
"zen-space-routing-most-recent-space",
]);
const sectionHeader = this.doc.createXULElement("menuitem");
sectionHeader.setAttribute("label", openInSpace.value);
sectionHeader.setAttribute("disabled", "true");
sectionHeader.classList.add("menu-section-header");
popupElement.appendChild(sectionHeader);
let availOptions = [];
let createXulItem = (text, id, iconPath = null) => {
if (text === "sep") {
popupElement.appendChild(this.doc.createXULElement("menuseparator"));
return;
}
availOptions.push(id || text);
const menuItem = this.doc.createXULElement("menuitem");
menuItem.setAttribute("label", text);
menuItem.setAttribute("value", id || text);
if (iconPath) {
if (iconPath.startsWith("chrome://")) {
menuItem.setAttribute("class", "menuitem-iconic");
menuItem.setAttribute("image", iconPath);
} else {
menuItem.setAttribute("label", `${iconPath} ${text}`);
}
}
popupElement.appendChild(menuItem);
};
const workspaces = this.openerWindow.gZenWorkspaces.getWorkspaces();
createXulItem(mostRecentSpace.value, "most-recent-space");
createXulItem("sep");
workspaces.forEach(workspace => {
createXulItem(workspace.name, workspace.uuid, workspace.icon);
});
// Check if the workspace still exists, if not use default
if (availOptions.includes(value)) {
selectElement.value = value;
} else {
selectElement.value = "most-recent-space";
}
}
/**
* Uninitializes the boost editor by cleaning up event listeners and observers.
*/
uninit() {
nsZenSpaceRoutingDialog.OBSERVERS.forEach(observe => {
Services.obs.removeObserver(this, observe);
});
}
/**
* Kills all other Space Routing dialog instances
*/
killOtherShareInstances() {
Services.obs.notifyObservers(null, "zen-space-routing-kill");
}
/**
* Observer callback that handles notifications from the observer service.
* Closes the control window when a 'zen-space-routing-kill' notification is received.
*
* @param {object} subject - The subject of the notification.
* @param {string} topic - The topic of the notification.
*/
observe(subject, topic) {
switch (topic) {
case "zen-space-routing-kill":
this.editorWindow.close();
break;
}
}
/**
* Callback for when the user presses the close button
*/
onClosePressed() {
this.editorWindow.close();
}
/**
* Handles the window close event
*/
handleClose() {
gZenSpaceRoutingManager.saveRoutes();
}
}

View File

@@ -1,452 +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 { JSONFile } from "resource://gre/modules/JSONFile.sys.mjs";
class nsZenSpaceRoutingManager {
#file = null;
#saveFilename = "zen-space-routing.jsonlz4";
static SKIP_TYPE = {
NONE: "none",
SKIPPED_TAB: "skipped_tab",
RESTORED_TAB: "restored_tab",
};
constructor() {
this.#readFromDisk();
}
/**
* Callback that will be executed from tabbrowser.js
* This method can be used to stop the tab from being created.
*
* @param {string} uriString - The URI as a string
* @param {object} options - The tab creation options
* @param {Window} win - The window which the tab will be added to
* @returns {object} Returns an object with { shouldEarlyExit, userContextId, isRouteFound, targetRoute }
*/
onBeforeAddTab(uriString, options, win) {
let userContextId = null;
let isRouteFound = false;
let targetRoute = null;
if (
this.#shouldSkipProcessing(options, win) !=
nsZenSpaceRoutingManager.SKIP_TYPE.NONE
) {
return {
shouldEarlyExit: false,
userContextId,
isRouteFound,
targetRoute,
};
}
targetRoute = this.routeUri(uriString, options);
switch (targetRoute) {
case "most-recent-space":
break;
default: {
const targetWorkspace =
win?.gZenWorkspaces?.getWorkspaceFromId(targetRoute);
if (targetWorkspace) {
userContextId = targetWorkspace.containerTabId;
isRouteFound = true;
}
}
}
return { shouldEarlyExit: false, userContextId, isRouteFound, targetRoute };
}
/**
* Callback that will be executed from tabbrowser.js
*
* @param {string} uriString - The URI as a string
* @param {Element} newTab - The tab element
* @param {object} options - The tab creation options
* @param {Window} win - The window which the tab was added to
* @param {object} [beforeResult] - The result returned by onBeforeAddTab for
* this tab. When present its precomputed targetRoute is reused instead of
* running routeUri() a second time.
*/
onAfterAddTab(uriString, newTab, options, win, beforeResult) {
const targetRoute = beforeResult?.targetRoute;
if (!targetRoute) {
return;
}
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
*
* @param {object} options - The tab creation options
* @param {Window} win - The owning window
* @returns {SKIP_TYPE} The type of skip or null if not skipped
*/
#shouldSkipProcessing(options, win) {
if (options.skipRoute || options.pinned || options.tabGroup) {
return nsZenSpaceRoutingManager.SKIP_TYPE.SKIPPED_TAB;
}
// addTab() is being called when the session restores.
// To avoid automatically routing these tabs,
// a check if the restore is already complete is needed
if (!win.gZenStartup.isReady) {
return nsZenSpaceRoutingManager.SKIP_TYPE.RESTORED_TAB;
}
return nsZenSpaceRoutingManager.SKIP_TYPE.NONE;
}
/**
* Will route the given tab to a space if a rule applies
*
* @param {string} targetRoute - The precomputed route for the tab
* @param {Element} newTab - The tab element
* @param {Window} win - The window which the tab was added to
* @private
*/
async #routeToWorkspace(targetRoute, newTab, win) {
try {
if (!newTab || !newTab.parentNode) {
return;
}
switch (targetRoute) {
case "most-recent-space":
break;
default: {
const workspaces = win?.gZenWorkspaces;
const targetWorkspace = workspaces?.getWorkspaceFromId?.(targetRoute);
if (targetWorkspace) {
workspaces.moveTabToWorkspace(newTab, targetWorkspace.uuid);
const mostRecentWindow =
Services.wm.getMostRecentWindow("navigator:browser");
const isOriginatingWindow = win === mostRecentWindow;
if (isOriginatingWindow) {
win.gZenWorkspaces.lastSelectedWorkspaceTabs[
targetWorkspace.uuid
] = newTab;
await win.gZenWorkspaces.changeWorkspace(targetWorkspace);
}
}
}
}
} catch (err) {
console.error("[ZenSpaceRouting]: Error moving tab to workspace:", err);
}
}
/**
* This will give the id of the workspace this uri will
* route to, or "most-recent-space"
*
* @param {string} uriString - The uri which will be routed
* @param {object} options - The tab creation options
* @returns {string} Route instructions
*/
routeUri(uriString, options) {
const isExternal = options.fromExternal;
// Go over all routes and return the open type for the first match
const allRoutes = this.getAllRoutes();
for (const route of allRoutes) {
if (this.isRouteMatching(uriString, route)) {
return route.openIn;
}
}
// If nothing matches and it's an external link,
// use the default external route
if (isExternal) {
return this.getDefaultExternalRoute();
}
// If nothing matches, open in most recent space
return "most-recent-space";
}
/**
* Checks if a given rule matches a uriString
*
* @param {string} uriString - The uri
* @param {object} route - The route
* @returns {boolean} True if the rule matches
*/
isRouteMatching(uriString, route) {
if (typeof uriString !== "string" || typeof route?.reference !== "string") {
return false;
}
let reference = route.reference.toLowerCase();
if (reference.trim() == "") {
return false;
}
const uri = uriString.toLowerCase();
switch (route.matchType) {
case "contains":
if (uri.includes(reference)) {
return true;
}
break;
case "equal-to":
if (this.#normalizeURL(uri) == this.#normalizeURL(reference)) {
return true;
}
break;
case "regex": {
let unmodifiedReference = route.reference;
try {
// Use unmodified parameters for the regex test
const regex = new RegExp(unmodifiedReference);
if (regex.test(uriString)) {
return true;
}
} catch (e) {
console.error(
"[ZenSpaceRouting] Failed to resolve regular expression:",
unmodifiedReference,
e
);
}
break;
}
}
return false;
}
/**
* Will remove any protocol sequences to normalize the url
*
* @param {string} uriString - The url
* @returns {string} The normalized url
*/
#normalizeURL(uriString) {
if (!uriString) {
return "";
}
let clean = uriString.trim();
// Remove protocol sequences with regex
clean = clean.replace(/^https?:\/\//i, "");
clean = clean.replace(/^www\./i, "");
// If there is a trailing slash, remove
if (clean.endsWith("/")) {
clean = clean.slice(0, -1);
}
return clean;
}
/**
* Opens the Space Routing editor in a new popup window.
*
* @param {Window} parentWindow - The parent browser window
* @returns {Window|null} The instanced editor window
*/
async openSpaceRoutingDialog(parentWindow) {
await parentWindow.gDialogBox.open(
"chrome://browser/content/zen-components/windows/zen-space-routing.xhtml",
{
features: "resizable=no",
sizeTo: "available",
allowDuplicateDialogs: false,
parentWindow,
}
);
}
/**
* @returns {object} Returns a new empty Space Routing route
*/
getEmptyRoute() {
return {
id: crypto.randomUUID(),
reference: "",
openIn: "most-recent-space",
matchType: "contains",
};
}
/**
* @returns {Array<object>} A copy of the routes list
*/
getAllRoutes() {
if (!this.#file?.data?.routes) {
return [];
}
return structuredClone(this.#file.data.routes);
}
/**
* Returns a specific route
*
* @param {string} id - The ID of the given route
* @returns {object|null} The route, or null if no route has the given id
*/
getRoute(id) {
const idx = this.#file.data.routes.findIndex(r => r.id === id);
if (idx === -1) {
return null;
}
return structuredClone(this.#file.data.routes[idx]);
}
/**
* Will update an existing route
*
* @param {object} route - The updated route
*/
updateRoute(route) {
const idx = this.#file.data.routes.findIndex(r => r.id === route.id);
if (idx === -1) {
return;
}
this.#file.data.routes[idx] = structuredClone(route);
}
/**
* Creates a new route and returns it
*
* @returns {object} Returns the empty route
*/
createNewRoute() {
const newRoute = this.getEmptyRoute();
this.#file.data.routes.push(newRoute);
return structuredClone(newRoute);
}
/**
* Removes an existing route with the given id
*
* @param {string} id - The given id
*/
removeRoute(id) {
const objWithIdIndex = this.#file.data.routes.findIndex(r => r.id === id);
if (objWithIdIndex === -1) {
return;
}
this.#file.data.routes.splice(objWithIdIndex, 1);
}
/**
* @returns {string} Returns the default route type for external links
*/
getDefaultExternalRoute() {
return this.#file?.data?.defaultRouteExternal ?? "most-recent-space";
}
/**
* @param {string} routeType - Sets the default route type for external links
*/
setDefaultExternalRoute(routeType) {
this.#file.data.defaultRouteExternal = routeType;
}
/**
* Saves all routes. The list of
* routes is stripped of empty routes
* before being saved
*/
saveRoutes() {
this.#file.data.routes = this.#file.data.routes.filter(
route => route.reference.trim() !== ""
);
this.#writeToDisk();
}
/**
* Writes the Space Routing data back onto the disk.
*
* @private
*/
#writeToDisk() {
this.#file.saveSoon();
}
/**
* Reads Space Routing data from disk and decompresses it.
*
* @returns {Promise<Map>} A promise that resolves to an array of Space Routing rules.
* @private
*/
async #readFromDisk() {
this.#file = new JSONFile({
path: this.#storePath,
compression: "lz4",
dataPostProcessor(data) {
if (!data || typeof data !== "object") {
data = {};
}
if (!Array.isArray(data.routes)) {
data.routes = [];
}
if (typeof data.defaultRouteExternal !== "string") {
data.defaultRouteExternal = "most-recent-space";
}
return data;
},
});
await this.#file.load();
}
/**
* Gets the file path where Space Routing data is stored in the user's profile directory.
*
* @returns {string} The full path to the Space Routing storage file.
* @private
*/
get #storePath() {
const profilePath = PathUtils.profileDir;
return PathUtils.join(profilePath, this.#saveFilename);
}
}
export const gZenSpaceRoutingManager = new nsZenSpaceRoutingManager();

View File

@@ -1,9 +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/.
# Styles
content/browser/zen-styles/zen-space-routing.css (../../zen/space-routing/zen-space-routing.css)
# Windows
* content/browser/zen-components/windows/zen-space-routing.xhtml (../../zen/space-routing/zen-space-routing.inc.xhtml)

View File

@@ -1,8 +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/.
EXTRA_JS_MODULES.zen.spacerouting += [
"ZenSpaceRoutingDialog.mjs",
"ZenSpaceRoutingManager.sys.mjs",
]

View File

@@ -1,288 +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/. */
@namespace html "http://www.w3.org/1999/xhtml";
@namespace xul "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
:root {
--background-color-canvas: light-dark(white, #212223) !important;
--select-background-color: light-dark(#eceef0, #3e3f43);
--input-background-color: light-dark(#f1f2f4, #2c2d31);
--hr-color: light-dark(#cdced4, #313235);
--sr-width: 510px;
--sr-height: 500px;
--content-padding: 26px;
--content-padding-vertical: 16px;
--text-color: light-dark(#4c4c4c, #dbdcdf);
--text-color-secondary: light-dark(#5c5e65, #8b8e98);
--text-color-error: light-dark(#9d2222, #d03535);
--rules-gap: 14px;
--sr-border-radius: 12px;
}
window {
height: var(--sr-height);
max-height: var(--sr-height);
min-height: var(--sr-height);
width: var(--sr-width);
max-width: var(--sr-width);
min-width: var(--sr-width);
}
#sr-container {
height: var(--sr-height);
width: var(--sr-width);
border-radius: var(--sr-border-radius);
overflow: hidden;
font-family: system-ui !important;
position: relative;
display: flex;
}
#sr-empty-content {
display: flex;
flex-direction: column;
text-align: center;
gap: 24px;
position: absolute;
left: 84px;
right: 84px;
bottom: 100px;
top: 100px;
justify-content: center;
& image {
aspect-ratio: 1;
height: 75px;
min-height: 0;
max-height: none;
max-width: none;
min-width: 0;
color: var(--text-color-secondary);
-moz-context-properties: fill, fill-opacity;
fill: currentColor;
fill-opacity: 0.65;
}
& p {
font-size: small;
opacity: 0.8;
color: var(--text-color);
}
}
#sr-new-route {
padding: 4px 6px !important;
}
html|hr {
border-color: var(--hr-color) !important;
padding: 0;
margin: 0;
margin-left: 22px;
margin-right: 22px;
border-width: 0.5px;
}
h3 {
font-size: medium;
color: var(--text-color);
font-weight: normal;
}
h4 {
font-size: small;
color: var(--text-color-secondary);
}
p {
color: var(--text-color);
}
.select {
width: 150px;
min-height: unset;
height: 26px;
padding: 4px;
background-color: var(--select-background-color) !important;
color: var(--text-color);
margin: 0;
&.match-type-select {
width: 100px;
}
&.open-in-select {
width: 165px;
}
}
menulist[image]::part(icon) {
width: 16px;
height: 16px;
min-width: 16px;
min-height: 16px;
-moz-context-properties: fill, stroke;
fill: currentColor;
margin-inline-end: 0;
padding-left: 2px;
}
.sr-rule-row {
display: flex;
align-items: center;
gap: 8px;
}
.sr-rule-container {
display: flex;
flex-direction: column;
gap: 4px;
margin: 0 24px;
& p {
color: var(--text-color-secondary);
}
}
.sr-rule-bottom {
margin-left: 87px;
}
.input {
background-color: var(--input-background-color);
color: var(--text-color);
margin: 0;
flex-grow: 1;
}
.invalid {
color: var(--text-color-error) !important;
text-decoration-line: underline;
text-decoration-style: wavy;
text-decoration-thickness: 1px;
text-decoration-color: var(--text-color-error);
}
button {
background-color: var(--select-background-color) !important;
min-height: unset !important;
padding: 2px !important;
display: flex;
justify-content: center;
margin: 0;
& hbox {
display: none;
}
}
.sr-remove,
.close-icon {
& hbox {
display: flex;
}
opacity: 0.5;
appearance: none !important;
border: none !important;
height: 32px;
width: 32px;
padding: 3px;
min-width: 0 !important;
color: var(--text-color-secondary);
-moz-context-properties: fill, fill-opacity;
fill: currentColor;
&:not(:hover) {
background: none !important;
}
}
#sr-header {
width: 100%;
padding: var(--content-padding) 20px;
align-items: center;
display: flex;
flex-direction: row;
}
.sr-left {
margin-left: auto;
display: flex;
flex-direction: row;
align-items: center;
gap: 6px;
& button {
color: var(--text-color);
opacity: 1;
&:hover .sr-remove {
opacity: 0.8;
}
}
}
#sr-footer {
width: 100%;
padding: var(--content-padding);
align-items: center;
display: flex;
flex-direction: row;
}
#sr-content {
width: 100%;
overflow: scroll;
padding: 20px var(--content-padding);
flex-grow: 1;
display: flex;
flex-direction: column;
gap: var(--rules-gap);
}
.sr-label-container {
display: flex;
align-items: center;
gap: 6px;
color: var(--text-color-secondary);
}
.sr-label {
margin: 0;
}
.sr-url-icon,
.sr-open-in-icon {
width: 16px;
height: 16px;
-moz-context-properties: fill, fill-opacity;
fill: currentColor;
fill-opacity: 0.65;
}

View File

@@ -1,96 +0,0 @@
#filter substitution
<?xml version="1.0"?>
# -*- Mode: HTML -*-
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<!DOCTYPE window>
<window
xmlns:html="http://www.w3.org/1999/xhtml"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
windowtype="Zen:SpaceRouting"
customtitlebar="true"
id="zenSpaceRoutingDialog"
dialogroot="true"
>
<linkset>
<html:link
rel="stylesheet"
href="chrome://browser/content/zen-styles/zen-space-routing.css"
type="text/css"
/>
<html:link
rel="stylesheet"
href="chrome://global/skin/global.css"
type="text/css"
/>
<html:link
rel="stylesheet"
href="chrome://browser/content/zen-styles/zen-theme.css"
type="text/css"
/>
<html:link
rel="stylesheet"
href="chrome://browser/skin/zen-icons/icons.css"
type="text/css"
/>
<html:link
rel="stylesheet"
type="text/css"
href="chrome://browser/content/zen-styles/zen-popup.css"
/>
<html:link rel="localization" href="browser/zen-space-routing.ftl" />
</linkset>
<vbox id="sr-container">
<hbox id="sr-header">
<h3 data-l10n-id="zen-space-routing-dialog-title"></h3>
<html:div class="sr-left">
<button
id="sr-new-route"
data-l10n-id="zen-space-routing-new-route"
></button>
<button
class="close-icon"
data-l10n-id="zen-space-routing-close-button"
id="sr-close"
></button>
</html:div>
</hbox>
<html:hr />
<vbox id="sr-content">
<vbox id="sr-empty-content">
<image class="sr-airplane"></image>
<p data-l10n-id="zen-space-routing-rulepanel-placeholder"></p>
</vbox>
# All rules will be injected here later
</vbox>
<html:hr />
<hbox id="sr-footer">
<h4 data-l10n-id="zen-space-routing-external-default"></h4>
<html:div class="sr-left">
<menulist class="select" id="sr-default-external-open-in">
<menupopup id="sr-default-external-open-in-popup">
# Select open in types will be injected here
</menupopup>
</menulist>
</html:div>
</hbox>
</vbox>
<script>
const { nsZenSpaceRoutingDialog } = ChromeUtils.importESModule(
"resource:///modules/zen/spacerouting/ZenSpaceRoutingDialog.mjs",
);
const args = window.arguments?.[0] || {};
window.spaceroutingDialog = new nsZenSpaceRoutingDialog(
document,
window,
args.parentWindow,
);
</script>
</window>

View File

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

View File

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

View File

@@ -955,7 +955,7 @@ class nsZenWorkspaces {
}
gZenEmojiPicker.open(anchor, {
closeOnSelect: false,
allowNone: !hasNoIcon,
allowNone: hasNoIcon,
onSelect: async icon => {
const workspace = this.getWorkspaceFromId(workspaceId);
if (!workspace) {
@@ -1010,14 +1010,6 @@ class nsZenWorkspaces {
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");
if (!workspaceID) {
return null;
@@ -1506,6 +1498,7 @@ class nsZenWorkspaces {
continue;
}
tab.owner = null;
if (container) {
if (tab.group?.hasAttribute("split-view-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) {
const isEssential = tab.getAttribute("zen-essential") === "true";
const tabWorkspaceId = tab.getAttribute("zen-workspace-id");
@@ -3014,8 +2986,7 @@ class nsZenWorkspaces {
if (
triggeringPrincipal &&
triggeringPrincipal.isAddonOrExpandedAddonPrincipal &&
typeof userContextId === "undefined"
triggeringPrincipal.isAddonOrExpandedAddonPrincipal
) {
return [userContextId, false, undefined];
}

View File

@@ -20,6 +20,7 @@
display: none !important;
}
border-radius: var(--button-border-radius) !important;
background: transparent;
appearance: unset !important;
height: fit-content;
@@ -31,6 +32,7 @@
& toolbarbutton {
margin: 0;
max-width: 28px;
border-radius: var(--toolbarbutton-border-radius);
height: 28px;
display: flex;
justify-content: center;
@@ -40,7 +42,6 @@
fill-opacity: 0.6;
-moz-context-properties: fill-opacity, fill;
fill: currentColor;
scroll-margin: 0 20px;
& .zen-workspace-icon {
pointer-events: none;
@@ -105,7 +106,7 @@
&[icons-overflow] {
gap: 0 !important;
justify-content: safe center;
justify-content: center;
& toolbarbutton {
margin: 0;
@@ -321,11 +322,7 @@ zen-workspace {
position: absolute;
height: 100%;
overflow: hidden;
color: color-mix(in srgb, var(--toolbox-textcolor) 95%, var(--zen-primary-color));
--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;
color: var(--toolbox-textcolor);
@media not (prefers-reduced-motion: reduce) {
transition: padding-top 0.1s;

View File

@@ -1226,10 +1226,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
const currentTab = gZenGlanceManager.getTabOrGlanceParent(
window.gBrowser.selectedTab
);
const newTab = this.openAndSwitchToTab(url, {
skipRoute: true,
inBackground: false,
});
const newTab = this.openAndSwitchToTab(url, { inBackground: false });
this.splitTabs([currentTab, newTab], undefined, 1);
}

View File

@@ -235,7 +235,7 @@
}
@media (-moz-platform: macos) {
--border-radius-medium: 12px;
--border-radius-medium: 14px;
--tab-border-radius: 8px;
}
@@ -303,6 +303,8 @@
border-bottom: 0 solid transparent !important;
--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;
&[overflow]::after,
@@ -1239,6 +1241,8 @@
background: var(--zen-essential-tab-selected-bg);
margin: var(--zen-essential-bg-margin);
border-radius: calc(var(--border-radius-medium) - var(--zen-essential-bg-margin));
/* stylelint-disable-next-line property-no-unknown */
corner-shape: var(--zen-squircle-value);
position: absolute;
inset: 0;
z-index: 0;

View File

@@ -20,5 +20,3 @@ support-files = [
["browser_glance_prev_tab.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"
is_direct_path = true
[sandbox]
source = "security/sandbox/test"
is_direct_path = true
disable = [
"browser_bug1393259.js",
]
[sandbox.replace-manifest]
"../../../" = "../../../../"
[shell]
source = "browser/components/shell/test"
is_direct_path = true

View File

@@ -10,6 +10,7 @@
BROWSER_CHROME_MANIFESTS += [
"readermode/browser.toml",
"safebrowsing/browser.toml",
"sandbox/browser.toml",
"shell/browser.toml",
"tabMediaIndicator/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

@@ -13,7 +13,6 @@ BROWSER_CHROME_MANIFESTS += [
"pinned/browser.toml",
"popover/browser.toml",
"site_control/browser.toml",
"space_routing/browser.toml",
"spaces/browser.toml",
"split_view/browser.toml",
"tabs/browser.toml",

View File

@@ -1,22 +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/.
[DEFAULT]
support-files = [
"head.js",
]
["browser_space_routing_crud.js"]
["browser_space_routing_dialog.js"]
["browser_space_routing_fuzz.js"]
["browser_space_routing_on_add_tab.js"]
["browser_space_routing_redirect_navigation.js"]
["browser_space_routing_route_matching.js"]
["browser_space_routing_route_uri.js"]

View File

@@ -1,115 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_setup(async function () {
clearAllRoutes();
const savedDefault = gZenSpaceRoutingManager.getDefaultExternalRoute();
registerCleanupFunction(() => {
clearAllRoutes();
gZenSpaceRoutingManager.setDefaultExternalRoute(savedDefault);
});
});
add_task(async function test_empty_route_shape_and_unique_ids() {
const a = gZenSpaceRoutingManager.getEmptyRoute();
const b = gZenSpaceRoutingManager.getEmptyRoute();
Assert.equal(a.reference, "", "Empty route starts with no reference");
Assert.equal(
a.openIn,
"most-recent-space",
"Empty route defaults to most-recent-space"
);
Assert.equal(a.matchType, "contains", "Empty route defaults to 'contains'");
Assert.equal(typeof a.id, "string", "Empty route has a string id");
ok(a.id.length, "Empty route id is non-empty");
Assert.notEqual(a.id, b.id, "Each empty route gets a unique id");
});
add_task(async function test_create_get_update_remove_lifecycle() {
clearAllRoutes();
Assert.equal(
gZenSpaceRoutingManager.getAllRoutes().length,
0,
"Precondition: no routes"
);
const created = gZenSpaceRoutingManager.createNewRoute();
Assert.equal(
gZenSpaceRoutingManager.getAllRoutes().length,
1,
"createNewRoute() appends one route"
);
created.reference = "zen-browser.app";
created.openIn = "ws-42";
created.matchType = "equal-to";
gZenSpaceRoutingManager.updateRoute(created);
const fetched = gZenSpaceRoutingManager.getRoute(created.id);
Assert.equal(fetched.reference, "zen-browser.app", "reference persisted");
Assert.equal(fetched.openIn, "ws-42", "openIn persisted");
Assert.equal(fetched.matchType, "equal-to", "matchType persisted");
gZenSpaceRoutingManager.removeRoute(created.id);
Assert.equal(
gZenSpaceRoutingManager.getAllRoutes().length,
0,
"removeRoute() deletes the route"
);
});
add_task(async function test_remove_only_targets_the_given_id() {
clearAllRoutes();
const keep1 = addRoute({ reference: "a" });
const drop = addRoute({ reference: "b" });
const keep2 = addRoute({ reference: "c" });
gZenSpaceRoutingManager.removeRoute(drop.id);
const ids = gZenSpaceRoutingManager.getAllRoutes().map(r => r.id);
Assert.deepEqual(
ids,
[keep1.id, keep2.id],
"Only the targeted route is removed; order of the rest is preserved"
);
});
add_task(async function test_reads_return_copies_not_internal_refs() {
clearAllRoutes();
const created = gZenSpaceRoutingManager.createNewRoute();
const fromGet = gZenSpaceRoutingManager.getRoute(created.id);
fromGet.reference = "mutated-via-getRoute";
Assert.equal(
gZenSpaceRoutingManager.getRoute(created.id).reference,
"",
"getRoute() returns a copy; external mutation does not leak"
);
const all = gZenSpaceRoutingManager.getAllRoutes();
all[0].reference = "mutated-via-getAllRoutes";
Assert.equal(
gZenSpaceRoutingManager.getRoute(created.id).reference,
"",
"getAllRoutes() returns copies; external mutation does not leak"
);
});
add_task(async function test_default_external_route_getter_setter() {
gZenSpaceRoutingManager.setDefaultExternalRoute("ws-default");
Assert.equal(
gZenSpaceRoutingManager.getDefaultExternalRoute(),
"ws-default",
"setDefaultExternalRoute() round-trips through the getter"
);
gZenSpaceRoutingManager.setDefaultExternalRoute("most-recent-space");
Assert.equal(
gZenSpaceRoutingManager.getDefaultExternalRoute(),
"most-recent-space",
"The default external route can be changed again"
);
});

View File

@@ -1,255 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_setup(async function () {
clearAllRoutes();
const savedDefault = gZenSpaceRoutingManager.getDefaultExternalRoute();
registerCleanupFunction(() => {
clearAllRoutes();
gZenSpaceRoutingManager.setDefaultExternalRoute(savedDefault);
});
});
add_task(async function test_empty_placeholder_and_add_route() {
clearAllRoutes();
const dlg = await openRoutingDialog();
try {
const doc = dlg.document;
const emptyText = doc.getElementById("sr-empty-content");
const content = doc.getElementById("sr-content");
Assert.notEqual(
emptyText.style.display,
"none",
"The empty-state placeholder is visible when there are no routes"
);
doc.getElementById("sr-new-route").click();
Assert.equal(
content.querySelectorAll(".sr-rule-container").length,
1,
"Clicking 'New Route' injects one route element"
);
Assert.equal(
emptyText.style.display,
"none",
"The empty-state placeholder is hidden once a route exists"
);
Assert.equal(
gZenSpaceRoutingManager.getAllRoutes().length,
1,
"The new route is persisted into the manager"
);
} finally {
await closeRoutingDialog(dlg);
clearAllRoutes();
}
});
add_task(async function test_remove_route_via_ui() {
clearAllRoutes();
addRoute({ reference: "github.com" });
const dlg = await openRoutingDialog();
try {
const doc = dlg.document;
Assert.equal(
doc.querySelectorAll(".sr-rule-container").length,
1,
"Existing route is rendered on open"
);
doc.querySelector(".sr-remove").click();
Assert.equal(
doc.querySelectorAll(".sr-rule-container").length,
0,
"The route element is removed from the DOM"
);
Assert.equal(
gZenSpaceRoutingManager.getAllRoutes().length,
0,
"The route is removed from the manager"
);
Assert.equal(
doc.getElementById("sr-empty-content").style.display,
"flex",
"The empty-state placeholder returns after the last route is removed"
);
} finally {
await closeRoutingDialog(dlg);
clearAllRoutes();
}
});
add_task(async function test_match_type_updates_placeholder_and_store() {
clearAllRoutes();
const route = addRoute({ reference: "", matchType: "contains" });
const dlg = await openRoutingDialog();
try {
const doc = dlg.document;
const menulist = doc.querySelector(".sr-rule-container .match-type-select");
const input = doc.querySelector(".sr-rule-container .input");
Assert.equal(
input.placeholder,
"zen-browser.app",
"The 'contains' placeholder is the plain domain"
);
menulist.value = "regex";
menulist.dispatchEvent(new Event("command", { bubbles: true }));
Assert.equal(
input.placeholder,
"zen-browser\\.app",
"Switching to 'regex' updates the placeholder to an escaped pattern"
);
Assert.equal(
gZenSpaceRoutingManager.getRoute(route.id).matchType,
"regex",
"The match type change is persisted to the manager"
);
} finally {
await closeRoutingDialog(dlg);
clearAllRoutes();
}
});
add_task(async function test_invalid_regex_is_flagged_and_not_saved() {
clearAllRoutes();
const route = addRoute({ reference: "", matchType: "regex" });
const dlg = await openRoutingDialog();
try {
const doc = dlg.document;
const input = doc.querySelector(".sr-rule-container .input");
input.value = "([";
input.dispatchEvent(new Event("input", { bubbles: true }));
ok(
input.classList.contains("invalid"),
"An invalid regex marks the input as invalid"
);
Assert.equal(
gZenSpaceRoutingManager.getRoute(route.id).reference,
"",
"An invalid regex is NOT written to the route"
);
input.value = "zen.*app";
input.dispatchEvent(new Event("input", { bubbles: true }));
ok(
!input.classList.contains("invalid"),
"A subsequently valid regex clears the invalid state"
);
Assert.equal(
gZenSpaceRoutingManager.getRoute(route.id).reference,
"zen.*app",
"A valid regex is written to the route"
);
} finally {
await closeRoutingDialog(dlg);
clearAllRoutes();
}
});
add_task(async function test_default_external_select_updates_store() {
clearAllRoutes();
await gZenWorkspaces.promiseInitialized;
gZenSpaceRoutingManager.setDefaultExternalRoute("most-recent-space");
const dlg = await openRoutingDialog();
try {
const doc = dlg.document;
const select = doc.getElementById("sr-default-external-open-in");
await TestUtils.waitForCondition(
() => select.querySelectorAll("menuitem").length > 1,
"External-default options were populated"
);
const workspaceUuid = gZenWorkspaces.getWorkspaces()[0].uuid;
select.value = workspaceUuid;
select.dispatchEvent(new Event("command", { bubbles: true }));
Assert.equal(
gZenSpaceRoutingManager.getDefaultExternalRoute(),
workspaceUuid,
"Changing the external-default select updates the manager"
);
} finally {
await closeRoutingDialog(dlg);
gZenSpaceRoutingManager.setDefaultExternalRoute("most-recent-space");
}
});
add_task(async function test_routes_are_saved_on_close() {
clearAllRoutes();
const dlg = await openRoutingDialog();
let saveCalls = 0;
const realSave = gZenSpaceRoutingManager.saveRoutes;
gZenSpaceRoutingManager.saveRoutes = function () {
saveCalls++;
return realSave.call(this);
};
try {
const closed = promiseRoutingDialogClosed();
dlg.close();
await TestUtils.waitForCondition(
() => saveCalls > 0,
"Closing the dialog flushes routes to disk via saveRoutes()"
);
await closed;
} finally {
delete gZenSpaceRoutingManager.saveRoutes;
}
});
add_task(async function test_open_broadcasts_kill_to_other_instances() {
clearAllRoutes();
let killNotified = false;
const observer = {
observe(_subject, topic) {
if (topic === "zen-space-routing-kill") {
killNotified = true;
}
},
};
Services.obs.addObserver(observer, "zen-space-routing-kill");
let dlg;
try {
dlg = await openRoutingDialog();
ok(
killNotified,
"Opening a dialog broadcasts 'zen-space-routing-kill' so others can close"
);
} finally {
Services.obs.removeObserver(observer, "zen-space-routing-kill");
if (dlg) {
await closeRoutingDialog(dlg);
}
}
});
add_task(async function test_kill_notification_closes_dialog() {
clearAllRoutes();
await openRoutingDialog();
const closed = promiseRoutingDialogClosed();
Services.obs.notifyObservers(null, "zen-space-routing-kill");
await closed;
const container = document.getElementById("window-modal-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,363 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const TARGET_WS = { uuid: "ws-target", containerTabId: 7 };
add_setup(async function () {
clearAllRoutes();
registerCleanupFunction(() => clearAllRoutes());
});
add_task(async function test_onBeforeAddTab_resolves_container_for_match() {
clearAllRoutes();
addRoute({
reference: "github.com",
matchType: "contains",
openIn: TARGET_WS.uuid,
});
const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] });
const result = gZenSpaceRoutingManager.onBeforeAddTab(
"https://github.com/zen",
{},
win
);
Assert.deepEqual(
result,
{
shouldEarlyExit: false,
userContextId: TARGET_WS.containerTabId,
isRouteFound: true,
targetRoute: TARGET_WS.uuid,
},
"A matching route resolves to the workspace's containerTabId"
);
});
add_task(async function test_onBeforeAddTab_no_match_returns_no_route() {
clearAllRoutes();
const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] });
const result = gZenSpaceRoutingManager.onBeforeAddTab(
"https://example.com",
{},
win
);
Assert.deepEqual(
result,
{
shouldEarlyExit: false,
userContextId: null,
isRouteFound: false,
targetRoute: "most-recent-space",
},
"An unmatched URL (most-recent-space) reports no container and no route"
);
});
add_task(async function test_onBeforeAddTab_route_to_missing_workspace() {
clearAllRoutes();
addRoute({
reference: "github.com",
matchType: "contains",
openIn: "ws-does-not-exist",
});
const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] });
const result = gZenSpaceRoutingManager.onBeforeAddTab(
"https://github.com",
{},
win
);
Assert.deepEqual(
result,
{
shouldEarlyExit: false,
userContextId: null,
isRouteFound: false,
targetRoute: "ws-does-not-exist",
},
"A route to a non-existent workspace yields no container and no route"
);
});
add_task(async function test_onBeforeAddTab_skips_special_tab_options() {
clearAllRoutes();
addRoute({
reference: "github.com",
matchType: "contains",
openIn: TARGET_WS.uuid,
});
const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] });
for (const skipOption of ["skipRoute", "pinned", "tabGroup"]) {
const result = gZenSpaceRoutingManager.onBeforeAddTab(
"https://github.com/zen",
{ [skipOption]: true },
win
);
Assert.deepEqual(
result,
{
shouldEarlyExit: false,
userContextId: null,
isRouteFound: false,
targetRoute: null,
},
`Option '${skipOption}' skips routing even though a rule matches`
);
}
});
add_task(async function test_onBeforeAddTab_skips_until_startup_ready() {
clearAllRoutes();
addRoute({
reference: "github.com",
matchType: "contains",
openIn: TARGET_WS.uuid,
});
const win = makeFakeWindow({ ready: false, workspaces: [TARGET_WS] });
const result = gZenSpaceRoutingManager.onBeforeAddTab(
"https://github.com/zen",
{},
win
);
Assert.deepEqual(
result,
{
shouldEarlyExit: false,
userContextId: null,
isRouteFound: false,
targetRoute: null,
},
"While gZenStartup.isReady is false (session restore), routing is skipped"
);
});
add_task(async function test_onAfterAddTab_moves_tab_on_non_origin_window() {
clearAllRoutes();
addRoute({
reference: "github.com",
matchType: "contains",
openIn: TARGET_WS.uuid,
});
const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] });
const fakeTab = { parentNode: {} };
gZenSpaceRoutingManager.onAfterAddTab(
"https://github.com/zen",
fakeTab,
{},
win,
{ targetRoute: TARGET_WS.uuid }
);
await TestUtils.waitForCondition(
() => win.gZenWorkspaces.moveCalls.length === 1,
"moveTabToWorkspace was called once"
);
Assert.equal(
win.gZenWorkspaces.moveCalls[0].uuid,
TARGET_WS.uuid,
"The tab is moved to the matched workspace"
);
Assert.equal(
win.gZenWorkspaces.moveCalls[0].tab,
fakeTab,
"The correct tab element is moved"
);
Assert.equal(
win.gZenWorkspaces.changeCalls.length,
0,
"A non-originating window does not switch the active workspace"
);
});
add_task(async function test_onAfterAddTab_reuses_before_result() {
clearAllRoutes();
const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] });
const fakeTab = { parentNode: {} };
// No routes exist, so a fresh routeUri() would yield "most-recent-space" and
// move nothing. The tab is still moved to TARGET_WS, proving onAfterAddTab
// routes purely from the precomputed result rather than recomputing.
gZenSpaceRoutingManager.onAfterAddTab(
"https://example.com",
fakeTab,
{},
win,
{ targetRoute: TARGET_WS.uuid }
);
await TestUtils.waitForCondition(
() => win.gZenWorkspaces.moveCalls.length === 1,
"moveTabToWorkspace used the precomputed route"
);
Assert.equal(
win.gZenWorkspaces.moveCalls[0].uuid,
TARGET_WS.uuid,
"onAfterAddTab routes using the precomputed targetRoute, not a fresh routeUri()"
);
});
add_task(async function test_onAfterAddTab_ignores_detached_tab() {
clearAllRoutes();
const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] });
gZenSpaceRoutingManager.onAfterAddTab(
"https://github.com/zen",
{ parentNode: null },
{},
win,
{ targetRoute: TARGET_WS.uuid }
);
await flushEventLoop();
Assert.equal(
win.gZenWorkspaces.moveCalls.length,
0,
"A detached tab (no parentNode) is never moved"
);
});
add_task(
async function test_onAfterAddTab_does_nothing_for_most_recent_space() {
clearAllRoutes();
const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] });
gZenSpaceRoutingManager.onAfterAddTab(
"https://example.com",
{ parentNode: {} },
{},
win,
{ targetRoute: "most-recent-space" }
);
await flushEventLoop();
Assert.equal(
win.gZenWorkspaces.moveCalls.length,
0,
"A 'most-recent-space' route does not move the tab"
);
}
);
add_task(async function test_onAfterAddTab_does_nothing_when_skipped() {
clearAllRoutes();
const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] });
// onBeforeAddTab reports targetRoute null for skipped/unready tabs; without a
// route there is nothing for onAfterAddTab to do.
gZenSpaceRoutingManager.onAfterAddTab(
"https://github.com/zen",
{ parentNode: {} },
{},
win,
{ targetRoute: null }
);
await flushEventLoop();
Assert.equal(
win.gZenWorkspaces.moveCalls.length,
0,
"A null targetRoute (skipped tab) is not routed"
);
});
add_task(async function test_onAfterAddTab_ignores_missing_before_result() {
clearAllRoutes();
const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] });
gZenSpaceRoutingManager.onAfterAddTab(
"https://github.com/zen",
{ parentNode: {} },
{},
win
);
await flushEventLoop();
Assert.equal(
win.gZenWorkspaces.moveCalls.length,
0,
"Without a beforeResult there is no precomputed route, so nothing is moved"
);
});
add_task(async function test_onAfterAddTab_activates_workspace_on_origin() {
clearAllRoutes();
await gZenWorkspaces.promiseInitialized;
await gZenWorkspaces.createAndSaveWorkspace("SR Origin Test");
const workspaces = gZenWorkspaces.getWorkspaces();
const target = workspaces[workspaces.length - 1];
const isOriginating =
window === Services.wm.getMostRecentWindow("navigator:browser");
ok(isOriginating, "Precondition: the test window is the most-recent window");
const ws = window.gZenWorkspaces;
const origMove = ws.moveTabToWorkspace;
const origChange = ws.changeWorkspace;
const origLastSelected = ws.lastSelectedWorkspaceTabs;
let moved = null;
let changedTo = null;
ws.lastSelectedWorkspaceTabs = {};
ws.moveTabToWorkspace = (tab, uuid) => {
moved = { tab, uuid };
};
ws.changeWorkspace = workspace => {
changedTo = workspace;
return Promise.resolve();
};
const tab = BrowserTestUtils.addTab(gBrowser, "about:blank", {
skipAnimation: true,
skipRoute: true,
});
try {
gZenSpaceRoutingManager.onAfterAddTab(
"https://github.com/zen",
tab,
{},
window,
{ targetRoute: target.uuid }
);
await TestUtils.waitForCondition(
() => moved,
"moveTabToWorkspace was called"
);
Assert.equal(moved.uuid, target.uuid, "Moved to the matched workspace");
Assert.equal(moved.tab, tab, "Moved the tab we passed in");
await TestUtils.waitForCondition(
() => changedTo,
"changeWorkspace was called on the originating window"
);
Assert.equal(
changedTo.uuid,
target.uuid,
"Activated the matched workspace"
);
Assert.equal(
ws.lastSelectedWorkspaceTabs[target.uuid],
tab,
"The moved tab is remembered as the workspace's last-selected tab"
);
} finally {
ws.moveTabToWorkspace = origMove;
ws.changeWorkspace = origChange;
ws.lastSelectedWorkspaceTabs = origLastSelected;
BrowserTestUtils.removeTab(tab);
await gZenWorkspaces.removeWorkspace(target.uuid);
}
});

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

@@ -1,108 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(async function test_contains_is_case_insensitive_substring() {
const route = { reference: "GitHub", matchType: "contains" };
ok(
gZenSpaceRoutingManager.isRouteMatching("https://github.com/zen", route),
"'contains' matches a substring regardless of case"
);
ok(
gZenSpaceRoutingManager.isRouteMatching("https://api.GITHUB.com/v3", route),
"'contains' matches when the URL casing differs from the reference"
);
ok(
!gZenSpaceRoutingManager.isRouteMatching("https://gitlab.com/zen", route),
"'contains' rejects a URL that does not include the reference"
);
});
add_task(async function test_equal_to_normalizes_protocol_and_www() {
const route = { reference: "github.com", matchType: "equal-to" };
ok(
gZenSpaceRoutingManager.isRouteMatching("https://www.github.com/", route),
"'equal-to' ignores https://, www. and a trailing slash"
);
ok(
gZenSpaceRoutingManager.isRouteMatching("HTTP://GitHub.com", route),
"'equal-to' is case-insensitive and strips http://"
);
ok(
!gZenSpaceRoutingManager.isRouteMatching("https://github.com/zen", route),
"'equal-to' does not match when a path is present (not an exact host)"
);
ok(
!gZenSpaceRoutingManager.isRouteMatching("https://notgithub.com", route),
"'equal-to' requires the whole normalized URL to be equal"
);
});
add_task(async function test_regex_match_is_case_sensitive_on_raw_uri() {
ok(
gZenSpaceRoutingManager.isRouteMatching("https://zen-browser.app", {
reference: "^https://.*\\.app$",
matchType: "regex",
}),
"'regex' matches against the raw URI"
);
ok(
!gZenSpaceRoutingManager.isRouteMatching("https://github.com", {
reference: "GitHub",
matchType: "regex",
}),
"'regex' is case-sensitive (no implicit lower-casing like 'contains')"
);
});
add_task(async function test_invalid_regex_is_swallowed() {
let threw = false;
let result;
try {
result = gZenSpaceRoutingManager.isRouteMatching(
"https://zen-browser.app",
{
reference: "([",
matchType: "regex",
}
);
} catch (e) {
threw = true;
}
ok(!threw, "An invalid regex does not throw out of isRouteMatching");
Assert.strictEqual(result, false, "An invalid regex never matches");
});
add_task(async function test_empty_reference_never_matches() {
for (const matchType of ["contains", "equal-to", "regex"]) {
ok(
!gZenSpaceRoutingManager.isRouteMatching("https://github.com", {
reference: "",
matchType,
}),
`An empty reference never matches (${matchType})`
);
ok(
!gZenSpaceRoutingManager.isRouteMatching("https://github.com", {
reference: " ",
matchType,
}),
`A whitespace-only reference never matches (${matchType})`
);
}
});
add_task(async function test_unknown_match_type_does_not_match() {
ok(
!gZenSpaceRoutingManager.isRouteMatching("https://github.com", {
reference: "github.com",
matchType: "starts-with",
}),
"An unsupported match type falls through to no match"
);
});

View File

@@ -1,66 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_setup(async function () {
clearAllRoutes();
const savedDefault = gZenSpaceRoutingManager.getDefaultExternalRoute();
registerCleanupFunction(() => {
clearAllRoutes();
gZenSpaceRoutingManager.setDefaultExternalRoute(savedDefault);
});
});
add_task(async function test_no_match_returns_most_recent_space() {
clearAllRoutes();
addRoute({ reference: "github.com", matchType: "contains", openIn: "ws-1" });
Assert.equal(
gZenSpaceRoutingManager.routeUri("https://example.com", {}),
"most-recent-space",
"A non-matching, non-external URL routes to most-recent-space"
);
});
add_task(async function test_first_matching_route_wins() {
clearAllRoutes();
addRoute({ reference: "github", matchType: "contains", openIn: "ws-first" });
addRoute({ reference: "github", matchType: "contains", openIn: "ws-second" });
Assert.equal(
gZenSpaceRoutingManager.routeUri("https://github.com/zen", {}),
"ws-first",
"The openIn of the first matching route is returned, later matches ignored"
);
});
add_task(async function test_external_default_only_applies_without_match() {
clearAllRoutes();
gZenSpaceRoutingManager.setDefaultExternalRoute("ws-external");
addRoute({ reference: "github", matchType: "contains", openIn: "ws-rule" });
Assert.equal(
gZenSpaceRoutingManager.routeUri("https://github.com", {
fromExternal: true,
}),
"ws-rule",
"A matching rule wins even for external links"
);
Assert.equal(
gZenSpaceRoutingManager.routeUri("https://example.com", {
fromExternal: true,
}),
"ws-external",
"An unmatched external link uses the default external route"
);
Assert.equal(
gZenSpaceRoutingManager.routeUri("https://example.com", {
fromExternal: false,
}),
"most-recent-space",
"An unmatched internal link ignores the external default"
);
});

View File

@@ -1,102 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { gZenSpaceRoutingManager } = ChromeUtils.importESModule(
"resource:///modules/zen/spacerouting/ZenSpaceRoutingManager.sys.mjs"
);
const SR_DIALOG_URI =
"chrome://browser/content/zen-components/windows/zen-space-routing.xhtml";
function clearAllRoutes() {
for (const route of gZenSpaceRoutingManager.getAllRoutes()) {
gZenSpaceRoutingManager.removeRoute(route.id);
}
}
function addRoute({
reference = "",
openIn = "most-recent-space",
matchType = "contains",
} = {}) {
const route = gZenSpaceRoutingManager.createNewRoute();
route.reference = reference;
route.openIn = openIn;
route.matchType = matchType;
gZenSpaceRoutingManager.updateRoute(route);
return route;
}
function makeFakeWindow({
ready = true,
workspaces = [],
workspaceEnabled = true,
} = {}) {
return {
gZenStartup: { isReady: ready },
gZenWorkspaces: {
workspaceEnabled,
moveCalls: [],
changeCalls: [],
lastSelectedWorkspaceTabs: {},
getWorkspaceFromId(id) {
return workspaces.find(w => w.uuid === id) || null;
},
moveTabToWorkspace(tab, uuid) {
this.moveCalls.push({ tab, uuid });
},
changeWorkspace(workspace) {
this.changeCalls.push(workspace);
return Promise.resolve();
},
},
};
}
async function flushEventLoop() {
for (let i = 0; i < 5; i++) {
await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
}
}
async function openRoutingDialog() {
// openSpaceRoutingDialog() presents an in-window modal through gDialogBox, so
// the dialog is a subdialog rather than a separate top-level window.
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));
const dialogWin = await dialogPromise;
await TestUtils.waitForCondition(
() => dialogWin.spaceroutingDialog?.initialized,
"Space Routing dialog finished initializing"
);
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) {
const closed = promiseRoutingDialogClosed();
dialogWin.close();
await closed;
}

View File

@@ -26,8 +26,6 @@ support-files = [
["browser_private_mode_startup.js"]
["browser_select_tab_switches_space.js"]
["browser_unload_all_other_spaces.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");
}
// 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() {

View File

@@ -81,11 +81,6 @@ const globalActionsTemplate = [
return !tab.hasAttribute("zen-empty-tab") && tab.pinned;
},
},
{
label: "Open Space Routing",
command: "cmd_zenOpenSpaceRoutingSettings",
icon: "chrome://browser/skin/zen-icons/selectable/airplane.svg",
},
{
label: "New Boost",
icon: "chrome://browser/skin/zen-icons/boost.svg",

View File

@@ -40,8 +40,6 @@ export default [
"gZenViewSplitter",
"gZenSpaceRoutingManager",
"Ci",
"Cu",
"Cc",

View File

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