Files
tubestation/waterfox/browser/components/sidebar/common/tst-api.js
2025-11-06 14:13:52 +00:00

1503 lines
53 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,
wait,
configs,
isLinux,
} from './common.js';
import * as ApiTabs from '/common/api-tabs.js';
import * as Constants from './constants.js';
import * as SidebarConnection from './sidebar-connection.js';
import * as TabsStore from './tabs-store.js';
import {
Tab,
kPERMISSION_INCOGNITO,
kPERMISSIONS_ALL,
} from './TreeItem.js';
function log(...args) {
internalLogger('common/tst-api', ...args);
}
export const onInitialized = new EventListenerManager();
export const onRegistered = new EventListenerManager();
export const onUnregistered = new EventListenerManager();
export const onMessageExternal = {
$listeners: new Set(),
addListener(listener) {
this.$listeners.add(listener);
},
removeListener(listener) {
this.$listeners.delete(listener);
},
dispatch(...args) {
return Array.from(this.$listeners, listener => listener(...args));
}
};
export const kREGISTER_SELF = 'register-self';
export const kUNREGISTER_SELF = 'unregister-self';
export const kGET_VERSION = 'get-version';
export const kWAIT_FOR_SHUTDOWN = 'wait-for-shutdown';
export const kPING = 'ping';
export const kNOTIFY_READY = 'ready';
export const kNOTIFY_SHUTDOWN = 'shutdown'; // defined but not notified for now.
export const kNOTIFY_SIDEBAR_SHOW = 'sidebar-show';
export const kNOTIFY_SIDEBAR_HIDE = 'sidebar-hide';
export const kNOTIFY_TABS_RENDERED = 'tabs-rendered';
export const kNOTIFY_TABS_UNRENDERED = 'tabs-unrendered';
export const kNOTIFY_TAB_STICKY_STATE_CHANGED = 'tab-sticky-state-changed';
export const kNOTIFY_TAB_CLICKED = 'tab-clicked'; // for backward compatibility
export const kNOTIFY_TAB_DBLCLICKED = 'tab-dblclicked';
export const kNOTIFY_TAB_MOUSEDOWN = 'tab-mousedown';
export const kNOTIFY_TAB_MOUSEUP = 'tab-mouseup';
export const kNOTIFY_TABBAR_CLICKED = 'tabbar-clicked'; // for backward compatibility
export const kNOTIFY_TABBAR_MOUSEDOWN = 'tabbar-mousedown';
export const kNOTIFY_TABBAR_MOUSEUP = 'tabbar-mouseup';
export const kNOTIFY_EXTRA_CONTENTS_CLICKED = 'extra-contents-clicked';
export const kNOTIFY_EXTRA_CONTENTS_DBLCLICKED = 'extra-contents-dblclicked';
export const kNOTIFY_EXTRA_CONTENTS_MOUSEDOWN = 'extra-contents-mousedown';
export const kNOTIFY_EXTRA_CONTENTS_MOUSEUP = 'extra-contents-mouseup';
export const kNOTIFY_EXTRA_CONTENTS_KEYDOWN = 'extra-contents-keydown';
export const kNOTIFY_EXTRA_CONTENTS_KEYUP = 'extra-contents-keyup';
export const kNOTIFY_EXTRA_CONTENTS_INPUT = 'extra-contents-input';
export const kNOTIFY_EXTRA_CONTENTS_CHANGE = 'extra-contents-change';
export const kNOTIFY_EXTRA_CONTENTS_COMPOSITIONSTART = 'extra-contents-compositionstart';
export const kNOTIFY_EXTRA_CONTENTS_COMPOSITIONUPDATE = 'extra-contents-compositionupdate';
export const kNOTIFY_EXTRA_CONTENTS_COMPOSITIONEND = 'extra-contents-compositionend';
export const kNOTIFY_EXTRA_CONTENTS_FOCUS = 'extra-contents-focus';
export const kNOTIFY_EXTRA_CONTENTS_BLUR = 'extra-contents-blur';
export const kNOTIFY_TABBAR_OVERFLOW = 'tabbar-overflow';
export const kNOTIFY_TABBAR_UNDERFLOW = 'tabbar-underflow';
export const kNOTIFY_NEW_TAB_BUTTON_CLICKED = 'new-tab-button-clicked';
export const kNOTIFY_NEW_TAB_BUTTON_MOUSEDOWN = 'new-tab-button-mousedown';
export const kNOTIFY_NEW_TAB_BUTTON_MOUSEUP = 'new-tab-button-mouseup';
export const kNOTIFY_TAB_MOUSEMOVE = 'tab-mousemove';
export const kNOTIFY_TAB_MOUSEOVER = 'tab-mouseover';
export const kNOTIFY_TAB_MOUSEOUT = 'tab-mouseout';
export const kNOTIFY_TAB_DRAGREADY = 'tab-dragready';
export const kNOTIFY_TAB_DRAGCANCEL = 'tab-dragcancel';
export const kNOTIFY_TAB_DRAGSTART = 'tab-dragstart';
export const kNOTIFY_TAB_DRAGENTER = 'tab-dragenter';
export const kNOTIFY_TAB_DRAGEXIT = 'tab-dragexit';
export const kNOTIFY_TAB_DRAGEND = 'tab-dragend';
export const kNOTIFY_TREE_ATTACHED = 'tree-attached';
export const kNOTIFY_TREE_DETACHED = 'tree-detached';
export const kNOTIFY_TREE_COLLAPSED_STATE_CHANGED = 'tree-collapsed-state-changed';
export const kNOTIFY_NATIVE_TAB_DRAGSTART = 'native-tab-dragstart';
export const kNOTIFY_NATIVE_TAB_DRAGEND = 'native-tab-dragend';
export const kNOTIFY_PERMISSIONS_CHANGED = 'permissions-changed';
export const kNOTIFY_NEW_TAB_PROCESSED = 'new-tab-processed';
export const kSTART_CUSTOM_DRAG = 'start-custom-drag';
export const kNOTIFY_TRY_MOVE_FOCUS_FROM_COLLAPSING_TREE = 'try-move-focus-from-collapsing-tree';
export const kNOTIFY_TRY_REDIRECT_FOCUS_FROM_COLLAPSED_TAB = 'try-redirect-focus-from-collaped-tab';
export const kNOTIFY_TRY_EXPAND_TREE_FROM_FOCUSED_PARENT = 'try-expand-tree-from-focused-parent';
export const kNOTIFY_TRY_EXPAND_TREE_FROM_FOCUSED_BUNDLED_PARENT = 'try-expand-tree-from-focused-bundled-parent';
export const kNOTIFY_TRY_EXPAND_TREE_FROM_ATTACHED_CHILD = 'try-expand-tree-from-attached-child';
export const kNOTIFY_TRY_EXPAND_TREE_FROM_FOCUSED_COLLAPSED_TAB = 'try-expand-tree-from-focused-collapsed-tab';
export const kNOTIFY_TRY_EXPAND_TREE_FROM_LONG_PRESS_CTRL_KEY = 'try-expand-tree-from-long-press-ctrl-key';
export const kNOTIFY_TRY_EXPAND_TREE_FROM_END_TAB_SWITCH = 'try-expand-tree-from-end-tab-switch';
export const kNOTIFY_TRY_EXPAND_TREE_FROM_EXPAND_COMMAND = 'try-expand-tree-from-expand-command';
export const kNOTIFY_TRY_EXPAND_TREE_FROM_EXPAND_ALL_COMMAND = 'try-expand-tree-from-expand-all-command';
export const kNOTIFY_TRY_COLLAPSE_TREE_FROM_OTHER_EXPANSION = 'try-collapse-tree-from-other-expansion';
export const kNOTIFY_TRY_COLLAPSE_TREE_FROM_COLLAPSE_COMMAND = 'try-collapse-tree-from-collapse-command';
export const kNOTIFY_TRY_COLLAPSE_TREE_FROM_COLLAPSE_ALL_COMMAND = 'try-collapse-tree-from-collapse-all-command';
export const kNOTIFY_TRY_FIXUP_TREE_ON_TAB_MOVED = 'try-fixup-tree-on-tab-moved';
export const kNOTIFY_TRY_HANDLE_NEWTAB = 'try-handle-newtab';
export const kNOTIFY_TRY_SCROLL_TO_ACTIVATED_TAB = 'try-scroll-to-activated-tab';
export const kGET_TREE = 'get-tree';
export const kGET_LIGHT_TREE = 'get-light-tree';
export const kATTACH = 'attach';
export const kDETACH = 'detach';
export const kINDENT = 'indent';
export const kDEMOTE = 'demote';
export const kOUTDENT = 'outdent';
export const kPROMOTE = 'promote';
export const kMOVE_UP = 'move-up';
export const kMOVE_TO_START = 'move-to-start';
export const kMOVE_DOWN = 'move-down';
export const kMOVE_TO_END = 'move-to-end';
export const kMOVE_BEFORE = 'move-before';
export const kMOVE_AFTER = 'move-after';
export const kFOCUS = 'focus';
export const kCREATE = 'create';
export const kDUPLICATE = 'duplicate';
export const kGROUP_TABS = 'group-tabs';
export const kOPEN_IN_NEW_WINDOW = 'open-in-new-window';
export const kREOPEN_IN_CONTAINER = 'reopen-in-container';
export const kGET_TREE_STRUCTURE = 'get-tree-structure';
export const kSET_TREE_STRUCTURE = 'set-tree-structure';
export const kSTICK_TAB = 'stick-tab';
export const kUNSTICK_TAB = 'unstick-tab';
export const kTOGGLE_STICKY_STATE = 'toggle-sticky-state';
export const kREGISTER_AUTO_STICKY_STATES = 'register-auto-sticky-states';
export const kUNREGISTER_AUTO_STICKY_STATES = 'unregister-auto-sticky-states';
export const kCOLLAPSE_TREE = 'collapse-tree';
export const kEXPAND_TREE = 'expand-tree';
export const kTOGGLE_TREE_COLLAPSED = 'toggle-tree-collapsed';
export const kADD_TAB_STATE = 'add-tab-state';
export const kREMOVE_TAB_STATE = 'remove-tab-state';
export const kSCROLL = 'scroll';
export const kSTOP_SCROLL = 'stop-scroll';
export const kSCROLL_LOCK = 'scroll-lock';
export const kSCROLL_UNLOCK = 'scroll-unlock';
export const kNOTIFY_SCROLLED = 'scrolled';
export const kBLOCK_GROUPING = 'block-grouping';
export const kUNBLOCK_GROUPING = 'unblock-grouping';
export const kSET_TOOLTIP_TEXT = 'set-tooltip-text';
export const kCLEAR_TOOLTIP_TEXT = 'clear-tooltip-text';
export const kGRANT_TO_REMOVE_TABS = 'grant-to-remove-tabs';
export const kOPEN_ALL_BOOKMARKS_WITH_STRUCTURE = 'open-all-bookmarks-with-structure';
export const kSET_EXTRA_CONTENTS = 'set-extra-contents';
export const kCLEAR_EXTRA_CONTENTS = 'clear-extra-contents';
export const kCLEAR_ALL_EXTRA_CONTENTS = 'clear-all-extra-contents';
export const kSET_EXTRA_TAB_CONTENTS = 'set-extra-tab-contents'; // for backward compatibility
export const kCLEAR_EXTRA_TAB_CONTENTS = 'clear-extra-tab-contents'; // for backward compatibility
export const kCLEAR_ALL_EXTRA_TAB_CONTENTS = 'clear-all-extra-tab-contents'; // for backward compatibility
export const kSET_EXTRA_NEW_TAB_BUTTON_CONTENTS = 'set-extra-new-tab-button-contents'; // for backward compatibility
export const kCLEAR_EXTRA_NEW_TAB_BUTTON_CONTENTS = 'clear-extra-new-tab-button-contents'; // for backward compatibility
export const kSET_EXTRA_CONTENTS_PROPERTIES = 'set-extra-contents-properties';
export const kFOCUS_TO_EXTRA_CONTENTS = 'focus-to-extra-contents';
export const kGET_DRAG_DATA = 'get-drag-data';
const BULK_MESSAGING_TYPES = new Set([
kNOTIFY_SIDEBAR_SHOW,
kNOTIFY_SIDEBAR_HIDE,
kNOTIFY_TABS_RENDERED,
kNOTIFY_TABS_UNRENDERED,
kNOTIFY_TAB_STICKY_STATE_CHANGED,
kNOTIFY_EXTRA_CONTENTS_FOCUS,
kNOTIFY_EXTRA_CONTENTS_BLUR,
kNOTIFY_TABBAR_OVERFLOW,
kNOTIFY_TABBAR_UNDERFLOW,
kNOTIFY_TAB_MOUSEMOVE,
kNOTIFY_TAB_MOUSEOVER,
kNOTIFY_TAB_MOUSEOUT,
kNOTIFY_TAB_DRAGREADY,
kNOTIFY_TAB_DRAGCANCEL,
kNOTIFY_TAB_DRAGSTART,
kNOTIFY_TAB_DRAGENTER,
kNOTIFY_TAB_DRAGEXIT,
kNOTIFY_TAB_DRAGEND,
kNOTIFY_TREE_ATTACHED,
kNOTIFY_TREE_DETACHED,
kNOTIFY_TREE_COLLAPSED_STATE_CHANGED,
kNOTIFY_NATIVE_TAB_DRAGSTART,
kNOTIFY_NATIVE_TAB_DRAGEND,
kNOTIFY_PERMISSIONS_CHANGED,
]);
export const kCONTEXT_MENU_OPEN = 'contextMenu-open';
export const kCONTEXT_MENU_CREATE = 'contextMenu-create';
export const kCONTEXT_MENU_UPDATE = 'contextMenu-update';
export const kCONTEXT_MENU_REMOVE = 'contextMenu-remove';
export const kCONTEXT_MENU_REMOVE_ALL = 'contextMenu-remove-all';
export const kCONTEXT_MENU_CLICK = 'contextMenu-click';
export const kCONTEXT_MENU_SHOWN = 'contextMenu-shown';
export const kCONTEXT_MENU_HIDDEN = 'contextMenu-hidden';
export const kFAKE_CONTEXT_MENU_OPEN = 'fake-contextMenu-open';
export const kFAKE_CONTEXT_MENU_CREATE = 'fake-contextMenu-create';
export const kFAKE_CONTEXT_MENU_UPDATE = 'fake-contextMenu-update';
export const kFAKE_CONTEXT_MENU_REMOVE = 'fake-contextMenu-remove';
export const kFAKE_CONTEXT_MENU_REMOVE_ALL = 'fake-contextMenu-remove-all';
export const kFAKE_CONTEXT_MENU_CLICK = 'fake-contextMenu-click';
export const kFAKE_CONTEXT_MENU_SHOWN = 'fake-contextMenu-shown';
export const kFAKE_CONTEXT_MENU_HIDDEN = 'fake-contextMenu-hidden';
export const kOVERRIDE_CONTEXT = 'override-context';
export const kCOMMAND_BROADCAST_API_REGISTERED = 'ws:broadcast-registered';
export const kCOMMAND_BROADCAST_API_UNREGISTERED = 'ws:broadcast-unregistered';
export const kCOMMAND_BROADCAST_API_PERMISSION_CHANGED = 'ws:permission-changed';
export const kCOMMAND_REQUEST_INITIALIZE = 'ws:request-initialize';
export const kCOMMAND_REQUEST_CONTROL_STATE = 'ws:request-control-state';
export const kCOMMAND_GET_ADDONS = 'ws:get-addons';
export const kCOMMAND_SET_API_PERMISSION = 'ws:set-api-permisssion';
export const kCOMMAND_NOTIFY_PERMISSION_CHANGED = 'ws:notify-api-permisssion-changed';
export const kCOMMAND_UNREGISTER_ADDON = 'ws:unregister-addon';
export const INTERNAL_CALL_PREFIX = 'ws:api:';
export const INTERNAL_CALL_PREFIX_MATCHER = new RegExp(`^${INTERNAL_CALL_PREFIX}`);
export const kNEWTAB_CONTEXT_NEWTAB_COMMAND = 'newtab-command';
export const kNEWTAB_CONTEXT_WITH_OPENER = 'with-opener';
export const kNEWTAB_CONTEXT_DUPLICATED = 'duplicated';
export const kNEWTAB_CONTEXT_FROM_PINNED = 'from-pinned';
export const kNEWTAB_CONTEXT_FROM_EXTERNAL = 'from-external';
export const kNEWTAB_CONTEXT_WEBSITE_SAME_TO_ACTIVE_TAB = 'website-same-to-active-tab';
export const kNEWTAB_CONTEXT_FROM_ABOUT_ADDONS = 'from-about-addons';
export const kNEWTAB_CONTEXT_UNKNOWN = 'unknown';
const mAddons = new Map();
let mScrollLockedBy = {};
let mGroupingBlockedBy = {};
const mPendingMessagesFor = new Map();
const mMessagesPendedAt = new Map();
// you should use this to reduce memory usage around effective favicons
export function clearCache(cache) {
cache.effectiveFavIconUrls = {};
}
// This function is complex a little, but we should not make a custom class for this purpose,
// bacause instances of the class will be very short-life and increases RAM usage on
// massive tabs case.
export async function exportTab(sourceTab, { addonId, light, isContextTab, interval, permissions, cache, cacheKey } = {}) {
const normalizedSourceTab = Tab.get(sourceTab);
if (!normalizedSourceTab)
throw new Error(`Fatal error: tried to export not a tab. ${sourceTab}`);
sourceTab = normalizedSourceTab;
if (!interval)
interval = 0;
if (!cache)
cache = {};
if (!cache.tabs)
cache.tabs = {};
if (!cache.effectiveFavIconUrls)
cache.effectiveFavIconUrls = {};
if (!permissions) {
permissions = (!addonId || addonId == browser.runtime.id) ?
kPERMISSIONS_ALL :
new Set(getGrantedPermissionsForAddon(addonId));
if (addonId &&
configs.incognitoAllowedExternalAddons.includes(addonId))
permissions.add(kPERMISSION_INCOGNITO);
}
if (!cacheKey)
cacheKey = `${sourceTab.id}:${Array.from(permissions).sort().join(',')}`;
if (!sourceTab ||
(sourceTab.incognito &&
!permissions.has(kPERMISSION_INCOGNITO)))
return null;
// The promise is cached here instead of the result,
// to avoid cache miss caused by concurrent call.
if (!(cacheKey in cache.tabs)) {
cache.tabs[cacheKey] = sourceTab.$TST.exportForAPI({ addonId, light, isContextTab, interval, permissions, cache, cacheKey });
}
return cache.tabs[cacheKey];
}
export function getAddon(id) {
return mAddons.get(id);
}
export function getGrantedPermissionsForAddon(id) {
const addon = getAddon(id);
return addon?.grantedPermissions || new Set();
}
function registerAddon(id, addon) {
log('addon is registered: ', id, addon);
// inherit properties from last effective value
const oldAddon = getAddon(id);
if (oldAddon) {
for (const param of [
'name',
'icons',
'listeningTypes',
'allowBulkMessaging',
'lightTree',
'style',
'permissions',
]) {
if (!(param in addon) && param in oldAddon) {
addon[param] = oldAddon[param];
}
}
}
if (!addon.listeningTypes) {
// for backward compatibility, send all message types available on TST 2.4.16 by default.
addon.listeningTypes = [
kNOTIFY_READY,
kNOTIFY_SHUTDOWN,
kNOTIFY_TAB_CLICKED,
kNOTIFY_TAB_MOUSEDOWN,
kNOTIFY_TAB_MOUSEUP,
kNOTIFY_NEW_TAB_BUTTON_CLICKED,
kNOTIFY_NEW_TAB_BUTTON_MOUSEDOWN,
kNOTIFY_NEW_TAB_BUTTON_MOUSEUP,
kNOTIFY_TABBAR_CLICKED,
kNOTIFY_TABBAR_MOUSEDOWN,
kNOTIFY_TABBAR_MOUSEUP
];
}
let requestedPermissions = addon.permissions || [];
if (!Array.isArray(requestedPermissions))
requestedPermissions = [requestedPermissions];
addon.requestedPermissions = new Set(requestedPermissions);
const grantedPermissions = configs.grantedExternalAddonPermissions[id] || [];
addon.grantedPermissions = new Set(grantedPermissions);
if (Constants.IS_BACKGROUND &&
!addon.bypassPermissionCheck &&
addon.requestedPermissions.size > 0 &&
addon.grantedPermissions.size != addon.requestedPermissions.size)
notifyPermissionRequest(addon, addon.requestedPermissions);
addon.id = id;
addon.lastRegistered = Date.now();
mAddons.set(id, addon);
onRegistered.dispatch(addon);
}
const mPermissionNotificationForAddon = new Map();
async function notifyPermissionRequest(addon, requestedPermissions) {
log('notifyPermissionRequest ', addon, requestedPermissions);
if (mPermissionNotificationForAddon.has(addon.id))
return;
mPermissionNotificationForAddon.set(addon.id, -1);
const id = await browser.notifications.create({
type: 'basic',
iconUrl: Constants.kNOTIFICATION_DEFAULT_ICON,
title: browser.i18n.getMessage('api_requestedPermissions_title'),
message: browser.i18n.getMessage(`api_requestedPermissions_message${isLinux() ? '_linux' : ''}`, [
addon.name || addon.title || addon.id,
Array.from(requestedPermissions, permission => {
if (permission == kPERMISSION_INCOGNITO)
return null;
try {
return browser.i18n.getMessage(`api_requestedPermissions_type_${permission}`) || permission;
}
catch(_error) {
return permission;
}
}).filter(permission => !!permission).join('\n')
])
});
mPermissionNotificationForAddon.set(addon.id, id);
}
function setPermissions(addon, permisssions) {
addon.grantedPermissions = permisssions;
const cachedPermissions = JSON.parse(JSON.stringify(configs.grantedExternalAddonPermissions));
cachedPermissions[addon.id] = Array.from(addon.grantedPermissions);
configs.grantedExternalAddonPermissions = cachedPermissions;
notifyPermissionChanged(addon);
}
function notifyPermissionChanged(addon) {
const permissions = Array.from(addon.grantedPermissions);
browser.runtime.sendMessage({
type: kCOMMAND_BROADCAST_API_PERMISSION_CHANGED,
id: addon.id,
permissions
});
if (addon.id == browser.runtime.id)
return;
browser.runtime.sendMessage(addon.id, {
type: kNOTIFY_PERMISSIONS_CHANGED,
grantedPermissions: permissions.filter(permission => permission.startsWith('!')),
privateWindowAllowed: configs.incognitoAllowedExternalAddons.includes(addon.id)
}).catch(ApiTabs.createErrorHandler());
}
function unregisterAddon(id) {
const addon = getAddon(id);
log('addon is unregistered: ', id, addon);
onUnregistered.dispatch(addon);
mAddons.delete(id);
mPendingMessagesFor.delete(id);
mMessagesPendedAt.delete(id);
delete mScrollLockedBy[id];
delete mGroupingBlockedBy[id];
}
export function getAddons() {
return mAddons.entries();
}
const mConnections = new Map();
function onCommonCommand(message, sender) {
if (!message ||
typeof message.type != 'string')
return;
const addon = getAddon(sender.id);
switch (message.type) {
case kSCROLL_LOCK:
mScrollLockedBy[sender.id] = true;
if (!addon)
registerAddon(sender.id, sender);
return Promise.resolve(true);
case kSCROLL_UNLOCK:
delete mScrollLockedBy[sender.id];
return Promise.resolve(true);
case kBLOCK_GROUPING:
mGroupingBlockedBy[sender.id] = true;
if (!addon)
registerAddon(sender.id, sender);
return Promise.resolve(true);
case kUNBLOCK_GROUPING:
delete mGroupingBlockedBy[sender.id];
return Promise.resolve(true);
case kSET_EXTRA_TAB_CONTENTS:
if (!addon)
registerAddon(sender.id, sender);
break;
case kSET_EXTRA_NEW_TAB_BUTTON_CONTENTS:
if (!addon)
registerAddon(sender.id, sender);
break;
}
}
// =======================================================================
// for backend
// =======================================================================
let mInitialized = false;
let mPromisedInitialized = null;
if (Constants.IS_BACKGROUND) {
browser.runtime.onMessageExternal.addListener(onBackendCommand);
browser.runtime.onConnectExternal.addListener(port => {
if (!mInitialized ||
!configs.APIEnabled)
return;
const sender = port.sender;
mConnections.set(sender.id, port);
port.onMessage.addListener(message => {
const messages = message.messages || [message];
for (const oneMessage of messages) {
onMessageExternal.dispatch(oneMessage, sender);
SidebarConnection.sendMessage({
type: 'external',
oneMessage,
sender
});
}
});
port.onDisconnect.addListener(_message => {
mConnections.delete(sender.id);
onBackendCommand({
type: kUNREGISTER_SELF,
sender
}).catch(ApiTabs.createErrorSuppressor());
});
});
}
export async function initAsBackend() {
// We must listen API messages from other addons here beacause:
// * Before notification messages are sent to other addons.
// * After configs are loaded and TST's background page is almost completely initialized.
// (to prevent troubles like breakage of `configs.cachedExternalAddons`, see also:
// https://github.com/piroor/treestyletab/issues/2300#issuecomment-498947370 )
mInitialized = true;
let resolver;
mPromisedInitialized = new Promise((resolve, _reject) => {
resolver = resolve;
});
const manifest = browser.runtime.getManifest();
registerAddon(browser.runtime.id, {
id: browser.runtime.id,
internalId: browser.runtime.getURL('').replace(/^moz-extension:\/\/([^\/]+)\/.*$/, '$1'),
icons: manifest.icons,
listeningTypes: [
kNOTIFY_EXTRA_CONTENTS_CLICKED,
kNOTIFY_EXTRA_CONTENTS_DBLCLICKED,
kNOTIFY_EXTRA_CONTENTS_MOUSEDOWN,
kNOTIFY_EXTRA_CONTENTS_MOUSEUP,
kNOTIFY_EXTRA_CONTENTS_KEYDOWN,
kNOTIFY_EXTRA_CONTENTS_KEYUP,
kNOTIFY_EXTRA_CONTENTS_INPUT,
kNOTIFY_EXTRA_CONTENTS_CHANGE,
kNOTIFY_EXTRA_CONTENTS_COMPOSITIONSTART,
kNOTIFY_EXTRA_CONTENTS_COMPOSITIONUPDATE,
kNOTIFY_EXTRA_CONTENTS_COMPOSITIONEND,
kNOTIFY_EXTRA_CONTENTS_FOCUS,
kNOTIFY_EXTRA_CONTENTS_BLUR,
],
bypassPermissionCheck: true,
allowBulkMessaging: true,
lightTree: true,
});
const respondedAddons = [];
const notifiedAddons = {};
const notifyAddons = configs.knownExternalAddons.concat(configs.cachedExternalAddons);
log('initAsBackend: notifyAddons = ', notifyAddons);
await Promise.all(notifyAddons.map(async id => {
if (id in notifiedAddons)
return;
notifiedAddons[id] = true;
try {
id = await new Promise((resolve, reject) => {
let responded = false;
browser.runtime.sendMessage(id, {
type: kNOTIFY_READY
}).then(() => {
responded = true;
resolve(id);
}).catch(ApiTabs.createErrorHandler(reject));
setTimeout(() => {
if (!responded)
reject(new Error(`TSTAPI.initAsBackend: addon ${id} does not respond.`));
}, 3000);
});
if (id)
respondedAddons.push(id);
}
catch(e) {
console.log(`TSTAPI.initAsBackend: failed to send "ready" message to "${id}":`, e);
}
}));
log('initAsBackend: respondedAddons = ', respondedAddons);
configs.cachedExternalAddons = respondedAddons;
onInitialized.dispatch();
resolver();
}
if (Constants.IS_BACKGROUND) {
browser.notifications.onClicked.addListener(notificationId => {
if (!mInitialized)
return;
for (const [addonId, id] of mPermissionNotificationForAddon.entries()) {
if (id != notificationId)
continue;
mPermissionNotificationForAddon.delete(addonId);
browser.tabs.create({
url: `moz-extension://${location.host}/options/options.html#externalAddonPermissionsGroup`
});
break;
}
});
browser.notifications.onClosed.addListener((notificationId, _byUser) => {
if (!mInitialized)
return;
for (const [addonId, id] of mPermissionNotificationForAddon.entries()) {
if (id != notificationId)
continue;
mPermissionNotificationForAddon.delete(addonId);
break;
}
});
SidebarConnection.onConnected.addListener((windowId, openCount) => {
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_NOTIFY_CONNECTION_READY,
windowId,
openCount,
});
});
SidebarConnection.onDisconnected.addListener((windowId, openCount) => {
broadcastMessage({
type: kNOTIFY_SIDEBAR_HIDE,
window: windowId,
windowId,
openCount
});
});
/*
// This mechanism doesn't work actually.
// See also: https://github.com/piroor/treestyletab/issues/2128#issuecomment-454650407
const mConnectionsForAddons = new Map();
browser.runtime.onConnectExternal.addListener(port => {
if (!mInitialized)
return;
const sender = port.sender;
log('Connected: ', sender.id);
const connections = mConnectionsForAddons.get(sender.id) || new Set();
connections.add(port);
const addon = getAddon(sender.id);
if (!addon) { // treat as register-self
const message = {
id: sender.id,
internalId: sender.url.replace(/^moz-extension:\/\/([^\/]+)\/.*$/, '$1'),
newlyInstalled: !configs.cachedExternalAddons.includes(sender.id)
};
registerAddon(sender.id, message);
browser.runtime.sendMessage({
type: kCOMMAND_BROADCAST_API_REGISTERED,
sender,
message
}).catch(ApiTabs.createErrorSuppressor());
if (message.newlyInstalled)
configs.cachedExternalAddons = configs.cachedExternalAddons.concat([sender.id]);
}
const onMessage = message => {
onBackendCommand(message, sender);
};
port.onMessage.addListener(onMessage);
const onDisconnected = _message => {
log('Disconnected: ', sender.id);
port.onMessage.removeListener(onMessage);
port.onDisconnect.removeListener(onDisconnected);
connections.delete(port);
if (connections.size > 0)
return;
setTimeout(() => {
// if it is not re-registered while 10sec, it may be uninstalled.
if (getAddon(sender.id))
return;
configs.cachedExternalAddons = configs.cachedExternalAddons.filter(id => id != sender.id);
}, 10 * 1000);
browser.runtime.sendMessage({
type: kCOMMAND_BROADCAST_API_UNREGISTERED,
sender
}).catch(ApiTabs.createErrorSuppressor());
unregisterAddon(sender.id);
mConnectionsForAddons.delete(sender.id);
}
port.onDisconnect.addListener(onDisconnected);
});
*/
browser.runtime.onMessage.addListener((message, _sender) => {
if (!mInitialized ||
!message ||
typeof message.type != 'string')
return;
switch (message.type) {
case kCOMMAND_REQUEST_INITIALIZE:
return Promise.resolve({
addons: exportAddons(),
scrollLocked: mScrollLockedBy,
groupingLocked: mGroupingBlockedBy
});
case kCOMMAND_REQUEST_CONTROL_STATE:
return Promise.resolve({
scrollLocked: mScrollLockedBy,
groupingLocked: mGroupingBlockedBy
});
case kCOMMAND_GET_ADDONS:
return mPromisedInitialized.then(() => {
const addons = [];
for (const [id, addon] of mAddons.entries()) {
addons.push({
id,
label: addon.name || addon.title || addon.id,
permissions: Array.from(addon.requestedPermissions),
permissionsGranted: Array.from(addon.requestedPermissions).join(',') == Array.from(addon.grantedPermissions).join(',')
});
}
return addons;
});
case kCOMMAND_SET_API_PERMISSION:
setPermissions(getAddon(message.id), new Set(message.permissions));
break;
case kCOMMAND_NOTIFY_PERMISSION_CHANGED:
notifyPermissionChanged(getAddon(message.id));
break;
case kCOMMAND_UNREGISTER_ADDON:
unregisterAddon(message.id);
break;
}
});
}
const mPromisedOnBeforeUnload = new Promise((resolve, _reject) => {
// If this promise doesn't do anything then there seems to be a timeout so it only works if TST is disabled within about 10 seconds after this promise is used as a response to a message. After that it will not throw an error for the waiting extension.
// If we use the following then the returned promise will be rejected when TST is disabled even for longer times:
window.addEventListener('beforeunload', () => resolve());
});
const mWaitingShutdownMessages = new Map();
function onBackendCommand(message, sender) {
if (message?.messages)
return Promise.all(
message.messages.map(oneMessage => onBackendCommand(oneMessage, sender))
);
if (!mInitialized ||
!message ||
typeof message != 'object' ||
typeof message.type != 'string')
return;
const results = onMessageExternal.dispatch(message, sender);
const firstPromise = results.find(result => result instanceof Promise);
if (firstPromise)
return firstPromise;
switch (message.type) {
case kPING:
return Promise.resolve(true);
case kREGISTER_SELF:
return (async () => {
message.internalId = sender.url.replace(/^moz-extension:\/\/([^\/]+)\/.*$/, '$1');
message.id = sender.id;
message.subPanel = message.subPanel || message.subpanel || null;
if (message.subPanel) {
const url = typeof message.subPanel.url === 'string' && new URL(message.subPanel.url, new URL('/', sender.url));
if (!url || url.hostname !== message.internalId) {
console.error(`"subPanel.url" must refer to a page packed in the registering extension.`);
message.subPanel.url = 'about:blank?error=invalid-origin'
} else
message.subPanel.url = url.href;
}
message.newlyInstalled = !configs.cachedExternalAddons.includes(sender.id);
registerAddon(sender.id, message);
browser.runtime.sendMessage({
type: kCOMMAND_BROADCAST_API_REGISTERED,
sender: sender,
message: message
}).catch(ApiTabs.createErrorSuppressor());
if (message.newlyInstalled)
configs.cachedExternalAddons = configs.cachedExternalAddons.concat([sender.id]);
if (message.listeningTypes &&
message.listeningTypes.includes(kWAIT_FOR_SHUTDOWN) &&
!mWaitingShutdownMessages.has(sender.id)) {
const onShutdown = () => {
const storedShutdown = mWaitingShutdownMessages.get(sender.id);
// eslint-disable-next-line no-use-before-define
if (storedShutdown && storedShutdown !== promisedShutdown)
return; // it is obsolete
const addon = getAddon(sender.id);
const lastRegistered = addon?.lastRegistered;
setTimeout(() => {
// if it is re-registered immediately, it was updated or reloaded.
const addon = getAddon(sender.id);
if (addon &&
addon.lastRegistered != lastRegistered)
return;
// otherwise it is uninstalled.
browser.runtime.sendMessage({
type: kCOMMAND_BROADCAST_API_UNREGISTERED,
sender
}).catch(ApiTabs.createErrorSuppressor());
unregisterAddon(sender.id);
configs.cachedExternalAddons = configs.cachedExternalAddons.filter(id => id != sender.id);
}, 350);
};
const promisedShutdown = (async () => {
try {
const shouldUninit = await browser.runtime.sendMessage(sender.id, {
type: kWAIT_FOR_SHUTDOWN
});
if (!shouldUninit)
return;
}
catch (_error) {
// Extension was disabled.
}
finally {
mWaitingShutdownMessages.delete(sender.id);
}
onShutdown();
})();
mWaitingShutdownMessages.set(sender.id, promisedShutdown);
promisedShutdown.catch(onShutdown);
}
return {
grantedPermissions: Array.from(getGrantedPermissionsForAddon(sender.id)).filter(permission => permission.startsWith('!')),
privateWindowAllowed: configs.incognitoAllowedExternalAddons.includes(sender.id)
};
})();
case kUNREGISTER_SELF:
return (async () => {
browser.runtime.sendMessage({
type: kCOMMAND_BROADCAST_API_UNREGISTERED,
sender
}).catch(ApiTabs.createErrorSuppressor());
unregisterAddon(sender.id);
configs.cachedExternalAddons = configs.cachedExternalAddons.filter(id => id != sender.id);
return true;
})();
case kWAIT_FOR_SHUTDOWN:
return mPromisedOnBeforeUnload;
default:
return onCommonCommand(message, sender);
}
}
function exportAddons() {
const exported = {};
for (const [id, addon] of getAddons()) {
exported[id] = addon;
}
return exported;
}
export function isGroupingBlocked() {
return Object.keys(mGroupingBlockedBy).length > 0;
}
// =======================================================================
// for frontend
// =======================================================================
export async function initAsFrontend() {
let resolver;
mPromisedInitialized = new Promise((resolve, _reject) => {
resolver = resolve;
});
log('initAsFrontend: start');
let response;
while (true) {
response = await browser.runtime.sendMessage({ type: kCOMMAND_REQUEST_INITIALIZE });
if (response)
break;
await wait(10);
}
browser.runtime.onMessageExternal.addListener(onFrontendCommand);
log('initAsFrontend: response = ', response);
importAddons(response.addons);
for (const [, addon] of getAddons()) {
onRegistered.dispatch(addon);
}
mScrollLockedBy = response.scrollLocked;
mGroupingBlockedBy = response.groupingLocked;
onInitialized.dispatch();
log('initAsFrontend: finish');
resolver();
mPromisedInitialized = null;
}
function onFrontendCommand(message, sender) {
//console.log('onFrontendCommand ', message, sender);
if (!configs.APIEnabled)
return;
if (message?.messages)
return Promise.all(
message.messages.map(oneMessage => onFrontendCommand(oneMessage, sender))
);
if (message &&
typeof message == 'object' &&
typeof message.type == 'string') {
const results = onMessageExternal.dispatch(message, sender);
log('onMessageExternal: ', message, ' => ', results, 'sender: ', sender);
const firstPromise = results.find(result => result instanceof Promise);
if (firstPromise)
return firstPromise;
}
if (configs.incognitoAllowedExternalAddons.includes(sender.id) ||
!document.documentElement.classList.contains('incognito'))
return onCommonCommand(message, sender);
}
if (Constants.IS_SIDEBAR) {
browser.runtime.onMessage.addListener((message, _sender) => {
if (!message ||
typeof message != 'object' ||
typeof message.type != 'string')
return;
switch (message.type) {
case kCOMMAND_BROADCAST_API_REGISTERED:
registerAddon(message.sender.id, message.message);
break;
case kCOMMAND_BROADCAST_API_UNREGISTERED:
unregisterAddon(message.sender.id);
break;
case kCOMMAND_BROADCAST_API_PERMISSION_CHANGED: {
const addon = getAddon(message.id);
addon.grantedPermissions = new Set(message.permissions);
}; break;
}
});
}
function importAddons(addons) {
if (!addons)
console.log(new Error('null import'));
for (const id of Object.keys(mAddons)) {
unregisterAddon(id);
}
for (const [id, addon] of Object.entries(addons)) {
registerAddon(id, addon);
}
}
export function isScrollLocked() {
return Object.keys(mScrollLockedBy).length > 0;
}
export async function notifyScrolled(params = {}) {
const lockers = Object.keys(mScrollLockedBy);
const tab = params.tab;
const windowId = TabsStore.getCurrentWindowId();
const tabs = Tab.getTabs(windowId);
const cache = {};
const results = await broadcastMessage({
type: kNOTIFY_SCROLLED,
tab: tab && tabs.find(another => another.id == tab.id),
tabs,
overflow: params.overflow,
window: windowId,
windowId,
deltaX: params.event.deltaX,
deltaY: params.event.deltaY,
deltaZ: params.event.deltaZ,
deltaMode: params.event.deltaMode,
scrollTop: params.scrollContainer.scrollTop,
scrollTopMax: params.scrollContainer.scrollTopMax,
altKey: params.event.altKey,
ctrlKey: params.event.ctrlKey,
metaKey: params.event.metaKey,
shiftKey: params.event.shiftKey,
clientX: params.event.clientX,
clientY: params.event.clientY,
}, {
targets: lockers,
tabProperties: ['tab', 'tabs'],
cache,
});
for (const result of results) {
if (!result || result.error || result.result === undefined)
delete mScrollLockedBy[result.id];
}
clearCache(cache);
}
// =======================================================================
// Common utilities to send notification messages to other addons
// =======================================================================
export async function tryOperationAllowed(type, message = {}, { targets, except, tabProperties, cache } = {}) {
if (mPromisedInitialized)
await mPromisedInitialized;
if (!hasListenerForMessageType(type, { targets, except })) {
//log(`=> ${type}: no listener, always allowed`);
return true;
}
cache = cache || {};
const results = await broadcastMessage({ ...message, type }, {
targets,
except,
tabProperties,
cache,
}).catch(error => {
if (configs.debug)
console.error(error);
return [];
});
if (!results) {
log(`=> ${type}: allowed because no one responded`);
return true;
}
if (results.flat().some(result => result?.result)) {
log(`=> ${type}: canceled by some helper addon`);
return false;
}
log(`=> ${type}: allowed by all helper addons`);
return true;
}
export function hasListenerForMessageType(type, { targets, except } = {}) {
return getListenersForMessageType(type, { targets, except }).length > 0;
}
export function getListenersForMessageType(type, { targets, except } = {}) {
targets = targets instanceof Set ? targets : new Set(Array.isArray(targets) ? targets : targets ? [targets] : []);
except = except instanceof Set ? except : new Set(Array.isArray(except) ? except : except ? [except] : []);
const finalTargets = new Set();
for (const [id, addon] of getAddons()) {
if (addon.listeningTypes.includes(type) &&
(targets.size == 0 || targets.has(id)) &&
!except.has(id))
finalTargets.add(id);
}
//log('getListenersForMessageType ', { type, targets, except, finalTargets, all: mAddons });
return Array.from(finalTargets, getAddon);
}
export async function sendMessage(addonId, message, { tabProperties, cache, isContextTab } = {}) {
if (mPromisedInitialized)
await mPromisedInitialized;
cache = cache || {};
const incognitoParams = { windowId: message.windowId || message.window };
for (const key of tabProperties) {
if (!message[key])
continue;
if (Array.isArray(message[key]))
incognitoParams.tab = message[key][0].tab;
else
incognitoParams.tab = message[key].tab;
break;
}
if (!isSafeAtIncognito(addonId, incognitoParams))
throw new Error(`Message from incognito source is not allowed for ${addonId}`);
const safeMessage = await sanitizeMessage(message, { addonId, tabProperties, cache, isContextTab });
const result = directSendMessage(addonId, safeMessage);
if (result.error)
throw result.error;
return result.result;
}
export async function broadcastMessage(message, { targets, except, tabProperties, cache, isContextTab } = {}) {
if (!configs.APIEnabled)
return [];
if (mPromisedInitialized)
await mPromisedInitialized;
const listenerAddons = getListenersForMessageType(message.type, { targets, except });
tabProperties = tabProperties || [];
cache = cache || {};
log(`broadcastMessage: sending message for ${message.type}: `, {
message,
listenerAddons,
tabProperties
});
const promisedResults = spawnMessages(new Set(listenerAddons.map(addon => addon.id)), {
message,
tabProperties,
cache,
isContextTab,
});
return Promise.all(promisedResults).then(results => {
log(`broadcastMessage: got responses for ${message.type}: `, results);
return results;
}).catch(ApiTabs.createErrorHandler());
}
function* spawnMessages(targets, { message, tabProperties, cache, isContextTab }) {
tabProperties = tabProperties || [];
cache = cache || {};
const incognitoParams = { windowId: message.windowId || message.window };
for (const key of tabProperties) {
if (!message[key])
continue;
if (Array.isArray(message[key]))
incognitoParams.tab = message[key][0].tab;
else
incognitoParams.tab = message[key].tab;
break;
}
const send = async (id) => {
if (!isSafeAtIncognito(id, incognitoParams))
return {
id,
result: undefined
};
const allowedMessage = await sanitizeMessage(message, { addonId: id, tabProperties, cache, isContextTab });
const addon = getAddon(id) || {};
if (BULK_MESSAGING_TYPES.has(message.type) &&
addon.allowBulkMessaging) {
const startAt = `${Date.now()}-${parseInt(Math.random() * 65000)}`;
mMessagesPendedAt.set(id, startAt);
const messages = mPendingMessagesFor.get(id) || [];
messages.push(allowedMessage);
mPendingMessagesFor.set(id, messages);
(Constants.IS_BACKGROUND ?
setTimeout : // because window.requestAnimationFrame is decelerate for an invisible document.
window.requestAnimationFrame)(() => {
if (mMessagesPendedAt.get(id) != startAt)
return;
const messages = mPendingMessagesFor.get(id);
mPendingMessagesFor.delete(id);
if (!messages || messages.length == 0)
return;
directSendMessage(id, messages.length == 1 ? messages[0] : { messages });
}, 0);
return {
id,
result: null,
};
}
return directSendMessage(id, allowedMessage);
};
for (const id of targets) {
yield send(id);
}
}
async function directSendMessage(id, message) {
try {
const result = await (id == browser.runtime.id ?
browser.runtime.sendMessage(
Array.isArray(message) ?
message.map(message => ({ ...message, type: `${INTERNAL_CALL_PREFIX}${message.type}` })) :
({ ...message, type: `${INTERNAL_CALL_PREFIX}${message.type}` })
) :
browser.runtime.sendMessage(id, message));
return {
id,
result,
};
}
catch(error) {
console.log(`Error on sending message to ${id}`, message, error);
if (error &&
error.message == 'Could not establish connection. Receiving end does not exist.') {
browser.runtime.sendMessage(id, { type: kNOTIFY_READY }).catch(_error => {
console.log(`Unregistering missing helper addon ${id}...`);
unregisterAddon(id);
if (Constants.IS_SIDEBAR)
browser.runtime.sendMessage({ type: kCOMMAND_UNREGISTER_ADDON, id });
});
}
return {
id,
error,
};
}
}
export function isSafeAtIncognito(addonId, { tab, windowId }) {
if (addonId == browser.runtime.id)
return true;
const win = windowId && TabsStore.windows.get(windowId);
const hasIncognitoInfo = win?.incognito || tab?.incognito;
return !hasIncognitoInfo || configs.incognitoAllowedExternalAddons.includes(addonId);
}
async function sanitizeMessage(message, { addonId, tabProperties, cache, isContextTab }) {
const addon = getAddon(addonId);
if (!message ||
!tabProperties ||
tabProperties.length == 0 ||
addon.bypassPermissionCheck)
return message;
cache = cache || {};
const sanitizedProperties = {};
const tasks = [];
if (tabProperties) {
for (const name of tabProperties) {
const treeItem = message[name];
if (!treeItem)
continue;
if (Array.isArray(treeItem))
tasks.push((async treeItems => {
const tabs = await Promise.all(treeItems.map(treeItem => exportTab(treeItem, {
addonId: addon.id,
light: !!addon.lightTree,
cache,
isContextTab,
})));
sanitizedProperties[name] = tabs.filter(tab => !!tab);
})(treeItem));
else
tasks.push((async () => {
sanitizedProperties[name] = await exportTab(treeItem, {
addonId: addon.id,
light: !!addon.lightTree,
cache,
isContextTab,
});
})());
}
}
await Promise.all(tasks);
return { ...message, ...sanitizedProperties };
}
// =======================================================================
// Common utilities for request-response type API call
// =======================================================================
export async function getTargetTabs(message, sender) {
const tabQuery = message.tabs || message.tabIds || message.tab || message.tabId;
const windowId = message.window || message.windowId;
if (Array.isArray(tabQuery))
await Promise.all(tabQuery.map(oneTabQuery => {
if (typeof oneTabQuery == 'number')
return Tab.waitUntilTracked(oneTabQuery)
return true;
}));
else if (typeof tabQuery == 'number')
await Tab.waitUntilTracked(tabQuery);
if (windowId)
await Tab.waitUntilTrackedAll(windowId);
const queryOptions = {};
if (Array.isArray(queryOptions.states)) {
queryOptions.states = queryOptions.states || [];
queryOptions.states.push(...queryOptions.states.map(state => [state, true]));
}
if (Array.isArray(queryOptions.statesNot)) {
queryOptions.states = queryOptions.statesNot || [];
queryOptions.states.push(...queryOptions.statesNot.map(state => [state, false]));
}
if (Array.isArray(tabQuery))
return getTabsByQueries(tabQuery, { windowId, queryOptions, sender });
if (windowId) {
if (tabQuery == '*')
return Tab.getAllTabs(windowId, { ...queryOptions, iterator: true });
else if (!tabQuery)
return Tab.getRootTabs(windowId, { ...queryOptions, iterator: true });
}
if (tabQuery == '*') {
const win = await browser.windows.getLastFocused({
windowTypes: ['normal']
}).catch(ApiTabs.createErrorHandler());
return Tab.getAllTabs(win.id, { ...queryOptions, iterator: true });
}
if (tabQuery) {
let tabs = await getTabsByQueries([tabQuery], { windowId, queryOptions, sender });
if (queryOptions.states)
tabs = tabs.filter(tab => {
const unified = new Set([...tab.$TST.states, ...queryOptions.states]);
return unified.size == tab.$TST.states.size;
});
if (queryOptions.statesNot)
tabs = tabs.filter(tab => {
const unified = new Set([...tab.$TST.states, ...queryOptions.statesNot]);
return unified.size > tab.$TST.states.size;
});
return tabs;
}
return [];
}
export async function getTargetRenderedTabs(message, sender) {
// Don't touch to this "tabs" until they are finally returned.
// Populating it to an array while operations will finishes
// the iterator and returned tabs will become just blank.
const tabs = await getTargetTabs(message, sender);
if (!tabs)
return tabs;
const windowId = message.window ||
message.windowId ||
await browser.windows.getLastFocused({
windowTypes: ['normal']
}).catch(ApiTabs.createErrorHandler()).then(win => win?.id);
const renderedTabIds = await browser.runtime.sendMessage({
type: Constants.kCOMMAND_GET_RENDERED_TAB_IDS,
windowId,
});
const renderedTabIdsSet = new Set(renderedTabIds);
return Array.from(tabs).filter(tab => renderedTabIdsSet.has(tab.id));
}
async function getTabsByQueries(queries, { windowId, queryOptions, sender }) {
const win = !windowId && await browser.windows.getLastFocused({
populate: true
}).catch(ApiTabs.createErrorHandler());
const activeWindow = TabsStore.windows.get(windowId || win.id) || win;
const tabs = await Promise.all(queries.map(query => getTabsByQuery(query, { activeWindow, queryOptions, sender }).catch(error => {
console.error(error);
return null;
})));
log('getTabsByQueries: ', queries, ' => ', tabs, 'sender: ', sender, windowId);
return tabs.flat().filter(tab => !!tab);
}
async function getTabsByQuery(query, { activeWindow, queryOptions, sender }) {
log('getTabsByQuery: ', { query, activeWindow, queryOptions, sender });
if (query && typeof query == 'object' && typeof query.id == 'number') // tabs.Tab
query = query.id;
let id = query;
query = String(query).toLowerCase();
let baseTab = Tab.getActiveTab(activeWindow.id);
// this sometimes happen when the active tab was detached from the window
if (!baseTab)
return null;
const nonActiveTabMatched = query.match(/^([^-]+)-of-(.+)$/i);
if (nonActiveTabMatched) {
query = nonActiveTabMatched[1];
id = nonActiveTabMatched[2];
if (/^\d+$/.test(id))
id = parseInt(id);
baseTab = Tab.get(id) || Tab.getByUniqueId(id);
if (!baseTab)
return null;
}
switch (query) {
case 'active':
case 'current':
return baseTab;
case 'parent':
return baseTab.$TST.parent;
case 'root':
return baseTab.$TST.rootTab;
case 'next':
return baseTab.$TST.nextTab;
case 'nextcyclic':
return baseTab.$TST.nextTab || Tab.getFirstTab(baseTab.windowId, queryOptions || {});
case 'previous':
case 'prev':
return baseTab.$TST.previousTab;
case 'previouscyclic':
case 'prevcyclic':
return baseTab.$TST.previousTab || Tab.getLastTab(baseTab.windowId, queryOptions || {});
case 'nextsibling':
return baseTab.$TST.nextSiblingTab;
case 'nextsiblingcyclic': {
const nextSibling = baseTab.$TST.nextSiblingTab;
if (nextSibling)
return nextSibling;
const parent = baseTab.$TST.parent;
if (parent)
return parent.$TST.firstChild;
return Tab.getFirstTab(baseTab.windowId, queryOptions || {});
}
case 'previoussibling':
case 'prevsibling':
return baseTab.$TST.previousSiblingTab;
case 'previoussiblingcyclic':
case 'prevsiblingcyclic': {
const previousSiblingTab = baseTab.$TST.previousSiblingTab;
if (previousSiblingTab)
return previousSiblingTab;
const parent = baseTab.$TST.parent;
if (parent)
return parent.$TST.lastChild;
return Tab.getLastRootTab(baseTab.windowId, queryOptions || {});
}
case 'nextvisible':
return baseTab.$TST.nearestVisibleFollowingTab;
case 'nextvisiblecyclic':
return baseTab.$TST.nearestVisibleFollowingTab || Tab.getFirstVisibleTab(baseTab.windowId, queryOptions || {});
case 'previousvisible':
case 'prevvisible':
return baseTab.$TST.nearestVisiblePrecedingTab;
case 'previousvisiblecyclic':
case 'prevvisiblecyclic':
return baseTab.$TST.nearestVisiblePrecedingTab || Tab.getLastVisibleTab(baseTab.windowId, queryOptions || {});
case 'lastdescendant':
return baseTab.$TST.lastDescendant;
case 'sendertab':
return Tab.get(sender?.tab?.id) || null;
case 'highlighted':
case 'multiselected':
return Tab.getHighlightedTabs(baseTab.windowId, queryOptions || {});
case 'allvisibles':
return Tab.getVisibleTabs(baseTab.windowId, queryOptions || {});
case 'normalvisibles':
return Tab.getVisibleTabs(baseTab.windowId, { ...(queryOptions || {}), normal: true });
default:
return Tab.get(id) || Tab.getByUniqueId(id);
}
}
export function formatResult(results, originalMessage) {
if (Array.isArray(originalMessage.tabs) ||
originalMessage.tab == '*' ||
originalMessage.tabs == '*')
return results;
if (originalMessage.tab)
return results[0];
return results;
}
const TABS_ARRAY_QUERY_MATCHER = /^(\*|allvisibles|normalvisibles)$/i;
export async function formatTabResult(exportedTabs, originalMessage) {
exportedTabs = await Promise.all(exportedTabs);
if (Array.isArray(originalMessage.tabs) ||
TABS_ARRAY_QUERY_MATCHER.test(originalMessage.tab) ||
TABS_ARRAY_QUERY_MATCHER.test(originalMessage.tabs))
return exportedTabs.filter(tab => !!tab);
return exportedTabs.length == 0 ?
null :
exportedTabs[0];
}