Files
tubestation/waterfox/browser/components/sidebar/background/handle-new-tabs.js
2025-11-06 14:13:52 +00:00

599 lines
23 KiB
JavaScript

/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import {
log as internalLogger,
dumpTab,
configs,
isFirefoxViewTab,
} from '/common/common.js';
import * as Constants from '/common/constants.js';
import * as TabsInternalOperation from '/common/tabs-internal-operation.js';
import * as TabsStore from '/common/tabs-store.js';
import * as TreeBehavior from '/common/tree-behavior.js';
import * as TSTAPI from '/common/tst-api.js';
import { Tab } from '/common/TreeItem.js';
import * as TabsMove from './tabs-move.js';
import * as TabsOpen from './tabs-open.js';
import * as Tree from './tree.js';
function log(...args) {
internalLogger('background/handle-new-tabs', ...args);
}
Tab.onBeforeCreate.addListener(async (tab, info) => {
const activeTab = info.activeTab || Tab.getActiveTab(tab.windowId);
// Special case, when all these conditions are true:
// 1) A new blank tab is configured to be opened as a child of the active tab.
// 2) The active tab is pinned.
// 3) Tabs opened from a pinned parent are configured to be placed near the
// opener pinned tab.
// then we fakely attach the new blank tab to the active pinned tab.
// See also https://github.com/piroor/treestyletab/issues/3296
const shouldAttachToPinnedOpener = (
!tab.openerTabId &&
!tab.pinned &&
tab.$TST.isNewTabCommandTab &&
Constants.kCONTROLLED_NEWTAB_POSITION.has(configs.autoAttachOnNewTabCommand) &&
(
(activeTab?.pinned &&
Constants.kCONTROLLED_INSERTION_POSITION.has(configs.insertNewTabFromPinnedTabAt)) ||
(isFirefoxViewTab(activeTab) &&
Constants.kCONTROLLED_INSERTION_POSITION.has(configs.insertNewTabFromFirefoxViewAt))
)
);
if (shouldAttachToPinnedOpener)
tab.openerTabId = activeTab.id;
});
// this should return false if the tab is / may be moved while processing
Tab.onCreating.addListener((tab, info = {}) => {
if (info.duplicatedInternally)
return true;
log('Tabs.onCreating ', dumpTab(tab), tab.openerTabId, info);
const activeTab = info.activeTab || Tab.getActiveTab(tab.windowId);
const opener = tab.$TST.openerTab;
if (opener) {
tab.$TST.setAttribute(Constants.kPERSISTENT_ORIGINAL_OPENER_TAB_ID, opener.$TST.uniqueId.id);
if (!info.bypassTabControl)
TabsStore.addToBeGroupedTab(tab);
}
else {
let dontMove = false;
if (!info.maybeOrphan &&
!info.bypassTabControl &&
activeTab &&
!info.restored) {
let autoAttachBehavior = configs.autoAttachOnNewTabCommand;
if (tab.$TST.nextTab &&
activeTab == tab.$TST.previousTab) {
// New tab opened with browser.tabs.insertAfterCurrent=true may have
// next tab. In this case the tab is expected to be placed next to the
// active tab always, so we should change the behavior specially.
// See also:
// https://github.com/piroor/treestyletab/issues/2054
// https://github.com/piroor/treestyletab/issues/2194#issuecomment-505272940
dontMove = true;
switch (autoAttachBehavior) {
case Constants.kNEWTAB_OPEN_AS_ORPHAN:
case Constants.kNEWTAB_OPEN_AS_SIBLING:
case Constants.kNEWTAB_OPEN_AS_NEXT_SIBLING:
if (activeTab.$TST.hasChild)
autoAttachBehavior = Constants.kNEWTAB_OPEN_AS_CHILD;
else
autoAttachBehavior = Constants.kNEWTAB_OPEN_AS_NEXT_SIBLING;
break;
case Constants.kNEWTAB_OPEN_AS_CHILD:
default:
break;
}
}
if (tab.$TST.isNewTabCommandTab) {
if (!info.positionedBySelf) {
log('behave as a tab opened by new tab command');
return handleNewTabFromActiveTab(tab, {
activeTab,
autoAttachBehavior,
dontMove,
openedWithCookieStoreId: info.openedWithCookieStoreId,
inheritContextualIdentityMode: configs.inheritContextualIdentityToChildTabMode,
context: TSTAPI.kNEWTAB_CONTEXT_NEWTAB_COMMAND,
}).then(moved => !moved);
}
return false;
}
else if (activeTab != tab) {
tab.$TST.temporaryMetadata.set('possibleOpenerTab', activeTab.id);
}
if (!info.fromExternal)
tab.$TST.temporaryMetadata.set('isNewTab', true);
}
if (info.fromExternal &&
!info.bypassTabControl) {
log('behave as a tab opened from external application');
// we may need to reopen the tab with loaded URL
if (configs.inheritContextualIdentityToTabsFromExternalMode != Constants.kCONTEXTUAL_IDENTITY_DEFAULT)
tab.$TST.temporaryMetadata.set('fromExternal', true);
return notifyToTryHandleNewTab(tab, {
context: TSTAPI.kNEWTAB_CONTEXT_FROM_EXTERNAL,
activeTab,
}).then(allowed => {
if (!allowed) {
log(' => handling is canceled by someone');
return true;
}
return Tree.behaveAutoAttachedTab(tab, {
baseTab: activeTab,
behavior: configs.autoAttachOnOpenedFromExternal,
dontMove,
broadcast: true
}).then(moved => !moved);
});
}
log('behave as a tab opened with any URL ', tab.title, tab.url);
if (!info.restored &&
!info.positionedBySelf &&
!info.bypassTabControl &&
configs.autoAttachOnAnyOtherTrigger != Constants.kNEWTAB_DO_NOTHING) {
if (configs.inheritContextualIdentityToTabsFromAnyOtherTriggerMode != Constants.kCONTEXTUAL_IDENTITY_DEFAULT)
tab.$TST.temporaryMetadata.set('anyOtherTrigger', true);
log('controlled as a new tab from other unknown trigger');
return notifyToTryHandleNewTab(tab, {
context: TSTAPI.kNEWTAB_CONTEXT_UNKNOWN,
activeTab,
}).then(allowed => {
if (!allowed) {
log(' => handling is canceled by someone');
return true;
}
return Tree.behaveAutoAttachedTab(tab, {
baseTab: activeTab,
behavior: configs.autoAttachOnAnyOtherTrigger,
dontMove,
broadcast: true
}).then(moved => !moved);
});
}
if (info.positionedBySelf)
tab.$TST.temporaryMetadata.set('positionedBySelf', true);
return true;
}
log(`opener: ${dumpTab(opener)}, positionedBySelf = ${info.positionedBySelf}`);
if (!info.bypassTabControl &&
opener &&
(opener.pinned || isFirefoxViewTab(opener)) &&
opener.windowId == tab.windowId) {
return handleTabsFromPinnedOpener(tab, opener, { activeTab }).then(moved => !moved);
}
else if (!info.maybeOrphan || info.bypassTabControl) {
if (info.fromExternal &&
!info.bypassTabControl &&
configs.inheritContextualIdentityToTabsFromExternalMode != Constants.kCONTEXTUAL_IDENTITY_DEFAULT)
tab.$TST.temporaryMetadata.set('fromExternal', true);
return notifyToTryHandleNewTab(tab, {
context: info.fromExternal && !info.bypassTabControl ?
TSTAPI.kNEWTAB_CONTEXT_FROM_EXTERNAL :
info.duplicated ?
TSTAPI.kNEWTAB_CONTEXT_DUPLICATED :
TSTAPI.kNEWTAB_CONTEXT_WITH_OPENER,
activeTab,
openerTab: opener,
}).then(allowed => {
if (!allowed) {
log(' => handling is canceled by someone');
return true;
}
const behavior = info.fromExternal && !info.bypassTabControl ?
configs.autoAttachOnOpenedFromExternal :
info.duplicated ?
configs.autoAttachOnDuplicated :
configs.autoAttachOnOpenedWithOwner;
return Tree.behaveAutoAttachedTab(tab, {
baseTab: opener,
behavior,
dontMove: info.positionedBySelf || info.mayBeReplacedWithContainer,
broadcast: true
}).then(moved => !moved);
});
}
return true;
});
async function notifyToTryHandleNewTab(tab, { context, activeTab, openerTab } = {}) {
const cache = {};
const result = TSTAPI.tryOperationAllowed(
TSTAPI.kNOTIFY_TRY_HANDLE_NEWTAB,
{ tab,
activeTab,
openerTab,
context },
{ tabProperties: ['tab', 'activeTab', 'openerTab'], cache }
);
TSTAPI.clearCache(cache);
return result;
}
async function handleNewTabFromActiveTab(tab, { url, activeTab, autoAttachBehavior, dontMove, openedWithCookieStoreId, inheritContextualIdentityMode, context } = {}) {
log('handleNewTabFromActiveTab: activeTab = ', dumpTab(activeTab), { url, activeTab, autoAttachBehavior, dontMove, inheritContextualIdentityMode, context });
if (activeTab &&
activeTab.$TST.ancestors.includes(tab)) {
log(' => ignore restored ancestor tab');
return false;
}
const allowed = await notifyToTryHandleNewTab(tab, {
context,
activeTab,
});
if (!allowed) {
log(' => handling is canceled by someone');
return false;
}
const moved = await Tree.behaveAutoAttachedTab(tab, {
baseTab: activeTab,
behavior: autoAttachBehavior,
broadcast: true,
dontMove: dontMove || false
});
if (openedWithCookieStoreId) {
log('handleNewTabFromActiveTab: do not reopen tab opened with contextual identity explicitly');
return moved;
}
if (tab.cookieStoreId && tab.cookieStoreId != 'firefox-default') {
log('handleNewTabFromActiveTab: do not reopen tab opened with non-default contextual identity ', tab.cookieStoreId);
return moved;
}
const parent = tab.$TST.parent;
let cookieStoreId = null;
switch (inheritContextualIdentityMode) {
case Constants.kCONTEXTUAL_IDENTITY_FROM_PARENT:
if (parent)
cookieStoreId = parent.cookieStoreId;
break;
case Constants.kCONTEXTUAL_IDENTITY_FROM_LAST_ACTIVE:
cookieStoreId = activeTab.cookieStoreId
break;
default:
return moved;
}
if ((tab.cookieStoreId || 'firefox-default') == (cookieStoreId || 'firefox-default')) {
log('handleNewTabFromActiveTab: no need to reopen with inherited contextual identity ', cookieStoreId);
return moved;
}
if (!configs.inheritContextualIdentityToUnopenableURLTabs &&
!TabsOpen.isOpenable(url)) {
log('handleNewTabFromActiveTab: not openable URL, skip reopening ', cookieStoreId, url);
return moved;
}
log('handleNewTabFromActiveTab: reopen with inherited contextual identity ', cookieStoreId);
// We need to prevent grouping of this original tab and the reopened tab
// by the "multiple tab opened in XXX msec" feature.
const win = TabsStore.windows.get(tab.windowId);
win.openedNewTabs.delete(tab.id);
await TabsOpen.openURIInTab(url || null, {
windowId: activeTab.windowId,
parent,
insertBefore: tab,
active: tab.active,
cookieStoreId
});
TabsInternalOperation.removeTab(tab);
return moved;
}
async function handleTabsFromPinnedOpener(tab, opener, { activeTab } = {}) {
const allowed = await notifyToTryHandleNewTab(tab, {
context: TSTAPI.kNEWTAB_CONTEXT_FROM_PINNED,
activeTab,
openerTab: opener,
});
if (!allowed) {
log('handleTabsFromPinnedOpener: handling is canceled by someone');
return false;
}
const parent = Tab.getGroupTabForOpener(opener);
if (parent) {
log('handleTabsFromPinnedOpener: attach to corresponding group tab');
tab.$TST.setAttribute(Constants.kPERSISTENT_ALREADY_GROUPED_FOR_PINNED_OPENER, true);
tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
// it could be updated already...
const lastRelatedTab = opener.$TST.lastRelatedTabId == tab.id ?
opener.$TST.previousLastRelatedTab :
opener.$TST.lastRelatedTab;
// If there is already opened group tab, it is more natural that
// opened tabs are treated as a tab opened from unpinned tabs.
const insertAt = configs.autoAttachOnOpenedWithOwner == Constants.kNEWTAB_OPEN_AS_CHILD_NEXT_TO_LAST_RELATED_TAB ?
Constants.kINSERT_NEXT_TO_LAST_RELATED_TAB :
configs.autoAttachOnOpenedWithOwner == Constants.kNEWTAB_OPEN_AS_CHILD_TOP ?
Constants.kINSERT_TOP :
configs.autoAttachOnOpenedWithOwner == Constants.kNEWTAB_OPEN_AS_CHILD_END ?
Constants.kINSERT_END :
undefined;
return Tree.attachTabTo(tab, parent, {
lastRelatedTab,
insertAt,
forceExpand: true, // this is required to avoid the group tab itself is active from active tab in collapsed tree
broadcast: true
});
}
if ((configs.autoGroupNewTabsFromPinned ||
configs.autoGroupNewTabsFromFirefoxView) &&
tab.$TST.needToBeGroupedSiblings.length > 0) {
log('handleTabsFromPinnedOpener: controlled by auto-grouping');
return false;
}
switch (isFirefoxViewTab(opener) ? configs.insertNewTabFromFirefoxViewAt : configs.insertNewTabFromPinnedTabAt) {
case Constants.kINSERT_NEXT_TO_LAST_RELATED_TAB: {
// it could be updated already...
const lastRelatedTab = opener.$TST.lastRelatedTab != tab ?
opener.$TST.lastRelatedTab :
opener.$TST.previousLastRelatedTab;
if (lastRelatedTab) {
log(`handleTabsFromPinnedOpener: place after last related tab ${dumpTab(lastRelatedTab)}`);
tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
return TabsMove.moveTabAfter(tab, lastRelatedTab.$TST.lastDescendant || lastRelatedTab, {
delayedMove: true,
broadcast: true
});
}
const lastPinnedTab = Tab.getLastPinnedTab(tab.windowId);
if (lastPinnedTab) {
log(`handleTabsFromPinnedOpener: place after last pinned tab ${dumpTab(lastPinnedTab)}`);
tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
return TabsMove.moveTabAfter(tab, lastPinnedTab, {
delayedMove: true,
broadcast: true
});
}
const firstNormalTab = Tab.getFirstNormalTab(tab.windowId);
if (firstNormalTab) {
log(`handleTabsFromPinnedOpener: place before first unpinned tab ${dumpTab(firstNormalTab)}`);
tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
return TabsMove.moveTabBefore(tab, firstNormalTab, {
delayedMove: true,
broadcast: true
});
}
};
case Constants.kINSERT_TOP: {
const lastPinnedTab = Tab.getLastPinnedTab(tab.windowId);
if (lastPinnedTab) {
log(`handleTabsFromPinnedOpener: opened from pinned opener: place after last pinned tab ${dumpTab(lastPinnedTab)}`);
tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
return TabsMove.moveTabAfter(tab, lastPinnedTab, {
delayedMove: true,
broadcast: true
});
}
const firstNormalTab = Tab.getFirstNormalTab(tab.windowId);
if (firstNormalTab) {
log(`handleTabsFromPinnedOpener: opened from pinned opener: place before first pinned tab ${dumpTab(firstNormalTab)}`);
tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
return TabsMove.moveTabBefore(tab, firstNormalTab, {
delayedMove: true,
broadcast: true
});
}
}; break;
case Constants.kINSERT_END: {
const lastTab = Tab.getLastTab(tab.windowId);
log('handleTabsFromPinnedOpener: opened from pinned opener: place after the last tab ', lastTab);
tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
return TabsMove.moveTabAfter(tab, lastTab, {
delayedMove: true,
broadcast: true
});
};
}
return Promise.resolve(false);
}
Tab.onCreated.addListener((tab, info = {}) => {
if (!info.duplicated ||
info.bypassTabControl)
return;
const original = info.originalTab;
log('duplicated ', dumpTab(tab), dumpTab(original));
if (info.duplicatedInternally) {
log('duplicated by internal operation');
tab.$TST.addState(Constants.kTAB_STATE_DUPLICATING, { broadcast: true });
TabsStore.addDuplicatingTab(tab);
}
else {
// On old versions of Firefox, duplicated tabs had no openerTabId so they were
// not handled by Tab.onCreating listener. Today they are already handled before
// here, so this is just a failsafe (or for old versions of Firefox).
// See also: https://github.com/piroor/treestyletab/issues/2830#issuecomment-831414189
Tree.behaveAutoAttachedTab(tab, {
baseTab: original,
behavior: configs.autoAttachOnDuplicated,
dontMove: info.positionedBySelf || info.movedBySelfWhileCreation || info.mayBeReplacedWithContainer,
broadcast: true
});
}
});
Tab.onUpdated.addListener((tab, changeInfo) => {
if ('openerTabId' in changeInfo &&
configs.syncParentTabAndOpenerTab &&
!tab.$TST.updatingOpenerTabIds.includes(changeInfo.openerTabId) /* accept only changes from outside of TST */) {
Tab.waitUntilTrackedAll(tab.windowId).then(() => {
const parent = tab.$TST.openerTab;
if (!parent ||
parent.windowId != tab.windowId ||
parent == tab.$TST.parent)
return;
Tree.attachTabTo(tab, parent, {
insertAt: Constants.kINSERT_NEAREST,
forceExpand: tab.active,
broadcast: true
});
});
}
if (tab.$TST.temporaryMetadata.has('openedCompletely') &&
tab.windowId == tab.$windowIdOnCreated && // Don't treat tab as "opened from active tab" if it is moved across windows while loading
(changeInfo.url || changeInfo.status == 'complete') &&
(tab.$TST.temporaryMetadata.has('isNewTab') ||
tab.$TST.temporaryMetadata.has('fromExternal') ||
tab.$TST.temporaryMetadata.has('anyOtherTrigger'))) {
log('loaded tab ', dumpTab(tab), {
isNewTab: tab.$TST.temporaryMetadata.has('isNewTab'),
fromExternal: tab.$TST.temporaryMetadata.has('fromExternal'),
anyOtherTrigger: tab.$TST.temporaryMetadata.has('anyOtherTrigger'),
});
tab.$TST.temporaryMetadata.delete('isNewTab');
const possibleOpenerTab = Tab.get(tab.$TST.temporaryMetadata.get('possibleOpenerTab'));
tab.$TST.temporaryMetadata.delete('possibleOpenerTab');
log('possibleOpenerTab ', dumpTab(possibleOpenerTab));
if (tab.$TST.temporaryMetadata.has('fromExternal')) {
tab.$TST.temporaryMetadata.delete('fromExternal');
log('behave as a tab opened from external application (delayed)');
handleNewTabFromActiveTab(tab, {
url: tab.url,
activeTab: possibleOpenerTab,
autoAttachBehavior: configs.autoAttachOnOpenedFromExternal,
inheritContextualIdentityMode: configs.inheritContextualIdentityToTabsFromExternalMode,
context: TSTAPI.kNEWTAB_CONTEXT_FROM_EXTERNAL,
});
return;
}
if (tab.$TST.temporaryMetadata.has('anyOtherTrigger')) {
tab.$TST.temporaryMetadata.delete('anyOtherTrigger');
log('behave as a tab opened from any other trigger (delayed)');
handleNewTabFromActiveTab(tab, {
url: tab.url,
activeTab: possibleOpenerTab,
autoAttachBehavior: configs.autoAttachOnAnyOtherTrigger,
inheritContextualIdentityMode: configs.inheritContextualIdentityToTabsFromAnyOtherTriggerMode,
context: TSTAPI.kNEWTAB_CONTEXT_UNKNOWN,
});
return;
}
const win = TabsStore.windows.get(tab.windowId);
log('win.openedNewTabs ', win.openedNewTabs);
if (tab.$TST.parent ||
!possibleOpenerTab ||
win.openedNewTabs.has(tab.id) ||
tab.$TST.temporaryMetadata.has('openedWithOthers') ||
tab.$TST.temporaryMetadata.has('positionedBySelf')) {
log(' => no need to control ', {
parent: tab.$TST.parent,
possibleOpenerTab,
openedNewTab: win.openedNewTabs.has(tab.id),
openedWithOthers: tab.$TST.temporaryMetadata.has('openedWithOthers'),
positionedBySelf: tab.$TST.temporaryMetadata.has('positionedBySelf')
});
return;
}
if (tab.$TST.isNewTabCommandTab) {
log('behave as a tab opened by new tab command (delayed)');
tab.$TST.addState(Constants.kTAB_STATE_NEW_TAB_COMMAND_TAB);
handleNewTabFromActiveTab(tab, {
activeTab: possibleOpenerTab,
autoAttachBehavior: configs.autoAttachOnNewTabCommand,
inheritContextualIdentityMode: configs.inheritContextualIdentityToChildTabMode,
context: TSTAPI.kNEWTAB_CONTEXT_NEWTAB_COMMAND,
});
return;
}
const siteMatcher = /^\w+:\/\/([^\/]+)(?:$|\/.*$)/;
const openerTabSite = possibleOpenerTab.url.match(siteMatcher);
const newTabSite = tab.url.match(siteMatcher);
if (openerTabSite &&
newTabSite &&
tab.url != possibleOpenerTab.url && // It may be opened by "Duplciate Tab" or "Open in New Container Tab" if the URL is completely same.
openerTabSite[1] == newTabSite[1]) {
log('behave as a tab opened from same site (delayed)');
tab.$TST.addState(Constants.kTAB_STATE_OPENED_FOR_SAME_WEBSITE);
handleNewTabFromActiveTab(tab, {
url: tab.url,
activeTab: possibleOpenerTab,
autoAttachBehavior: configs.autoAttachSameSiteOrphan,
inheritContextualIdentityMode: configs.inheritContextualIdentityToSameSiteOrphanMode,
context: TSTAPI.kNEWTAB_CONTEXT_WEBSITE_SAME_TO_ACTIVE_TAB,
});
return;
}
log('checking special openers (delayed)', { opener: possibleOpenerTab.url, child: tab.url });
for (const rule of Constants.kAGGRESSIVE_OPENER_TAB_DETECTION_RULES_WITH_URL) {
if (rule.opener.test(possibleOpenerTab.url) &&
rule.child.test(tab.url)) {
log('behave as a tab opened from special opener (delayed)', { rule });
handleNewTabFromActiveTab(tab, {
url: tab.url,
activeTab: possibleOpenerTab,
autoAttachBehavior: configs.autoAttachOnOpenedWithOwner,
context: TSTAPI.kNEWTAB_CONTEXT_FROM_ABOUT_ADDONS,
});
return;
}
}
}
});
Tab.onAttached.addListener(async (tab, attachInfo = {}) => {
if (!attachInfo.windowId)
return;
const parentTabOperationBehavior = TreeBehavior.getParentTabOperationBehavior(tab, {
context: Constants.kPARENT_TAB_OPERATION_CONTEXT_MOVE,
...attachInfo,
});
if (parentTabOperationBehavior != Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE)
return;
log('Tabs.onAttached ', dumpTab(tab), attachInfo);
log('descendants of attached tab: ', () => attachInfo.descendants.map(dumpTab));
const movedTabs = await Tree.moveTabs(attachInfo.descendants, {
destinationWindowId: tab.windowId,
insertAfter: tab
});
log('moved descendants: ', () => movedTabs.map(dumpTab));
if (attachInfo.descendants.length == movedTabs.length) {
await Tree.applyTreeStructureToTabs(
[tab, ...movedTabs],
attachInfo.structure
);
}
else {
for (const movedTab of movedTabs) {
Tree.attachTabTo(movedTab, tab, {
broadcast: true,
dontMove: true
});
}
}
});