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 $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 $values * @return array */ private static function withoutBlanks(array $values): array { return array_values(array_filter($values, fn (string $value) => $value !== '')); } }