1503 lines
53 KiB
JavaScript
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];
|
|
}
|