miraheze/ts-portal
A Laravel/PHP package from Miraheze for the TS Portal. It provides the portal’s PHP components and integration points, making it easy to add TS Portal functionality to a Laravel application via Composer.
TSPortal is Miraheze's Laravel-based Trust & Safety portal — the app behind wiki requests, reports, and appeals. Treat it as a standard Laravel app first, and a MediaWiki-integrated app second.
Minimal bootstrap:
composer install, copy .env.example to .env, php artisan key:generate.DB_* at MySQL/MariaDB (the schema uses standard Eloquent migrations; SQLite is fine for tests only).php artisan migrate, then php artisan serve (or Sail/Valet).php artisan test before touching anything. This release bumps PHPUnit, Mockery, and the framework, so get a green baseline first.Where to look first:
routes/web.php and routes/api.php — the API surface is small and is where the security-relevant writekey logic lives.app/Http/Controllers — the request/report/appeal flows.app/Models + database/migrations — the domain is mostly a handful of core models plus status/pivot tables.config/ and .env.example — OAuth, queue, mail, and API settings.First real use case: log in via OAuth as a test user, submit a report or wiki request through the UI, and watch it flow through the staff queue. That single path exercises auth, authorization policies, form validation, notifications, and the API — which is exactly the surface area v37 touched.
OAuth identity as the user source of truth. Users come from MediaWiki via laravel/socialite. Don't build local password auth or duplicate profile fields; read the identity from the Socialite user object and store the stable central ID. When you add a feature, ask "does this need a local user row, or can it read from the OAuth payload?" — the latter is almost always right.
Age/eligibility gating is a policy concern, not a form field. v37 fixed an underage check (GHSA-4835-jf76-3g69). Model this as an explicit, single-source eligibility check used by every entry point (registration, appeal submission, report creation), not an inline if in one controller. If you add a new submission path, route it through the same check.
Writekey-protected API calls. The API validates a writekey; since v37 this uses hash_equals for constant-time comparison. Consume it by sending the key in the configured header from your client, and on the server side keep validation in middleware rather than per-controller. Never store or log the raw key, and never add a second comparison path — reuse the existing one.
Form Requests + Policies. Validation lives in Form Requests, authorization in Policies/Gates. This is the idiomatic Laravel shape and it's what makes the flows testable. When adding a new action on a request/report, add the policy method alongside it rather than checking roles in the controller.
Queue the slow stuff. Anything that talks to the MediaWiki API (creating wikis, posting talk-page notices, syncing statuses) belongs in a queued job. Local development should use QUEUE_CONNECTION=sync; production needs a real worker. Watch for jobs that swallow exceptions — a failed on-wiki notification should surface, not disappear.
WithoutTimestamps over public $timestamps = false. This release migrated models to the attribute form. Follow that convention for new pivot/log models; it keeps timestamp intent visible at the class declaration and is the direction the framework is going.
Testing. Feature tests with RefreshDatabase for flows, Http::fake() for MediaWiki/Guzzle calls, and Mockery for collaborators. Given the Guzzle major bump, mock at the Laravel HTTP client layer rather than the Guzzle handler layer — it survives dependency upgrades.
Http facade wraps Guzzle, so most call sites are insulated — but custom handlers, middleware stacks, Client::create() calls, and anything touching PSR-7 request/response internals may break. Grep for GuzzleHttp\ before deploying and exercise the MediaWiki API integration end-to-end.hash_equals for the writekey and the corrected underage check are both regressions that a naive refactor could reintroduce. If you have local modifications in those areas, rebase them onto the v37 code rather than re-applying your version.composer update -W locally first rather than a bare composer update, and commit the lock file.updated_at-based sorting some admin views may rely on. If a listing looks unsorted or a latest() scope misbehaves, check whether that model was moved to the attribute.php artisan config:clear and queue:restart after changing OAuth or API settings — stale cached config is the most common "my change did nothing" cause here, especially with queue workers.php artisan tinker against a local DB, Log::debug around the MediaWiki client, and Http::fake()/Http::preventStrayRequests() in tests. Enable APP_DEBUG locally only — these flows handle user reports and personal data, so production error pages must stay generic.How can I help you explore Laravel packages today?