Optimize anonymous public sessions

This commit is contained in:
2026-05-01 11:42:10 +02:00
parent 35011001ba
commit 961d21e91e
35 changed files with 888 additions and 66 deletions

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\View\Middleware\ShareErrorsFromSession;
class ConditionalShareErrorsFromSession extends ShareErrorsFromSession
{
public function handle($request, Closure $next): mixed
{
if (! $request instanceof Request) {
return parent::handle($request, $next);
}
if ($request->attributes->get('skinbase.session_skipped') === true || ! $request->hasSession()) {
return $next($request);
}
return parent::handle($request, $next);
}
}

View File

@@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Session\Middleware\StartSession;
use Symfony\Component\HttpFoundation\Response;
class ConditionalStartSession extends StartSession
{
public function handle($request, Closure $next): mixed
{
if (! $request instanceof Request || ! config('skinbase-sessions.enabled', true)) {
return parent::handle($request, $next);
}
if ($this->shouldSkipSession($request)) {
$request->attributes->set('skinbase.session_skipped', true);
$response = $next($request);
if ($response instanceof Response && config('skinbase-sessions.debug_header', false)) {
$response->headers->set('X-Skinbase-Session', 'skipped');
}
return $response;
}
$request->attributes->set('skinbase.session_skipped', false);
$response = parent::handle($request, $next);
if ($response instanceof Response && config('skinbase-sessions.debug_header', false)) {
$response->headers->set('X-Skinbase-Session', 'started');
}
return $response;
}
protected function shouldSkipSession(Request $request): bool
{
if (! $this->isSafeReadMethod($request)) {
return false;
}
if ($this->hasExistingSessionCookie($request)) {
return false;
}
if ($this->matchesAnyPath($request, config('skinbase-sessions.always_session_paths', []))) {
return false;
}
if (! $this->matchesAnyPath($request, config('skinbase-sessions.public_paths', []))) {
return false;
}
if (config('skinbase-sessions.skip_anonymous_public_get', true)) {
return true;
}
return config('skinbase-sessions.skip_known_crawlers_on_public_get', true)
&& $this->isKnownCrawler($request);
}
protected function isSafeReadMethod(Request $request): bool
{
return in_array($request->getMethod(), ['GET', 'HEAD'], true);
}
protected function hasExistingSessionCookie(Request $request): bool
{
$cookieName = config('session.cookie');
return is_string($cookieName)
&& $cookieName !== ''
&& $request->cookies->has($cookieName);
}
protected function matchesAnyPath(Request $request, array $patterns): bool
{
foreach ($patterns as $pattern) {
if (! is_string($pattern) || $pattern === '') {
continue;
}
if ($pattern === '/' && $request->path() === '/') {
return true;
}
$normalizedPattern = trim($pattern, '/');
if ($normalizedPattern !== '' && $request->is($normalizedPattern)) {
return true;
}
}
return false;
}
protected function isKnownCrawler(Request $request): bool
{
$userAgent = strtolower((string) $request->userAgent());
if ($userAgent === '') {
return false;
}
foreach (config('skinbase-sessions.bot_user_agent_keywords', []) as $keyword) {
$normalizedKeyword = strtolower((string) $keyword);
if ($normalizedKeyword !== '' && str_contains($userAgent, $normalizedKeyword)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
use Illuminate\Http\Request;
class ConditionalValidateCsrfToken extends ValidateCsrfToken
{
public function handle($request, Closure $next): mixed
{
if ($request instanceof Request && $request->attributes->get('skinbase.session_skipped') === true) {
return $next($request);
}
return parent::handle($request, $next);
}
}

View File

@@ -12,6 +12,15 @@ final class HandleInertiaRequests extends Middleware
{
protected $rootView = 'upload';
protected function canReadSessionAuth(Request $request): bool
{
if ($request->attributes->get('skinbase.session_skipped') === true) {
return false;
}
return $request->hasSession();
}
/**
* Select the root Blade view based on route prefix.
*/
@@ -58,13 +67,16 @@ final class HandleInertiaRequests extends Middleware
public function share(Request $request): array
{
$canReadSessionAuth = $this->canReadSessionAuth($request);
$user = $canReadSessionAuth ? $request->user() : null;
return array_merge(parent::share($request), [
'auth' => [
'user' => $request->user() ? [
'id' => $request->user()->id,
'name' => $request->user()->name,
'is_admin' => $request->user()->isAdmin(),
'is_moderator' => $request->user()->isModerator(),
'user' => $user ? [
'id' => $user->id,
'name' => $user->name,
'is_admin' => $user->isAdmin(),
'is_moderator' => $user->isModerator(),
] : null,
],
'cdn' => [
@@ -84,8 +96,8 @@ final class HandleInertiaRequests extends Middleware
'group_assets' => (bool) config('features.group_assets', true),
'group_activity_feed' => (bool) config('features.group_activity_feed', true),
],
'studio_groups' => $request->user()
? app(GroupService::class)->studioOptionsForUser($request->user())
'studio_groups' => $user
? app(GroupService::class)->studioOptionsForUser($user)
: [],
]);
}

View File

@@ -151,6 +151,11 @@ class AppServiceProvider extends ServiceProvider
$displayName = null;
$userId = null;
$toolbarContentTypes = collect();
$request = request();
$canReadSessionAuth = $request instanceof \Illuminate\Http\Request
&& $request->hasSession()
&& $request->attributes->get('skinbase.session_skipped') !== true;
$authUser = $canReadSessionAuth ? Auth::user() : null;
try {
$toolbarContentTypes = $this->app
@@ -162,8 +167,9 @@ class AppServiceProvider extends ServiceProvider
$toolbarContentTypes = collect();
}
if (Auth::check()) {
$userId = Auth::id();
if ($authUser) {
$authUser->loadMissing('profile');
$userId = (int) $authUser->id;
try {
$uploadCount = DB::table('artworks')->where('user_id', $userId)->count();
} catch (\Throwable $e) {
@@ -200,19 +206,18 @@ class AppServiceProvider extends ServiceProvider
try {
$receivedCommentsCount = $this->app->make(ReceivedCommentsInboxService::class)
->unreadCountForUser(Auth::user());
->unreadCountForUser($authUser);
} catch (\Throwable $e) {
$receivedCommentsCount = 0;
}
try {
$profile = DB::table('user_profiles')->where('user_id', $userId)->first();
$avatarHash = $profile->avatar_hash ?? null;
$avatarHash = $authUser->profile?->avatar_hash;
} catch (\Throwable $e) {
$avatarHash = null;
}
$displayName = Auth::user()->name ?: (Auth::user()->username ?? '');
$displayName = $authUser->name ?: ($authUser->username ?? '');
}
$view->with(compact('userId','uploadCount', 'favCount', 'msgCount', 'noticeCount', 'receivedCommentsCount', 'avatarHash', 'displayName', 'toolbarContentTypes'));