История коммитов

.
refactor(collections,system): replace raw $_SESSION with Session facade
Collections (4 admin controllers): $_SESSION['success_message'] → $this->session->flash()/getFlash(), removed pullFlash() helper.

System (4 files):
- Comments.php: $_SESSION['code'] → $this->session->set/get, fixed $owner type (bool→int) for isBlockedBy()
- TranslatorServiceFactory.php: $_SESSION['lng'] → $session->set/get/has, injected via $container
- Validator/Rules/Captcha.php: $_SESSION[$this->sessionField] → $session->has/get via di()
- Security/Csrf.php: $_SESSION['_csrf'][$token_id] → $this->session->set/get, constructor injection
.
refactor(admin): replace raw $_SESSION with Session facade
- 14 admin controllers now inject Johncms\Http\Session\n- Use session->flash()/getFlash() for success messages\n- All files are final readonly classes with constructor injection
.
refactor(news): replace raw $_SESSION with Session facade
- Inject Johncms\Http\Session into AdminController, AdminSectionController, AdminArticleController, Article

- Use flash()/getFlash() for success messages, set()/get() for delete_token and view tracking

- Remove redundant $services->set(Article::class) that lacked autowire

- Make Article final readonly with promoted constructor properties
.
refactor(system): remove PHP version check from bootstrap
Composer's autoloader already enforces the PHP version requirement via platform config, making this check redundant.
.
docs(plan): record stages 2c, 2d and 3b as done
Per-module notes for the 2c sweep, the 2d decision to keep compression in the
web server, and the 3b session/send() reasoning. Also records what was left
deliberately (BanIP, both UserFactory::userUnset(), the path-less cookie in
ChangePasswordController) and which stage picks it up.
.
feat(http): complete the kernel with a Response pipeline and terminate()
Stage 3b of the HTTP kernel migration.

MiddlewareInterface::handle() now returns a Response instead of mixed, across
all 15 implementations. To make that true at runtime and not just in the
signature, normalization moved inside the pipeline: the handler closure passed
to MiddlewareDispatcher::dispatch() calls ResponseNormalizer::normalize()
itself, and the dispatcher is typed on Response throughout. The transitional
controller contract (Response|string|null) is unchanged, it is just normalized
one step earlier.

The legacy status seam is gone with it. handleRaw() no longer reads
http_response_code() to build the status, and handle() no longer resets it to
200: after stage 2c no controller sets the status that way, the only remaining
callers being GlobalErrorHandler and the pre-kernel BanIP.

Kernel implements TerminableInterface. UserStat and the mail queue moved into
terminate(): both are post-response side effects the response does not depend
on. The mail queue condition is kept 1:1, including the isSuccessful() check.
public/index.php is down to handle() -> send() -> terminate().

send() is now the full one, with fastcgi_finish_request(). That is only safe
because handle() closes the session once the response is built (guarded by
PHP_SESSION_ACTIVE and non-console mode): PHP otherwise holds the session file
locked until shutdown, so the next request from the same visitor would queue
behind the mail batch running in terminate().

Refs: .claude/http-kernel-migration-plan.md stage 3b
.
refactor(http): drop the global output buffer
Stage 2d of the HTTP kernel migration.

news/Admin/AdminController::index() was the last controller writing to the
output directly; it now returns a Response. With that gone, the
ob_start('ob_gzhandler') block at the end of system/bootstrap.php can go too:
compression is the web server's job (nginx already has gzip on, and text/html
is compressed unconditionally). The buffer also masked premature output, which
would have silently sent headers and lost the status or the Location of the
Response being built.

The four ob_start()/ob_get_clean() pairs wrapping the legacy Comments class are
self-contained and do not depend on the removed global buffer.

Refs: .claude/http-kernel-migration-plan.md stage 2d
.
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
.
refactor(help,redirect,mail,login): return Response objects instead of writing output
Stage 2c of the HTTP kernel migration, applied to four small modules: 33
sites in 13 files, no exit/header()/http_response_code() left in any of them.

- help: legacy redirect handler keeps its 301; SetMySmiliesController's three
mutually exclusive header() branches sharing one exit became three returns
with the branch selection unchanged.
- redirect: the four redirects now return RedirectResponse with their targets
untouched. This module redirects to external addresses by design, so no
same-origin check was added.
- mail: DownloadFileController redirects to a static file URL, so it returns
RedirectResponse rather than BinaryFileResponse.
- login: both controllers return RedirectResponse; setcookie() is left as is.

Also recorded in the plan two debts found while reviewing this batch:

- The 13 setcookie() calls belong to stage 2c but are untouched by the
per-module sweep, so 2c must not be called done until they are gone.
- LogoutController puts a FILTER_SANITIZE_SPECIAL_CHARS-filtered Referer into
an href. That filter does not block javascript:, so the cancel button on
/logout can execute script. It predates this change and belongs to the
escaping task (1a-ter), so parity is kept here.

Refs: .claude/http-kernel-migration-plan.md stage 2c
.
refactor(news): return Response objects instead of writing output
Stage 2c of the HTTP kernel migration, applied to the news module: no
exit/header()/http_response_code() left in modules/news.

- Admin article and section deletion used exit($exception->getMessage()),
which printed the exception text to the visitor with status 200 and logged
nothing. Both now log the exception and return a real 500, with details
gated behind DebugDetailsPolicy like the rest of the error output.
- Comment and file-upload endpoints return JsonResponse; every non-200 status
(400, 403, 404, 422, 500) is preserved.
- Helpers::returnJson() echoed JSON and called exit from a utility class. It
had two callers, so it is removed and both build their response directly.
- Admin redirects return RedirectResponse; all were 302 already.

The upload error branches still put $e->getMessage() into the JSON body,
bypassing DebugDetailsPolicy. That predates this change and the frontend shows
the text, so parity is kept here and the fix is noted in the plan instead.

Refs: .claude/http-kernel-migration-plan.md stage 2c