fix(sync): parse RFC 6266 Content-Disposition (filename*) — WOO-553 - #1840
fix(sync): parse RFC 6266 Content-Disposition (filename*) — WOO-553#1840WilcoLouwerse wants to merge 4 commits into
Conversation
Port the RFC 6266 parser fix from the WOO stable hotfix
(hotfix/woo-552-content-disposition-rfc6266, release
`v0.2.24-woo-1`) to the development branch, so the next main /
stable release does not re-introduce the bug.
Same shape as the stable fix:
- Replace the naive `explode('=', $header)` in
`SynchronizationService::getFilenameFromHeaders()` with a call
to a new private helper `parseContentDispositionFilename()`
that:
- splits the header on `;` (so `filename*=UTF-8''pct-encoded`
no longer fools the extractor),
- prefers `filename*` over `filename` per RFC 6266 §4.3,
- accepts case-insensitive parameter names,
- strips quotes on the plain `filename` value,
- decodes the pct-encoded UTF-8 payload of `filename*` via
`rawurldecode()` (RFC 3986 §2.1),
- logs and falls back on non-UTF-8 charsets.
- Add the second helper `decodeRfc5987ExtendedValue()`.
- Add 10 unit tests via
`SynchronizationServiceContentDispositionTest`, reaching the
private parser through `ReflectionClass` — same tests that
cover the stable fix.
Method signature on development is
`getFilenameFromHeaders(array $response, ObjectEntity $result)`
(the URL-fallback branch reads the request via
`$result->getObject()['request']`); only the Content-Disposition
branch is touched by this commit, the URL-fallback logic is
left as-is.
Triggered by xxllnc's Zaken API change of 2026-08-19 (started
emitting both `filename` and `filename*`, breaking OpenWoo.App
document downloads; rolled back but scheduled to re-roll
behind a feature toggle). Stable hotfix `v0.2.24-woo-1` is
already live as prerelease for direct install on WOO
production; this PR ensures the next main-line release does
not regress.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WilcoLouwerse
left a comment
There was a problem hiding this comment.
Verdict: REQUEST_CHANGES (Thorough) — self-review posted as COMMENT (GitHub blocks self-APPROVE/REQUEST_CHANGES)
Recommendation: fix the 4 🔴 CI-red blockers before merge — they're all mechanical style rules I violated in my own PR-introduced code.
Findings — 4 🔴, 2 🟡, 2 🟢 (inline for the code-attached ones; the two below are cross-cutting):
🟢 Test coverage — nice-to-have follow-ups
The 10 tests cover the DoD scenarios well. Additional cases worth considering (non-blocking):
filename*=UTF-8''(empty pct-encoded value) — asserts that the extended value returning empty falls back to plainfilename.- Whitespace around
=(filename = "foo.pdf") — the currenttrim(substr(...))handles this, but a test locks the behaviour. - Path-traversal probe (
filename="../../../etc/passwd") — asserts the parser returns the raw string verbatim (sanitization is downstream's responsibility).
🟢 Path-traversal / disk-write sanitization surface (pre-existing)
$filename from getFilenameFromHeaders() flows directly into FileService::saveFile(fileName: $filename, ...) at line 8823 with no sanitization at this layer. Both the old explode('=') extractor and the new RFC 6266 parser pass raw filenames through, so this PR does not change the attack surface — it's not a regression. But: if OpenRegister's FileService::saveFile() doesn't reject ../ sequences or path separators, a hostile upstream could write files outside the intended location. Worth a follow-up ticket to audit FileService::saveFile()'s filename sanitization, tracked separately from WOO-552.
CI status
- 🔴
quality / PHP Quality (phpcs)— 3 errors on new code (findings #1, #3, #4 below). - 🔴
quality / PHP Quality (phpmd)— 1 violation on new code (finding #2 below). - 🟡
quality / PHP Quality (phpstan)— still running when I checked. - ✅ All other checks passing (lint, psalm, PHPUnit test matrix, license, security, CodeQL, coverage baseline).
All four CI-red findings are trivial 5-minute fixes in my own new helpers.
Addresses the 8 findings from the self-review on #1840: phpcs / phpmd (CI-red) — SynchronizationService.php: - Line 9056, 9128: use named arguments on internal helper calls. - Line 9120: rename `$eq` → `$eqPos` (phpmd ShortVariable, min 3). - Line 9171: capitalize inline comment ("Decode via rawurldecode()…"). Parser correctness (RFC 6266 §4 quoted-string grammar): - Replace `explode(';', $header)` with a quoted-string-aware tokenizer `splitHeaderParameters()` so a `;` inside `filename="…"` no longer corrupts the value. `filename="foo;bar.pdf"` now returns `foo;bar.pdf` (was `foo`). Test file (SynchronizationServiceContentDispositionTest.php): - Fix namespace: `OCA\OpenConnector\Tests\...` → `OCA\Integriq\Tests\...` to match composer autoload-dev psr-4 rule (was skipped by composer's autoloader with a warning; every other test in tests/Unit/Service/ uses the Integriq namespace). - Add 3 tests: - `testFilenameWithSemicolonInsideQuotedValuePreservesFilename` - `testFilenameWithPathTraversalPayloadIsReturnedVerbatim` (parser contract test — sanitization is downstream's responsibility) - `testFilenameWithWhitespaceAroundEqualsIsAccepted` Refs: WOO-552, WOO-553 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WilcoLouwerse
left a comment
There was a problem hiding this comment.
Verdict: APPROVE (Quick) — self-review posted as COMMENT (GitHub blocks self-APPROVE)
All 6 prior inline findings addressed by 8b119af, verified by grep/inspection:
| Prior | Finding | Fix in 8b119af |
|---|---|---|
| 🔴 | Named arg on parseContentDispositionFilename |
line 9056 → headerValue: $... |
| 🔴 | Named arg on decodeRfc5987ExtendedValue |
line 9129 → value: $value |
| 🔴 | $eq → $eqPos (phpmd ShortVariable) |
line 9120 (and refs) |
| 🔴 | Inline comment capitalization | line 9210 → // Decode via rawurldecode() — … |
| 🟡 | Test namespace psr-4 mismatch | OCA\Integriq\Tests\Unit\Service |
| 🟡 | Quoted-string ; breaks the parse |
new splitHeaderParameters() tokenizer |
Beyond the findings:
- 3 new tests locking behaviour:
testFilenameWithSemicolonInsideQuotedValuePreservesFilename,testFilenameWithPathTraversalPayloadIsReturnedVerbatim,testFilenameWithWhitespaceAroundEqualsIsAccepted. Total 13 tests (was 10).
CI status on the new SHA: still queued when this review was written. Mechanical rules (named args, ShortVariable, comment capitalization) are satisfied by inspection; if phpcs/phpmd re-emit any of these on the new SHA, it will be a rule I missed rather than a rule I ignored. Will monitor and post a follow-up if CI stays red.
Resolving all 6 prior inline threads (all self-authored).
Quality Report — ConductionNL/integriq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ❌ | ||||
| phpmd | ❌ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 141/141 | |||
| npm | ✅ | ✅ 537/537 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-09-04 10:00 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/integriq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 141/141 | |||
| npm | ✅ | ✅ 537/537 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ✅ | ||||
| Playwright | ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-09-04 10:38 UTC
Download the full PDF report from the workflow artifacts.
| // are parameters. Splitting on `;` (instead of `=`) is what the | ||
| // naive pre-WOO-552 code got wrong: any `=` inside a value (bv. | ||
| // the charset''value shape of filename*) fooled the extractor. | ||
| $segments = array_map('trim', explode(';', $headerValue)); |
There was a problem hiding this comment.
explode(';', $headerValue) also splits semicolons that are inside a quoted filename. For example, attachment; filename="rapport; versie 2.pdf" would now return rapport instead of rapport; versie 2.pdf. Could we parse parameter separators while respecting quoted strings, and add a test for this case?
There was a problem hiding this comment.
Goed gezien, Barry — die exact case. Wilco had 'm eerder al 🟡-gevlagd in zijn tweede self-review en de fix + tokenizer in commit 8b119af doorgevoerd:
- Nieuwe private helper
splitHeaderParameters(string): arrayopSynchronizationService— quoted-string-aware tokenizer die"-open state tracked en alleen op;buiten quotes splitst (per RFC 6266 §4). parseContentDispositionFilename()roept nu die helper aan i.p.v.explode(';', ...).- Test
testFilenameWithSemicolonInsideQuotedValuePreservesFilenameopfoo;bar.pdf(regel 133).
Nu in commit 205a54e een extra test testFilenameWithSemicolonAndSpaceInsideQuotedValuePreservesFilename met jouw exacte string attachment; filename="rapport; versie 2.pdf" toegevoegd — zo staat de garantie letterlijk in de suite, niet alleen impliciet via het foo;bar.pdf geval. 13 → 14 tests.
Zelfde fix + 4 tests (waaronder jouw voorbeeld) ook geport naar de WOO-stable hotfix branch hotfix/woo-552-content-disposition-rfc6266 — een fresh prerelease v0.2.24-woo-2 wordt nu gecut die v0.2.24-woo-1 op openwoo.commonground.nu opvolgt.
Thanks voor de scherpe review!
…O-553 Add `testFilenameWithSemicolonAndSpaceInsideQuotedValuePreservesFilename` covering Barry's exact string from his PR #1840 review: attachment; filename="rapport; versie 2.pdf" The existing `testFilenameWithSemicolonInsideQuotedValuePreservesFilename` already exercises the same tokenizer path via `foo;bar.pdf`, but the new test locks the concrete example he raised so the guarantee is explicit in the suite, not only implied. Also mirrored to the WOO stable hotfix branch alongside the tokenizer backport (WOO-552 hotfix cut of v0.2.24-woo-2). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… WOO-552 Address Barry's review comment on the WOO-553 dev-branch PR (#1840 line 9111) by porting the same fix to the WOO stable hotfix line: `explode(';', $header)` also splits semicolons that live inside a quoted filename. `attachment; filename="rapport; versie 2.pdf"` corrupts to `rapport` instead of `rapport; versie 2.pdf`. The fix is a small quoted-string-aware tokenizer `splitHeaderParameters()` that tracks a `"`-open state and only splits `;` outside quotes, per RFC 6266 §4 grammar. Changes: - New private helper `splitHeaderParameters(string): array` on `SynchronizationService`. - `parseContentDispositionFilename()` now delegates the `;`-split to that helper instead of `explode(';', ...)` — behaviour on well-formed xxllnc headers is unchanged; corrupt scenarios that the naive split would drop are now preserved verbatim. - Rename local `$eq` → `$eqPos` in the parser (matches the post-review dev-branch style, avoids the phpmd ShortVariable rule if the hotfix line ever gets those checks). - 4 new unit tests locking behaviour: * `testFilenameWithSemicolonInsideQuotedValuePreservesFilename` — the general quoted-`;` case. * `testFilenameWithSemicolonAndSpaceInsideQuotedValuePreservesFilename` — Barry's exact example from PR #1840 (rapport; versie 2.pdf). * `testFilenameWithPathTraversalPayloadIsReturnedVerbatim` — contract: sanitization is downstream's responsibility. * `testFilenameWithWhitespaceAroundEqualsIsAccepted` — locks `filename = "…"` handling. Total: 14 tests (was 10), all green locally. `appinfo/info.xml` version bumped `0.2.24-woo-1` → `0.2.24-woo-2` so the next push cuts a fresh prerelease `v0.2.24-woo-2` that supersedes the currently-installed `v0.2.24-woo-1` on openwoo.commonground.nu. Deploy via `app_versions` app pin (same mechanic Wilco used on r17). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Quality Report — ConductionNL/integriq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 141/141 | |||
| npm | ✅ | ✅ 537/537 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ✅ | ||||
| Playwright | ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-09-04 11:35 UTC
Download the full PDF report from the workflow artifacts.
The test imported OCA\OpenConnector\Service\SynchronizationService, but integriq's class lives under OCA\Integriq\Service\SynchronizationService (matching every other test in this directory). ReflectionClass failed on the missing class, producing 14 PHPUnit errors across every PHP/NC matrix cell and cascading into the Quality Report failure. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Quality Report — ConductionNL/integriq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 141/141 | |||
| npm | ✅ | ✅ 537/537 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-09-04 11:56 UTC
Download the full PDF report from the workflow artifacts.
Summary
Forward-port the RFC 6266 Content-Disposition parser fix from the WOO stable hotfix (
v0.2.24-woo-1, currently live as prerelease) to thedevelopmentbranch. Without this PR the bug re-lands on production as soon as WOO upgrades to a newer main/stable release.openwoo.commonground.nu): https://github.com/ConductionNL/integriq/releases/tag/v0.2.24-woo-1What changed
SynchronizationService::getFilenameFromHeaders()ondevelopmentstill contained the naïveexplode('=', $header)extractor (line 9057-9058 oflib/Service/SynchronizationService.php) that xxllnc's RFC 6266 header shape (filename="…"; filename*=UTF-8''…) breaks: the extractor splits on=insidefilename*=, ends up with"…"; filename*in[1], andtrim(…, '"')leaves interior punctuation intact. Result: corrupt filename →saveFile()refuses or produces an unfindable file.Fix:
parseContentDispositionFilename()that:;(sofilename*=UTF-8''pct-encodedno longer fools the extractor),filename*overfilenameper RFC 6266 §4.3,filenamevalue,filename*viarawurldecode()(RFC 3986 §2.1),decodeRfc5987ExtendedValue().SynchronizationServiceContentDispositionTest, which reaches the private parser throughReflectionClass— same shape as the tests already living on the stable hotfix branch, where they pass 10/10.The URL-fallback branch is left untouched — the development signature (
ObjectEntity $result, request via$result->getObject()['request']) differs from stable, but that branch is outside the scope of this fix.Test plan
php -l lib/Service/SynchronizationService.php— no syntax errors.Why this exists as a separate PR
The WOO stable hotfix (
v0.2.24-woo-1) is already prerelease-cut and live onopenwoo.commonground.nu. This PR ensures the fix carries forward into the next main-line release rather than getting lost on a hotfix branch that never merges to development.🤖 Generated with Claude Code