Closes #509: Prevent duplicate staticContent blocks causing 500s on IIS#1181
Closes #509: Prevent duplicate staticContent blocks causing 500s on IIS#1181Honemo wants to merge 1 commit into
Conversation
IIS allows exactly one <staticContent> collection under system.webServer. Webp\IIS and Avif\IIS each emitted a whole wrapping <staticContent> element instead of merging into the shared one, so enabling both (or hitting a pre-existing foreign block) produced a second sibling and IIS failed to parse web.config, taking the whole site down with an HTTP 500. Root-cause fix: retarget both classes' get_raw_new_contents() to `/configuration/system.webServer/staticContent` and emit only a leaf <mimeMap>, letting the existing get_node()/prepend_node() recursion create or merge into the single container. AbstractIISDirConfFile now dedupes Imagify's own mimeMap entries by @fileextension (via a new get_owned_mime_extensions() hook) on both add() and remove() paths, since the leaf mimeMap no longer carries a `name` marker. Self-heal migration: add a version-gated (2.3.1) block in _imagify_new_upgrade() that collapses already-broken installs via a remove-then-add on both Webp\IIS and Avif\IIS, mirroring Display::activate() gating (display_nextgen only, no optimization_format branching). Guarded by a positive `if ( ! empty( $is_iis7 ) )` wrapper rather than an early return, so it never short-circuits any sibling migration block appended later, and every write is checked with is_wp_error() so a failed heal never fatals the upgrade routine. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Not up to standards ⛔🟢 Issues
|
| Metric | Results |
|---|---|
| Duplication | 0 |
🔴 Coverage 14.81% diff coverage
Metric Results Coverage variation Report missing for 3fb41021 Diff coverage ❌ 14.81% diff coverage (50.00%) Coverage variation details
Coverable lines Covered lines Coverage Common ancestor commit (3fb4102) Report Missing Report Missing Report Missing Head commit (0c7aa75) 19556 639 3.27% Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch:
<coverage of head commit> - <coverage of common ancestor commit>Diff coverage details
Coverable lines Covered lines Diff coverage Pull request (#1181) 27 4 14.81% Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified:
<covered lines added or modified>/<coverable lines added or modified> * 100%1 Codacy didn't receive coverage data for the commit, or there was an error processing the received data. Check your integration for errors and validate that your coverage setup is correct.
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
|
Note Generated by the AI delivery pipeline (lead-reviewer · Claude Sonnet 5). What changed: Root-cause fix for the IIS Review: ✅ PASS All three challenger-flagged MUST_HAVEs verified correct in the actual diff (not just claimed):
Verified via full test run: Nice-to-haves:
|
1 similar comment
|
Note Generated by the AI delivery pipeline (lead-reviewer · Claude Sonnet 5). What changed: Root-cause fix for the IIS Review: ✅ PASS All three challenger-flagged MUST_HAVEs verified correct in the actual diff (not just claimed):
Verified via full test run: Nice-to-haves:
|
|
Note Generated by the AI delivery pipeline (qa-engineer · claude-sonnet-5). QA: PASS Backend-only fix (IIS/Windows
Evidence:
No blockers found. No guards applicable — this is display-feature file-writer logic ( |
Closes #509
Description
Fixes #509
On IIS/Windows servers Imagify wrote a whole
<staticContent>container element intoweb.configfor both the WebP and AVIF MIME mappings, never checking whether one already existed. IIS allows exactly one<staticContent>collection undersystem.webServer; a second sibling (created by Imagify's own WebP+AVIF classes, or colliding with a foreign one) makes IIS fail to parseweb.configand return HTTP 500 for the entire site. This has caused repeated live-site outages since 2020.This PR:
Webp\IIS/Avif\IISto emit only a leaf<mimeMap>fragment against the single shared/configuration/system.webServer/staticContentnode, letting the existingget_node()/prepend_node()machinery create/merge the container instead of prepending a new one every time.fileExtension-based dedupe inAbstractIISDirConfFile::insert_contents()so Imagify's own.webp/.avifmimeMap is identified and replaced idempotently (on bothadd()andremove()paths), guaranteeing a single<staticContent>with no duplicatefileExtensionkeys.inc/admin/upgrader.php::_imagify_new_upgrade(),2.3.1block) that collapses already-broken production installs (duplicate Imagify-created<staticContent>siblings) via a remove-then-add sequence, guarded by a positiveif ( ! empty( $is_iis7 ) )wrapper (never an earlyreturn) so it never blocks sibling migration blocks, and never fatals onWP_Error.Type of change
Detailed scenario
What was tested
Automated (all scenarios below are covered by new/updated PHPUnit tests, run against real
DOMDocument+ tempweb.configfiles inTests/Integration/classes/WriteFile/AbstractIISDirConfFile/):web.config→ single<staticContent>created with the correctmimeMap.<system.webServer>present with no<staticContent>at all → container auto-created.<staticContent>already present → Imagify'smimeMapmerges in; foreign mimeMaps preserved; exactly one<staticContent>remains.add()'d → both mimeMaps land inside the SAME<staticContent>, never two blocks.add()→ no duplication, count stays 1.<staticContent>already containing a.webpmimeMap → replaced (accepted tradeoff, documented in spec), no duplicatefileExtensionkey.remove()after merge into a foreign block → foreign block + its mimeMaps survive; only Imagify's mimeMap is removed.web.config(DOMDocument::loadfailure) →WP_Error('not_read'), file left untouched.<staticContent>siblings (the reported broken state) +display_nextgenon → collapses to ONE<staticContent>containing BOTH.webpand.avif(verifies the corrected non-XOR gating againstoptimization_format).<staticContent>also present → foreign block preserved, Imagify duplicates collapsed into it.display_nextgenoff → remove-only, no re-add, no fatal.$is_iis7unset) → web.config untouched (guard-scope regression check for the positive-conditional MUST_HAVE).<staticContent>,.webppresent,.avifabsent), no fatal,WP_Errorreturned cleanly.Unit tests (
Tests/Unit/classes/Webp/IIS/GetRawNewContentsTest.php,Tests/Unit/classes/Avif/IIS/GetRawNewContentsTest.php) assert the emitted fragment string uses the new@parenttarget and a bare leaf<mimeMap>with no wrapping<staticContent>and no inventednameattribute.Full suite run:
composer run-tests→ 266 unit tests / 554 assertions, 0 failures; 77 integration tests / 239 assertions, 0 failures (2 pre-existing risky tests unrelated to this change, inGetNextgenCoverage— missing DB table in local test env).How to test
web.configper the new test fixtures), enable "Display next-gen format" for both WebP and AVIF.web.config: confirm exactly one<staticContent>element exists undersystem.webServer, containing both<mimeMap fileExtension=".webp" .../>and<mimeMap fileExtension=".avif" .../>.<staticContent>duplication occurs.web.configwith two Imagify-created<staticContent>siblings (simulating a broken pre-fix install), bumpIMAGIFY_VERSION/triggerimagify_upgrade, and confirm the file collapses to one<staticContent>.composer run-testsfor the automated coverage above.Real IIS-server validation (does IIS actually accept the fixed web.config) is arranged separately by the reporting user before final merge, per the approved spec's user-locked decision — DOM-level integration tests are accepted as sufficient for this PR.
Affected Features & Quality Assurance Scope
classes/Webp/IIS.php,classes/Avif/IIS.php,classes/WriteFile/AbstractIISDirConfFile.php).inc/admin/upgrader.php) — one new version-gated block, IIS-only, no effect on Apache/nginx.<preConditions>singleton collision inWebp|Avif/RewriteRules/IIS.php.Technical description
Documentation
No public API surface changed (no new hooks, options, REST routes, or capabilities) —
get_owned_mime_extensions()is a new protected internal method used only by the IIS writer classes themselves. Thedocsskill was run and returnedSKIPfor this reason.New dependencies
None.
Risks
High risk area (live-site-outage bug, cannot validate against a real IIS server in CI). Mitigated by: root-cause fix that structurally prevents future duplicate
<staticContent>collections; a version-gated, IIS-only,WP_Error-guarded self-heal migration that never fatalsadmin_init; and comprehensive DOM-level integration tests covering all edge cases in the approved spec (fresh file, foreign blocks, idempotency, partial failure, graceful skip). Real IIS-server validation is arranged separately by the user before final merge.Mandatory Checklist
Code validation
Code style
Unticked items justification
N/A — all mandatory checklist items apply and are ticked.
Additional Checks
Follow-up tickets
<preConditions>singleton collision in WebP/AVIF rewrite-rules classes (out of scope for this PR)Pipeline status
This PR is intentionally kept in draft pending the reporting user's own review and real IIS-server validation before merge (per the approved spec's locked decision — DOM-level integration tests are accepted as sufficient for the automated pipeline, but production IIS behavior has not been independently verified by a human).