1
0
mirror of https://github.com/flarum/core.git synced 2025-08-28 10:30:54 +02:00

chore: replace request handling with illuminate http & router

This commit is contained in:
Sami Mazouz
2023-08-11 14:19:44 +01:00
parent a60e3d174f
commit 7d4549ea34
28 changed files with 503 additions and 892 deletions

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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)
);
};

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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');
};

View File

@@ -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');
}
}

View File

@@ -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)
);
};

View File

@@ -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[]

View File

@@ -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],
];

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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'
);
}

View File

@@ -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

View File

@@ -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'));
}
/**

View File

@@ -1,29 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Http\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;
class ExecuteRoute implements Middleware
{
/**
* Executes the route handler resolved in ResolveRoute.
*/
public function process(Request $request, Handler $handler): Response
{
$handler = $request->getAttribute('routeHandler');
$parameters = $request->getAttribute('routeParameters');
return $handler($request, $parameters);
}
}

View File

@@ -1,30 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Http\Middleware;
use Illuminate\Support\Str;
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;
class ParseJsonBody implements Middleware
{
public function process(Request $request, Handler $handler): Response
{
if (Str::contains($request->getHeaderLine('content-type'), 'json')) {
$input = json_decode($request->getBody(), true);
$request = $request->withParsedBody($input ?: []);
}
return $handler->handle($request);
}
}

View File

@@ -1,26 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Http\Middleware;
use Illuminate\Support\Arr;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface as Middleware;
use Psr\Http\Server\RequestHandlerInterface;
class ProcessIp implements Middleware
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$ipAddress = Arr::get($request->getServerParams(), 'REMOTE_ADDR', '127.0.0.1');
return $handler->handle($request->withAttribute('ipAddress', $ipAddress));
}
}

View File

@@ -1,66 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Http\Middleware;
use FastRoute\Dispatcher;
use Flarum\Http\Exception\MethodNotAllowedException;
use Flarum\Http\Exception\RouteNotFoundException;
use Flarum\Http\RouteCollection;
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;
class ResolveRoute implements Middleware
{
protected ?Dispatcher\GroupCountBased $dispatcher = null;
public function __construct(
protected RouteCollection $routes
) {
}
/**
* Resolve the given request from our route collection.
*
* @throws MethodNotAllowedException
* @throws RouteNotFoundException
*/
public function process(Request $request, Handler $handler): Response
{
$method = $request->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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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");
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Flarum\Http;
use Illuminate\Routing\Router as IlluminateRouter;
class Router extends IlluminateRouter
{
public function __construct(...$args)
{
parent::__construct(...$args);
$this->routes = new RouteCollection();
}
public function forgetRoute(string $name): void
{
$this->routes->forgetNamedRoute($name);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Flarum\Http;
use Illuminate\Contracts\Container\Container;
use Illuminate\Routing\RoutingServiceProvider as IlluminateRoutingServiceProvider;
class RoutingServiceProvider extends IlluminateRoutingServiceProvider
{
protected function registerRouter(): void
{
$this->app->singleton('router', function (Container $container) {
return new Router($container['events'], $container);
});
}
}

View File

@@ -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);