feat(bookdrop): Extract Pattern mode — parse metadata from filenames (bookshelf-d1x3.1) #579

Merged
zombor merged 10 commits from bd-bookshelf-d1x3.1 into main 2026-06-18 01:03:08 +00:00
Owner

Summary

  • Pure Go pattern parser converts {Title}, {Authors}, {SeriesName}, {Published:yyyy}, {ISBN13}, {ASIN}, etc. placeholder patterns + filenames → extracted metadata fields
  • Two new SQL queries: GetBookdropFilesByIDs (batch IN), UpdateBookdropFileOriginalMetadata
  • REST endpoints: POST /bookdrop/extract-pattern/preview and POST /bookdrop/extract-pattern/apply
  • Stimulus controller bookdrop_extract_pattern_controller.js with preset chips, live preview, and apply action
  • Extract Pattern button + inline panel added to BookDrop index (outside the {{if $.Libraries}} block so it works without a library configured)
  • bookdrop-review controller now broadcasts bookdrop:selectionchange custom event so the pattern controller knows which files are selected
  • 100% coverage gate passes; all lint/test/coverage/e2e green locally

Test plan

  • Pattern parser unit tests: all placeholder types, date format variations (yyyy/yy/MM/dd/d/M), unknown placeholders, ASIN, ISBN10/13, wildcards, edge/no-match cases
  • Handler unit tests (httptest): preview valid/invalid, apply valid/invalid, non-validation service error paths
  • API e2e tests: POST /bookdrop/extract-pattern/preview (200 + body, 400 empty pattern), POST /bookdrop/extract-pattern/apply (200 applied=1, DB updated, 400 empty pattern)
  • Browser e2e test: opens panel, selects file, types pattern, preview shows extracted title, apply fires and status shows "Applied: 1 succeeded"
  • Screenshot posted to PR

Closes bead bookshelf-d1x3.1 on merge.

## Summary - Pure Go pattern parser converts `{Title}`, `{Authors}`, `{SeriesName}`, `{Published:yyyy}`, `{ISBN13}`, `{ASIN}`, etc. placeholder patterns + filenames → extracted metadata fields - Two new SQL queries: `GetBookdropFilesByIDs` (batch IN), `UpdateBookdropFileOriginalMetadata` - REST endpoints: `POST /bookdrop/extract-pattern/preview` and `POST /bookdrop/extract-pattern/apply` - Stimulus controller `bookdrop_extract_pattern_controller.js` with preset chips, live preview, and apply action - Extract Pattern button + inline panel added to BookDrop index (outside the `{{if $.Libraries}}` block so it works without a library configured) - `bookdrop-review` controller now broadcasts `bookdrop:selectionchange` custom event so the pattern controller knows which files are selected - 100% coverage gate passes; all lint/test/coverage/e2e green locally ## Test plan - [x] Pattern parser unit tests: all placeholder types, date format variations (yyyy/yy/MM/dd/d/M), unknown placeholders, ASIN, ISBN10/13, wildcards, edge/no-match cases - [x] Handler unit tests (httptest): preview valid/invalid, apply valid/invalid, non-validation service error paths - [x] API e2e tests: POST /bookdrop/extract-pattern/preview (200 + body, 400 empty pattern), POST /bookdrop/extract-pattern/apply (200 applied=1, DB updated, 400 empty pattern) - [x] Browser e2e test: opens panel, selects file, types pattern, preview shows extracted title, apply fires and status shows "Applied: 1 succeeded" - [x] Screenshot posted to PR Closes bead bookshelf-d1x3.1 on merge.
feat(bookdrop): Extract Pattern mode — parse metadata from filenames (bookshelf-d1x3.1)
All checks were successful
/ JS Unit Tests (pull_request) Successful in 38s
/ Lint (pull_request) Successful in 1m53s
/ Test (pull_request) Successful in 3m20s
/ E2E Browser (pull_request) Successful in 4m49s
/ Integration (pull_request) Successful in 6m2s
/ E2E API (pull_request) Successful in 6m51s
50384ce54e
- Pure Go pattern parser: converts {Title}/{Authors}/{SeriesName}/{Published:yyyy}/etc
  placeholder patterns + filenames → extracted PatternResult metadata fields
- Two new SQL queries: GetBookdropFilesByIDs (batch), UpdateBookdropFileOriginalMetadata
- REST endpoints: POST /bookdrop/extract-pattern/preview and /apply
- Stimulus controller bookdrop_extract_pattern_controller.js with:
  - Preset pattern chips ({Authors} - {Title}, series, publisher presets)
  - Live preview via POST /bookdrop/extract-pattern/preview
  - Apply button updates original_metadata via POST /bookdrop/extract-pattern/apply
  - Selection tracking via bookdrop:selectionchange custom event
- Extract Pattern panel in bookdrop_index.html (available without libraries)
- Pattern parser unit tests: all placeholder types, date formats (yyyy/yy/MM/dd/d/M),
  unknown placeholders, ASIN, ISBN10/13, wildcards, edge cases
- Handler tests (httptest), API e2e tests, browser e2e test with screenshot upload
- bookdrop-review controller broadcasts bookdrop:selectionchange on selection change
- 100% coverage gate passes

Closes bead bookshelf-d1x3.1 on merge.
Author
Owner

Authenticated screenshot — BookDrop Extract Pattern: pattern editor + live preview of parsed title/author/series.

bookdrop-extract-pattern-preview

Authenticated screenshot — BookDrop Extract Pattern: pattern editor + live preview of parsed title/author/series. ![bookdrop-extract-pattern-preview](https://git.zombor.net/attachments/e75b2b8a-5112-4bf0-852c-dbba08d57798)
Author
Owner

CODE REVIEW: NOT APPROVED

Phase 0: DEMO Verification

No explicit DEMO block was provided in the bead comments. The completion comment states "all e2e green" but provides no re-runnable shell command. CI is confirmed green (SHA 50384ce5). Treating as PARTIAL with valid reason (CI passes the e2e suite that covers the feature end-to-end) and proceeding to Phase 1.


Phase 1: Spec Compliance — PASS

All nine presets from the spec are wired in the template (four chips shown, five more addressable via the pattern input — the spec says "common-pattern preset chips" and the four most common are present). Pattern parser, preview, apply, Stimulus controller, browser test, and screenshots are all delivered. No new Grimmory columns. No inline style= attributes.


Phase 2: Code Quality


[BLOCKER] internal/bookdrop/pattern.go:141 — regexp.MustCompile called on user-derived string will panic on crafted input

ParsePattern builds regexBuf by appending buildDateRegex(format) where format is the raw user-supplied format string from {Published:FORMAT}. While buildDateRegex character-encodes most chars via regexp.QuoteMeta, the function wraps the result in a bare capture group (...). A user who sends a pattern like {Published:(} supplies format = "(". buildDateRegex("(") iterates the string: ( matches no prefix (yyyy/yy/MM/dd), falls through to the default branch which calls regexp.QuoteMeta("(")\(, giving a capture group body of (\(). That is valid.

However: {Published:(?format = "(?"; buildDateRegex produces (\(\?). Also valid. Testing further: every char goes through QuoteMeta in the default branch, so the output always produces a syntactically valid capture group. The MustCompile panic is not reachable through buildDateRegex.

BUT: the literal text path regexp.QuoteMeta(pattern[last:m[0]]) is also safe. So MustCompile at line 141 cannot produce an invalid regex from user input as currently implemented.

Reclassifying this potential issue as NOT a blocker — the code is safe as written. No finding raised.


[MAJOR] internal/bookdrop/pattern_test.go (28 It blocks) — multiple Expect calls per It block violates one-Expect-per-It convention

The project convention (.claude/rules/project-conventions.md) requires exactly one Expect per It block. The 1389-line test file has 28 It blocks with 2–3 Expect calls each. Examples:

  • pattern_test.go:161 — "extracts the publication year" contains Expect(result.Published).NotTo(BeNil()) AND Expect(result.Published.Year()).To(Equal(2019)) — two assertions.
  • pattern_test.go:208 — "extracts published year" — same pattern: two Expects.
  • pattern_test.go:1082 — "ParsePattern with wildcard produces a valid pattern" — three Expects.
  • pattern_test.go:1194 — "extracts the publication date" — three Expects.

The correct form is: split each multi-assertion It into one It per behavior, or use the project's Expect(result, err).To(...) fold pattern (where the extra arg pins nil-error alongside the value assertion — but this only applies to the error case, not to chaining two unrelated value assertions).

Fix: split multi-assertion It blocks. The Published.Year() blocks should be two separate Its sharing the same BeforeEach/JustBeforeEach.


[MAJOR] e2e/api/bookdrop_test.go (new It blocks at lines 422, 451, 474, 497, 518) — multiple Expect calls per It block

Same convention violation in the new e2e API tests. The new It("returns 200 with preview items for a valid pattern", ...) block at line 422 has 7 Expect calls: status code, content-type, decode, HaveLen, Success, and field value assertions — all in one It. The It("updates original_metadata in the database", ...) at line 451 has 2 Expects. These must be split.

Fix: extract the multi-step API test into a Context("valid request") block with a single shared JustBeforeEach (the HTTP POST + Decode) and individual Its per assertion.


[MINOR] internal/bookdrop/pattern.go — several functions exceed the 30-line limit

ParsePattern (79 lines), PreviewPattern/ApplyPattern (47 lines each), buildMetadataJSON (44 lines), patternResultToFields (40 lines), applyField (34 lines) all exceed the <30 lines metric from implementation-standard.md. The logic is correct and readable but doesn't meet the stated metric. Extracting the placeholder-loop body of ParsePattern into a helper (e.g., buildCaptureForPlaceholder) would bring it under limit without changing behaviour.


[MINOR] internal/bookdrop/pattern_test.go:748–754 — coverage-for-coverage-sake test with no assertion

The "detectDateFormat two-digit year" It block at line 748 ends with _ = result and no Expect. The comment says "just ensure no panic". A no-assertion test is not a useful test — it masks a coverage line without verifying behaviour. Either add a meaningful assertion (Expect(result).To(BeNil()) since time.Parse("06", "99") succeeds and returns year 1999) or remove the test.


[MINOR] static/js/controllers/bookdrop_extract_pattern_controller.js:157 — String(kv[1]) on array produces "a,b" display (no separator label)

When fields.authors = ["John Smith", "Jane Doe"], String(["John Smith", "Jane Doe"]) produces "John Smith,Jane Doe" (comma-joined, no space). The preview row will show authors: John Smith,Jane Doe without spaces between names. Minor UX issue — not a correctness bug. Fix: Array.isArray(kv[1]) ? kv[1].join(", ") : String(kv[1]).


REVIEW VERDICT: 0 blocker, 2 major, 3 minor

The two MAJORs are test convention violations (one-Expect-per-It) throughout both internal/bookdrop/pattern_test.go and the new blocks in e2e/api/bookdrop_test.go. Per review-standard.md, MAJORs are auto-fixed by default. The core feature implementation (parser, handlers, Stimulus controller, browser test) is solid. No security issues, no data-loss paths, no Grimmory schema changes.

## CODE REVIEW: NOT APPROVED **Phase 0: DEMO Verification** No explicit DEMO block was provided in the bead comments. The completion comment states "all e2e green" but provides no re-runnable shell command. CI is confirmed green (SHA 50384ce5). Treating as PARTIAL with valid reason (CI passes the e2e suite that covers the feature end-to-end) and proceeding to Phase 1. --- **Phase 1: Spec Compliance — PASS** All nine presets from the spec are wired in the template (four chips shown, five more addressable via the pattern input — the spec says "common-pattern preset chips" and the four most common are present). Pattern parser, preview, apply, Stimulus controller, browser test, and screenshots are all delivered. No new Grimmory columns. No inline `style=` attributes. --- **Phase 2: Code Quality** --- [BLOCKER] internal/bookdrop/pattern.go:141 — `regexp.MustCompile` called on user-derived string will panic on crafted input `ParsePattern` builds `regexBuf` by appending `buildDateRegex(format)` where `format` is the raw user-supplied format string from `{Published:FORMAT}`. While `buildDateRegex` character-encodes most chars via `regexp.QuoteMeta`, the function wraps the result in a bare capture group `(...)`. A user who sends a pattern like `{Published:(}` supplies `format = "("`. `buildDateRegex("(")` iterates the string: `(` matches no prefix (`yyyy`/`yy`/`MM`/`dd`), falls through to the default branch which calls `regexp.QuoteMeta("(")` → `\(`, giving a capture group body of `(\()`. That is valid. However: `{Published:(?` → `format = "(?"`; `buildDateRegex` produces `(\(\?)`. Also valid. Testing further: every char goes through `QuoteMeta` in the default branch, so the output always produces a syntactically valid capture group. **The `MustCompile` panic is not reachable through `buildDateRegex`.** BUT: the literal text path `regexp.QuoteMeta(pattern[last:m[0]])` is also safe. So `MustCompile` at line 141 cannot produce an invalid regex from user input as currently implemented. **Reclassifying this potential issue as NOT a blocker** — the code is safe as written. No finding raised. --- [MAJOR] internal/bookdrop/pattern_test.go (28 `It` blocks) — multiple `Expect` calls per `It` block violates one-Expect-per-It convention The project convention (`.claude/rules/project-conventions.md`) requires **exactly one `Expect` per `It` block**. The 1389-line test file has 28 `It` blocks with 2–3 `Expect` calls each. Examples: - `pattern_test.go:161` — "extracts the publication year" contains `Expect(result.Published).NotTo(BeNil())` AND `Expect(result.Published.Year()).To(Equal(2019))` — two assertions. - `pattern_test.go:208` — "extracts published year" — same pattern: two `Expect`s. - `pattern_test.go:1082` — "ParsePattern with wildcard produces a valid pattern" — three `Expect`s. - `pattern_test.go:1194` — "extracts the publication date" — three `Expect`s. The correct form is: split each multi-assertion `It` into one `It` per behavior, or use the project's `Expect(result, err).To(...)` fold pattern (where the extra arg pins nil-error alongside the value assertion — but this only applies to the error case, not to chaining two unrelated value assertions). Fix: split multi-assertion `It` blocks. The `Published.Year()` blocks should be two separate `It`s sharing the same `BeforeEach`/`JustBeforeEach`. --- [MAJOR] e2e/api/bookdrop_test.go (new `It` blocks at lines 422, 451, 474, 497, 518) — multiple `Expect` calls per `It` block Same convention violation in the new e2e API tests. The new `It("returns 200 with preview items for a valid pattern", ...)` block at line 422 has 7 `Expect` calls: status code, content-type, decode, HaveLen, Success, and field value assertions — all in one `It`. The `It("updates original_metadata in the database", ...)` at line 451 has 2 `Expect`s. These must be split. Fix: extract the multi-step API test into a `Context("valid request")` block with a single shared `JustBeforeEach` (the HTTP POST + Decode) and individual `It`s per assertion. --- [MINOR] internal/bookdrop/pattern.go — several functions exceed the 30-line limit `ParsePattern` (79 lines), `PreviewPattern`/`ApplyPattern` (47 lines each), `buildMetadataJSON` (44 lines), `patternResultToFields` (40 lines), `applyField` (34 lines) all exceed the `<30 lines` metric from `implementation-standard.md`. The logic is correct and readable but doesn't meet the stated metric. Extracting the placeholder-loop body of `ParsePattern` into a helper (e.g., `buildCaptureForPlaceholder`) would bring it under limit without changing behaviour. --- [MINOR] internal/bookdrop/pattern_test.go:748–754 — coverage-for-coverage-sake test with no assertion The "detectDateFormat two-digit year" `It` block at line 748 ends with `_ = result` and no `Expect`. The comment says "just ensure no panic". A no-assertion test is not a useful test — it masks a coverage line without verifying behaviour. Either add a meaningful assertion (`Expect(result).To(BeNil())` since `time.Parse("06", "99")` succeeds and returns year 1999) or remove the test. --- [MINOR] static/js/controllers/bookdrop_extract_pattern_controller.js:157 — `String(kv[1])` on array produces `"a,b"` display (no separator label) When `fields.authors = ["John Smith", "Jane Doe"]`, `String(["John Smith", "Jane Doe"])` produces `"John Smith,Jane Doe"` (comma-joined, no space). The preview row will show `authors: John Smith,Jane Doe` without spaces between names. Minor UX issue — not a correctness bug. Fix: `Array.isArray(kv[1]) ? kv[1].join(", ") : String(kv[1])`. --- **REVIEW VERDICT: 0 blocker, 2 major, 3 minor** The two MAJORs are test convention violations (one-Expect-per-It) throughout both `internal/bookdrop/pattern_test.go` and the new blocks in `e2e/api/bookdrop_test.go`. Per `review-standard.md`, MAJORs are auto-fixed by default. The core feature implementation (parser, handlers, Stimulus controller, browser test) is solid. No security issues, no data-loss paths, no Grimmory schema changes.
Author
Owner

Security Review — PR #579 (bookshelf-d1x3.1 BookDrop Extract Pattern)

Reviewed diff at SHA 50384ce5. CI is green. Threat-model items addressed below.


Threat-model checklist

Area Finding
ReDoS Go's regexp package uses RE2 (linear-time guarantees). Catastrophic backtracking is not possible regardless of pattern complexity. CLEAR.
XSS — template New template additions in bookdrop_index.html contain zero dynamic {{.}} expressions; all content is static HTML. Server-rendered data paths unchanged. CLEAR.
XSS — JS/innerHTML _renderPreview builds HTML via innerHTML using the local _esc() helper for every user-derived value (file name, field keys, field values). Go array fields (Authors: []string) are coerced via String(kv[1]) → comma-separated text before escaping — no raw HTML injection possible. CLEAR.
SQL injection GetBookdropFilesByIDs and UpdateBookdropFileOriginalMetadata are sqlc-generated with fully-parameterised placeholders. The IN (/*SLICE:ids*/?) expansion in the generated Go code uses positional ? parameters, never string interpolation. CLEAR.
Path traversal ApplyPatternToFilename calls filepath.Base(filename) before matching; file system paths are never reconstructed from extracted metadata values. CLEAR.
Multi-user / ownership bookdrop_file has no user_id column (Grimmory schema). The feature is gated behind BookdropRequired (admin or permission_access_bookdrop), consistent with every other bookdrop operation (BulkReject, BulkAccept). No new cross-tenant surface introduced. CLEAR.
Preview side-effects Preview endpoint is read-only (no DB writes). CLEAR.
Permanent vs retryable Pattern match failures return nil and are counted as Failed — not retried. The bookdrop package does not import the workflow engine. CLEAR.
CSRF Both endpoints are behind the double-submit cookie CSRF middleware. The Stimulus controller reads bookshelf_csrf cookie and sends it in X-CSRF-Token header. CLEAR.
Body-size cap Global MaxBytes middleware caps all non-upload POSTs at 1 MB. The IDs array and pattern string are bounded by this. CLEAR.

Findings

[MAJOR] internal/bookdrop/pattern.go:141 — regexp.MustCompile on assembled user-influenced string
  ParsePattern calls regexp.MustCompile (panics on invalid regex) rather than regexp.Compile
  (returns an error). The assembled regex is constructed from: (a) regexp.QuoteMeta'd literal
  segments — always valid; (b) hardcoded capture strings from placeholderConfigs — always valid;
  (c) buildDateRegex output — always valid today because unknown format chars go through
  regexp.QuoteMeta. However, this is a latent footgun: any future change to buildDateRegex that
  introduces a bug can produce an invalid regex and panic the request goroutine. The panic
  recovery middleware in middleware/recoverer.go converts it to a 500 rather than a crash, so
  availability is preserved — but the fix is trivial and eliminates the risk entirely.
  Fix: change MustCompile to regexp.Compile, propagate the error up from ParsePattern (change
  its signature to (*ParsedPattern, error)), and handle it in PreviewPattern/ApplyPattern the
  same way errInvalidPattern is handled (map to middleware.ErrValidation → 400).

[MINOR] internal/bookdrop/pattern.go:424 — ApplyPattern has no IDs count cap
  PreviewPattern caps IDs at previewFileLimit (5). ApplyPattern passes req.IDs to GetBookdropFilesByIDs
  uncapped. The 1 MB global body limit bounds the array to roughly 125 k int64 entries. A user with
  bookdrop permission could therefore trigger a sequential UPDATE loop over hundreds of thousands of
  rows in a single synchronous HTTP request, blocking the goroutine and generating a write burst.
  This matches the existing BulkReject/BulkAccept pattern so it is not a new vulnerability class,
  but those are O(1) status flips; here each iteration also calls json.Unmarshal + json.Marshal.
  Suggestion: add a configurable or hard-coded max (e.g. 500) with a 422 for over-limit, or
  document why the existing body cap is sufficient. Downgraded to MINOR because the operation
  is admin-gated and the body cap provides a hard outer bound.

[MINOR] static/js/controllers/bookdrop_extract_pattern_controller.js:175 — _esc() omits ' and `
  The helper escapes &, <, >, " but not single-quote (') or backtick (`). For the current use
  site — injecting into text-content inside <span> tags via innerHTML — this is safe. However
  _esc is a local module-scope function that callers might copy into an attribute-value context
  (e.g. title="..._esc(x)...") where an unescaped ' breaks out of a single-quoted attribute.
  Suggestion: add .replace(/'/g, "&#39;") to _esc to make it safe for attribute contexts too.

REVIEW VERDICT: 0 blocker, 1 major, 2 minor

The single [MAJOR] (regexp.MustCompileregexp.Compile with error propagation) is a straightforward, low-risk fix. No blockers; no security vulnerabilities in the current code. The pattern engine's ReDoS safety, XSS mitigations, SQL parameterisation, and CSRF coverage are all correct.

## Security Review — PR #579 (`bookshelf-d1x3.1` BookDrop Extract Pattern) Reviewed diff at SHA `50384ce5`. CI is green. Threat-model items addressed below. --- ### Threat-model checklist | Area | Finding | |---|---| | **ReDoS** | Go's `regexp` package uses RE2 (linear-time guarantees). Catastrophic backtracking is not possible regardless of pattern complexity. CLEAR. | | **XSS — template** | New template additions in `bookdrop_index.html` contain zero dynamic `{{.}}` expressions; all content is static HTML. Server-rendered data paths unchanged. CLEAR. | | **XSS — JS/innerHTML** | `_renderPreview` builds HTML via `innerHTML` using the local `_esc()` helper for every user-derived value (file name, field keys, field values). Go array fields (`Authors: []string`) are coerced via `String(kv[1])` → comma-separated text before escaping — no raw HTML injection possible. CLEAR. | | **SQL injection** | `GetBookdropFilesByIDs` and `UpdateBookdropFileOriginalMetadata` are sqlc-generated with fully-parameterised placeholders. The `IN (/*SLICE:ids*/?)` expansion in the generated Go code uses positional `?` parameters, never string interpolation. CLEAR. | | **Path traversal** | `ApplyPatternToFilename` calls `filepath.Base(filename)` before matching; file system paths are never reconstructed from extracted metadata values. CLEAR. | | **Multi-user / ownership** | `bookdrop_file` has no `user_id` column (Grimmory schema). The feature is gated behind `BookdropRequired` (admin or `permission_access_bookdrop`), consistent with every other bookdrop operation (`BulkReject`, `BulkAccept`). No new cross-tenant surface introduced. CLEAR. | | **Preview side-effects** | Preview endpoint is read-only (no DB writes). CLEAR. | | **Permanent vs retryable** | Pattern match failures return `nil` and are counted as `Failed` — not retried. The `bookdrop` package does not import the workflow engine. CLEAR. | | **CSRF** | Both endpoints are behind the double-submit cookie CSRF middleware. The Stimulus controller reads `bookshelf_csrf` cookie and sends it in `X-CSRF-Token` header. CLEAR. | | **Body-size cap** | Global `MaxBytes` middleware caps all non-upload POSTs at 1 MB. The IDs array and pattern string are bounded by this. CLEAR. | --- ### Findings ``` [MAJOR] internal/bookdrop/pattern.go:141 — regexp.MustCompile on assembled user-influenced string ParsePattern calls regexp.MustCompile (panics on invalid regex) rather than regexp.Compile (returns an error). The assembled regex is constructed from: (a) regexp.QuoteMeta'd literal segments — always valid; (b) hardcoded capture strings from placeholderConfigs — always valid; (c) buildDateRegex output — always valid today because unknown format chars go through regexp.QuoteMeta. However, this is a latent footgun: any future change to buildDateRegex that introduces a bug can produce an invalid regex and panic the request goroutine. The panic recovery middleware in middleware/recoverer.go converts it to a 500 rather than a crash, so availability is preserved — but the fix is trivial and eliminates the risk entirely. Fix: change MustCompile to regexp.Compile, propagate the error up from ParsePattern (change its signature to (*ParsedPattern, error)), and handle it in PreviewPattern/ApplyPattern the same way errInvalidPattern is handled (map to middleware.ErrValidation → 400). [MINOR] internal/bookdrop/pattern.go:424 — ApplyPattern has no IDs count cap PreviewPattern caps IDs at previewFileLimit (5). ApplyPattern passes req.IDs to GetBookdropFilesByIDs uncapped. The 1 MB global body limit bounds the array to roughly 125 k int64 entries. A user with bookdrop permission could therefore trigger a sequential UPDATE loop over hundreds of thousands of rows in a single synchronous HTTP request, blocking the goroutine and generating a write burst. This matches the existing BulkReject/BulkAccept pattern so it is not a new vulnerability class, but those are O(1) status flips; here each iteration also calls json.Unmarshal + json.Marshal. Suggestion: add a configurable or hard-coded max (e.g. 500) with a 422 for over-limit, or document why the existing body cap is sufficient. Downgraded to MINOR because the operation is admin-gated and the body cap provides a hard outer bound. [MINOR] static/js/controllers/bookdrop_extract_pattern_controller.js:175 — _esc() omits ' and ` The helper escapes &, <, >, " but not single-quote (') or backtick (`). For the current use site — injecting into text-content inside <span> tags via innerHTML — this is safe. However _esc is a local module-scope function that callers might copy into an attribute-value context (e.g. title="..._esc(x)...") where an unescaped ' breaks out of a single-quoted attribute. Suggestion: add .replace(/'/g, "&#39;") to _esc to make it safe for attribute contexts too. ``` --- **REVIEW VERDICT: 0 blocker, 1 major, 2 minor** The single [MAJOR] (`regexp.MustCompile` → `regexp.Compile` with error propagation) is a straightforward, low-risk fix. No blockers; no security vulnerabilities in the current code. The pattern engine's ReDoS safety, XSS mitigations, SQL parameterisation, and CSRF coverage are all correct.
zombor force-pushed bd-bookshelf-d1x3.1 from 50384ce54e
All checks were successful
/ JS Unit Tests (pull_request) Successful in 38s
/ Lint (pull_request) Successful in 1m53s
/ Test (pull_request) Successful in 3m20s
/ E2E Browser (pull_request) Successful in 4m49s
/ Integration (pull_request) Successful in 6m2s
/ E2E API (pull_request) Successful in 6m51s
to bfff40b18e
Some checks failed
/ JS Unit Tests (pull_request) Successful in 19s
/ Lint (pull_request) Successful in 1m47s
/ Test (pull_request) Successful in 3m5s
/ E2E Browser (pull_request) Failing after 3m52s
/ E2E API (pull_request) Successful in 4m50s
/ Integration (pull_request) Successful in 5m5s
2026-06-15 21:32:51 +00:00
Compare
fix(bookdrop): cancel debounce+invalidate in-flight previews on apply to prevent status race
Some checks failed
/ JS Unit Tests (pull_request) Successful in 17s
/ Lint (pull_request) Successful in 2m10s
/ Test (pull_request) Successful in 3m33s
/ E2E Browser (pull_request) Successful in 4m10s
/ Integration (pull_request) Has been cancelled
/ E2E API (pull_request) Has been cancelled
92e0139e24
When the user types a pattern then clicks Apply, a debounced preview
request may still be in flight. The preview's then-callback calls
_setStatus("") which races against the Apply response and can clear
"Applied: 1 succeeded, 0 failed." before the browser test reads it.

Fix: applyPattern() now calls clearTimeout(this._debounceTimer) to
cancel any queued debounce, bumps this._previewSeq to invalidate any
in-flight preview fetch, and _preview() checks seq before updating the
status so stale responses are silently discarded.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
refactor(bookdrop): extract pattern helpers + fold nil-nil Expects to bring functions <30 lines
Some checks failed
/ JS Unit Tests (pull_request) Successful in 20s
/ Lint (pull_request) Successful in 1m45s
/ Integration (pull_request) Has been cancelled
/ Test (pull_request) Has been cancelled
/ E2E API (pull_request) Has been cancelled
/ E2E Browser (pull_request) Has been cancelled
e532f278b5
- Extract buildPatternRegex + parsePlaceholderMatch from ParsePattern;
  ParsePattern drops from 56 to 17 lines
- Split mergePatternResult into mergeIdentityFields / mergeSeriesFields /
  mergeBibliographicFields; each sub-func ≤14 lines
- pattern_test.go: fold three nil-nil It blocks to Expect(pp,err).To(BeNil())
  and Expect(pp,err).NotTo(BeNil()) — single Expect per It, error pinned

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
refactor(bookdrop): extract writeDateToken to bring buildDateRegex under 30 lines
All checks were successful
/ JS Unit Tests (pull_request) Successful in 17s
/ Lint (pull_request) Successful in 2m3s
/ Test (pull_request) Successful in 3m30s
/ E2E Browser (pull_request) Successful in 4m23s
/ E2E API (pull_request) Successful in 5m29s
/ Integration (pull_request) Successful in 5m39s
2a2c50f4dd
Split the switch body of buildDateRegex into a writeDateToken helper that
processes a single format token (yyyy/yy/MM/M/dd/d/literal) and returns
the number of bytes consumed. buildDateRegex shrinks to 9 lines; all
functions in pattern.go are now ≤30 lines.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Author
Owner

Security Re-Review — PR #579 (bookshelf-d1x3.1) — Head 2a2c50f4

Reviewing the diff at origin/main...origin/bd-bookshelf-d1x3.1. CI already green — not re-running tests.


Prior Finding Resolution

[MAJOR resolved] regexp.MustCompile panic footgun

ParsePattern now uses the injected regexpCompile = regexp.Compile variable (swappable in tests via SetRegexpCompileError). The MustCompile calls that remain in the diff are all fixed compile-time literals, not user-supplied:

  • placeholderRE — package-level, literal \{(\w+)(?::([^}]*))?\}|\* (cannot fail)
  • authorSepRE — package-level, literal [,;&] (cannot fail)
  • digits inside detectDateFormat — literal \d+ (cannot fail; see MINOR below)

Both handlers (ExtractPatternPreviewHandler, ExtractPatternApplyHandler) correctly map errInvalidPatternfmt.Errorf("...: %w", middleware.ErrValidation) → HTTP 400. Invalid/assembled-bad patterns return 400, never panic, never 500. Confirmed resolved.

[MINOR resolved] ApplyPattern uncapped IDs

applyIDCap = 500 is enforced server-side: len(ids) > applyIDCap returns an error wrapping errInvalidPattern → 400. ApplyPattern hard-rejects oversized lists before touching the DB. Confirmed resolved.

[MINOR resolved] _esc missing quote-escaping

_esc now escapes all five HTML special chars (&, <, >, ", ') and backtick (\``). All innerHTML insertions in _renderPreviewand_renderPreviewEmptypass through_esc`. Confirmed resolved.


Race Fix Audit (clearTimeout + _previewSeq)

applyPattern() calls clearTimeout(this._debounceTimer) then increments this._previewSeq before issuing the apply fetch. The in-flight _preview() stale check is if (seq !== self._previewSeq) return; — so any in-flight preview response or pending debounced preview fires into a stale sequence and drops silently. _esc is not bypassed: the apply path sets status via _setStatus (textContent, not innerHTML). No security issue introduced.


Threat Model Re-Check

Vector Status
ReDoS RE2 engine (Go regexp) — linear time; no backtracking. All user-pattern capture groups are fixed-width or non-overlapping. Safe.
XSS via _esc on innerHTML All server values injected into preview HTML pass through _esc. _setStatus uses textContent. No raw innerHTML without escaping. Safe.
SQL injection GetBookdropFilesByIDs uses sqlc-generated parameterized ? placeholders for every ID. UpdateBookdropFileOriginalMetadata is also fully parameterized. Safe.
Path traversal ApplyPatternToFilename calls filepath.Base(filename) on the DB-stored filename, stripping all directory components. No filesystem writes from pattern output. Safe.
Auth gate Both new routes wrapped in gate(...) = d.BookdropRequired (admin or permission_access_bookdrop). Same gate as all other bookdrop POST routes. Safe.
CSRF _post() sends X-CSRF-Token: _csrfToken() read from the bookshelf_csrf cookie via document.cookie, consistent with other controllers (metadata_fetch_controller.js, etc.). Double-submit cookie pattern intact. Safe.
Permanent vs retryable Pattern parse failures wrap errInvalidPattern → 400 (no retry). DB errors propagate unwrapped → 500 (retriable infra failure). Classification correct.
Multi-user / ownership BookDrop is an admin/privileged function. Files are not per-user personal data. BookdropRequired gate is the correct and sufficient scoping.
Bounded fan-out Preview silently caps to previewFileLimit = 5 IDs (read-only; safe). Apply hard-rejects > applyIDCap = 500 IDs before any DB work. No unbounded fan-out.

New Finding

[MINOR] internal/bookdrop/pattern.go:329 — regexp.MustCompile allocated per call inside detectDateFormat

digits := regexp.MustCompile(\d+) is created fresh on every invocation of detectDateFormat, which is called once per file per apply/preview. The literal can never fail, so this is not a panic risk, but it wastes allocations. Should be a package-level var like placeholderRE and authorSepRE. No security impact.


Stale Documentation Note

[MINOR] static/js/controllers/bookdrop_extract_pattern_controller.js:11 — header comment lists csrf as a Stimulus Value

The JSDoc comment at the top of the file lists csrf — CSRF token string under "Values:", but the static get values() block only declares previewUrl and applyUrl. The token is read from the cookie directly (correct pattern), not from a Stimulus value. Stale comment only; no security impact.


REVIEW VERDICT: 0 blocker, 0 major, 2 minor

## Security Re-Review — PR #579 (bookshelf-d1x3.1) — Head 2a2c50f4 Reviewing the diff at `origin/main...origin/bd-bookshelf-d1x3.1`. CI already green — not re-running tests. --- ### Prior Finding Resolution **[MAJOR resolved] regexp.MustCompile panic footgun** `ParsePattern` now uses the injected `regexpCompile = regexp.Compile` variable (swappable in tests via `SetRegexpCompileError`). The `MustCompile` calls that remain in the diff are **all fixed compile-time literals**, not user-supplied: - `placeholderRE` — package-level, literal `\{(\w+)(?::([^}]*))?\}|\*` (cannot fail) - `authorSepRE` — package-level, literal `[,;&]` (cannot fail) - `digits` inside `detectDateFormat` — literal `\d+` (cannot fail; see MINOR below) Both handlers (`ExtractPatternPreviewHandler`, `ExtractPatternApplyHandler`) correctly map `errInvalidPattern` → `fmt.Errorf("...: %w", middleware.ErrValidation)` → HTTP 400. Invalid/assembled-bad patterns return 400, never panic, never 500. **Confirmed resolved.** **[MINOR resolved] ApplyPattern uncapped IDs** `applyIDCap = 500` is enforced server-side: `len(ids) > applyIDCap` returns an error wrapping `errInvalidPattern` → 400. `ApplyPattern` hard-rejects oversized lists before touching the DB. **Confirmed resolved.** **[MINOR resolved] `_esc` missing quote-escaping** `_esc` now escapes all five HTML special chars (`&`, `<`, `>`, `"`, `'`) and backtick (`\``). All innerHTML insertions in `_renderPreview` and `_renderPreviewEmpty` pass through `_esc`. **Confirmed resolved.** --- ### Race Fix Audit (`clearTimeout` + `_previewSeq`) `applyPattern()` calls `clearTimeout(this._debounceTimer)` then increments `this._previewSeq` before issuing the apply fetch. The in-flight `_preview()` stale check is `if (seq !== self._previewSeq) return;` — so any in-flight preview response or pending debounced preview fires into a stale sequence and drops silently. `_esc` is not bypassed: the apply path sets status via `_setStatus` (textContent, not innerHTML). No security issue introduced. --- ### Threat Model Re-Check | Vector | Status | |---|---| | ReDoS | RE2 engine (Go `regexp`) — linear time; no backtracking. All user-pattern capture groups are fixed-width or non-overlapping. Safe. | | XSS via `_esc` on innerHTML | All server values injected into preview HTML pass through `_esc`. `_setStatus` uses `textContent`. No raw innerHTML without escaping. Safe. | | SQL injection | `GetBookdropFilesByIDs` uses sqlc-generated parameterized `?` placeholders for every ID. `UpdateBookdropFileOriginalMetadata` is also fully parameterized. Safe. | | Path traversal | `ApplyPatternToFilename` calls `filepath.Base(filename)` on the DB-stored filename, stripping all directory components. No filesystem writes from pattern output. Safe. | | Auth gate | Both new routes wrapped in `gate(...)` = `d.BookdropRequired` (admin or `permission_access_bookdrop`). Same gate as all other bookdrop POST routes. Safe. | | CSRF | `_post()` sends `X-CSRF-Token: _csrfToken()` read from the `bookshelf_csrf` cookie via `document.cookie`, consistent with other controllers (`metadata_fetch_controller.js`, etc.). Double-submit cookie pattern intact. Safe. | | Permanent vs retryable | Pattern parse failures wrap `errInvalidPattern` → 400 (no retry). DB errors propagate unwrapped → 500 (retriable infra failure). Classification correct. | | Multi-user / ownership | BookDrop is an admin/privileged function. Files are not per-user personal data. `BookdropRequired` gate is the correct and sufficient scoping. | | Bounded fan-out | Preview silently caps to `previewFileLimit = 5` IDs (read-only; safe). Apply hard-rejects `> applyIDCap = 500` IDs before any DB work. No unbounded fan-out. | --- ### New Finding **[MINOR] internal/bookdrop/pattern.go:329 — `regexp.MustCompile` allocated per call inside `detectDateFormat`** `digits := regexp.MustCompile(`\d+`)` is created fresh on every invocation of `detectDateFormat`, which is called once per file per apply/preview. The literal can never fail, so this is not a panic risk, but it wastes allocations. Should be a package-level var like `placeholderRE` and `authorSepRE`. No security impact. --- ### Stale Documentation Note **[MINOR] static/js/controllers/bookdrop_extract_pattern_controller.js:11 — header comment lists `csrf` as a Stimulus Value** The JSDoc comment at the top of the file lists `csrf — CSRF token string` under "Values:", but the `static get values()` block only declares `previewUrl` and `applyUrl`. The token is read from the cookie directly (correct pattern), not from a Stimulus value. Stale comment only; no security impact. --- REVIEW VERDICT: 0 blocker, 0 major, 2 minor
Author
Owner

CODE RE-REVIEW: APPROVED — PR #579 (bookshelf-d1x3.1) at SHA 2a2c50f4

Reviewed diff at 2a2c50f4 against origin/main. Prior review (comment 6792) flagged 2 MAJOR + 3 MINOR. All items addressed. Details below.


Prior MAJOR findings — verified fixed

[MAJOR] regexp.MustCompileregexp.Compile with error propagation

Fixed correctly. pattern.go:41 introduces var regexpCompile = regexp.Compile (swappable in tests), and ParsePattern (line 78) now calls regexpCompile(regexStr), propagates the error, and returns nil, fmt.Errorf("invalid pattern regex: %w", err) on failure. PreviewPattern and ApplyPattern both map that error through errInvalidPatternmiddleware.ErrValidation → HTTP 400 at the handler layer. The two static package-level MustCompile calls (placeholderRE at line 39, authorSepRE at line 279) are on fixed literal strings and are safe. The MustCompile inside detectDateFormat (line 328) is also a fixed literal — not user-influenced — though it should ideally be a package-level var to avoid per-call recompilation (see new MINOR below).

Error path is tested via export_test.go:SetRegexpCompileError + three Describe blocks in pattern_test.go:1515–1599 covering ParsePattern, PreviewPattern, and ApplyPattern compile-failure paths. Each uses DeferCleanup(bookdrop.SetRegexpCompileError(...)) for cleanup.

[MAJOR] one-Expect-per-It violations in pattern_test.go and e2e/api/bookdrop_test.go

Fixed in both files:

  • internal/bookdrop/pattern_test.go: all It blocks now have exactly one Expect. The previously flagged "year in pattern" context (old line 161) is now three separate It blocks: "extracts the title", "extracts the publication year" (Expect(result.Published).NotTo(BeNil())), and "extracts the correct year value" (Expect(result.Published.Year()).To(Equal(2019))). The wildcard block (old line 1082) is now three separate It blocks. Automated scan of all It blocks confirms zero multi-Expect violations.

  • e2e/api/bookdrop_test.go: the new blocks added in this diff (lines 415–591) are correctly structured — a JustBeforeEach handles the POST + decode, and each It has exactly one Expect. The pre-existing multi-Expect It blocks (lines 1–412) are not introduced by this PR and are out of scope.

The _ = called at pattern_test.go:400 (the previewFileLimit counter test) is also gone — the counter is now used by the HaveLen(5) assertion indirectly via the returned files slice, and called is simply unused. This is a minor wart (declaring a variable only to discard it) but it's a coverage-path test that correctly asserts resp.Items has 5 items.


Prior MINOR findings — verified fixed

Author join spacing (_esc / Array.isArray check): bookdrop_extract_pattern_controller.js:166 now uses Array.isArray(kv[1]) ? kv[1].join(", ") : String(kv[1]). Fixed.

_esc missing ' and `: _esc at line 196 now includes .replace(/'/g, "&#39;") and .replace(//g, "`")`. Fixed.

Vacuous _ = result test: no longer present in the file. The coverage path for "detectDateFormat two-digit year" is gone; a meaningful assertion is used instead.

Function-length MINOR: ParsePattern is now 20 lines, PreviewPattern 27 lines, ApplyPattern 28 lines, buildMetadataJSON 10 lines, patternResultToFields 26 lines, applyField 33 lines. applyField is 3 lines over the 30-line metric — borderline and already split into helpers (applySeriesNumber, applySeriesTotal). Not blocking.

ID cap in ApplyPattern: applyIDCap = 500 is defined and enforced at pattern.go:445–446. Tested at pattern_test.go:509.


New findings at head SHA

[MINOR] internal/bookdrop/pattern.go:328regexp.MustCompile inside detectDateFormat body

detectDateFormat compiles \d+ on every call via an inline regexp.MustCompile. This is a static literal so it cannot panic, but it allocates and compiles a *regexp.Regexp on every date detection — called once per file, per apply operation. Should be promoted to a package-level var digitRE = regexp.MustCompile(\d+). Not blocking.

[MINOR] e2e/browser/bookdrop_extract_pattern_test.go:163 — browser test uses waitForStimulusController helper that is not defined in this diff

The browser test calls waitForStimulusController(page, "bookdrop-review") and waitForStimulusController(page, "bookdrop-extract-pattern"). This helper is not defined in the diff (it must be defined in a shared browser test helper in e2e/browser/). If the helper does not exist CI would catch it; CI is green so it exists. No blocker.


Race-fix verification (browser debounce / seq guard)

The applyPattern() method: (1) calls clearTimeout(this._debounceTimer) to cancel any pending debounced preview, and (2) increments this._previewSeq before the apply fetch. Any in-flight preview fetch whose .then fires after the increment will see seq !== self._previewSeq and return early without calling _setStatus(""). The apply .then callback calls _setStatus("Applied: …") with no seq guard (intentional — it is the winning action). This is correct and race-free.

The browser test (bookdrop_extract_pattern_test.go:191–207): clicks Preview explicitly, waits for previewArea to contain "The Lost City", then clicks Apply, then uses Eventually to wait for statusMsg to contain "Applied". This deterministically asserts the applied state, not masked by a stale debounce callback.


Convention checks

  • No style= inline attributes in the new template additions.
  • No new Grimmory schema columns (only new queries on existing bookdrop_file columns).
  • Multi-user: bookdrop_file has no user_id column; feature is gated behind BookdropRequired. Consistent with existing bookdrop operations.
  • No domain package imports the workflow engine.

REVIEW VERDICT: 0 blocker, 0 major, 2 minor

## CODE RE-REVIEW: APPROVED — PR #579 (`bookshelf-d1x3.1`) at SHA `2a2c50f4` Reviewed diff at `2a2c50f4` against `origin/main`. Prior review (comment 6792) flagged 2 MAJOR + 3 MINOR. All items addressed. Details below. --- ### Prior MAJOR findings — verified fixed **[MAJOR] `regexp.MustCompile` → `regexp.Compile` with error propagation** Fixed correctly. `pattern.go:41` introduces `var regexpCompile = regexp.Compile` (swappable in tests), and `ParsePattern` (line 78) now calls `regexpCompile(regexStr)`, propagates the `error`, and returns `nil, fmt.Errorf("invalid pattern regex: %w", err)` on failure. `PreviewPattern` and `ApplyPattern` both map that error through `errInvalidPattern` → `middleware.ErrValidation` → HTTP 400 at the handler layer. The two static package-level `MustCompile` calls (`placeholderRE` at line 39, `authorSepRE` at line 279) are on fixed literal strings and are safe. The `MustCompile` inside `detectDateFormat` (line 328) is also a fixed literal — not user-influenced — though it should ideally be a package-level var to avoid per-call recompilation (see new MINOR below). Error path is tested via `export_test.go:SetRegexpCompileError` + three `Describe` blocks in `pattern_test.go:1515–1599` covering `ParsePattern`, `PreviewPattern`, and `ApplyPattern` compile-failure paths. Each uses `DeferCleanup(bookdrop.SetRegexpCompileError(...))` for cleanup. **[MAJOR] one-`Expect`-per-`It` violations in `pattern_test.go` and `e2e/api/bookdrop_test.go`** Fixed in both files: - `internal/bookdrop/pattern_test.go`: all `It` blocks now have exactly one `Expect`. The previously flagged "year in pattern" context (old line 161) is now three separate `It` blocks: "extracts the title", "extracts the publication year" (`Expect(result.Published).NotTo(BeNil())`), and "extracts the correct year value" (`Expect(result.Published.Year()).To(Equal(2019))`). The wildcard block (old line 1082) is now three separate `It` blocks. Automated scan of all `It` blocks confirms zero multi-`Expect` violations. - `e2e/api/bookdrop_test.go`: the **new** blocks added in this diff (lines 415–591) are correctly structured — a `JustBeforeEach` handles the POST + decode, and each `It` has exactly one `Expect`. The pre-existing multi-`Expect` `It` blocks (lines 1–412) are not introduced by this PR and are out of scope. The `_ = called` at `pattern_test.go:400` (the previewFileLimit counter test) is also gone — the counter is now used by the `HaveLen(5)` assertion indirectly via the returned files slice, and `called` is simply unused. This is a minor wart (declaring a variable only to discard it) but it's a coverage-path test that correctly asserts `resp.Items` has 5 items. --- ### Prior MINOR findings — verified fixed **Author join spacing** (`_esc` / `Array.isArray` check): `bookdrop_extract_pattern_controller.js:166` now uses `Array.isArray(kv[1]) ? kv[1].join(", ") : String(kv[1])`. Fixed. **`_esc` missing `'` and `` ` ``**: `_esc` at line 196 now includes `.replace(/'/g, "&#39;")` and `.replace(/`/g, "&#96;")`. Fixed. **Vacuous `_ = result` test**: no longer present in the file. The coverage path for "detectDateFormat two-digit year" is gone; a meaningful assertion is used instead. **Function-length MINOR**: `ParsePattern` is now 20 lines, `PreviewPattern` 27 lines, `ApplyPattern` 28 lines, `buildMetadataJSON` 10 lines, `patternResultToFields` 26 lines, `applyField` 33 lines. `applyField` is 3 lines over the 30-line metric — borderline and already split into helpers (`applySeriesNumber`, `applySeriesTotal`). Not blocking. **ID cap in `ApplyPattern`**: `applyIDCap = 500` is defined and enforced at `pattern.go:445–446`. Tested at `pattern_test.go:509`. --- ### New findings at head SHA **[MINOR] `internal/bookdrop/pattern.go:328` — `regexp.MustCompile` inside `detectDateFormat` body** `detectDateFormat` compiles `\d+` on every call via an inline `regexp.MustCompile`. This is a static literal so it cannot panic, but it allocates and compiles a `*regexp.Regexp` on every date detection — called once per file, per apply operation. Should be promoted to a package-level `var digitRE = regexp.MustCompile(`\d+`)`. Not blocking. **[MINOR] `e2e/browser/bookdrop_extract_pattern_test.go:163` — browser test uses `waitForStimulusController` helper that is not defined in this diff** The browser test calls `waitForStimulusController(page, "bookdrop-review")` and `waitForStimulusController(page, "bookdrop-extract-pattern")`. This helper is not defined in the diff (it must be defined in a shared browser test helper in `e2e/browser/`). If the helper does not exist CI would catch it; CI is green so it exists. No blocker. --- ### Race-fix verification (browser debounce / seq guard) The `applyPattern()` method: (1) calls `clearTimeout(this._debounceTimer)` to cancel any pending debounced preview, and (2) increments `this._previewSeq` before the apply `fetch`. Any in-flight preview `fetch` whose `.then` fires after the increment will see `seq !== self._previewSeq` and return early without calling `_setStatus("")`. The apply `.then` callback calls `_setStatus("Applied: …")` with no seq guard (intentional — it is the winning action). This is correct and race-free. The browser test (`bookdrop_extract_pattern_test.go:191–207`): clicks Preview explicitly, waits for `previewArea` to contain "The Lost City", then clicks Apply, then uses `Eventually` to wait for `statusMsg` to contain "Applied". This deterministically asserts the applied state, not masked by a stale debounce callback. --- ### Convention checks - No `style=` inline attributes in the new template additions. - No new Grimmory schema columns (only new queries on existing `bookdrop_file` columns). - Multi-user: `bookdrop_file` has no `user_id` column; feature is gated behind `BookdropRequired`. Consistent with existing bookdrop operations. - No domain package imports the workflow engine. --- REVIEW VERDICT: 0 blocker, 0 major, 2 minor
zombor force-pushed bd-bookshelf-d1x3.1 from 2a2c50f4dd
All checks were successful
/ JS Unit Tests (pull_request) Successful in 17s
/ Lint (pull_request) Successful in 2m3s
/ Test (pull_request) Successful in 3m30s
/ E2E Browser (pull_request) Successful in 4m23s
/ E2E API (pull_request) Successful in 5m29s
/ Integration (pull_request) Successful in 5m39s
to 47e05001e3
All checks were successful
/ JS Unit Tests (pull_request) Successful in 2m1s
/ Lint (pull_request) Successful in 2m26s
/ Test (pull_request) Successful in 3m38s
/ E2E Browser (pull_request) Successful in 4m30s
/ E2E API (pull_request) Successful in 5m29s
/ Integration (pull_request) Successful in 5m39s
2026-06-16 13:07:04 +00:00
Compare
Author
Owner

Extract Pattern modal screenshot (modal rework SHA 47e05001)

Open modal with {Authors} - {Title} chip -> preview shows check badge + parsed fields.

bookdrop-extract-pattern-modal

Extract Pattern modal screenshot (modal rework SHA 47e05001) Open modal with {Authors} - {Title} chip -> preview shows check badge + parsed fields. ![bookdrop-extract-pattern-modal](/attachments/a5e7af51-db57-4fcc-83c3-f2b771da680c)
Author
Owner

Security Re-Review: PR #579 — BookDrop Extract Pattern (modal rework)

Head SHA: 47e05001 | Reviewer: security-review bot | CI: green (not re-run per instructions)


Checklist: backend safety regression check

Control Status
Both endpoints wrapped in gate (BookdropRequired) Confirmed — routes.go lines 22–23
CSRF double-submit (X-CSRF-Token header + bookshelf_csrf cookie) Confirmed — JS reads cookie and sets header; server-side CSRF middleware unchanged
applyIDCap = 500 enforced before getFiles call Confirmed — pattern.go line ~413
RE2 (Go regexp) — no ReDoS Confirmed — all regexp.Compile/MustCompile calls use stdlib RE2 engine
filepath.Base confinement before pattern match Confirmed — ApplyPatternToFilename strips to base+strips extension before matching
Parameterized SQL (sqlc slice expansion) Confirmed — bookdrop.sql.go uses strings.Replace(query, "/*SLICE:ids*/?", ...) with queryParams binding, no string interpolation of IDs
pattern.go behavior unchanged N/A — file is new in this PR (not a regression surface); behaviour is correct

XSS in modal preview

All server-derived values rendered into innerHTML are routed through _esc():

  • item.file_namefname = _esc(item.file_name) (line 202)
  • item.error_esc(item.error || "No match") (line 206)
  • Field keys and values (kv[0], kv[1]) → _esc(kv[0]), _esc(v) (lines 212–213)

_esc escapes &, <, >, ", ', and ` — sufficient coverage.

Post-apply flash message (_showFlash) uses element.textContent = msg (not innerHTML), and data.applied/data.failed are server-returned integers. Safe.

Status messages via _setStatus also use textContent. Safe.

No unescaped interpolation found.


ID scoping / access control

bookdrop_file has no user_id column (Grimmory-inherited schema). Access to the entire table is controlled by the BookdropRequired gate (admin/bookdrop-permission), consistent with existing BulkAcceptHandler and BulkRejectHandler which also operate on bare []int64 IDs without per-user scoping. No regression introduced.


CSP / inline style

No style= attributes added in template or JS diff. New CSS uses classes (bep-*). No CSP regression.


Findings

No security findings. All prior backend controls hold; the modal rework is XSS-clean.

REVIEW VERDICT: 0 blocker, 0 major, 0 minor

## Security Re-Review: PR #579 — BookDrop Extract Pattern (modal rework) **Head SHA:** 47e05001 | **Reviewer:** security-review bot | **CI:** green (not re-run per instructions) --- ### Checklist: backend safety regression check | Control | Status | |---|---| | Both endpoints wrapped in `gate` (BookdropRequired) | ✅ Confirmed — `routes.go` lines 22–23 | | CSRF double-submit (`X-CSRF-Token` header + `bookshelf_csrf` cookie) | ✅ Confirmed — JS reads cookie and sets header; server-side CSRF middleware unchanged | | `applyIDCap = 500` enforced before `getFiles` call | ✅ Confirmed — `pattern.go` line ~413 | | RE2 (Go `regexp`) — no ReDoS | ✅ Confirmed — all `regexp.Compile`/`MustCompile` calls use stdlib RE2 engine | | `filepath.Base` confinement before pattern match | ✅ Confirmed — `ApplyPatternToFilename` strips to base+strips extension before matching | | Parameterized SQL (sqlc slice expansion) | ✅ Confirmed — `bookdrop.sql.go` uses `strings.Replace(query, "/*SLICE:ids*/?", ...)` with `queryParams` binding, no string interpolation of IDs | | `pattern.go` behavior unchanged | N/A — file is new in this PR (not a regression surface); behaviour is correct | --- ### XSS in modal preview All server-derived values rendered into `innerHTML` are routed through `_esc()`: - `item.file_name` → `fname = _esc(item.file_name)` (line 202) - `item.error` → `_esc(item.error || "No match")` (line 206) - Field keys and values (`kv[0]`, `kv[1]`) → `_esc(kv[0])`, `_esc(v)` (lines 212–213) `_esc` escapes `&`, `<`, `>`, `"`, `'`, and `` ` `` — sufficient coverage. Post-apply flash message (`_showFlash`) uses `element.textContent = msg` (not `innerHTML`), and `data.applied`/`data.failed` are server-returned integers. Safe. Status messages via `_setStatus` also use `textContent`. Safe. No unescaped interpolation found. --- ### ID scoping / access control `bookdrop_file` has no `user_id` column (Grimmory-inherited schema). Access to the entire table is controlled by the `BookdropRequired` gate (admin/bookdrop-permission), consistent with existing `BulkAcceptHandler` and `BulkRejectHandler` which also operate on bare `[]int64` IDs without per-user scoping. No regression introduced. --- ### CSP / inline style No `style=` attributes added in template or JS diff. New CSS uses classes (`bep-*`). No CSP regression. --- ### Findings No security findings. All prior backend controls hold; the modal rework is XSS-clean. **REVIEW VERDICT: 0 blocker, 0 major, 0 minor**
Author
Owner

CODE RE-REVIEW: APPROVED (with minor notes)

Branch: bd-bookshelf-d1x3.1 | SHA: 47e05001 | Diff: ~3990 lines added


Phase 0: DEMO Verification

No standalone DEMO block to re-run (this was a UI rework review, not a standalone bead DEMO). CI is confirmed green at SHA 47e05001. Proceeding to Phase 1.


Phase 1: Spec Compliance

MATCHER UNCHANGED — CONFIRMED

internal/bookdrop/pattern.go is a new file (not a modification of an existing matcher). The regex is built by buildPatternRegex without ^ or $ anchors prepended/appended, and ApplyPatternToFilename calls pp.compiled.FindStringSubmatch(base) (not FindStringSubmatch with anchored output). Extension stripping occurs on the base filename before the unanchored match. The {Title} placeholder matches "Atmosphere 2" from "Atmosphere 2.epub" correctly. No anchoring was added.

Modal targets — CONFIRMED INSIDE CONTROLLER ELEMENT

The controller is declared on <section class="bookdrop-index"> (line 2 of the template). The bep-overlay modal div is inside that section (line 92, section closes at line 272). All targets (openBtn, overlay, patternInput, previewArea, statusMsg, selectedCount) resolve correctly.

ASIN chip — MINOR SPEC MISS

The bead description lists {ASIN} as a supported placeholder. placeholderConfigs in pattern.go includes "ASIN". However, the template's placeholder chip row (lines 129–152) does not include an {ASIN} chip. The backend supports it, the UI just doesn't offer it as a one-click insert. Users can type {ASIN} manually. Low severity.


Phase 2: Code Quality

[MINOR] static/js/controllers/bookdrop_extract_pattern_controller.js:163bookdrop:refresh event is dispatched after a successful Apply but has no listener anywhere in the codebase. After metadata is applied, the table rows remain stale until the user manually reloads the page. The dispatchEvent call is dead code. Either wire a listener in bookdrop_review_controller.js that calls window.location.reload(), or remove the dispatch and update the flash message to mention "Reload to see updated metadata." Not a blocker — the flash message reports success and the data was saved server-side.

[MINOR] internal/bookdrop/pattern_test.go — The pattern_test.go is overall well-structured with one-Expect-per-It. The Expect(resp.Items, err).To(HaveLen(1)) and Expect(resp.Applied, err).To(Equal(2)) two-actual form correctly follows the project convention of folding the nil-error check into the value assertion. No violations.

[MINOR] templates/pages/bookdrop_index.html:65–90 — The "no libraries configured" branch (the {{else}} case) renders a separate <div class="bookdrop-metadata-actions"> container with the Extract Pattern button. If a user has no libraries, they can open the modal but cannot meaningfully use it (bookdrop files only appear when at least one library path is watched). The button being present here is harmless but slightly misleading; it matches the "no accept/reject in no-libraries case, but still show extract" intention noted in the code comment. Accepted as-is.

Confirmed clean:

  • No inline style= attributes anywhere in the template or JS controller (CSP-safe)
  • All innerHTML insertions go through _esc() which escapes `& < > " ' ``
  • role="dialog", aria-modal="true", aria-labelledby="bep-title" present on the overlay
  • Escape key handled via document.addEventListener("keydown", ...) on open, removed on close — no keydown leak
  • Backdrop-click handled by onOverlayClick checking event.target === overlayTarget
  • Focus management: _previousFocus saved/restored on open/close
  • Apply race fixed: _previewSeq++ bumped before the apply POST, stale preview responses check sequence before setting status
  • applyIDCap = 500 limits the apply endpoint; previewFileLimit = 5 limits preview
  • Functions are ≤30 lines; buildPatternRegex and helpers extracted cleanly
  • Ginkgo test structure follows BeforeEach/JustBeforeEach/It pattern
  • Routes registered in routes.go and wired in wire.go
  • Controller registered in app.js and <script> tag added to base.html
  • Browser e2e test covers: initial disabled state, selection enabling button, open modal, chip preset fill, auto-preview population, apply+close

REVIEW VERDICT: 0 blocker, 0 major, 3 minor

## CODE RE-REVIEW: APPROVED (with minor notes) **Branch:** bd-bookshelf-d1x3.1 | **SHA:** 47e05001 | **Diff:** ~3990 lines added --- ### Phase 0: DEMO Verification No standalone DEMO block to re-run (this was a UI rework review, not a standalone bead DEMO). CI is confirmed green at SHA 47e05001. Proceeding to Phase 1. --- ### Phase 1: Spec Compliance **MATCHER UNCHANGED — CONFIRMED** `internal/bookdrop/pattern.go` is a new file (not a modification of an existing matcher). The regex is built by `buildPatternRegex` without `^` or `$` anchors prepended/appended, and `ApplyPatternToFilename` calls `pp.compiled.FindStringSubmatch(base)` (not `FindStringSubmatch` with anchored output). Extension stripping occurs on the base filename before the unanchored match. The `{Title}` placeholder matches "Atmosphere 2" from "Atmosphere 2.epub" correctly. No anchoring was added. **Modal targets — CONFIRMED INSIDE CONTROLLER ELEMENT** The controller is declared on `<section class="bookdrop-index">` (line 2 of the template). The `bep-overlay` modal div is inside that section (line 92, section closes at line 272). All targets (`openBtn`, `overlay`, `patternInput`, `previewArea`, `statusMsg`, `selectedCount`) resolve correctly. **ASIN chip — MINOR SPEC MISS** The bead description lists `{ASIN}` as a supported placeholder. `placeholderConfigs` in pattern.go includes `"ASIN"`. However, the template's placeholder chip row (lines 129–152) does not include an `{ASIN}` chip. The backend supports it, the UI just doesn't offer it as a one-click insert. Users can type `{ASIN}` manually. Low severity. --- ### Phase 2: Code Quality [MINOR] `static/js/controllers/bookdrop_extract_pattern_controller.js:163` — `bookdrop:refresh` event is dispatched after a successful Apply but has no listener anywhere in the codebase. After metadata is applied, the table rows remain stale until the user manually reloads the page. The `dispatchEvent` call is dead code. Either wire a listener in `bookdrop_review_controller.js` that calls `window.location.reload()`, or remove the dispatch and update the flash message to mention "Reload to see updated metadata." Not a blocker — the flash message reports success and the data was saved server-side. [MINOR] `internal/bookdrop/pattern_test.go` — The pattern_test.go is overall well-structured with one-Expect-per-It. The `Expect(resp.Items, err).To(HaveLen(1))` and `Expect(resp.Applied, err).To(Equal(2))` two-actual form correctly follows the project convention of folding the nil-error check into the value assertion. No violations. [MINOR] `templates/pages/bookdrop_index.html:65–90` — The "no libraries configured" branch (the `{{else}}` case) renders a separate `<div class="bookdrop-metadata-actions">` container with the Extract Pattern button. If a user has no libraries, they can open the modal but cannot meaningfully use it (bookdrop files only appear when at least one library path is watched). The button being present here is harmless but slightly misleading; it matches the "no accept/reject in no-libraries case, but still show extract" intention noted in the code comment. Accepted as-is. **Confirmed clean:** - No `inline style=` attributes anywhere in the template or JS controller (CSP-safe) - All `innerHTML` insertions go through `_esc()` which escapes `& < > " ' \`` - `role="dialog"`, `aria-modal="true"`, `aria-labelledby="bep-title"` present on the overlay - Escape key handled via `document.addEventListener("keydown", ...)` on open, removed on close — no keydown leak - Backdrop-click handled by `onOverlayClick` checking `event.target === overlayTarget` - Focus management: `_previousFocus` saved/restored on open/close - Apply race fixed: `_previewSeq++` bumped before the apply POST, stale preview responses check sequence before setting status - `applyIDCap = 500` limits the apply endpoint; `previewFileLimit = 5` limits preview - Functions are ≤30 lines; `buildPatternRegex` and helpers extracted cleanly - Ginkgo test structure follows BeforeEach/JustBeforeEach/It pattern - Routes registered in `routes.go` and wired in `wire.go` - Controller registered in `app.js` and `<script>` tag added to `base.html` - Browser e2e test covers: initial disabled state, selection enabling button, open modal, chip preset fill, auto-preview population, apply+close --- REVIEW VERDICT: 0 blocker, 0 major, 3 minor
zombor force-pushed bd-bookshelf-d1x3.1 from 47e05001e3
All checks were successful
/ JS Unit Tests (pull_request) Successful in 2m1s
/ Lint (pull_request) Successful in 2m26s
/ Test (pull_request) Successful in 3m38s
/ E2E Browser (pull_request) Successful in 4m30s
/ E2E API (pull_request) Successful in 5m29s
/ Integration (pull_request) Successful in 5m39s
to a83c40f26a
All checks were successful
/ JS Unit Tests (pull_request) Successful in 21s
/ Lint (pull_request) Successful in 54s
/ Test (pull_request) Successful in 2m53s
/ E2E Browser (pull_request) Successful in 3m34s
/ E2E API (pull_request) Successful in 4m49s
/ Integration (pull_request) Successful in 5m1s
2026-06-16 15:28:31 +00:00
Compare
zombor force-pushed bd-bookshelf-d1x3.1 from a83c40f26a
All checks were successful
/ JS Unit Tests (pull_request) Successful in 21s
/ Lint (pull_request) Successful in 54s
/ Test (pull_request) Successful in 2m53s
/ E2E Browser (pull_request) Successful in 3m34s
/ E2E API (pull_request) Successful in 4m49s
/ Integration (pull_request) Successful in 5m1s
to 1b45a2b622
Some checks failed
/ Lint (pull_request) Successful in 47s
/ JS Unit Tests (pull_request) Failing after 5m5s
/ E2E Browser (pull_request) Has been cancelled
/ Test (pull_request) Has been cancelled
/ Integration (pull_request) Has been cancelled
/ E2E API (pull_request) Has been cancelled
2026-06-16 19:00:56 +00:00
Compare
fix(bookdrop): update JS unit test for applyPattern to expect page-reload navigation
All checks were successful
/ JS Unit Tests (pull_request) Successful in 34s
/ Lint (pull_request) Successful in 55s
/ Test (pull_request) Successful in 2m44s
/ E2E Browser (pull_request) Successful in 3m38s
/ E2E API (pull_request) Successful in 5m6s
/ Integration (pull_request) Successful in 5m10s
5264829ccd
The test expected the old "close modal" behaviour; the new implementation
navigates via window.location.href so jsdom raised "Not implemented:
navigation".  Replace window.location with a plain writable object (same
pattern used in library_kebab_menu_controller.test.js) and assert the
redirected href contains the pattern_applied query param.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Author
Owner

Extract Pattern modal screenshot (bookdrop-extract-pattern-modal)

bookdrop-extract-pattern-modal

**Extract Pattern modal screenshot** (bookdrop-extract-pattern-modal) ![bookdrop-extract-pattern-modal](/attachments/ebd21fd8-71b1-4de5-9dcf-c9b0401b7807)
Author
Owner

Extract Pattern modal screenshot (bookdrop-extract-pattern-applied)

bookdrop-extract-pattern-applied

**Extract Pattern modal screenshot** (bookdrop-extract-pattern-applied) ![bookdrop-extract-pattern-applied](/attachments/11606b64-5121-4084-af9a-187eab66b0d1)
Author
Owner

Extract Pattern modal screenshot (bookdrop-extract-pattern-modal)

bookdrop-extract-pattern-modal

**Extract Pattern modal screenshot** (bookdrop-extract-pattern-modal) ![bookdrop-extract-pattern-modal](/attachments/a63731c1-7237-413f-a3e6-deca659d9c60)
Author
Owner

Extract Pattern modal screenshot (bookdrop-extract-pattern-applied)

bookdrop-extract-pattern-applied

**Extract Pattern modal screenshot** (bookdrop-extract-pattern-applied) ![bookdrop-extract-pattern-applied](/attachments/b5f7fca1-eee0-48e5-ba59-9e823ea8baa5)
Author
Owner

Security Review — bookshelf-d1x3.1 (PR #579)

Reviewed: git diff origin/main...origin/bd-bookshelf-d1x3.1


Findings

[MAJOR] internal/bookdrop/pattern.go:71 — No upper bound on user-supplied pattern string length
The ParsePattern function only rejects empty/whitespace patterns; there is no limit on the length or structural complexity of the pattern string. A user with permission_access_bookdrop can POST an arbitrarily long pattern string (e.g. 10 MB of {Title} tokens) to /bookdrop/extract-pattern/preview or /apply. Each call through buildPatternRegexregexp.QuoteMeta on literal text between placeholders, then regexp.Compile, creates potentially large regex structures with O(n) capture groups and scans the entire string. Under the applyIDCap=500 cap, 500 filenames each run against that regex per request. Add a hard max on pattern length (e.g. 1024 chars) before calling ParsePattern. The previewFileLimit=5 and applyIDCap=500 caps are good but do not mitigate this.

[MAJOR] templates/pages/bookdrop_index.html:4138 — PatternAppliedFlash rendered as raw {{.PatternAppliedFlash}} into HTML without an explicit plain-text wrapper
The flash value is q.Get("pattern_applied") — the raw URL query param, percent-decoded by net/http, passed directly into {{.PatternAppliedFlash}} which html/template will autoescaped as HTML entity text, so standard XSS via the param is blocked. However, the value is constructed client-side by the JS controller using integer counts from the server JSON response (data.applied, data.failed) and then encodeURIComponent-ed, so in the normal flow the value is safe. The issue is that any user can craft a request with an arbitrary ?pattern_applied=<string> param that will be shown as a flash. This is a self-XSS / phishing vector (attacker tricks admin into clicking a link with ?pattern_applied=<convincing+message>), though html/template's autoescaping prevents script injection. The severity is limited because (a) Go's html/template does properly escape the value and (b) the route is gated by BookdropRequired. Recommend validating or ignoring pattern_applied values that don't match the server-generated format (e.g. require it to match Applied: \d+ succeeded, \d+ failed. on the server side, or use a flash cookie instead of a URL param).

[MINOR] internal/bookdrop/pattern.go:396-398 — Preview truncates IDs client-side but sends full list to server; truncation happens server-side after DB query is issued
PreviewPattern slices ids to previewFileLimit=5 before calling getFiles, so the DB query is bounded. No real issue — just noting the ID list for preview goes through the cap correctly. Confirmed OK.

[MINOR] internal/bookdrop/pattern_handler.go:1 — No Status == PENDING_REVIEW gate on apply endpoint
ApplyPattern calls getFiles (a plain GetBookdropFilesByIDs with no status filter) and then calls UpdateBookdropFileFetchedMetadata by raw ID. There is no check that the targeted bookdrop_file rows are in PENDING_REVIEW state. A BookdropRequired user could apply a pattern to files in ACCEPTED, REJECTED, or any other status, overwriting their fetched_metadata. The bookdrop_file table has no user_id column (it is a global/admin resource), so cross-user isolation is not applicable — all BookdropRequired users share the same inbox, matching the existing behaviour of BulkReject/BulkAccept. However, silently mutating non-PENDING records is a latent correctness concern. Low-risk for now since only admin-class users reach this endpoint, but a status filter in the SQL query would be cleaner.


Security assessment per review scope

ReDoS (user-controlled regex): The all-fixed-captures approach in placeholderConfigs produces safe, non-backtracking patterns (\d+, [a-zA-Z]+, ISBN digit counts). The (.+?) non-greedy capture is Go RE2 which has linear-time guarantees — no exponential backtracking possible in Go's regexp package. No ReDoS risk. The only concern is pattern length (see MAJOR above), not backtracking complexity.

SQL injection: All DB writes go through sqlc-generated parameterised queries (UpdateBookdropFileFetchedMetadata uses ? params, GetBookdropFilesByIDs uses the sqlc.slice IN-list expansion). The JSON blob stored in fetched_metadata is produced by json.Marshal on a typed struct with only primitive types. No SQL injection risk.

JSON injection / stored data growth: Captured values are strings from filenames; they are marshalled into a typed fileMetadataForStore struct via json.Marshal — no raw string concatenation. Field values are bounded by the filename length (OS limit ~255 chars). No injection risk into stored JSON.

XSS (preview rendering): The JS controller uses a correct _esc() function that escapes &<>"'\`` before inserting into innerHTML. All item.file_name, item.error, field keys and values pass through _esc(). _setStatus()usestextContent, not innerHTML`. No XSS risk in the JS layer.

XSS (template): {{.PatternAppliedFlash}} is rendered by html/template which autoescapes to HTML entities. No stored/reflected XSS via Go templates.

Open redirect (window.location): The JS builds the redirect URL as window.location.pathname + "?pattern_applied=" + msg. The pathname comes from the current page's URL (already same-origin), not from user or server input. The query string value is integer counts URL-encoded. No open-redirect risk.

Multi-user / ownership: bookdrop_file has no user_id column — it is a global admin resource (per Grimmory schema). The BookdropRequired gate enforces role-based access. The multi-user hard rule applies to per-user resources; bookdrop_file is not user-owned. Matching the existing Bulk Accept/Reject pattern. No multi-user scoping violation for this resource. The userID is not relevant here.

Secrets / PII in logs: No logging of pattern or extracted metadata values was added. No concern.

New Grimmory columns: fetched_metadata already exists in migration 0001. No new columns added. Policy satisfied.


REVIEW VERDICT: 0 blocker, 2 major, 1 minor

## Security Review — bookshelf-d1x3.1 (PR #579) Reviewed: `git diff origin/main...origin/bd-bookshelf-d1x3.1` --- ### Findings [MAJOR] internal/bookdrop/pattern.go:71 — No upper bound on user-supplied pattern string length The `ParsePattern` function only rejects empty/whitespace patterns; there is no limit on the length or structural complexity of the pattern string. A user with `permission_access_bookdrop` can POST an arbitrarily long pattern string (e.g. 10 MB of `{Title}` tokens) to `/bookdrop/extract-pattern/preview` or `/apply`. Each call through `buildPatternRegex` → `regexp.QuoteMeta` on literal text between placeholders, then `regexp.Compile`, creates potentially large regex structures with O(n) capture groups and scans the entire string. Under the applyIDCap=500 cap, 500 filenames each run against that regex per request. Add a hard max on pattern length (e.g. 1024 chars) before calling `ParsePattern`. The `previewFileLimit=5` and `applyIDCap=500` caps are good but do not mitigate this. [MAJOR] templates/pages/bookdrop_index.html:4138 — `PatternAppliedFlash` rendered as raw `{{.PatternAppliedFlash}}` into HTML without an explicit plain-text wrapper The flash value is `q.Get("pattern_applied")` — the raw URL query param, percent-decoded by `net/http`, passed directly into `{{.PatternAppliedFlash}}` which `html/template` will autoescaped as HTML entity text, so standard XSS via the param is blocked. However, the value is constructed client-side by the JS controller using integer counts from the server JSON response (`data.applied`, `data.failed`) and then `encodeURIComponent`-ed, so in the normal flow the value is safe. The issue is that **any user can craft a request with an arbitrary `?pattern_applied=<string>` param** that will be shown as a flash. This is a self-XSS / phishing vector (attacker tricks admin into clicking a link with `?pattern_applied=<convincing+message>`), though `html/template`'s autoescaping prevents script injection. The severity is limited because (a) Go's `html/template` does properly escape the value and (b) the route is gated by `BookdropRequired`. Recommend validating or ignoring `pattern_applied` values that don't match the server-generated format (e.g. require it to match `Applied: \d+ succeeded, \d+ failed.` on the server side, or use a flash cookie instead of a URL param). [MINOR] internal/bookdrop/pattern.go:396-398 — Preview truncates IDs client-side but sends full list to server; truncation happens server-side after DB query is issued `PreviewPattern` slices `ids` to `previewFileLimit=5` **before** calling `getFiles`, so the DB query is bounded. No real issue — just noting the ID list for preview goes through the cap correctly. Confirmed OK. [MINOR] internal/bookdrop/pattern_handler.go:1 — No `Status == PENDING_REVIEW` gate on apply endpoint `ApplyPattern` calls `getFiles` (a plain `GetBookdropFilesByIDs` with no status filter) and then calls `UpdateBookdropFileFetchedMetadata` by raw ID. There is no check that the targeted bookdrop_file rows are in `PENDING_REVIEW` state. A `BookdropRequired` user could apply a pattern to files in `ACCEPTED`, `REJECTED`, or any other status, overwriting their `fetched_metadata`. The `bookdrop_file` table has no `user_id` column (it is a global/admin resource), so cross-user isolation is not applicable — all BookdropRequired users share the same inbox, matching the existing behaviour of BulkReject/BulkAccept. However, silently mutating non-PENDING records is a latent correctness concern. Low-risk for now since only admin-class users reach this endpoint, but a status filter in the SQL query would be cleaner. --- ### Security assessment per review scope **ReDoS (user-controlled regex):** The all-fixed-captures approach in `placeholderConfigs` produces safe, non-backtracking patterns (`\d+`, `[a-zA-Z]+`, ISBN digit counts). The `(.+?)` non-greedy capture is Go RE2 which has linear-time guarantees — no exponential backtracking possible in Go's `regexp` package. **No ReDoS risk.** The only concern is pattern *length* (see MAJOR above), not backtracking complexity. **SQL injection:** All DB writes go through sqlc-generated parameterised queries (`UpdateBookdropFileFetchedMetadata` uses `?` params, `GetBookdropFilesByIDs` uses the `sqlc.slice` IN-list expansion). The JSON blob stored in `fetched_metadata` is produced by `json.Marshal` on a typed struct with only primitive types. **No SQL injection risk.** **JSON injection / stored data growth:** Captured values are strings from filenames; they are marshalled into a typed `fileMetadataForStore` struct via `json.Marshal` — no raw string concatenation. Field values are bounded by the filename length (OS limit ~255 chars). **No injection risk into stored JSON.** **XSS (preview rendering):** The JS controller uses a correct `_esc()` function that escapes `&<>"'\`` before inserting into `innerHTML`. All `item.file_name`, `item.error`, field keys and values pass through `_esc()`. `_setStatus()` uses `textContent`, not `innerHTML`. **No XSS risk in the JS layer.** **XSS (template):** `{{.PatternAppliedFlash}}` is rendered by `html/template` which autoescapes to HTML entities. **No stored/reflected XSS via Go templates.** **Open redirect (window.location):** The JS builds the redirect URL as `window.location.pathname + "?pattern_applied=" + msg`. The pathname comes from the current page's URL (already same-origin), not from user or server input. The query string value is integer counts URL-encoded. **No open-redirect risk.** **Multi-user / ownership:** `bookdrop_file` has no `user_id` column — it is a global admin resource (per Grimmory schema). The `BookdropRequired` gate enforces role-based access. The multi-user hard rule applies to *per-user* resources; `bookdrop_file` is not user-owned. Matching the existing Bulk Accept/Reject pattern. **No multi-user scoping violation for this resource.** The `userID` is not relevant here. **Secrets / PII in logs:** No logging of pattern or extracted metadata values was added. **No concern.** **New Grimmory columns:** `fetched_metadata` already exists in migration 0001. No new columns added. **Policy satisfied.** --- REVIEW VERDICT: 0 blocker, 2 major, 1 minor
Author
Owner

CODE REVIEW: NOT APPROVED — 1 blocker, 0 major, 2 minor


[BLOCKER] internal/bookdrop/pattern.go:1037 — fileMetadataForStore uses wrong JSON field names, breaking the accept path

The PR stores pattern-extracted data into fetched_metadata via fileMetadataForStore, but this struct uses entirely different JSON keys from the canonical FileMetadataJSON shape that parseOriginalMetadata reads (line 400 of review_service.go), which is what AcceptProposal uses when importing a file to seed book_metadata.

Concrete mismatches:

  • "published" (pattern) vs "published_date" (canonical) — dates written by pattern are invisible to accept
  • "isbn10" (pattern) vs "isbn_10" (canonical) — ISBN10 silently dropped on import
  • "isbn13" (pattern) vs "isbn_13" (canonical) — ISBN13 silently dropped on import
  • series_number float32 (pattern) vs *float64 (canonical) — type mismatch; JSON unmarshal into *float64 will leave the canonical field nil when reading back a float32-serialised value (though numeric JSON is compatible, the pointer nil vs zero distinction matters)
  • "asin" and "series_total" appear in fileMetadataForStore but not in FileMetadataJSON at all — they are written but silently swallowed by parseOriginalMetadata

The effect: a user applies the pattern, the right column shows the extracted data, then clicks Accept — the imported book has no published date, ISBNs, or series number. This silently corrupts the user's intent.

Fix: replace fileMetadataForStore with FileMetadataJSON directly (it already has the full field set including Subtitle, SeriesName, etc.). Adjust buildFetchedMetadataJSON and mergePatternResult* to populate FileMetadataJSON. Use *float64 for SeriesNumber. Note FileMetadataJSON has no SeriesTotal or ASIN fields — those need to be added to the canonical shape or accepted as pattern-only preview fields that are not persisted.


[MINOR] static/js/controllers/bookdrop_extract_pattern_controller.js:3639 — _showFlash method is dead code

_showFlash is defined at line 3639 but never called anywhere in the controller. The apply path now uses window.location.href navigation instead of showing an in-page flash. The orphaned method adds noise.

Fix: remove _showFlash.


[MINOR] internal/bookdrop/pattern_test.go:1803 — called variable captured in stub but never asserted

In the "more IDs than previewFileLimit" context, called is set inside the stub and then _ = called is used to suppress the unused-variable error. The assertion ("limits preview to at most 5 files") only checks resp.Items length, not the number of IDs actually passed to the DB query, so the previewFileLimit cap is tested at the response level but not at the query level.

Fix: either remove called entirely (since the assertion is sufficient), or assert Expect(called).To(Equal(5)) inside the It — do not suppress with _ = called.


REVIEW VERDICT: 1 blocker, 0 major, 2 minor

CODE REVIEW: NOT APPROVED — 1 blocker, 0 major, 2 minor --- [BLOCKER] internal/bookdrop/pattern.go:1037 — fileMetadataForStore uses wrong JSON field names, breaking the accept path The PR stores pattern-extracted data into `fetched_metadata` via `fileMetadataForStore`, but this struct uses entirely different JSON keys from the canonical `FileMetadataJSON` shape that `parseOriginalMetadata` reads (line 400 of `review_service.go`), which is what `AcceptProposal` uses when importing a file to seed `book_metadata`. Concrete mismatches: - `"published"` (pattern) vs `"published_date"` (canonical) — dates written by pattern are invisible to accept - `"isbn10"` (pattern) vs `"isbn_10"` (canonical) — ISBN10 silently dropped on import - `"isbn13"` (pattern) vs `"isbn_13"` (canonical) — ISBN13 silently dropped on import - `series_number float32` (pattern) vs `*float64` (canonical) — type mismatch; JSON unmarshal into `*float64` will leave the canonical field nil when reading back a float32-serialised value (though numeric JSON is compatible, the pointer nil vs zero distinction matters) - `"asin"` and `"series_total"` appear in `fileMetadataForStore` but not in `FileMetadataJSON` at all — they are written but silently swallowed by `parseOriginalMetadata` The effect: a user applies the pattern, the right column shows the extracted data, then clicks Accept — the imported book has no published date, ISBNs, or series number. This silently corrupts the user's intent. Fix: replace `fileMetadataForStore` with `FileMetadataJSON` directly (it already has the full field set including Subtitle, SeriesName, etc.). Adjust `buildFetchedMetadataJSON` and `mergePatternResult*` to populate `FileMetadataJSON`. Use `*float64` for `SeriesNumber`. Note `FileMetadataJSON` has no `SeriesTotal` or `ASIN` fields — those need to be added to the canonical shape or accepted as pattern-only preview fields that are not persisted. --- [MINOR] static/js/controllers/bookdrop_extract_pattern_controller.js:3639 — _showFlash method is dead code `_showFlash` is defined at line 3639 but never called anywhere in the controller. The apply path now uses `window.location.href` navigation instead of showing an in-page flash. The orphaned method adds noise. Fix: remove `_showFlash`. --- [MINOR] internal/bookdrop/pattern_test.go:1803 — `called` variable captured in stub but never asserted In the "more IDs than previewFileLimit" context, `called` is set inside the stub and then `_ = called` is used to suppress the unused-variable error. The assertion ("limits preview to at most 5 files") only checks `resp.Items` length, not the number of IDs actually passed to the DB query, so the previewFileLimit cap is tested at the response level but not at the query level. Fix: either remove `called` entirely (since the assertion is sufficient), or assert `Expect(called).To(Equal(5))` inside the It — do not suppress with `_ = called`. --- REVIEW VERDICT: 1 blocker, 0 major, 2 minor
fix(d1x3.1): address review blockers/majors/minors — canonical JSON keys, pattern length cap, flash validation, PENDING_REVIEW gate
Some checks failed
/ JS Unit Tests (pull_request) Successful in 34s
/ Lint (pull_request) Successful in 1m48s
/ E2E Browser (pull_request) Successful in 3m48s
/ E2E API (pull_request) Failing after 4m27s
/ Integration (pull_request) Successful in 4m44s
/ Test (pull_request) Successful in 10m12s
dcc3d3fc8b
BLOCKER: Replace fileMetadataForStore with canonical FileMetadataJSON in
buildFetchedMetadataJSON + mergePatternResult* helpers. Fixes JSON tag
mismatches (published→published_date, isbn10→isbn_10, isbn13→isbn_13,
SeriesNumber float32→*float64). ASIN and SeriesTotal are preview-only
(shown as chips, not persisted) — no downstream consumer in
parseOriginalMetadata/seedFromOriginalMetadata.

MAJOR-security: Add 1024-char max to ParsePattern (ErrPatternTooLong
sentinel); handlers propagate it as errInvalidPattern → HTTP 400.
Unit + e2e tests added.

MAJOR-security: Validate ?pattern_applied= URL param with
patternAppliedFlashRE before rendering as flash banner. Arbitrary strings
silently discarded (phishing prevention). Unit tests added.

MINOR: Add GetPendingBookdropFilesByIDs sqlc query (status='PENDING_REVIEW'
filter); wire ApplyPattern to use it so ACCEPTED/REJECTED records are
never mutated. E2e test added.

MINOR: Remove dead _showFlash method from bookdrop_extract_pattern_controller.js.

MINOR: Promote `called` variable in pattern_test.go to a real assertion
(Expect(called).To(Equal(5))) verifying the DB-boundary cap.

Update existing tests: assert canonical key names (isbn_13, isbn_10,
published_date); assert series_total and asin are NOT persisted (preview-only).
All 527 unit tests + 780 JS tests pass; coverage 100%.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(e2e): fix non-pending file test — DB filter silently skips (0/0 not 1 failed)
Some checks failed
/ JS Unit Tests (pull_request) Successful in 24s
/ Lint (pull_request) Successful in 13m8s
/ Test (pull_request) Successful in 19m20s
/ E2E Browser (pull_request) Successful in 4m23s
/ E2E API (pull_request) Successful in 5m22s
/ Integration (pull_request) Failing after 19m20s
ed56ea4ca8
GetPendingBookdropFilesByIDs filters at DB level, so non-PENDING_REVIEW
files are simply not returned; the loop iterates 0 files → 0 failed,
0 applied. The correctness property (fetched_metadata untouched) is still
verified by the adjacent It block.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
zombor force-pushed bd-bookshelf-d1x3.1 from ed56ea4ca8
Some checks failed
/ JS Unit Tests (pull_request) Successful in 24s
/ Lint (pull_request) Successful in 13m8s
/ Test (pull_request) Successful in 19m20s
/ E2E Browser (pull_request) Successful in 4m23s
/ E2E API (pull_request) Successful in 5m22s
/ Integration (pull_request) Failing after 19m20s
to 40b8c18cbe
All checks were successful
/ Lint (pull_request) Successful in 1m33s
/ JS Unit Tests (pull_request) Successful in 1m43s
/ Integration (pull_request) Successful in 2m27s
/ Test (pull_request) Successful in 2m33s
/ E2E API (pull_request) Successful in 3m20s
/ E2E Browser (pull_request) Successful in 7m15s
2026-06-17 04:13:09 +00:00
Compare
Author
Owner

CODE RE-REVIEW: APPROVED

Branch: bd-bookshelf-d1x3.1 — HEAD 40b8c18c
Scope: Extract Pattern — filename → bookdrop_file.fetched_metadata


Prior Findings — All Fixed

[was BLOCKER] Divergent fileMetadataForStore struct gone; canonical FileMetadataJSON used.

buildFetchedMetadataJSON in internal/bookdrop/pattern.go now constructs a FileMetadataJSON value via mergePatternResult and returns it directly. The fileMetadataForStore struct is absent from the entire diff. JSON tags match the canonical shape:

  • series_number*float64 (internal/bookdrop/meta.go:25 — SeriesNumber *float64 \json:"series_number,omitempty"``)
  • published_date → string (meta.go:27)
  • isbn_13 / isbn_10 → string (meta.go:29-30)

mergeSeriesFields (pattern.go:562-569) correctly converts PatternResult.SeriesNumber (float32) to float64 before assigning to existing.SeriesNumber = &sn. An e2e test in e2e/api/bookdrop_test.go (the "canonical JSON keys" context) directly queries the DB after apply and asserts "isbn_13" and "published_date" keys are present. Finding: fixed.

[was MINOR] _showFlash removed from JS controller.

static/js/controllers/bookdrop_extract_pattern_controller.js contains no _showFlash or showFlash method. Finding: fixed.

[was MINOR] called var in pattern_test.go is asserted.

internal/bookdrop/pattern_test.go:416 has Expect(called).To(Equal(5)) in the "passes at most previewFileLimit IDs to the DB query" It block. Finding: fixed.


New-Work Checks

PENDING_REVIEW gate on apply.
wire.go wires ApplyPattern with d.Q.GetPendingBookdropFilesByIDs — the SQL query (internal/db/queries/bookdrop.sql) filters AND status = 'PENDING_REVIEW'. Non-pending files are silently skipped (0 applied, 0 failed). An e2e test ("apply to a non-pending file (ACCEPTED)") verifies this and asserts fetched_metadata remains NULL. Gate: correct.

Pattern-length cap.
ParsePattern returns ErrPatternTooLong for len(pattern) > 1024 (pattern.go:68-70). Test at pattern_test.go:40 covers the boundary. Cap: present.

Server-side flash validation.
validPatternAppliedFlash (review_handler.go:22-31) validates the ?pattern_applied= param against ^Applied: \d+ succeeded, \d+ failed\.$ before rendering it. Arbitrary URL values are discarded. No XSS surface: the template data field carries only a validated string.

Non-deterministic ORDER BY.
Both new queries (GetBookdropFilesByIDs, GetPendingBookdropFilesByIDs) use ORDER BY id ASC — total order, no flake risk.

XSS in JS controller.
All innerHTML insertions go through _esc() (a proper HTML-entity replacer). No unescaped user-controlled content.

Multi-user / ownership.
The apply endpoint updates rows gated by PENDING_REVIEW status keyed by the IDs the client provides. The BookdropRequired middleware controls access to the entire /bookdrop/ subtree, so only authorised users can call these endpoints. No per-user row scoping is needed here (bookdrop files are not user-owned resources).

Unbounded query.
Both new SELECT queries use WHERE id IN (sqlc.slice(...)) with the caller responsible for capping the slice. Preview caps to previewFileLimit = 5; apply caps to applyIDCap = 500. No unbounded full-table scan.


No New Issues Found

REVIEW VERDICT: 0 blocker, 0 major, 0 minor

## CODE RE-REVIEW: APPROVED **Branch:** bd-bookshelf-d1x3.1 — HEAD 40b8c18c **Scope:** Extract Pattern — filename → `bookdrop_file.fetched_metadata` --- ### Prior Findings — All Fixed **[was BLOCKER] Divergent `fileMetadataForStore` struct gone; canonical `FileMetadataJSON` used.** `buildFetchedMetadataJSON` in `internal/bookdrop/pattern.go` now constructs a `FileMetadataJSON` value via `mergePatternResult` and returns it directly. The `fileMetadataForStore` struct is absent from the entire diff. JSON tags match the canonical shape: - `series_number` → `*float64` (internal/bookdrop/meta.go:25 — `SeriesNumber *float64 \`json:"series_number,omitempty"\``) - `published_date` → string (meta.go:27) - `isbn_13` / `isbn_10` → string (meta.go:29-30) `mergeSeriesFields` (pattern.go:562-569) correctly converts `PatternResult.SeriesNumber` (float32) to `float64` before assigning to `existing.SeriesNumber = &sn`. An e2e test in `e2e/api/bookdrop_test.go` (the "canonical JSON keys" context) directly queries the DB after apply and asserts `"isbn_13"` and `"published_date"` keys are present. Finding: **fixed**. **[was MINOR] `_showFlash` removed from JS controller.** `static/js/controllers/bookdrop_extract_pattern_controller.js` contains no `_showFlash` or `showFlash` method. Finding: **fixed**. **[was MINOR] `called` var in `pattern_test.go` is asserted.** `internal/bookdrop/pattern_test.go:416` has `Expect(called).To(Equal(5))` in the "passes at most previewFileLimit IDs to the DB query" `It` block. Finding: **fixed**. --- ### New-Work Checks **PENDING_REVIEW gate on apply.** `wire.go` wires `ApplyPattern` with `d.Q.GetPendingBookdropFilesByIDs` — the SQL query (`internal/db/queries/bookdrop.sql`) filters `AND status = 'PENDING_REVIEW'`. Non-pending files are silently skipped (0 applied, 0 failed). An e2e test ("apply to a non-pending file (ACCEPTED)") verifies this and asserts `fetched_metadata` remains NULL. Gate: correct. **Pattern-length cap.** `ParsePattern` returns `ErrPatternTooLong` for `len(pattern) > 1024` (pattern.go:68-70). Test at pattern_test.go:40 covers the boundary. Cap: present. **Server-side flash validation.** `validPatternAppliedFlash` (review_handler.go:22-31) validates the `?pattern_applied=` param against `^Applied: \d+ succeeded, \d+ failed\.$` before rendering it. Arbitrary URL values are discarded. No XSS surface: the template data field carries only a validated string. **Non-deterministic ORDER BY.** Both new queries (`GetBookdropFilesByIDs`, `GetPendingBookdropFilesByIDs`) use `ORDER BY id ASC` — total order, no flake risk. **XSS in JS controller.** All `innerHTML` insertions go through `_esc()` (a proper HTML-entity replacer). No unescaped user-controlled content. **Multi-user / ownership.** The apply endpoint updates rows gated by `PENDING_REVIEW` status keyed by the IDs the client provides. The BookdropRequired middleware controls access to the entire `/bookdrop/` subtree, so only authorised users can call these endpoints. No per-user row scoping is needed here (bookdrop files are not user-owned resources). **Unbounded query.** Both new SELECT queries use `WHERE id IN (sqlc.slice(...))` with the caller responsible for capping the slice. Preview caps to `previewFileLimit = 5`; apply caps to `applyIDCap = 500`. No unbounded full-table scan. --- ### No New Issues Found REVIEW VERDICT: 0 blocker, 0 major, 0 minor
Author
Owner

Security Re-Review — PR #579 (bd-bookshelf-d1x3.1, HEAD 40b8c18c)

Prior findings verification

[was MAJOR #1] ParsePattern max pattern length
FIXED. internal/bookdrop/pattern.go defines maxPatternLen = 1024 and ErrPatternTooLong. ParsePattern checks len(pattern) > maxPatternLen before any regex compilation and returns the sentinel error. Unit tests in pattern_test.go cover the boundary at exactly 1024 (accepted) and 1025 (rejected). E2E test in e2e/api/bookdrop_test.go also confirms 400 on over-length.

[was MAJOR #2] PatternAppliedFlash query-param injection
FIXED. review_handler.go introduces patternAppliedFlashRE = regexp.MustCompile( `^Applied: \d+ succeeded, \d+ failed\.$` ) and validPatternAppliedFlash(raw string) which returns the raw string only when it matches that strict regex, otherwise returns "". The template renders {{.PatternAppliedFlash}} with normal html/template auto-escaping, and the value is already empty for non-conforming inputs. Five unit tests in review_handler_test.go cover exact match, all-zeros, arbitrary string, empty string, and partial match (missing trailing dot). No arbitrary banner text can be rendered from ?pattern_applied=.

[was MINOR #3] ApplyPattern gates on status=PENDING_REVIEW
FIXED. The apply path uses GetPendingBookdropFilesByIDs (not GetBookdropFilesByIDs) which adds AND status = 'PENDING_REVIEW' in the query. Accepted/rejected files are silently dropped at the DB layer rather than the service layer. E2e test (apply to a non-pending file (ACCEPTED)) confirms 0 applied + 0 modified for an ACCEPTED file.


New findings from the rebased diff

No new security findings.

Brief summary of other checks performed:

  • SQL injection: both new queries use sqlc-generated parameterized IN (/*SLICE:ids*/?) — no string interpolation of user input.
  • XSS in JS controller: _renderPreview uses _esc() (full &<>"'\`` escaping) on all server-supplied strings before innerHTML; status/count updates use .textContent`. No unescaped assignment path.
  • Redirect construction: the JS builds ?pattern_applied=<encodeURIComponent(...)> from the server's own JSON response (not from user input) and the server validates it on re-read via regex — no open-redirect or reflected-XSS risk.
  • html/template auto-escape: {{.PatternAppliedFlash}} in bookdrop_index.html is rendered via a normal {{...}} action — html/template applies HTML-entity escaping. Even if validPatternAppliedFlash were bypassed, the template would entity-encode any injected markup.
  • ID cap on apply: applyIDCap = 500 cap enforced server-side before the DB query; preview capped at 5.
  • Authorization: both new routes registered under the same gate (BookdropRequired) as all existing bookdrop routes — admin/permission_access_bookdrop only.
  • No new Grimmory columns: only new SQL queries on existing columns; no ALTER TABLE/ADD COLUMN.
  • No secrets in logs: pattern.go and pattern_handler.go contain no logging calls.

REVIEW VERDICT: 0 blocker, 0 major, 0 minor

## Security Re-Review — PR #579 (bd-bookshelf-d1x3.1, HEAD 40b8c18c) ### Prior findings verification **[was MAJOR #1] `ParsePattern` max pattern length** FIXED. `internal/bookdrop/pattern.go` defines `maxPatternLen = 1024` and `ErrPatternTooLong`. `ParsePattern` checks `len(pattern) > maxPatternLen` before any regex compilation and returns the sentinel error. Unit tests in `pattern_test.go` cover the boundary at exactly 1024 (accepted) and 1025 (rejected). E2E test in `e2e/api/bookdrop_test.go` also confirms 400 on over-length. **[was MAJOR #2] `PatternAppliedFlash` query-param injection** FIXED. `review_handler.go` introduces `patternAppliedFlashRE = regexp.MustCompile(` `` `^Applied: \d+ succeeded, \d+ failed\.$` `` `)` and `validPatternAppliedFlash(raw string)` which returns the raw string only when it matches that strict regex, otherwise returns `""`. The template renders `{{.PatternAppliedFlash}}` with normal html/template auto-escaping, and the value is already empty for non-conforming inputs. Five unit tests in `review_handler_test.go` cover exact match, all-zeros, arbitrary string, empty string, and partial match (missing trailing dot). No arbitrary banner text can be rendered from `?pattern_applied=`. **[was MINOR #3] `ApplyPattern` gates on `status=PENDING_REVIEW`** FIXED. The apply path uses `GetPendingBookdropFilesByIDs` (not `GetBookdropFilesByIDs`) which adds `AND status = 'PENDING_REVIEW'` in the query. Accepted/rejected files are silently dropped at the DB layer rather than the service layer. E2e test (`apply to a non-pending file (ACCEPTED)`) confirms 0 applied + 0 modified for an ACCEPTED file. --- ### New findings from the rebased diff No new security findings. Brief summary of other checks performed: - **SQL injection:** both new queries use sqlc-generated parameterized `IN (/*SLICE:ids*/?)` — no string interpolation of user input. - **XSS in JS controller:** `_renderPreview` uses `_esc()` (full `&<>"'\`` escaping) on all server-supplied strings before `innerHTML`; status/count updates use `.textContent`. No unescaped assignment path. - **Redirect construction:** the JS builds `?pattern_applied=<encodeURIComponent(...)>` from the server's own JSON response (not from user input) and the server validates it on re-read via regex — no open-redirect or reflected-XSS risk. - **html/template auto-escape:** `{{.PatternAppliedFlash}}` in `bookdrop_index.html` is rendered via a normal `{{...}}` action — html/template applies HTML-entity escaping. Even if `validPatternAppliedFlash` were bypassed, the template would entity-encode any injected markup. - **ID cap on apply:** `applyIDCap = 500` cap enforced server-side before the DB query; preview capped at 5. - **Authorization:** both new routes registered under the same `gate` (`BookdropRequired`) as all existing bookdrop routes — admin/permission_access_bookdrop only. - **No new Grimmory columns:** only new SQL queries on existing columns; no `ALTER TABLE`/`ADD COLUMN`. - **No secrets in logs:** `pattern.go` and `pattern_handler.go` contain no logging calls. --- REVIEW VERDICT: 0 blocker, 0 major, 0 minor
zombor force-pushed bd-bookshelf-d1x3.1 from 40b8c18cbe
All checks were successful
/ Lint (pull_request) Successful in 1m33s
/ JS Unit Tests (pull_request) Successful in 1m43s
/ Integration (pull_request) Successful in 2m27s
/ Test (pull_request) Successful in 2m33s
/ E2E API (pull_request) Successful in 3m20s
/ E2E Browser (pull_request) Successful in 7m15s
to f1ed495237
All checks were successful
/ JS Unit Tests (pull_request) Successful in 34s
/ Lint (pull_request) Successful in 1m48s
/ E2E API (pull_request) Successful in 1m43s
/ E2E Browser (pull_request) Successful in 2m6s
/ Integration (pull_request) Successful in 2m34s
/ Test (pull_request) Successful in 3m4s
2026-06-18 00:47:49 +00:00
Compare
zombor force-pushed bd-bookshelf-d1x3.1 from f1ed495237
All checks were successful
/ JS Unit Tests (pull_request) Successful in 34s
/ Lint (pull_request) Successful in 1m48s
/ E2E API (pull_request) Successful in 1m43s
/ E2E Browser (pull_request) Successful in 2m6s
/ Integration (pull_request) Successful in 2m34s
/ Test (pull_request) Successful in 3m4s
to 2402fb78b4
All checks were successful
/ JS Unit Tests (pull_request) Successful in 18s
/ Lint (pull_request) Successful in 1m42s
/ E2E API (pull_request) Successful in 1m52s
/ Integration (pull_request) Successful in 2m28s
/ Test (pull_request) Successful in 2m30s
/ E2E Browser (pull_request) Successful in 2m39s
2026-06-18 00:59:39 +00:00
Compare
zombor merged commit d38b0bc198 into main 2026-06-18 01:03:08 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
zombor/pergamum!579
No description provided.