diff --git a/composer.json b/composer.json index 2b95c0c5c..3c1010f6f 100644 --- a/composer.json +++ b/composer.json @@ -150,7 +150,6 @@ "pusher/pusher-php-server": "^7.2", "s9e/text-formatter": "^2.13", "staudenmeir/eloquent-eager-limit": "^1.8.2", - "sycho/json-api": "^0.5.0", "sycho/sourcemap": "^2.0.0", "symfony/config": "^6.3", "symfony/console": "^6.3", diff --git a/extensions/likes/extend.php b/extensions/likes/extend.php index e84f0c5a1..56c147dfe 100644 --- a/extensions/likes/extend.php +++ b/extensions/likes/extend.php @@ -45,13 +45,13 @@ return [ ->fields(PostResourceFields::class) ->endpoint( [Endpoint\Index::class, Endpoint\Show::class, Endpoint\Create::class, Endpoint\Update::class], - function (Endpoint\Index|Endpoint\Show|Endpoint\Create|Endpoint\Update $endpoint): Endpoint\Endpoint { + function (Endpoint\Index|Endpoint\Show|Endpoint\Create|Endpoint\Update $endpoint): Endpoint\EndpointInterface { return $endpoint->addDefaultInclude(['likes']); } ), (new Extend\ApiResource(Resource\DiscussionResource::class)) - ->endpoint(Endpoint\Show::class, function (Endpoint\Show $endpoint): Endpoint\Endpoint { + ->endpoint(Endpoint\Show::class, function (Endpoint\Show $endpoint): Endpoint\EndpointInterface { return $endpoint->addDefaultInclude(['posts.likes']); }), diff --git a/extensions/mentions/extend.php b/extensions/mentions/extend.php index a58dc5764..38ef58492 100644 --- a/extensions/mentions/extend.php +++ b/extensions/mentions/extend.php @@ -63,7 +63,7 @@ return [ (new Extend\ApiResource(Resource\PostResource::class)) ->fields(PostResourceFields::class) - ->endpoint([Endpoint\Index::class, Endpoint\Show::class], function (Endpoint\Index|Endpoint\Show $endpoint): Endpoint\Endpoint { + ->endpoint([Endpoint\Index::class, Endpoint\Show::class], function (Endpoint\Index|Endpoint\Show $endpoint): Endpoint\EndpointInterface { return $endpoint->addDefaultInclude(['mentionedBy', 'mentionedBy.user', 'mentionedBy.discussion']); }) ->endpoint(Endpoint\Index::class, function (Endpoint\Index $endpoint): Endpoint\Index { @@ -131,7 +131,7 @@ return [ }), (new Extend\ApiResource(Resource\PostResource::class)) - ->endpoint([Endpoint\Index::class, Endpoint\Show::class], function (Endpoint\Index|Endpoint\Show $endpoint): Endpoint\Endpoint { + ->endpoint([Endpoint\Index::class, Endpoint\Show::class], function (Endpoint\Index|Endpoint\Show $endpoint): Endpoint\EndpointInterface { return $endpoint->eagerLoad(['mentionsTags']); }), ]), diff --git a/extensions/statistics/src/Api/Controller/ShowStatisticsData.php b/extensions/statistics/src/Api/Controller/ShowStatisticsData.php index 9319a6d1b..841cc2a53 100644 --- a/extensions/statistics/src/Api/Controller/ShowStatisticsData.php +++ b/extensions/statistics/src/Api/Controller/ShowStatisticsData.php @@ -12,6 +12,7 @@ namespace Flarum\Statistics\Api\Controller; use Carbon\Carbon; use DateTime; use Flarum\Discussion\Discussion; +use Flarum\Http\Exception\InvalidParameterException; use Flarum\Http\RequestUtil; use Flarum\Post\Post; use Flarum\Post\RegisteredTypesScope; @@ -24,7 +25,6 @@ use Laminas\Diactoros\Response\JsonResponse; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; -use Tobscure\JsonApi\Exception\InvalidParameterException; class ShowStatisticsData implements RequestHandlerInterface { diff --git a/extensions/tags/src/Api/Resource/TagResource.php b/extensions/tags/src/Api/Resource/TagResource.php index 033851db8..71e5e0089 100644 --- a/extensions/tags/src/Api/Resource/TagResource.php +++ b/extensions/tags/src/Api/Resource/TagResource.php @@ -40,7 +40,7 @@ class TagResource extends AbstractDatabaseResource } } - public function find(string $id, \Tobyz\JsonApiServer\Context $context): ?object + public function find(string $id, Context $context): ?object { $actor = $context->getActor(); diff --git a/framework/core/composer.json b/framework/core/composer.json index 18999dd6f..198626278 100644 --- a/framework/core/composer.json +++ b/framework/core/composer.json @@ -78,7 +78,6 @@ "psr/http-server-middleware": "^1.0.2", "s9e/text-formatter": "^2.13", "staudenmeir/eloquent-eager-limit": "^1.8.2", - "sycho/json-api": "^0.5.0", "sycho/sourcemap": "^2.0.0", "symfony/config": "^6.3", "symfony/console": "^6.3", diff --git a/framework/core/src/Api/ApiServiceProvider.php b/framework/core/src/Api/ApiServiceProvider.php index cd4b62aa8..339ed159a 100644 --- a/framework/core/src/Api/ApiServiceProvider.php +++ b/framework/core/src/Api/ApiServiceProvider.php @@ -10,6 +10,7 @@ namespace Flarum\Api; use Flarum\Api\Controller\AbstractSerializeController; +use Flarum\Api\Endpoint\EndpointInterface; use Flarum\Api\Serializer\AbstractSerializer; use Flarum\Api\Serializer\BasicDiscussionSerializer; use Flarum\Api\Serializer\NotificationSerializer; @@ -24,6 +25,7 @@ use Flarum\Http\UrlGenerator; use Illuminate\Contracts\Container\Container; use Laminas\Stratigility\MiddlewarePipe; use ReflectionClass; +use Tobyz\JsonApiServer\Endpoint\Endpoint; class ApiServiceProvider extends AbstractServiceProvider { @@ -42,6 +44,8 @@ class ApiServiceProvider extends AbstractServiceProvider Resource\DiscussionResource::class, Resource\NotificationResource::class, Resource\AccessTokenResource::class, + Resource\MailSettingResource::class, + Resource\ExtensionReadmeResource::class, ]; }); @@ -155,8 +159,7 @@ class ApiServiceProvider extends AbstractServiceProvider public function boot(Container $container): void { - AbstractSerializeController::setContainer($container); - AbstractSerializer::setContainer($container); + // } protected function populateRoutes(RouteCollection $routes, Container $container): void @@ -186,14 +189,16 @@ class ApiServiceProvider extends AbstractServiceProvider * None of the injected dependencies should be directly used within * the `endpoints` method. Encourage using callbacks. * - * @var \Flarum\Api\Endpoint\Endpoint[] $endpoints + * @var array $endpoints */ $endpoints = $resource->resolveEndpoints(true); foreach ($endpoints as $endpoint) { - $route = $endpoint->route(); + $method = $endpoint->method; + $path = rtrim("/$type$endpoint->path", '/'); + $name = "$type.$endpoint->name"; - $routes->addRoute($route->method, rtrim("/$type$route->path", '/'), "$type.$route->name", $factory->toApiResource($resource::class, $endpoint::class)); + $routes->addRoute($method, $path, $name, $factory->toApiResource($resource::class, $endpoint->name)); } } } diff --git a/framework/core/src/Api/Context.php b/framework/core/src/Api/Context.php index 06a10b23b..f2c025a7a 100644 --- a/framework/core/src/Api/Context.php +++ b/framework/core/src/Api/Context.php @@ -13,7 +13,6 @@ use Tobyz\JsonApiServer\Resource\Resource; class Context extends BaseContext { protected ?SearchResults $search = null; - protected int|string|null $modelId = null; /** * Data passed internally when reusing resource endpoint logic. @@ -26,13 +25,6 @@ class Context extends BaseContext */ protected array $parameters = []; - public function withModelId(int|string|null $id): static - { - $new = clone $this; - $new->modelId = $id; - return $new; - } - public function withSearchResults(SearchResults $search): static { $new = clone $this; @@ -47,11 +39,6 @@ class Context extends BaseContext return $new; } - public function getModelId(): int|string|null - { - return $this->modelId; - } - public function getSearchResults(): ?SearchResults { return $this->search; diff --git a/framework/core/src/Api/Controller/AbstractCreateController.php b/framework/core/src/Api/Controller/AbstractCreateController.php deleted file mode 100644 index 00b9c774b..000000000 --- a/framework/core/src/Api/Controller/AbstractCreateController.php +++ /dev/null @@ -1,21 +0,0 @@ -withStatus(201); - } -} diff --git a/framework/core/src/Api/Controller/AbstractListController.php b/framework/core/src/Api/Controller/AbstractListController.php deleted file mode 100644 index d5aa7f740..000000000 --- a/framework/core/src/Api/Controller/AbstractListController.php +++ /dev/null @@ -1,46 +0,0 @@ -extractLimit($request); - $offset = $this->extractOffset($request); - - $document->addPaginationLinks( - $url, - $request->getQueryParams(), - $offset, - $limit, - $total, - ); - - $document->setMeta([ - 'total' => $total, - 'perPage' => $limit, - 'page' => $offset / $limit + 1, - ]); - } -} diff --git a/framework/core/src/Api/Controller/AbstractSerializeController.php b/framework/core/src/Api/Controller/AbstractSerializeController.php deleted file mode 100644 index 2c65d74f0..000000000 --- a/framework/core/src/Api/Controller/AbstractSerializeController.php +++ /dev/null @@ -1,432 +0,0 @@ -|null - */ - public ?string $serializer; - - /** - * The relationships that are included by default. - * - * @var string[] - */ - public array $include = []; - - /** - * The relationships that are available to be included. - * - * @var string[] - */ - public array $optionalInclude = []; - - /** - * The maximum number of records that can be requested. - */ - public int $maxLimit = 50; - - /** - * The number of records included by default. - */ - public int $limit = 20; - - /** - * The fields that are available to be sorted by. - * - * @var string[] - */ - public array $sortFields = []; - - /** - * The default sort field and order to use. - * - * @var array|null - */ - public ?array $sort = null; - - protected static Container $container; - - /** - * @var array, callable[]> - */ - protected static array $beforeDataCallbacks = []; - - /** - * @var array, callable[]> - */ - protected static array $beforeSerializationCallbacks = []; - - /** - * @var string[][] - */ - protected static array $loadRelations = []; - - /** - * @var array - */ - protected static array $loadRelationCallables = []; - - public function handle(ServerRequestInterface $request): ResponseInterface - { - $document = new Document; - - foreach (array_reverse(array_merge([static::class], class_parents($this))) as $class) { - if (isset(static::$beforeDataCallbacks[$class])) { - foreach (static::$beforeDataCallbacks[$class] as $callback) { - $callback($this); - } - } - } - - $data = $this->data($request, $document); - - foreach (array_reverse(array_merge([static::class], class_parents($this))) as $class) { - if (isset(static::$beforeSerializationCallbacks[$class])) { - foreach (static::$beforeSerializationCallbacks[$class] as $callback) { - $callback($this, $data, $request, $document); - } - } - } - - if (empty($this->serializer)) { - throw new InvalidArgumentException('Serializer required for controller: '.static::class); - } - - $serializer = static::$container->make($this->serializer); - $serializer->setRequest($request); - - $element = $this->createElement($data, $serializer) - ->with($this->extractInclude($request)) - ->fields($this->extractFields($request)); - - $document->setData($element); - - return new JsonApiResponse($document); - } - - /** - * Get the data to be serialized and assigned to the response document. - */ - abstract protected function data(ServerRequestInterface $request, Document $document): mixed; - - /** - * Create a PHP JSON-API Element for output in the document. - */ - abstract protected function createElement(mixed $data, SerializerInterface $serializer): ElementInterface; - - /** - * Returns the relations to load added by extenders. - * - * @return string[] - */ - protected function getRelationsToLoad(Collection $models): array - { - $addedRelations = []; - - foreach (array_reverse(array_merge([static::class], class_parents($this))) as $class) { - if (isset(static::$loadRelations[$class])) { - $addedRelations = array_merge($addedRelations, static::$loadRelations[$class]); - } - } - - return $addedRelations; - } - - /** - * Returns the relation callables to load added by extenders. - * - * @return array - */ - protected function getRelationCallablesToLoad(Collection $models): array - { - $addedRelationCallables = []; - - foreach (array_reverse(array_merge([static::class], class_parents($this))) as $class) { - if (isset(static::$loadRelationCallables[$class])) { - $addedRelationCallables = array_merge($addedRelationCallables, static::$loadRelationCallables[$class]); - } - } - - return $addedRelationCallables; - } - - /** - * Eager loads the required relationships. - */ - protected function loadRelations(Collection $models, array $relations, ServerRequestInterface $request = null): void - { - $addedRelations = $this->getRelationsToLoad($models); - $addedRelationCallables = $this->getRelationCallablesToLoad($models); - - foreach ($addedRelationCallables as $name => $relation) { - $addedRelations[] = $name; - } - - if (! empty($addedRelations)) { - usort($addedRelations, function ($a, $b) { - return substr_count($a, '.') - substr_count($b, '.'); - }); - - foreach ($addedRelations as $relation) { - if (str_contains($relation, '.')) { - $parentRelation = Str::beforeLast($relation, '.'); - - if (! in_array($parentRelation, $relations, true)) { - continue; - } - } - - $relations[] = $relation; - } - } - - if (! empty($relations)) { - $relations = array_unique($relations); - } - - $callableRelations = []; - $nonCallableRelations = []; - - foreach ($relations as $relation) { - if (isset($addedRelationCallables[$relation])) { - $load = $addedRelationCallables[$relation]; - - $callableRelations[$relation] = function ($query) use ($load, $request, $relations) { - $load($query, $request, $relations); - }; - } else { - $nonCallableRelations[] = $relation; - } - } - - if (! empty($callableRelations)) { - $models->loadMissing($callableRelations); - } - - if (! empty($nonCallableRelations)) { - $models->loadMissing($nonCallableRelations); - } - } - - /** - * @throws \Tobscure\JsonApi\Exception\InvalidParameterException - */ - protected function extractInclude(ServerRequestInterface $request): array - { - $available = array_merge($this->include, $this->optionalInclude); - - return $this->buildParameters($request)->getInclude($available) ?: $this->include; - } - - protected function extractFields(ServerRequestInterface $request): array - { - return $this->buildParameters($request)->getFields(); - } - - /** - * @throws \Tobscure\JsonApi\Exception\InvalidParameterException - */ - protected function extractSort(ServerRequestInterface $request): ?array - { - return $this->buildParameters($request)->getSort($this->sortFields) ?: $this->sort; - } - - /** - * @throws \Tobscure\JsonApi\Exception\InvalidParameterException - */ - protected function extractOffset(ServerRequestInterface $request): int - { - return (int) $this->buildParameters($request)->getOffset($this->extractLimit($request)) ?: 0; - } - - /** - * @throws \Tobscure\JsonApi\Exception\InvalidParameterException - */ - protected function extractLimit(ServerRequestInterface $request): int - { - return (int) $this->buildParameters($request)->getLimit($this->maxLimit) ?: $this->limit; - } - - protected function extractFilter(ServerRequestInterface $request): array - { - return $this->buildParameters($request)->getFilter() ?: []; - } - - protected function buildParameters(ServerRequestInterface $request): Parameters - { - return new Parameters($request->getQueryParams()); - } - - protected function sortIsDefault(ServerRequestInterface $request): bool - { - return ! Arr::get($request->getQueryParams(), 'sort'); - } - - /** - * Set the serializer that will serialize data for the endpoint. - */ - public function setSerializer(string $serializer): void - { - $this->serializer = $serializer; - } - - /** - * Include the given relationship by default. - */ - public function addInclude(array|string $name): void - { - $this->include = array_merge($this->include, (array) $name); - } - - /** - * Don't include the given relationship by default. - */ - public function removeInclude(array|string $name): void - { - $this->include = array_diff($this->include, (array) $name); - } - - /** - * Make the given relationship available for inclusion. - */ - public function addOptionalInclude(array|string $name): void - { - $this->optionalInclude = array_merge($this->optionalInclude, (array) $name); - } - - /** - * Don't allow the given relationship to be included. - */ - public function removeOptionalInclude(array|string $name): void - { - $this->optionalInclude = array_diff($this->optionalInclude, (array) $name); - } - - /** - * Set the default number of results. - */ - public function setLimit(int $limit): void - { - $this->limit = $limit; - } - - /** - * Set the maximum number of results. - */ - public function setMaxLimit(int $max): void - { - $this->maxLimit = $max; - } - - /** - * Allow sorting results by the given field. - */ - public function addSortField(array|string $field): void - { - $this->sortFields = array_merge($this->sortFields, (array) $field); - } - - /** - * Disallow sorting results by the given field. - */ - public function removeSortField(array|string $field): void - { - $this->sortFields = array_diff($this->sortFields, (array) $field); - } - - /** - * Set the default sort order for the results. - */ - public function setSort(array $sort): void - { - $this->sort = $sort; - } - - public static function getContainer(): Container - { - return static::$container; - } - - /** - * @internal - */ - public static function setContainer(Container $container): void - { - static::$container = $container; - } - - /** - * @internal - */ - public static function addDataPreparationCallback(string $controllerClass, callable $callback): void - { - if (! isset(static::$beforeDataCallbacks[$controllerClass])) { - static::$beforeDataCallbacks[$controllerClass] = []; - } - - static::$beforeDataCallbacks[$controllerClass][] = $callback; - } - - /** - * @internal - */ - public static function addSerializationPreparationCallback(string $controllerClass, callable $callback): void - { - if (! isset(static::$beforeSerializationCallbacks[$controllerClass])) { - static::$beforeSerializationCallbacks[$controllerClass] = []; - } - - static::$beforeSerializationCallbacks[$controllerClass][] = $callback; - } - - /** - * @internal - */ - public static function setLoadRelations(string $controllerClass, array $relations): void - { - if (! isset(static::$loadRelations[$controllerClass])) { - static::$loadRelations[$controllerClass] = []; - } - - static::$loadRelations[$controllerClass] = array_merge(static::$loadRelations[$controllerClass], $relations); - } - - /** - * @internal - */ - public static function setLoadRelationCallables(string $controllerClass, array $relations): void - { - if (! isset(static::$loadRelationCallables[$controllerClass])) { - static::$loadRelationCallables[$controllerClass] = []; - } - - static::$loadRelationCallables[$controllerClass] = array_merge(static::$loadRelationCallables[$controllerClass], $relations); - } -} diff --git a/framework/core/src/Api/Controller/AbstractShowController.php b/framework/core/src/Api/Controller/AbstractShowController.php deleted file mode 100644 index b87b5c594..000000000 --- a/framework/core/src/Api/Controller/AbstractShowController.php +++ /dev/null @@ -1,21 +0,0 @@ -bus->dispatch( - new DeleteAvatar(Arr::get($request->getQueryParams(), 'id'), RequestUtil::getActor($request)) - ); - } -} diff --git a/framework/core/src/Api/Controller/ShowExtensionReadmeController.php b/framework/core/src/Api/Controller/ShowExtensionReadmeController.php deleted file mode 100644 index 034d6d0e3..000000000 --- a/framework/core/src/Api/Controller/ShowExtensionReadmeController.php +++ /dev/null @@ -1,37 +0,0 @@ -getQueryParams(), 'name'); - - RequestUtil::getActor($request)->assertAdmin(); - - return $this->extensions->getExtension($extensionName); - } -} diff --git a/framework/core/src/Api/Controller/ShowForumController.php b/framework/core/src/Api/Controller/ShowForumController.php index 677c9b18e..76ab32bc9 100644 --- a/framework/core/src/Api/Controller/ShowForumController.php +++ b/framework/core/src/Api/Controller/ShowForumController.php @@ -9,7 +9,6 @@ namespace Flarum\Api\Controller; -use Flarum\Api\Endpoint\Show; use Flarum\Api\JsonApi; use Flarum\Api\Resource\ForumResource; use Psr\Http\Message\ResponseInterface; @@ -26,7 +25,7 @@ class ShowForumController implements RequestHandlerInterface { return $this->api ->forResource(ForumResource::class) - ->forEndpoint(Show::class) + ->forEndpoint('show') ->handle($request); } } diff --git a/framework/core/src/Api/Controller/ShowMailSettingsController.php b/framework/core/src/Api/Controller/ShowMailSettingsController.php index 90a4dc654..bc23c7b66 100644 --- a/framework/core/src/Api/Controller/ShowMailSettingsController.php +++ b/framework/core/src/Api/Controller/ShowMailSettingsController.php @@ -9,36 +9,23 @@ namespace Flarum\Api\Controller; -use Flarum\Api\Serializer\MailSettingsSerializer; -use Flarum\Http\RequestUtil; -use Flarum\Settings\SettingsRepositoryInterface; -use Illuminate\Contracts\Validation\Factory; +use Flarum\Api\JsonApi; +use Flarum\Api\Resource\MailSettingResource; +use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use Tobscure\JsonApi\Document; +use Psr\Http\Server\RequestHandlerInterface; -class ShowMailSettingsController extends AbstractShowController +class ShowMailSettingsController implements RequestHandlerInterface { - public ?string $serializer = MailSettingsSerializer::class; + public function __construct( + protected JsonApi $api + ) {} - protected function data(ServerRequestInterface $request, Document $document): array + public function handle(ServerRequestInterface $request): ResponseInterface { - RequestUtil::getActor($request)->assertAdmin(); - - $drivers = array_map(function ($driver) { - return self::$container->make($driver); - }, self::$container->make('mail.supported_drivers')); - - $settings = self::$container->make(SettingsRepositoryInterface::class); - $configured = self::$container->make('flarum.mail.configured_driver'); - $actual = self::$container->make('mail.driver'); - $validator = self::$container->make(Factory::class); - - $errors = $configured->validate($settings, $validator); - - return [ - 'drivers' => $drivers, - 'sending' => $actual->canSend(), - 'errors' => $errors, - ]; + return $this->api + ->forResource(MailSettingResource::class) + ->forEndpoint('show') + ->handle($request); } } diff --git a/framework/core/src/Api/Controller/UploadAvatarController.php b/framework/core/src/Api/Controller/UploadAvatarController.php deleted file mode 100644 index 015fc3e08..000000000 --- a/framework/core/src/Api/Controller/UploadAvatarController.php +++ /dev/null @@ -1,40 +0,0 @@ -getQueryParams(), 'id'); - $actor = RequestUtil::getActor($request); - $file = Arr::get($request->getUploadedFiles(), 'avatar'); - - return $this->bus->dispatch( - new UploadAvatar($id, $file, $actor) - ); - } -} diff --git a/framework/core/src/Api/Controller/UploadImageController.php b/framework/core/src/Api/Controller/UploadImageController.php index cb1a82268..ac760f164 100644 --- a/framework/core/src/Api/Controller/UploadImageController.php +++ b/framework/core/src/Api/Controller/UploadImageController.php @@ -9,6 +9,7 @@ namespace Flarum\Api\Controller; +use Flarum\Api\JsonApi; use Flarum\Http\RequestUtil; use Flarum\Settings\SettingsRepositoryInterface; use Illuminate\Contracts\Filesystem\Factory; @@ -16,9 +17,9 @@ use Illuminate\Contracts\Filesystem\Filesystem; use Illuminate\Support\Arr; use Illuminate\Support\Str; use Intervention\Image\Interfaces\EncodedImageInterface; +use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\UploadedFileInterface; -use Tobscure\JsonApi\Document; abstract class UploadImageController extends ShowForumController { @@ -28,13 +29,16 @@ abstract class UploadImageController extends ShowForumController protected string $filenamePrefix = ''; public function __construct( + JsonApi $api, protected SettingsRepositoryInterface $settings, Factory $filesystemFactory ) { + parent::__construct($api); + $this->uploadDir = $filesystemFactory->disk('flarum-assets'); } - public function data(ServerRequestInterface $request, Document $document): array + public function handle(ServerRequestInterface $request): ResponseInterface { RequestUtil::getActor($request)->assertAdmin(); @@ -52,7 +56,7 @@ abstract class UploadImageController extends ShowForumController $this->settings->set($this->filePathSettingKey, $uploadName); - return parent::data($request, $document); + return parent::handle($request); } abstract protected function makeImage(UploadedFileInterface $file): EncodedImageInterface; diff --git a/framework/core/src/Api/Endpoint/Concerns/HasAuthorization.php b/framework/core/src/Api/Endpoint/Concerns/HasAuthorization.php index 41db94412..da6b44082 100644 --- a/framework/core/src/Api/Endpoint/Concerns/HasAuthorization.php +++ b/framework/core/src/Api/Endpoint/Concerns/HasAuthorization.php @@ -20,6 +20,8 @@ trait HasAuthorization */ protected null|string|Closure $ability = null; + protected bool $admin = false; + public function authenticated(bool|Closure $condition = true): self { $this->authenticated = $condition; @@ -34,6 +36,13 @@ trait HasAuthorization return $this; } + public function admin(bool $admin = true): self + { + $this->admin = $admin; + + return $this; + } + public function getAuthenticated(Context $context): bool { if (is_bool($this->authenticated)) { @@ -68,6 +77,10 @@ trait HasAuthorization $actor->assertRegistered(); } + if ($this->admin) { + $actor->assertAdmin(); + } + if ($ability = $this->getAuthorized($context)) { $actor->assertCan($ability, $context->model); } diff --git a/framework/core/src/Api/Endpoint/Concerns/HasCustomRoute.php b/framework/core/src/Api/Endpoint/Concerns/HasCustomRoute.php deleted file mode 100644 index 947f0e417..000000000 --- a/framework/core/src/Api/Endpoint/Concerns/HasCustomRoute.php +++ /dev/null @@ -1,15 +0,0 @@ -path = $path; - - return $this; - } -} diff --git a/framework/core/src/Api/Endpoint/Concerns/HasEagerLoading.php b/framework/core/src/Api/Endpoint/Concerns/HasEagerLoading.php index 90bd0d224..0135f1eff 100644 --- a/framework/core/src/Api/Endpoint/Concerns/HasEagerLoading.php +++ b/framework/core/src/Api/Endpoint/Concerns/HasEagerLoading.php @@ -2,12 +2,13 @@ namespace Flarum\Api\Endpoint\Concerns; +use Flarum\Api\Context; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Support\Str; -use Psr\Http\Message\ServerRequestInterface; +use Tobyz\JsonApiServer\Laravel\EloquentResource; /** * This is directed at eager loading relationships apart from the request includes. @@ -66,8 +67,14 @@ trait HasEagerLoading /** * Eager loads the required relationships. */ - protected function loadRelations(Collection $models, ServerRequestInterface $request, array $included = []): void + protected function loadRelations(Collection $models, Context $context, array $included = []): void { + if (! $context->collection instanceof EloquentResource) { + return; + } + + $request = $context->request; + $included = $this->stringInclude($included); $models = $models->filter(fn ($model) => $model instanceof Model); diff --git a/framework/core/src/Api/Endpoint/Create.php b/framework/core/src/Api/Endpoint/Create.php index ca5c8d754..8f5efc4a9 100644 --- a/framework/core/src/Api/Endpoint/Create.php +++ b/framework/core/src/Api/Endpoint/Create.php @@ -2,81 +2,27 @@ namespace Flarum\Api\Endpoint; +use Flarum\Api\Context; use Flarum\Api\Endpoint\Concerns\HasAuthorization; use Flarum\Api\Endpoint\Concerns\HasCustomHooks; -use Flarum\Api\Endpoint\Concerns\HasCustomRoute; use Flarum\Api\Endpoint\Concerns\HasEagerLoading; use Illuminate\Database\Eloquent\Collection; -use Psr\Http\Message\ResponseInterface; -use RuntimeException; -use Tobyz\JsonApiServer\Context; use Tobyz\JsonApiServer\Endpoint\Create as BaseCreate; -use Tobyz\JsonApiServer\Exception\ForbiddenException; -use Tobyz\JsonApiServer\Resource\Creatable; -use function Tobyz\JsonApiServer\json_api_response; -class Create extends BaseCreate implements Endpoint +class Create extends BaseCreate implements EndpointInterface { use HasAuthorization; use HasEagerLoading; - use HasCustomRoute; use HasCustomHooks; - public function handle(Context $context): ?ResponseInterface + public function setUp(): void { - $model = $this->execute($context); + parent::setUp(); - return json_api_response($document = $this->showResource($context, $model)) - ->withStatus(201) - ->withHeader('Location', $document['data']['links']['self']); - } + $this->after(function (Context $context, object $model) { + $this->loadRelations(Collection::make([$model]), $context, $this->getInclude($context)); - public function execute(Context $context): object - { - $collection = $context->collection; - - if (!$collection instanceof Creatable) { - throw new RuntimeException( - sprintf('%s must implement %s', get_class($collection), Creatable::class), - ); - } - - if (!$this->isVisible($context)) { - throw new ForbiddenException(); - } - - $this->callBeforeHook($context); - - $data = $this->parseData($context); - - $context = $context - ->withResource($resource = $context->resource($data['type'])) - ->withModel($model = $collection->newModel($context)); - - $this->assertFieldsValid($context, $data); - $this->fillDefaultValues($context, $data); - $this->deserializeValues($context, $data); - $this->assertDataValid($context, $data); - - $this->setValues($context, $data); - - $context = $context->withModel($model = $resource->create($model, $context)); - - $this->saveFields($context, $data); - - $model = $this->callAfterHook($context, $model); - - $this->loadRelations(Collection::make([$model]), $context->request, $this->getInclude($context)); - - return $model; - } - - public function route(): EndpointRoute - { - return new EndpointRoute( - name: 'create', - path: $this->path ?? '/', - method: 'POST', - ); + return $model; + }); } } diff --git a/framework/core/src/Api/Endpoint/Delete.php b/framework/core/src/Api/Endpoint/Delete.php index 65c5bcd2c..bf0908021 100644 --- a/framework/core/src/Api/Endpoint/Delete.php +++ b/framework/core/src/Api/Endpoint/Delete.php @@ -3,70 +3,9 @@ namespace Flarum\Api\Endpoint; use Flarum\Api\Endpoint\Concerns\HasAuthorization; -use Flarum\Api\Endpoint\Concerns\HasCustomRoute; -use Flarum\Api\Resource\Contracts\Deletable; -use Nyholm\Psr7\Response; -use Psr\Http\Message\ResponseInterface; -use RuntimeException; -use Tobyz\JsonApiServer\Context; use Tobyz\JsonApiServer\Endpoint\Delete as BaseDelete; -use Tobyz\JsonApiServer\Exception\ForbiddenException; -use function Tobyz\JsonApiServer\json_api_response; -class Delete extends BaseDelete implements Endpoint +class Delete extends BaseDelete implements EndpointInterface { use HasAuthorization; - use HasCustomRoute; - - /** {@inheritdoc} */ - public function handle(Context $context): ?ResponseInterface - { - $segments = explode('/', $context->path()); - - if (count($segments) !== 2) { - return null; - } - - $context = $context->withModelId($segments[1]); - - $this->execute($context); - - if ($meta = $this->serializeMeta($context)) { - return json_api_response(['meta' => $meta]); - } - - return new Response(204); - } - - public function execute(Context $context): bool - { - $model = $this->findResource($context, $context->getModelId()); - - $context = $context->withResource( - $resource = $context->resource($context->collection->resource($model, $context)), - ); - - if (!$resource instanceof Deletable) { - throw new RuntimeException( - sprintf('%s must implement %s', get_class($resource), Deletable::class), - ); - } - - if (!$this->isVisible($context = $context->withModel($model))) { - throw new ForbiddenException(); - } - - $resource->deleteAction($model, $context); - - return true; - } - - public function route(): EndpointRoute - { - return new EndpointRoute( - name: 'delete', - path: $this->path ?? '/{id}', - method: 'DELETE', - ); - } } diff --git a/framework/core/src/Api/Endpoint/Endpoint.php b/framework/core/src/Api/Endpoint/Endpoint.php index 034066f78..98787ac56 100644 --- a/framework/core/src/Api/Endpoint/Endpoint.php +++ b/framework/core/src/Api/Endpoint/Endpoint.php @@ -2,16 +2,9 @@ namespace Flarum\Api\Endpoint; -use Psr\Http\Message\ResponseInterface as Response; -use Tobyz\JsonApiServer\Context; use Tobyz\JsonApiServer\Endpoint\Endpoint as BaseEndpoint; -interface Endpoint extends BaseEndpoint +class Endpoint extends BaseEndpoint implements EndpointInterface { - /** @var \Flarum\Api\Context $context */ - public function handle(Context $context): ?Response; - - public function execute(Context $context): mixed; - - public function route(): EndpointRoute; + // } diff --git a/framework/core/src/Api/Endpoint/EndpointInterface.php b/framework/core/src/Api/Endpoint/EndpointInterface.php new file mode 100644 index 000000000..f26b74d45 --- /dev/null +++ b/framework/core/src/Api/Endpoint/EndpointInterface.php @@ -0,0 +1,8 @@ +getInclude($context); - $this->loadRelations($models, $context->request, $include); + $this->loadRelations($models, $context, $include); $serializer = new Serializer($context); @@ -142,13 +136,4 @@ class Index extends BaseIndex implements Endpoint return json_api_response(compact('data', 'included', 'meta', 'links')); } - - public function route(): EndpointRoute - { - return new EndpointRoute( - name: 'index', - path: $this->path ?? '/', - method: 'GET', - ); - } } diff --git a/framework/core/src/Api/Endpoint/Show.php b/framework/core/src/Api/Endpoint/Show.php index 73dd6ca5b..e99d7c720 100644 --- a/framework/core/src/Api/Endpoint/Show.php +++ b/framework/core/src/Api/Endpoint/Show.php @@ -2,68 +2,29 @@ namespace Flarum\Api\Endpoint; +use Flarum\Api\Context; use Flarum\Api\Endpoint\Concerns\ExtractsListingParams; use Flarum\Api\Endpoint\Concerns\HasAuthorization; use Flarum\Api\Endpoint\Concerns\HasCustomHooks; -use Flarum\Api\Endpoint\Concerns\HasCustomRoute; use Flarum\Api\Endpoint\Concerns\HasEagerLoading; use Illuminate\Database\Eloquent\Collection; -use Psr\Http\Message\ResponseInterface; -use Tobyz\JsonApiServer\Context; -use Tobyz\JsonApiServer\Endpoint\Concerns\FindsResources; -use Tobyz\JsonApiServer\Endpoint\Concerns\ShowsResources; use Tobyz\JsonApiServer\Endpoint\Show as BaseShow; -use Tobyz\JsonApiServer\Exception\ForbiddenException; -use function Tobyz\JsonApiServer\json_api_response; -class Show extends BaseShow implements Endpoint +class Show extends BaseShow implements EndpointInterface { - use FindsResources; - use ShowsResources; use HasAuthorization; use HasEagerLoading; - use HasCustomRoute; use ExtractsListingParams; use HasCustomHooks; - public function handle(Context $context): ?ResponseInterface + public function setUp(): void { - $segments = explode('/', $context->path()); + parent::setUp(); - $path = $this->route()->path; + $this->after(function (Context $context, object $model) { + $this->loadRelations(Collection::make([$model]), $context, $this->getInclude($context)); - if ($path !== '/' && count($segments) !== 2) { - return null; - } - - $context = $context->withModelId($path === '/' ? 1 : $segments[1]); - - $this->callBeforeHook($context); - - $model = $this->execute($context); - - if (!$this->isVisible($context = $context->withModel($model))) { - throw new ForbiddenException(); - } - - $model = $this->callAfterHook($context, $model); - - $this->loadRelations(Collection::make([$model]), $context->request, $this->getInclude($context)); - - return json_api_response($this->showResource($context, $model)); - } - - public function execute(Context $context): object - { - return $this->findResource($context, $context->getModelId()); - } - - public function route(): EndpointRoute - { - return new EndpointRoute( - name: 'show', - path: $this->path ?? '/{id}', - method: 'GET', - ); + return $model; + }); } } diff --git a/framework/core/src/Api/Endpoint/Update.php b/framework/core/src/Api/Endpoint/Update.php index bcb5e9997..f49aa86a9 100644 --- a/framework/core/src/Api/Endpoint/Update.php +++ b/framework/core/src/Api/Endpoint/Update.php @@ -2,85 +2,27 @@ namespace Flarum\Api\Endpoint; +use Flarum\Api\Context; use Flarum\Api\Endpoint\Concerns\HasAuthorization; use Flarum\Api\Endpoint\Concerns\HasCustomHooks; -use Flarum\Api\Endpoint\Concerns\HasCustomRoute; use Flarum\Api\Endpoint\Concerns\HasEagerLoading; use Illuminate\Database\Eloquent\Collection; -use Psr\Http\Message\ResponseInterface; -use RuntimeException; -use Tobyz\JsonApiServer\Context; use Tobyz\JsonApiServer\Endpoint\Update as BaseUpdate; -use Tobyz\JsonApiServer\Exception\ForbiddenException; -use Tobyz\JsonApiServer\Resource\Updatable; -use function Tobyz\JsonApiServer\json_api_response; -class Update extends BaseUpdate implements Endpoint +class Update extends BaseUpdate implements EndpointInterface { use HasAuthorization; use HasEagerLoading; - use HasCustomRoute; use HasCustomHooks; - public function handle(Context $context): ?ResponseInterface + public function setUp(): void { - $segments = explode('/', $context->path()); + parent::setUp(); - if (count($segments) !== 2) { - return null; - } + $this->after(function (Context $context, object $model) { + $this->loadRelations(Collection::make([$model]), $context, $this->getInclude($context)); - $context = $context->withModelId($segments[1]); - - $model = $this->execute($context); - - return json_api_response($this->showResource($context, $model)); - } - - public function execute(Context $context): object - { - $model = $this->findResource($context, $context->getModelId()); - - $context = $context->withResource( - $resource = $context->resource($context->collection->resource($model, $context)), - ); - - if (!$resource instanceof Updatable) { - throw new RuntimeException( - sprintf('%s must implement %s', get_class($resource), Updatable::class), - ); - } - - if (!$this->isVisible($context = $context->withModel($model))) { - throw new ForbiddenException(); - } - - $this->callBeforeHook($context); - - $data = $this->parseData($context); - - $this->assertFieldsValid($context, $data); - $this->deserializeValues($context, $data); - $this->assertDataValid($context, $data); - $this->setValues($context, $data); - - $context = $context->withModel($model = $resource->update($model, $context)); - - $this->saveFields($context, $data); - - $model = $this->callAfterHook($context, $model); - - $this->loadRelations(Collection::make([$model]), $context->request, $this->getInclude($context)); - - return $model; - } - - public function route(): EndpointRoute - { - return new EndpointRoute( - name: 'update', - path: $this->path ?? '/{id}', - method: 'PATCH', - ); + return $model; + }); } } diff --git a/framework/core/src/Api/JsonApi.php b/framework/core/src/Api/JsonApi.php index 6a82d89ed..101031409 100644 --- a/framework/core/src/Api/JsonApi.php +++ b/framework/core/src/Api/JsonApi.php @@ -2,8 +2,7 @@ namespace Flarum\Api; -use Flarum\Api\Endpoint\Endpoint; -use Flarum\Api\Endpoint\EndpointRoute; +use Flarum\Api\Endpoint\EndpointInterface; use Flarum\Api\Resource\AbstractDatabaseResource; use Flarum\Http\RequestUtil; use Illuminate\Contracts\Container\Container; @@ -11,6 +10,7 @@ use Laminas\Diactoros\ServerRequestFactory; use Laminas\Diactoros\Uri; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; +use Tobyz\JsonApiServer\Endpoint\Endpoint; use Tobyz\JsonApiServer\Exception\BadRequestException; use Tobyz\JsonApiServer\JsonApi as BaseJsonApi; use Tobyz\JsonApiServer\Resource\Collection; @@ -19,7 +19,7 @@ use Tobyz\JsonApiServer\Resource\Resource; class JsonApi extends BaseJsonApi { protected string $resourceClass; - protected string $endpoint; + protected string $endpointName; protected ?Request $baseRequest = null; protected ?Container $container = null; @@ -30,16 +30,16 @@ class JsonApi extends BaseJsonApi return $this; } - public function forEndpoint(string $endpoint): self + public function forEndpoint(string $endpointName): self { - $this->endpoint = $endpoint; + $this->endpointName = $endpointName; return $this; } protected function makeContext(Request $request): Context { - if (! $this->endpoint || ! $this->resourceClass || ! class_exists($this->resourceClass)) { + if (! $this->endpointName || ! $this->resourceClass || ! class_exists($this->resourceClass)) { throw new BadRequestException('No resource or endpoint specified'); } @@ -50,11 +50,11 @@ class JsonApi extends BaseJsonApi ->withEndpoint($this->findEndpoint($collection)); } - protected function findEndpoint(?Collection $collection): Endpoint + protected function findEndpoint(?Collection $collection): Endpoint&EndpointInterface { - /** @var \Flarum\Api\Endpoint\Endpoint $endpoint */ + /** @var Endpoint&EndpointInterface $endpoint */ foreach ($collection->resolveEndpoints() as $endpoint) { - if ($endpoint::class === $this->endpoint) { + if ($endpoint->name === $this->endpointName) { return $endpoint; } } @@ -76,11 +76,8 @@ class JsonApi extends BaseJsonApi return $context->endpoint->handle($context); } - public function execute(array $body, array $internal = [], array $options = []): mixed + public function process(array $body, array $internal = [], array $options = []): mixed { - /** @var EndpointRoute $route */ - $route = (new $this->endpoint)->route(); - $request = $this->baseRequest ?? ServerRequestFactory::fromGlobals(); if (! empty($options['actor'])) { @@ -90,8 +87,6 @@ class JsonApi extends BaseJsonApi $resource = $this->getCollection($this->resourceClass); $request = $request - ->withMethod($route->method) - ->withUri(new Uri($route->path)) ->withParsedBody([ ...$body, 'data' => [ @@ -110,7 +105,13 @@ class JsonApi extends BaseJsonApi $context = $context->withInternal($key, $value); } - return $context->endpoint->execute($context); + $context = $context->withRequest( + $request + ->withMethod($context->endpoint->method) + ->withUri(new Uri($context->endpoint->path)) + ); + + return $context->endpoint->process($context); } public function validateQueryParameters(Request $request): void diff --git a/framework/core/src/Api/JsonApiResponse.php b/framework/core/src/Api/JsonApiResponse.php index b05a5dcb6..6ef793d6a 100644 --- a/framework/core/src/Api/JsonApiResponse.php +++ b/framework/core/src/Api/JsonApiResponse.php @@ -10,11 +10,10 @@ namespace Flarum\Api; use Laminas\Diactoros\Response\JsonResponse; -use Tobscure\JsonApi\Document; class JsonApiResponse extends JsonResponse { - public function __construct(Document $document, $status = 200, array $headers = [], $encodingOptions = 15) + public function __construct(array $document, $status = 200, array $headers = [], $encodingOptions = 15) { $headers['content-type'] = 'application/vnd.api+json'; diff --git a/framework/core/src/Api/Resource/AbstractDatabaseResource.php b/framework/core/src/Api/Resource/AbstractDatabaseResource.php index c9faa998a..6ea0ceb7f 100644 --- a/framework/core/src/Api/Resource/AbstractDatabaseResource.php +++ b/framework/core/src/Api/Resource/AbstractDatabaseResource.php @@ -59,18 +59,18 @@ abstract class AbstractDatabaseResource extends BaseResource implements throw new RuntimeException('Not supported in Flarum, please use a model searcher instead https://docs.flarum.org/extend/search.'); } - public function create(object $model, Context $context): object + public function createAction(object $model, Context $context): object { - $model = parent::create($model, $context); + $model = parent::createAction($model, $context); $this->dispatchEventsFor($model, $context->getActor()); return $model; } - public function update(object $model, Context $context): object + public function updateAction(object $model, Context $context): object { - $model = parent::update($model, $context); + $model = parent::updateAction($model, $context); $this->dispatchEventsFor($model, $context->getActor()); diff --git a/framework/core/src/Api/Resource/Contracts/Deletable.php b/framework/core/src/Api/Resource/Contracts/Deletable.php index 3e177ba5c..1ba1b4eb2 100644 --- a/framework/core/src/Api/Resource/Contracts/Deletable.php +++ b/framework/core/src/Api/Resource/Contracts/Deletable.php @@ -7,5 +7,5 @@ use Tobyz\JsonApiServer\Resource\Deletable as BaseDeletable; interface Deletable extends BaseDeletable { - public function deleteAction(object $model, Context $context): void; + // } diff --git a/framework/core/src/Api/Resource/DiscussionResource.php b/framework/core/src/Api/Resource/DiscussionResource.php index 7ee42c1c7..1dd8c91e6 100644 --- a/framework/core/src/Api/Resource/DiscussionResource.php +++ b/framework/core/src/Api/Resource/DiscussionResource.php @@ -5,7 +5,6 @@ namespace Flarum\Api\Resource; use Carbon\Carbon; use Flarum\Api\Context; use Flarum\Api\Endpoint; -use Flarum\Api\Endpoint\Create; use Flarum\Api\JsonApi; use Flarum\Api\Schema; use Flarum\Api\Sort\SortColumn; @@ -302,9 +301,9 @@ class DiscussionResource extends AbstractDatabaseResource // Now that the discussion has been created, we can add the first post. // We will do this by running the PostReply command. $post = $api->forResource(PostResource::class) - ->forEndpoint(Create::class) + ->forEndpoint('create') ->withRequest($context->request) - ->execute([ + ->process([ 'data' => [ 'attributes' => [ 'content' => Arr::get($context->body(), 'data.attributes.content'), diff --git a/framework/core/src/Api/Resource/ExtensionReadmeResource.php b/framework/core/src/Api/Resource/ExtensionReadmeResource.php new file mode 100644 index 000000000..a6355117f --- /dev/null +++ b/framework/core/src/Api/Resource/ExtensionReadmeResource.php @@ -0,0 +1,60 @@ +getId(); + } + + public function find(string $id, Context $context): ?object + { + return $this->extensions->getExtension($id); + } + + public function endpoints(): array + { + return [ + Endpoint\Show::make() + ->admin(), + ]; + } + + public function fields(): array + { + return [ + Schema\Str::make('content') + ->get(fn (Extension $extension) => $extension->getReadme()), + ]; + } +} diff --git a/framework/core/src/Api/Resource/ForumResource.php b/framework/core/src/Api/Resource/ForumResource.php index c492c78f4..22d792946 100644 --- a/framework/core/src/Api/Resource/ForumResource.php +++ b/framework/core/src/Api/Resource/ForumResource.php @@ -38,6 +38,11 @@ class ForumResource extends AbstractResource implements Findable return '1'; } + public function id(\Tobyz\JsonApiServer\Context $context): ?string + { + return '1'; + } + public function find(string $id, \Tobyz\JsonApiServer\Context $context): ?object { return new stdClass(); @@ -48,7 +53,7 @@ class ForumResource extends AbstractResource implements Findable return [ Endpoint\Show::make() ->defaultInclude(['groups', 'actor.groups']) - ->path('/'), + ->route('GET', '/'), ]; } diff --git a/framework/core/src/Api/Resource/MailSettingResource.php b/framework/core/src/Api/Resource/MailSettingResource.php new file mode 100644 index 000000000..7ee849df1 --- /dev/null +++ b/framework/core/src/Api/Resource/MailSettingResource.php @@ -0,0 +1,78 @@ +route('GET', '/') + ->admin(), + ]; + } + + public function fields(): array + { + return [ + Schema\Arr::make('fields') + ->get(function () { + return array_map(fn (DriverInterface $driver) => $driver->availableSettings(), array_map(function ($driver) { + return $this->container->make($driver); + }, $this->container->make('mail.supported_drivers'))); + }), + Schema\Boolean::make('sending') + ->get(function () { + /** @var DriverInterface $actual */ + $actual = $this->container->make('mail.driver'); + + return $actual->canSend(); + }), + Schema\Arr::make('errors') + ->get(function () { + /** @var DriverInterface $configured */ + $configured = $this->container->make('flarum.mail.configured_driver'); + + return $configured->validate($this->settings, $this->validator); + }), + ]; + } +} diff --git a/framework/core/src/Api/Resource/UserResource.php b/framework/core/src/Api/Resource/UserResource.php index 48b924ef8..cb7dcc340 100644 --- a/framework/core/src/Api/Resource/UserResource.php +++ b/framework/core/src/Api/Resource/UserResource.php @@ -6,11 +6,15 @@ use Flarum\Api\Context; use Flarum\Api\Endpoint; use Flarum\Api\Schema; use Flarum\Api\Sort\SortColumn; +use Flarum\Bus\Dispatcher; use Flarum\Foundation\ValidationException; +use Flarum\Http\RequestUtil; use Flarum\Http\SlugManager; use Flarum\Locale\TranslatorInterface; use Flarum\Settings\SettingsRepositoryInterface; use Flarum\User\AvatarUploader; +use Flarum\User\Command\DeleteAvatar; +use Flarum\User\Command\UploadAvatar; use Flarum\User\Event\Deleting; use Flarum\User\Event\GroupsChanged; use Flarum\User\Event\RegisteringFromProvider; @@ -32,7 +36,8 @@ class UserResource extends AbstractDatabaseResource protected SlugManager $slugManager, protected SettingsRepositoryInterface $settings, protected ImageManager $imageManager, - protected AvatarUploader $avatarUploader + protected AvatarUploader $avatarUploader, + protected Dispatcher $bus, ) { } @@ -105,6 +110,22 @@ class UserResource extends AbstractDatabaseResource ->can('searchUsers') ->defaultInclude(['groups']) ->paginate(), + Endpoint\Endpoint::make('avatar.upload') + ->route('POST', '/{id}/avatar') + ->action(function (Context $context) { + $file = Arr::get($context->request->getUploadedFiles(), 'avatar'); + + return $this->bus->dispatch( + new UploadAvatar((int) $context->modelId, $file, $context->getActor()) + ); + }), + Endpoint\Endpoint::make('avatar.delete') + ->route('DELETE', '/{id}/avatar') + ->action(function (Context $context) { + return $this->bus->dispatch( + new DeleteAvatar(Arr::get($context->request->getQueryParams(), 'id'), $context->getActor()) + ); + }), ]; } diff --git a/framework/core/src/Api/Serializer/AbstractSerializer.php b/framework/core/src/Api/Serializer/AbstractSerializer.php deleted file mode 100644 index a14209453..000000000 --- a/framework/core/src/Api/Serializer/AbstractSerializer.php +++ /dev/null @@ -1,234 +0,0 @@ - - */ - protected static array $attributeMutators = []; - - /** - * @var array> - */ - protected static array $customRelations = []; - - public function getRequest(): Request - { - return $this->request; - } - - public function setRequest(Request $request): void - { - $this->request = $request; - $this->actor = RequestUtil::getActor($request); - } - - public function getActor(): User - { - return $this->actor; - } - - public function getAttributes(mixed $model, array $fields = null): array - { - if (! is_object($model) && ! is_array($model)) { - return []; - } - - $attributes = $this->getDefaultAttributes($model); - - foreach (array_reverse(array_merge([static::class], class_parents($this))) as $class) { - if (isset(static::$attributeMutators[$class])) { - foreach (static::$attributeMutators[$class] as $callback) { - $attributes = array_merge( - $attributes, - $callback($this, $model, $attributes) - ); - } - } - } - - return $attributes; - } - - /** - * Get the default set of serialized attributes for a model. - */ - abstract protected function getDefaultAttributes(object|array $model): array; - - public function formatDate(DateTime $date = null): ?string - { - return $date?->format(DateTime::RFC3339); - } - - public function getRelationship($model, $name) - { - if ($relationship = $this->getCustomRelationship($model, $name)) { - return $relationship; - } - - return parent::getRelationship($model, $name); - } - - /** - * Get a custom relationship. - */ - protected function getCustomRelationship(object|array $model, string $name): ?Relationship - { - foreach (array_merge([static::class], class_parents($this)) as $class) { - $callback = Arr::get(static::$customRelations, "$class.$name"); - - if (is_callable($callback)) { - $relationship = $callback($this, $model); - - if (isset($relationship) && ! ($relationship instanceof Relationship)) { - throw new LogicException( - 'GetApiRelationship handler must return an instance of '.Relationship::class - ); - } - - return $relationship; - } - } - - return null; - } - - /** - * Get a relationship builder for a has-one relationship. - */ - public function hasOne(object|array $model, SerializerInterface|Closure|string $serializer, string $relation = null): ?Relationship - { - return $this->buildRelationship($model, $serializer, $relation); - } - - /** - * Get a relationship builder for a has-many relationship. - */ - public function hasMany(object|array $model, SerializerInterface|Closure|string $serializer, string $relation = null): ?Relationship - { - return $this->buildRelationship($model, $serializer, $relation, true); - } - - protected function buildRelationship(object|array $model, SerializerInterface|Closure|string $serializer, string $relation = null, bool $many = false): ?Relationship - { - if (is_null($relation)) { - list(, , $caller) = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 3); - - $relation = $caller['function']; - } - - $data = $this->getRelationshipData($model, $relation); - - if ($data) { - $serializer = $this->resolveSerializer($serializer, $model, $data); - - $type = $many ? Collection::class : Resource::class; - - $element = new $type($data, $serializer); - - return new Relationship($element); - } - - return null; - } - - protected function getRelationshipData(object|array $model, string $relation): mixed - { - if (is_object($model)) { - return $model->$relation; - } - - return $model[$relation]; - } - - /** - * @throws InvalidArgumentException - */ - protected function resolveSerializer(SerializerInterface|Closure|string $serializer, object|array $model, mixed $data): SerializerInterface - { - if ($serializer instanceof Closure) { - $serializer = call_user_func($serializer, $model, $data); - } - - if (is_string($serializer)) { - $serializer = $this->resolveSerializerClass($serializer); - } - - if (! ($serializer instanceof SerializerInterface)) { - throw new InvalidArgumentException('Serializer must be an instance of ' - .SerializerInterface::class); - } - - return $serializer; - } - - protected function resolveSerializerClass(string $class): object - { - $serializer = static::$container->make($class); - - $serializer->setRequest($this->request); - - return $serializer; - } - - public static function getContainer(): Container - { - return static::$container; - } - - /** - * @internal - */ - public static function setContainer(Container $container): void - { - static::$container = $container; - } - - /** - * @internal - */ - public static function addAttributeMutator(string $serializerClass, callable $callback): void - { - if (! isset(static::$attributeMutators[$serializerClass])) { - static::$attributeMutators[$serializerClass] = []; - } - - static::$attributeMutators[$serializerClass][] = $callback; - } - - /** - * @internal - */ - public static function setRelationship(string $serializerClass, string $relation, callable $callback): void - { - static::$customRelations[$serializerClass][$relation] = $callback; - } -} diff --git a/framework/core/src/Api/Serializer/AccessTokenSerializer.php b/framework/core/src/Api/Serializer/AccessTokenSerializer.php deleted file mode 100644 index 6b8c8e744..000000000 --- a/framework/core/src/Api/Serializer/AccessTokenSerializer.php +++ /dev/null @@ -1,66 +0,0 @@ -request->getAttribute('session'); - - $agent = new Agent(); - $agent->setUserAgent($model->last_user_agent); - - $attributes = [ - 'token' => $model->token, - 'userId' => $model->user_id, - 'createdAt' => $this->formatDate($model->created_at), - 'lastActivityAt' => $this->formatDate($model->last_activity_at), - 'isCurrent' => $session && $session->get('access_token') === $model->token, - 'isSessionToken' => in_array($model->type, ['session', 'session_remember'], true), - 'title' => $model->title, - 'lastIpAddress' => $model->last_ip_address, - 'device' => $this->translator->trans('core.forum.security.browser_on_operating_system', [ - 'browser' => $agent->browser(), - 'os' => $agent->platform(), - ]), - ]; - - // Unset hidden attributes (like the token value on session tokens) - foreach ($model->getHidden() as $name) { - unset($attributes[$name]); - } - - // Hide the token value to non-actors no matter who they are. - if (isset($attributes['token']) && $this->getActor()->id !== $model->user_id) { - unset($attributes['token']); - } - - return $attributes; - } -} diff --git a/framework/core/src/Api/Serializer/BasicDiscussionSerializer.php b/framework/core/src/Api/Serializer/BasicDiscussionSerializer.php deleted file mode 100644 index 7260f09df..000000000 --- a/framework/core/src/Api/Serializer/BasicDiscussionSerializer.php +++ /dev/null @@ -1,77 +0,0 @@ - $model->title, - 'slug' => $this->slugManager->forResource(Discussion::class)->toSlug($model), - ]; - } - - protected function user(Discussion $discussion): ?Relationship - { - return $this->hasOne($discussion, BasicUserSerializer::class); - } - - protected function firstPost(Discussion $discussion): ?Relationship - { - return $this->hasOne($discussion, BasicPostSerializer::class); - } - - protected function lastPostedUser(Discussion $discussion): ?Relationship - { - return $this->hasOne($discussion, BasicUserSerializer::class); - } - - protected function lastPost(Discussion $discussion): ?Relationship - { - return $this->hasOne($discussion, BasicPostSerializer::class); - } - - protected function posts(Discussion $discussion): ?Relationship - { - return $this->hasMany($discussion, PostSerializer::class); - } - - protected function mostRelevantPost(Discussion $discussion): ?Relationship - { - return $this->hasOne($discussion, PostSerializer::class); - } - - protected function hiddenUser(Discussion $discussion): ?Relationship - { - return $this->hasOne($discussion, BasicUserSerializer::class); - } -} diff --git a/framework/core/src/Api/Serializer/BasicPostSerializer.php b/framework/core/src/Api/Serializer/BasicPostSerializer.php deleted file mode 100644 index 8193e2e24..000000000 --- a/framework/core/src/Api/Serializer/BasicPostSerializer.php +++ /dev/null @@ -1,72 +0,0 @@ - (int) $model->number, - 'createdAt' => $this->formatDate($model->created_at), - 'contentType' => $model->type - ]; - - if ($model instanceof CommentPost) { - try { - $attributes['contentHtml'] = $model->formatContent($this->request); - $attributes['renderFailed'] = false; - } catch (Exception $e) { - $attributes['contentHtml'] = $this->translator->trans('core.lib.error.render_failed_message'); - $this->log->report($e); - $attributes['renderFailed'] = true; - } - } else { - $attributes['content'] = $model->content; - } - - return $attributes; - } - - protected function user(Post $post): ?Relationship - { - return $this->hasOne($post, BasicUserSerializer::class); - } - - protected function discussion(Post $post): ?Relationship - { - return $this->hasOne($post, BasicDiscussionSerializer::class); - } -} diff --git a/framework/core/src/Api/Serializer/BasicUserSerializer.php b/framework/core/src/Api/Serializer/BasicUserSerializer.php deleted file mode 100644 index 0c23b9f47..000000000 --- a/framework/core/src/Api/Serializer/BasicUserSerializer.php +++ /dev/null @@ -1,53 +0,0 @@ - $model->username, - 'displayName' => $model->display_name, - 'avatarUrl' => $model->avatar_url, - 'slug' => $this->slugManager->forResource(User::class)->toSlug($model) - ]; - } - - protected function groups(User $user): Relationship - { - if ($this->getActor()->can('viewHiddenGroups')) { - return $this->hasMany($user, GroupSerializer::class); - } - - return $this->hasMany($user, GroupSerializer::class, 'visibleGroups'); - } -} diff --git a/framework/core/src/Api/Serializer/CurrentUserSerializer.php b/framework/core/src/Api/Serializer/CurrentUserSerializer.php deleted file mode 100644 index b98bf7875..000000000 --- a/framework/core/src/Api/Serializer/CurrentUserSerializer.php +++ /dev/null @@ -1,39 +0,0 @@ - (bool) $model->is_email_confirmed, - 'email' => $model->email, - 'markedAllAsReadAt' => $this->formatDate($model->marked_all_as_read_at), - 'unreadNotificationCount' => (int) $model->getUnreadNotificationCount(), - 'newNotificationCount' => (int) $model->getNewNotificationCount(), - 'preferences' => (array) $model->preferences, - 'isAdmin' => $model->isAdmin(), - ]; - - return $attributes; - } -} diff --git a/framework/core/src/Api/Serializer/DiscussionSerializer.php b/framework/core/src/Api/Serializer/DiscussionSerializer.php deleted file mode 100644 index 705324015..000000000 --- a/framework/core/src/Api/Serializer/DiscussionSerializer.php +++ /dev/null @@ -1,49 +0,0 @@ - (int) $model->comment_count, - 'participantCount' => (int) $model->participant_count, - 'createdAt' => $this->formatDate($model->created_at), - 'lastPostedAt' => $this->formatDate($model->last_posted_at), - 'lastPostNumber' => (int) $model->last_post_number, - 'canReply' => $this->actor->can('reply', $model), - 'canRename' => $this->actor->can('rename', $model), - 'canDelete' => $this->actor->can('delete', $model), - 'canHide' => $this->actor->can('hide', $model) - ]; - - if ($model->hidden_at) { - $attributes['isHidden'] = true; - $attributes['hiddenAt'] = $this->formatDate($model->hidden_at); - } - - Discussion::setStateUser($this->actor); - - if ($state = $model->state) { - $attributes += [ - 'lastReadAt' => $this->formatDate($state->last_read_at), - 'lastReadPostNumber' => (int) $state->last_read_post_number - ]; - } - - return $attributes; - } -} diff --git a/framework/core/src/Api/Serializer/ExtensionReadmeSerializer.php b/framework/core/src/Api/Serializer/ExtensionReadmeSerializer.php deleted file mode 100644 index 4396cd045..000000000 --- a/framework/core/src/Api/Serializer/ExtensionReadmeSerializer.php +++ /dev/null @@ -1,35 +0,0 @@ - $model->getReadme() - ]; - } - - public function getId($extension) - { - return $extension->getId(); - } - - public function getType($extension) - { - return 'extension-readmes'; - } -} diff --git a/framework/core/src/Api/Serializer/ForumSerializer.php b/framework/core/src/Api/Serializer/ForumSerializer.php deleted file mode 100644 index 29f467e3a..000000000 --- a/framework/core/src/Api/Serializer/ForumSerializer.php +++ /dev/null @@ -1,133 +0,0 @@ -config = $config; - $this->assetsFilesystem = $filesystemFactory->disk('flarum-assets'); - $this->settings = $settings; - $this->url = $url; - } - - public function getId($model) - { - return '1'; - } - - /** - * @param array $model - */ - protected function getDefaultAttributes(object|array $model): array - { - $attributes = [ - 'title' => $this->settings->get('forum_title'), - 'description' => $this->settings->get('forum_description'), - 'showLanguageSelector' => (bool) $this->settings->get('show_language_selector', true), - 'baseUrl' => $url = $this->url->to('forum')->base(), - 'basePath' => $path = parse_url($url, PHP_URL_PATH) ?: '', - 'baseOrigin' => substr($url, 0, strlen($url) - strlen($path)), - 'debug' => $this->config->inDebugMode(), - 'apiUrl' => $this->url->to('api')->base(), - 'welcomeTitle' => $this->settings->get('welcome_title'), - 'welcomeMessage' => $this->settings->get('welcome_message'), - 'themePrimaryColor' => $this->settings->get('theme_primary_color'), - 'themeSecondaryColor' => $this->settings->get('theme_secondary_color'), - 'logoUrl' => $this->getLogoUrl(), - 'faviconUrl' => $this->getFaviconUrl(), - 'headerHtml' => $this->settings->get('custom_header'), - 'footerHtml' => $this->settings->get('custom_footer'), - 'allowSignUp' => (bool) $this->settings->get('allow_sign_up'), - 'defaultRoute' => $this->settings->get('default_route'), - 'canViewForum' => $this->actor->can('viewForum'), - 'canStartDiscussion' => $this->actor->can('startDiscussion'), - 'canSearchUsers' => $this->actor->can('searchUsers'), - 'canCreateAccessToken' => $this->actor->can('createAccessToken'), - 'canModerateAccessTokens' => $this->actor->can('moderateAccessTokens'), - 'canEditUserCredentials' => $this->actor->hasPermission('user.editCredentials'), - 'assetsBaseUrl' => rtrim($this->assetsFilesystem->url(''), '/'), - 'jsChunksBaseUrl' => $this->assetsFilesystem->url('js'), - ]; - - if ($this->actor->can('administrate')) { - $attributes['adminUrl'] = $this->url->to('admin')->base(); - $attributes['version'] = Application::VERSION; - } - - return $attributes; - } - - protected function groups(array $model): ?Relationship - { - return $this->hasMany($model, GroupSerializer::class); - } - - protected function getLogoUrl(): ?string - { - $logoPath = $this->settings->get('logo_path'); - - return $logoPath ? $this->getAssetUrl($logoPath) : null; - } - - protected function getFaviconUrl(): ?string - { - $faviconPath = $this->settings->get('favicon_path'); - - return $faviconPath ? $this->getAssetUrl($faviconPath) : null; - } - - public function getAssetUrl(string $assetPath): string - { - return $this->assetsFilesystem->url($assetPath); - } - - protected function actor(array $model): ?Relationship - { - return $this->hasOne($model, CurrentUserSerializer::class); - } -} diff --git a/framework/core/src/Api/Serializer/MailSettingsSerializer.php b/framework/core/src/Api/Serializer/MailSettingsSerializer.php deleted file mode 100644 index bff907e53..000000000 --- a/framework/core/src/Api/Serializer/MailSettingsSerializer.php +++ /dev/null @@ -1,40 +0,0 @@ - array_map([$this, 'serializeDriver'], $model['drivers']), - 'sending' => $model['sending'], - 'errors' => $model['errors'], - ]; - } - - private function serializeDriver(DriverInterface $driver): array - { - return $driver->availableSettings(); - } - - public function getId($model) - { - return 'global'; - } -} diff --git a/framework/core/src/Api/Serializer/NotificationSerializer.php b/framework/core/src/Api/Serializer/NotificationSerializer.php deleted file mode 100644 index 70bf2288e..000000000 --- a/framework/core/src/Api/Serializer/NotificationSerializer.php +++ /dev/null @@ -1,66 +0,0 @@ - $model->type, - 'content' => $model->data, - 'createdAt' => $this->formatDate($model->created_at), - 'isRead' => (bool) $model->read_at - ]; - } - - protected function user(Notification $notification): ?Relationship - { - return $this->hasOne($notification, BasicUserSerializer::class); - } - - protected function fromUser(Notification $notification): ?Relationship - { - return $this->hasOne($notification, BasicUserSerializer::class); - } - - protected function subject(Notification $notification): ?Relationship - { - return $this->hasOne($notification, function (Notification $notification) { - return static::$subjectSerializers[$notification->type]; - }); - } - - public static function setSubjectSerializer(string $type, string $serializer): void - { - static::$subjectSerializers[$type] = $serializer; - } -} diff --git a/framework/core/src/Api/Serializer/PostSerializer.php b/framework/core/src/Api/Serializer/PostSerializer.php deleted file mode 100644 index 7bf965c51..000000000 --- a/framework/core/src/Api/Serializer/PostSerializer.php +++ /dev/null @@ -1,77 +0,0 @@ -actor->can('edit', $model); - - if ($model instanceof CommentPost) { - if ($canEdit) { - $attributes['content'] = $model->content; - } - if ($this->actor->can('viewIps', $model)) { - $attributes['ipAddress'] = $model->ip_address; - } - } else { - $attributes['content'] = $model->content; - } - - if ($model->edited_at) { - $attributes['editedAt'] = $this->formatDate($model->edited_at); - } - - if ($model->hidden_at) { - $attributes['isHidden'] = true; - $attributes['hiddenAt'] = $this->formatDate($model->hidden_at); - } - - $attributes += [ - 'canEdit' => $canEdit, - 'canDelete' => $this->actor->can('delete', $model), - 'canHide' => $this->actor->can('hide', $model) - ]; - - return $attributes; - } - - protected function user(Post $post): ?Relationship - { - return $this->hasOne($post, UserSerializer::class); - } - - protected function discussion(Post $post): ?Relationship - { - return $this->hasOne($post, BasicDiscussionSerializer::class); - } - - protected function editedUser(Post $post): ?Relationship - { - return $this->hasOne($post, BasicUserSerializer::class); - } - - protected function hiddenUser(Post $post): ?Relationship - { - return $this->hasOne($post, BasicUserSerializer::class); - } -} diff --git a/framework/core/src/Api/Serializer/UserSerializer.php b/framework/core/src/Api/Serializer/UserSerializer.php deleted file mode 100644 index 28647f27e..000000000 --- a/framework/core/src/Api/Serializer/UserSerializer.php +++ /dev/null @@ -1,48 +0,0 @@ - $this->formatDate($model->joined_at), - 'discussionCount' => (int) $model->discussion_count, - 'commentCount' => (int) $model->comment_count, - 'canEdit' => $this->actor->can('edit', $model), - 'canEditCredentials' => $this->actor->can('editCredentials', $model), - 'canEditGroups' => $this->actor->can('editGroups', $model), - 'canDelete' => $this->actor->can('delete', $model), - ]; - - if ($model->getPreference('discloseOnline') || $this->actor->can('viewLastSeenAt', $model)) { - $attributes += [ - 'lastSeenAt' => $this->formatDate($model->last_seen_at) - ]; - } - - if ($attributes['canEditCredentials'] || $this->actor->id === $model->id) { - $attributes += [ - 'isEmailConfirmed' => (bool) $model->is_email_confirmed, - 'email' => $model->email - ]; - } - - return $attributes; - } -} diff --git a/framework/core/src/Api/routes.php b/framework/core/src/Api/routes.php index 0957f9963..edff27657 100644 --- a/framework/core/src/Api/routes.php +++ b/framework/core/src/Api/routes.php @@ -46,20 +46,6 @@ return function (RouteCollection $map, RouteHandlerFactory $route) { |-------------------------------------------------------------------------- */ - // Upload avatar - $map->post( - '/users/{id}/avatar', - 'users.avatar.upload', - $route->toController(Controller\UploadAvatarController::class) - ); - - // Remove avatar - $map->delete( - '/users/{id}/avatar', - 'users.avatar.delete', - $route->toController(Controller\DeleteAvatarController::class) - ); - // send confirmation email $map->post( '/users/{id}/send-confirmation', @@ -107,13 +93,6 @@ return function (RouteCollection $map, RouteHandlerFactory $route) { $route->toController(Controller\UninstallExtensionController::class) ); - // Get readme for an extension - $map->get( - '/extension-readmes/{name}', - 'extension-readmes.show', - $route->toController(Controller\ShowExtensionReadmeController::class) - ); - // Update settings $map->post( '/settings', diff --git a/framework/core/src/Extend/ApiResource.php b/framework/core/src/Extend/ApiResource.php index 0f40d3b80..55164c3c7 100644 --- a/framework/core/src/Extend/ApiResource.php +++ b/framework/core/src/Extend/ApiResource.php @@ -3,7 +3,7 @@ namespace Flarum\Extend; use Flarum\Api\Controller\AbstractSerializeController; -use Flarum\Api\Endpoint\Endpoint; +use Flarum\Api\Endpoint\EndpointInterface; use Flarum\Api\Resource\Contracts\Collection; use Flarum\Api\Resource\Contracts\Resource; use Flarum\Extension\Extension; @@ -64,7 +64,7 @@ class ApiResource implements ExtenderInterface /** * Modify an endpoint. * - * @param class-string<\Flarum\Api\Endpoint\Endpoint>|array<\Flarum\Api\Endpoint\Endpoint> $endpointClass the class name of the endpoint. + * @param class-string<\Flarum\Api\Endpoint\EndpointInterface>|array<\Flarum\Api\Endpoint\EndpointInterface> $endpointClass the class name of the endpoint. * or an array of class names of the endpoints. * @param callable|class-string $mutator a callable that accepts an endpoint and returns the modified endpoint. */ @@ -182,7 +182,7 @@ class ApiResource implements ExtenderInterface [$endpointsToRemove, $condition] = $removeEndpointClass; if ($this->isApplicable($condition, $resource, $container)) { - $endpoints = array_filter($endpoints, fn (Endpoint $endpoint) => ! in_array($endpoint::class, $endpointsToRemove)); + $endpoints = array_filter($endpoints, fn (EndpointInterface $endpoint) => ! in_array($endpoint::class, $endpointsToRemove)); } } @@ -194,8 +194,8 @@ class ApiResource implements ExtenderInterface $mutateEndpoint = ContainerUtil::wrapCallback($mutator, $container); $endpoint = $mutateEndpoint($endpoint, $resource); - if (! $endpoint instanceof Endpoint) { - throw new \RuntimeException('The endpoint mutator must return an instance of ' . Endpoint::class); + if (! $endpoint instanceof EndpointInterface) { + throw new \RuntimeException('The endpoint mutator must return an instance of ' . EndpointInterface::class); } } } diff --git a/framework/core/src/Foundation/ErrorHandling/JsonApiFormatter.php b/framework/core/src/Foundation/ErrorHandling/JsonApiFormatter.php index e6276c44a..456cd8e58 100644 --- a/framework/core/src/Foundation/ErrorHandling/JsonApiFormatter.php +++ b/framework/core/src/Foundation/ErrorHandling/JsonApiFormatter.php @@ -12,7 +12,6 @@ namespace Flarum\Foundation\ErrorHandling; use Flarum\Api\JsonApiResponse; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; -use Tobscure\JsonApi\Document; /** * A formatter to render exceptions as valid {JSON:API} error object. @@ -28,15 +27,13 @@ class JsonApiFormatter implements HttpFormatter public function format(HandledError $error, Request $request): Response { - $document = new Document; - if ($error->hasDetails()) { - $document->setErrors($this->withDetails($error)); + $errors = $this->withDetails($error); } else { - $document->setErrors($this->default($error)); + $errors = $this->default($error); } - return new JsonApiResponse($document, $error->getStatusCode()); + return new JsonApiResponse(compact('errors'), $error->getStatusCode()); } private function default(HandledError $error): array diff --git a/framework/core/src/Foundation/MaintenanceModeHandler.php b/framework/core/src/Foundation/MaintenanceModeHandler.php index a9a720476..e5cbcbbf5 100644 --- a/framework/core/src/Foundation/MaintenanceModeHandler.php +++ b/framework/core/src/Foundation/MaintenanceModeHandler.php @@ -15,7 +15,6 @@ 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 { @@ -46,10 +45,12 @@ class MaintenanceModeHandler implements RequestHandlerInterface private function apiResponse(): ResponseInterface { return new JsonResponse( - (new Document)->setErrors([ - 'status' => '503', - 'title' => self::MESSAGE - ]), + [ + 'errors' => [ + 'status' => '503', + 'title' => self::MESSAGE + ], + ], 503, ['Content-Type' => 'application/vnd.api+json'] ); diff --git a/framework/core/src/Http/RouteHandlerFactory.php b/framework/core/src/Http/RouteHandlerFactory.php index 2abe89096..7f8da0f10 100644 --- a/framework/core/src/Http/RouteHandlerFactory.php +++ b/framework/core/src/Http/RouteHandlerFactory.php @@ -41,11 +41,10 @@ class RouteHandlerFactory /** * @param class-string<\Tobyz\JsonApiServer\Resource\AbstractResource> $resourceClass - * @param class-string<\Flarum\Api\Endpoint\Endpoint> $endpointClass */ - public function toApiResource(string $resourceClass, string $endpointClass): Closure + public function toApiResource(string $resourceClass, string $endpointName): Closure { - return function (Request $request, array $routeParams) use ($resourceClass, $endpointClass) { + return function (Request $request, array $routeParams) use ($resourceClass, $endpointName) { /** @var JsonApi $api */ $api = $this->container->make(JsonApi::class); @@ -54,7 +53,7 @@ class RouteHandlerFactory $request = $request->withQueryParams(array_merge($request->getQueryParams(), $routeParams)); return $api->forResource($resourceClass) - ->forEndpoint($endpointClass) + ->forEndpoint($endpointName) ->handle($request); }; } diff --git a/framework/core/tests/integration/api/AbstractSerializeControllerTest.php b/framework/core/tests/integration/api/AbstractSerializeControllerTest.php deleted file mode 100644 index cd5be3d89..000000000 --- a/framework/core/tests/integration/api/AbstractSerializeControllerTest.php +++ /dev/null @@ -1,53 +0,0 @@ -extend( - (new Extend\Routes('api')) - ->get('/dummy-serialize', 'dummy-serialize', DummySerializeController::class) - ); - - $response = $this->send( - $this->request('GET', '/api/dummy-serialize') - ); - - $json = json_decode($contents = (string) $response->getBody(), true); - - $this->assertEquals(500, $response->getStatusCode(), $contents); - $this->assertStringStartsWith('InvalidArgumentException: Serializer required for controller: '.DummySerializeController::class, $json['errors'][0]['detail']); - } -} - -class DummySerializeController extends AbstractSerializeController -{ - public ?string $serializer = null; - - protected function data(ServerRequestInterface $request, Document $document): mixed - { - return []; - } - - protected function createElement(mixed $data, SerializerInterface $serializer): ElementInterface - { - return $data; - } -} diff --git a/framework/core/tests/integration/extenders/EventTest.php b/framework/core/tests/integration/extenders/EventTest.php index 39ed0c733..310f3f8ab 100644 --- a/framework/core/tests/integration/extenders/EventTest.php +++ b/framework/core/tests/integration/extenders/EventTest.php @@ -32,8 +32,8 @@ class EventTest extends TestCase $api = $this->app()->getContainer()->make(JsonApi::class); return $api->forResource(GroupResource::class) - ->forEndpoint(Create::class) - ->execute( + ->forEndpoint('create') + ->process( body: [ 'data' => [ 'attributes' => [ diff --git a/framework/core/tests/integration/extenders/MailTest.php b/framework/core/tests/integration/extenders/MailTest.php index 9e1d7d2a7..d22dad21d 100644 --- a/framework/core/tests/integration/extenders/MailTest.php +++ b/framework/core/tests/integration/extenders/MailTest.php @@ -34,7 +34,11 @@ class MailTest extends TestCase ]) ); - $fields = json_decode($response->getBody()->getContents(), true)['data']['attributes']['fields']; + $body = $response->getBody()->getContents(); + + $this->assertEquals(200, $response->getStatusCode(), $body); + + $fields = json_decode($body, true)['data']['attributes']['fields']; // The custom driver does not exist $this->assertArrayNotHasKey('custom', $fields); diff --git a/framework/core/tests/integration/policy/DiscussionPolicyTest.php b/framework/core/tests/integration/policy/DiscussionPolicyTest.php index d3092d8dc..99f9bee93 100644 --- a/framework/core/tests/integration/policy/DiscussionPolicyTest.php +++ b/framework/core/tests/integration/policy/DiscussionPolicyTest.php @@ -101,8 +101,8 @@ class DiscussionPolicyTest extends TestCase $api ->forResource(PostResource::class) - ->forEndpoint(Create::class) - ->execute( + ->forEndpoint('create') + ->process( body: [ 'data' => [ 'attributes' => [