sourcemonkey 0.0.0-stage → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +87 -2
- package/dist/bytes.js +37 -0
- package/dist/channel-placeholder.js +5 -0
- package/dist/collection-scan.js +17 -0
- package/dist/collections.html +353 -0
- package/dist/compare.html +358 -0
- package/dist/composed-source.js +205 -0
- package/dist/connect-prompt.html +47 -0
- package/dist/debug/icons.html +211 -0
- package/dist/debug/storage.html +163 -0
- package/dist/debug-log.js +75 -0
- package/dist/diagnostic-location.js +64 -0
- package/dist/eval-hook.js +308 -0
- package/dist/extension/background.js +5263 -0
- package/dist/extension/channel.js +133 -0
- package/dist/extension/code-links.js +187 -0
- package/dist/extension/code-tabs.js +460 -0
- package/dist/extension/collection-dialog.js +619 -0
- package/dist/extension/collection-display.js +322 -0
- package/dist/extension/collections.js +2905 -0
- package/dist/extension/compare.js +867 -0
- package/dist/extension/concurrency.js +186 -0
- package/dist/extension/connect-grants.js +109 -0
- package/dist/extension/connect-prompt-page.js +70 -0
- package/dist/extension/connect-prompt.js +266 -0
- package/dist/extension/copy-button.js +63 -0
- package/dist/extension/copy-dialog.js +502 -0
- package/dist/extension/copy-preference.js +66 -0
- package/dist/extension/debug/icons.js +203 -0
- package/dist/extension/debug/storage.js +365 -0
- package/dist/extension/deep-link.js +77 -0
- package/dist/extension/delete-dialog.js +432 -0
- package/dist/extension/delete-icon.js +40 -0
- package/dist/extension/details-tooltips.js +66 -0
- package/dist/extension/directories.js +679 -0
- package/dist/extension/export-backup.js +407 -0
- package/dist/extension/export-dialog.js +142 -0
- package/dist/extension/file-access.js +74 -0
- package/dist/extension/file-changes.js +346 -0
- package/dist/extension/frame-info.js +85 -0
- package/dist/extension/github-provider.js +336 -0
- package/dist/extension/glyph.js +84 -0
- package/dist/extension/gm-bindings.js +601 -0
- package/dist/extension/gm-download.js +419 -0
- package/dist/extension/gm-menu.js +228 -0
- package/dist/extension/gm-notification.js +141 -0
- package/dist/extension/gm-originator-map.js +23 -0
- package/dist/extension/gm-tab-data.js +121 -0
- package/dist/extension/gm-tab.js +160 -0
- package/dist/extension/gm-url-change.js +111 -0
- package/dist/extension/gm-url-scheme.js +27 -0
- package/dist/extension/gm-values.js +176 -0
- package/dist/extension/gm-window-control.js +104 -0
- package/dist/extension/gm-xhr-headers.js +333 -0
- package/dist/extension/gm-xhr.js +584 -0
- package/dist/extension/injector.js +3145 -0
- package/dist/extension/install-capture.js +277 -0
- package/dist/extension/line-diff.js +233 -0
- package/dist/extension/local-copy.js +560 -0
- package/dist/extension/local-delete.js +276 -0
- package/dist/extension/location-kinds.js +108 -0
- package/dist/extension/log-format.js +158 -0
- package/dist/extension/managed-changes.js +361 -0
- package/dist/extension/managed-items.js +57 -0
- package/dist/extension/match-index.js +375 -0
- package/dist/extension/missing-scripts.js +66 -0
- package/dist/extension/more-menu.js +109 -0
- package/dist/extension/move-dialog.js +197 -0
- package/dist/extension/offscreen.js +37 -0
- package/dist/extension/options.js +504 -0
- package/dist/extension/page-heading.js +170 -0
- package/dist/extension/page-links.js +199 -0
- package/dist/extension/parse-check-client.js +70 -0
- package/dist/extension/parse-check.js +161 -0
- package/dist/extension/parse-sandbox-host.js +172 -0
- package/dist/extension/parse-sandbox.js +175 -0
- package/dist/extension/popup-commands-open.js +41 -0
- package/dist/extension/popup-view.js +387 -0
- package/dist/extension/popup.js +703 -0
- package/dist/extension/prism-userscript.js +53 -0
- package/dist/extension/refresh-error.js +136 -0
- package/dist/extension/refresh-targets.js +160 -0
- package/dist/extension/related-versions.js +1144 -0
- package/dist/extension/resize-observer-loop.js +19 -0
- package/dist/extension/row-cache.js +387 -0
- package/dist/extension/script-blobs.js +413 -0
- package/dist/extension/script-details.js +658 -0
- package/dist/extension/script-enablement.js +285 -0
- package/dist/extension/script-identity.js +310 -0
- package/dist/extension/script-keys.js +54 -0
- package/dist/extension/script-log-id.js +155 -0
- package/dist/extension/script-log.js +290 -0
- package/dist/extension/script-rejection.js +104 -0
- package/dist/extension/script-sites.js +158 -0
- package/dist/extension/script-store.js +3507 -0
- package/dist/extension/script-updater.js +991 -0
- package/dist/extension/script-url-hash.js +61 -0
- package/dist/extension/select-all.js +37 -0
- package/dist/extension/select-open-guard.js +90 -0
- package/dist/extension/site-icon.js +1261 -0
- package/dist/extension/sm-dev-link.js +302 -0
- package/dist/extension/solo-click.js +58 -0
- package/dist/extension/sticky-numbers.js +188 -0
- package/dist/extension/storage-breakdown.js +253 -0
- package/dist/extension/storage-scan.js +86 -0
- package/dist/extension/storage-usage.js +194 -0
- package/dist/extension/structural-keys.js +29 -0
- package/dist/extension/sw-teardown.js +36 -0
- package/dist/extension/tab-status.js +403 -0
- package/dist/extension/toolbar-summary.js +105 -0
- package/dist/extension/user-scripts-access.js +43 -0
- package/dist/extension/userscript.js +3672 -0
- package/dist/extension/value-store.js +204 -0
- package/dist/extension/version-probe-fetch.js +68 -0
- package/dist/extension/worker-client.js +109 -0
- package/dist/extension/worker-ops.js +171 -0
- package/dist/extension/zip-writer.js +24 -0
- package/dist/external-files.js +878 -0
- package/dist/file-loader.js +436 -0
- package/dist/file-locations.js +439 -0
- package/dist/generated-wrappers.js +5 -0
- package/dist/github-auth.js +148 -0
- package/dist/github-urls.js +46 -0
- package/dist/gm-api-catalog.js +217 -0
- package/dist/gm-connect.js +273 -0
- package/dist/gm-xhr-types.js +5 -0
- package/dist/http-read.js +142 -0
- package/dist/icons/icon-128.png +0 -0
- package/dist/icons/icon-16.png +0 -0
- package/dist/icons/icon-48.png +0 -0
- package/dist/local-file-overrides.js +558 -0
- package/dist/manifest.json +38 -0
- package/dist/markup-response.js +126 -0
- package/dist/match-targets.js +305 -0
- package/dist/menu-captions.js +22 -0
- package/dist/offscreen.html +39 -0
- package/dist/options.html +197 -0
- package/dist/parse-check-protocol.js +51 -0
- package/dist/parse-location.js +171 -0
- package/dist/parse-sandbox.html +56 -0
- package/dist/phase-timing.js +155 -0
- package/dist/popup.html +197 -0
- package/dist/public-suffixes.js +6957 -0
- package/dist/run-context.js +31 -0
- package/dist/runtime-messages.js +34 -0
- package/dist/sandboxed-page.js +35 -0
- package/dist/script-build.js +787 -0
- package/dist/script-display.js +383 -0
- package/dist/script-icon.js +43 -0
- package/dist/script-links.js +204 -0
- package/dist/script-source-url.js +214 -0
- package/dist/script-world.js +65 -0
- package/dist/sm-dev-protocol.js +84 -0
- package/dist/source-map.js +177 -0
- package/dist/stack-link.js +138 -0
- package/dist/styles.css +825 -0
- package/dist/unsafe-window-use.js +229 -0
- package/dist/url-match.js +734 -0
- package/dist/userscript-header.js +248 -0
- package/dist/userscript.html +1165 -0
- package/dist/vendor/fflate.LICENSE +21 -0
- package/dist/vendor/fflate.js +2692 -0
- package/dist/vendor/prism/prism-line-numbers.css +1 -0
- package/dist/vendor/prism/prism-line-numbers.js +1 -0
- package/dist/vendor/prism/prism.css +1 -0
- package/dist/vendor/prism/prism.js +1946 -0
- package/dist/version-compare.js +141 -0
- package/dist/version-probe.js +120 -0
- package/lib/build-info.json +1 -0
- package/lib/bytes.d.ts +3 -0
- package/lib/bytes.js +37 -0
- package/lib/channel-placeholder.d.ts +1 -0
- package/lib/channel-placeholder.js +5 -0
- package/lib/collection-scan.d.ts +8 -0
- package/lib/collection-scan.js +17 -0
- package/lib/composed-source.d.ts +28 -0
- package/lib/composed-source.js +205 -0
- package/lib/debug-log.d.ts +9 -0
- package/lib/debug-log.js +75 -0
- package/lib/diagnostic-location.d.ts +8 -0
- package/lib/diagnostic-location.js +64 -0
- package/lib/eval-hook.d.ts +34 -0
- package/lib/eval-hook.js +308 -0
- package/lib/external-files.d.ts +111 -0
- package/lib/external-files.js +878 -0
- package/lib/file-loader.d.ts +48 -0
- package/lib/file-loader.js +436 -0
- package/lib/file-locations.d.ts +44 -0
- package/lib/file-locations.js +439 -0
- package/lib/generated-wrappers.d.ts +11 -0
- package/lib/generated-wrappers.js +5 -0
- package/lib/github-auth.d.ts +12 -0
- package/lib/github-auth.js +148 -0
- package/lib/github-urls.d.ts +1 -0
- package/lib/github-urls.js +46 -0
- package/lib/gm-api-catalog.d.ts +24 -0
- package/lib/gm-api-catalog.js +217 -0
- package/lib/gm-connect.d.ts +34 -0
- package/lib/gm-connect.js +273 -0
- package/lib/gm-xhr-types.d.ts +68 -0
- package/lib/gm-xhr-types.js +5 -0
- package/lib/harness/browser-extensions.d.ts +73 -0
- package/lib/harness/browser-extensions.js +285 -0
- package/lib/harness/browser-lang.d.ts +20 -0
- package/lib/harness/browser-lang.js +86 -0
- package/lib/harness/browser-launch.d.ts +14 -0
- package/lib/harness/browser-launch.js +184 -0
- package/lib/harness/checks.d.ts +110 -0
- package/lib/harness/checks.js +321 -0
- package/lib/harness/cli.d.ts +2 -0
- package/lib/harness/cli.js +2259 -0
- package/lib/harness/command-args.d.ts +91 -0
- package/lib/harness/command-args.js +369 -0
- package/lib/harness/command-lines.d.ts +80 -0
- package/lib/harness/command-lines.js +180 -0
- package/lib/harness/command-list.d.ts +206 -0
- package/lib/harness/command-list.js +1475 -0
- package/lib/harness/console-commands.d.ts +463 -0
- package/lib/harness/console-commands.js +3216 -0
- package/lib/harness/download-watch.d.ts +21 -0
- package/lib/harness/download-watch.js +156 -0
- package/lib/harness/extension-backend.d.ts +41 -0
- package/lib/harness/extension-backend.js +100 -0
- package/lib/harness/extension-host.d.ts +112 -0
- package/lib/harness/extension-host.js +552 -0
- package/lib/harness/extension-launch.d.ts +67 -0
- package/lib/harness/extension-launch.js +285 -0
- package/lib/harness/extension-link.d.ts +315 -0
- package/lib/harness/extension-link.js +607 -0
- package/lib/harness/external-manager.d.ts +146 -0
- package/lib/harness/external-manager.js +414 -0
- package/lib/harness/fixture.d.ts +165 -0
- package/lib/harness/fixture.js +461 -0
- package/lib/harness/frame-sessions.d.ts +71 -0
- package/lib/harness/frame-sessions.js +248 -0
- package/lib/harness/frames.d.ts +68 -0
- package/lib/harness/frames.js +216 -0
- package/lib/harness/help-format.d.ts +17 -0
- package/lib/harness/help-format.js +127 -0
- package/lib/harness/host.d.ts +352 -0
- package/lib/harness/host.js +423 -0
- package/lib/harness/index.d.ts +8 -0
- package/lib/harness/index.js +10 -0
- package/lib/harness/input-file.d.ts +18 -0
- package/lib/harness/input-file.js +61 -0
- package/lib/harness/json-output.d.ts +565 -0
- package/lib/harness/json-output.js +71 -0
- package/lib/harness/manager-actions.d.ts +21 -0
- package/lib/harness/manager-actions.js +129 -0
- package/lib/harness/manager-commands.d.ts +142 -0
- package/lib/harness/manager-commands.js +1196 -0
- package/lib/harness/manager-fetch.d.ts +33 -0
- package/lib/harness/manager-fetch.js +239 -0
- package/lib/harness/managers.d.ts +24 -0
- package/lib/harness/managers.js +27 -0
- package/lib/harness/output-style.d.ts +125 -0
- package/lib/harness/output-style.js +322 -0
- package/lib/harness/page-commands.d.ts +134 -0
- package/lib/harness/page-commands.js +1141 -0
- package/lib/harness/page-output.d.ts +67 -0
- package/lib/harness/page-output.js +139 -0
- package/lib/harness/page-watch.d.ts +171 -0
- package/lib/harness/page-watch.js +607 -0
- package/lib/harness/parse-check-node.d.ts +3 -0
- package/lib/harness/parse-check-node.js +132 -0
- package/lib/harness/path-args.d.ts +22 -0
- package/lib/harness/path-args.js +148 -0
- package/lib/harness/playwright-chromium.d.ts +16 -0
- package/lib/harness/playwright-chromium.js +86 -0
- package/lib/harness/prepare.d.ts +168 -0
- package/lib/harness/prepare.js +364 -0
- package/lib/harness/prompt-editor.d.ts +17 -0
- package/lib/harness/prompt-editor.js +354 -0
- package/lib/harness/prompt-history.d.ts +13 -0
- package/lib/harness/prompt-history.js +89 -0
- package/lib/harness/rebuild-advice.d.ts +6 -0
- package/lib/harness/rebuild-advice.js +39 -0
- package/lib/harness/run-log.d.ts +47 -0
- package/lib/harness/run-log.js +140 -0
- package/lib/harness/run-logs.d.ts +132 -0
- package/lib/harness/run-logs.js +241 -0
- package/lib/harness/run.d.ts +194 -0
- package/lib/harness/run.js +1973 -0
- package/lib/harness/runs.d.ts +229 -0
- package/lib/harness/runs.js +726 -0
- package/lib/harness/save-html.d.ts +39 -0
- package/lib/harness/save-html.js +89 -0
- package/lib/harness/saved-pages.d.ts +120 -0
- package/lib/harness/saved-pages.js +275 -0
- package/lib/harness/script-mark.d.ts +17 -0
- package/lib/harness/script-mark.js +56 -0
- package/lib/harness/snapshot.d.ts +143 -0
- package/lib/harness/snapshot.js +474 -0
- package/lib/harness/stuck-tabs.d.ts +19 -0
- package/lib/harness/stuck-tabs.js +131 -0
- package/lib/harness/tab-refs.d.ts +20 -0
- package/lib/harness/tab-refs.js +80 -0
- package/lib/harness/tampermonkey.d.ts +71 -0
- package/lib/harness/tampermonkey.js +356 -0
- package/lib/harness/tm-ids.d.ts +18 -0
- package/lib/harness/tm-ids.js +88 -0
- package/lib/harness/violentmonkey.d.ts +34 -0
- package/lib/harness/violentmonkey.js +162 -0
- package/lib/harness/wait.d.ts +77 -0
- package/lib/harness/wait.js +458 -0
- package/lib/http-read.d.ts +30 -0
- package/lib/http-read.js +142 -0
- package/lib/local-file-overrides.d.ts +35 -0
- package/lib/local-file-overrides.js +558 -0
- package/lib/manifest.json +38 -0
- package/lib/markup-response.d.ts +11 -0
- package/lib/markup-response.js +126 -0
- package/lib/match-targets.d.ts +18 -0
- package/lib/match-targets.js +305 -0
- package/lib/menu-captions.d.ts +1 -0
- package/lib/menu-captions.js +22 -0
- package/lib/parse-check-protocol.d.ts +27 -0
- package/lib/parse-check-protocol.js +51 -0
- package/lib/parse-location.d.ts +12 -0
- package/lib/parse-location.js +171 -0
- package/lib/phase-timing.d.ts +12 -0
- package/lib/phase-timing.js +155 -0
- package/lib/public-suffixes.d.ts +1 -0
- package/lib/public-suffixes.js +6957 -0
- package/lib/run-context.d.ts +2 -0
- package/lib/run-context.js +31 -0
- package/lib/runtime-messages.d.ts +10 -0
- package/lib/runtime-messages.js +34 -0
- package/lib/sandboxed-page.d.ts +5 -0
- package/lib/sandboxed-page.js +35 -0
- package/lib/script-build.d.ts +111 -0
- package/lib/script-build.js +787 -0
- package/lib/script-display.d.ts +51 -0
- package/lib/script-display.js +383 -0
- package/lib/script-icon.d.ts +16 -0
- package/lib/script-icon.js +43 -0
- package/lib/script-links.d.ts +24 -0
- package/lib/script-links.js +204 -0
- package/lib/script-source-url.d.ts +8 -0
- package/lib/script-source-url.js +214 -0
- package/lib/script-world.d.ts +6 -0
- package/lib/script-world.js +65 -0
- package/lib/sm-dev-protocol.d.ts +251 -0
- package/lib/sm-dev-protocol.js +84 -0
- package/lib/source-map.d.ts +32 -0
- package/lib/source-map.js +177 -0
- package/lib/stack-link.d.ts +15 -0
- package/lib/stack-link.js +138 -0
- package/lib/unsafe-window-use.d.ts +1 -0
- package/lib/unsafe-window-use.js +229 -0
- package/lib/url-match.d.ts +40 -0
- package/lib/url-match.js +734 -0
- package/lib/userscript-header.d.ts +33 -0
- package/lib/userscript-header.js +248 -0
- package/lib/version-compare.d.ts +6 -0
- package/lib/version-compare.js +141 -0
- package/lib/version-probe.d.ts +17 -0
- package/lib/version-probe.js +120 -0
- package/package.json +55 -3
- package/skills/SourceMonkey-dev/SKILL.md +868 -0
- package/skills/SourceMonkey-dev/references/cdp.md +293 -0
- package/skills/SourceMonkey-dev/references/commands.md +297 -0
- package/skills/SourceMonkey-dev/references/other-managers.md +63 -0
- package/skills/SourceMonkey-dev/references/specs.md +46 -0
- package/skills/SourceMonkey-friction/SKILL.md +103 -0
- package/skills/SourceMonkey-friction/references/template.md +59 -0
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
// URL matching for userscripts.
|
|
2
|
+
//
|
|
3
|
+
// Implements Tampermonkey-style three flavours of pattern:
|
|
4
|
+
//
|
|
5
|
+
// @match Chrome match-pattern grammar: <scheme>://<host>/<path>
|
|
6
|
+
// - scheme: *, http, https, file, ftp
|
|
7
|
+
// - host: *, *.domain, exact domain, or empty (for file://)
|
|
8
|
+
// - path: glob with `*` (matches any chars including `/`)
|
|
9
|
+
// Plus the special literal `<all_urls>` (any http/https/file/ftp URL).
|
|
10
|
+
// Some shapes Chrome's parser refuses are read leniently
|
|
11
|
+
// first — see `canonicalMatchPatterns`.
|
|
12
|
+
//
|
|
13
|
+
// @include Free-form glob over the *whole* URL string. `*` matches any
|
|
14
|
+
// run of characters. If the pattern is wrapped in `/.../` it
|
|
15
|
+
// is treated as a JavaScript regex.
|
|
16
|
+
//
|
|
17
|
+
// @exclude Same syntax as @include. If any @exclude matches, the
|
|
18
|
+
// script does not match — excludes win over both @match and
|
|
19
|
+
// @include.
|
|
20
|
+
//
|
|
21
|
+
// All three also take Tampermonkey's `.tld` host suffix, meaning "any
|
|
22
|
+
// public suffix" — see `.tld` hosts below.
|
|
23
|
+
//
|
|
24
|
+
// `urlMatchesScript(url, matching)` is the entry point used by the
|
|
25
|
+
// background SW; the per-pattern helpers are exported for tests.
|
|
26
|
+
import { ICANN_SUFFIX_RULES } from './public-suffixes.js';
|
|
27
|
+
const HTTP_LIKE_SCHEMES = /^(https?|file|ftp):/i;
|
|
28
|
+
// Escape every regex metacharacter except `*`, which we want to keep so
|
|
29
|
+
// the caller can substitute it in. Used by both pattern flavours.
|
|
30
|
+
function escapeForRegexExceptStar(s) {
|
|
31
|
+
return s.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
32
|
+
}
|
|
33
|
+
// Convert a `*`-glob (where `*` matches any chars, including `/`) to an
|
|
34
|
+
// anchored RegExp.
|
|
35
|
+
function globToRegExp(glob, flags = '') {
|
|
36
|
+
const body = escapeForRegexExceptStar(glob).replace(/\*/g, '.*');
|
|
37
|
+
return new RegExp('^' + body + '$', flags);
|
|
38
|
+
}
|
|
39
|
+
// ---------- `.tld` hosts ----------
|
|
40
|
+
//
|
|
41
|
+
// Tampermonkey and Violentmonkey read a host ending in `.tld` as "this
|
|
42
|
+
// name under any public suffix": `*://www.google.tld/*` runs on
|
|
43
|
+
// www.google.com, www.google.de and www.google.co.uk. They take it in
|
|
44
|
+
// `@match`, `@include` and `@exclude` alike.
|
|
45
|
+
//
|
|
46
|
+
// A public suffix is one under which anyone can register a name. We
|
|
47
|
+
// take them from the ICANN section of the Public Suffix List (PSL,
|
|
48
|
+
// publicsuffix.org), generated into public-suffixes.ts:
|
|
49
|
+
// - That covers multi-level ones (`co.uk`, `com.au`) where a
|
|
50
|
+
// registry keeps the second level, and not `com.de`, an ordinary
|
|
51
|
+
// name someone owns.
|
|
52
|
+
// - The PSL's private section (`blogspot.com`, `github.io`) is left
|
|
53
|
+
// out: `www.google.blogspot.com` belongs to whoever registered it
|
|
54
|
+
// there, not to Google. (Violentmonkey includes it.)
|
|
55
|
+
// - So are names no registry hands out (`.local`, `.lan`): the PSL's
|
|
56
|
+
// implicit "any unknown label" rule isn't applied.
|
|
57
|
+
//
|
|
58
|
+
// Matching tries each public suffix the URL's own host ends in, with
|
|
59
|
+
// `.tld` replaced by it, as an ordinary pattern (see `tldSuffixesOf`).
|
|
60
|
+
//
|
|
61
|
+
// The list stays out of the page: the wrapper's in-page matcher reads
|
|
62
|
+
// `.tld` loosely, and the service worker's start check (the reply to
|
|
63
|
+
// `sm:check-pending`) applies this exact reading. See docs/matching.md.
|
|
64
|
+
// PSL rules, parsed on first use: plain suffixes, `*.x` wildcards
|
|
65
|
+
// (every label under x is a suffix), and `!y` exceptions to them.
|
|
66
|
+
let suffixRules = null;
|
|
67
|
+
function rules() {
|
|
68
|
+
if (suffixRules)
|
|
69
|
+
return suffixRules;
|
|
70
|
+
const r = { plain: new Set(), wild: new Set(), except: new Set() };
|
|
71
|
+
for (const line of ICANN_SUFFIX_RULES.split('\n')) {
|
|
72
|
+
if (!line)
|
|
73
|
+
continue;
|
|
74
|
+
if (line.startsWith('!'))
|
|
75
|
+
r.except.add(line.slice(1));
|
|
76
|
+
else if (line.startsWith('*.'))
|
|
77
|
+
r.wild.add(line.slice(2));
|
|
78
|
+
else
|
|
79
|
+
r.plain.add(line);
|
|
80
|
+
}
|
|
81
|
+
suffixRules = r;
|
|
82
|
+
return r;
|
|
83
|
+
}
|
|
84
|
+
// Is this (lowercase, punycode) name exactly an ICANN public suffix?
|
|
85
|
+
export function isPublicSuffix(name) {
|
|
86
|
+
const r = rules();
|
|
87
|
+
if (r.except.has(name))
|
|
88
|
+
return false;
|
|
89
|
+
if (r.plain.has(name))
|
|
90
|
+
return true;
|
|
91
|
+
const dot = name.indexOf('.');
|
|
92
|
+
return dot > 0 && r.wild.has(name.slice(dot + 1));
|
|
93
|
+
}
|
|
94
|
+
// The public suffixes a URL's host ends in, shortest first: for
|
|
95
|
+
// `www.google.co.uk`, `uk` and `co.uk`. Never the whole host, since a
|
|
96
|
+
// `.tld` pattern always names something in front of it.
|
|
97
|
+
function tldSuffixesOf(url) {
|
|
98
|
+
let hostname;
|
|
99
|
+
try {
|
|
100
|
+
hostname = new URL(url).hostname.toLowerCase();
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
const labels = hostname.split('.');
|
|
106
|
+
const out = [];
|
|
107
|
+
for (let k = 1; k < labels.length; k++) {
|
|
108
|
+
const suffix = labels.slice(-k).join('.');
|
|
109
|
+
if (isPublicSuffix(suffix))
|
|
110
|
+
out.push(suffix);
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
// A `.tld` pattern tested as each of its readings for this URL, with
|
|
115
|
+
// `.tld` replaced by one of the host's public suffixes. Null when the
|
|
116
|
+
// pattern has no `.tld` host, for the caller to test it as it is.
|
|
117
|
+
function testTld(pattern, url, globStar, test) {
|
|
118
|
+
const at = tldIndex(pattern, globStar);
|
|
119
|
+
if (at < 0)
|
|
120
|
+
return null;
|
|
121
|
+
return tldSuffixesOf(url).some((suffix) => test(pattern.slice(0, at) + '.' + suffix + pattern.slice(at + 4)));
|
|
122
|
+
}
|
|
123
|
+
// Where the `.tld` that ends a pattern's host starts, or -1 when its
|
|
124
|
+
// host doesn't end in one. The host is what follows `://` (the start,
|
|
125
|
+
// for a pattern naming no scheme) up to the next `/`, less a `:port`.
|
|
126
|
+
// Only a host's LAST label counts: `.tld` elsewhere is literal text.
|
|
127
|
+
//
|
|
128
|
+
// `globStar`: an `@include` glob may run the host straight into a `*`
|
|
129
|
+
// with no `/` (`http://www.google.tld*`), as older scripts do. The
|
|
130
|
+
// `.tld` still ends the host, and the `*` stays a glob star. Not for a
|
|
131
|
+
// `@match`, whose grammar has no `*` after the host.
|
|
132
|
+
//
|
|
133
|
+
// A `.tld` with no name in front (`*.tld`, a bare `.tld`) stays
|
|
134
|
+
// literal: "any host under any TLD" is not what anybody means by it.
|
|
135
|
+
function tldIndex(pattern, globStar = false) {
|
|
136
|
+
const sep = pattern.indexOf('://');
|
|
137
|
+
const start = sep < 0 ? 0 : sep + 3;
|
|
138
|
+
const slash = pattern.indexOf('/', start);
|
|
139
|
+
let host = pattern.slice(start, slash < 0 ? pattern.length : slash).replace(/:(\d+|\*)$/, '');
|
|
140
|
+
if (globStar && host.endsWith('*'))
|
|
141
|
+
host = host.slice(0, -1);
|
|
142
|
+
if (!host.toLowerCase().endsWith('.tld'))
|
|
143
|
+
return -1;
|
|
144
|
+
const base = host.slice(0, -4);
|
|
145
|
+
if (base === '' || base === '*')
|
|
146
|
+
return -1;
|
|
147
|
+
return start + host.length - 4;
|
|
148
|
+
}
|
|
149
|
+
// Does this glob or `@match` pattern have a `.tld` host — one Chrome
|
|
150
|
+
// can't match for us? Regex patterns never do: `.tld` in a regex is
|
|
151
|
+
// the author's own regex text.
|
|
152
|
+
export function hostEndsInTld(pattern) {
|
|
153
|
+
if (isRegexPattern(pattern))
|
|
154
|
+
return false;
|
|
155
|
+
if (pattern === '<all_urls>')
|
|
156
|
+
return false;
|
|
157
|
+
return tldIndex(pattern, true) >= 0;
|
|
158
|
+
}
|
|
159
|
+
// A glob Chrome can register for a `.tld` `@match` or `@include`, as a
|
|
160
|
+
// coarse first pass: the `.tld` becomes `.*`, so `*://www.google.tld/*`
|
|
161
|
+
// registers as `*://www.google.*/*`. It reaches more than `.tld` means
|
|
162
|
+
// (`www.google.evil.com`, and a glob `*` also crosses `/`), so the
|
|
163
|
+
// wrapper's in-page matcher and the worker's `.tld` check must still
|
|
164
|
+
// narrow it. A `@match` host's leading `*.` also covers the bare name
|
|
165
|
+
// (`*.imdb.tld` runs on `imdb.com`), which as a glob needs `*imdb.*`.
|
|
166
|
+
// Only for patterns where `hostEndsInTld` is true.
|
|
167
|
+
export function tldCoarseGlob(pattern, isMatch) {
|
|
168
|
+
const at = tldIndex(pattern, !isMatch);
|
|
169
|
+
let head = pattern.slice(0, at);
|
|
170
|
+
if (isMatch) {
|
|
171
|
+
const sep = head.indexOf('://');
|
|
172
|
+
if (sep >= 0 && head.startsWith('*.', sep + 3))
|
|
173
|
+
head = head.slice(0, sep + 3) + '*' + head.slice(sep + 5);
|
|
174
|
+
}
|
|
175
|
+
return head + '.*' + pattern.slice(at + 4);
|
|
176
|
+
}
|
|
177
|
+
function isRegexPattern(pattern) {
|
|
178
|
+
return pattern.length >= 2 && pattern.startsWith('/') && pattern.endsWith('/');
|
|
179
|
+
}
|
|
180
|
+
// ---------- lenient readings ----------
|
|
181
|
+
//
|
|
182
|
+
// The concrete schemes a `@match` may name — `MATCH_SCHEMES` minus the
|
|
183
|
+
// `*` that stands for http-or-https. Also what a scheme glob is
|
|
184
|
+
// expanded against.
|
|
185
|
+
const CONCRETE_SCHEMES = ['http', 'https', 'file', 'ftp'];
|
|
186
|
+
// Rewrite the `@match` shapes Chrome's parser refuses but the other
|
|
187
|
+
// managers run, into the patterns Chrome's grammar spells them with.
|
|
188
|
+
// Returns one pattern per concrete scheme the original names — usually
|
|
189
|
+
// one, and the input verbatim when there is nothing to rewrite.
|
|
190
|
+
//
|
|
191
|
+
// *.example.com/* → *://*.example.com/* (scheme omitted)
|
|
192
|
+
// http*://example.com/* → *://example.com/* (scheme glob)
|
|
193
|
+
// *s://example.com/* → https://example.com/*
|
|
194
|
+
// https://example.com → https://example.com/ (no path)
|
|
195
|
+
// *://*example.com/* → *://*.example.com/* (`*` stuck to a label)
|
|
196
|
+
//
|
|
197
|
+
// The last two are host-and-path readings, in `readHostAndPath`.
|
|
198
|
+
//
|
|
199
|
+
// Why: an omitted scheme and `http*` together account for nearly every
|
|
200
|
+
// `@match` in the Greasy Fork corpus that Chrome refuses — the rewrite
|
|
201
|
+
// turns 877 of those 1,035 patterns into ones it takes, across the
|
|
202
|
+
// 1,991 most-installed scripts, which are working scripts under the
|
|
203
|
+
// managers their authors use (see docs/matching.md). Both shapes have
|
|
204
|
+
// one obvious reading; Chrome's parser is the only thing that can't
|
|
205
|
+
// spell it, so we spell it for it.
|
|
206
|
+
//
|
|
207
|
+
// A scheme glob is ENUMERATED, not approximated: `*` comes back only
|
|
208
|
+
// when the glob names exactly the pair `*` stands for, and every other
|
|
209
|
+
// expansion comes back as concrete schemes (`*s://` → `https://`). So
|
|
210
|
+
// no caller has to re-check the scheme afterwards — a rounded-up `*`
|
|
211
|
+
// would have to be filtered somewhere, and Chrome, which does the
|
|
212
|
+
// matching, has nowhere to filter before injecting.
|
|
213
|
+
//
|
|
214
|
+
// A scheme glob matching no supported scheme (`chrome-*://`) is left
|
|
215
|
+
// alone, so `matchPatternProblem` still names it as unsupported rather
|
|
216
|
+
// than silently widening it.
|
|
217
|
+
//
|
|
218
|
+
// This runs ahead of BOTH the matcher and the validator, so what we
|
|
219
|
+
// register with Chrome and what our own matcher says stay the same
|
|
220
|
+
// thing, and `pageMatcher` builds the wrapper's regexes from it too.
|
|
221
|
+
export function canonicalMatchPatterns(pattern) {
|
|
222
|
+
if (pattern === '<all_urls>')
|
|
223
|
+
return [pattern];
|
|
224
|
+
// A broken scheme (`//host/…`, `*//host/…`, `://host/…`) is left as
|
|
225
|
+
// written, for `matchPatternProblem` to flag. Supplying `*://` would
|
|
226
|
+
// turn `*//host/…` into "any host, path beginning `//host/`".
|
|
227
|
+
if (BROKEN_SCHEME.test(pattern))
|
|
228
|
+
return [pattern];
|
|
229
|
+
// Supply the scheme the author left out.
|
|
230
|
+
let p = pattern;
|
|
231
|
+
if (!p.includes('://'))
|
|
232
|
+
p = '*://' + p;
|
|
233
|
+
p = readHostAndPath(p);
|
|
234
|
+
const sep = p.indexOf('://');
|
|
235
|
+
const scheme = p.slice(0, sep).toLowerCase();
|
|
236
|
+
const rest = p.slice(sep); // keeps the `://`
|
|
237
|
+
if (scheme === '*')
|
|
238
|
+
return anyScheme(rest);
|
|
239
|
+
if (!scheme.includes('*'))
|
|
240
|
+
return [p];
|
|
241
|
+
let schemeRx;
|
|
242
|
+
try {
|
|
243
|
+
schemeRx = globToRegExp(scheme);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return [pattern];
|
|
247
|
+
}
|
|
248
|
+
const expanded = CONCRETE_SCHEMES.filter((s) => schemeRx.test(s));
|
|
249
|
+
if (expanded.length === 0)
|
|
250
|
+
return [pattern];
|
|
251
|
+
// Exactly the pair `*` already means, so say it the way `*` is said.
|
|
252
|
+
if (expanded.join() === 'http,https')
|
|
253
|
+
return anyScheme(rest);
|
|
254
|
+
return expanded.map((s) => s + rest);
|
|
255
|
+
}
|
|
256
|
+
// A pattern starting with `//`, `*//` or `:`: slashes or a colon with
|
|
257
|
+
// no scheme in front of them. Too odd to guess at; see
|
|
258
|
+
// docs/match-rules.md.
|
|
259
|
+
const BROKEN_SCHEME = /^(\*?\/\/|:)/;
|
|
260
|
+
// The host-and-path half of the lenient readings, for a pattern that
|
|
261
|
+
// has its `://`:
|
|
262
|
+
// - No path reads as the root page, `/`, as Tampermonkey reads it.
|
|
263
|
+
// Violentmonkey never runs one, so this takes nothing away there.
|
|
264
|
+
// - A `*` stuck to the front of a host (`*example.com`) reads as
|
|
265
|
+
// `*.example.com`: the name and its subdomains, which is what such
|
|
266
|
+
// authors mean. Tampermonkey runs it on `example.com` alone, and
|
|
267
|
+
// Violentmonkey as a glob that also takes `fooexample.com`; this
|
|
268
|
+
// covers the first and the sensible part of the second.
|
|
269
|
+
// Any other `*` inside a host is left for `matchPatternProblem`.
|
|
270
|
+
function readHostAndPath(p) {
|
|
271
|
+
const start = p.indexOf('://') + 3;
|
|
272
|
+
const slash = p.indexOf('/', start);
|
|
273
|
+
let host = slash < 0 ? p.slice(start) : p.slice(start, slash);
|
|
274
|
+
const path = slash < 0 ? '/' : p.slice(slash);
|
|
275
|
+
// The port's own `*` (`:*`) doesn't count as a second one.
|
|
276
|
+
const bareHost = host.replace(/:(\d+|\*)$/, '');
|
|
277
|
+
if (/^\*[^.*:]/.test(bareHost) && !bareHost.includes('*', 1))
|
|
278
|
+
host = '*.' + host.slice(1);
|
|
279
|
+
return p.slice(0, start) + host + path;
|
|
280
|
+
}
|
|
281
|
+
// `*://<rest>` — http-or-https — spelled the way Chrome will take it.
|
|
282
|
+
//
|
|
283
|
+
// Chrome's parser refuses `*` beside a numeric port ("Invalid port"),
|
|
284
|
+
// though it takes the same port after a named scheme and takes
|
|
285
|
+
// `*://host:*/…` as well. Probed against Chrome 148; the e2e case in
|
|
286
|
+
// `tests/e2e/injection.spec.ts` is the standing check.
|
|
287
|
+
// Writing the pair out is the only spelling of those patterns Chrome
|
|
288
|
+
// will register, and means exactly what `*` does.
|
|
289
|
+
function anyScheme(rest) {
|
|
290
|
+
const host = rest.slice(3).split('/')[0];
|
|
291
|
+
if (!/:\d+$/.test(host))
|
|
292
|
+
return ['*' + rest];
|
|
293
|
+
return ['http' + rest, 'https' + rest];
|
|
294
|
+
}
|
|
295
|
+
// Test a single Chrome @match pattern against a URL. Returns false for
|
|
296
|
+
// any malformed input rather than throwing — callers loop over many
|
|
297
|
+
// patterns and a typo in one shouldn't break the others.
|
|
298
|
+
export function matchPattern(pattern, url) {
|
|
299
|
+
if (pattern === '<all_urls>')
|
|
300
|
+
return HTTP_LIKE_SCHEMES.test(url);
|
|
301
|
+
// `.tld` is found in each canonical form, so it's judged on the
|
|
302
|
+
// pattern as read, the same text that is registered.
|
|
303
|
+
return canonicalMatchPatterns(pattern).some((p) => testTld(p, url, false, (q) => matchCanonical(q, url)) ?? matchCanonical(p, url));
|
|
304
|
+
}
|
|
305
|
+
// `matchPattern` for a pattern already in Chrome's spelling.
|
|
306
|
+
function matchCanonical(pattern, url) {
|
|
307
|
+
const parts = /^(\*|https?|file|ftp):\/\/([^/]*)(\/.*)$/i.exec(pattern);
|
|
308
|
+
if (!parts)
|
|
309
|
+
return false;
|
|
310
|
+
const [, scheme, host, path] = parts;
|
|
311
|
+
let parsed;
|
|
312
|
+
try {
|
|
313
|
+
parsed = new URL(url);
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
// Scheme: `*` only covers http(s), per Chrome's match-pattern rules.
|
|
319
|
+
if (scheme === '*') {
|
|
320
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
else if (parsed.protocol !== `${scheme.toLowerCase()}:`) {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
// Host. file:// URLs typically have an empty host; we allow `*` to
|
|
327
|
+
// match that too so `file:///*` style patterns work.
|
|
328
|
+
//
|
|
329
|
+
// Chrome's documented match-pattern grammar disallows ports, but
|
|
330
|
+
// Chrome's actual matcher accepts them — `http://host:port/*` will
|
|
331
|
+
// be registered and Chrome will match it against URLs on that port.
|
|
332
|
+
// (Not beside a `*` scheme, which is why `anyScheme` above writes
|
|
333
|
+
// that pair out.)
|
|
334
|
+
// Honoring that here keeps OUR matcher in step with Chrome's, which
|
|
335
|
+
// matters because the badge classification uses our matcher and would
|
|
336
|
+
// otherwise show UNEXPECTED for every wrapper Chrome happily injected.
|
|
337
|
+
// Strategy: split off a trailing `:port` if present and check
|
|
338
|
+
// hostname + port separately. Pattern hosts without a port match any
|
|
339
|
+
// port (the user opted not to constrain it).
|
|
340
|
+
const colon = host.lastIndexOf(':');
|
|
341
|
+
// Must look like host:digits — IPv6 literals are bracketed so a colon
|
|
342
|
+
// outside `[…]` is unambiguous; we don't support bracketed v6 here.
|
|
343
|
+
// `:*` is any port, as in Chrome.
|
|
344
|
+
const hasPort = colon > 0 && /^(\d+|\*)$/.test(host.slice(colon + 1));
|
|
345
|
+
const patternHost = hasPort ? host.slice(0, colon) : host;
|
|
346
|
+
const patternPort = hasPort ? host.slice(colon + 1) : '';
|
|
347
|
+
if (patternHost === '*') {
|
|
348
|
+
// any host (including empty)
|
|
349
|
+
}
|
|
350
|
+
else if (patternHost.startsWith('*.')) {
|
|
351
|
+
const suffix = patternHost.slice(2).toLowerCase();
|
|
352
|
+
const h = parsed.hostname.toLowerCase();
|
|
353
|
+
if (h !== suffix && !h.endsWith('.' + suffix))
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
else if (patternHost === '') {
|
|
357
|
+
if (parsed.hostname !== '')
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
else if (parsed.hostname.toLowerCase() !== patternHost.toLowerCase()) {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
// If the pattern pinned a port, the URL must use exactly that port
|
|
364
|
+
// (after URL parsing normalises default ports — http defaults port to
|
|
365
|
+
// '', so a pattern of `http://host:80/*` won't match `http://host/`
|
|
366
|
+
// because parsed.port is ''. That mirrors Chrome's literal compare.)
|
|
367
|
+
if (patternPort !== '' && patternPort !== '*' && parsed.port !== patternPort)
|
|
368
|
+
return false;
|
|
369
|
+
// Path glob runs against pathname + query, so authors can target
|
|
370
|
+
// ?id=... etc. Not the fragment: Chrome's matcher ignores it, so a
|
|
371
|
+
// `#` in the pattern matches nothing and a URL's `#hash` never stops
|
|
372
|
+
// a match.
|
|
373
|
+
// A bare `?` (an empty query) counts, as Chrome reads it, though
|
|
374
|
+
// `search` drops it.
|
|
375
|
+
const bareQuery = !parsed.search && parsed.href.split('#')[0].endsWith('?');
|
|
376
|
+
const tail = parsed.pathname + (bareQuery ? '?' : parsed.search);
|
|
377
|
+
try {
|
|
378
|
+
return globToRegExp(path).test(tail);
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// ---------- @match validation ----------
|
|
385
|
+
//
|
|
386
|
+
// Chrome re-parses every `@match` we hand `chrome.userScripts.register`
|
|
387
|
+
// with its own match-pattern grammar, and refuses the WHOLE
|
|
388
|
+
// registration — every other pattern in it, and the script with them —
|
|
389
|
+
// over a single malformed one. Its complaint comes back in Chrome's
|
|
390
|
+
// coordinates ("matches[1]: Empty path"), with no way back to the
|
|
391
|
+
// header line that caused it.
|
|
392
|
+
//
|
|
393
|
+
// So we check the same grammar here, at parse time, where the line IS
|
|
394
|
+
// still in hand: `script-store.ts` turns a problem into a located
|
|
395
|
+
// warning (or into the script's error, when it leaves nothing to match
|
|
396
|
+
// on) and `injector.ts` drops the pattern before Chrome sees it.
|
|
397
|
+
//
|
|
398
|
+
// Deliberately narrower than Chrome's parser. It flags only shapes that
|
|
399
|
+
// are certainly wrong, because a false positive would stop a working
|
|
400
|
+
// script from running for no reason. Subtler refusals (a malformed
|
|
401
|
+
// port, a host Chrome's canonicalizer rejects) still reach Chrome and
|
|
402
|
+
// still come back as REJECTED — see docs/diagnostics.md.
|
|
403
|
+
//
|
|
404
|
+
// The invariant that makes dropping safe: every pattern flagged here is
|
|
405
|
+
// one `matchPattern` above can never match, so no page loses a script
|
|
406
|
+
// it was getting. `tests/unit/url-match.test.mjs` pins it.
|
|
407
|
+
// The schemes a `@match` may name, which is also exactly the set
|
|
408
|
+
// `matchPattern` implements. `*` covers http and https only, per
|
|
409
|
+
// Chrome's grammar.
|
|
410
|
+
export const MATCH_SCHEMES = ['*', 'http', 'https', 'file', 'ftp'];
|
|
411
|
+
// The problem with a `@match` pattern, or null when Chrome's grammar
|
|
412
|
+
// accepts it — after the lenient rewrites in `canonicalMatchPatterns`,
|
|
413
|
+
// which is the same text `buildRegistrationSpec` registers.
|
|
414
|
+
//
|
|
415
|
+
// The problem names the pattern the author WROTE, not its rewritten
|
|
416
|
+
// form: that is the text they have to go and find in the header.
|
|
417
|
+
//
|
|
418
|
+
// A pattern is a problem only when EVERY canonical form is one. A
|
|
419
|
+
// scheme glob can expand into a usable form and an unusable one
|
|
420
|
+
// (`f*:///*` → `file:///*`, legal, plus `ftp:///*`, an empty host for a
|
|
421
|
+
// scheme that needs one), and `buildRegistrationSpec` registers the
|
|
422
|
+
// forms that survive — so reporting on the worst of them would warn
|
|
423
|
+
// about a pattern that is running, or store a script INVALID for
|
|
424
|
+
// having no usable `@match` while one of its forms was registered.
|
|
425
|
+
export function matchPatternProblem(pattern) {
|
|
426
|
+
if (pattern === '<all_urls>')
|
|
427
|
+
return null;
|
|
428
|
+
let first = null;
|
|
429
|
+
for (const canonical of canonicalMatchPatterns(pattern)) {
|
|
430
|
+
const problem = canonicalPatternProblem(canonical);
|
|
431
|
+
if (!problem)
|
|
432
|
+
return null;
|
|
433
|
+
first ??= { ...problem, pattern };
|
|
434
|
+
}
|
|
435
|
+
return first;
|
|
436
|
+
}
|
|
437
|
+
// The check itself, over a pattern already in Chrome's spelling. Every
|
|
438
|
+
// shape that reaches here has a `://` — `canonicalMatchPatterns`
|
|
439
|
+
// supplies one when the author didn't — except a broken scheme, which
|
|
440
|
+
// it leaves as written.
|
|
441
|
+
function canonicalPatternProblem(pattern) {
|
|
442
|
+
if (BROKEN_SCHEME.test(pattern)) {
|
|
443
|
+
return {
|
|
444
|
+
pattern,
|
|
445
|
+
what: 'doesn\'t start with a scheme and "://"',
|
|
446
|
+
fix: 'start it with "*://" for http and https',
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
const sep = pattern.indexOf('://');
|
|
450
|
+
const scheme = pattern.slice(0, sep);
|
|
451
|
+
if (!MATCH_SCHEMES.includes(scheme.toLowerCase())) {
|
|
452
|
+
return {
|
|
453
|
+
pattern,
|
|
454
|
+
what: `names the unsupported scheme "${scheme}"`,
|
|
455
|
+
fix: `use one of ${MATCH_SCHEMES.join(', ')} ("*" covers http and https)`,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
const rest = pattern.slice(sep + 3);
|
|
459
|
+
// Always found: `readHostAndPath` supplies a path where there was none.
|
|
460
|
+
const slash = rest.indexOf('/');
|
|
461
|
+
const host = rest.slice(0, slash);
|
|
462
|
+
// Chrome takes a `:port` off the host before judging it, so
|
|
463
|
+
// `https://:8080/*` is an empty host rather than a host named
|
|
464
|
+
// ":8080". Strip the same suffix — the port itself is left to Chrome
|
|
465
|
+
// (a malformed one is exactly the kind of subtler refusal this
|
|
466
|
+
// function doesn't try to reproduce).
|
|
467
|
+
const bareHost = host.replace(/:(\d+|\*)$/, '');
|
|
468
|
+
// `file://` URLs have no host, so an empty one is only wrong for the
|
|
469
|
+
// schemes that do.
|
|
470
|
+
if (bareHost === '' && scheme.toLowerCase() !== 'file') {
|
|
471
|
+
return {
|
|
472
|
+
pattern,
|
|
473
|
+
what: 'has no host',
|
|
474
|
+
fix: 'name a host, or "*" for any host',
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
// Characters no hostname can hold. Checked because the scheme-less
|
|
478
|
+
// reading supplies a scheme for ANY text: a regex someone pasted into
|
|
479
|
+
// an `@match` (`https?:\/\/weibo.com/p/*`, a real one) reads as a
|
|
480
|
+
// pattern whose host is `https?:\`, which would otherwise pass every
|
|
481
|
+
// check below and be handed to Chrome — and Chrome refusing it takes
|
|
482
|
+
// the whole registration down. A reject-list rather than a list of
|
|
483
|
+
// legal characters, so an internationalized host still passes.
|
|
484
|
+
//
|
|
485
|
+
// No `:` in the list, deliberately. A colon that survived the port
|
|
486
|
+
// strip means a malformed port (`https://host:notaport/*`), and ports
|
|
487
|
+
// are the part of the grammar this function leaves to Chrome — see
|
|
488
|
+
// the narrowness note above, and `tests/e2e/rejected-registration.spec.ts`,
|
|
489
|
+
// which provokes REJECTED with exactly that shape.
|
|
490
|
+
if (/[\s\\?#@%"<>{}|^`]/.test(bareHost)) {
|
|
491
|
+
return {
|
|
492
|
+
pattern,
|
|
493
|
+
what: `names the invalid host "${bareHost}"`,
|
|
494
|
+
fix: 'use a host name like example.com, "*" for any host, '
|
|
495
|
+
+ 'or "*.example.com" for a domain and its subdomains',
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
// A host wildcard is either the whole host or the leading label —
|
|
499
|
+
// anywhere else Chrome answers "Invalid host wildcard".
|
|
500
|
+
const afterWildcard = bareHost === '*' ? '' : bareHost.startsWith('*.') ? bareHost.slice(2) : bareHost;
|
|
501
|
+
if (afterWildcard.includes('*')) {
|
|
502
|
+
return {
|
|
503
|
+
pattern,
|
|
504
|
+
what: 'uses "*" inside its host',
|
|
505
|
+
fix: '"*" has to be the whole host or its first label, as in *.example.com',
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
// Chrome takes this one, but matches it against URLs without their
|
|
509
|
+
// fragment, so it can never match. Violentmonkey does match it, so a
|
|
510
|
+
// script written there would otherwise just never run here.
|
|
511
|
+
if (rest.slice(slash).includes('#')) {
|
|
512
|
+
return {
|
|
513
|
+
pattern,
|
|
514
|
+
what: 'names a #hash that @match can\'t see',
|
|
515
|
+
fix: 'use @include to match on the hash',
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
// The warning for a pattern we dropped from a script that still has
|
|
521
|
+
// others to run on. Parallel to the unknown-`@grant` wording: name what
|
|
522
|
+
// is being ignored first, since that is the fact the reader is
|
|
523
|
+
// scanning the Log for.
|
|
524
|
+
export function ignoredMatchMessage(problem) {
|
|
525
|
+
const fix = problem.fix ? `; ${problem.fix}` : '';
|
|
526
|
+
return `Ignoring @match ${problem.pattern}, which ${problem.what}${fix}`;
|
|
527
|
+
}
|
|
528
|
+
// The pieces `unusableMatchMessage` is built from, kept separate so the
|
|
529
|
+
// recognizer below can be derived from the same strings rather than
|
|
530
|
+
// from a copy of the wording.
|
|
531
|
+
//
|
|
532
|
+
// The clause deliberately ends the way `NO_MATCH_OR_INCLUDE`
|
|
533
|
+
// (`script-store.ts`) does: the two failures are the same outcome
|
|
534
|
+
// reached two ways — no pattern at all, versus none Chrome will take —
|
|
535
|
+
// and reading alike is the point.
|
|
536
|
+
const MATCH_TAG = '@match ';
|
|
537
|
+
const NEVER_RUNS = ', so this script would never run on any page';
|
|
538
|
+
// The error for a script whose `@match` patterns are ALL unusable and
|
|
539
|
+
// which has no `@include` to fall back on. Same problem, different
|
|
540
|
+
// consequence — the script cannot run at all, so this is the row's
|
|
541
|
+
// `errorMessage` rather than a warning beside a working script.
|
|
542
|
+
//
|
|
543
|
+
// Starts with the tag rather than a capitalized word, like the
|
|
544
|
+
// `@require` failures do: the tag names what the reader has to go and
|
|
545
|
+
// fix, and a `@match` isn't a word we get to recase.
|
|
546
|
+
export function unusableMatchMessage(problem) {
|
|
547
|
+
const fix = problem.fix ? `; ${problem.fix}` : '';
|
|
548
|
+
return `${MATCH_TAG}${problem.pattern} ${problem.what}${NEVER_RUNS}${fix}`;
|
|
549
|
+
}
|
|
550
|
+
// One of those messages with the offending pattern elided, or null when
|
|
551
|
+
// the message is something else. What a bulk survey groups on — dozens
|
|
552
|
+
// of scripts making the same mistake with different URLs are one row,
|
|
553
|
+
// not dozens (`tests/greasyfork-examples/validate-corpus.mjs`).
|
|
554
|
+
//
|
|
555
|
+
// Derived from the constants above rather than matching the wording: a
|
|
556
|
+
// recognizer that carries its own copy of the sentence goes stale the
|
|
557
|
+
// first time the sentence is reworded, and does it silently. See
|
|
558
|
+
// docs/diagnostics.md, "One string, one place".
|
|
559
|
+
export function unusableMatchGroupKey(message) {
|
|
560
|
+
if (!message.startsWith(MATCH_TAG) || !message.includes(NEVER_RUNS))
|
|
561
|
+
return null;
|
|
562
|
+
const afterPattern = message.indexOf(' ', MATCH_TAG.length);
|
|
563
|
+
if (afterPattern < 0)
|
|
564
|
+
return null;
|
|
565
|
+
return `${MATCH_TAG}… ${message.slice(afterPattern + 1)}`;
|
|
566
|
+
}
|
|
567
|
+
// Test a single @include / @exclude pattern against a URL. Patterns
|
|
568
|
+
// wrapped in `/.../` are regex; everything else is a `*`-glob over the
|
|
569
|
+
// whole URL string. Both ignore case, as Tampermonkey's and
|
|
570
|
+
// Violentmonkey's do. Chrome tests globs case-sensitively, so what we
|
|
571
|
+
// register is widened to cover that (`chromeIncludeGlob`), and the
|
|
572
|
+
// wrapper's matcher holds the script to this reading.
|
|
573
|
+
export function includePattern(pattern, url) {
|
|
574
|
+
if (isRegexPattern(pattern)) {
|
|
575
|
+
try {
|
|
576
|
+
return new RegExp(pattern.slice(1, -1), 'i').test(url);
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
const tld = testTld(pattern, url, true, (p) => includePattern(p, url));
|
|
583
|
+
if (tld !== null)
|
|
584
|
+
return tld;
|
|
585
|
+
try {
|
|
586
|
+
return globToRegExp(pattern, 'i').test(url);
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
// The glob to register with Chrome for a glob `@include`, which we read
|
|
593
|
+
// ignoring case and Chrome tests case-sensitively. It must cover every
|
|
594
|
+
// URL the `@include` does, and the wrapper's matcher narrows it.
|
|
595
|
+
// - Chrome lowercases a URL's scheme and host before testing, so a
|
|
596
|
+
// host is lowercased and stays exact.
|
|
597
|
+
// - Every other letter becomes `?`, Chrome's one-character wildcard,
|
|
598
|
+
// which takes it in either case.
|
|
599
|
+
// - The host is the text before the first `/` (after a leading
|
|
600
|
+
// `scheme://`), when it has a dot, or follows `scheme://` with no
|
|
601
|
+
// `*` (`localhost`). `*Foo/bar*` has none: its `Foo` may well be
|
|
602
|
+
// in the path.
|
|
603
|
+
export function chromeIncludeGlob(glob) {
|
|
604
|
+
const sep = glob.indexOf('://');
|
|
605
|
+
const firstSlash = glob.indexOf('/');
|
|
606
|
+
const start = sep >= 0 && sep < firstSlash ? sep + 3 : 0;
|
|
607
|
+
const slash = glob.indexOf('/', start);
|
|
608
|
+
const hostEnd = slash < 0 ? glob.length : slash;
|
|
609
|
+
const host = glob.slice(start, hostEnd);
|
|
610
|
+
const isHost = host.includes('.') || (start > 0 && !host.includes('*'));
|
|
611
|
+
const end = isHost ? hostEnd : start;
|
|
612
|
+
return glob.slice(0, end).toLowerCase() + glob.slice(end).replace(/[a-z]/gi, '?');
|
|
613
|
+
}
|
|
614
|
+
export function pageMatcher(matching) {
|
|
615
|
+
const include = [];
|
|
616
|
+
const exclude = [];
|
|
617
|
+
for (const m of matching.matches) {
|
|
618
|
+
for (const p of canonicalMatchPatterns(m)) {
|
|
619
|
+
const rx = matchRegex(p);
|
|
620
|
+
if (rx)
|
|
621
|
+
include.push(rx);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
for (const inc of matching.includes) {
|
|
625
|
+
const rx = includeRegex(inc);
|
|
626
|
+
if (rx)
|
|
627
|
+
include.push(rx);
|
|
628
|
+
}
|
|
629
|
+
for (const ex of matching.excludes) {
|
|
630
|
+
if (hostEndsInTld(ex))
|
|
631
|
+
continue;
|
|
632
|
+
const rx = includeRegex(ex);
|
|
633
|
+
if (rx)
|
|
634
|
+
exclude.push(rx);
|
|
635
|
+
}
|
|
636
|
+
const tld = matching.includes.some(hostEndsInTld) || matching.excludes.some(hostEndsInTld)
|
|
637
|
+
|| matching.matches.some((m) => canonicalMatchPatterns(m).some(hostEndsInTld));
|
|
638
|
+
return tld ? { include, exclude, tld } : { include, exclude };
|
|
639
|
+
}
|
|
640
|
+
// Any run of labels, standing in for a `.tld` in the page's loose reading.
|
|
641
|
+
const LOOSE_TLD = '(?:\\.[a-z0-9-]+)+';
|
|
642
|
+
// The host part of a URL: up to the port, path, query or fragment, past
|
|
643
|
+
// any `user:pass@`. A bracketed IPv6 literal holds colons of its own.
|
|
644
|
+
const URL_HOST = '(?:\\[[^\\]]*\\]|[^/?#:@\\[]*)';
|
|
645
|
+
// An `@include` / `@exclude` as a page regex, ignoring case as
|
|
646
|
+
// `includePattern` does.
|
|
647
|
+
function includeRegex(pattern) {
|
|
648
|
+
if (isRegexPattern(pattern)) {
|
|
649
|
+
const source = pattern.slice(1, -1);
|
|
650
|
+
try {
|
|
651
|
+
new RegExp(source, 'i');
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
return [source, 'i'];
|
|
657
|
+
}
|
|
658
|
+
const glob = (g) => escapeForRegexExceptStar(g).replace(/\*/g, '.*');
|
|
659
|
+
const at = tldIndex(pattern, true);
|
|
660
|
+
if (at < 0)
|
|
661
|
+
return ['^' + glob(pattern) + '$', 'i'];
|
|
662
|
+
return ['^' + glob(pattern.slice(0, at)) + LOOSE_TLD + glob(pattern.slice(at + 4)) + '$', 'i'];
|
|
663
|
+
}
|
|
664
|
+
// A canonical `@match` as a regex over the whole URL, testing what
|
|
665
|
+
// `matchCanonical` does: scheme, host, port, then the path glob against
|
|
666
|
+
// the path and query (a bare `?` included, as in Chrome). Case matters
|
|
667
|
+
// in the path, so no `i` flag: the URL's scheme and host are already
|
|
668
|
+
// lowercase, so the pattern's are lowercased to meet them.
|
|
669
|
+
function matchRegex(pattern) {
|
|
670
|
+
if (pattern === '<all_urls>')
|
|
671
|
+
return ['^(?:https?|file|ftp):', 'i'];
|
|
672
|
+
const parts = /^(\*|https?|file|ftp):\/\/([^/]*)(\/.*)$/i.exec(pattern);
|
|
673
|
+
if (!parts)
|
|
674
|
+
return null;
|
|
675
|
+
const [, scheme, host, path] = parts;
|
|
676
|
+
// The path glob never sees the fragment, so a `#` in it matches nothing.
|
|
677
|
+
if (path.includes('#'))
|
|
678
|
+
return null;
|
|
679
|
+
const colon = host.lastIndexOf(':');
|
|
680
|
+
const hasPort = colon > 0 && /^(\d+|\*)$/.test(host.slice(colon + 1));
|
|
681
|
+
const patternHost = (hasPort ? host.slice(0, colon) : host).toLowerCase();
|
|
682
|
+
const patternPort = hasPort ? host.slice(colon + 1) : '';
|
|
683
|
+
// A `user@` in the pattern's host never matches a URL's host.
|
|
684
|
+
if (patternHost.includes('@'))
|
|
685
|
+
return null;
|
|
686
|
+
const esc = (s) => escapeForRegexExceptStar(s).replace(/\*/g, '\\*');
|
|
687
|
+
const anySub = '(?:' + URL_HOST + '\\.)?';
|
|
688
|
+
let hostRx;
|
|
689
|
+
if (patternHost === '*') {
|
|
690
|
+
hostRx = URL_HOST;
|
|
691
|
+
}
|
|
692
|
+
else if (tldIndex(patternHost) >= 0) {
|
|
693
|
+
const base = patternHost.slice(0, -4);
|
|
694
|
+
hostRx = base.startsWith('*.') ? anySub + esc(base.slice(2)) + LOOSE_TLD : esc(base) + LOOSE_TLD;
|
|
695
|
+
}
|
|
696
|
+
else if (patternHost.startsWith('*.')) {
|
|
697
|
+
hostRx = anySub + esc(patternHost.slice(2));
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
hostRx = esc(patternHost);
|
|
701
|
+
}
|
|
702
|
+
const portRx = patternPort === '' || patternPort === '*' ? '(?::\\d+)?' : ':' + patternPort;
|
|
703
|
+
const schemeRx = scheme === '*' ? 'https?' : scheme.toLowerCase();
|
|
704
|
+
// The path glob covers the path and query, never the fragment.
|
|
705
|
+
const pathRx = escapeForRegexExceptStar(path).replace(/\*/g, '[^#]*');
|
|
706
|
+
return ['^' + schemeRx + '://(?:[^/?#@]*@)?' + hostRx + portRx + pathRx + '(?:#.*)?$', ''];
|
|
707
|
+
}
|
|
708
|
+
// Combined script-level test: a script matches a URL iff
|
|
709
|
+
// - no @exclude pattern matches, AND
|
|
710
|
+
// - at least one @match or @include pattern matches.
|
|
711
|
+
// A script with zero match/include patterns never matches anything,
|
|
712
|
+
// which is intentional — validateHeader already flags those as errors.
|
|
713
|
+
//
|
|
714
|
+
// Stops at the first rule that settles it: this runs for every script
|
|
715
|
+
// on every URL the badge checks. `explainUrlMatch` is the same rule,
|
|
716
|
+
// listing every hit instead; `tests/unit/url-match.test.mjs` pins the
|
|
717
|
+
// two together.
|
|
718
|
+
export function urlMatchesScript(url, matching) {
|
|
719
|
+
if (matching.excludes.some((ex) => includePattern(ex, url)))
|
|
720
|
+
return false;
|
|
721
|
+
return matching.matches.some((m) => matchPattern(m, url)) || matching.includes.some((inc) => includePattern(inc, url));
|
|
722
|
+
}
|
|
723
|
+
// The same test, also naming the rules that answered, for a developer
|
|
724
|
+
// asking why a script did or didn't run. Slower than `urlMatchesScript`,
|
|
725
|
+
// since it tries every rule to list them all.
|
|
726
|
+
export function explainUrlMatch(url, matching) {
|
|
727
|
+
const excludedBy = matching.excludes.filter((ex) => includePattern(ex, url)).map((ex) => `@exclude ${ex}`);
|
|
728
|
+
const matchedBy = [
|
|
729
|
+
...matching.matches.filter((m) => matchPattern(m, url)).map((m) => `@match ${m}`),
|
|
730
|
+
...matching.includes.filter((inc) => includePattern(inc, url)).map((inc) => `@include ${inc}`),
|
|
731
|
+
];
|
|
732
|
+
const matches = excludedBy.length === 0 && matchedBy.length > 0;
|
|
733
|
+
return { matches, rule: excludedBy[0] ?? matchedBy[0] ?? null, matchedBy, excludedBy };
|
|
734
|
+
}
|