diff --git a/framework/core/src/Admin/AdminServiceProvider.php b/framework/core/src/Admin/AdminServiceProvider.php index bde292eb2..51f889f0e 100644 --- a/framework/core/src/Admin/AdminServiceProvider.php +++ b/framework/core/src/Admin/AdminServiceProvider.php @@ -12,6 +12,7 @@ namespace Flarum\Admin; use Flarum\Extension\Event\Disabled; use Flarum\Extension\Event\Enabled; use Flarum\Foundation\AbstractServiceProvider; +use Flarum\Foundation\Config; use Flarum\Foundation\ErrorHandling\Registry; use Flarum\Foundation\ErrorHandling\Reporter; use Flarum\Foundation\ErrorHandling\ViewFormatter; @@ -22,39 +23,39 @@ use Flarum\Frontend\AddTranslations; use Flarum\Frontend\Compiler\Source\SourceCollector; use Flarum\Frontend\RecompileFrontendAssets; use Flarum\Http\Middleware as HttpMiddleware; -use Flarum\Http\RouteCollection; +use Flarum\Http\Router; use Flarum\Http\RouteHandlerFactory; -use Flarum\Http\UrlGenerator; use Flarum\Locale\LocaleManager; use Flarum\Settings\Event\Saved; use Illuminate\Contracts\Container\Container; -use Laminas\Stratigility\MiddlewarePipe; class AdminServiceProvider extends AbstractServiceProvider { public function register(): void { - $this->container->extend(UrlGenerator::class, function (UrlGenerator $url, Container $container) { - return $url->addCollection('admin', $container->make('flarum.admin.routes'), 'admin'); - }); + $this->booted(function (Container $container) { + /** @var Router $router */ + $router = $container->make(Router::class); + /** @var Config $config */ + $config = $container->make(Config::class); - $this->container->singleton('flarum.admin.routes', function () { - $routes = new RouteCollection; - $this->populateRoutes($routes); + $router->middlewareGroup('admin', $container->make('flarum.admin.middleware')); - return $routes; + $factory = $container->make(RouteHandlerFactory::class); + + $router->middleware('admin')->prefix($config->path('admin'))->group( + fn (Router $router) => (include __DIR__.'/routes.php')($router, $factory) + ); }); $this->container->singleton('flarum.admin.middleware', function () { return [ HttpMiddleware\InjectActorReference::class, 'flarum.admin.error_handler', - HttpMiddleware\ParseJsonBody::class, HttpMiddleware\StartSession::class, HttpMiddleware\RememberFromCookie::class, HttpMiddleware\AuthenticateWithSession::class, HttpMiddleware\SetLocale::class, - 'flarum.admin.route_resolver', HttpMiddleware\CheckCsrfToken::class, Middleware\RequireAdministrateAbility::class, HttpMiddleware\ReferrerPolicyHeader::class, @@ -71,22 +72,6 @@ class AdminServiceProvider extends AbstractServiceProvider ); }); - $this->container->bind('flarum.admin.route_resolver', function (Container $container) { - return new HttpMiddleware\ResolveRoute($container->make('flarum.admin.routes')); - }); - - $this->container->singleton('flarum.admin.handler', function (Container $container) { - $pipe = new MiddlewarePipe; - - foreach ($container->make('flarum.admin.middleware') as $middleware) { - $pipe->pipe($container->make($middleware)); - } - - $pipe->pipe(new HttpMiddleware\ExecuteRoute()); - - return $pipe; - }); - $this->container->bind('flarum.assets.admin', function (Container $container) { /** @var \Flarum\Frontend\Assets $assets */ $assets = $container->make('flarum.assets.factory')('admin'); @@ -143,12 +128,4 @@ class AdminServiceProvider extends AbstractServiceProvider } ); } - - protected function populateRoutes(RouteCollection $routes): void - { - $factory = $this->container->make(RouteHandlerFactory::class); - - $callback = include __DIR__.'/routes.php'; - $callback($routes, $factory); - } } diff --git a/framework/core/src/Admin/Middleware/DisableBrowserCache.php b/framework/core/src/Admin/Middleware/DisableBrowserCache.php index 731ea8157..9b561e053 100644 --- a/framework/core/src/Admin/Middleware/DisableBrowserCache.php +++ b/framework/core/src/Admin/Middleware/DisableBrowserCache.php @@ -9,17 +9,22 @@ namespace Flarum\Admin\Middleware; -use Psr\Http\Message\ResponseInterface as Response; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Server\MiddlewareInterface as Middleware; -use Psr\Http\Server\RequestHandlerInterface as Handler; +use Closure; +use Flarum\Http\Middleware\IlluminateMiddlewareInterface; +use Illuminate\Http\Request; +use Symfony\Component\HttpFoundation\Response; -class DisableBrowserCache implements Middleware +class DisableBrowserCache implements IlluminateMiddlewareInterface { - public function process(Request $request, Handler $handler): Response + /** + * @inheritDoc + */ + public function handle(Request $request, Closure $next): Response { - $response = $handler->handle($request); + $response = $next($request); - return $response->withHeader('Cache-Control', 'max-age=0, no-store'); + $response->headers->set('Cache-Control', 'max-age=0, no-store'); + + return $response; } } diff --git a/framework/core/src/Admin/Middleware/RequireAdministrateAbility.php b/framework/core/src/Admin/Middleware/RequireAdministrateAbility.php index f91fe9faf..50ce41780 100644 --- a/framework/core/src/Admin/Middleware/RequireAdministrateAbility.php +++ b/framework/core/src/Admin/Middleware/RequireAdministrateAbility.php @@ -9,18 +9,18 @@ namespace Flarum\Admin\Middleware; +use Closure; +use Flarum\Http\Middleware\IlluminateMiddlewareInterface; use Flarum\Http\RequestUtil; -use Psr\Http\Message\ResponseInterface as Response; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Server\MiddlewareInterface as Middleware; -use Psr\Http\Server\RequestHandlerInterface as Handler; +use Illuminate\Http\Request; +use Symfony\Component\HttpFoundation\Response; -class RequireAdministrateAbility implements Middleware +class RequireAdministrateAbility implements IlluminateMiddlewareInterface { - public function process(Request $request, Handler $handler): Response + public function handle(Request $request, Closure $next): Response { RequestUtil::getActor($request)->assertAdmin(); - return $handler->handle($request); + return $next($request); } } diff --git a/framework/core/src/Admin/routes.php b/framework/core/src/Admin/routes.php index d81517dfb..ab93c9449 100644 --- a/framework/core/src/Admin/routes.php +++ b/framework/core/src/Admin/routes.php @@ -9,19 +9,17 @@ use Flarum\Admin\Content\Index; use Flarum\Admin\Controller\UpdateExtensionController; -use Flarum\Http\RouteCollection; +use Flarum\Http\Router; use Flarum\Http\RouteHandlerFactory; -return function (RouteCollection $map, RouteHandlerFactory $route) { - $map->get( - '/', - 'index', - $route->toAdmin(Index::class) - ); +return function (Router $router, RouteHandlerFactory $factory) { + + $router + ->get('/', $factory->toAdmin(Index::class)) + ->name('index'); + + $router + ->post('/extensions/{name}', $factory->toController(UpdateExtensionController::class)) + ->name('extensions.update'); - $map->post( - '/extensions/{name}', - 'extensions.update', - $route->toController(UpdateExtensionController::class) - ); }; diff --git a/framework/core/src/Api/ApiServiceProvider.php b/framework/core/src/Api/ApiServiceProvider.php index acd7a8b63..5e176624c 100644 --- a/framework/core/src/Api/ApiServiceProvider.php +++ b/framework/core/src/Api/ApiServiceProvider.php @@ -14,29 +14,29 @@ use Flarum\Api\Serializer\AbstractSerializer; use Flarum\Api\Serializer\BasicDiscussionSerializer; use Flarum\Api\Serializer\NotificationSerializer; use Flarum\Foundation\AbstractServiceProvider; +use Flarum\Foundation\Config; use Flarum\Foundation\ErrorHandling\JsonApiFormatter; use Flarum\Foundation\ErrorHandling\Registry; use Flarum\Foundation\ErrorHandling\Reporter; use Flarum\Http\Middleware as HttpMiddleware; -use Flarum\Http\RouteCollection; -use Flarum\Http\RouteHandlerFactory; -use Flarum\Http\UrlGenerator; +use Flarum\Http\Router; use Illuminate\Contracts\Container\Container; -use Laminas\Stratigility\MiddlewarePipe; class ApiServiceProvider extends AbstractServiceProvider { public function register(): void { - $this->container->extend(UrlGenerator::class, function (UrlGenerator $url, Container $container) { - return $url->addCollection('api', $container->make('flarum.api.routes'), 'api'); - }); + $this->booted(function (Container $container) { + /** @var Router $router */ + $router = $container->make(Router::class); + /** @var Config $config */ + $config = $container->make(Config::class); - $this->container->singleton('flarum.api.routes', function () { - $routes = new RouteCollection; - $this->populateRoutes($routes); + $router->middlewareGroup('api', $container->make('flarum.api.middleware')); - return $routes; + $router->middleware('api')->prefix($config->path('api'))->group( + fn (Router $router) => (include __DIR__.'/routes.php')($router) + ); }); $this->container->singleton('flarum.api.throttlers', function () { @@ -57,14 +57,12 @@ class ApiServiceProvider extends AbstractServiceProvider return [ HttpMiddleware\InjectActorReference::class, 'flarum.api.error_handler', - HttpMiddleware\ParseJsonBody::class, Middleware\FakeHttpMethods::class, HttpMiddleware\StartSession::class, HttpMiddleware\RememberFromCookie::class, HttpMiddleware\AuthenticateWithSession::class, HttpMiddleware\AuthenticateWithHeader::class, HttpMiddleware\SetLocale::class, - 'flarum.api.route_resolver', HttpMiddleware\CheckCsrfToken::class, Middleware\ThrottleApi::class ]; @@ -78,22 +76,6 @@ class ApiServiceProvider extends AbstractServiceProvider ); }); - $this->container->bind('flarum.api.route_resolver', function (Container $container) { - return new HttpMiddleware\ResolveRoute($container->make('flarum.api.routes')); - }); - - $this->container->singleton('flarum.api.handler', function (Container $container) { - $pipe = new MiddlewarePipe; - - foreach ($this->container->make('flarum.api.middleware') as $middleware) { - $pipe->pipe($container->make($middleware)); - } - - $pipe->pipe(new HttpMiddleware\ExecuteRoute()); - - return $pipe; - }); - $this->container->singleton('flarum.api.notification_serializers', function () { return [ 'discussionRenamed' => BasicDiscussionSerializer::class @@ -103,7 +85,6 @@ class ApiServiceProvider extends AbstractServiceProvider $this->container->singleton('flarum.api_client.exclude_middleware', function () { return [ HttpMiddleware\InjectActorReference::class, - HttpMiddleware\ParseJsonBody::class, Middleware\FakeHttpMethods::class, HttpMiddleware\StartSession::class, HttpMiddleware\AuthenticateWithSession::class, @@ -113,22 +94,14 @@ class ApiServiceProvider extends AbstractServiceProvider ]; }); - $this->container->singleton(Client::class, function ($container) { - $pipe = new MiddlewarePipe; - + $this->container->singleton(Client::class, function (Container $container) { $exclude = $container->make('flarum.api_client.exclude_middleware'); $middlewareStack = array_filter($container->make('flarum.api.middleware'), function ($middlewareClass) use ($exclude) { return ! in_array($middlewareClass, $exclude); }); - foreach ($middlewareStack as $middleware) { - $pipe->pipe($container->make($middleware)); - } - - $pipe->pipe(new HttpMiddleware\ExecuteRoute()); - - return new Client($pipe); + return new Client($middlewareStack, $container); }); } @@ -149,12 +122,4 @@ class ApiServiceProvider extends AbstractServiceProvider NotificationSerializer::setSubjectSerializer($type, $serializer); } } - - protected function populateRoutes(RouteCollection $routes): void - { - $factory = $this->container->make(RouteHandlerFactory::class); - - $callback = include __DIR__.'/routes.php'; - $callback($routes, $factory); - } } diff --git a/framework/core/src/Api/Middleware/FakeHttpMethods.php b/framework/core/src/Api/Middleware/FakeHttpMethods.php index 489ae3e53..8d00aabbd 100644 --- a/framework/core/src/Api/Middleware/FakeHttpMethods.php +++ b/framework/core/src/Api/Middleware/FakeHttpMethods.php @@ -9,23 +9,23 @@ namespace Flarum\Api\Middleware; -use Psr\Http\Message\ResponseInterface as Response; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Server\MiddlewareInterface as Middleware; -use Psr\Http\Server\RequestHandlerInterface as Handler; +use Closure; +use Flarum\Http\Middleware\IlluminateMiddlewareInterface; +use Illuminate\Http\Request; +use Symfony\Component\HttpFoundation\Response; -class FakeHttpMethods implements Middleware +class FakeHttpMethods implements IlluminateMiddlewareInterface { const HEADER_NAME = 'x-http-method-override'; - public function process(Request $request, Handler $handler): Response + public function handle(Request $request, Closure $next): Response { if ($request->getMethod() === 'POST' && $request->hasHeader(self::HEADER_NAME)) { - $fakeMethod = $request->getHeaderLine(self::HEADER_NAME); + $fakeMethod = $request->header(self::HEADER_NAME); - $request = $request->withMethod(strtoupper($fakeMethod)); + $request->setMethod(strtoupper($fakeMethod)); } - return $handler->handle($request); + return $next($request); } } diff --git a/framework/core/src/Api/Middleware/ThrottleApi.php b/framework/core/src/Api/Middleware/ThrottleApi.php index 317695678..35bcd0dea 100644 --- a/framework/core/src/Api/Middleware/ThrottleApi.php +++ b/framework/core/src/Api/Middleware/ThrottleApi.php @@ -9,26 +9,26 @@ namespace Flarum\Api\Middleware; +use Closure; +use Flarum\Http\Middleware\IlluminateMiddlewareInterface; use Flarum\Post\Exception\FloodingException; -use Psr\Http\Message\ResponseInterface as Response; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Server\MiddlewareInterface as Middleware; -use Psr\Http\Server\RequestHandlerInterface as Handler; +use Illuminate\Http\Request; +use Symfony\Component\HttpFoundation\Response; -class ThrottleApi implements Middleware +class ThrottleApi implements IlluminateMiddlewareInterface { public function __construct( protected array $throttlers ) { } - public function process(Request $request, Handler $handler): Response + public function handle(Request $request, Closure $next): Response { if ($this->throttle($request)) { throw new FloodingException; } - return $handler->handle($request); + return $next($request); } public function throttle(Request $request): bool diff --git a/framework/core/src/Api/routes.php b/framework/core/src/Api/routes.php index 80baf9cd7..dac4f8aaf 100644 --- a/framework/core/src/Api/routes.php +++ b/framework/core/src/Api/routes.php @@ -8,58 +8,45 @@ */ use Flarum\Api\Controller; -use Flarum\Http\RouteCollection; use Flarum\Http\RouteHandlerFactory; +use Flarum\Http\Router; + +return function (Router $router, RouteHandlerFactory $factory) { -return function (RouteCollection $map, RouteHandlerFactory $route) { // Get forum information - $map->get( - '/', - 'forum.show', - $route->toController(Controller\ShowForumController::class) - ); + $router + ->get('/', $factory->toController(Controller\ShowForumController::class)) + ->name('forum.show'); // List access tokens - $map->get( - '/access-tokens', - 'access-tokens.index', - $route->toController(Controller\ListAccessTokensController::class) - ); + $router + ->get('/access-tokens', $factory->toController(Controller\ListAccessTokensController::class)) + ->name('access-tokens.index'); // Create access token - $map->post( - '/access-tokens', - 'access-tokens.create', - $route->toController(Controller\CreateAccessTokenController::class) - ); + $router + ->post('/access-tokens', $factory->toController(Controller\CreateAccessTokenController::class)) + ->name('access-tokens.create'); // Delete access token - $map->delete( - '/access-tokens/{id}', - 'access-tokens.delete', - $route->toController(Controller\DeleteAccessTokenController::class) - ); + $router + ->delete('/access-tokens/{id}', $factory->toController(Controller\DeleteAccessTokenController::class)) + ->name('access-tokens.delete'); // Create authentication token - $map->post( - '/token', - 'token', - $route->toController(Controller\CreateTokenController::class) - ); + $router + ->post('/token', $factory->toController(Controller\CreateTokenController::class)) + ->name('token'); // Terminate all other sessions - $map->delete( - '/sessions', - 'sessions.delete', - $route->toController(Controller\TerminateAllOtherSessionsController::class) - ); + $router + ->delete('/sessions', $factory->toController(Controller\TerminateAllOtherSessionsController::class)) + ->name('sessions.delete'); // Send forgot password email - $map->post( - '/forgot', - 'forgot', - $route->toController(Controller\ForgotPasswordController::class) - ); + $router + ->post('/forgot', $factory->toController(Controller\ForgotPasswordController::class)) + ->name('forgot'); /* |-------------------------------------------------------------------------- @@ -68,60 +55,50 @@ return function (RouteCollection $map, RouteHandlerFactory $route) { */ // List users - $map->get( - '/users', - 'users.index', - $route->toController(Controller\ListUsersController::class) - ); + $router + ->get('/users', $factory->toController(Controller\ListUsersController::class)) + ->name('users.index'); // Register a user - $map->post( - '/users', - 'users.create', - $route->toController(Controller\CreateUserController::class) - ); + $router + ->post('/users', $factory->toController(Controller\CreateUserController::class)) + ->name('users.create'); // Get a single user - $map->get( - '/users/{id}', - 'users.show', - $route->toController(Controller\ShowUserController::class) - ); + $router + ->get('/users/{id}', $factory->toController(Controller\ShowUserController::class)) + ->name('users.show') + ->whereNumber('id'); // Edit a user - $map->patch( - '/users/{id}', - 'users.update', - $route->toController(Controller\UpdateUserController::class) - ); + $router + ->patch('/users/{id}', $factory->toController(Controller\UpdateUserController::class)) + ->name('users.update') + ->whereNumber('id'); // Delete a user - $map->delete( - '/users/{id}', - 'users.delete', - $route->toController(Controller\DeleteUserController::class) - ); + $router + ->delete('/users/{id}', $factory->toController(Controller\DeleteUserController::class)) + ->name('users.delete') + ->whereNumber('id'); // Upload avatar - $map->post( - '/users/{id}/avatar', - 'users.avatar.upload', - $route->toController(Controller\UploadAvatarController::class) - ); + $router + ->post('/users/{id}/avatar', $factory->toController(Controller\UploadAvatarController::class)) + ->name('users.avatar.upload') + ->whereNumber('id'); // Remove avatar - $map->delete( - '/users/{id}/avatar', - 'users.avatar.delete', - $route->toController(Controller\DeleteAvatarController::class) - ); + $router + ->delete('/users/{id}/avatar', $factory->toController(Controller\DeleteAvatarController::class)) + ->name('users.avatar.delete') + ->whereNumber('id'); // send confirmation email - $map->post( - '/users/{id}/send-confirmation', - 'users.confirmation.send', - $route->toController(Controller\SendConfirmationEmailController::class) - ); + $router + ->post('/users/{id}/send-confirmation', $factory->toController(Controller\SendConfirmationEmailController::class)) + ->name('users.confirmation.send') + ->whereNumber('id'); /* |-------------------------------------------------------------------------- @@ -130,32 +107,25 @@ return function (RouteCollection $map, RouteHandlerFactory $route) { */ // List notifications for the current user - $map->get( - '/notifications', - 'notifications.index', - $route->toController(Controller\ListNotificationsController::class) - ); + $router + ->get('/notifications', $factory->toController(Controller\ListNotificationsController::class)) + ->name('notifications.index'); // Mark all notifications as read - $map->post( - '/notifications/read', - 'notifications.readAll', - $route->toController(Controller\ReadAllNotificationsController::class) - ); + $router + ->post('/notifications/read', $factory->toController(Controller\ReadAllNotificationsController::class)) + ->name('notifications.readAll'); // Mark a single notification as read - $map->patch( - '/notifications/{id}', - 'notifications.update', - $route->toController(Controller\UpdateNotificationController::class) - ); + $router + ->patch('/notifications/{id}', $factory->toController(Controller\UpdateNotificationController::class)) + ->name('notifications.update') + ->whereNumber('id'); // Delete all notifications for the current user. - $map->delete( - '/notifications', - 'notifications.deleteAll', - $route->toController(Controller\DeleteAllNotificationsController::class) - ); + $router + ->delete('/notifications', $factory->toController(Controller\DeleteAllNotificationsController::class)) + ->name('notifications.deleteAll'); /* |-------------------------------------------------------------------------- @@ -164,39 +134,32 @@ return function (RouteCollection $map, RouteHandlerFactory $route) { */ // List discussions - $map->get( - '/discussions', - 'discussions.index', - $route->toController(Controller\ListDiscussionsController::class) - ); + $router + ->get('/discussions', $factory->toController(Controller\ListDiscussionsController::class)) + ->name('discussions.index'); // Create a discussion - $map->post( - '/discussions', - 'discussions.create', - $route->toController(Controller\CreateDiscussionController::class) - ); + $router + ->post('/discussions', $factory->toController(Controller\CreateDiscussionController::class)) + ->name('discussions.create'); // Show a single discussion - $map->get( - '/discussions/{id}', - 'discussions.show', - $route->toController(Controller\ShowDiscussionController::class) - ); + $router + ->get('/discussions/{id}', $factory->toController(Controller\ShowDiscussionController::class)) + ->name('discussions.show') + ->whereNumber('id'); // Edit a discussion - $map->patch( - '/discussions/{id}', - 'discussions.update', - $route->toController(Controller\UpdateDiscussionController::class) - ); + $router + ->patch('/discussions/{id}', $factory->toController(Controller\UpdateDiscussionController::class)) + ->name('discussions.update') + ->whereNumber('id'); // Delete a discussion - $map->delete( - '/discussions/{id}', - 'discussions.delete', - $route->toController(Controller\DeleteDiscussionController::class) - ); + $router + ->delete('/discussions/{id}', $factory->toController(Controller\DeleteDiscussionController::class)) + ->name('discussions.delete') + ->whereNumber('id'); /* |-------------------------------------------------------------------------- @@ -205,39 +168,32 @@ return function (RouteCollection $map, RouteHandlerFactory $route) { */ // List posts, usually for a discussion - $map->get( - '/posts', - 'posts.index', - $route->toController(Controller\ListPostsController::class) - ); + $router + ->get('/posts', $factory->toController(Controller\ListPostsController::class)) + ->name('posts.index'); // Create a post - $map->post( - '/posts', - 'posts.create', - $route->toController(Controller\CreatePostController::class) - ); + $router + ->post('/posts', $factory->toController(Controller\CreatePostController::class)) + ->name('posts.create'); // Show a single or multiple posts by ID - $map->get( - '/posts/{id}', - 'posts.show', - $route->toController(Controller\ShowPostController::class) - ); + $router + ->get('/posts/{id}', $factory->toController(Controller\ShowPostController::class)) + ->name('posts.show') + ->whereNumber('id'); // Edit a post - $map->patch( - '/posts/{id}', - 'posts.update', - $route->toController(Controller\UpdatePostController::class) - ); + $router + ->patch('/posts/{id}', $factory->toController(Controller\UpdatePostController::class)) + ->name('posts.update') + ->whereNumber('id'); // Delete a post - $map->delete( - '/posts/{id}', - 'posts.delete', - $route->toController(Controller\DeletePostController::class) - ); + $router + ->delete('/posts/{id}', $factory->toController(Controller\DeletePostController::class)) + ->name('posts.delete') + ->whereNumber('id'); /* |-------------------------------------------------------------------------- @@ -246,39 +202,32 @@ return function (RouteCollection $map, RouteHandlerFactory $route) { */ // List groups - $map->get( - '/groups', - 'groups.index', - $route->toController(Controller\ListGroupsController::class) - ); + $router + ->get('/groups', $factory->toController(Controller\ListGroupsController::class)) + ->name('groups.index'); // Create a group - $map->post( - '/groups', - 'groups.create', - $route->toController(Controller\CreateGroupController::class) - ); + $router + ->post('/groups', $factory->toController(Controller\CreateGroupController::class)) + ->name('groups.create'); // Show a single group - $map->get( - '/groups/{id}', - 'groups.show', - $route->toController(Controller\ShowGroupController::class) - ); + $router + ->get('/groups/{id}', $factory->toController(Controller\ShowGroupController::class)) + ->name('groups.show') + ->whereNumber('id'); // Edit a group - $map->patch( - '/groups/{id}', - 'groups.update', - $route->toController(Controller\UpdateGroupController::class) - ); + $router + ->patch('/groups/{id}', $factory->toController(Controller\UpdateGroupController::class)) + ->name('groups.update') + ->whereNumber('id'); // Delete a group - $map->delete( - '/groups/{id}', - 'groups.delete', - $route->toController(Controller\DeleteGroupController::class) - ); + $router + ->delete('/groups/{id}', $factory->toController(Controller\DeleteGroupController::class)) + ->name('groups.delete') + ->whereNumber('id'); /* |-------------------------------------------------------------------------- @@ -287,86 +236,63 @@ return function (RouteCollection $map, RouteHandlerFactory $route) { */ // Toggle an extension - $map->patch( - '/extensions/{name}', - 'extensions.update', - $route->toController(Controller\UpdateExtensionController::class) - ); + $router + ->patch('/extensions/{name}', $factory->toController(Controller\UpdateExtensionController::class)) + ->name('extensions.update'); // Uninstall an extension - $map->delete( - '/extensions/{name}', - 'extensions.delete', - $route->toController(Controller\UninstallExtensionController::class) - ); + $router + ->delete('/extensions/{name}', $factory->toController(Controller\UninstallExtensionController::class)) + ->name('extensions.delete'); // Get readme for an extension - $map->get( - '/extension-readmes/{name}', - 'extension-readmes.show', - $route->toController(Controller\ShowExtensionReadmeController::class) - ); + $router + ->get('/extension-readmes/{name}', $factory->toController(Controller\ShowExtensionReadmeController::class)) + ->name('extension-readmes.show'); // Update settings - $map->post( - '/settings', - 'settings', - $route->toController(Controller\SetSettingsController::class) - ); + $router + ->post('/settings', $factory->toController(Controller\SetSettingsController::class)) + ->name('settings'); // Update a permission - $map->post( - '/permission', - 'permission', - $route->toController(Controller\SetPermissionController::class) - ); + $router + ->post('/permission', $factory->toController(Controller\SetPermissionController::class)) + ->name('permission'); // Upload a logo - $map->post( - '/logo', - 'logo', - $route->toController(Controller\UploadLogoController::class) - ); + $router + ->post('/logo', $factory->toController(Controller\UploadLogoController::class)) + ->name('logo'); // Remove the logo - $map->delete( - '/logo', - 'logo.delete', - $route->toController(Controller\DeleteLogoController::class) - ); + $router + ->delete('/logo', $factory->toController(Controller\DeleteLogoController::class)) + ->name('logo.delete'); // Upload a favicon - $map->post( - '/favicon', - 'favicon', - $route->toController(Controller\UploadFaviconController::class) - ); + $router + ->post('/favicon', $factory->toController(Controller\UploadFaviconController::class)) + ->name('favicon'); // Remove the favicon - $map->delete( - '/favicon', - 'favicon.delete', - $route->toController(Controller\DeleteFaviconController::class) - ); + $router + ->delete('/favicon', $factory->toController(Controller\DeleteFaviconController::class)) + ->name('favicon.delete'); // Clear the cache - $map->delete( - '/cache', - 'cache.clear', - $route->toController(Controller\ClearCacheController::class) - ); + $router + ->delete('/cache', $factory->toController(Controller\ClearCacheController::class)) + ->name('cache.clear'); // List available mail drivers, available fields and validation status - $map->get( - '/mail/settings', - 'mailSettings.index', - $route->toController(Controller\ShowMailSettingsController::class) - ); + $router + ->get('/mail/settings', $factory->toController(Controller\ShowMailSettingsController::class)) + ->name('mailSettings.index'); // Send test mail post - $map->post( - '/mail/test', - 'mailTest', - $route->toController(Controller\SendTestMailController::class) - ); + $router + ->post('/mail/test', $factory->toController(Controller\SendTestMailController::class)) + ->name('mailTest'); + }; diff --git a/framework/core/src/Forum/ForumServiceProvider.php b/framework/core/src/Forum/ForumServiceProvider.php index 65eb75c21..bcca324cb 100644 --- a/framework/core/src/Forum/ForumServiceProvider.php +++ b/framework/core/src/Forum/ForumServiceProvider.php @@ -13,6 +13,7 @@ use Flarum\Extension\Event\Disabled; use Flarum\Extension\Event\Enabled; use Flarum\Formatter\Formatter; use Flarum\Foundation\AbstractServiceProvider; +use Flarum\Foundation\Config; use Flarum\Foundation\ErrorHandling\Registry; use Flarum\Foundation\ErrorHandling\Reporter; use Flarum\Foundation\ErrorHandling\ViewFormatter; @@ -24,9 +25,8 @@ use Flarum\Frontend\Assets; use Flarum\Frontend\Compiler\Source\SourceCollector; use Flarum\Frontend\RecompileFrontendAssets; use Flarum\Http\Middleware as HttpMiddleware; -use Flarum\Http\RouteCollection; use Flarum\Http\RouteHandlerFactory; -use Flarum\Http\UrlGenerator; +use Flarum\Http\Router; use Flarum\Locale\LocaleManager; use Flarum\Settings\Event\Saved; use Flarum\Settings\Event\Saving; @@ -34,39 +34,41 @@ use Flarum\Settings\SettingsRepositoryInterface; use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\View\Factory; -use Laminas\Stratigility\MiddlewarePipe; use Symfony\Contracts\Translation\TranslatorInterface; class ForumServiceProvider extends AbstractServiceProvider { public function register(): void { - $this->container->extend(UrlGenerator::class, function (UrlGenerator $url, Container $container) { - return $url->addCollection('forum', $container->make('flarum.forum.routes')); - }); + $this->booted(function (Container $container) { + /** @var Router $router */ + $router = $container->make(Router::class); + /** @var Config $config */ + $config = $container->make(Config::class); - $this->container->singleton('flarum.forum.routes', function (Container $container) { - $routes = new RouteCollection; - $this->populateRoutes($routes, $container); + $router->middlewareGroup('forum', $container->make('flarum.forum.middleware')); - return $routes; - }); + $factory = $container->make(RouteHandlerFactory::class); - $this->container->afterResolving('flarum.forum.routes', function (RouteCollection $routes, Container $container) { - $this->setDefaultRoute($routes, $container); + $router->middleware('forum')->prefix($config->path('forum'))->group( + fn (Router $router) => (include __DIR__.'/routes.php')($router, $factory) + ); + + $this->setDefaultRoute( + $router, + $container->make(SettingsRepositoryInterface::class) + ); }); $this->container->singleton('flarum.forum.middleware', function () { return [ HttpMiddleware\InjectActorReference::class, 'flarum.forum.error_handler', - HttpMiddleware\ParseJsonBody::class, HttpMiddleware\CollectGarbage::class, HttpMiddleware\StartSession::class, HttpMiddleware\RememberFromCookie::class, HttpMiddleware\AuthenticateWithSession::class, HttpMiddleware\SetLocale::class, - 'flarum.forum.route_resolver', HttpMiddleware\CheckCsrfToken::class, HttpMiddleware\ShareErrorsFromSession::class, HttpMiddleware\FlarumPromotionHeader::class, @@ -83,22 +85,6 @@ class ForumServiceProvider extends AbstractServiceProvider ); }); - $this->container->bind('flarum.forum.route_resolver', function (Container $container) { - return new HttpMiddleware\ResolveRoute($container->make('flarum.forum.routes')); - }); - - $this->container->singleton('flarum.forum.handler', function (Container $container) { - $pipe = new MiddlewarePipe; - - foreach ($container->make('flarum.forum.middleware') as $middleware) { - $pipe->pipe($container->make($middleware)); - } - - $pipe->pipe(new HttpMiddleware\ExecuteRoute()); - - return $pipe; - }); - $this->container->bind('flarum.assets.forum', function (Container $container) { /** @var Assets $assets */ $assets = $container->make('flarum.assets.factory')('forum'); @@ -194,29 +180,10 @@ class ForumServiceProvider extends AbstractServiceProvider ); } - protected function populateRoutes(RouteCollection $routes, Container $container): void + protected function setDefaultRoute(Router $router, SettingsRepositoryInterface $settings): void { - $factory = $container->make(RouteHandlerFactory::class); - - $callback = include __DIR__.'/routes.php'; - $callback($routes, $factory); - } - - protected function setDefaultRoute(RouteCollection $routes, Container $container): void - { - $factory = $container->make(RouteHandlerFactory::class); - $defaultRoute = $container->make('flarum.settings')->get('default_route'); - - if (isset($routes->getRouteData()[0]['GET'][$defaultRoute]['handler'])) { - $toDefaultController = $routes->getRouteData()[0]['GET'][$defaultRoute]['handler']; - } else { - $toDefaultController = $factory->toForum(Content\Index::class); - } - - $routes->get( - '/', - 'default', - $toDefaultController - ); + $defaultRoute = $settings->get('default_route'); + $action = $router->getRoutes()->getByName($defaultRoute)?->getAction() ?? 'index'; + $router->get('/', $action)->name('default'); } } diff --git a/framework/core/src/Forum/routes.php b/framework/core/src/Forum/routes.php index d0264cb8c..e8d4a3ddd 100644 --- a/framework/core/src/Forum/routes.php +++ b/framework/core/src/Forum/routes.php @@ -9,85 +9,64 @@ use Flarum\Forum\Content; use Flarum\Forum\Controller; -use Flarum\Http\RouteCollection; use Flarum\Http\RouteHandlerFactory; +use Flarum\Http\Router; -return function (RouteCollection $map, RouteHandlerFactory $route) { - $map->get( - '/all', - 'index', - $route->toForum(Content\Index::class) - ); +return function (Router $router, RouteHandlerFactory $factory) { - $map->get( - '/d/{id:\d+(?:-[^/]*)?}[/{near:[^/]*}]', - 'discussion', - $route->toForum(Content\Discussion::class) - ); + $router + ->get('/all', $factory->toForum(Content\Index::class)) + ->name('index'); - $map->get( - '/u/{username}[/{filter:[^/]*}]', - 'user', - $route->toForum(Content\User::class) - ); + $router + ->get('/d/{id}/{near?}', $factory->toForum(Content\Discussion::class)) + ->where('id', '\d+(?:-[^/]*)?') + ->where('near', '[^/]*') + ->name('discussion'); - $map->get( - '/settings', - 'settings', - $route->toForum(Content\AssertRegistered::class) - ); + $router + ->get('/u/{username}/{filter?}', $factory->toForum(Content\User::class)) + ->where('filter', '[^/]*') + ->name('user'); - $map->get( - '/notifications', - 'notifications', - $route->toForum(Content\AssertRegistered::class) - ); + $router + ->get('/settings', $factory->toForum(Content\AssertRegistered::class)) + ->name('settings'); - $map->get( - '/logout', - 'logout', - $route->toController(Controller\LogOutController::class) - ); + $router + ->get('/notifications', $factory->toForum(Content\AssertRegistered::class)) + ->name('notifications'); - $map->post( - '/global-logout', - 'globalLogout', - $route->toController(Controller\GlobalLogOutController::class) - ); + $router + ->get('/logout', $factory->toController(Controller\LogOutController::class)) + ->name('logout'); - $map->post( - '/login', - 'login', - $route->toController(Controller\LogInController::class) - ); + $router + ->post('/global-logout', $factory->toController(Controller\GlobalLogOutController::class)) + ->name('globalLogout'); - $map->post( - '/register', - 'register', - $route->toController(Controller\RegisterController::class) - ); + $router + ->post('/login', $factory->toController(Controller\LogInController::class)) + ->name('login'); - $map->get( - '/confirm/{token}', - 'confirmEmail', - $route->toController(Controller\ConfirmEmailViewController::class), - ); + $router + ->post('/register', $factory->toController(Controller\RegisterController::class)) + ->name('register'); - $map->post( - '/confirm/{token}', - 'confirmEmail.submit', - $route->toController(Controller\ConfirmEmailController::class), - ); + $router + ->get('/confirm/{token}', $factory->toController(Controller\ConfirmEmailViewController::class)) + ->name('confirmEmail'); - $map->get( - '/reset/{token}', - 'resetPassword', - $route->toController(Controller\ResetPasswordController::class) - ); + $router + ->post('/confirm/{token}', $factory->toController(Controller\ConfirmEmailController::class)) + ->name('confirmEmail.submit'); + + $router + ->get('/reset/{token}', $factory->toController(Controller\ResetPasswordController::class)) + ->name('resetPassword'); + + $router + ->post('/reset', $factory->toController(Controller\SavePasswordController::class)) + ->name('savePassword'); - $map->post( - '/reset', - 'savePassword', - $route->toController(Controller\SavePasswordController::class) - ); }; diff --git a/framework/core/src/Foundation/AppInterface.php b/framework/core/src/Foundation/AppInterface.php index 7bf115e9c..2bbf784ea 100644 --- a/framework/core/src/Foundation/AppInterface.php +++ b/framework/core/src/Foundation/AppInterface.php @@ -10,14 +10,13 @@ namespace Flarum\Foundation; use Illuminate\Contracts\Container\Container; -use Psr\Http\Server\RequestHandlerInterface; use Symfony\Component\Console\Command\Command; interface AppInterface { public function getContainer(): Container; - public function getRequestHandler(): RequestHandlerInterface; + public function getMiddlewareStack(): array; /** * @return Command[] diff --git a/framework/core/src/Foundation/Application.php b/framework/core/src/Foundation/Application.php index 90b2bb724..d510d90df 100644 --- a/framework/core/src/Foundation/Application.php +++ b/framework/core/src/Foundation/Application.php @@ -10,6 +10,7 @@ namespace Flarum\Foundation; use Flarum\Foundation\Concerns\InteractsWithLaravel; +use Flarum\Http\RoutingServiceProvider; use Illuminate\Container\Container as IlluminateContainer; use Illuminate\Contracts\Foundation\Application as LaravelApplication; use Illuminate\Events\EventServiceProvider; @@ -69,21 +70,15 @@ class Application extends IlluminateContainer implements LaravelApplication IlluminateContainer::setInstance($this); $this->instance('app', $this); - $this->alias('app', IlluminateContainer::class); - $this->instance('container', $this); - $this->alias('container', IlluminateContainer::class); - $this->instance('flarum', $this); - $this->alias('flarum', self::class); - $this->instance('flarum.paths', $this->paths); - $this->alias('flarum.paths', Paths::class); } protected function registerBaseServiceProviders(): void { $this->register(new EventServiceProvider($this)); + $this->register(new RoutingServiceProvider($this)); } public function register($provider, $force = false): ServiceProvider @@ -166,13 +161,15 @@ class Application extends IlluminateContainer implements LaravelApplication $this->fireAppCallbacks($this->bootedCallbacks); } - protected function bootProvider(ServiceProvider $provider): mixed + protected function bootProvider(ServiceProvider $provider): void { + $provider->callBootingCallbacks(); + if (method_exists($provider, 'boot')) { - return $this->call([$provider, 'boot']); + $this->call([$provider, 'boot']); } - return null; + $provider->callBootedCallbacks(); } public function booting(mixed $callback): void @@ -199,11 +196,12 @@ class Application extends IlluminateContainer implements LaravelApplication public function registerCoreContainerAliases(): void { $aliases = [ - 'app' => [\Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::class, \Psr\Container\ContainerInterface::class], + 'app' => [\Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::class, \Psr\Container\ContainerInterface::class], 'blade.compiler' => [\Illuminate\View\Compilers\BladeCompiler::class], 'cache' => [\Illuminate\Cache\CacheManager::class, \Illuminate\Contracts\Cache\Factory::class], 'cache.store' => [\Illuminate\Cache\Repository::class, \Illuminate\Contracts\Cache\Repository::class], 'config' => [\Illuminate\Config\Repository::class, \Illuminate\Contracts\Config\Repository::class], + 'container' => [\Illuminate\Contracts\Container\Container::class, \Psr\Container\ContainerInterface::class], 'db' => [\Illuminate\Database\DatabaseManager::class], 'db.connection' => [\Illuminate\Database\Connection::class, \Illuminate\Database\ConnectionInterface::class], 'events' => [\Illuminate\Events\Dispatcher::class, \Illuminate\Contracts\Events\Dispatcher::class], @@ -211,8 +209,13 @@ class Application extends IlluminateContainer implements LaravelApplication 'filesystem' => [\Illuminate\Filesystem\FilesystemManager::class, \Illuminate\Contracts\Filesystem\Factory::class], 'filesystem.disk' => [\Illuminate\Contracts\Filesystem\Filesystem::class], 'filesystem.cloud' => [\Illuminate\Contracts\Filesystem\Cloud::class], + 'flarum' => [\Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::class, \Psr\Container\ContainerInterface::class, self::class], + 'flarum.paths' => [Paths::class], 'hash' => [\Illuminate\Contracts\Hashing\Hasher::class], 'mailer' => [\Illuminate\Mail\Mailer::class, \Illuminate\Contracts\Mail\Mailer::class, \Illuminate\Contracts\Mail\MailQueue::class], + 'router' => [\Flarum\Http\Router::class, \Illuminate\Routing\Router::class, \Illuminate\Contracts\Routing\Registrar::class, \Illuminate\Contracts\Routing\BindingRegistrar::class], + 'session' => [\Illuminate\Session\SessionManager::class], + 'session.store' => [\Illuminate\Session\Store::class, \Illuminate\Contracts\Session\Session::class], 'validator' => [\Illuminate\Validation\Factory::class, \Illuminate\Contracts\Validation\Factory::class], 'view' => [\Illuminate\View\Factory::class, \Illuminate\Contracts\View\Factory::class], ]; diff --git a/framework/core/src/Foundation/Config.php b/framework/core/src/Foundation/Config.php index f4d9c57aa..2b9e14c16 100644 --- a/framework/core/src/Foundation/Config.php +++ b/framework/core/src/Foundation/Config.php @@ -39,6 +39,15 @@ class Config implements ArrayAccess return $this->data['offline'] ?? false; } + public function path(string $frontend): string + { + return match(true) { + isset($this->data['paths'][$frontend]) => $this->data['paths'][$frontend], + $frontend === 'forum' => '/', + default => $frontend, + }; + } + private function requireKeys(mixed ...$keys): void { foreach ($keys as $key) { diff --git a/framework/core/src/Foundation/InstalledApp.php b/framework/core/src/Foundation/InstalledApp.php index c794753d6..4a7d66978 100644 --- a/framework/core/src/Foundation/InstalledApp.php +++ b/framework/core/src/Foundation/InstalledApp.php @@ -33,29 +33,16 @@ class InstalledApp implements AppInterface return $this->container; } - public function getRequestHandler(): RequestHandlerInterface + public function getMiddlewareStack(): array { - if ($this->config->inMaintenanceMode()) { - return $this->container->make('flarum.maintenance.handler'); - } elseif ($this->needsUpdate()) { - return $this->getUpdaterHandler(); - } +// if ($this->config->inMaintenanceMode()) { +// return $this->container->make('flarum.maintenance.handler'); +// } - $pipe = new MiddlewarePipe; - - $pipe->pipe(new HttpMiddleware\ProcessIp()); - $pipe->pipe(new BasePath($this->basePath())); - $pipe->pipe(new OriginalMessages); - $pipe->pipe( - new BasePathRouter([ - $this->subPath('api') => 'flarum.api.handler', - $this->subPath('admin') => 'flarum.admin.handler', - '/' => 'flarum.forum.handler', - ]) - ); - $pipe->pipe(new RequestHandler($this->container)); - - return $pipe; + return match ($this->needsUpdate()) { + true => $this->getUpdaterMiddlewareStack(), + false => $this->getStandardMiddlewareStack(), + }; } protected function needsUpdate(): bool @@ -66,16 +53,19 @@ class InstalledApp implements AppInterface return $version !== Application::VERSION; } - protected function getUpdaterHandler(): RequestHandlerInterface|MiddlewarePipe + protected function getUpdaterMiddlewareStack(): array { - $pipe = new MiddlewarePipe; - $pipe->pipe(new BasePath($this->basePath())); - $pipe->pipe( - new HttpMiddleware\ResolveRoute($this->container->make('flarum.update.routes')) - ); - $pipe->pipe(new HttpMiddleware\ExecuteRoute()); + return [ + new BasePath($this->basePath()), + ]; + } - return $pipe; + protected function getStandardMiddlewareStack(): array + { + return [ + new BasePath($this->basePath()), + new OriginalMessages, + ]; } protected function basePath(): string @@ -83,11 +73,6 @@ class InstalledApp implements AppInterface return $this->config->url()->getPath() ?: '/'; } - protected function subPath(string $pathName): string - { - return '/'.($this->config['paths'][$pathName] ?? $pathName); - } - public function getConsoleCommands(): array { return array_map(function ($command) { diff --git a/framework/core/src/Foundation/MaintenanceModeHandler.php b/framework/core/src/Foundation/MaintenanceModeHandler.php index a9a720476..2f3584b24 100644 --- a/framework/core/src/Foundation/MaintenanceModeHandler.php +++ b/framework/core/src/Foundation/MaintenanceModeHandler.php @@ -9,22 +9,22 @@ namespace Flarum\Foundation; +use Flarum\Http\Controller\AbstractController; +use Illuminate\Http\Request; use Illuminate\Support\Str; use Laminas\Diactoros\Response\HtmlResponse; use Laminas\Diactoros\Response\JsonResponse; use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\RequestHandlerInterface; use Tobscure\JsonApi\Document; -class MaintenanceModeHandler implements RequestHandlerInterface +class MaintenanceModeHandler extends AbstractController { const MESSAGE = 'Currently down for maintenance. Please come back later.'; /** * Handle the request and return a response. */ - public function handle(ServerRequestInterface $request): ResponseInterface + public function __invoke(Request $request): ResponseInterface { // Special handling for API requests: they get a proper API response if ($this->isApiRequest($request)) { @@ -35,10 +35,10 @@ class MaintenanceModeHandler implements RequestHandlerInterface return new HtmlResponse(self::MESSAGE, 503); } - private function isApiRequest(ServerRequestInterface $request): bool + private function isApiRequest(Request $request): bool { return Str::contains( - $request->getHeaderLine('Accept'), + $request->header('Accept'), 'application/vnd.api+json' ); } diff --git a/framework/core/src/Http/AccessToken.php b/framework/core/src/Http/AccessToken.php index 41f57c97d..e855f21ad 100644 --- a/framework/core/src/Http/AccessToken.php +++ b/framework/core/src/Http/AccessToken.php @@ -15,9 +15,8 @@ use Flarum\Database\ScopeVisibilityTrait; use Flarum\User\User; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Relations\BelongsTo; -use Illuminate\Support\Arr; +use Illuminate\Http\Request; use Illuminate\Support\Str; -use Psr\Http\Message\ServerRequestInterface; /** * @property int $id @@ -96,7 +95,7 @@ class AccessToken extends AbstractModel * Update the time of last usage of a token. * If a request object is provided, the IP address and User Agent will also be logged. */ - public function touch($attribute = null, ServerRequestInterface $request = null): bool + public function touch($attribute = null, Request $request = null): bool { $now = Carbon::now(); @@ -105,11 +104,11 @@ class AccessToken extends AbstractModel } if ($request) { - $this->last_ip_address = $request->getAttribute('ipAddress'); + $this->last_ip_address = $request->ip(); // We truncate user agent so it fits in the database column // The length is hard-coded as the column length // It seems like MySQL or Laravel already truncates values, but we'll play safe and do it ourselves - $agent = Arr::get($request->getServerParams(), 'HTTP_USER_AGENT'); + $agent = $request->server->get('HTTP_USER_AGENT'); $this->last_user_agent = substr($agent ?? '', 0, 255); } else { // If no request is provided, we set the values back to null diff --git a/framework/core/src/Http/CookieFactory.php b/framework/core/src/Http/CookieFactory.php index bf955ccb0..4e1ce03b6 100644 --- a/framework/core/src/Http/CookieFactory.php +++ b/framework/core/src/Http/CookieFactory.php @@ -9,9 +9,9 @@ namespace Flarum\Http; -use Dflydev\FigCookies\Modifier\SameSite; -use Dflydev\FigCookies\SetCookie; +use DateTime; use Flarum\Foundation\Config; +use Symfony\Component\HttpFoundation\Cookie; class CookieFactory { @@ -40,16 +40,14 @@ class CookieFactory * This method returns a cookie instance for use with the Set-Cookie HTTP header. * It will be pre-configured according to Flarum's base URL and protocol. */ - public function make(string $name, ?string $value = null, ?int $maxAge = null): SetCookie + public function make(string $name, ?string $value = null, ?int $maxAge = null): Cookie { - $cookie = SetCookie::create($this->getName($name), $value); + $cookie = Cookie::create($this->getName($name), $value); // Make sure we send both the MaxAge and Expires parameters (the former // is not supported by all browser versions) if ($maxAge) { - $cookie = $cookie - ->withMaxAge($maxAge) - ->withExpires(time() + $maxAge); + $cookie = $cookie->withExpires(time() + $maxAge); } if ($this->domain != null) { @@ -57,20 +55,20 @@ class CookieFactory } // Explicitly set SameSite value, use sensible default if no value provided - $cookie = $cookie->withSameSite(SameSite::{$this->samesite ?? 'lax'}()); + $cookie = $cookie->withSameSite($this->samesite ?? 'lax'); return $cookie ->withPath($this->path) ->withSecure($this->secure) - ->withHttpOnly(true); + ->withHttpOnly(); } /** * Make an expired cookie instance. */ - public function expire(string $name): SetCookie + public function expire(string $name): Cookie { - return $this->make($name)->expire(); + return $this->make($name)->withExpires(new DateTime('-5 years')); } /** diff --git a/framework/core/src/Http/Middleware/ExecuteRoute.php b/framework/core/src/Http/Middleware/ExecuteRoute.php deleted file mode 100644 index fb5c4a1a5..000000000 --- a/framework/core/src/Http/Middleware/ExecuteRoute.php +++ /dev/null @@ -1,29 +0,0 @@ -getAttribute('routeHandler'); - $parameters = $request->getAttribute('routeParameters'); - - return $handler($request, $parameters); - } -} diff --git a/framework/core/src/Http/Middleware/ParseJsonBody.php b/framework/core/src/Http/Middleware/ParseJsonBody.php deleted file mode 100644 index 9106f8cb6..000000000 --- a/framework/core/src/Http/Middleware/ParseJsonBody.php +++ /dev/null @@ -1,30 +0,0 @@ -getHeaderLine('content-type'), 'json')) { - $input = json_decode($request->getBody(), true); - - $request = $request->withParsedBody($input ?: []); - } - - return $handler->handle($request); - } -} diff --git a/framework/core/src/Http/Middleware/ProcessIp.php b/framework/core/src/Http/Middleware/ProcessIp.php deleted file mode 100644 index 0a5ae94bf..000000000 --- a/framework/core/src/Http/Middleware/ProcessIp.php +++ /dev/null @@ -1,26 +0,0 @@ -getServerParams(), 'REMOTE_ADDR', '127.0.0.1'); - - return $handler->handle($request->withAttribute('ipAddress', $ipAddress)); - } -} diff --git a/framework/core/src/Http/Middleware/ResolveRoute.php b/framework/core/src/Http/Middleware/ResolveRoute.php deleted file mode 100644 index df9066f95..000000000 --- a/framework/core/src/Http/Middleware/ResolveRoute.php +++ /dev/null @@ -1,66 +0,0 @@ -getMethod(); - $uri = $request->getUri()->getPath() ?: '/'; - - $routeInfo = $this->getDispatcher()->dispatch($method, $uri); - - switch ($routeInfo[0]) { - case Dispatcher::NOT_FOUND: - throw new RouteNotFoundException($uri); - case Dispatcher::METHOD_NOT_ALLOWED: - throw new MethodNotAllowedException($method); - default: - $request = $request - ->withAttribute('routeName', $routeInfo[1]['name']) - ->withAttribute('routeHandler', $routeInfo[1]['handler']) - ->withAttribute('routeParameters', $routeInfo[2]); - - return $handler->handle($request); - } - } - - protected function getDispatcher(): Dispatcher\GroupCountBased - { - if (! isset($this->dispatcher)) { - $this->dispatcher = new Dispatcher\GroupCountBased($this->routes->getRouteData()); - } - - return $this->dispatcher; - } -} diff --git a/framework/core/src/Http/Rememberer.php b/framework/core/src/Http/Rememberer.php index 24dc23d90..374e7183c 100644 --- a/framework/core/src/Http/Rememberer.php +++ b/framework/core/src/Http/Rememberer.php @@ -9,8 +9,7 @@ namespace Flarum\Http; -use Dflydev\FigCookies\FigResponseCookies; -use Psr\Http\Message\ResponseInterface; +use Symfony\Component\HttpFoundation\Response; class Rememberer { @@ -24,19 +23,21 @@ class Rememberer /** * Sets the remember cookie on a response. */ - public function remember(ResponseInterface $response, RememberAccessToken $token): ResponseInterface + public function remember(Response $response, RememberAccessToken $token): Response { - return FigResponseCookies::set( - $response, + $response->headers->setCookie( $this->cookie->make(self::COOKIE_NAME, $token->token, RememberAccessToken::rememberCookieLifeTime()) ); + + return $response; } - public function forget(ResponseInterface $response): ResponseInterface + public function forget(Response $response): Response { - return FigResponseCookies::set( - $response, + $response->headers->setCookie( $this->cookie->expire(self::COOKIE_NAME) ); + + return $response; } } diff --git a/framework/core/src/Http/RequestUtil.php b/framework/core/src/Http/RequestUtil.php index 47f930868..b7c0931df 100644 --- a/framework/core/src/Http/RequestUtil.php +++ b/framework/core/src/Http/RequestUtil.php @@ -10,29 +10,78 @@ namespace Flarum\Http; use Flarum\User\User; -use Psr\Http\Message\ServerRequestInterface as Request; +use Illuminate\Http\Request as IlluminateRequest; +use Laminas\Diactoros\ResponseFactory; +use Laminas\Diactoros\ServerRequestFactory; +use Laminas\Diactoros\StreamFactory; +use Laminas\Diactoros\UploadedFileFactory; +use Psr\Http\Message\ServerRequestInterface; +use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory; +use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory; +use Symfony\Component\HttpFoundation\Request as SymfonyRequest; +use Symfony\Component\HttpFoundation\Response as SymfonyResponse; class RequestUtil { - public static function getActor(Request $request): User + public static function getActor(ServerRequestInterface|SymfonyRequest $request): User { - return $request->getAttribute('actorReference')->getActor(); + return self::getActorReference($request)->getActor(); } - public static function withActor(Request $request, User $actor): Request + public static function withActor(ServerRequestInterface|SymfonyRequest $request, User $actor): ServerRequestInterface|SymfonyRequest { - $actorReference = $request->getAttribute('actorReference'); + $actorReference = self::getActorReference($request); if (! $actorReference) { $actorReference = new ActorReference; - $request = $request->withAttribute('actorReference', $actorReference); + $request = self::setActorReference($request, $actorReference); } $actorReference->setActor($actor); - // @deprecated in 1.0 - $request = $request->withAttribute('actor', $actor); + return $request; + } + + private static function setActorReference(ServerRequestInterface|SymfonyRequest $request, ActorReference $reference): ServerRequestInterface|SymfonyRequest + { + if ($request instanceof ServerRequestInterface) { + $request = $request->withAttribute('actorReference', $reference); + } else { + $request->attributes->set('actorReference', $reference); + } return $request; } + + private static function getActorReference(ServerRequestInterface|SymfonyRequest $request): ?ActorReference + { + if ($request instanceof ServerRequestInterface) { + return $request->getAttribute('actorReference'); + } + + return $request->attributes->get('actorReference'); + } + + public static function toIlluminate(ServerRequestInterface $request): IlluminateRequest + { + $httpFoundationFactory = new HttpFoundationFactory(); + + return IlluminateRequest::createFromBase( + $httpFoundationFactory->createRequest($request) + ); + } + + public static function toPsr7(SymfonyRequest $request): ServerRequestInterface + { + $psrHttpFactory = new PsrHttpFactory( + new ServerRequestFactory(), new StreamFactory(), new UploadedFileFactory(), new ResponseFactory() + ); + + return $psrHttpFactory->createRequest($request); + } + + public static function responseToSymfony(\Psr\Http\Message\ResponseInterface $response): SymfonyResponse + { + return (new HttpFoundationFactory())->createResponse($response); + } } diff --git a/framework/core/src/Http/RouteCollection.php b/framework/core/src/Http/RouteCollection.php index 9d20e22e0..35e90bd0b 100644 --- a/framework/core/src/Http/RouteCollection.php +++ b/framework/core/src/Http/RouteCollection.php @@ -9,142 +9,28 @@ namespace Flarum\Http; -use FastRoute\DataGenerator; -use FastRoute\RouteParser; -use Illuminate\Support\Arr; +use Illuminate\Routing\RouteCollection as IlluminateRouteCollection; /** * @internal */ -class RouteCollection +class RouteCollection extends IlluminateRouteCollection { - protected array $reverse = []; - protected DataGenerator $dataGenerator; - protected RouteParser $routeParser; - protected array $routes = []; - protected array $pendingRoutes = []; - - public function __construct() + public function forgetNamedRoute($name): void { - $this->dataGenerator = new DataGenerator\GroupCountBased; - $this->routeParser = new RouteParser\Std; - } + $route = $this->getByName($name); - public function get(string $path, string $name, callable|string $handler): self - { - return $this->addRoute('GET', $path, $name, $handler); - } - - public function post(string $path, string $name, callable|string $handler): self - { - return $this->addRoute('POST', $path, $name, $handler); - } - - public function put(string $path, string $name, callable|string $handler): self - { - return $this->addRoute('PUT', $path, $name, $handler); - } - - public function patch(string $path, string $name, callable|string $handler): self - { - return $this->addRoute('PATCH', $path, $name, $handler); - } - - public function delete(string $path, string $name, callable|string $handler): self - { - return $this->addRoute('DELETE', $path, $name, $handler); - } - - public function addRoute(string $method, string $path, string $name, callable|string $handler): self - { - if (isset($this->routes[$name])) { - throw new \RuntimeException("Route $name already exists"); + if (! $route) { + return; } - $this->routes[$name] = $this->pendingRoutes[$name] = compact('method', 'path', 'handler'); + // Remove the quick lookup. + unset($this->nameList[$name]); - return $this; - } - - public function removeRoute(string $name): self - { - unset($this->routes[$name], $this->pendingRoutes[$name]); - - return $this; - } - - protected function applyRoutes(): void - { - foreach ($this->pendingRoutes as $name => $route) { - $routeDatas = $this->routeParser->parse($route['path']); - - foreach ($routeDatas as $routeData) { - $this->dataGenerator->addRoute($route['method'], $routeData, ['name' => $name, 'handler' => $route['handler']]); - } - - $this->reverse[$name] = $routeDatas; + // Remove from the routes. + foreach ($route->methods() as $method) { + unset($this->routes[$method][$route->getDomain().$route->uri()]); + unset($this->allRoutes[$method.$route->getDomain().$route->uri()]); } - - $this->pendingRoutes = []; - } - - public function getRoutes(): array - { - return $this->routes; - } - - public function getRouteData(): array - { - if (! empty($this->pendingRoutes)) { - $this->applyRoutes(); - } - - return $this->dataGenerator->getData(); - } - - protected function fixPathPart(mixed $part, array $parameters, string $routeName): string - { - if (! is_array($part)) { - return $part; - } - - if (! array_key_exists($part[0], $parameters)) { - throw new \InvalidArgumentException("Could not generate URL for route '$routeName': no value provided for required part '$part[0]'."); - } - - return $parameters[$part[0]]; - } - - public function getPath(string $name, array $parameters = []): string - { - if (! empty($this->pendingRoutes)) { - $this->applyRoutes(); - } - - if (isset($this->reverse[$name])) { - $maxMatches = 0; - $matchingParts = $this->reverse[$name][0]; - - // For a given route name, we want to choose the option that best matches the given parameters. - // Each routing option is an array of parts. Each part is either a constant string - // (which we don't care about here), or an array where the first element is the parameter name - // and the second element is a regex into which the parameter value is inserted, if the parameter matches. - foreach ($this->reverse[$name] as $parts) { - foreach ($parts as $i => $part) { - if (is_array($part) && Arr::exists($parameters, $part[0]) && $i > $maxMatches) { - $maxMatches = $i; - $matchingParts = $parts; - } - } - } - - $fixedParts = array_map(function ($part) use ($parameters, $name) { - return $this->fixPathPart($part, $parameters, $name); - }, $matchingParts); - - return '/'.ltrim(implode('', $fixedParts), '/'); - } - - throw new \RuntimeException("Route $name not found"); } } diff --git a/framework/core/src/Http/RouteHandlerFactory.php b/framework/core/src/Http/RouteHandlerFactory.php index f09e4b7bf..b18f21fff 100644 --- a/framework/core/src/Http/RouteHandlerFactory.php +++ b/framework/core/src/Http/RouteHandlerFactory.php @@ -12,9 +12,7 @@ namespace Flarum\Http; use Closure; use Flarum\Frontend\Controller as FrontendController; use Illuminate\Contracts\Container\Container; -use InvalidArgumentException; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Server\RequestHandlerInterface as Handler; +use Psr\Http\Server\RequestHandlerInterface; /** * @internal @@ -26,28 +24,27 @@ class RouteHandlerFactory ) { } - public function toController(callable|string $controller): Closure + public function toController(callable|string $controller): callable|string|array { - return function (Request $request, array $routeParams) use ($controller) { - $controller = $this->resolveController($controller); + // If it's a class and it implements the RequestHandlerInterface, we'll + // assume it's a PSR-7 request handler and we'll return [controller, 'handle'] + // as the callable. + if (is_string($controller) && class_exists($controller) && in_array(RequestHandlerInterface::class, class_implements($controller))) { + return [$controller, 'handle']; + } - $request = $request->withQueryParams(array_merge($request->getQueryParams(), $routeParams)); - - return $controller->handle($request); - }; + return $controller; } - public function toFrontend(string $frontend, callable|string|null $content = null): Closure + public function toFrontend(string $frontend, callable|string|null $content = null): callable { - return $this->toController(function (Container $container) use ($frontend, $content) { - $frontend = $container->make("flarum.frontend.$frontend"); + $frontend = $this->container->make("flarum.frontend.$frontend"); - if ($content) { - $frontend->content(is_callable($content) ? $content : $container->make($content)); - } + if ($content) { + $frontend->content(is_callable($content) ? $content : $this->container->make($content)); + } - return new FrontendController($frontend); - }); + return new FrontendController($frontend); } public function toForum(string $content = null): Closure @@ -59,17 +56,4 @@ class RouteHandlerFactory { return $this->toFrontend('admin', $content); } - - private function resolveController(callable|string $controller): Handler - { - $controller = is_callable($controller) - ? $this->container->call($controller) - : $this->container->make($controller); - - if (! $controller instanceof Handler) { - throw new InvalidArgumentException('Controller must be an instance of '.Handler::class); - } - - return $controller; - } } diff --git a/framework/core/src/Http/Router.php b/framework/core/src/Http/Router.php new file mode 100644 index 000000000..e5f270c37 --- /dev/null +++ b/framework/core/src/Http/Router.php @@ -0,0 +1,20 @@ +routes = new RouteCollection(); + } + + public function forgetRoute(string $name): void + { + $this->routes->forgetNamedRoute($name); + } +} diff --git a/framework/core/src/Http/RoutingServiceProvider.php b/framework/core/src/Http/RoutingServiceProvider.php new file mode 100644 index 000000000..b0d818cbc --- /dev/null +++ b/framework/core/src/Http/RoutingServiceProvider.php @@ -0,0 +1,16 @@ +app->singleton('router', function (Container $container) { + return new Router($container['events'], $container); + }); + } +} diff --git a/framework/core/src/Http/Server.php b/framework/core/src/Http/Server.php index 4fff8e0a0..c37061fa4 100644 --- a/framework/core/src/Http/Server.php +++ b/framework/core/src/Http/Server.php @@ -9,15 +9,12 @@ namespace Flarum\Http; +use Flarum\Foundation\AppInterface; use Flarum\Foundation\ErrorHandling\LogReporter; use Flarum\Foundation\SiteInterface; use Illuminate\Contracts\Container\Container; -use Laminas\Diactoros\Response; -use Laminas\Diactoros\ServerRequest; -use Laminas\Diactoros\ServerRequestFactory; -use Laminas\HttpHandlerRunner\Emitter\SapiEmitter; -use Laminas\HttpHandlerRunner\RequestHandlerRunner; -use Laminas\Stratigility\Middleware\ErrorResponseGenerator; +use Illuminate\Http\Request; +use Illuminate\Routing\Pipeline; use Psr\Log\LoggerInterface; use Throwable; @@ -30,18 +27,17 @@ class Server public function listen(): void { - $runner = new RequestHandlerRunner( - $this->safelyBootAndGetHandler(), - new SapiEmitter, - [ServerRequestFactory::class, 'fromGlobals'], - function (Throwable $e) { - $generator = new ErrorResponseGenerator; + $request = Request::capture(); + $siteApp = $this->safelyBoot(); + $container = $siteApp->getContainer(); + $globalMiddleware = $siteApp->getMiddlewareStack(); - return $generator($e, new ServerRequest, new Response); - } - ); - - $runner->run(); + (new Pipeline($container)) + ->send($request) + ->through($globalMiddleware) + ->then(function (Request $request) use ($container) { + return $container->make(Router::class)->dispatch($request); + }); } /** @@ -52,10 +48,10 @@ class Server * * @throws Throwable */ - private function safelyBootAndGetHandler() // @phpstan-ignore-line + private function safelyBoot(): AppInterface { try { - return $this->site->bootApp()->getRequestHandler(); + return $this->site->bootApp(); } catch (Throwable $e) { // Apply response code first so whatever happens, it's set before anything is printed http_response_code(500);