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

.
refactor(router): gate a route with a permission instead of an if around it
A route was declared or not declared depending on who was asking: nine routes.php files took
the current user as an argument and wrapped their staff routes in 'if ($user->rights >= 9)'.
The route is declared once now and says what it needs — ->permission('news.manage') — while
RequirePermissionMiddleware, which the kernel puts in front of any route carrying the
attribute, decides who gets through.

The permission is an attribute of the route rather than a middleware of its own: middlewares
are resolved by class name and live as long as the process, so they cannot take 'which
permission' in a constructor, and one class per permission would be a hundred classes saying
the same thing. Same mechanism Route::module() and Route::withoutCsrf() already use. A group
can name a permission for all of its routes; one naming its own keeps it.

404 becomes 403, on purpose. A route that does not exist and a route that is not yours are
different things, and the second one is debuggable: the addresses of the admin panel are
known anyway. A guest is redirected to sign in instead, since for them it is usually the
session that is missing. The rare route whose very existence is the secret asks for
->permission('...', hidden: true) and answers 404 as before.

The branches that only asked for a session — the file uploads of the forum and the guestbook,
the writing half of the forum — became RequireAuthMiddleware, which takes no parameter and
is therefore an ordinary middleware. The forum ones are listed and given the middleware in a
loop rather than put in a nested group: a nested group compiles after the '/forum/{sectionPath}'
catch-all of its parent and would never be matched.

What this buys beyond uniformity: the route collection no longer depends on the visitor, so
RouteCollectorFactory asks the container for nothing, the collection can be dumped and
cached, and routing no longer has to identify the visitor first. Three new permissions come
with it — admin.system_check, news.manage and collections.manage — plus news.comments.post,
which is what the picture upload of a comment is worth and what a ban takes away.

The signature of routes.php is a breaking change for third-party modules; it lands in the
same 10.0 break as the removal of the number, and docs/ has the section for module authors.

An installation that has already run auth:apply-default-permissions needs it once more with
--force.
.
refactor(forum): pick the curator candidates by their roles, not by the number
The last query of the forum against users.rights: the list a curator is chosen from excluded
the staff with 'rights < 6 and rights != 3'. It excludes whoever holds a granted role now —
the same definition the list of the administration uses — because somebody who already
moderates the forum has no need to be appointed to one topic of it.

The downloads and the library moderators used to appear in the list, since neither number was
excluded. They no longer do, which follows the reading of the roles the module settled on one
commit earlier: a moderator of another section is staff, not a member to appoint.
.
refactor(community): find the staff by their roles, not by the number
The list of the administration was 'users.rights > 0' — a query, not a comparison, which is
why it outlived the rest of stage five. The staff are now the accounts holding a role that
was granted to them and has not run out; the roles applied to everybody by default are not
a grant, which is what keeps the whole register out of the list. It is the same definition
the adaptive antiflood already uses for 'is anybody of the staff around'.

The order is by the highest role of the account, taken as a column of its own rather than
through a join: an account holding two roles would otherwise appear twice. Grouping by the
account instead was the first attempt and MariaDB rejects it under ONLY_FULL_GROUP_BY.
.
refactor(system): ask permissions in the two legacy classes of the core
Counters and Comments were the last of the core still comparing against users.rights, and
they were skipped when the rest of the core was converted because both reach across into the
modules. They still do — Counters queries the tables of the library and the downloads by
name — so they now ask those modules' permissions by name as well: the counter of the
moderation queue asks downloads.moderate, the queue of the library library.moderate, the new
photos album.moderate, the unread topics forum.deleted.view and the admin chat
guestbook.admin_club.view.

The block of notifications for the staff is split the same way it was split in the module
that renders it, and for the same reason: counting the queue of the library for whoever may
work it, rather than for everybody above a 7.

The shared comments engine gets system.comments.moderate — the fixed 'access_level = 6' it
carried is a permission now — and stops storing the number of whoever replied. A reply
records the level of its author and whether they were staff, and the guard against
overwriting the reply of somebody above you compares levels. Replies written before this
carry the old number, and the closest role at or below it is what that number stood for, so
they keep their meaning instead of quietly ranking as nothing.

An installation that has already run auth:apply-default-permissions needs it once more with
--force.
.
refactor(theme): ask permissions for the staff links of the menu
The link to the admin panel asks admin.access and the link to the admin chat asks
guestbook.admin_club.view, instead of both hanging off one number that had to be at least
1 for either and at least 7 for the first. They are two separate entries now, because they
are two separate answers: a moderator who has the chat but not the panel used to be handled
by nesting one condition inside the other.

The remaining reads of the number in this template are the four module links, which turn on
the mod_* settings and move with them.
.
refactor(modules): ask permissions instead of the rights number in the rest of them
What was left outside the big modules, and most of it turned out to be two questions the
core already answers. The smilies kept for the staff — the catalogue, the page of one's own
set, the private message being written — ask system.smilies.admin. Where a visitor came
from — the row of the online list, of the community list, the meta of a private message,
and the pages of the guests and of the IP activity — asks users.origin.view, the same key
the forum and the downloads settled on.

The comments of the news get news.comments.moderate. Deleting somebody else's comment
asked for a 6 in one place and for a 7 in the other, for the same action on the same
screen; there is one answer now, and it also decides who sees the address a comment came
from.

The notifications of the staff stop being one block behind one number. The queue of the
library is offered to whoever may work it, the queue of the downloads likewise, and the
accounts awaiting registration to whoever may open the admin panel — a library moderator
was not shown the queue that is theirs to clear.

What is left for later: the two legacy classes of the core, Counters and Comments, the
conditions still wrapped around route declarations, and the listing of the staff, which
reads the column with a query rather than a comparison.

An installation that has already run auth:apply-default-permissions needs it once more
with --force.
.
refactor(guestbook): ask permissions instead of the rights number
The guestbook asked the number in five places and meant four different things. Emptying it
is guestbook.clear, editing and deleting the entries of others is guestbook.entry.manage,
the reply shown as coming from the staff is guestbook.entry.reply, and the admin club — the
second guestbook behind the same pages — is guestbook.admin_club.view. The list of accounts
allowed into the club by hand stays where it was, next to the permission.

The reply link stops lying. It appeared on every entry the visitor could edit, while the
screen behind it asked for a super moderator: it now asks the permission the screen asks.

Under an entry there were two things at once — where it came from and what may be done to
it — and one number decided both. They are two permissions now, so the meta block carries
either half on its own: users.origin.view brings the address and the user agent,
guestbook.entry.manage brings the buttons.

RoleLevels learns to answer about a whole page at once. Comparing the visitor against the
author of every entry would have been a query per row; grantedLevelsFor() asks the roles
of all the authors in one, and a test holds it to the same answer the per-account call
gives.

The profile module asks guestbook.admin_club.view too, which was the number it had left:
whether the entries of the club count as somebody's activity is the guestbook's question.

An installation that has already run auth:apply-default-permissions needs it once more
with --force.
.
refactor(library): ask permissions instead of the rights number
Fourteen comparisons against users.rights become one permission: library.moderate, held by
the library moderator, the super moderator and the administrator. 'rights > 4' meant that
and nothing else — creating and editing sections, editing and deleting articles, deleting
their images, the pre-moderation queue, and reading an article that has not been published
yet.

The one check that was not about the library asks the core permission it belongs to: an
article rendered the smilies kept for the staff for anybody with a number above zero, and
now for whoever holds system.smilies.admin.

The provider is registered by hand in the module's services file: this module does not
load its Services directory as a whole, and a provider nobody instantiates declares
nothing.

What is left in the module for later: the mod_lib config, which moves when the config keys
become permissions of the guest and the user roles.

An installation that has already run auth:apply-default-permissions needs it once more
with --force.
.
refactor(album): ask permissions instead of the rights number
Eighteen comparisons against users.rights become two permissions the albums declare, and
the two constants every use case carried its own copy of — MODERATOR_RIGHTS = 6 and
ADMIN_RIGHTS = 7 — are gone with them.

The two numbers stood for one thing. Deleting somebody else's photo took a 6, editing
their album took a 7, opening a private album took a 7 while downloading a photo out of
the same album took a 6 — differences no rule explains and none of them survives as a
distinction: acting on the albums and the photos of another account is album.moderate,
which the super moderator and the administrator hold by default. The bypass argument of
the access guard goes away with them; there is one answer to 'may this visitor look past
the lock', not one per caller.

The comments of the photos are switched by the setting of the downloads — this module
took them from there — and the 'rights >= 7' that read them past a switched-off setting is
album.comments.always_view. The setting stays a setting; only who may pass it is a
permission.

An installation that has already run auth:apply-default-permissions needs it once more
with --force.
.
refactor(downloads): ask permissions instead of the rights number
Twenty-odd comparisons against users.rights become four permissions the downloads declare.
'rights === 4 || rights >= 6' says downloads.moderate, the 'rights <= 6' of moving a file
says downloads.file.move, the two 'rights === 9' of the folder form say
downloads.upload_rules.manage, and the 'rights >= 7' that let somebody past a switched-off
setting says downloads.comments.always_view — which is what it always meant, since the
toggle of the comments is a setting of the module and not a permission of anybody.

Two dead ends close. The Move File item sat in the same menu as Edit and Delete, shown to
the staff of the section, while the screen behind it asked for an administrator: it now
asks the permission the screen asks. And creating a folder took 'users may upload here'
from the request whatever the visitor was allowed — only the check of the formats was
gated — so a folder could be opened up by anybody who could reach the form; both the field
and its formats now belong to the one permission.

What is left in the module for later: the checks bound to the mod_down config, which move
when the config keys become permissions of the guest and the user roles, and the staff
smilies of the comment author, which need the column until stage 6 finds it a replacement.

An installation that has already run auth:apply-default-permissions needs it once more
with --force.