mirror of
https://github.com/zen-browser/desktop.git
synced 2026-06-14 15:33:42 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7a8e79299 | ||
|
|
0048f21a52 |
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
9a6aa4c359d1fb6ac60decc82402f82d49a17cea
|
||||
5c4d14a559bf26eb4ab3e136d2084310ebe51ac0
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
4099
src/external-patches/firefox/corner_shape_support/D296935.patch
Normal file
4099
src/external-patches/firefox/corner_shape_support/D296935.patch
Normal file
File diff suppressed because it is too large
Load Diff
1199
src/external-patches/firefox/corner_shape_support/D297660.patch
Normal file
1199
src/external-patches/firefox/corner_shape_support/D297660.patch
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -133,10 +133,6 @@ document.addEventListener(
|
||||
gZenWorkspaces.unloadAllOtherWorkspaces();
|
||||
break;
|
||||
}
|
||||
case "cmd_zenOpenSpaceRoutingSettings": {
|
||||
gZenSpaceRoutingManager.openSpaceRoutingDialog(window);
|
||||
break;
|
||||
}
|
||||
case "cmd_zenNewNavigatorUnsynced":
|
||||
OpenBrowserWindow({ zenSyncedWindow: false });
|
||||
break;
|
||||
|
||||
@@ -214,7 +214,6 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
skipAnimation: true,
|
||||
ownerTab: currentTab,
|
||||
triggeringPrincipal: data.triggeringPrincipal,
|
||||
skipRoute: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -19,5 +19,4 @@ DIRS += [
|
||||
"sessionstore",
|
||||
"share",
|
||||
"spaces",
|
||||
"space-routing",
|
||||
]
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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)
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -20,5 +20,3 @@ support-files = [
|
||||
["browser_glance_prev_tab.js"]
|
||||
|
||||
["browser_glance_select_parent.js"]
|
||||
|
||||
["browser_issue_14049.js"]
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
44
src/zen/tests/mochitests/sandbox/browser.toml
Normal file
44
src/zen/tests/mochitests/sandbox/browser.toml
Normal 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",
|
||||
]
|
||||
203
src/zen/tests/mochitests/sandbox/browser_bug1393259.js
Normal file
203
src/zen/tests/mochitests/sandbox/browser_bug1393259.js
Normal 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);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -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'",
|
||||
]
|
||||
@@ -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'",
|
||||
]
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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
|
||||
});
|
||||
@@ -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
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
@@ -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
|
||||
});
|
||||
@@ -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
|
||||
});
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/zen/tests/mochitests/sandbox/browser_profiler.toml
Normal file
21
src/zen/tests/mochitests/sandbox/browser_profiler.toml
Normal 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'",
|
||||
]
|
||||
153
src/zen/tests/mochitests/sandbox/browser_sandbox_profiler.js
Normal file
153
src/zen/tests/mochitests/sandbox/browser_sandbox_profiler.js
Normal 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();
|
||||
});
|
||||
78
src/zen/tests/mochitests/sandbox/browser_sandbox_test.js
Normal file
78
src/zen/tests/mochitests/sandbox/browser_sandbox_test.js
Normal 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);
|
||||
}
|
||||
20
src/zen/tests/mochitests/sandbox/browser_snap.toml
Normal file
20
src/zen/tests/mochitests/sandbox/browser_snap.toml
Normal 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'",
|
||||
]
|
||||
26
src/zen/tests/mochitests/sandbox/browser_xdg_default.toml
Normal file
26
src/zen/tests/mochitests/sandbox/browser_xdg_default.toml
Normal 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'"
|
||||
]
|
||||
@@ -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'"
|
||||
]
|
||||
@@ -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'",
|
||||
]
|
||||
19
src/zen/tests/mochitests/sandbox/bug1393259.html
Normal file
19
src/zen/tests/mochitests/sandbox/bug1393259.html
Normal 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>
|
||||
85
src/zen/tests/mochitests/sandbox/mac_register_font.py
Executable file
85
src/zen/tests/mochitests/sandbox/mac_register_font.py
Executable 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()
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"]
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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() {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -40,8 +40,6 @@ export default [
|
||||
|
||||
"gZenViewSplitter",
|
||||
|
||||
"gZenSpaceRoutingManager",
|
||||
|
||||
"Ci",
|
||||
"Cu",
|
||||
"Cc",
|
||||
|
||||
10
surfer.json
10
surfer.json
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user