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,679 @@
|
|
|
1
|
+
// Collection I/O: fetch + scan for every collection kind.
|
|
2
|
+
//
|
|
3
|
+
// Owns: content fetch and the scanner that turns a collection path/URL
|
|
4
|
+
// into a list of FoundScript entries. Path helpers, the `file://`
|
|
5
|
+
// reader and listing parser are in `src/file-locations.ts`, which this
|
|
6
|
+
// re-exports. All persistence lives in `src/extension/script-store.ts`; this
|
|
7
|
+
// module is pure I/O.
|
|
8
|
+
//
|
|
9
|
+
// The following collection kinds all flow through the same downstream
|
|
10
|
+
// pipeline:
|
|
11
|
+
// - local directory (an absolute filesystem path; recursive scan)
|
|
12
|
+
// - local single file (an absolute path ending in .user.js)
|
|
13
|
+
// - http single file (an http(s) URL whose pathname ends in .user.js)
|
|
14
|
+
// - manifest (anything whose pathname's basename is
|
|
15
|
+
// `script_manifest.json` — an absolute local
|
|
16
|
+
// path, a file:// URL, or an http(s) URL —
|
|
17
|
+
// returning a JSON list of script URLs that
|
|
18
|
+
// resolve against the manifest URL. The exact
|
|
19
|
+
// filename is the only manifest shape we accept.)
|
|
20
|
+
//
|
|
21
|
+
// HTTP requirements:
|
|
22
|
+
// - manifest: "<all_urls>" (or a narrower http(s) pattern) in
|
|
23
|
+
// host_permissions so the SW/collections page can fetch arbitrary
|
|
24
|
+
// origins without CORS blocking the response body.
|
|
25
|
+
//
|
|
26
|
+
// File requirements:
|
|
27
|
+
// - manifest: "file:///*" in host_permissions
|
|
28
|
+
// - user: enabled "Allow access to file URLs" on chrome://extensions
|
|
29
|
+
// The toggle is persistent across browser sessions, so unlike the File
|
|
30
|
+
// System Access API there is no per-session re-grant.
|
|
31
|
+
import { MAX_SCAN_DEPTH, MANIFEST_FILENAME } from '../collection-scan.js';
|
|
32
|
+
import { mapWithConcurrency, SCAN_DIRS_CONCURRENCY } from './concurrency.js';
|
|
33
|
+
// Page-only API under the hood, but the wrapper answers "allowed" from
|
|
34
|
+
// any context that can't ask — and only `classifyLocation`, which only
|
|
35
|
+
// the collection dialog calls, uses it.
|
|
36
|
+
import { isFileSchemeAccessAllowed } from './file-access.js';
|
|
37
|
+
export { MAX_SCAN_DEPTH } from '../collection-scan.js';
|
|
38
|
+
// A single script typed where a collection was expected. Two spellings
|
|
39
|
+
// of one message: the bare sentence for a surface that offers the
|
|
40
|
+
// install route itself (the collection dialog puts a button under it),
|
|
41
|
+
// and the sentence plus the fix for surfaces that can only refuse.
|
|
42
|
+
export const SINGLE_SCRIPT_NOT_A_COLLECTION = 'This is a single script, not a collection';
|
|
43
|
+
export const SINGLE_SCRIPT_USE_ADD_FIELD = `${SINGLE_SCRIPT_NOT_A_COLLECTION}. To install it, paste it in the Add field instead.`;
|
|
44
|
+
// A readable local file of a shape no collection can be built from.
|
|
45
|
+
// Says what would work rather than what didn't, per the "name the fix"
|
|
46
|
+
// rule.
|
|
47
|
+
export const UNSUPPORTED_LOCATION_FILE = `File must be a script (.user.js) or a manifest (${MANIFEST_FILENAME})`;
|
|
48
|
+
// An http(s) URL that names no collection we recognise. GitHub is the
|
|
49
|
+
// only folder we can scan over http — a plain web directory has no
|
|
50
|
+
// listing to read — so naming it is not an example but the whole rule.
|
|
51
|
+
// Deliberately shaped like `UNSUPPORTED_LOCATION_FILE`: one sentence
|
|
52
|
+
// saying what would work.
|
|
53
|
+
export const HTTP_URL_NOT_A_COLLECTION = `URL must be a GitHub folder or a manifest (${MANIFEST_FILENAME})`;
|
|
54
|
+
// A local path nothing could be read from. A file:// read fails
|
|
55
|
+
// identically for "not there" and "not allowed", so the caller passes
|
|
56
|
+
// in the answer to the second (`isFileSchemeAccessAllowed`) and the
|
|
57
|
+
// message names one cause rather than listing both.
|
|
58
|
+
//
|
|
59
|
+
// The off case opens on the same words as the file-access banner
|
|
60
|
+
// (`renderFileAccessBanner`) so the two read as one story, and points
|
|
61
|
+
// at the settings without repeating the banner's link: the banner is
|
|
62
|
+
// already on the page behind this dialog.
|
|
63
|
+
export function locationNotReadableMessage(location, fileAccess) {
|
|
64
|
+
// `Could not read …` matches the wording every other read failure in
|
|
65
|
+
// this module uses, so one session can't show two names for one
|
|
66
|
+
// failure.
|
|
67
|
+
//
|
|
68
|
+
// Ends with a period, unlike the single-sentence messages above: the
|
|
69
|
+
// no-trailing-period rule is there so a message can take a `(in
|
|
70
|
+
// <where>)` suffix, which these never do — they render alone in the
|
|
71
|
+
// dialog's error slot — and a run of sentences whose last one is the
|
|
72
|
+
// only bare one reads as a typo. Same shape as the other multi-
|
|
73
|
+
// sentence messages here (`SINGLE_SCRIPT_USE_ADD_FIELD`).
|
|
74
|
+
return fileAccess
|
|
75
|
+
? `Could not read ${location}. Check that the path exists.`
|
|
76
|
+
: `Could not read ${location}. File access is disabled for this extension. Enable in extension settings.`;
|
|
77
|
+
}
|
|
78
|
+
// The shapes a location may be typed in: an http(s) URL, a file:// URL,
|
|
79
|
+
// or an absolute POSIX path that isn't the filesystem root. Shared by
|
|
80
|
+
// every surface that accepts one, so the rule and the message
|
|
81
|
+
// (`LOCATION_SHAPES`) can't drift apart.
|
|
82
|
+
export function isValidLocationShape(input) {
|
|
83
|
+
if (isHttpUrl(input))
|
|
84
|
+
return true;
|
|
85
|
+
if (isFileUrl(input))
|
|
86
|
+
return !isFilesystemRoot(locationFsPath(input));
|
|
87
|
+
return input.startsWith('/') && !isFilesystemRoot(input);
|
|
88
|
+
}
|
|
89
|
+
// Whether a POSIX path names the filesystem root, however it is
|
|
90
|
+
// spelled. Scanning `/` would walk the whole disk, so every spelling
|
|
91
|
+
// has to be refused — not just the bare one: `//`, `/.` and `/../..`
|
|
92
|
+
// all land in the same place, and a length test (what this used to be)
|
|
93
|
+
// lets each of them through. A path built only from separators and
|
|
94
|
+
// dots can't name anything else.
|
|
95
|
+
function isFilesystemRoot(path) {
|
|
96
|
+
return /^[/.]*$/.test(path.trim());
|
|
97
|
+
}
|
|
98
|
+
// Decide what a location IS, reading it where we can rather than
|
|
99
|
+
// guessing from its name. Local paths get probed — a directory listing
|
|
100
|
+
// first, then a plain file read — so a path that doesn't exist says so
|
|
101
|
+
// instead of being filed as a directory and failing later in a scan.
|
|
102
|
+
//
|
|
103
|
+
// http(s) URLs are classified by shape alone: there is no remote
|
|
104
|
+
// listing, and a keystroke-rate classifier must not fire network reads
|
|
105
|
+
// at arbitrary origins. Their readability is checked when the
|
|
106
|
+
// collection is created.
|
|
107
|
+
//
|
|
108
|
+
// Called from the collection dialog, a page. The reads would work from
|
|
109
|
+
// the worker too (docs/worker-writes.md#checked-what-the-worker-can-do),
|
|
110
|
+
// but its file-access check (`isFileSchemeAccessAllowed`) can only ask
|
|
111
|
+
// from a page.
|
|
112
|
+
export async function classifyLocation(input) {
|
|
113
|
+
const value = input.trim();
|
|
114
|
+
if (!isValidLocationShape(value)) {
|
|
115
|
+
return { kind: 'invalid', message: `Location ${LOCATION_SHAPES}` };
|
|
116
|
+
}
|
|
117
|
+
if (isHttpUrl(value)) {
|
|
118
|
+
if (isSingleFileInput(value))
|
|
119
|
+
return { kind: 'script' };
|
|
120
|
+
if (isManifestCollection(value))
|
|
121
|
+
return { kind: 'manifest' };
|
|
122
|
+
if (isGithubDirInput(value))
|
|
123
|
+
return { kind: 'directory' };
|
|
124
|
+
return { kind: 'invalid', message: HTTP_URL_NOT_A_COLLECTION };
|
|
125
|
+
}
|
|
126
|
+
// Local. Probe through the canonical URL rather than a filesystem
|
|
127
|
+
// path: `locationFsPath` mangles the Windows shapes `pathToFileUrl`
|
|
128
|
+
// can't rebuild, and a URL the user typed is already in the form the
|
|
129
|
+
// read wants.
|
|
130
|
+
// Safe to strip: the shape check above refused everything that
|
|
131
|
+
// collapses to `/`, so this can't eat `file://`'s own slashes.
|
|
132
|
+
const url = collectionCanonicalUrl(value).replace(/\/+$/, '');
|
|
133
|
+
// Directory first: Chrome serves a listing for a directory URL with
|
|
134
|
+
// OR without the trailing slash, so a plain read can't tell the two
|
|
135
|
+
// apart — but only a directory answers the slashed form at all.
|
|
136
|
+
if (await canReadFileUrl(url + '/'))
|
|
137
|
+
return { kind: 'directory' };
|
|
138
|
+
if (await canReadFileUrl(url)) {
|
|
139
|
+
if (isSingleFileInput(value))
|
|
140
|
+
return { kind: 'script' };
|
|
141
|
+
if (isManifestCollection(value))
|
|
142
|
+
return { kind: 'manifest' };
|
|
143
|
+
return { kind: 'invalid', message: UNSUPPORTED_LOCATION_FILE };
|
|
144
|
+
}
|
|
145
|
+
// Which of the two reasons it is IS knowable, so the message says
|
|
146
|
+
// which rather than listing both: with file access off, every local
|
|
147
|
+
// path reads as missing, and "check that the path exists" would send
|
|
148
|
+
// the user hunting for a file that is sitting right there.
|
|
149
|
+
const fileAccess = await isFileSchemeAccessAllowed();
|
|
150
|
+
return {
|
|
151
|
+
kind: 'invalid',
|
|
152
|
+
message: locationNotReadableMessage(canonicalCollectionInput(value), fileAccess),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
// GitHub-specific collection handling lives in `github-provider.ts`. We
|
|
156
|
+
// re-export only the symbols `script-store.ts` and `options.ts` use
|
|
157
|
+
// directly. Callers always go through `normalizeCollectionInput`; today
|
|
158
|
+
// that's just `normalizeGithubInput` under its public name, but
|
|
159
|
+
// keeping the public name generic lets future providers slot in
|
|
160
|
+
// without changing the callers.
|
|
161
|
+
export { isGithubDirCollection } from './github-provider.js';
|
|
162
|
+
export { normalizeGithubInput as normalizeCollectionInput } from './github-provider.js';
|
|
163
|
+
import { githubDirScriptFileUrl, githubDirScriptDisplayUrl, isGithubDirCollection, isGithubDirInput, scanGithubDir, } from './github-provider.js';
|
|
164
|
+
import { githubRawToBlobUrl } from '../github-urls.js';
|
|
165
|
+
export { githubRawToBlobUrl } from '../github-urls.js';
|
|
166
|
+
import { githubAuthHeaders } from '../github-auth.js';
|
|
167
|
+
import { decodeForDisplay, leafName } from '../script-display.js';
|
|
168
|
+
import { mustHopToWorker, readHttpViaWorker } from '../http-read.js';
|
|
169
|
+
import { MarkupResponseError, isMarkupContentType, looksLikeMarkup, markupResponseMessage, } from '../markup-response.js';
|
|
170
|
+
import { baseName, canonicalCollectionInput, collectionCanonicalUrl, isContainerCollection, isFileUrl, isHttpUrl, isManifestCollection, isSingleFileInput, joinPath, listDirectory, LOCATION_SHAPES, locationFsPath, NOT_A_DIRECTORY_LISTING, parentDir, pathToFileUrl, readFileUrlText, readFileUrlWithStat, statFromListing, urlPathname, } from '../file-locations.js';
|
|
171
|
+
// Location and listing helpers, re-exported for the callers that reach
|
|
172
|
+
// them through here.
|
|
173
|
+
export * from '../file-locations.js';
|
|
174
|
+
// Scratch-script and sm-dev collection naming, likewise.
|
|
175
|
+
export * from './location-kinds.js';
|
|
176
|
+
import { isScratchUrl, scratchTextKey } from './location-kinds.js';
|
|
177
|
+
// Leaf name (last path segment) of an http(s) URL, percent-decoded —
|
|
178
|
+
// used as the FoundScript.name for HTTP manifest entries so the
|
|
179
|
+
// collections page shows "foo.user.js" rather than the whole URL.
|
|
180
|
+
// Returns '' for anything that doesn't parse as a URL, which callers
|
|
181
|
+
// use as their "fall back to the raw entry" signal.
|
|
182
|
+
export function urlBaseName(input) {
|
|
183
|
+
return decodeForDisplay(leafName(urlPathname(input)));
|
|
184
|
+
}
|
|
185
|
+
async function fetchScratchText(url) {
|
|
186
|
+
const got = await chrome.storage.local.get(scratchTextKey(url));
|
|
187
|
+
const text = got[scratchTextKey(url)];
|
|
188
|
+
if (typeof text !== 'string')
|
|
189
|
+
throw new Error(`Could not read ${url}: The scratch script's text was cleared. Push it again.`);
|
|
190
|
+
return text;
|
|
191
|
+
}
|
|
192
|
+
// ---------- File / directory fetch ----------
|
|
193
|
+
export function fetchFileUrlText(url) {
|
|
194
|
+
if (isScratchUrl(url))
|
|
195
|
+
return fetchScratchText(url);
|
|
196
|
+
return readFileUrlText(url);
|
|
197
|
+
}
|
|
198
|
+
// `fetchFileUrlText` for text that is about to be treated as a script.
|
|
199
|
+
//
|
|
200
|
+
// The markup guard can't live in `fetchFileUrlText` itself: among its
|
|
201
|
+
// other callers is the one that reads Chrome's `file://` directory
|
|
202
|
+
// listing, which is HTML *by design* (`parseListing`). So the two
|
|
203
|
+
// script-body call sites — the refresh pipeline and the preview —
|
|
204
|
+
// share this wrapper instead.
|
|
205
|
+
//
|
|
206
|
+
// What it catches locally: an `.user.js` path that is really a
|
|
207
|
+
// directory (Chrome answers with a generated listing), and an HTML
|
|
208
|
+
// file that got the suffix by mistake. The `file://` legs often report
|
|
209
|
+
// no Content-Type at all, so the body test is the only one available.
|
|
210
|
+
export async function fetchScriptFileUrlText(url) {
|
|
211
|
+
return refuseMarkup(url, await fetchFileUrlText(url));
|
|
212
|
+
}
|
|
213
|
+
// `fetchScriptFileUrlText` for a `file://` script, with the size and
|
|
214
|
+
// modified time a listing of its folder would give. See
|
|
215
|
+
// `readFileUrlWithStat`.
|
|
216
|
+
export async function fetchScriptFileWithStat(url) {
|
|
217
|
+
const read = await readFileUrlWithStat(url);
|
|
218
|
+
refuseMarkup(url, read.text);
|
|
219
|
+
return read;
|
|
220
|
+
}
|
|
221
|
+
function refuseMarkup(url, text) {
|
|
222
|
+
if (looksLikeMarkup(text)) {
|
|
223
|
+
// No `Could not read ${url}: ` prefix, unlike its siblings here:
|
|
224
|
+
// the local wording already names the file, and the prefix would
|
|
225
|
+
// print the same URL twice in one sentence.
|
|
226
|
+
throw new MarkupResponseError(markupResponseMessage({ localUrl: url }));
|
|
227
|
+
}
|
|
228
|
+
return text;
|
|
229
|
+
}
|
|
230
|
+
// Best-effort "can this file:// URL be read" probe. Returns false on
|
|
231
|
+
// any error (including non-existence and permission failures) —
|
|
232
|
+
// callers should only rely on a true result.
|
|
233
|
+
async function canReadFileUrl(url) {
|
|
234
|
+
try {
|
|
235
|
+
await fetchFileUrlText(url);
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Best-effort "does the path point to a readable file" probe. Used to
|
|
243
|
+
// turn the otherwise-cryptic "Could not read …/" message into a
|
|
244
|
+
// clearer one when the user types a path to an existing non-userscript
|
|
245
|
+
// file.
|
|
246
|
+
async function isReadableFile(absolutePath) {
|
|
247
|
+
return await canReadFileUrl(pathToFileUrl(absolutePath));
|
|
248
|
+
}
|
|
249
|
+
// Whether an input names one script file, checked against the raw
|
|
250
|
+
// input before any canonicalization: an http(s) or file:// URL, or an
|
|
251
|
+
// absolute local path — the same shapes the collections page's Add
|
|
252
|
+
// field accepts for a single script. Checked before canonicalizing
|
|
253
|
+
// because `collectionCanonicalUrl` turns anything else into a file://
|
|
254
|
+
// URL, and an `ftp://…` would otherwise fail with a baffling
|
|
255
|
+
// `file:///ftp%3A//…` error instead of being named.
|
|
256
|
+
export function isSingleFileLocation(input) {
|
|
257
|
+
return isHttpUrl(input) || isFileUrl(input) || input.startsWith('/');
|
|
258
|
+
}
|
|
259
|
+
// The text at a single-file location — the two shapes one can take once
|
|
260
|
+
// canonicalized, http(s) or file://. What the install preview fetches,
|
|
261
|
+
// and what the compare page fetches to stand a download against an
|
|
262
|
+
// installed copy.
|
|
263
|
+
export async function fetchSingleFileText(url) {
|
|
264
|
+
if (isFileUrl(url))
|
|
265
|
+
return fetchScriptFileUrlText(url);
|
|
266
|
+
const result = await fetchHttpText(url);
|
|
267
|
+
// `unchanged` needs a conditional request to happen, and we send
|
|
268
|
+
// none — but the type includes it, so fail loudly rather than
|
|
269
|
+
// rendering an empty page if that ever changes.
|
|
270
|
+
if (result.kind !== 'ok')
|
|
271
|
+
throw new Error('No content returned');
|
|
272
|
+
return result.text;
|
|
273
|
+
}
|
|
274
|
+
// Fetch an http(s) URL, optionally with conditional headers. A 304
|
|
275
|
+
// response surfaces as `{kind: 'unchanged'}` so callers can preserve
|
|
276
|
+
// the prior content without re-parsing. We use `cache: 'no-store'`
|
|
277
|
+
// because we maintain our own validators (etag / lastModified) on each
|
|
278
|
+
// script row — the browser HTTP cache would otherwise serve a stale
|
|
279
|
+
// 200 without ever asking the server.
|
|
280
|
+
export async function fetchHttpText(url, opts = {}) {
|
|
281
|
+
const headers = {};
|
|
282
|
+
if (opts.etag)
|
|
283
|
+
headers['If-None-Match'] = opts.etag;
|
|
284
|
+
if (opts.lastModified)
|
|
285
|
+
headers['If-Modified-Since'] = opts.lastModified;
|
|
286
|
+
// Page contexts hand the read to the service worker: a document that
|
|
287
|
+
// fetches for itself acts on the response's `Link: rel=preload`
|
|
288
|
+
// headers, and a login page — the case the markup check below exists
|
|
289
|
+
// for — is exactly the kind of response that sends them. See
|
|
290
|
+
// src/http-read.ts.
|
|
291
|
+
if (mustHopToWorker()) {
|
|
292
|
+
const reply = await readHttpViaWorker({ url, as: 'text', headers });
|
|
293
|
+
if (reply) {
|
|
294
|
+
if (reply.kind === 'error') {
|
|
295
|
+
throw new Error(`Could not read ${url}: ${reply.message ?? reply.reason}`);
|
|
296
|
+
}
|
|
297
|
+
return interpretHttpText(url, opts, {
|
|
298
|
+
status: reply.status,
|
|
299
|
+
contentType: reply.contentType,
|
|
300
|
+
etag: reply.etag,
|
|
301
|
+
lastModified: reply.lastModified,
|
|
302
|
+
text: reply.text ?? '',
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
// No answer at all (worker evicted mid-message, extension
|
|
306
|
+
// reloading). Falling through reads it here, which beats failing a
|
|
307
|
+
// read the user asked for.
|
|
308
|
+
}
|
|
309
|
+
// Attach the configured GitHub token when the URL targets a GitHub
|
|
310
|
+
// host. No-op for any other URL. Lets private-repo raw URLs and
|
|
311
|
+
// private repo manifests fetch under auth without each call site
|
|
312
|
+
// having to opt in. Header host-gating is enforced inside
|
|
313
|
+
// githubAuthHeaders so a token can never be leaked to a non-GitHub
|
|
314
|
+
// host. (Over the hop the worker attaches the same headers from its
|
|
315
|
+
// own copy of the token, so it never rides on a message.)
|
|
316
|
+
Object.assign(headers, githubAuthHeaders(url));
|
|
317
|
+
let response;
|
|
318
|
+
try {
|
|
319
|
+
response = await fetch(url, { cache: 'no-store', headers });
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
throw new Error(`Could not read ${url}: ${err instanceof Error ? err.message : String(err)}`);
|
|
323
|
+
}
|
|
324
|
+
if (response.status === 304)
|
|
325
|
+
return { kind: 'unchanged' };
|
|
326
|
+
return interpretHttpText(url, opts, {
|
|
327
|
+
status: response.status,
|
|
328
|
+
contentType: response.headers.get('content-type') ?? '',
|
|
329
|
+
etag: response.headers.get('ETag'),
|
|
330
|
+
lastModified: response.headers.get('Last-Modified'),
|
|
331
|
+
text: await response.text(),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
// What the response MEANS, applied identically to a response we read
|
|
335
|
+
// here and to one the worker read for us — so the two paths can't come
|
|
336
|
+
// to disagree about which statuses fail or which bodies are markup.
|
|
337
|
+
//
|
|
338
|
+
// A 304 never reaches here from the local path (it has no body to
|
|
339
|
+
// read); over the hop it arrives as an ordinary status and is answered
|
|
340
|
+
// the same way.
|
|
341
|
+
function interpretHttpText(url, opts, res) {
|
|
342
|
+
if (res.status === 304)
|
|
343
|
+
return { kind: 'unchanged' };
|
|
344
|
+
if (res.status < 200 || res.status > 299) {
|
|
345
|
+
throw new Error(`Could not read ${url}: HTTP ${res.status}`);
|
|
346
|
+
}
|
|
347
|
+
// Every caller of this treats the result as JavaScript (a script
|
|
348
|
+
// body) or JSON (a manifest); neither can be markup. Rejecting here
|
|
349
|
+
// rather than letting it through is what keeps a refresh that lands
|
|
350
|
+
// on a login page from replacing a working script with a page of
|
|
351
|
+
// HTML — a fetch failure preserves the prior copy, a "successful"
|
|
352
|
+
// fetch of garbage does not.
|
|
353
|
+
//
|
|
354
|
+
// The header is quoted verbatim, not lowercased: it is evidence, and
|
|
355
|
+
// evidence should read as what the server actually sent.
|
|
356
|
+
const markupHeader = isMarkupContentType(res.contentType);
|
|
357
|
+
if (markupHeader || looksLikeMarkup(res.text)) {
|
|
358
|
+
throw new MarkupResponseError(`Could not read ${url}: ${markupResponseMessage({
|
|
359
|
+
...(markupHeader ? { contentType: res.contentType } : {}),
|
|
360
|
+
expected: opts.expects ?? 'JavaScript',
|
|
361
|
+
})}`);
|
|
362
|
+
}
|
|
363
|
+
const result = { kind: 'ok', text: res.text };
|
|
364
|
+
if (res.etag)
|
|
365
|
+
result.etag = res.etag;
|
|
366
|
+
if (res.lastModified)
|
|
367
|
+
result.lastModified = res.lastModified;
|
|
368
|
+
return result;
|
|
369
|
+
}
|
|
370
|
+
// ---------- Scanning ----------
|
|
371
|
+
// Parse a manifest body. Accepts either a bare JSON array or
|
|
372
|
+
// `{scripts: [...]}`. Anything else is rejected with a message that
|
|
373
|
+
// points at the supported shapes. Empty strings inside the list are
|
|
374
|
+
// dropped so a trailing newline / typo doesn't fail the whole source.
|
|
375
|
+
//
|
|
376
|
+
// List elements are either a URL string or an object with a `path`
|
|
377
|
+
// holding that string. The object form lets a manifest carry extra
|
|
378
|
+
// per-script bookkeeping (author's notes, tags, …) alongside the URL;
|
|
379
|
+
// every field other than `path` is ignored here, so unknown keys are
|
|
380
|
+
// forward-compatible rather than an error.
|
|
381
|
+
export function parseManifest(body) {
|
|
382
|
+
let parsed;
|
|
383
|
+
try {
|
|
384
|
+
parsed = JSON.parse(body);
|
|
385
|
+
}
|
|
386
|
+
catch (err) {
|
|
387
|
+
throw new Error(`Manifest is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
388
|
+
}
|
|
389
|
+
let arr;
|
|
390
|
+
if (Array.isArray(parsed)) {
|
|
391
|
+
arr = parsed;
|
|
392
|
+
}
|
|
393
|
+
else if (parsed && typeof parsed === 'object' && Array.isArray(parsed.scripts)) {
|
|
394
|
+
arr = parsed.scripts;
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
throw new Error('Manifest must be a JSON array of entries, or `{"scripts": [...]}`');
|
|
398
|
+
}
|
|
399
|
+
const entries = [];
|
|
400
|
+
for (const item of arr) {
|
|
401
|
+
let value;
|
|
402
|
+
if (typeof item === 'string') {
|
|
403
|
+
value = item;
|
|
404
|
+
}
|
|
405
|
+
else if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
406
|
+
// An array is also `typeof 'object'`, hence the isArray guard —
|
|
407
|
+
// otherwise `[["a.user.js"]]` would look for a `path` field on it.
|
|
408
|
+
const entryPath = item.path;
|
|
409
|
+
if (typeof entryPath !== 'string') {
|
|
410
|
+
throw new Error('Manifest entry objects must have a "path" string');
|
|
411
|
+
}
|
|
412
|
+
value = entryPath;
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
throw new Error('Manifest entries must all be URL strings or objects with a "path"');
|
|
416
|
+
}
|
|
417
|
+
const trimmed = value.trim();
|
|
418
|
+
if (trimmed)
|
|
419
|
+
entries.push(trimmed);
|
|
420
|
+
}
|
|
421
|
+
return entries;
|
|
422
|
+
}
|
|
423
|
+
export async function scanForUserScripts(absolutePath, depth = 0, prefix = '',
|
|
424
|
+
// Listings to share with the caller — a manifest refresh stats the
|
|
425
|
+
// manifest file from the same folder its entries usually sit in.
|
|
426
|
+
listings) {
|
|
427
|
+
// Collection-kind dispatch. Container collections don't have a
|
|
428
|
+
// scannable backing store — refreshCollection builds a synthetic scan
|
|
429
|
+
// from the existing scriptIds and never calls this for them. The
|
|
430
|
+
// remaining branches are scheme-agnostic (raw path / file:// /
|
|
431
|
+
// http(s):// handled uniformly); local directory is the only branch
|
|
432
|
+
// that rejects HTTP, because there's no remote listing API.
|
|
433
|
+
if (depth === 0) {
|
|
434
|
+
if (isContainerCollection(absolutePath)) {
|
|
435
|
+
throw new Error('Internal error: this input kind is refreshed directly, not scanned');
|
|
436
|
+
}
|
|
437
|
+
if (isManifestCollection(absolutePath)) {
|
|
438
|
+
return scanManifest(collectionCanonicalUrl(absolutePath), listings);
|
|
439
|
+
}
|
|
440
|
+
if (isGithubDirCollection(absolutePath)) {
|
|
441
|
+
return scanGithubDir(absolutePath);
|
|
442
|
+
}
|
|
443
|
+
if (isHttpUrl(absolutePath)) {
|
|
444
|
+
// No mention of "container collections": that's how we store a
|
|
445
|
+
// stand-alone script, and the reader adding a URL has no name for
|
|
446
|
+
// it — from the page it just looks like adding the script itself.
|
|
447
|
+
throw new Error(HTTP_URL_NOT_A_COLLECTION);
|
|
448
|
+
}
|
|
449
|
+
// Local-directory only. Normalise file:// → raw path once so the
|
|
450
|
+
// downstream listing + recursion all see a single form.
|
|
451
|
+
absolutePath = locationFsPath(absolutePath);
|
|
452
|
+
}
|
|
453
|
+
// `readonly` because a listing can be batch-shared — see
|
|
454
|
+
// `DirectoryListingCache`. The scan only reads it.
|
|
455
|
+
let entries;
|
|
456
|
+
try {
|
|
457
|
+
entries = await listDirectory(absolutePath);
|
|
458
|
+
}
|
|
459
|
+
catch (err) {
|
|
460
|
+
// Common foot-gun at the top level: the user types a path to a file
|
|
461
|
+
// whose name doesn't end with `.user.js` (e.g. `notes.md`). The
|
|
462
|
+
// listing fetch above asks Chrome for `file://…/notes.md/`, which
|
|
463
|
+
// produces an opaque "could not read" message. Probe by reading the
|
|
464
|
+
// path as a plain file; if that succeeds we know the path exists
|
|
465
|
+
// but isn't a userscript, so swap in a clearer message.
|
|
466
|
+
// Not when the listing was refused for its shape: Chrome serves a
|
|
467
|
+
// listing for a directory URL without the slash too, so the probe
|
|
468
|
+
// would "succeed" on a real folder and blame the user's path.
|
|
469
|
+
const shapeRefused = err instanceof Error && err.message.includes(NOT_A_DIRECTORY_LISTING);
|
|
470
|
+
if (depth === 0 && !shapeRefused && await isReadableFile(absolutePath)) {
|
|
471
|
+
throw new Error(`${absolutePath} is a file, not a folder, and its name doesn't end in .user.js`);
|
|
472
|
+
}
|
|
473
|
+
throw err;
|
|
474
|
+
}
|
|
475
|
+
const results = [];
|
|
476
|
+
const subdirs = [];
|
|
477
|
+
for (const entry of entries) {
|
|
478
|
+
if (!entry.isDir && entry.name.endsWith('.user.js')) {
|
|
479
|
+
const found = { name: entry.name, relativePath: prefix + entry.name };
|
|
480
|
+
if (entry.size !== undefined)
|
|
481
|
+
found.size = entry.size;
|
|
482
|
+
if (entry.mtime !== undefined)
|
|
483
|
+
found.mtime = entry.mtime;
|
|
484
|
+
results.push(found);
|
|
485
|
+
}
|
|
486
|
+
else if (entry.isDir && depth < MAX_SCAN_DEPTH) {
|
|
487
|
+
subdirs.push(entry);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
// Sibling directories are independent, so they are listed together
|
|
491
|
+
// rather than one after another. This used to be an `await` inside the
|
|
492
|
+
// loop, which made the scan cost one round trip per directory however
|
|
493
|
+
// shallow the tree — the dominant phase of a refresh once the other
|
|
494
|
+
// fan-outs were fixed.
|
|
495
|
+
//
|
|
496
|
+
// Rejection still fails the whole scan, as it did: the caller turns
|
|
497
|
+
// that into the collection's `scanError`, and a listing we couldn't
|
|
498
|
+
// read means we don't know what's there.
|
|
499
|
+
const nested = await mapWithConcurrency(subdirs, SCAN_DIRS_CONCURRENCY, (entry) => scanForUserScripts(joinPath(absolutePath, entry.name), depth + 1, prefix + entry.name + '/'));
|
|
500
|
+
for (const sub of nested)
|
|
501
|
+
results.push(...sub);
|
|
502
|
+
// Sorted here, so the concurrent walk above can finish in any order
|
|
503
|
+
// without changing what the caller sees.
|
|
504
|
+
results.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
505
|
+
return results;
|
|
506
|
+
}
|
|
507
|
+
// ---------- Per-collection-kind scanners ----------
|
|
508
|
+
// Scheme-aware text fetcher used for the manifest body. Per-entry
|
|
509
|
+
// fetches (with their own per-scheme dispatch and conditional-GET
|
|
510
|
+
// handling) live in script-store.ts; this helper only needs to read
|
|
511
|
+
// a body, not negotiate freshness.
|
|
512
|
+
async function fetchManifestText(canonicalUrl) {
|
|
513
|
+
if (isHttpUrl(canonicalUrl)) {
|
|
514
|
+
const fetched = await fetchHttpText(canonicalUrl, { expects: 'JSON' });
|
|
515
|
+
if (fetched.kind !== 'ok') {
|
|
516
|
+
// Unreachable in practice: this call sends no conditional-GET
|
|
517
|
+
// headers, so `fetchHttpText` can only answer `ok` or throw.
|
|
518
|
+
throw new Error(`Internal error: unexpected result reading the manifest (${fetched.kind})`);
|
|
519
|
+
}
|
|
520
|
+
return fetched.text;
|
|
521
|
+
}
|
|
522
|
+
// file:// URLs (the only other scheme we accept here — pathToFileUrl
|
|
523
|
+
// wrapped any raw path before reaching us).
|
|
524
|
+
return await fetchFileUrlText(canonicalUrl);
|
|
525
|
+
}
|
|
526
|
+
// Fetch the manifest and turn each listed entry into a FoundScript.
|
|
527
|
+
// Works for any manifest scheme — http(s):// or file:// — because both
|
|
528
|
+
// share `new URL(entry, base)` resolution semantics and the only piece
|
|
529
|
+
// that differs is the manifest-body fetch.
|
|
530
|
+
//
|
|
531
|
+
// `relativePath` stores the raw entry (typically a bare filename for
|
|
532
|
+
// relative cases) so it survives across refreshes for stable
|
|
533
|
+
// per-script IDs; the per-entry fetcher resolves it back to an
|
|
534
|
+
// absolute URL via scriptFileUrl. Duplicate entries collapse to the
|
|
535
|
+
// first occurrence — the manifest is the user's source of truth, so
|
|
536
|
+
// we treat dupes as a harmless typo rather than an error.
|
|
537
|
+
//
|
|
538
|
+
// The manifest body itself is re-downloaded every refresh (no
|
|
539
|
+
// If-None-Match round-trip on the manifest URL). The per-script fetches
|
|
540
|
+
// still 304 individually, so the bandwidth cost is bounded. See TODO.md
|
|
541
|
+
// "HTTP collections — follow-ups" for the planned fix. Schemes a
|
|
542
|
+
// manifest entry's *resolved* URL is allowed to use. We mirror the
|
|
543
|
+
// suffix-test policy on the collection URL itself (which only accepts
|
|
544
|
+
// http(s) / file: / raw path): a manifest the user added shouldn't be
|
|
545
|
+
// able to escalate the SW's `<all_urls>` reach into things like `data:`
|
|
546
|
+
// (arbitrary inline content), `chrome-extension:` (cross-extension
|
|
547
|
+
// probing), or `blob:` (off-origin smuggling). The user can always add
|
|
548
|
+
// such collections directly if they want them.
|
|
549
|
+
const ALLOWED_ENTRY_SCHEMES = new Set(['http:', 'https:', 'file:']);
|
|
550
|
+
// Resolve a manifest entry against the manifest's canonical URL.
|
|
551
|
+
//
|
|
552
|
+
// On top of plain RFC 3986 resolution, if the entry doesn't bring its
|
|
553
|
+
// own query string and lands in the same origin as the manifest, the
|
|
554
|
+
// manifest's query string is propagated onto the resolved URL. This
|
|
555
|
+
// lets a single auth-bearing manifest URL (e.g. GitHub's
|
|
556
|
+
// `…/script_manifest.json?token=…`) reach its companion scripts
|
|
557
|
+
// without each manifest entry having to repeat the token. Cross-origin
|
|
558
|
+
// or self-querying entries are left alone — the propagation rule only
|
|
559
|
+
// fires when the entry is clearly "another file alongside the
|
|
560
|
+
// manifest". Throws on a malformed entry; the caller layers
|
|
561
|
+
// scheme-gating on top.
|
|
562
|
+
export function resolveManifestEntryUrl(entry, canonicalUrl) {
|
|
563
|
+
const base = new URL(canonicalUrl);
|
|
564
|
+
let resolved;
|
|
565
|
+
try {
|
|
566
|
+
resolved = new URL(entry, canonicalUrl);
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
throw new Error(`Manifest entry is not a valid URL: ${entry}`);
|
|
570
|
+
}
|
|
571
|
+
if (!resolved.search && base.search && resolved.origin === base.origin) {
|
|
572
|
+
resolved.search = base.search;
|
|
573
|
+
}
|
|
574
|
+
return resolved;
|
|
575
|
+
}
|
|
576
|
+
async function scanManifest(canonicalUrl,
|
|
577
|
+
// One listing per folder the entries sit in, for the stat below.
|
|
578
|
+
listings = new Map()) {
|
|
579
|
+
const body = await fetchManifestText(canonicalUrl);
|
|
580
|
+
const rawEntries = parseManifest(body);
|
|
581
|
+
const seen = new Set();
|
|
582
|
+
const results = [];
|
|
583
|
+
for (const entry of rawEntries) {
|
|
584
|
+
if (seen.has(entry))
|
|
585
|
+
continue;
|
|
586
|
+
seen.add(entry);
|
|
587
|
+
// Resolve once: validates the URL, gates the scheme, and gives us a
|
|
588
|
+
// friendly display name. The actual fetch URL is rebuilt at refresh
|
|
589
|
+
// time via scriptFileUrl from collectionCanonicalUrl + entry.
|
|
590
|
+
const resolved = resolveManifestEntryUrl(entry, canonicalUrl);
|
|
591
|
+
if (!ALLOWED_ENTRY_SCHEMES.has(resolved.protocol)) {
|
|
592
|
+
throw new Error(`Manifest entry scheme ${resolved.protocol} is not allowed (use http, https, or file)`);
|
|
593
|
+
}
|
|
594
|
+
// Use urlBaseName on the resolved URL so percent-encoded segments
|
|
595
|
+
// come out human-readable, mirroring scanSingleFile's HTTP path.
|
|
596
|
+
const name = urlBaseName(resolved.href) || entry;
|
|
597
|
+
const found = { name, relativePath: entry };
|
|
598
|
+
// A `file://` entry gets the same listing stat a directory scan
|
|
599
|
+
// gives its files, so the refresh can skip an unchanged one and a
|
|
600
|
+
// page can tell an edited one (file-changes.ts). Best effort: an
|
|
601
|
+
// entry whose folder won't list just stays always-fetch.
|
|
602
|
+
if (resolved.protocol === 'file:') {
|
|
603
|
+
const stat = await statFileByListing(locationFsPath(resolved.href), listings);
|
|
604
|
+
if (stat?.size !== undefined)
|
|
605
|
+
found.size = stat.size;
|
|
606
|
+
if (stat?.mtime !== undefined)
|
|
607
|
+
found.mtime = stat.mtime;
|
|
608
|
+
}
|
|
609
|
+
results.push(found);
|
|
610
|
+
}
|
|
611
|
+
return results;
|
|
612
|
+
}
|
|
613
|
+
// The listing entry for one file by absolute path — its parent folder
|
|
614
|
+
// listed once per `cache` — or null when it isn't listed or the folder
|
|
615
|
+
// can't be read.
|
|
616
|
+
export async function statFileByListing(absolutePath, cache) {
|
|
617
|
+
try {
|
|
618
|
+
return await statFromListing(parentDir(absolutePath), baseName(absolutePath), cache);
|
|
619
|
+
}
|
|
620
|
+
catch {
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
// Build the fetchable URL for a specific scanned script. One branch per
|
|
625
|
+
// collection-kind (container, manifest, github-dir, directory); each is
|
|
626
|
+
// scheme-agnostic and works for raw paths, file:// URLs, and http(s)
|
|
627
|
+
// URLs uniformly via `collectionCanonicalUrl` / `locationFsPath`.
|
|
628
|
+
//
|
|
629
|
+
// Why directory uses path-joining rather than URL resolution: the
|
|
630
|
+
// `relativePath` for a local directory comes from Chrome's listing
|
|
631
|
+
// verbatim (e.g. `weird?name.user.js`), and URL resolution would treat
|
|
632
|
+
// `?...` as a query string. `pathToFileUrl` percent-encodes per
|
|
633
|
+
// segment, which survives those characters correctly. Manifest entries
|
|
634
|
+
// are user-authored and expected to already be properly URL-encoded
|
|
635
|
+
// when they contain special characters.
|
|
636
|
+
export function scriptFileUrl(collectionPath, relativePath) {
|
|
637
|
+
if (isContainerCollection(collectionPath)) {
|
|
638
|
+
// Container scripts carry the full URL/path the user added in
|
|
639
|
+
// their `relativePath`. Coerce raw fs paths to file:// URLs so the
|
|
640
|
+
// downstream fetcher sees a uniform URL form.
|
|
641
|
+
if (isHttpUrl(relativePath) || isFileUrl(relativePath) || isScratchUrl(relativePath))
|
|
642
|
+
return relativePath;
|
|
643
|
+
return pathToFileUrl(relativePath);
|
|
644
|
+
}
|
|
645
|
+
if (isManifestCollection(collectionPath)) {
|
|
646
|
+
return resolveManifestEntryUrl(relativePath, collectionCanonicalUrl(collectionPath)).href;
|
|
647
|
+
}
|
|
648
|
+
if (isGithubDirCollection(collectionPath)) {
|
|
649
|
+
return githubDirScriptFileUrl(collectionPath, relativePath);
|
|
650
|
+
}
|
|
651
|
+
// Local directory.
|
|
652
|
+
return pathToFileUrl(joinPath(locationFsPath(collectionPath), relativePath));
|
|
653
|
+
}
|
|
654
|
+
// Human-displayable location for a specific scanned script. Three
|
|
655
|
+
// distinct shapes:
|
|
656
|
+
// - file:// → bare fs path (more readable than the percent-encoded
|
|
657
|
+
// URL form).
|
|
658
|
+
// - github (github-dir collection, github-manifest collection, or
|
|
659
|
+
// any github URL added as a script inside the github container) →
|
|
660
|
+
// `github.com/<o>/<r>/blob/<ref>/<path>` page URL. The raw URL is
|
|
661
|
+
// what we fetch from; the blob URL is what a human would open in
|
|
662
|
+
// a browser.
|
|
663
|
+
// - non-github http(s):// → URL verbatim.
|
|
664
|
+
// This is what the viewer's Location row shows for every collection kind
|
|
665
|
+
// without each collection-kind needing its own special case.
|
|
666
|
+
export function scriptAbsolutePath(collectionPath, relativePath) {
|
|
667
|
+
if (isGithubDirCollection(collectionPath)) {
|
|
668
|
+
return githubDirScriptDisplayUrl(collectionPath, relativePath);
|
|
669
|
+
}
|
|
670
|
+
const url = scriptFileUrl(collectionPath, relativePath);
|
|
671
|
+
// GitHub manifest entries and github-container scripts are stored as
|
|
672
|
+
// raw URLs (after `normalizeGithubInput`). Convert back to the github
|
|
673
|
+
// page URL so the Location row matches a github-dir collection's
|
|
674
|
+
// display.
|
|
675
|
+
const blobUrl = githubRawToBlobUrl(url);
|
|
676
|
+
if (blobUrl)
|
|
677
|
+
return blobUrl;
|
|
678
|
+
return isFileUrl(url) ? locationFsPath(url) : url;
|
|
679
|
+
}
|