diff --git a/README.md b/README.md index 4438f33..223e162 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ const manager = new Manager({ console.log("Hide progress bar"); } }, - inject_user_script: code => { + inject_plugin: plugin => { console.log("Code of UserScript plugin for embedding in a page:"); - console.log(code); + console.log(plugin['code']); } }); @@ -50,4 +50,4 @@ const uniqId = getUniqId("tmp"); ## License -[GPL-3.0 license](/LICENSE) \ No newline at end of file +[GPL-3.0 license](/LICENSE) diff --git a/package.json b/package.json index 910d732..9cb2ad9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lib-iitc-manager", - "version": "1.4.4", + "version": "1.5.0", "description": "Library for managing IITC plugins", "main": "src/index.js", "type": "module", @@ -30,7 +30,6 @@ "prettier": "^2.7.1" }, "dependencies": { - "eslint": "^8.13.0", "xhr2": "^0.2.1" } } diff --git a/src/index.js b/src/index.js index defa864..b4714d4 100644 --- a/src/index.js +++ b/src/index.js @@ -2,5 +2,6 @@ import { Manager } from './manager.js'; import { parseMeta, ajaxGet, getUniqId, getUID, check_meta_match_pattern, wait, clearWait } from './helpers.js'; +import { check_matching } from './matching.js'; -export { Manager, parseMeta, ajaxGet, getUniqId, getUID, check_meta_match_pattern, wait, clearWait }; +export { Manager, parseMeta, ajaxGet, getUniqId, getUID, check_meta_match_pattern, wait, clearWait, check_matching }; diff --git a/src/manager.js b/src/manager.js index b4c1b64..bc9f44e 100644 --- a/src/manager.js +++ b/src/manager.js @@ -75,33 +75,37 @@ export class Manager extends Worker { */ async inject() { const storage = await this.storage.get([ - this.channel + '_iitc_code', + this.channel + '_iitc_core', this.channel + '_plugins_flat', this.channel + '_plugins_local', this.channel + '_plugins_user', ]); - const iitc_code = storage[this.channel + '_iitc_code']; - + const iitc_core = storage[this.channel + '_iitc_core']; const plugins_local = storage[this.channel + '_plugins_local']; const plugins_user = storage[this.channel + '_plugins_user']; - if (iitc_code !== undefined) { - const userscripts = []; + if (iitc_core !== undefined && iitc_core['code'] !== undefined) { + const plugins_to_inject = []; // IITC is injected first, then plugins. This is the correct order, because the initialization of IITC takes some time. // During this time, plugins have time to be added to `window.bootPlugins` and are not started immediately. // In addition, thanks to the injecting of plugins after IITC, // plugins do not throw errors when attempting to access IITC, leaflet, etc. during the execution of the wrapper. - userscripts.push(iitc_code); + plugins_to_inject.push(iitc_core); const plugins_flat = storage[this.channel + '_plugins_flat']; for (const uid of Object.keys(plugins_flat)) { if (plugins_flat[uid]['status'] === 'on') { - userscripts.push(plugins_flat[uid]['user'] === true ? plugins_user[uid]['code'] : plugins_local[uid]['code']); + plugins_to_inject.push(plugins_flat[uid]['user'] === true ? plugins_user[uid] : plugins_local[uid]); } } - await Promise.all(userscripts.map((code) => this.inject_user_script(code))); + await Promise.all( + plugins_to_inject.map((pl) => { + this.inject_user_script(pl['code']); + this.inject_plugin(pl); + }) + ); } } @@ -144,6 +148,7 @@ export class Manager extends Worker { } this.inject_user_script(plugins_flat[uid]['user'] === true ? plugins_user[uid]['code'] : plugins_local[uid]['code']); + this.inject_plugin(plugins_flat[uid]['user'] === true ? plugins_user[uid] : plugins_local[uid]); await this._save({ plugins_flat: plugins_flat, @@ -159,6 +164,7 @@ export class Manager extends Worker { plugins_local[uid]['code'] = response; this.inject_user_script(plugins_local[uid]['code']); + this.inject_plugin(plugins_local[uid]); await this._save({ plugins_flat: plugins_flat, diff --git a/src/matching.js b/src/matching.js new file mode 100644 index 0000000..b83621f --- /dev/null +++ b/src/matching.js @@ -0,0 +1,112 @@ +// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3 + +const CACHE = {}; +const RE_URL = /(.*?):\/\/([^/]*)\/(.*)/; + +/** + * Checks the URL for match/include plugin. + * + * @param {plugin} meta - Object with data from ==UserScript== header. + * @param {string} url - Page URL. + * @return {boolean} + */ +export function check_matching(meta, url) { + const match = meta.match || []; + const include = meta.include || []; + const match_exclude = meta['exclude-match'] || []; + const exclude = meta.exclude || []; + + // match all if no @match or @include rule and set url === '' + let ok = !match.length && !include.length && url === ''; + // @match + ok = ok || testMatch(url, match); + // @include + ok = ok || testInclude(url, include); + // @exclude-match + ok = ok && !testMatch(url, match_exclude); + // @exclude + ok = ok && !testInclude(url, exclude); + return ok; +} + +function str2RE(str) { + const re = str.replace(/([.?/])/g, '\\$1').replace(/\*/g, '.*?'); + return RegExp(`^${re}$`); +} + +/** + * Test glob rules like `@include` and `@exclude`. + */ +export function testInclude(url, rules) { + return rules.some((rule) => { + const key = `re:${rule}`; + let re = CACHE[key]; + if (!re) { + re = makeIncludeRegExp(rule); + CACHE[key] = re; + } + return re.test(url); + }); +} + +function makeIncludeRegExp(str) { + if (str.length > 1 && str[0] === '/' && str[str.length - 1] === '/') { + return RegExp(str.slice(1, -1)); // Regular-expression + } + return str2RE(str); // Wildcard +} + +/** + * Test match rules like `@match` and `@exclude_match`. + */ +export function testMatch(url, rules) { + return rules.some((rule) => { + const key = `match:${rule}`; + let matcher = CACHE[key]; + if (!matcher) { + matcher = makeMatchRegExp(rule); + CACHE[key] = matcher; + } + return matcher.test(url); + }); +} + +function makeMatchRegExp(rule) { + let test; + if (rule === '') test = () => true; + else { + const ruleParts = rule.match(RE_URL); + test = (url) => { + const parts = url.match(RE_URL); + return !!ruleParts && !!parts && matchScheme(ruleParts[1], parts[1]) && matchHost(ruleParts[2], parts[2]) && matchPath(ruleParts[3], parts[3]); + }; + } + return { test }; +} + +function matchScheme(rule, data) { + // exact match + if (rule === data) return 1; + // * = http | https + if (rule === '*' && /^https?$/i.test(data)) return 1; + return 0; +} + +function matchHost(rule, data) { + // * matches all + if (rule === '*') return 1; + // exact match + if (rule === data) return 1; + // *.example.com + if (/^\*\.[^*]*$/.test(rule)) { + // matches the specified domain + if (rule.slice(2) === data) return 1; + // matches subdomains + if (str2RE(rule).test(data)) return 1; + } + return 0; +} + +function matchPath(rule, data) { + return str2RE(rule).test(data); +} diff --git a/src/migrations.js b/src/migrations.js index a41d382..767869c 100644 --- a/src/migrations.js +++ b/src/migrations.js @@ -1,14 +1,15 @@ // @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3 -import { isSet, getUID } from './helpers.js'; +import { isSet, getUID, parseMeta } from './helpers.js'; export function number_of_migrations() { return migrates.length; } -const migrates = [migration_0001, migration_0002, migration_0003]; +const migrates = [migration_0001, migration_0002, migration_0003, migration_0004]; export async function migrate(storage) { + const storage_iitc_code = await storage.get(['release_iitc_code', 'beta_iitc_code', 'custom_iitc_code']); const storage_plugins_flat = await storage.get([ 'release_plugins_flat', 'beta_plugins_flat', @@ -33,17 +34,17 @@ export async function migrate(storage) { for (const migrate of migrates) { const index = migrates.indexOf(migrate); if (parseInt(storage_misc['storage_version']) < index + 1) { - await migrate(storage_plugins_flat, storage_plugins_user, storage_misc); + await migrate(storage_iitc_code, storage_plugins_flat, storage_plugins_user, storage_misc); is_migrated = true; } } storage_misc['storage_version'] = migrates.length; - await storage.set({ ...storage_plugins_flat, ...storage_plugins_user, ...storage_misc }); + await storage.set({ ...storage_iitc_code, ...storage_plugins_flat, ...storage_plugins_user, ...storage_misc }); return is_migrated; } -async function migration_0001(storage_plugins_flat) { +async function migration_0001(storage_iitc_code, storage_plugins_flat) { for (let channel of Object.keys(storage_plugins_flat)) { if (!isSet(storage_plugins_flat[channel])) continue; @@ -59,7 +60,7 @@ async function migration_0001(storage_plugins_flat) { async function migration_0002() {} -async function migration_0003(storage_plugins_flat, storage_plugins_user, storage_misc) { +async function migration_0003(storage_iitc_code, storage_plugins_flat, storage_plugins_user, storage_misc) { if (['test', 'local'].includes(storage_misc.channel)) { storage_misc.channel = 'release'; storage_misc.network_host.custom = storage_misc.network_host.local; @@ -82,3 +83,16 @@ async function migration_0003(storage_plugins_flat, storage_plugins_user, storag } } } + +async function migration_0004(storage_iitc_code) { + for (let channel_iitc_code of Object.keys(storage_iitc_code)) { + const code = storage_iitc_code[channel_iitc_code]; + const channel = channel_iitc_code.replace('_iitc_code', ''); + delete storage_iitc_code[channel_iitc_code]; + + if (isSet(code)) { + storage_iitc_code[channel + 'iitc_core'] = parseMeta(code); + storage_iitc_code[channel + 'iitc_core']['code'] = code; + } + } +} diff --git a/src/worker.js b/src/worker.js index ea299f7..f76ab54 100644 --- a/src/worker.js +++ b/src/worker.js @@ -28,6 +28,8 @@ import { ajaxGet, clearWait, getUID, isSet, parseMeta, wait } from './helpers.js * @property {manager.progressbar} progressbar - Function for controls the display of progress bar. * @property {manager.inject_user_script} inject_user_script - Function for injecting UserScript code * into the Ingress Intel window. + * @property {manager.inject_plugin} inject_plugin - Function for injecting UserScript plugin + * into the Ingress Intel window. */ /** @@ -98,11 +100,20 @@ import { ajaxGet, clearWait, getUID, isSet, parseMeta, wait } from './helpers.js /** * Calls a function that injects UserScript code into the Ingress Intel window. * + * @deprecated since version 1.5.0. Use {@link manager.inject_plugin} instead. * @callback manager.inject_user_script * @memberOf manager * @param {string} code - UserScript code to run in the Ingress Intel window */ +/** + * Calls a function that injects UserScript plugin into the Ingress Intel window. + * + * @callback manager.inject_plugin + * @memberOf manager + * @param {plugin} plugin - UserScript plugin to run in the Ingress Intel window + */ + /** * Key-value data in storage * @@ -122,9 +133,12 @@ import { ajaxGet, clearWait, getUID, isSet, parseMeta, wait } from './helpers.js * @property {string} version * @property {string} description * @property {string} namespace - * @property {string} match - * @property {string} include - * @property {string} grant + * @property {string[]} match + * @property {string[]} include + * @property {string[]} exclude-match + * @property {string[]} exclude + * @property {string[]} require + * @property {string[]} grant */ /** @@ -146,7 +160,8 @@ export class Worker { this.storage = typeof this.config.storage !== 'undefined' ? this.config.storage : console.error("config key 'storage' is not set"); this.message = this.config.message; this.progressbar = this.config.progressbar; - this.inject_user_script = this.config.inject_user_script; + this.inject_user_script = this.config.inject_user_script || function () {}; + this.inject_plugin = this.config.inject_plugin || function () {}; this.is_initialized = false; this._init().then(); @@ -211,7 +226,7 @@ export class Worker { async _save(options) { const data = {}; Object.keys(options).forEach((key) => { - if (['iitc_version', 'last_modified', 'iitc_code', 'categories', 'plugins_flat', 'plugins_local', 'plugins_user'].indexOf(key) !== -1) { + if (['iitc_version', 'last_modified', 'iitc_core', 'categories', 'plugins_flat', 'plugins_local', 'plugins_user'].indexOf(key) !== -1) { data[this.channel + '_' + key] = options[key]; } else { data[key] = options[key]; @@ -342,8 +357,10 @@ export class Worker { const p_iitc = async () => { const iitc_code = await this._getUrl(this.network_host[this.channel] + '/total-conversion-build.user.js'); if (iitc_code) { + const iitc_core = parseMeta(iitc_code); + iitc_core['code'] = iitc_code; await this._save({ - iitc_code: iitc_code, + iitc_core: iitc_core, }); } }; diff --git a/test/manager.0.base.spec.js b/test/manager.0.base.spec.js index 23599f2..3a07708 100644 --- a/test/manager.0.base.spec.js +++ b/test/manager.0.base.spec.js @@ -19,6 +19,9 @@ describe('manage.js base integration tests', function () { inject_user_script: function callBack(data) { expect(data).to.include('// ==UserScript=='); }, + inject_plugin: function callBack(data) { + expect(data['code']).to.include('// ==UserScript=='); + }, progressbar: function callBack(is_show) { expect(is_show).to.be.oneOf([true, false]); }, diff --git a/test/manager.1.build-in.spec.js b/test/manager.1.build-in.spec.js index 7a75295..937e932 100644 --- a/test/manager.1.build-in.spec.js +++ b/test/manager.1.build-in.spec.js @@ -19,6 +19,9 @@ describe('manage.js build-in plugins integration tests', function () { inject_user_script: function callBack(data) { expect(data).to.include('// ==UserScript=='); }, + inject_plugin: function callBack(data) { + expect(data['code']).to.include('// ==UserScript=='); + }, progressbar: function callBack(is_show) { expect(is_show).to.be.oneOf([true, false]); }, diff --git a/test/manager.2.external.spec.js b/test/manager.2.external.spec.js index c4c07af..2cb0823 100644 --- a/test/manager.2.external.spec.js +++ b/test/manager.2.external.spec.js @@ -32,6 +32,9 @@ describe('manage.js external plugins integration tests', function () { inject_user_script: function callBack(data) { expect(data).to.include('// ==UserScript=='); }, + inject_plugin: function callBack(data) { + expect(data['code']).to.include('// ==UserScript=='); + }, progressbar: function callBack(is_show) { expect(is_show).to.be.oneOf([true, false]); }, diff --git a/test/manager.3.repo.spec.js b/test/manager.3.repo.spec.js index 6873696..dd82b9a 100644 --- a/test/manager.3.repo.spec.js +++ b/test/manager.3.repo.spec.js @@ -19,6 +19,9 @@ describe('manage.js custom repo integration tests', function () { inject_user_script: function callBack(data) { expect(data).to.include('// ==UserScript=='); }, + inject_plugin: function callBack(data) { + expect(data['code']).to.include('// ==UserScript=='); + }, progressbar: function callBack(is_show) { expect(is_show).to.be.oneOf([true, false]); }, @@ -53,7 +56,7 @@ describe('manage.js custom repo integration tests', function () { }); it('Check the IITC version', async function () { - const script = await storage.get(['custom_iitc_code']).then((data) => data['custom_iitc_code']); + const script = await storage.get(['custom_iitc_core']).then((data) => data['custom_iitc_core']['code']); expect(script).to.include('@version 0.99.0'); }); diff --git a/test/matching.spec.js b/test/matching.spec.js new file mode 100644 index 0000000..675d34b --- /dev/null +++ b/test/matching.spec.js @@ -0,0 +1,123 @@ +// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3 + +import { describe, it } from 'mocha'; +import { check_matching } from '../src/matching.js'; +import { expect } from 'chai'; + +describe('scheme', function () { + it('should match all', function () { + const script = { + match: ['*://*/*'], + }; + expect(check_matching(script, 'https://intel.ingress.com/'), 'should match').to.be.true; + expect(check_matching(script, 'http://example.com/'), 'should match').to.be.true; + }); + it('should match exact', function () { + const script = { + match: ['http://*/*'], + }; + expect(check_matching(script, 'https://intel.ingress.com/'), 'should not match `https`').to.be.false; + expect(check_matching(script, 'http://example.com/'), 'should match `http`').to.be.true; + expect(check_matching(script, 'https://example.com/'), 'should not match `https`').to.be.false; + }); +}); + +describe('host', function () { + it('should match domain', function () { + const script = { + match: ['*://www.example.com/'], + }; + expect(check_matching(script, 'http://www.example.com/'), 'should match').to.be.true; + expect(check_matching(script, 'http://sub.www.example.com/'), 'should not match subdomains').to.be.false; + expect(check_matching(script, 'http://www.example.net/'), 'should not match another domains').to.be.false; + }); + it('should match subdomains', function () { + const script = { + match: ['*://*.example.com/'], + }; + expect(check_matching(script, 'http://www.example.com/'), 'should match subdomains').to.be.true; + expect(check_matching(script, 'http://a.b.example.com/'), 'should match subdomains').to.be.true; + expect(check_matching(script, 'http://example.com/'), 'should match specified domain').to.be.true; + expect(check_matching(script, 'http://www.example.net/'), 'should not match another domains').to.be.false; + }); +}); + +describe('path', function () { + it('should match any', function () { + const script = { + match: ['http://www.example.com/*'], + }; + expect(check_matching(script, 'http://www.example.com/'), 'should match `/`').to.be.true; + expect(check_matching(script, 'http://www.example.com/api/'), 'should match any').to.be.true; + }); + it('should match exact', function () { + const script = { + match: ['http://www.example.com/a/b/c'], + }; + expect(check_matching(script, 'http://www.example.com/a/b/c'), 'should match exact').to.be.true; + expect(check_matching(script, 'http://www.example.com/a/b/c/d'), 'should not match').to.be.false; + }); +}); + +describe('include', function () { + it('should include any', function () { + const script = { + include: ['*'], + }; + expect(check_matching(script, 'https://www.example.com/'), 'should match `http | https`').to.be.true; + }); + it('should include by regexp', function () { + const script = { + match: ['http://www.example.com/*', 'http://www.example2.com/*'], + }; + expect(check_matching(script, 'http://www.example.com/'), 'should match `/`').to.be.true; + expect(check_matching(script, 'http://www.example2.com/data/'), 'include by prefix').to.be.true; + expect(check_matching(script, 'http://www.example3.com/'), 'should not match').to.be.false; + }); +}); + +describe('exclude', function () { + it('should exclude any', function () { + const script = { + match: ['*://*/*'], + exclude: ['*'], + }; + expect(check_matching(script, 'https://www.example.com/'), 'should exclude `http | https`').to.be.false; + }); + it('should include by regexp', function () { + const script = { + match: ['*://*/*'], + exclude: ['http://www.example.com/*', 'http://www.example2.com/*'], + }; + expect(check_matching(script, 'http://www.example.com/'), 'should exclude `/`').to.be.false; + expect(check_matching(script, 'http://www.example2.com/data/'), 'exclude by prefix').to.be.false; + expect(check_matching(script, 'http://www.example3.com/'), 'not exclude by prefix').to.be.true; + }); +}); + +describe('exclude-match', function () { + it('should exclude any', function () { + const script = { + match: ['*://*/*'], + 'exclude-match': ['*://*/*'], + }; + expect(check_matching(script, 'https://www.example.com/'), 'should exclude `http | https`').to.be.false; + }); + it('should include by regexp', function () { + const script = { + match: ['*://*/*'], + 'exclude-match': ['http://www.example.com/*', 'http://www.example2.com/*'], + }; + expect(check_matching(script, 'http://www.example.com/'), 'should exclude `/`').to.be.false; + expect(check_matching(script, 'http://www.example2.com/data/'), 'exclude by prefix').to.be.false; + expect(check_matching(script, 'http://www.example3.com/'), 'not exclude by prefix').to.be.true; + }); +}); + +describe('', function () { + it('should match all if no @match or @include rule and set url is ``', function () { + const script = {}; + expect(check_matching(script, 'https://intel.ingress.com/'), 'not match real url').to.be.false; + expect(check_matching(script, ''), 'should match keyword ``').to.be.true; + }); +}); diff --git a/test/migrations.spec.js b/test/migrations.spec.js index 2967016..c5b4b62 100644 --- a/test/migrations.spec.js +++ b/test/migrations.spec.js @@ -5,9 +5,12 @@ import { migrate, number_of_migrations } from '../src/migrations.js'; import storage from '../test/storage.js'; import { expect } from 'chai'; +const iitc_code = '// ==UserScript==\n// @author jonatkins\n// ==/UserScript=='; + describe('test migrations', function () { before(async function () { await storage.set({ + release_iitc_code: iitc_code, release_plugins_flat: { 'Bookmarks for maps and portals+https://github.com/IITC-CE/ingress-intel-total-conversion': { id: 'bookmarks', @@ -53,5 +56,11 @@ describe('test migrations', function () { const db_data = await storage.get(['storage_version']); expect(db_data['storage_version']).to.equal(number_of_migrations()); }); + it('Should create `iitc_core` object', async function () { + const db_data = await storage.get(['release_iitc_core']); + expect(db_data['release_iitc_core']).to.be.an('object'); + expect(db_data['release_iitc_core']['author']).to.equal('jonatkins'); + expect(db_data['release_iitc_core']['code']).to.to.include('jonatkins'); + }); }); });