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

simba77
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
simba77
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
simba77
refactor(system): remove PHP version check from bootstrap
Composer's autoloader already enforces the PHP version requirement via platform config, making this check redundant.
simba77
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.
simba77
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
simba77
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
simba77
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
simba77
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
simba77
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
simba77
feat(http): introduce HTTP kernel with Request/Response objects
Replaces the PSR-7 request with symfony/http-foundation and adds a real HTTP
kernel, so the application can run in a long-running runtime without leaking
state between requests. Covers stages 0-3a of the migration plan plus stage 2c
for the downloads and library modules.

Request (stage 1):
- Johncms\Http\Request extends HttpFoundation Request with nine explicit
accessors (body/bodyInt/bodyList/bodyInts/hasBody/queryParam/queryInt/
queryInts/isPost); filterVar() is gone.
- RequestFactory configures trusted proxies, headers and hosts. Default is to
trust no proxy: a non-empty default is both unsafe behind docker port
publishing and impossible to override, because configs merge recursively.
- UploadedFileDTO/Mapper remove the HTTP type from Application and Domain.
- TrimStringsMiddleware preserves the historical always-trim behaviour.
- Johncms\System\Http\* is removed; guzzlehttp/psr7 is dropped.

Response and control flow (stage 2):
- redirect(), pageNotFound() and checkRedirect() throw instead of calling exit;
ExceptionResponseFactory turns those into responses. MethodNotAllowedException
replaces a 405 body that used to be served with status 200.
- ResponseNormalizer accepts Response|string|null, so controllers migrate one at
a time. Render no longer swallows template errors into the page body.
- downloads and library no longer emit exit/header()/http_response_code():
DownloadArticleController streams with its headers preserved 1:1, and each
module error renderer moved to Response together with its controllers.

Kernel (stage 3a):
- Johncms\Http\Kernel implements HttpKernelInterface and maps redirect, 404,
405, 400 and 500 centrally. public/index.php is down to 38 lines.
- Request became synthetic so the kernel can publish the current one.
- New tests/Functional suite boots the app once and drives real requests.

Static analysis gate (stage 1.5):
- PHPStan level 5 replaces psalm, which excluded 20 of 21 modules, and runs in
verify.sh between cs-check and test.

Fixes found while doing the above:
- Files could not be downloaded at all and screenshot upload was dead: both
compared queryParam() to null, which is never true. Same defect hit album and
admin more quietly. PHPStan had reported all four as notIdentical.alwaysTrue,
but the stage 1.5 baseline had accepted them.
- Album validation errors fataled on a readonly property redeclaration, JSON
posts to news comments fataled on getContent(), and the 10.x installer could
not run at all because stage 1d never reached public/install/.
- Rating::addVote() redirected to an unvalidated Referer; it now keeps only the
path and query, and rejects paths starting with // or /\.

BREAKING: sessions are unaffected here, but Johncms\System\Http\* is gone and
installations behind a proxy must now set http.trusted_proxies explicitly, or
getIp() returns the proxy address. A cached container dump must be deleted.