From af68f2610c9d2176d98d7c943691f788b95f809c Mon Sep 17 00:00:00 2001 From: "YUKI \"Piro\" Hiroshi" Date: Wed, 25 Sep 2024 14:48:22 +0900 Subject: [PATCH 1/2] Replace implementation of Chrome extension with ThinBridge --- .gitignore | 1 - Makefile | 34 +-- README.md | 23 +- chrome/.eslintrc.js | 15 ++ chrome/.gitignore | 3 + chrome/Makefile | 12 + chrome/background.js | 599 +++++++++++++++++++++++++++++++++++++++++++ chrome/manifest.json | 23 ++ 8 files changed, 680 insertions(+), 30 deletions(-) mode change 100755 => 100644 README.md create mode 100644 chrome/.eslintrc.js create mode 100644 chrome/.gitignore create mode 100644 chrome/Makefile create mode 100644 chrome/background.js create mode 100644 chrome/manifest.json diff --git a/.gitignore b/.gitignore index 7e0489c..7ba01c1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,3 @@ extlib/*.js node_modules package-lock.json -chrome diff --git a/Makefile b/Makefile index 9d39dfb..d7ebc44 100644 --- a/Makefile +++ b/Makefile @@ -1,52 +1,40 @@ -.PHONY: prepare xpi chrome chrome-dev host managed install_dependency lint format install_hook +.PHONY: xpi-prepare xpi chrome host managed install_dependency lint format install_hook TIMESTAMP=$(shell date +%Y%m%d) NPM_MOD_DIR := $(CURDIR)/node_modules NPM_BIN_DIR := $(NPM_MOD_DIR)/.bin -prepare: +xpi-prepare: git submodule update --init cp submodules/webextensions-lib-configs/Configs.js extlib/; echo 'export default Configs;' >> extlib/Configs.js cp submodules/webextensions-lib-options/Options.js extlib/; echo 'export default Options;' >> extlib/Options.js cp submodules/webextensions-lib-l10n/l10n.js extlib/; echo 'export default l10n;' >> extlib/l10n.js -xpi: prepare +xpi: xpi-prepare rm -f ieview-we.xpi zip -r -9 ieview-we.xpi manifest.json *.js _locales common options misc/128x128.png extlib -x '*/.*' -x extlib/browser-polyfill.min.js -chrome: prepare +chrome: [ -d node_modules ] || npm install - rm -rf chrome ieview-we-${TIMESTAMP}.zip - mkdir -p chrome/misc - cat manifest.json | jq 'del(.applications)' | jq '.storage.managed_schema = "managed_schema.json"' > chrome/manifest.json - cp -r managed_schema.json _locales background common options extlib chrome/ - cp -r misc/128x128.png chrome/misc - find chrome -name '.*' | xargs rm -rf - #cp node_modules/webextension-polyfill/dist/browser-polyfill.min.js chrome/extlib/ - sed -i -E -e 's;("scripts": *\[);\1"extlib/browser-polyfill.min.js",;' chrome/manifest.json - #sed -i -E -e 's;;<\1>;' chrome/options/options.html - sed -i -E -e 's;\bbrowser\.;chrome.;g' -e 's;window.messenger;false;g' chrome/*/*.js - cd chrome && zip -r ../ieview-we-${TIMESTAMP}.zip . - -chrome-dev: chrome - rm -rf ieview-we-dev-${TIMESTAMP}.zip - sed -i -E -e 's/IE View WE/IE View WE Developer Edition/g' chrome/_locales/*/messages.json - cd chrome && zip -r ../ieview-we-dev-${TIMESTAMP}.zip . + rm -rf ieview-we-chrome-${TIMESTAMP}.zip + cd chrome && make + cp chrome/ieview-we-chrome.zip ./ieview-we-chrome-${TIMESTAMP}.zip # knldjmfmopnpolahpmmgbagdohdnhkik DUMMY_KEY="MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDcBHwzDvyBQ6bDppkIs9MP4ksKqCMyXQ/A52JivHZKh4YO/9vJsT3oaYhSpDCE9RPocOEQvwsHsFReW2nUEc6OLLyoCFFxIb7KkLGsmfakkut/fFdNJYh0xOTbSN8YvLWcqph09XAY2Y/f0AL7vfO1cuCqtkMt8hFrBGWxDdf9CQIDAQAB" -chrome-test: chrome +chrome-test: cat chrome/manifest.json | jq '.key = ${DUMMY_KEY}' > chrome/manifest.json.tmp mv chrome/manifest.json.tmp chrome/manifest.json - cd chrome && zip -r ../ieview-we-test-${TIMESTAMP}.zip . + cd chrome && make dev + cp chrome/ieview-we-chrome-dev.zip ./ieview-we-chrome-dev-${TIMESTAMP}.zip host: host/build.sh rm -f ieview-we-host.zip cd host && zip -r -9 ../ieview-we-host.zip 386 amd64 *.bat *.json -managed: prepare +managed: xpi-prepare rm -f ieview-we-managed-storage.zip cd managed-storage && zip -r -9 ../ieview-we-managed-storage.zip *.bat *.json diff --git a/README.md b/README.md old mode 100755 new mode 100644 index ae734a0..a8c120a --- a/README.md +++ b/README.md @@ -4,22 +4,33 @@ Provides ability to open pages and links by Internet Explorer (Cloned IE View ba This works only on Windows. +# for Google Chrome (IE View WE MV3) + +This was initially started as a cloned IE View based on WebExtensions API, but finally dropped support of original IE View compatible features including the options page. +Only ThinBridge compatible implementation is left on the browser extension part, thus you need to install [ThinBridge](https://github.com/ThinBridge/ThinBridge/) as the native messaging host. +See [ThinBridge](https://github.com/ThinBridge/ThinBridge/) for more details. + +IE View WE MV3 msut be loaded via GPO. +See [the document describing how to install development version of the extension via GPO, in the TinBridge project](https://github.com/ThinBridge/ThinBridge/blob/master/DEVELOPMENT.md#how-to-try-extensions-for-development). + +# for Firefox (IE View WE MV2) + *IMPORTANT NOTE: The list of URLs which should be opened by IE is not compatible to the legacy version's one.* You need to rewrite them based on the [matching pattern spec for Firefox addons](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Match_patterns). For example: `http://example.com` => `http://example.com/*` (note that the added wild card to match any page under the domain.) -# Steps to install +## Steps to install 1. Download the MSI suitable for your environment's architecture, or a zip package "ieview-we-host.zip" from the [releases page](https://github.com/clear-code/ieview-we/releases/latest). 2. If you've downloaded an MSI, run it to install. Otherwise, unzip the downloaded file and double-click the batch file named "install.bat". 3. [Install "IE View WE" Firefox addon from its xpi package.](https://addons.mozilla.org/firefox/addon/ie-view-we/) -# Steps to uninstall +## Steps to uninstall 1. Uninstall "IE View WE" Firefox addon via the addon manager. 2. Double-click the batch file named `uninstall.bat`. -# How to customize Options +## How to customize Options There are some options which you can customize default behavior: You can also customize preset configuration via MCD. @@ -28,7 +39,7 @@ You can also customize preset configuration via MCD. * `extensions.ieview.ieargs` Command Line Arguments (default: empty) * `extensions.ieview.contextMenu` Add "Open by IE" items to the context menu (default: true) -## Rules to open by ... +### Rules to open by ... * `extensions.ieview.forceielist` Websites to be opened by IE always (default: empty) * `extensions.ieview.disableForce` Disable websites opened by IE always (dfault: false) @@ -37,14 +48,14 @@ You can also customize preset configuration via MCD. * `extensions.ieview.onlyMainFrame` Only check URL which is shown in location bar (default: true) * `extensions.ieview.ignoreQueryString` Ignore query string in URL (default: false) -## Logging & Debugging +### Logging & Debugging * `extensions.ieview.debug` Print Debug log (default: false) * `extensions.ieview.logging` Save log (default: true) * `extensions.ieview.logRotationTime` Rotate log file by specified hour (default: 24) * `extensions.ieview.logRotationCount` Max count of log files (default: 12) -## How to build the native messaging host and its installer +### How to build the native messaging host and its installer On Windows 10 + WSL: diff --git a/chrome/.eslintrc.js b/chrome/.eslintrc.js new file mode 100644 index 0000000..0f54413 --- /dev/null +++ b/chrome/.eslintrc.js @@ -0,0 +1,15 @@ +/* +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ +/*eslint-env commonjs*/ +/*eslint quote-props: ['error', "always"] */ + +'use strict'; + +module.exports = { + 'extends': [ + '../tools/eslint/for-module.js', + ], +}; diff --git a/chrome/.gitignore b/chrome/.gitignore new file mode 100644 index 0000000..318a80f --- /dev/null +++ b/chrome/.gitignore @@ -0,0 +1,3 @@ +*.zip +dev/ + diff --git a/chrome/Makefile b/chrome/Makefile new file mode 100644 index 0000000..66e385a --- /dev/null +++ b/chrome/Makefile @@ -0,0 +1,12 @@ +.PHONY: clean zip + +FILES = manifest.json \ + background.js + +all: zip + +clean: + rm -f *.zip + +zip: $(FILES) + zip -9 - $(FILES) > ieview-we-chrome.zip diff --git a/chrome/background.js b/chrome/background.js new file mode 100644 index 0000000..590c817 --- /dev/null +++ b/chrome/background.js @@ -0,0 +1,599 @@ +/* +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ +'use strict'; + +/* + * Basic settings for modern browsers + * + * Programming Note: Just tweak these constants for each browser. + * It should work fine across Edge, Chrome and Firefox without any + * further modifications. + */ +const BROWSER = 'chrome'; +const DMZ_SECTION = 'custom18'; +const CONTINUOUS_SECTION = 'custom19'; +const SERVER_NAME = 'com.clear_code.thinbridge'; +const ALARM_MINUTES = 0.5; +/* + * When `{cancel: 1}` is used to block loading, Edge shows a warning page which + * indicates that loading is canceled by an add-on. To avoid it, move back to + * the previous page instead of blocking. + */ +const CANCEL_REQUEST = {redirectUrl:`data:text/html,${escape('')}`}; +/* + * Although even if we return `CANCEL_REQUEST` from `onBeforeRequest()` on a + * sub-frame, `history.back()` will be performed against it's parent main + * frame when there is no page to back in the sub-frame. As a result main + * frame moves back to the previous page unexpectedly. + * To avoid it, just move to blank page instead. + */ +const CANCEL_REQUEST_FOR_SUBFRAME = {redirectUrl:'about:blank'}; +const REDIRECT_INTERVAL_LIMIT = 1000; + +/* + * ThinBridge's matching function (See BHORedirector/URLRedirectCore.h) + * + * 1. `?` represents a single character. + * 2. `*` represents an arbitrary substring. + * + * >>> wildcmp("http?://*.example.com/*", "https://www.example.com/") + * true + */ +function wildcmp(wild, string) { + let i = 0; + let j = 0; + let mp, cp; + + while ((j < string.length) && (wild[i] != '*')) { + if ((wild[i] != string[j]) && (wild[i] != '?')) { + return 0; + } + i += 1; + j += 1; + } + while (j < string.length) { + if (wild[i] == '*') { + i += 1; + + if (i == wild.length) { + return 1; + } + mp = i; + cp = j + 1 + } else if ((wild[i] == string[j]) || (wild[i] == '?')) { + i += 1; + j += 1; + } else { + i = mp; + j = cp; + cp += 1; + } + } + while (wild[i] == '*' && i < wild.length) { + i += 1; + } + return i >= wild.length; +}; + +/* + * Observe WebRequests with config fetched from ThinBridge. + * + * A typical configuration looks like this: + * + * { + * CloseEmptyTab:1, OnlyMainFrame:1, IgnoreQueryString:1, DefaultBrowser:"IE", + * Sections: [ + * {Name:"ie", Path:"", Patterns:["*://example.com/*"], Excludes:[]}, + * ... + * ] + * } + */ +const ThinBridgeTalkClient = { + newTabIds: new Set(), + knownTabIds: new Set(), + resumed: false, + + init() { + this.cached = null; + this.ensureLoadedAndConfigured(); + this.recentRequests = {}; + console.log('Running as Thinbridge Talk client'); + }, + + async ensureLoadedAndConfigured() { + return this._promisedLoadedAndConfigured = this._promisedLoadedAndConfigured || Promise.all([ + !this.cached && this.configure(), + this.load(), + ]); + }, + _promisedLoadedAndConfigured: null, + + async configure() { + const query = new String('C ' + BROWSER); + + const resp = await chrome.runtime.sendNativeMessage(SERVER_NAME, query); + if (chrome.runtime.lastError) { + console.log('Cannot fetch config', JSON.stringify(chrome.runtime.lastError)); + return; + } + const isStartup = (this.cached == null); + this.cached = resp.config; + this.cached.NamedSections = Object.fromEntries(resp.config.Sections.map(section => [section.Name.toLowerCase(), section])); + console.log('Fetch config', JSON.stringify(this.cached)); + + if (isStartup && !this.resumed) { + this.handleStartup(this.cached); + } + }, + + save() { + chrome.storage.session.set({ + newTabIds: [...this.newTabIds], + knownTabIds: [...this.knownTabIds], + }); + }, + + async load() { + if (this.$promisedLoaded) + return this.$promisedLoaded; + + console.log(`Redirector: loading previous state`); + return this.$promisedLoaded = new Promise(async (resolve, _reject) => { + try { + const { newTabIds, knownTabIds } = await chrome.storage.session.get({ newTabIds: null, knownTabIds: null }); + console.log(`ThinBridgeTalkClient: loaded newTabIds, knownTabIds => `, JSON.stringify(newTabIds), JSON.stringify(knownTabIds)); + this.resumed = !!(newTabIds || knownTabIds); + if (newTabIds) { + for (const tabId of newTabIds) { + this.newTabIds.add(tabId); + } + } + if (knownTabIds) { + for (const tabId of knownTabIds) { + this.knownTabIds.add(tabId); + } + } + } + catch(error) { + console.log('ThinBridgeTalkClient: failed to load previous state: ', error.name, error.message); + } + resolve(); + }); + }, + + /* + * Request redirection to Native Messaging Hosts. + * + * * chrome.tabs.get() is to confirm that the URL is originated from + * an actual tab (= not an internal prefetch request). + * + * * Request Example: "Q edge https://example.com/". + */ + redirect(url, tabId, closeTab) { + chrome.tabs.get(tabId).then(async tab => { + if (chrome.runtime.lastError) { + console.log(`* Ignore prefetch request`); + return; + } + if (!tab) { + console.log(`* URL is not coming from an actual tab`); + return; + } + + const query = new String('Q ' + BROWSER + ' ' + url); + await chrome.runtime.sendNativeMessage(SERVER_NAME, query); + + if (!closeTab) + return; + + let existingTab = tab; + let counter = 0; + do { + if (!existingTab) + break; + if (counter > 100) { + console.log(`couldn't close tab ${tabId} within ${counter} times retry.`); + break; + } + if (counter++ > 0) + console.log(`tab ${tabId} still exists: trying to close (${counter})`); + await chrome.tabs.remove(tabId); + } while (existingTab = await chrome.tabs.get(tabId).catch(_error => null)); + }); + }, + + match(section, url, namedSections) { + for (const name of (section.ExcludeGroups || [])) { + const foreignSection = namedSections[name.toLowerCase()]; + //console.log(`* Referring exclude group ${name}: ${JSON.stringify(foreignSection && (foreignSection.URLPatterns || foreignSection.Patterns))}`); + if (!foreignSection) + continue; + for (let pattern of (foreignSection.URLPatterns || foreignSection.Patterns || [])) { + if (Array.isArray(pattern)) { + pattern = pattern[0]; + } + if (wildcmp(pattern, url)) { + console.log(`* Match Exclude ${section.Name} (referring ${name}) [${pattern}]`); + return false; + } + } + } + + for (let pattern of (section.URLExcludePatterns || section.Excludes || [])) { + if (Array.isArray(pattern)) { + pattern = pattern[0]; + } + if (wildcmp(pattern, url)) { + console.log(`* Match Exclude ${section.Name} [${pattern}]`); + return false; + } + } + + for (let pattern of (section.URLPatterns || section.Patterns || [])) { + if (Array.isArray(pattern)) { + pattern = pattern[0]; + } + if (wildcmp(pattern, url)) { + console.log(`* Match ${section.Name} [${pattern}]`); + return true; + } + } + return false; + }, + + getBrowserName(section) { + const name = section.Name.toLowerCase(); + + if (name == DMZ_SECTION) + return name; + + /* Guess the browser name from the executable path */ + if (name.match(/^custom/i)) { + if (section.Path.match(RegExp(BROWSER, 'i'))) + return BROWSER; + } + return name; + }, + + checkRedirectIntervalLimit(tabId, url) { + const now = Date.now(); + let skip = false; + if (!this.recentRequests) { + // in unit test + return false; + } + for (const key in this.recentRequests) { + if (Math.abs(now - this.recentRequests[key].time) > REDIRECT_INTERVAL_LIMIT) + delete this.recentRequests[key]; + } + const recent = this.recentRequests[tabId]; + if (recent && recent.url === url) { + skip = true; + } + this.recentRequests[tabId] = { tabId: tabId, url: url, time: now } + return skip; + }, + + handleURLAndBlock(config, tabId, url, isClosableTab) { + if (!url) { + console.log(`* Empty URL found`); + return false; + } + + if (!/^https?:/.test(url)) { + console.log(`* Ignore non-HTTP/HTTPS URL ${url}`); + return false; + } + + // Just store recent request, don't block here. + // It should be determined by caller. + // (onBeforeRequest() should always block loading redirect URL.) + this.checkRedirectIntervalLimit(tabId, url); + + const urlToMatch = config.IgnoreQueryString ? url.replace(/\?.*/, '') : url; + + console.log(`* Lookup sections for ${urlToMatch}`); + + const closeTabOnRedirect = config.CloseEmptyTab && isClosableTab; + + let loadCount = 0; + let redirectCount = 0; + let isActionMode = false; + const matchedSectionNames = []; + sectionsLoop: + for (const section of config.Sections) { + console.log(`handleURLAndBlock: check for section ${section.Name} (${JSON.stringify(section)})`); + + if (section.Action) + isActionMode = true; + + if (!this.match(section, urlToMatch, config.NamedSections)) { + console.log(` => unmatched`); + continue; + } + + const sectionName = (section.Name || '').toLowerCase(); + matchedSectionNames.push(sectionName); + + console.log(` => matched, action = ${section.Action}`); + if (section.Action) { + // a.k.a "full mode" in IE View WE + switch(section.Action.toLowerCase()) { + case 'redirect': + redirectCount++; + break; + + case 'load': + default: + loadCount++; + break; + } + if (sectionName == DMZ_SECTION || sectionName == CONTINUOUS_SECTION) + break sectionsLoop; + } + else { + // Compatible mode with ManifestV2 version of this add-on + switch (this.getBrowserName(section)) { + case DMZ_SECTION: + console.log(` => action not defined, default action for CUSTOM18: load`); + loadCount++; + break sectionsLoop; + + case BROWSER.toLowerCase(): + console.log(` => action not defined, default action for ${BROWSER}: load`); + loadCount++; + break; + + default: + console.log(` => action not defined, default action: redirect`); + redirectCount++; + if (sectionName == CONTINUOUS_SECTION) + break sectionsLoop; + break; + } + } + } + console.log(`* Result: [${matchedSectionNames.join(', ')}]`); + + if (isActionMode) { + // a.k.a "full mode" in IE View WE + console.log(`* Dispatch as action mode`); + if (redirectCount > 0 || loadCount == 0) { + console.log(`* Redirect to another browser`); + this.redirect(url, tabId, closeTabOnRedirect); + } + console.log(`* Continue to load: ${loadCount > 0}`); + return loadCount == 0; + } + else { + // Compatible mode with ManifestV2 version of this add-on + console.log(`* Dispatch as compatible mode`); + + if (loadCount > 0) { + console.log(`* Continue to load`); + return false; + } + + if (redirectCount > 0) { + console.log(`* Redirect to another browser`); + this.redirect(url, tabId, closeTabOnRedirect); + return true; + } + + if (config.DefaultBrowser) { + console.log(`* Use DefaultBrowser: ${config.DefaultBrowser}`); + if (String(config.DefaultBrowser).toLowerCase() == BROWSER.toLowerCase()) { + return false; + } else { + this.redirect(url, tabId, closeTabOnRedirect); + return true; + } + } else { + console.log(`* DefaultBrowser is blank`); + return false; + } + } + }, + + /* Handle startup tabs preceding to onBeforeRequest */ + handleStartup(config) { + chrome.tabs.query({}).then(tabs => { + tabs.forEach((tab) => { + const url = tab.url || tab.pendingUrl; + console.log(`handleStartup ${url} (tab=${tab.id})`); + if (!this.handleURLAndBlock(config, tab.id, url, true)) + this.knownTabIds.add(tab.id); + }); + }); + }, + + async onTabCreated(tab) { + await this.ensureLoadedAndConfigured(); + this.newTabIds.add(tab.id); + this.save(); + }, + + async onTabRemoved(tabId, _removeInfo) { + await this.ensureLoadedAndConfigured(); + this.newTabIds.delete(tabId); + this.knownTabIds.delete(tabId); + this.save(); + }, + + async onTabUpdated(tabId, info, tab) { + await this.ensureLoadedAndConfigured(); + + // We should close new (empty) tab, but not for already handled tab by + // handleStartup or onBeforeReqeust that are possible to be called before + // onTabCreated. The later condition is the guard for it. + const isClosableTab = this.newTabIds.has(tabId) && !this.knownTabIds.has(tabId) + this.knownTabIds.add(tabId); + this.newTabIds.delete(tabId); + + const config = this.cached; + const url = tab.pendingUrl || tab.url; + + if (!config) { + this.save(); + return; + } + + console.log(`onTabUpdated ${url} (tab=${tabId}, windowId=${tab.windowId}, status=${info.status}/${tab.status})`); + + if (info.status !== 'loading' && + info.status !== undefined /* IE Mode tab on Edge will have undefined status */) + return; + + if (this.checkRedirectIntervalLimit(tabId, url)) { + console.log(`A request for same URL and same tabId already occurred in ${REDIRECT_INTERVAL_LIMIT} msec. Skip it.`); + return false; + } + + // If onBeforeRequest() fails to redirect due to missing config, the next chance to do it is here. + if (!this.handleURLAndBlock(config, tabId, url, isClosableTab)) + return; + + if (isClosableTab) { + // The tab is considered to be closed by handleURLAndBlock(). + return; + } + + /* Call executeScript() to stop the page loading immediately. + * Then let the tab go back to the previous page. + */ + chrome.scripting.executeScript({ + target: { tabId }, + func: function goBack() { + window.stop(); + window.history.back(); + }, + }); + }, + + /* Callback for webRequest.onBeforeRequest */ + onBeforeRequest(details) { + const config = this.cached; + const isMainFrame = (details.type == 'main_frame'); + + console.log(`onBeforeRequest ${details.url} (tab=${details.tabId})`); + + if (!config) { + console.log('* Config cache is empty. Fetching...'); + this.configure(); + return; + } + + if (details.tabId < 0 || + details.documentLifecycle == 'prerender') { + console.log(`* Ignore internal request`); + return; + } + + if (config.OnlyMainFrame && !isMainFrame) { + console.log(`* Ignore subframe request`); + return; + } + + const isClosableTab = isMainFrame && (this.newTabIds.has(details.tabId) || !this.knownTabIds.has(details.tabId)); + + if (this.handleURLAndBlock(config, details.tabId, details.url, isClosableTab)) { + if (isMainFrame) + return CANCEL_REQUEST; + else + return CANCEL_REQUEST_FOR_SUBFRAME; + } + + this.knownTabIds.add(details.tabId); + }, +}; + +chrome.webRequest.onBeforeRequest.addListener( + ThinBridgeTalkClient.onBeforeRequest.bind(ThinBridgeTalkClient), + { + urls: [''], + types: ['main_frame','sub_frame'] + }, + ['blocking'] +); + +/* Refresh config for every N minute */ +console.log('Poll config for every', ALARM_MINUTES , 'minutes'); +chrome.alarms.create('poll-config', {'periodInMinutes': ALARM_MINUTES}); + +chrome.alarms.onAlarm.addListener((alarm) => { + if (alarm.name === 'poll-config') { + ThinBridgeTalkClient.configure(); + } +}); + +/* Tab book-keeping for intelligent tab handlings */ +chrome.tabs.onCreated.addListener(ThinBridgeTalkClient.onTabCreated.bind(ThinBridgeTalkClient)); +chrome.tabs.onUpdated.addListener(ThinBridgeTalkClient.onTabUpdated.bind(ThinBridgeTalkClient)); + + +/* + * Support ThinBridge's resource cap feature + */ +const ResourceCap = { + + init() { + console.log('Running Resource Cap client'); + }, + + /* + * On each navigation, we ask the host program to check the + * current resource usage. + */ + onNavigationCommitted(details) { + console.log(`onNavigationCommitted: ${details.url}`); + + /* frameId != 0 indicates iframe requests */ + if (details.frameId) { + console.log(`* Ignore subframe requests`); + return; + } + + chrome.tabs.query({}).then(tabs => { + const ntabs = this.count(tabs); + console.log(`* Perform resource check (ntabs=${ntabs})`); + this.check(details.tabId, ntabs); + }); + }, + + check(tabId, ntabs) { + const query = new String(`R ${BROWSER} ${ntabs}`); + chrome.runtime.sendNativeMessage(SERVER_NAME, query).then(resp => { + // Need this to support ThinBridge v4.0.2.3 (or before) + if (chrome.runtime.lastError) { + return; + } + + if (resp.closeTab) { + chrome.tabs.remove(tabId).then(() => { + if (chrome.runtime.lastError) { + console.log(`* ${chrome.runtime.lastError}`); + return; + } + console.log(`* Close Tab#${tabId}`) + }); + } + }); + }, + + count(tabs) { + /* Exclude the internal pages such as "edge://blank" */ + tabs = tabs.filter((tab) => { + const url = tab.url || tab.pendingUrl; + return /^https?:/.test(url); + }); + return tabs.length; + } +}; + +chrome.webNavigation.onCommitted.addListener(ResourceCap.onNavigationCommitted.bind(ResourceCap)); + +ThinBridgeTalkClient.init(); +ResourceCap.init(); diff --git a/chrome/manifest.json b/chrome/manifest.json new file mode 100644 index 0000000..91ea78c --- /dev/null +++ b/chrome/manifest.json @@ -0,0 +1,23 @@ +{ + "manifest_version": 3, + "name": "IE View WE MV3", + "version": "3.0.0", + "description": "Provides ability to open pages and links by Internet Explorer (Originally cloned IE View based on WebExtensions-based)", + "permissions": [ + "nativeMessaging", + "alarms", + "scripting", + "storage", + "tabs", + "webNavigation", + "webRequest", + "webRequestBlocking" + ], + "host_permissions": [ + "" + ], + "background": { + "service_worker": "background.js", + "type": "module" + } +} From c5aaf48afb0104ea9a40ae8709aa12b3aca88418 Mon Sep 17 00:00:00 2001 From: "YUKI \"Piro\" Hiroshi" Date: Wed, 25 Sep 2024 15:12:58 +0900 Subject: [PATCH 2/2] Match description to the behavior --- chrome/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chrome/manifest.json b/chrome/manifest.json index 91ea78c..d0ab08c 100644 --- a/chrome/manifest.json +++ b/chrome/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "IE View WE MV3", "version": "3.0.0", - "description": "Provides ability to open pages and links by Internet Explorer (Originally cloned IE View based on WebExtensions-based)", + "description": "Provides ability to open pages and links by other browsers based on pre-defined rules for ThinBridge.", "permissions": [ "nativeMessaging", "alarms",