The previous MediaAttacher had ~5 responsibilities crammed in one class
(URL validation, HTTP fetch, streaming, MIME check, Storage write,
post.media merge) plus an `if ($app->runningUnitTests()) return true`
inside the SSRF guard — production code that knew about test mode.
Split into three:
- UrlSafetyGuard (interface) + DnsUrlSafetyGuard (default impl). The
full SSRF check is now isolated and unit-testable in
tests/Unit/Services/Post/DnsUrlSafetyGuardTest. Tests bind a permissive
guard in tests/TestCase::setUp so feature tests using Http::fake() with
synthetic hosts (cdn.example.com) still work — the runningUnitTests()
check inside production code is gone.
- MediaDownloader: takes a URL, returns a temp file + MIME or null. Uses
Http::sink with a Guzzle progress callback that throws once maxBytes
is exceeded, so we abort mid-stream without buffering the body in PHP
memory (the previous chunk loop was append-to-string which defeated
the whole point of streaming).
- MediaAttacher: pure orchestrator. Calls MediaDownloader, validates the
MIME against the post's enabled platforms, persists via Storage::putFileAs,
appends the Media record under a row lock. Drops from 275 to ~190 lines
with zero responsibility overlap.
The IO contract (`null` on failure, `['path','mime','bytes']` on success)
keeps MediaAttacher free of the temp-file lifecycle on the happy path —
ownership is documented in MediaDownloader's PHPDoc and the orchestrator
unlinks via try/finally in processOne().
Bind the interface in AppServiceProvider so production resolves
DnsUrlSafetyGuard automatically; tests override it.
7 new unit tests cover the SSRF cases (loopback, RFC1918, link-local,
zero/broadcast, IPv6 loopback/ULA/link-local, plus a public-IP positive
test). The 14 existing AttachMedia feature tests stay green.