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

343 lines
12 KiB
JavaScript

/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is the Tree Style Tab.
*
* The Initial Developer of the Original Code is YUKI "Piro" Hiroshi.
* Portions created by the Initial Developer are Copyright (C) 2011-2024
* the Initial Developer. All Rights Reserved.
*
* Contributor(s): YUKI "Piro" Hiroshi <piro.outsider.reflex@gmail.com>
* wanabe <https://github.com/wanabe>
* Tetsuharu OHZEKI <https://github.com/saneyuki>
* Xidorn Quan <https://github.com/upsuper> (Firefox 40+ support)
* lv7777 (https://github.com/lv7777)
*
* ***** END LICENSE BLOCK ******/
'use strict';
import EventListenerManager from '/extlib/EventListenerManager.js';
import {
log as internalLogger,
configs,
tryRevokeObjectURL,
} from '/common/common.js';
import * as ApiTabs from '/common/api-tabs.js';
import * as Constants from '/common/constants.js';
import * as SidebarConnection from '/common/sidebar-connection.js';
import * as TabsStore from '/common/tabs-store.js';
import { Tab } from '/common/TreeItem.js';
import * as TabsMove from './tabs-move.js';
import * as Tree from './tree.js';
export const onForbiddenURLRequested = new EventListenerManager();
function log(...args) {
internalLogger('background/tabs-open', ...args);
}
const SEARCH_PREFIX_MATCHER = /^(ext\+ws:search:|about:ws-search\?)/;
export async function loadURI(uri, options = {}) {
if (!options.windowId && !options.tab)
throw new Error('missing loading target window or tab');
try {
let tabId;
if (options.tab) {
tabId = options.tab.id;
}
else {
let tabs = await browser.tabs.query({
windowId: options.windowId,
active: true
}).catch(ApiTabs.createErrorHandler());
if (tabs.length == 0)
tabs = await browser.tabs.query({
windowId: options.windowId,
}).catch(ApiTabs.createErrorHandler());
tabId = tabs[0].id;
}
let searchQuery = null;
if (SEARCH_PREFIX_MATCHER.test(uri)) {
const query = uri.replace(SEARCH_PREFIX_MATCHER, '');
if (browser.search &&
typeof browser.search.search == 'function')
searchQuery = query;
else
uri = configs.defaultSearchEngine.replace(/%s/gi, query);
}
if (searchQuery) {
await browser.search.search({
query: searchQuery,
tabId
}).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
}
else {
await browser.tabs.update(tabId, {
url: sanitizeURL(uri),
}).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
}
}
catch(e) {
ApiTabs.handleMissingTabError(e);
}
}
export async function openNewTab(options = {}) {
const win = TabsStore.windows.get(options.windowId);
win.toBeOpenedNewTabCommandTab++;
return openURIInTab(options.url || options.uri || null, options);
}
export async function openURIInTab(uri, options = {}) {
const tabs = await openURIsInTabs([uri], options);
return tabs[0];
}
const FORBIDDEN_URL_MATCHER = /^(about|chrome|resource|file):/;
const ALLOWED_URL_MATCHER = /^about:blank(\?|$)/;
export async function openURIsInTabs(uris, { windowId, insertBefore, insertAfter, cookieStoreId, isOrphan, active, inBackground, discarded, opener, parent, fixPositions } = {}) {
log('openURIsInTabs: ', { uris, windowId, insertBefore, insertAfter, cookieStoreId, isOrphan, active, inBackground, discarded, opener, parent, fixPositions });
if (!windowId)
throw new Error('missing loading target window\n' + new Error().stack);
const tabs = [];
// Don't return the result of Tab.doAndGetNewTabs because their order can
// be inverted due to browser.tabs.insertAfterCurrent=true
const actuallyOpenedTabIds = new Set(await Tab.doAndGetNewTabs(async () => {
await Tab.waitUntilTrackedAll(windowId);
await TabsMove.waitUntilSynchronized(windowId);
const startIndex = Tab.calculateNewTabIndex({ insertAfter, insertBefore });
log('startIndex: ', startIndex);
const win = TabsStore.windows.get(windowId);
if (insertBefore ||
insertAfter ||
uris.some(uri => uri && typeof uri == 'object' && 'index' in uri))
win.toBeOpenedTabsWithPositions += uris.length;
if (cookieStoreId)
win.toBeOpenedTabsWithCookieStoreId += uris.length;
if (isOrphan)
win.toBeOpenedOrphanTabs += uris.length;
return Promise.all(uris.map(async (uri, index) => {
const params = {
windowId,
active: index == 0 && (active || (inBackground === false)),
};
if (uri && typeof uri == 'object') { // tabs.create() compatible
if ('active' in uri)
params.active = uri.active;
if ('cookieStoreId' in uri)
params.cookieStoreId = uri.cookieStoreId;
if ('discarded' in uri)
params.discarded = uri.discarded;
if ('index' in uri)
params.index = uri.index;
if ('openerTabId' in uri)
params.openerTabId = uri.openerTabId;
if ('openInReaderMode' in uri)
params.openInReaderMode = uri.openInReaderMode;
if ('pinned' in uri)
params.pinned = uri.pinned;
if ('selected' in uri)
params.active = uri.selected;
if ('title' in uri)
params.title = uri.title;
uri = uri.uri || uri.url;
}
let searchQuery = null;
if (uri) {
if (SEARCH_PREFIX_MATCHER.test(uri)) {
const query = uri.replace(SEARCH_PREFIX_MATCHER, '');
if (browser.search &&
typeof browser.search.search == 'function')
searchQuery = query;
else
params.url = configs.defaultSearchEngine.replace(/%s/gi, query);
}
else {
params.url = uri;
}
}
if (discarded &&
!params.active &&
!('discarded' in params))
params.discarded = true;
if (params.url == 'about:newtab')
delete params.url
if (params.url)
params.url = sanitizeURL(params.url);
if (!('url' in params /* about:newtab case */) ||
/^about:/.test(params.url))
params.discarded = false; // discarded tab cannot be opened with any about: URL
if (!params.discarded) // title cannot be set for non-discarded tabs
params.title = null;
if (opener && !params.openerTabId)
params.openerTabId = opener.id;
if (startIndex > -1 && !('index' in params))
params.index = startIndex + index;
if (cookieStoreId && !params.cookieStoreId)
params.cookieStoreId = cookieStoreId;
// Tabs opened with different container can take time to be tracked,
// then TabsStore.waitUntilTabsAreCreated() may be resolved before it is
// tracked like as "the tab is already closed". So we wait until the
// tab is correctly tracked.
const promisedNewTabTracked = new Promise((resolve, reject) => {
const listener = (tab) => {
Tab.onCreating.removeListener(listener);
browser.tabs.get(tab.id)
.then(resolve)
.catch(ApiTabs.createErrorSuppressor(reject));
};
Tab.onCreating.addListener(listener);
});
const createdTab = await browser.tabs.create(params).catch(ApiTabs.createErrorHandler());
await Promise.all([
promisedNewTabTracked, // TabsStore.waitUntilTabsAreCreated(createdTab.id),
searchQuery && browser.search.search({
query: searchQuery,
tabId: createdTab.id
}).catch(ApiTabs.createErrorHandler())
]);
const tab = Tab.get(createdTab.id);
log('created tab: ', tab);
if (!tab)
throw new Error('tab is already closed');
if (!opener &&
parent &&
!isOrphan)
await Tree.attachTabTo(tab, parent, {
insertBefore: insertBefore,
insertAfter: insertAfter,
forceExpand: params.active,
broadcast: true
});
else if (insertBefore)
await TabsMove.moveTabInternallyBefore(tab, insertBefore, {
broadcast: true
});
else if (insertAfter)
await TabsMove.moveTabInternallyAfter(tab, insertAfter, {
broadcast: true
});
log('tab is opened.');
await tab.$TST.opened;
tabs.push(tab);
tryRevokeObjectURL(tab.url);
return tab;
}));
}, windowId));
const openedTabs = tabs.filter(tab => actuallyOpenedTabIds.has(tab));
if (fixPositions &&
openedTabs.every((tab, index) => (index == 0) || (openedTabs[index-1].index - tab.index) == 1)) {
// tabs are opened with reversed order due to browser.tabs.insertAfterCurrent=true
let lastTab;
for (const tab of openedTabs.slice(0).reverse()) {
if (lastTab)
TabsMove.moveTabInternallyBefore(tab, lastTab);
lastTab = tab;
}
await TabsMove.waitUntilSynchronized(windowId);
}
return openedTabs;
}
function sanitizeURL(url) {
if (ALLOWED_URL_MATCHER.test(url))
return url;
// tabs.create() doesn't accept about:reader URLs so we fallback them to regular URLs.
if (/^about:reader\?/.test(url))
return (new URL(url)).searchParams.get('url') || 'about:blank';
if (FORBIDDEN_URL_MATCHER.test(url)) {
onForbiddenURLRequested.dispatch(url);
return `about:blank?forbidden-url=${url}`;
}
return url;
}
export function isOpenable(url) {
return !url || url == sanitizeURL(url);
}
function onMessage(message, openerTab) {
switch (message.type) {
case Constants.kCOMMAND_LOAD_URI:
loadURI(message.uri, {
tab: Tab.get(message.tabId)
});
break;
case Constants.kCOMMAND_OPEN_TAB:
if (!message.parentId && openerTab)
message.parentId = openerTab.id;
if (!message.windowId && openerTab)
message.windowId = openerTab.windowId;
Tab.waitUntilTracked([
message.parentId,
message.insertBeforeId,
message.insertAfterId
]).then(() => {
openURIsInTabs(message.uris || [message.uri], {
windowId: message.windowId,
parent: Tab.get(message.parentId),
insertBefore: Tab.get(message.insertBeforeId),
insertAfter: Tab.get(message.insertAfterId),
active: !!message.active,
discarded: message.discarded,
});
});
break;
case Constants.kCOMMAND_NEW_TABS:
Tab.waitUntilTracked([
message.openerId,
message.parentId,
message.insertBeforeId,
message.insertAfterId
]).then(() => {
log('new tabs requested: ', message);
openURIsInTabs(message.uris, {
windowId: message.windowId,
opener: Tab.get(message.openerId),
parent: Tab.get(message.parentId),
insertBefore: Tab.get(message.insertBeforeId),
insertAfter: Tab.get(message.insertAfterId),
active: message.active,
discarded: message.discarded,
});
});
break;
}
}
SidebarConnection.onMessage.addListener((windowId, message) => {
onMessage(message);
});
browser.runtime.onMessage.addListener((message, sender) => {
if (!message ||
typeof message.type != 'string' ||
message.type.indexOf('ws:') != 0)
return;
onMessage(message, sender.tab);
});