Skip to content

Releases: Panxatony/Saito

Saito 8.4.12 "obenauf"

Choose a tag to compare

@github-actions github-actions released this 17 Aug 20:32

[8.4.12] - 2026-08-17 "obenauf"

No migration. One file changed.

php bin/cake.php schema_cache clear
  • ✓ Fixed: the message 8.4.11 added never reached the web server's log. It
    was chained to the original exception, and PHP prints a chained exception
    previous-first — so the entry began with the library's own trace, and the web
    server truncated the line before the useful half. Tried on the test install:
    the operator saw exactly what they saw before 8.4.11.

    It is thrown unchained now and leads the entry. The library's wording is
    repeated inside it, so only its internal frames are lost — which were never
    the part worth reading.

    This is the second correction to the same fix in one evening, and both came
    from running it rather than reasoning about it. 8.4.11 was verified through
    the command line, where PHP prints the whole chain and nothing is truncated;
    the web path, which is the one an operator actually meets, behaved
    differently.

Saito 8.4.11 "beipackzettel"

Choose a tag to compare

@github-actions github-actions released this 17 Aug 20:12

[8.4.11] - 2026-08-17 "beipackzettel"

No migration. One file changed.

php bin/cake.php schema_cache clear

Warning

Before upgrading, check config/.env for values containing a space:

grep -nE '=[^"'"'"']*[[:space:]]' config/.env

Anything that matches needs quotes — export NAME="two words". This applies
to 8.4.10 as well; the release below only makes the failure legible.

  • ✓ Fixed: an unreadable crash when config/.env holds an unquoted value with
    a space.
    The reader introduced in 8.4.10 refuses NAME=macnemo Forum where
    the previous one accepted it — and it refuses it during bootstrap, before
    CakePHP has an error page. What an operator got was HTTP 500 and a stack trace
    naming neither the file nor the remedy.

    This is not hypothetical and was not caught by the migration's own testing:
    the production install went down for five minutes on the day 8.4.10 shipped,
    on a value written years earlier and read without complaint ever since. The
    parser comparison behind that release checked NAME="two words" — quoted —
    and never tried it without the quotes, which is the likelier way to write it.

    The failure remains a failure: continuing without the file would swap a clear
    stop for a confusing database error later. What changes is that the log now
    names the file, repeats the value the library objected to, and says what fixes
    it. Three tests hold down that the reader really does refuse this input and
    that its own words do not tell anybody what to do — the two facts the wrapped
    message rests on.

    docs/update.md and docs/upgrade.md carry the check to run before
    upgrading, which is the only place it helps.

Saito 8.4.10 "nachschub"

Choose a tag to compare

@github-actions github-actions released this 17 Aug 19:47

[8.4.10] - 2026-08-17 "nachschub"

No migration. The JS bundle changed with the Alpine.js update, so the
compiled assets go out with it.

php bin/cake.php schema_cache clear

Two suppliers had stopped producing — one nine years ago, one three — and this
release replaces them. A third was kept, deliberately, because it still delivers
what it is asked for.

  • Δ Changed: the RSS feeds are written with laminas/laminas-feed.
    suin/php-rss-writer last published in 2017 and still declares php >=5.4.0.
    Nothing was wrong with it; it simply stopped.

    The gate was not that the feed still validates but that the guid must not
    move
    — it is what subscribers are keyed on, and changing it makes every
    reader re-announce every posting it has already shown. Compared before and
    after across both feeds, against real postings and not only test data: guid,
    pubDate, link and the posting bodies are byte-identical. Nobody's reader
    will report anything as new.

    Two faults the old library had been swallowing came out in the process, and
    either would have replaced the whole document with an error rather than
    degrading quietly: an empty subject, which replies commonly have, and an empty
    body. Both are now handled.

    Visible differences are equivalent or better: item titles are escaped instead
    of CDATA-wrapped, atom:link is absolute where it used to be a bare path a
    reader cannot resolve, and the channel carries a description instead of an
    empty element. <generator> names the forum without a version rather than
    announcing the library, and the writer's slash:comments — which would have
    told every reader that every posting has no replies — is taken back out.

  • Δ Changed: config/.env is read with vlucas/phpdotenv.
    josegonzalez/dotenv last published in 2023 and sits on m1/env, silent since
    2020 — two dormant layers.

    symfony/dotenv was the first choice and was rejected after measuring: it
    interpolates the unbraced $name form as well, so a value containing a dollar
    sign — a database password, most plausibly — is silently rewritten, and
    truncated where the name is undefined. phpdotenv reads every case tried
    exactly as before.

    A key defined both in the environment and in config/.env no longer
    errors.
    The old loader raised "Key already defined" and answered with an
    HTTP 500; now the environment simply wins. If you removed a key from one side
    to work around that, you can put it back.

    One narrower change: \$ in config/.env now resolves to a literal $
    rather than staying two characters. A password written that way changes
    meaning. Bare $name and ${name} behave as before. See
    update.md.

  • ✓ Fixed: three places said the .env loader had to be switched on. It
    does not — config/bootstrap.php reads the file whenever it exists, and has
    for a long time. docs/deployment-debian.md, the header of
    config/.env.default (which also misspelled bootstrap.php) and the comment
    the other two were copied from now describe what actually happens, including
    that the environment wins twice over: APP_NAME present skips the file
    entirely, and otherwise only unset keys are filled in.

  • Δ Changed: Alpine.js 3.15.12 → 3.16.1, and the build tooling (cssnano,
    eslint, globals, typescript-eslint) to current.

Saito 8.4.9 "altschlüssel"

Choose a tag to compare

@github-actions github-actions released this 16 Aug 20:58

[8.4.9] - 2026-08-16 "altschlüssel"

No migration. One PHP file changed — no assets, no schema.

php bin/cake.php schema_cache clear
  • ✓ Fixed: a remember-me cookie that can never work again is now taken back.
    The token in that cookie has had three parts since 7.0.4 — account, expiry,
    signature. One with two parts is the shape the authentication library used
    before that, and it is refused, because its second part is a password hash and
    it carries no expiry at all. Refusing it was never the problem.

    Nothing cleared it. The browser presented the same dead cookie on every
    request that followed, so the member was returned to the login form over and
    over — every fifteen minutes in the case that surfaced this, second factor and
    all — and could do nothing about it, because the cookie is HttpOnly and
    therefore invisible and untouchable from their side. Anyone who last ticked
    "stay signed in" before 2026-06-30 can be carrying one.

    Only shapes that cannot work are taken back. A current three-part token that
    fails on its expiry or its signature is left alone: judging that belongs to
    the authenticator, and such a cookie carries a real lifetime and leaves on its
    own.

    The discard is written to saito-info.log. The silence is what made this
    expensive to find: when the cookie is refused no authenticator succeeds, so
    what reaches the application is the form authenticator complaining that some
    unrelated address "did not match /login" — a message pointing away from the
    cause. An evening went into session timeouts, the reverse proxy and the
    two-factor tables before the cookie itself came into view.

    What an operator will see: a member in this state is asked to log in one
    last time, and from then on "stay signed in" holds. Nobody who was working
    normally is affected — a valid cookie is never touched.

Saito 8.4.8 "weissblende"

Choose a tag to compare

@github-actions github-actions released this 16 Aug 16:21

[8.4.8] - 2026-08-16 "weissblende"

No migration. One stylesheet per macnemo preset changed; nothing else.

php bin/cake.php schema_cache clear
  • ✓ Fixed: the primary button reads white again. Bootstrap 5 works out a
    button's label colour from its background rather than always using white, and
    on macnemo's orange it picked black — so a button that had looked one way for
    years changed with 8.4.7 and looked wrong.

    Only the label is set back. The orange is untouched: it carries white at
    3.21:1, above the 3:1 the accessibility guidelines ask of large text, which a
    bold button label is. Bootstrap was applying the stricter body-text threshold,
    which is a defensible choice rather than a bug — just not the right one for a
    button.

Saito 8.4.7 "unterbau"

Choose a tag to compare

@github-actions github-actions released this 16 Aug 15:54

[8.4.7] - 2026-08-16 "unterbau"

No migration. The stylesheets and the JS bundle changed, so those go out
with it.

php bin/cake.php schema_cache clear

Nothing here is meant to be seen. The rooms look the same; what carries them has
been replaced — Bootstrap, the web fonts, the admin area's form rendering. The
one visible change is a number on a phone.

  • Δ Changed: the themes run on Bootstrap 5.3.8. 4.6.2 had had no
    maintenance since January 2023, and its JavaScript was never loaded here, so
    this was a maintenance question rather than a security one — but an
    unmaintained dependency is exactly what this line of releases is for.

    A major version does move things: spacing, corner radii and form controls all
    shift a little. Nothing was held to a pixel comparison, deliberately — that
    gate is what turns a migration into a reimplementation.

    Bootstrap 5.3 also brings a colour-mode implementation built on custom
    properties, which the hand-maintained night presets can build on later.

  • Δ Changed: the stylesheets have a spacing and text scale of their own.
    Seventy-three @extend onto Bootstrap's utility classes became real
    declarations against $space-xs$space-xl, $text-quiet and friends.
    @extend .mb-3 said "whatever Bootstrap means by three"; a theme could
    neither reason about that nor override it. Documented in
    customizing.md.

  • Δ Changed: the theme fonts come from @fontsource. The typeface-*
    packages stopped publishing in 2022. Cabin and Fenix are unchanged — same
    faces, same licence, a maintained package.

  • − Removed: the BootstrapUI dependency. It had not been doing anything
    since the CakePHP 5 upgrade: it supplied its helpers through
    Controller::$helpers, a property CakePHP 5 removed, so the admin area had
    been rendering with the framework's own FormHelper for some time without
    anyone noticing. Removing it changes nothing that is rendered — verified
    byte-for-byte across thirteen admin pages.

  • ✓ Fixed: the number of members online now shows on a phone. It is
    anchored to the widget's icon, and that icon is hidden on narrow screens — so
    the count went with it, and the folded state, the one where the figure is
    worth having, was the one state it never appeared in.

Saito 8.4.6 "dienstschlüssel"

Choose a tag to compare

@github-actions github-actions released this 15 Aug 08:52

[8.4.6] - 2026-08-15 "dienstschlüssel"

One migration runs, and it rewrites entries. Not additive like the five
before it: nine columns change type, so the table is rebuilt row by row.
Measured between 70 seconds and four and a half minutes for 680,000 postings,
depending on the server. On MariaDB 11.2 or later the forum stays writable while
it happens; everywhere else the table is readable but not writable until it
finishes. Run it from the command line — PHP's execution limit will cut a
rewrite short through the web updater.

php bin/cake.php migrations migrate
php bin/cake.php schema_cache clear

The key that comes with the office. A forum can now require a second factor of
the people who can change it — and, because a lock without a key is how an
operator loses their own forum, three ways back out.

  • + Added: a forum can require two-factor authentication of its moderators
    and administrators.
    off by default, so upgrading changes nothing for
    anybody; an operator sets it to mod or admin in the admin area. Ordinary
    members are never caught by it — the cost of a compromised member account is
    one member, the cost of a compromised administrator account is the forum.

    Every exemption in it is load-bearing. Somebody who switches the setting on
    while their authenticator app is on a phone in another room must still be able
    to set the second factor up, and to log out; both stay reachable from behind
    the gate, and both are tested one by one. For the case that has no interface
    left at all there is bin/cake two_factor_reset <username>, which clears the
    second factor, the recovery codes, the trusted devices and the passkeys of one
    named account without touching the account itself.

  • + Added: the columns postings are written to now outlive 2038. timestamp
    cannot hold an instant after 2038-01-19; nine columns move to datetime,
    which has no such limit, and useronline.time — a Unix timestamp in a signed
    INT, the same date by another road — becomes BIGINT.

    Eleven years off, and worth doing now rather than under pressure: the
    conversion has to rebuild the tables either way, and the two ways it can go
    wrong quietly are both handled. The session's timezone decides what a
    timestamp becomes when it turns into a datetime, so the migration pins it
    to UTC rather than trusting the host's clock; and a 0000-00-00 left over
    from an older MySQL would abort the whole chain under that server's default
    sql_mode, so those values are let through unchanged instead.

  • ✓ Fixed: the new setting showed its own translation key where its name and
    explanation belong. The admin area reads those from the nondynamic catalogue
    and the strings had been added to default, so the screen offered
    2fa_required_from_role_exp as if it were a sentence. A test now renders every
    settings label and fails on any key that reaches the page untranslated.

  • ✓ Fixed: the passkey is mentioned where people look for it. It could only
    be found by opening the second-factor screen and reading to the end, which is
    not where somebody wonders whether their fingerprint would work here.

  • Δ Changed: the upgrade and update documents describe what this migration costs
    and which releases need the command line, and the bot-filter section explains
    the exemption static files need — a challenged stylesheet arrives as
    Content-Type: text/html and the forum renders unstyled, with nothing in the
    log to find.

Saito 8.4.5 "handschlag"

Choose a tag to compare

@github-actions github-actions released this 07 Aug 08:05

[8.4.5] - 2026-08-07 "handschlag"

Two migrations run. One adds webauthn_credentials; the other deletes
credentials belonging to accounts that no longer exist, and touches nothing that
belongs to a live one. Neither has a down(). The stylesheets and the JS bundle
both changed, so those go out with it.

php bin/cake.php migrations migrate
php bin/cake.php schema_cache clear

A handshake instead of six digits: the device you are holding confirms the login
itself. And the release the account deletion should always have been — asking to
be erased now erases everything.

  • + Added: passkeys as a second factor. Touch ID, Face ID, Windows Hello or
    a hardware key instead of typing the code. The operating system checks the
    fingerprint or the face on the device and hands the forum a signature; no
    biometric reaches the server, and none can.

    An addition, never a replacement, in three ways that all matter. A passkey can
    only be registered once the code is already on, because it lives in one
    machine's secure enclave and the recovery codes that come with the code are
    the way back from a lost device. The code field stays exactly where it was, so
    a browser without JavaScript or a device without a sensor reaches the same
    second step. And the button stays hidden until the browser confirms an
    authenticator exists, because one that cannot work is worse than none.

    Two things surprise people once and then never again, so they are in the help:
    a passkey belongs to the device it was made on — register each one you use —
    and it works for one address only, which is exactly why a fake site cannot
    harvest it.

  • ✓ Fixed: deleting an account now deletes what it could be signed in with.
    It did not. Measured before the fix, a deleted member left behind an encrypted
    second-factor secret, ten hashed recovery codes, a trusted-device token and a
    passkey — thirteen rows for an account that no longer existed, plus any
    outstanding password-reset token.

    The five tables arrived one per release through the 8.4 line, each built and
    tested on its own, and none of those test runs asked what happens when the
    account goes. It matters most against the reason the data export already
    exists: this forum implements the GDPR's right of access as self-service, and
    leaving credentials behind after an erasure request does not sit beside that.

    The migration clears what has already accumulated. A fix that only applied
    from today would leave exactly the people who already exercised that right
    still on file.

  • Δ Changed: the API scope is decided by one test instead of two that could
    disagree.
    /api/v2 chooses JWT-only authentication, and the check for it was
    an unanchored match: any path merely containing the string picked it, so
    /entries/view/1/api/v2 would have shown a signed-in member as logged out.
    Stricter rather than looser, so never an escalation — but the CSRF exemption
    beside it has always been an anchored prefix, and a loose half of a pair
    invites harmonising them in the wrong direction. Both now use the same test.

Saito 8.4.4 "stammgast"

Choose a tag to compare

@github-actions github-actions released this 06 Aug 22:16

[8.4.4] - 2026-08-07 "stammgast"

One migration runs. It adds two_factor_trusted_devices; there is no
down(). The stylesheets changed — Bota, Nova and Macnemo, all three — so
those have to go out with it. The JS bundles did not.

php bin/cake.php migrations migrate
php bin/cake.php schema_cache clear

A regular is somebody the house recognises on the way in. Turning on the second
factor in 8.4.2 quietly took "stay signed in" away, and this puts it back for
devices that have actually proved themselves.

  • ✓ Fixed: "stay signed in" works again with a second factor. 8.4.2 shipped
    it broken in two ways at once, and the note in that release — that a
    remember-me cookie is refused outright for an enrolled account — described the
    second of them as if it were the whole design. It was half a design.

    The checkbox sits on the password form, but the cookie it asks for can only be
    minted a step later, once the code is in, and nothing carried the answer
    across: no remember-me cookie was ever written for an enrolled account. Even
    had one been, it was turned away at the door. The result was that switching on
    2FA meant signing in again and again — most visibly on a phone, where the
    browser drops sessions freely.

    The reasoning behind the refusal still holds: a remember-me cookie is
    stateless, validating against a username and a password hash, so a cookie made
    before an account enrolled cannot be told from a later one and no server can
    revoke either. What was missing was somewhere to write down which is which.

    two_factor_trusted_devices is that place. A row is written only after a
    second factor has actually been proved, and its token travels in a companion
    cookie; a remember-me cookie is honoured for an enrolled account only when a
    matching, unexpired row sits behind it. Cookies from before enrolment have no
    row and are still refused, so nothing that was closed has been reopened.

    Because the trust is a row now rather than a signature, it can be taken back.
    Signing out drops the device doing it and leaves the others alone; switching
    the second factor off, changing the password, or an administrator resetting it
    drops every one. The token is stored as a SHA-256, so reading the table yields
    nothing that could be put in a cookie, and the cookie itself carries the same
    flags as the remember-me cookie it travels with.

  • ✓ Fixed: code was unreadable inside an alert in every dark theme. The base
    stylesheet gives code the page's text colour, which is near-white in a dark
    preset; Bootstrap's contextual alerts keep their light backgrounds there,
    deliberately. Neither is wrong alone — the base rule reaching inside the alert
    is. Measured, the two-factor recovery codes came out at about 1.2:1 on pale
    green, which is not "hard to read" but absent, and they are shown exactly once
    with no second chance. Inside an alert, code now takes the alert's own colour.

    The recovery codes also had a class with no styles behind it, and now read as
    something meant to be copied off a screen: monospace, tracked, with room
    between the lines.

  • Δ Changed: league/commonmark is updated to 2.9.0. Six advisories were
    published against it an hour before this release was built — five denial of
    service, one bypassing the AttributesExtension filter on href and src
    and the release refused to package over them.

    None of them is reachable from the forum: markdown is parsed only for the help
    pages and the guided tour, which are files shipped with the release, while
    postings go through Saito's own parser. So this is hygiene rather than an
    incident. It still blocks a release, because knowingly shipping a dependency
    an hour after it was declared vulnerable is not a thing to do.

Saito 8.4.2 "zweitschlüssel"

Choose a tag to compare

@github-actions github-actions released this 06 Aug 13:53

[8.4.2] - 2026-08-06 "zweitschlüssel"

One migration runs. It adds two_factor_credentials and
two_factor_recovery_codes; there is no down(). Neither the stylesheets nor
the JS bundles changed, so those can stay as they are.

php bin/cake.php migrations migrate
php bin/cake.php schema_cache clear

A second key to the same door. Members can now protect their account with a
code from an authenticator app on top of the password — off by default, and
entirely their own choice.

Nothing changes for anybody who does not switch it on.

  • + Added: two-factor authentication (TOTP). Set it up in your profile: the
    forum shows a QR code, your app produces a six-digit code, and only once you
    have proved you can produce one does the second factor go live. Ten single-use
    recovery codes come with it, shown once, for the day the phone is gone.

    From then on the login has two steps. The second one happens inside the login
    overlay rather than on a page of its own, because it is a second step, not a
    second destination.

    The part that matters and cannot be seen: the password alone no longer
    signs anybody in.
    It is verified, and then nothing happens — no identity, no
    session, and above all no "remember me" cookie, which is minted at exactly the
    moment 2FA has not reached yet. A remember-me cookie made before an account
    enrolled is refused outright: it validates against a username and a password
    hash, so no server can revoke it, and the only way to stop it walking past the
    second factor is to turn it away at the door.

    Secrets are encrypted at rest, recovery codes are hashed like passwords, and
    neither table is mass-assignable. Turning the second factor off, or minting
    fresh recovery codes, asks for the password again — a borrowed session should
    not be able to do either quietly.

    Lost the phone and the codes? An administrator can clear the second factor
    from the user list. It asks for the administrator's own password and is logged
    either way, successful or not: removing somebody's second factor is exactly
    the step an attacker who reached an admin session would take, so it leaves a
    trace that can be read afterwards.

  • ✓ Fixed: the whole-forum export was written world-readable. It holds every
    member's e-mail address, and their IP addresses where the forum stores them,
    and the default umask made it 0644 — on a shared host, the membership list
    for any local account. It is 0600 in a 0700 directory now, and the command
    refuses to write rather than write unprotected.

  • Δ Changed: the nightly security job also checks that the security headers
    still arrive.
    Advisories were the only thing it watched, but server
    configuration drifts too and far more quietly: one installation was serving
    every static file and every upload without X-Content-Type-Options, and
    another had no HSTS at all. Nothing fails when that happens — pages render,
    uploads load — so it took a scheduled check to notice. Three probes per
    installation, because nginx applies headers per location and locations drift
    apart independently.

  • Δ Changed: a command-injection advisory in squizlabs/php_codesniffer is
    patched (dev-only, never runs on a server), and the one third-party GitHub
    Action is pinned to a commit rather than a tag its owner could repoint.