Files
ppeasy_vg12/app/Support/Demo.php
T
2026-10-09 08:21:06 +02:00

164 lines
5.1 KiB
PHP

<?php
namespace App\Support;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Routing\Route;
use Illuminate\Support\Str;
/**
* The public demo policy.
*
* The rules live in config/demo.php rather than in guards scattered through
* the controllers, so the same code base can be deployed as a demo or as a
* production install without a diff. App\Http\Middleware\DemoMode enforces
* them; nothing else in the application needs to know demo mode exists.
*/
class Demo
{
public static function enabled(): bool
{
return (bool) config('demo.enabled');
}
/**
* One of the shared accounts the demo hands out.
*
* The single place that decides what "the demo account" means. Never
* compare a username to 'admin' at a call site.
*/
public static function isProtectedUser(?User $user): bool
{
if (! self::enabled() || ! $user) {
return false;
}
// Blanks are dropped from both sides, so a stray empty entry in the
// config does not end up protecting every user without a username.
$protected = self::withoutBlanks(array_map('strtolower', (array) config('demo.protected_users', [])));
$identifiers = self::withoutBlanks([
Str::lower((string) $user->username),
Str::lower((string) $user->email),
]);
return array_intersect($identifiers, $protected) !== [];
}
/**
* Why this request is refused, or null when it may proceed.
*
* The order matters. The handful of state changing GET routes are matched
* before the safe verbs let everything else through, and the safe verbs
* are matched before the write rules so that the settings pages stay
* readable and a protected account can still be viewed.
*/
public static function blocks(Request $request): ?string
{
if (! self::enabled()) {
return null;
}
$route = $request->route();
if (self::matches($request, config('demo.allowed', []))) {
return null;
}
if (self::matches($request, config('demo.unsafe_reads', []))) {
return self::message();
}
if (in_array($request->method(), ['GET', 'HEAD', 'OPTIONS'], true)) {
return null;
}
if (self::matches($request, config('demo.blocked', []))) {
return self::message();
}
if (self::isProtectedUser(self::boundUser($route))) {
return self::message(__('The demo account cannot be modified.'));
}
if (self::matches($request, config('demo.self_routes', [])) && self::isProtectedUser($request->user())) {
return self::message(__('The demo account cannot be modified.'));
}
if (config('demo.protect_default_roles', true) && self::touchesSeededAuthorization($route)) {
return self::message(__('Default roles and permissions cannot be modified in the demo.'));
}
return null;
}
/**
* The configured override, the reason for this particular rule, or the
* generic refusal.
*/
public static function message(?string $default = null): string
{
return config('demo.message')
?: ($default ?: __('This action is disabled in the demo.'));
}
/**
* The route bound {user}, when there is one.
*
* Impersonation binds a raw {id} rather than a model, which is why that
* route sits on the blocklist instead of relying on this.
*/
private static function boundUser(?Route $route): ?User
{
$bound = $route?->parameter('user');
return $bound instanceof User ? $bound : null;
}
/**
* A seeded role or permission, identified by the removable column that
* RolesSeeder and PermissionsSeeder already set to false. Deleting one is
* refused by the remove requests; this covers renaming it.
*/
private static function touchesSeededAuthorization(?Route $route): bool
{
foreach (['role', 'permission'] as $parameter) {
$bound = $route?->parameter($parameter);
if ($bound && isset($bound->removable) && ! $bound->removable) {
return true;
}
}
return false;
}
/**
* Match a rule against the route name and against the request path.
*
* Names read better and survive a URI change, but most of routes/api.php
* is unnamed, so a name only matcher would leave the API half guarded.
* Names and paths cannot collide: one is dot separated, the other slash
* separated.
*
* @param array<int, string> $patterns
*/
private static function matches(Request $request, array $patterns): bool
{
$name = $request->route()?->getName();
return ($name !== null && Str::is($patterns, $name))
|| Str::is($patterns, $request->path());
}
/**
* @param array<int, string> $values
* @return array<int, string>
*/
private static function withoutBlanks(array $values): array
{
return array_values(array_filter($values, fn (string $value) => $value !== ''));
}
}