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

.
refactor(library): who may read it is a permission, not the mod_lib setting
library.view, held by the guest and the user roles; the middleware of the module asks it
instead of comparing the setting and the number in two branches. A library closed to
everybody was still open to the administrator, and the migration writes that exception out
the way it did for the forum.

No library.post to go with it: nothing in the module ever gated writing by the setting.
Submitting an article asks for a session and lands in the pre-moderation queue, and inventing
a permission that nothing checks is how a catalogue of rights starts lying.

The 'Library is closed' banner on the index goes: whoever sees the page may read it.

After updating: auth:apply-default-permissions --force, then auth:migrate-module-access.
.
refactor(guestbook): who may read and write is a permission, not the mod_guest setting
guestbook.view and guestbook.post, held by the guest and the user roles. The guestbook is the
module where the two questions were most obviously one number: 2 meant guests could write, 1
meant they could only read, 0 meant nobody could do either — and the code asked
'mod_guest === 2' in the middle of a condition about bans.

GuestbookAccess loses the configuration it was built with: canRead() and canWrite() ask the
two permissions, canClear() already asked one. The 'the guestbook is closed' banner goes the
way of the forum's: whoever sees the page may read it, and the closed guestbook was only ever
seen by the staff, for whom the role editor now says it.

A guestbook closed to everybody still let whoever may empty it in, which was another
comparison against the number; the migration writes it out as a grant to the administrator.

After updating: auth:apply-default-permissions --force, then auth:migrate-module-access.
.
refactor(forum): who may read and write is a permission, not the mod_forum setting
The second access system of the CMS starts moving into the roles. One number said whether the
forum was open to everybody, to registered visitors, to nobody, or to everybody for reading
only — four answers to two questions, which is exactly why it had to be a number. They are
forum.view and forum.post now, held by the guest and the user roles.

The read-only forum is the case worth spelling out: it meant 'everybody reads, only the staff
write', and one number could say that only because there was no way to say two things. It is
forum.view with the guest and the user, forum.post without the user — which is why the staff
roles carry forum.post of their own rather than inheriting it through 'user'.

auth:migrate-module-access reads the settings of a running site and writes what they meant
into the roles. It is the first migration here that also revokes: 'the forum is for registered
visitors' is the guest role *without* forum.view, and a migration that only added would hand
the forum back to the guests of every site that closed it. The exception a closed forum made
for the administrator was a comparison against the number rather than a setting, so it is
written out in the migration as a grant that is never taken back.

The banner above the listings said 'Forum is closed' or 'Read only' by reading the same
number. It says 'Read only' to whoever may not write, which is a statement about the visitor
rather than about the site — the 'closed' half was only ever seen by the staff of a closed
forum, and what a role may do is now in the role editor.

The row of the forum leaves /admin/modules-access, which is being emptied module by module
and disappears at the end of this series.

After updating: auth:apply-default-permissions --force, then auth:migrate-module-access.
.
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.