343 lines
12 KiB
JavaScript
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);
|
|
});
|