refactor: return Response objects instead of writing output
Stage 2c of the HTTP kernel migration, applied to the remaining modules and to
the legacy system/src classes. After this commit
grep -rnE 'exit|die\(|header\(|setcookie\(|http_response_code\(' over modules/ is
empty, and the 13 setcookie() calls tracked as a 2c debt are gone.
- collections/online/registration/notifications: dead exit after a never-typed
pageNotFound(), a 403 middleware, and two Location+exit pairs.
- guestbook/admin: three 403 branches, a JSON upload endpoint rebuilt on
JsonResponse, and both admin gates, which used to answer with
header('HTTP/1.0 403 Forbidden') + echo + exit.
- profile/login: nine 403 branches, one dynamic status, and the last four
setcookie() calls.
- album: eight controllers sharing the resolveContext(): T|string sentinel, now
T|Response; DownloadPhotoController redirects to a static file URL, so it
returns RedirectResponse rather than BinaryFileResponse.
- forum: ForumErrorRenderer returns a Response carrying the status, migrated
together with all 33 callers so no error page ends up with a 200; the 301 in
ForumIndexController is preserved explicitly.
- system/src/Comments.php: the three Location headers become redirect(). This
fixes a live bug: the wrapping controllers build a Response at status 200,
and Symfony's sendHeaders() overwrote the implicit 302, so the browser got a
Location header on a 200 response and ignored it.
Cookies are rebuilt with Cookie::create(..., secure: false, httpOnly: false,
sameSite: null) so no attribute the legacy calls never sent gets added.
ChangePasswordController is the only setcookie() without a path argument; the
RFC 6265 default path is reproduced explicitly instead of silently widening the
cookie to '/'.
Deliberately left, documented in the plan: BanIP.php runs from the bootstrap
before Kernel::handle(), so throwing from there would answer 500 instead of the
ban; both UserFactory::userUnset() have no response object to attach a cookie
to. Both are stage 5.
Refs: .claude/http-kernel-migration-plan.md stage 2c
the legacy system/src classes. After this commit
grep -rnE 'exit|die\(|header\(|setcookie\(|http_response_code\(' over modules/ is
empty, and the 13 setcookie() calls tracked as a 2c debt are gone.
- collections/online/registration/notifications: dead exit after a never-typed
pageNotFound(), a 403 middleware, and two Location+exit pairs.
- guestbook/admin: three 403 branches, a JSON upload endpoint rebuilt on
JsonResponse, and both admin gates, which used to answer with
header('HTTP/1.0 403 Forbidden') + echo + exit.
- profile/login: nine 403 branches, one dynamic status, and the last four
setcookie() calls.
- album: eight controllers sharing the resolveContext(): T|string sentinel, now
T|Response; DownloadPhotoController redirects to a static file URL, so it
returns RedirectResponse rather than BinaryFileResponse.
- forum: ForumErrorRenderer returns a Response carrying the status, migrated
together with all 33 callers so no error page ends up with a 200; the 301 in
ForumIndexController is preserved explicitly.
- system/src/Comments.php: the three Location headers become redirect(). This
fixes a live bug: the wrapping controllers build a Response at status 200,
and Symfony's sendHeaders() overwrote the implicit 302, so the browser got a
Location header on a 200 response and ignored it.
Cookies are rebuilt with Cookie::create(..., secure: false, httpOnly: false,
sameSite: null) so no attribute the legacy calls never sent gets added.
ChangePasswordController is the only setcookie() without a path argument; the
RFC 6265 default path is reproduced explicitly instead of silently widening the
cookie to '/'.
Deliberately left, documented in the plan: BanIP.php runs from the bootstrap
before Kernel::handle(), so throwing from there would answer 500 instead of the
ban; both UserFactory::userUnset() have no response object to attach a cookie
to. Both are stage 5.
Refs: .claude/http-kernel-migration-plan.md stage 2c