mirror of
https://github.com/flarum/core.git
synced 2025-08-08 09:26:34 +02:00
refactor: JSON:API (#3971)
* refactor: json:api refactor iteration 1 * chore: delete dead code * fix: regressions * chore: move additions/changes to package * feat: AccessTokenResource * feat: allow dependency injection in resources * feat: `ApiResource` extender * feat: improve * feat: refactor tags extension * feat: refactor flags extension * fix: regressions * fix: drop bc layer * feat: refactor suspend extension * feat: refactor subscriptions extension * feat: refactor approval extension * feat: refactor sticky extension * feat: refactor nicknames extension * feat: refactor mentions extension * feat: refactor lock extension * feat: refactor likes extension * chore: merge conflicts * feat: refactor extension-manager extension * feat: context current endpoint helpers * chore: minor * feat: cleaner sortmap implementation * chore: drop old package * chore: not needed (auto scoping) * fix: actor only fields * refactor: simplify index endpoint * feat: eager loading * test: adapt * test: phpstan * test: adapt * fix: typing * fix: approving content * tet: adapt frontend tests * chore: typings * chore: review * fix: breaking change
This commit is contained in:
@@ -79,7 +79,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",
|
||||
@@ -92,6 +91,7 @@
|
||||
"symfony/translation": "^6.3",
|
||||
"symfony/translation-contracts": "^2.5",
|
||||
"symfony/yaml": "^6.3",
|
||||
"flarum/json-api-server": "^1.0.0",
|
||||
"wikimedia/less.php": "^4.1"
|
||||
},
|
||||
"require-dev": {
|
||||
|
@@ -207,11 +207,9 @@ export default class CreateUserModal<CustomAttrs extends ICreateUserModalAttrs =
|
||||
|
||||
this.loading = true;
|
||||
|
||||
app
|
||||
.request({
|
||||
url: app.forum.attribute('apiUrl') + '/users',
|
||||
method: 'POST',
|
||||
body: { data: { attributes: this.submitData() } },
|
||||
app.store
|
||||
.createRecord('users', {})
|
||||
.save(this.submitData(), {
|
||||
errorHandler: this.onerror.bind(this),
|
||||
})
|
||||
.then(() => {
|
||||
|
@@ -121,6 +121,10 @@ export default abstract class PaginatedListState<T extends Model, P extends Pagi
|
||||
include,
|
||||
};
|
||||
|
||||
if (typeof params.include === 'undefined') {
|
||||
delete params.include;
|
||||
}
|
||||
|
||||
return app.store.find<T[]>(this.type, params).then((results) => {
|
||||
/*
|
||||
* If this state does not rely on a preloaded API document to know the page size,
|
||||
|
@@ -7,7 +7,7 @@ test('gambits are converted to filters', function () {
|
||||
q: 'lorem',
|
||||
created: '2023-07-07',
|
||||
hidden: true,
|
||||
author: ['behz'],
|
||||
author: 'behz',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ test('gambits are negated when prefixed with a dash', function () {
|
||||
q: 'lorem',
|
||||
'-created': '2023-07-07',
|
||||
'-hidden': true,
|
||||
'-author': ['behz'],
|
||||
'-author': 'behz',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,6 +29,6 @@ test('gambits are only applied for the correct resource type', function () {
|
||||
q: 'lorem email:behz@machine.local',
|
||||
created: '2023-07-07..2023-10-18',
|
||||
hidden: true,
|
||||
'-author': ['behz'],
|
||||
'-author': 'behz',
|
||||
});
|
||||
});
|
||||
|
@@ -1,107 +1,139 @@
|
||||
validation:
|
||||
accepted: "The :attribute must be accepted."
|
||||
active_url: "The :attribute is not a valid URL."
|
||||
after: "The :attribute must be a date after :date."
|
||||
after_or_equal: "The :attribute must be a date after or equal to :date."
|
||||
alpha: "The :attribute must only contain letters."
|
||||
alpha_dash: "The :attribute must only contain letters, numbers, dashes and underscores."
|
||||
alpha_num: "The :attribute must only contain letters and numbers."
|
||||
array: "The :attribute must be an array."
|
||||
before: "The :attribute must be a date before :date."
|
||||
before_or_equal: "The :attribute must be a date before or equal to :date."
|
||||
accepted: "The :attribute field must be accepted."
|
||||
accepted_if: "The :attribute field must be accepted when :other is :value."
|
||||
active_url: "The :attribute field must be a valid URL."
|
||||
after: "The :attribute field must be a date after :date."
|
||||
after_or_equal: "The :attribute field must be a date after or equal to :date."
|
||||
alpha: "The :attribute field must only contain letters."
|
||||
alpha_dash: "The :attribute field must only contain letters, numbers, dashes, and underscores."
|
||||
alpha_num: "The :attribute field must only contain letters and numbers."
|
||||
array: "The :attribute field must be an array."
|
||||
ascii: "The :attribute field must only contain single-byte alphanumeric characters and symbols."
|
||||
before: "The :attribute field must be a date before :date."
|
||||
before_or_equal: "The :attribute field must be a date before or equal to :date."
|
||||
between:
|
||||
numeric: "The :attribute must be between :min and :max."
|
||||
file: "The :attribute must be between :min and :max kilobytes."
|
||||
string: "The :attribute must be between :min and :max characters."
|
||||
array: "The :attribute must have between :min and :max items."
|
||||
array: "The :attribute field must contain between :min and :max items."
|
||||
file: "The :attribute field must be between :min and :max kilobytes."
|
||||
numeric: "The :attribute field must be between :min and :max."
|
||||
string: "The :attribute field must be between :min and :max characters."
|
||||
boolean: "The :attribute field must be true or false."
|
||||
confirmed: "The :attribute confirmation does not match."
|
||||
date: "The :attribute is not a valid date."
|
||||
date_equals: "The :attribute must be a date equal to :date."
|
||||
date_format: "The :attribute does not match the format :format."
|
||||
different: "The :attribute and :other must be different."
|
||||
digits: "The :attribute must be :digits digits."
|
||||
digits_between: "The :attribute must be between :min and :max digits."
|
||||
dimensions: "The :attribute has invalid image dimensions."
|
||||
can: "The :attribute field contains an unauthorized value."
|
||||
confirmed: "The :attribute field confirmation does not match."
|
||||
current_password: "The password is incorrect."
|
||||
date: "The :attribute field must be a valid date."
|
||||
date_equals: "The :attribute field must be a date equal to :date."
|
||||
date_format: "The :attribute field must match the format :format."
|
||||
decimal: "The :attribute field must have :decimal decimal places."
|
||||
declined: "The :attribute field must be declined."
|
||||
declined_if: "The :attribute field must be declined when :other is :value."
|
||||
different: "The :attribute field and :other must be different."
|
||||
digits: "The :attribute field must be :digits digits."
|
||||
digits_between: "The :attribute field must be between :min and :max digits."
|
||||
dimensions: "The :attribute field has invalid image dimensions."
|
||||
distinct: "The :attribute field has a duplicate value."
|
||||
email: "The :attribute must be a valid email address."
|
||||
ends_with: "The :attribute must end with one of the following: :values."
|
||||
doesnt_end_with: "The :attribute field must not end with one of the following: :values."
|
||||
doesnt_start_with: "The :attribute field must not start with one of the following: :values."
|
||||
email: "The :attribute field must be a valid email address."
|
||||
ends_with: "The :attribute field must end with one of the following: :values."
|
||||
enum: "The selected :attribute is invalid."
|
||||
exists: "The selected :attribute is invalid."
|
||||
file: "The :attribute must be a file."
|
||||
file_too_large: "The :attribute is too large."
|
||||
file_upload_failed: "The :attribute failed to upload."
|
||||
extensions: "The :attribute field must have one of the following extensions: :values."
|
||||
file: "The :attribute field must be a file."
|
||||
filled: "The :attribute field must have a value."
|
||||
gt:
|
||||
numeric: "The :attribute must be greater than :value."
|
||||
file: "The :attribute must be greater than :value kilobytes."
|
||||
string: "The :attribute must be greater than :value characters."
|
||||
array: "The :attribute must have more than :value items."
|
||||
array: "The :attribute field must have more than :value items."
|
||||
file: "The :attribute field must be greater than :value kilobytes."
|
||||
numeric: "The :attribute field must be greater than :value."
|
||||
string: "The :attribute field must be greater than :value characters."
|
||||
gte:
|
||||
numeric: "The :attribute must be greater than or equal :value."
|
||||
file: "The :attribute must be greater than or equal :value kilobytes."
|
||||
string: "The :attribute must be greater than or equal :value characters."
|
||||
array: "The :attribute must have :value items or more."
|
||||
array: "The :attribute field must have :value items or more."
|
||||
file: "The :attribute field must be greater than or equal to :value kilobytes."
|
||||
numeric: "The :attribute field must be greater than or equal to :value."
|
||||
string: "The :attribute field must be greater than or equal to :value characters."
|
||||
hex_color: "The :attribute field must be a valid hexadecimal color."
|
||||
image: "The :attribute must be an image."
|
||||
image: "The :attribute field must be an image."
|
||||
in: "The selected :attribute is invalid."
|
||||
in_array: "The :attribute field does not exist in :other."
|
||||
integer: "The :attribute must be an integer."
|
||||
ip: "The :attribute must be a valid IP address."
|
||||
ipv4: "The :attribute must be a valid IPv4 address."
|
||||
ipv6: "The :attribute must be a valid IPv6 address."
|
||||
json: "The :attribute must be a valid JSON string."
|
||||
in_array: "The :attribute field must exist in :other."
|
||||
integer: "The :attribute field must be an integer."
|
||||
ip: "The :attribute field must be a valid IP address."
|
||||
ipv4: "The :attribute field must be a valid IPv4 address."
|
||||
ipv6: "The :attribute field must be a valid IPv6 address."
|
||||
json: "The :attribute field must be a valid JSON string."
|
||||
lowercase: "The :attribute field must be lowercase."
|
||||
lt:
|
||||
numeric: "The :attribute must be less than :value."
|
||||
file: "The :attribute must be less than :value kilobytes."
|
||||
string: "The :attribute must be less than :value characters."
|
||||
array: "The :attribute must have less than :value items."
|
||||
array: "The :attribute field must have less than :value items."
|
||||
file: "The :attribute field must be less than :value kilobytes."
|
||||
numeric: "The :attribute field must be less than :value."
|
||||
string: "The :attribute field must be less than :value characters."
|
||||
lte:
|
||||
numeric: "The :attribute must be less than or equal :value."
|
||||
file: "The :attribute must be less than or equal :value kilobytes."
|
||||
string: "The :attribute must be less than or equal :value characters."
|
||||
array: "The :attribute must not have more than :value items."
|
||||
array: "The :attribute field must not have more than :value items."
|
||||
file: "The :attribute field must be less than or equal to :value kilobytes."
|
||||
numeric: "The :attribute field must be less than or equal to :value."
|
||||
string: "The :attribute field must be less than or equal to :value characters."
|
||||
mac_address: "The :attribute field must be a valid MAC address."
|
||||
max:
|
||||
numeric: "The :attribute must not be greater than :max."
|
||||
file: "The :attribute must not be greater than :max kilobytes."
|
||||
string: "The :attribute must not be greater than :max characters."
|
||||
array: "The :attribute must not have more than :max items."
|
||||
mimes: "The :attribute must be a file of type: :values."
|
||||
mimetypes: "The :attribute must be a file of type: :values."
|
||||
array: "The :attribute field must not have more than :max items."
|
||||
file: "The :attribute field must not be greater than :max kilobytes."
|
||||
numeric: "The :attribute field must not be greater than :max."
|
||||
string: "The :attribute field must not be greater than :max characters."
|
||||
max_digits: "The :attribute field must not have more than :max digits."
|
||||
mimes: "The :attribute field must be a file of type: :values."
|
||||
mimetypes: "The :attribute field must be a file of type: :values."
|
||||
min:
|
||||
numeric: "The :attribute must be at least :min."
|
||||
file: "The :attribute must be at least :min kilobytes."
|
||||
string: "The :attribute must be at least :min characters."
|
||||
array: "The :attribute must have at least :min items."
|
||||
multiple_of: "The :attribute must be a multiple of :value."
|
||||
array: "The :attribute field must have at least :min items."
|
||||
file: "The :attribute field must be at least :min kilobytes."
|
||||
numeric: "The :attribute field must be at least :min."
|
||||
string: "The :attribute field must be at least :min characters."
|
||||
min_digits: "The :attribute field must have at least :min digits."
|
||||
missing: "The :attribute field must be missing."
|
||||
missing_if: "The :attribute field must be missing when :other is :value."
|
||||
missing_unless: "The :attribute field must be missing unless :other is :value."
|
||||
missing_with: "The :attribute field must be missing when :values is present."
|
||||
missing_with_all: "The :attribute field must be missing when :values are present."
|
||||
multiple_of: "The :attribute field must be a multiple of :value."
|
||||
not_in: "The selected :attribute is invalid."
|
||||
not_regex: "The :attribute format is invalid."
|
||||
numeric: "The :attribute must be a number."
|
||||
password: "The password is incorrect."
|
||||
not_regex: "The :attribute field format is invalid."
|
||||
numeric: "The :attribute field must be a number."
|
||||
password:
|
||||
letters: "The :attribute field must contain at least one letter."
|
||||
mixed: "The :attribute field must contain at least one uppercase and one lowercase letter."
|
||||
numbers: "The :attribute field must contain at least one number."
|
||||
symbols: "The :attribute field must contain at least one symbol."
|
||||
uncompromised: "The given :attribute has appeared in a data leak. Please choose a different :attribute."
|
||||
present: "The :attribute field must be present."
|
||||
regex: "The :attribute format is invalid."
|
||||
present_if: "The :attribute field must be present when :other is :value."
|
||||
present_unless: "The :attribute field must be present unless :other is :value."
|
||||
present_with: "The :attribute field must be present when :values is present."
|
||||
present_with_all: "The :attribute field must be present when :values are present."
|
||||
prohibited: "The :attribute field is prohibited."
|
||||
prohibited_if: "The :attribute field is prohibited when :other is :value."
|
||||
prohibited_unless: "The :attribute field is prohibited unless :other is in :values."
|
||||
prohibits: "The :attribute field prohibits :other from being present."
|
||||
regex: "The :attribute field format is invalid."
|
||||
required: "The :attribute field is required."
|
||||
required_array_keys: "The :attribute field must contain entries for: :values."
|
||||
required_if: "The :attribute field is required when :other is :value."
|
||||
required_if_accepted: "The :attribute field is required when :other is accepted."
|
||||
required_unless: "The :attribute field is required unless :other is in :values."
|
||||
required_with: "The :attribute field is required when :values is present."
|
||||
required_with_all: "The :attribute field is required when :values are present."
|
||||
required_without: "The :attribute field is required when :values is not present."
|
||||
required_without_all: "The :attribute field is required when none of :values are present."
|
||||
prohibited: "The :attribute field is prohibited."
|
||||
prohibited_if: "The :attribute field is prohibited when :other is :value."
|
||||
prohibited_unless: "The :attribute field is prohibited unless :other is in :values."
|
||||
same: "The :attribute and :other must match."
|
||||
same: "The :attribute field must match :other."
|
||||
size:
|
||||
numeric: "The :attribute must be :size."
|
||||
file: "The :attribute must be :size kilobytes."
|
||||
string: "The :attribute must be :size characters."
|
||||
array: "The :attribute must contain :size items."
|
||||
starts_with: "The :attribute must start with one of the following: :values."
|
||||
string: "The :attribute must be a string."
|
||||
timezone: "The :attribute must be a valid zone."
|
||||
array: "The :attribute field must contain :size items."
|
||||
file: "The :attribute field must be :size kilobytes."
|
||||
numeric: "The :attribute field must be :size."
|
||||
string: "The :attribute field must be :size characters."
|
||||
starts_with: "The :attribute field must start with one of the following: :values."
|
||||
string: "The :attribute field must be a string."
|
||||
timezone: "The :attribute field must be a valid timezone."
|
||||
unique: "The :attribute has already been taken."
|
||||
uploaded: "The :attribute failed to upload."
|
||||
url: "The :attribute format is invalid."
|
||||
uuid: "The :attribute must be a valid UUID."
|
||||
uppercase: "The :attribute field must be uppercase."
|
||||
url: "The :attribute field must be a valid URL."
|
||||
ulid: "The :attribute field must be a valid ULID."
|
||||
uuid: "The :attribute field must be a valid UUID."
|
||||
|
||||
attributes:
|
||||
username: username
|
||||
|
@@ -9,10 +9,7 @@
|
||||
|
||||
namespace Flarum\Api;
|
||||
|
||||
use Flarum\Api\Controller\AbstractSerializeController;
|
||||
use Flarum\Api\Serializer\AbstractSerializer;
|
||||
use Flarum\Api\Serializer\BasicDiscussionSerializer;
|
||||
use Flarum\Api\Serializer\NotificationSerializer;
|
||||
use Flarum\Api\Endpoint\EndpointInterface;
|
||||
use Flarum\Foundation\AbstractServiceProvider;
|
||||
use Flarum\Foundation\ErrorHandling\JsonApiFormatter;
|
||||
use Flarum\Foundation\ErrorHandling\Registry;
|
||||
@@ -24,6 +21,8 @@ use Flarum\Http\RouteHandlerFactory;
|
||||
use Flarum\Http\UrlGenerator;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Laminas\Stratigility\MiddlewarePipe;
|
||||
use ReflectionClass;
|
||||
use Tobyz\JsonApiServer\Endpoint\Endpoint;
|
||||
|
||||
class ApiServiceProvider extends AbstractServiceProvider
|
||||
{
|
||||
@@ -33,9 +32,40 @@ class ApiServiceProvider extends AbstractServiceProvider
|
||||
return $url->addCollection('api', $container->make('flarum.api.routes'), 'api');
|
||||
});
|
||||
|
||||
$this->container->singleton('flarum.api.routes', function () {
|
||||
$this->container->singleton('flarum.api.resources', function () {
|
||||
return [
|
||||
Resource\ForumResource::class,
|
||||
Resource\UserResource::class,
|
||||
Resource\GroupResource::class,
|
||||
Resource\PostResource::class,
|
||||
Resource\DiscussionResource::class,
|
||||
Resource\NotificationResource::class,
|
||||
Resource\AccessTokenResource::class,
|
||||
Resource\MailSettingResource::class,
|
||||
Resource\ExtensionReadmeResource::class,
|
||||
];
|
||||
});
|
||||
|
||||
$this->container->singleton('flarum.api.resource_handler', function (Container $container) {
|
||||
$resources = $this->container->make('flarum.api.resources');
|
||||
|
||||
$api = new JsonApi('/');
|
||||
$api->container($container);
|
||||
|
||||
foreach ($resources as $resourceClass) {
|
||||
/** @var \Flarum\Api\Resource\AbstractResource|\Flarum\Api\Resource\AbstractDatabaseResource $resource */
|
||||
$resource = $container->make($resourceClass);
|
||||
$api->resource($resource->boot($api));
|
||||
}
|
||||
|
||||
return $api;
|
||||
});
|
||||
|
||||
$this->container->alias('flarum.api.resource_handler', JsonApi::class);
|
||||
|
||||
$this->container->singleton('flarum.api.routes', function (Container $container) {
|
||||
$routes = new RouteCollection;
|
||||
$this->populateRoutes($routes);
|
||||
$this->populateRoutes($routes, $container);
|
||||
|
||||
return $routes;
|
||||
});
|
||||
@@ -68,7 +98,8 @@ class ApiServiceProvider extends AbstractServiceProvider
|
||||
'flarum.api.route_resolver',
|
||||
'flarum.api.check_for_maintenance',
|
||||
HttpMiddleware\CheckCsrfToken::class,
|
||||
Middleware\ThrottleApi::class
|
||||
Middleware\ThrottleApi::class,
|
||||
HttpMiddleware\PopulateWithActor::class,
|
||||
];
|
||||
});
|
||||
|
||||
@@ -107,12 +138,6 @@ class ApiServiceProvider extends AbstractServiceProvider
|
||||
return $pipe;
|
||||
});
|
||||
|
||||
$this->container->singleton('flarum.api.notification_serializers', function () {
|
||||
return [
|
||||
'discussionRenamed' => BasicDiscussionSerializer::class
|
||||
];
|
||||
});
|
||||
|
||||
$this->container->singleton('flarum.api_client.exclude_middleware', function () {
|
||||
return [
|
||||
HttpMiddleware\InjectActorReference::class,
|
||||
@@ -144,27 +169,47 @@ class ApiServiceProvider extends AbstractServiceProvider
|
||||
|
||||
public function boot(Container $container): void
|
||||
{
|
||||
$this->setNotificationSerializers();
|
||||
|
||||
AbstractSerializeController::setContainer($container);
|
||||
|
||||
AbstractSerializer::setContainer($container);
|
||||
//
|
||||
}
|
||||
|
||||
protected function setNotificationSerializers(): void
|
||||
protected function populateRoutes(RouteCollection $routes, Container $container): void
|
||||
{
|
||||
$serializers = $this->container->make('flarum.api.notification_serializers');
|
||||
|
||||
foreach ($serializers as $type => $serializer) {
|
||||
NotificationSerializer::setSubjectSerializer($type, $serializer);
|
||||
}
|
||||
}
|
||||
|
||||
protected function populateRoutes(RouteCollection $routes): void
|
||||
{
|
||||
$factory = $this->container->make(RouteHandlerFactory::class);
|
||||
/** @var RouteHandlerFactory $factory */
|
||||
$factory = $container->make(RouteHandlerFactory::class);
|
||||
|
||||
$callback = include __DIR__.'/routes.php';
|
||||
$callback($routes, $factory);
|
||||
|
||||
$resources = $this->container->make('flarum.api.resources');
|
||||
|
||||
foreach ($resources as $resourceClass) {
|
||||
/**
|
||||
* This is an empty shell instance,
|
||||
* we only need it to get the endpoint routes and types.
|
||||
*
|
||||
* We avoid dependency injection here to avoid early resolution.
|
||||
*
|
||||
* @var \Flarum\Api\Resource\AbstractResource|\Flarum\Api\Resource\AbstractDatabaseResource $resource
|
||||
*/
|
||||
$resource = (new ReflectionClass($resourceClass))->newInstanceWithoutConstructor();
|
||||
|
||||
$type = $resource->type();
|
||||
|
||||
/**
|
||||
* None of the injected dependencies should be directly used within
|
||||
* the `endpoints` method. Encourage using callbacks.
|
||||
*
|
||||
* @var array<Endpoint&EndpointInterface> $endpoints
|
||||
*/
|
||||
$endpoints = $resource->resolveEndpoints(true);
|
||||
|
||||
foreach ($endpoints as $endpoint) {
|
||||
$method = $endpoint->method;
|
||||
$path = rtrim("/$type$endpoint->path", '/');
|
||||
$name = "$type.$endpoint->name";
|
||||
|
||||
$routes->addRoute($method, $path, $name, $factory->toApiResource($resource::class, $endpoint->name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
99
framework/core/src/Api/Context.php
Normal file
99
framework/core/src/Api/Context.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?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\Api;
|
||||
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Search\SearchResults;
|
||||
use Flarum\User\User;
|
||||
use Tobyz\JsonApiServer\Context as BaseContext;
|
||||
|
||||
class Context extends BaseContext
|
||||
{
|
||||
protected ?SearchResults $search = null;
|
||||
|
||||
/**
|
||||
* Data passed internally when reusing resource endpoint logic.
|
||||
*/
|
||||
protected array $internal = [];
|
||||
|
||||
/**
|
||||
* Parameters mutated on the current instance.
|
||||
* Useful for passing information between different field callbacks.
|
||||
*/
|
||||
protected array $parameters = [];
|
||||
|
||||
public function withSearchResults(SearchResults $search): static
|
||||
{
|
||||
$new = clone $this;
|
||||
$new->search = $search;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function withInternal(string $key, mixed $value): static
|
||||
{
|
||||
$new = clone $this;
|
||||
$new->internal[$key] = $value;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function getSearchResults(): ?SearchResults
|
||||
{
|
||||
return $this->search;
|
||||
}
|
||||
|
||||
public function internal(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->internal[$key] ?? $default;
|
||||
}
|
||||
|
||||
public function getActor(): User
|
||||
{
|
||||
return RequestUtil::getActor($this->request);
|
||||
}
|
||||
|
||||
public function setParam(string $key, mixed $default = null): static
|
||||
{
|
||||
$this->parameters[$key] = $default;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getParam(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->parameters[$key] ?? $default;
|
||||
}
|
||||
|
||||
public function creating(string|null $resource = null): bool
|
||||
{
|
||||
return $this->endpoint instanceof Endpoint\Create && (! $resource || is_a($this->collection, $resource));
|
||||
}
|
||||
|
||||
public function updating(string|null $resource = null): bool
|
||||
{
|
||||
return $this->endpoint instanceof Endpoint\Update && (! $resource || is_a($this->collection, $resource));
|
||||
}
|
||||
|
||||
public function deleting(string|null $resource = null): bool
|
||||
{
|
||||
return $this->endpoint instanceof Endpoint\Delete && (! $resource || is_a($this->collection, $resource));
|
||||
}
|
||||
|
||||
public function showing(string|null $resource = null): bool
|
||||
{
|
||||
return $this->endpoint instanceof Endpoint\Show && (! $resource || is_a($this->collection, $resource));
|
||||
}
|
||||
|
||||
public function listing(string|null $resource = null): bool
|
||||
{
|
||||
return $this->endpoint instanceof Endpoint\Index && (! $resource || is_a($this->collection, $resource));
|
||||
}
|
||||
}
|
@@ -1,21 +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\Api\Controller;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
abstract class AbstractCreateController extends AbstractShowController
|
||||
{
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
return parent::handle($request)->withStatus(201);
|
||||
}
|
||||
}
|
@@ -1,46 +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\Api\Controller;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Collection;
|
||||
use Tobscure\JsonApi\Document;
|
||||
use Tobscure\JsonApi\ElementInterface;
|
||||
use Tobscure\JsonApi\SerializerInterface;
|
||||
|
||||
abstract class AbstractListController extends AbstractSerializeController
|
||||
{
|
||||
protected function createElement(mixed $data, SerializerInterface $serializer): ElementInterface
|
||||
{
|
||||
return new Collection($data, $serializer);
|
||||
}
|
||||
|
||||
abstract protected function data(ServerRequestInterface $request, Document $document): iterable;
|
||||
|
||||
protected function addPaginationData(Document $document, ServerRequestInterface $request, string $url, ?int $total): void
|
||||
{
|
||||
$limit = $this->extractLimit($request);
|
||||
$offset = $this->extractOffset($request);
|
||||
|
||||
$document->addPaginationLinks(
|
||||
$url,
|
||||
$request->getQueryParams(),
|
||||
$offset,
|
||||
$limit,
|
||||
$total,
|
||||
);
|
||||
|
||||
$document->setMeta([
|
||||
'total' => $total,
|
||||
'perPage' => $limit,
|
||||
'page' => $offset / $limit + 1,
|
||||
]);
|
||||
}
|
||||
}
|
@@ -1,432 +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\Api\Controller;
|
||||
|
||||
use Flarum\Api\JsonApiResponse;
|
||||
use Flarum\Api\Serializer\AbstractSerializer;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use InvalidArgumentException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
use Tobscure\JsonApi\ElementInterface;
|
||||
use Tobscure\JsonApi\Parameters;
|
||||
use Tobscure\JsonApi\SerializerInterface;
|
||||
|
||||
abstract class AbstractSerializeController implements RequestHandlerInterface
|
||||
{
|
||||
/**
|
||||
* The name of the serializer class to output results with.
|
||||
*
|
||||
* @var class-string<AbstractSerializer>|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<string, string>|null
|
||||
*/
|
||||
public ?array $sort = null;
|
||||
|
||||
protected static Container $container;
|
||||
|
||||
/**
|
||||
* @var array<class-string<self>, callable[]>
|
||||
*/
|
||||
protected static array $beforeDataCallbacks = [];
|
||||
|
||||
/**
|
||||
* @var array<class-string<self>, callable[]>
|
||||
*/
|
||||
protected static array $beforeSerializationCallbacks = [];
|
||||
|
||||
/**
|
||||
* @var string[][]
|
||||
*/
|
||||
protected static array $loadRelations = [];
|
||||
|
||||
/**
|
||||
* @var array<string, callable>
|
||||
*/
|
||||
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<string, callable>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
@@ -1,21 +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\Api\Controller;
|
||||
|
||||
use Tobscure\JsonApi\Resource;
|
||||
use Tobscure\JsonApi\SerializerInterface;
|
||||
|
||||
abstract class AbstractShowController extends AbstractSerializeController
|
||||
{
|
||||
protected function createElement(mixed $data, SerializerInterface $serializer): \Tobscure\JsonApi\ElementInterface
|
||||
{
|
||||
return new Resource($data, $serializer);
|
||||
}
|
||||
}
|
@@ -1,60 +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\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\AccessTokenSerializer;
|
||||
use Flarum\Http\DeveloperAccessToken;
|
||||
use Flarum\Http\Event\DeveloperTokenCreated;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Contracts\Validation\Factory;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
/**
|
||||
* Not to be confused with the CreateTokenController,
|
||||
* this controller is used by the actor to manually create a developer type access token.
|
||||
*/
|
||||
class CreateAccessTokenController extends AbstractCreateController
|
||||
{
|
||||
public ?string $serializer = AccessTokenSerializer::class;
|
||||
|
||||
public function __construct(
|
||||
protected Dispatcher $events,
|
||||
protected Factory $validation
|
||||
) {
|
||||
}
|
||||
|
||||
public function data(ServerRequestInterface $request, Document $document): DeveloperAccessToken
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
$actor->assertRegistered();
|
||||
$actor->assertCan('createAccessToken');
|
||||
|
||||
$title = Arr::get($request->getParsedBody(), 'data.attributes.title');
|
||||
|
||||
$this->validation->make(compact('title'), [
|
||||
'title' => 'required|string|max:255',
|
||||
])->validate();
|
||||
|
||||
$token = DeveloperAccessToken::generate($actor->id);
|
||||
|
||||
$token->title = $title;
|
||||
$token->last_activity_at = null;
|
||||
|
||||
$token->save();
|
||||
|
||||
$this->events->dispatch(new DeveloperTokenCreated($token));
|
||||
|
||||
return $token;
|
||||
}
|
||||
}
|
@@ -1,62 +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\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\DiscussionSerializer;
|
||||
use Flarum\Discussion\Command\ReadDiscussion;
|
||||
use Flarum\Discussion\Command\StartDiscussion;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class CreateDiscussionController extends AbstractCreateController
|
||||
{
|
||||
public ?string $serializer = DiscussionSerializer::class;
|
||||
|
||||
public array $include = [
|
||||
'posts',
|
||||
'user',
|
||||
'lastPostedUser',
|
||||
'firstPost',
|
||||
'lastPost'
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected Dispatcher $bus
|
||||
) {
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): Discussion
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
$ipAddress = $request->getAttribute('ipAddress');
|
||||
|
||||
$discussion = $this->bus->dispatch(
|
||||
new StartDiscussion($actor, Arr::get($request->getParsedBody(), 'data', []), $ipAddress)
|
||||
);
|
||||
|
||||
// After creating the discussion, we assume that the user has seen all
|
||||
// the posts in the discussion; thus, we will mark the discussion
|
||||
// as read if they are logged in.
|
||||
if ($actor->exists) {
|
||||
$this->bus->dispatch(
|
||||
new ReadDiscussion($discussion->id, $actor, 1)
|
||||
);
|
||||
}
|
||||
|
||||
$this->loadRelations(new Collection([$discussion]), $this->extractInclude($request), $request);
|
||||
|
||||
return $discussion;
|
||||
}
|
||||
}
|
@@ -1,36 +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\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\GroupSerializer;
|
||||
use Flarum\Group\Command\CreateGroup;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class CreateGroupController extends AbstractCreateController
|
||||
{
|
||||
public ?string $serializer = GroupSerializer::class;
|
||||
|
||||
public function __construct(
|
||||
protected Dispatcher $bus
|
||||
) {
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): Group
|
||||
{
|
||||
return $this->bus->dispatch(
|
||||
new CreateGroup(RequestUtil::getActor($request), Arr::get($request->getParsedBody(), 'data', []))
|
||||
);
|
||||
}
|
||||
}
|
@@ -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\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\PostSerializer;
|
||||
use Flarum\Discussion\Command\ReadDiscussion;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Post\Command\PostReply;
|
||||
use Flarum\Post\CommentPost;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class CreatePostController extends AbstractCreateController
|
||||
{
|
||||
public ?string $serializer = PostSerializer::class;
|
||||
|
||||
public array $include = [
|
||||
'user',
|
||||
'discussion',
|
||||
'discussion.posts',
|
||||
'discussion.lastPostedUser'
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected Dispatcher $bus
|
||||
) {
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): CommentPost
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
$data = Arr::get($request->getParsedBody(), 'data', []);
|
||||
$discussionId = (int) Arr::get($data, 'relationships.discussion.data.id');
|
||||
$ipAddress = $request->getAttribute('ipAddress');
|
||||
|
||||
/** @var CommentPost $post */
|
||||
$post = $this->bus->dispatch(
|
||||
new PostReply($discussionId, $actor, $data, $ipAddress)
|
||||
);
|
||||
|
||||
// After replying, we assume that the user has seen all of the posts
|
||||
// in the discussion; thus, we will mark the discussion as read if
|
||||
// they are logged in.
|
||||
if ($actor->exists) {
|
||||
$this->bus->dispatch(
|
||||
new ReadDiscussion($discussionId, $actor, $post->number)
|
||||
);
|
||||
}
|
||||
|
||||
$discussion = $post->discussion;
|
||||
$discussion->setRelation('posts', $discussion->posts()->whereVisibleTo($actor)->orderBy('created_at')->pluck('id'));
|
||||
|
||||
$this->loadRelations($post->newCollection([$post]), $this->extractInclude($request), $request);
|
||||
|
||||
return $post;
|
||||
}
|
||||
}
|
@@ -1,36 +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\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\CurrentUserSerializer;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\User\Command\RegisterUser;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class CreateUserController extends AbstractCreateController
|
||||
{
|
||||
public ?string $serializer = CurrentUserSerializer::class;
|
||||
|
||||
public function __construct(
|
||||
protected Dispatcher $bus
|
||||
) {
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): User
|
||||
{
|
||||
return $this->bus->dispatch(
|
||||
new RegisterUser(RequestUtil::getActor($request), Arr::get($request->getParsedBody(), 'data', []))
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,46 +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\Api\Controller;
|
||||
|
||||
use Flarum\Http\AccessToken;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\User\Exception\PermissionDeniedException;
|
||||
use Illuminate\Contracts\Session\Session;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class DeleteAccessTokenController extends AbstractDeleteController
|
||||
{
|
||||
protected function delete(ServerRequestInterface $request): void
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
$id = Arr::get($request->getQueryParams(), 'id');
|
||||
|
||||
$actor->assertRegistered();
|
||||
|
||||
$token = AccessToken::query()->findOrFail($id);
|
||||
|
||||
/** @var Session|null $session */
|
||||
$session = $request->getAttribute('session');
|
||||
|
||||
// Current session should only be terminated through logout.
|
||||
if ($session && $token->token === $session->get('access_token')) {
|
||||
throw new PermissionDeniedException();
|
||||
}
|
||||
|
||||
// Don't give away the existence of the token.
|
||||
if ($actor->cannot('revoke', $token)) {
|
||||
throw new ModelNotFoundException();
|
||||
}
|
||||
|
||||
$token->delete();
|
||||
}
|
||||
}
|
@@ -1,35 +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\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\UserSerializer;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\User\Command\DeleteAvatar;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class DeleteAvatarController extends AbstractShowController
|
||||
{
|
||||
public ?string $serializer = UserSerializer::class;
|
||||
|
||||
public function __construct(
|
||||
protected Dispatcher $bus
|
||||
) {
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): mixed
|
||||
{
|
||||
return $this->bus->dispatch(
|
||||
new DeleteAvatar(Arr::get($request->getQueryParams(), 'id'), RequestUtil::getActor($request))
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,35 +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\Api\Controller;
|
||||
|
||||
use Flarum\Discussion\Command\DeleteDiscussion;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class DeleteDiscussionController extends AbstractDeleteController
|
||||
{
|
||||
public function __construct(
|
||||
protected Dispatcher $bus
|
||||
) {
|
||||
}
|
||||
|
||||
protected function delete(ServerRequestInterface $request): void
|
||||
{
|
||||
$id = Arr::get($request->getQueryParams(), 'id');
|
||||
$actor = RequestUtil::getActor($request);
|
||||
$input = $request->getParsedBody();
|
||||
|
||||
$this->bus->dispatch(
|
||||
new DeleteDiscussion($id, $actor, $input)
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,31 +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\Api\Controller;
|
||||
|
||||
use Flarum\Group\Command\DeleteGroup;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class DeleteGroupController extends AbstractDeleteController
|
||||
{
|
||||
public function __construct(
|
||||
protected Dispatcher $bus
|
||||
) {
|
||||
}
|
||||
|
||||
protected function delete(ServerRequestInterface $request): void
|
||||
{
|
||||
$this->bus->dispatch(
|
||||
new DeleteGroup(Arr::get($request->getQueryParams(), 'id'), RequestUtil::getActor($request))
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,31 +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\Api\Controller;
|
||||
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Post\Command\DeletePost;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class DeletePostController extends AbstractDeleteController
|
||||
{
|
||||
public function __construct(
|
||||
protected Dispatcher $bus
|
||||
) {
|
||||
}
|
||||
|
||||
protected function delete(ServerRequestInterface $request): void
|
||||
{
|
||||
$this->bus->dispatch(
|
||||
new DeletePost(Arr::get($request->getQueryParams(), 'id'), RequestUtil::getActor($request))
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,31 +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\Api\Controller;
|
||||
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\User\Command\DeleteUser;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class DeleteUserController extends AbstractDeleteController
|
||||
{
|
||||
public function __construct(
|
||||
protected Dispatcher $bus
|
||||
) {
|
||||
}
|
||||
|
||||
protected function delete(ServerRequestInterface $request): void
|
||||
{
|
||||
$this->bus->dispatch(
|
||||
new DeleteUser(Arr::get($request->getQueryParams(), 'id'), RequestUtil::getActor($request))
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,53 +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\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\AccessTokenSerializer;
|
||||
use Flarum\Http\AccessToken;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Http\UrlGenerator;
|
||||
use Flarum\Search\SearchCriteria;
|
||||
use Flarum\Search\SearchManager;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class ListAccessTokensController extends AbstractListController
|
||||
{
|
||||
public ?string $serializer = AccessTokenSerializer::class;
|
||||
|
||||
public function __construct(
|
||||
protected UrlGenerator $url,
|
||||
protected SearchManager $search
|
||||
) {
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): iterable
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
$actor->assertRegistered();
|
||||
|
||||
$offset = $this->extractOffset($request);
|
||||
$limit = $this->extractLimit($request);
|
||||
$filter = $this->extractFilter($request);
|
||||
|
||||
$tokens = $this->search->query(AccessToken::class, new SearchCriteria($actor, $filter, $limit, $offset));
|
||||
|
||||
$document->addPaginationLinks(
|
||||
$this->url->to('api')->route('access-tokens.index'),
|
||||
$request->getQueryParams(),
|
||||
$offset,
|
||||
$limit,
|
||||
$tokens->areMoreResults() ? null : 0
|
||||
);
|
||||
|
||||
return $tokens->getResults();
|
||||
}
|
||||
}
|
@@ -1,99 +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\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\DiscussionSerializer;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Http\UrlGenerator;
|
||||
use Flarum\Search\SearchCriteria;
|
||||
use Flarum\Search\SearchManager;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class ListDiscussionsController extends AbstractListController
|
||||
{
|
||||
public ?string $serializer = DiscussionSerializer::class;
|
||||
|
||||
public array $include = [
|
||||
'user',
|
||||
'lastPostedUser',
|
||||
'mostRelevantPost',
|
||||
'mostRelevantPost.user'
|
||||
];
|
||||
|
||||
public array $optionalInclude = [
|
||||
'firstPost',
|
||||
'lastPost'
|
||||
];
|
||||
|
||||
public ?array $sort = ['lastPostedAt' => 'desc'];
|
||||
|
||||
public array $sortFields = ['lastPostedAt', 'commentCount', 'createdAt'];
|
||||
|
||||
public function __construct(
|
||||
protected SearchManager $search,
|
||||
protected UrlGenerator $url
|
||||
) {
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): iterable
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
$filters = $this->extractFilter($request);
|
||||
$sort = $this->extractSort($request);
|
||||
$sortIsDefault = $this->sortIsDefault($request);
|
||||
|
||||
$limit = $this->extractLimit($request);
|
||||
$offset = $this->extractOffset($request);
|
||||
$include = array_merge($this->extractInclude($request), ['state']);
|
||||
|
||||
$results = $this->search->query(
|
||||
Discussion::class,
|
||||
new SearchCriteria($actor, $filters, $limit, $offset, $sort, $sortIsDefault)
|
||||
);
|
||||
|
||||
$this->addPaginationData(
|
||||
$document,
|
||||
$request,
|
||||
$this->url->to('api')->route('discussions.index'),
|
||||
$results->areMoreResults() ? null : 0
|
||||
);
|
||||
|
||||
Discussion::setStateUser($actor);
|
||||
|
||||
// Eager load groups for use in the policies (isAdmin check)
|
||||
if (in_array('mostRelevantPost.user', $include)) {
|
||||
$include[] = 'mostRelevantPost.user.groups';
|
||||
|
||||
// If the first level of the relationship wasn't explicitly included,
|
||||
// add it so the code below can look for it
|
||||
if (! in_array('mostRelevantPost', $include)) {
|
||||
$include[] = 'mostRelevantPost';
|
||||
}
|
||||
}
|
||||
|
||||
$results = $results->getResults();
|
||||
|
||||
$this->loadRelations($results, $include, $request);
|
||||
|
||||
if ($relations = array_intersect($include, ['firstPost', 'lastPost', 'mostRelevantPost'])) {
|
||||
foreach ($results as $discussion) {
|
||||
foreach ($relations as $relation) {
|
||||
if ($discussion->$relation) {
|
||||
$discussion->$relation->discussion = $discussion;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
@@ -1,65 +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\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\GroupSerializer;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Http\UrlGenerator;
|
||||
use Flarum\Search\SearchCriteria;
|
||||
use Flarum\Search\SearchManager;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class ListGroupsController extends AbstractListController
|
||||
{
|
||||
public ?string $serializer = GroupSerializer::class;
|
||||
|
||||
public array $sortFields = ['nameSingular', 'namePlural', 'isHidden'];
|
||||
|
||||
public int $limit = -1;
|
||||
|
||||
public function __construct(
|
||||
protected SearchManager $search,
|
||||
protected UrlGenerator $url
|
||||
) {
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): iterable
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
$filters = $this->extractFilter($request);
|
||||
$sort = $this->extractSort($request);
|
||||
$sortIsDefault = $this->sortIsDefault($request);
|
||||
|
||||
$limit = $this->extractLimit($request);
|
||||
$offset = $this->extractOffset($request);
|
||||
|
||||
$queryResults = $this->search->query(
|
||||
Group::class,
|
||||
new SearchCriteria($actor, $filters, $limit, $offset, $sort, $sortIsDefault)
|
||||
);
|
||||
|
||||
$document->addPaginationLinks(
|
||||
$this->url->to('api')->route('groups.index'),
|
||||
$request->getQueryParams(),
|
||||
$offset,
|
||||
$limit,
|
||||
$queryResults->areMoreResults() ? null : 0
|
||||
);
|
||||
|
||||
$results = $queryResults->getResults();
|
||||
|
||||
$this->loadRelations($results, [], $request);
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
@@ -1,100 +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\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\NotificationSerializer;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Http\UrlGenerator;
|
||||
use Flarum\Notification\NotificationRepository;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class ListNotificationsController extends AbstractListController
|
||||
{
|
||||
public ?string $serializer = NotificationSerializer::class;
|
||||
|
||||
public array $include = [
|
||||
'fromUser',
|
||||
'subject',
|
||||
'subject.discussion'
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected NotificationRepository $notifications,
|
||||
protected UrlGenerator $url
|
||||
) {
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): iterable
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
$actor->assertRegistered();
|
||||
|
||||
$actor->markNotificationsAsRead()->save();
|
||||
|
||||
$limit = $this->extractLimit($request);
|
||||
$offset = $this->extractOffset($request);
|
||||
$include = $this->extractInclude($request);
|
||||
|
||||
if (! in_array('subject', $include)) {
|
||||
$include[] = 'subject';
|
||||
}
|
||||
|
||||
$notifications = $this->notifications->findByUser($actor, $limit + 1, $offset);
|
||||
|
||||
$this->loadRelations($notifications, array_diff($include, ['subject.discussion']), $request);
|
||||
|
||||
$notifications = $notifications->all();
|
||||
|
||||
$areMoreResults = false;
|
||||
|
||||
if (count($notifications) > $limit) {
|
||||
array_pop($notifications);
|
||||
$areMoreResults = true;
|
||||
}
|
||||
|
||||
$this->addPaginationData(
|
||||
$document,
|
||||
$request,
|
||||
$this->url->to('api')->route('notifications.index'),
|
||||
$areMoreResults ? null : 0
|
||||
);
|
||||
|
||||
if (in_array('subject.discussion', $include)) {
|
||||
$this->loadSubjectDiscussions($notifications);
|
||||
}
|
||||
|
||||
return $notifications;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Flarum\Notification\Notification[] $notifications
|
||||
*/
|
||||
private function loadSubjectDiscussions(array $notifications): void
|
||||
{
|
||||
$ids = [];
|
||||
|
||||
foreach ($notifications as $notification) {
|
||||
if ($notification->subject && ($discussionId = $notification->subject->getAttribute('discussion_id'))) {
|
||||
$ids[] = $discussionId;
|
||||
}
|
||||
}
|
||||
|
||||
$discussions = Discussion::query()->find(array_unique($ids));
|
||||
|
||||
foreach ($notifications as $notification) {
|
||||
if ($notification->subject && ($discussionId = $notification->subject->getAttribute('discussion_id'))) {
|
||||
$notification->subject->setRelation('discussion', $discussions->find($discussionId));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,124 +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\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\PostSerializer;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Http\UrlGenerator;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Post\PostRepository;
|
||||
use Flarum\Search\SearchCriteria;
|
||||
use Flarum\Search\SearchManager;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
use Tobscure\JsonApi\Exception\InvalidParameterException;
|
||||
|
||||
class ListPostsController extends AbstractListController
|
||||
{
|
||||
public ?string $serializer = PostSerializer::class;
|
||||
|
||||
public array $include = [
|
||||
'user',
|
||||
'user.groups',
|
||||
'editedUser',
|
||||
'hiddenUser',
|
||||
'discussion'
|
||||
];
|
||||
|
||||
public array $sortFields = ['number', 'createdAt'];
|
||||
|
||||
public function __construct(
|
||||
protected SearchManager $search,
|
||||
protected PostRepository $posts,
|
||||
protected UrlGenerator $url
|
||||
) {
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): iterable
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
$filters = $this->extractFilter($request);
|
||||
$sort = $this->extractSort($request);
|
||||
$sortIsDefault = $this->sortIsDefault($request);
|
||||
|
||||
$limit = $this->extractLimit($request);
|
||||
$offset = $this->extractOffset($request);
|
||||
$include = $this->extractInclude($request);
|
||||
|
||||
$results = $this->search->query(
|
||||
Post::class,
|
||||
new SearchCriteria($actor, $filters, $limit, $offset, $sort, $sortIsDefault)
|
||||
);
|
||||
|
||||
$document->addPaginationLinks(
|
||||
$this->url->to('api')->route('posts.index'),
|
||||
$request->getQueryParams(),
|
||||
$offset,
|
||||
$limit,
|
||||
$results->areMoreResults() ? null : 0
|
||||
);
|
||||
|
||||
// Eager load discussion for use in the policies,
|
||||
// eager loading does not affect the JSON response,
|
||||
// the response only includes relations included in the request.
|
||||
if (! in_array('discussion', $include)) {
|
||||
$include[] = 'discussion';
|
||||
}
|
||||
|
||||
if (in_array('user', $include)) {
|
||||
$include[] = 'user.groups';
|
||||
}
|
||||
|
||||
$results = $results->getResults();
|
||||
|
||||
$this->loadRelations($results, $include, $request);
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @link https://github.com/flarum/framework/pull/3506
|
||||
*/
|
||||
protected function extractSort(ServerRequestInterface $request): ?array
|
||||
{
|
||||
$sort = [];
|
||||
|
||||
foreach ((parent::extractSort($request) ?: []) as $field => $direction) {
|
||||
$sort["posts.$field"] = $direction;
|
||||
}
|
||||
|
||||
return $sort;
|
||||
}
|
||||
|
||||
protected function extractOffset(ServerRequestInterface $request): int
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
$queryParams = $request->getQueryParams();
|
||||
$sort = $this->extractSort($request);
|
||||
$limit = $this->extractLimit($request);
|
||||
$filter = $this->extractFilter($request);
|
||||
|
||||
if (($near = Arr::get($queryParams, 'page.near')) > 1) {
|
||||
if (count($filter) > 1 || ! isset($filter['discussion']) || $sort) {
|
||||
throw new InvalidParameterException(
|
||||
'You can only use page[near] with filter[discussion] and the default sort order'
|
||||
);
|
||||
}
|
||||
|
||||
$offset = $this->posts->getIndexForNumber((int) $filter['discussion'], $near, $actor);
|
||||
|
||||
return max(0, $offset - $limit / 2);
|
||||
}
|
||||
|
||||
return parent::extractOffset($request);
|
||||
}
|
||||
}
|
@@ -1,81 +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\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\UserSerializer;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Http\UrlGenerator;
|
||||
use Flarum\Search\SearchCriteria;
|
||||
use Flarum\Search\SearchManager;
|
||||
use Flarum\User\User;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class ListUsersController extends AbstractListController
|
||||
{
|
||||
public ?string $serializer = UserSerializer::class;
|
||||
|
||||
public array $include = ['groups'];
|
||||
|
||||
public array $sortFields = [
|
||||
'username',
|
||||
'commentCount',
|
||||
'discussionCount',
|
||||
'lastSeenAt',
|
||||
'joinedAt'
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected SearchManager $search,
|
||||
protected UrlGenerator $url
|
||||
) {
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): iterable
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
$actor->assertCan('searchUsers');
|
||||
|
||||
if (! $actor->hasPermission('user.viewLastSeenAt')) {
|
||||
// If a user cannot see everyone's last online date, we prevent them from sorting by it
|
||||
// Otherwise this sort field would defeat the privacy setting discloseOnline
|
||||
// We use remove instead of add so that extensions can still completely disable the sort using the extender
|
||||
$this->removeSortField('lastSeenAt');
|
||||
}
|
||||
|
||||
$filters = $this->extractFilter($request);
|
||||
$sort = $this->extractSort($request);
|
||||
$sortIsDefault = $this->sortIsDefault($request);
|
||||
|
||||
$limit = $this->extractLimit($request);
|
||||
$offset = $this->extractOffset($request);
|
||||
$include = $this->extractInclude($request);
|
||||
|
||||
$results = $this->search->query(
|
||||
User::class,
|
||||
new SearchCriteria($actor, $filters, $limit, $offset, $sort, $sortIsDefault)
|
||||
);
|
||||
|
||||
$document->addPaginationLinks(
|
||||
$this->url->to('api')->route('users.index'),
|
||||
$request->getQueryParams(),
|
||||
$offset,
|
||||
$limit,
|
||||
$results->areMoreResults() ? null : 0
|
||||
);
|
||||
|
||||
$results = $results->getResults();
|
||||
|
||||
$this->loadRelations($results, $include, $request);
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
@@ -1,178 +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\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\DiscussionSerializer;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Discussion\DiscussionRepository;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Http\SlugManager;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Post\PostRepository;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class ShowDiscussionController extends AbstractShowController
|
||||
{
|
||||
public ?string $serializer = DiscussionSerializer::class;
|
||||
|
||||
public array $include = [
|
||||
'user',
|
||||
'posts',
|
||||
'posts.discussion',
|
||||
'posts.user',
|
||||
'posts.user.groups',
|
||||
'posts.editedUser',
|
||||
'posts.hiddenUser'
|
||||
];
|
||||
|
||||
public array $optionalInclude = [
|
||||
'user',
|
||||
'lastPostedUser',
|
||||
'firstPost',
|
||||
'lastPost'
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected DiscussionRepository $discussions,
|
||||
protected PostRepository $posts,
|
||||
protected SlugManager $slugManager
|
||||
) {
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): Discussion
|
||||
{
|
||||
$discussionId = Arr::get($request->getQueryParams(), 'id');
|
||||
$actor = RequestUtil::getActor($request);
|
||||
$include = $this->extractInclude($request);
|
||||
|
||||
if (Arr::get($request->getQueryParams(), 'bySlug', false)) {
|
||||
$discussion = $this->slugManager->forResource(Discussion::class)->fromSlug($discussionId, $actor);
|
||||
} else {
|
||||
$discussion = $this->discussions->findOrFail($discussionId, $actor);
|
||||
}
|
||||
|
||||
// If posts is included or a sub relation of post is included.
|
||||
if (in_array('posts', $include) || Str::contains(implode(',', $include), 'posts.')) {
|
||||
$postRelationships = $this->getPostRelationships($include);
|
||||
|
||||
$this->includePosts($discussion, $request, $postRelationships);
|
||||
}
|
||||
|
||||
$this->loadRelations(new Collection([$discussion]), array_filter($include, function ($relationship) {
|
||||
return ! Str::startsWith($relationship, 'posts');
|
||||
}), $request);
|
||||
|
||||
return $discussion;
|
||||
}
|
||||
|
||||
private function includePosts(Discussion $discussion, ServerRequestInterface $request, array $include): void
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
$limit = $this->extractLimit($request);
|
||||
$offset = $this->getPostsOffset($request, $discussion, $limit);
|
||||
|
||||
$allPosts = $this->loadPostIds($discussion, $actor);
|
||||
$loadedPosts = $this->loadPosts($discussion, $actor, $offset, $limit, $include, $request);
|
||||
|
||||
array_splice($allPosts, $offset, $limit, $loadedPosts);
|
||||
|
||||
$discussion->setRelation('posts', (new Post)->newCollection($allPosts));
|
||||
}
|
||||
|
||||
private function loadPostIds(Discussion $discussion, User $actor): array
|
||||
{
|
||||
return $discussion->posts()->whereVisibleTo($actor)->orderBy('number')->pluck('id')->all();
|
||||
}
|
||||
|
||||
private function getPostRelationships(array $include): array
|
||||
{
|
||||
$prefixLength = strlen($prefix = 'posts.');
|
||||
$relationships = [];
|
||||
|
||||
foreach ($include as $relationship) {
|
||||
if (substr($relationship, 0, $prefixLength) === $prefix) {
|
||||
$relationships[] = substr($relationship, $prefixLength);
|
||||
}
|
||||
}
|
||||
|
||||
return $relationships;
|
||||
}
|
||||
|
||||
private function getPostsOffset(ServerRequestInterface $request, Discussion $discussion, int $limit): int
|
||||
{
|
||||
$queryParams = $request->getQueryParams();
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
if (($near = Arr::get($queryParams, 'page.near')) > 1) {
|
||||
$offset = $this->posts->getIndexForNumber($discussion->id, $near, $actor);
|
||||
$offset = max(0, $offset - $limit / 2);
|
||||
} else {
|
||||
$offset = $this->extractOffset($request);
|
||||
}
|
||||
|
||||
return $offset;
|
||||
}
|
||||
|
||||
private function loadPosts(Discussion $discussion, User $actor, int $offset, int $limit, array $include, ServerRequestInterface $request): array
|
||||
{
|
||||
/** @var Builder $query */
|
||||
$query = $discussion->posts()->whereVisibleTo($actor);
|
||||
|
||||
$query->orderBy('number')->skip($offset)->take($limit);
|
||||
|
||||
$posts = $query->get();
|
||||
|
||||
/** @var Post $post */
|
||||
foreach ($posts as $post) {
|
||||
$post->setRelation('discussion', $discussion);
|
||||
}
|
||||
|
||||
$this->loadRelations($posts, $include, $request);
|
||||
|
||||
return $posts->all();
|
||||
}
|
||||
|
||||
protected function getRelationsToLoad(Collection $models): array
|
||||
{
|
||||
$addedRelations = parent::getRelationsToLoad($models);
|
||||
|
||||
if ($models->first() instanceof Discussion) {
|
||||
return $addedRelations;
|
||||
}
|
||||
|
||||
return $this->getPostRelationships($addedRelations);
|
||||
}
|
||||
|
||||
protected function getRelationCallablesToLoad(Collection $models): array
|
||||
{
|
||||
$addedCallableRelations = parent::getRelationCallablesToLoad($models);
|
||||
|
||||
if ($models->first() instanceof Discussion) {
|
||||
return $addedCallableRelations;
|
||||
}
|
||||
|
||||
$postCallableRelationships = $this->getPostRelationships(array_keys($addedCallableRelations));
|
||||
|
||||
$relationCallables = array_intersect_key($addedCallableRelations, array_flip(array_map(function ($relation) {
|
||||
return "posts.$relation";
|
||||
}, $postCallableRelationships)));
|
||||
|
||||
// remove posts. prefix from keys
|
||||
return array_combine(array_map(function ($relation) {
|
||||
return substr($relation, 6);
|
||||
}, array_keys($relationCallables)), array_values($relationCallables));
|
||||
}
|
||||
}
|
@@ -1,37 +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\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\ExtensionReadmeSerializer;
|
||||
use Flarum\Extension\Extension;
|
||||
use Flarum\Extension\ExtensionManager;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class ShowExtensionReadmeController extends AbstractShowController
|
||||
{
|
||||
public ?string $serializer = ExtensionReadmeSerializer::class;
|
||||
|
||||
public function __construct(
|
||||
protected ExtensionManager $extensions
|
||||
) {
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): ?Extension
|
||||
{
|
||||
$extensionName = Arr::get($request->getQueryParams(), 'name');
|
||||
|
||||
RequestUtil::getActor($request)->assertAdmin();
|
||||
|
||||
return $this->extensions->getExtension($extensionName);
|
||||
}
|
||||
}
|
@@ -9,25 +9,24 @@
|
||||
|
||||
namespace Flarum\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\ForumSerializer;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Api\JsonApi;
|
||||
use Flarum\Api\Resource\ForumResource;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
class ShowForumController extends AbstractShowController
|
||||
class ShowForumController implements RequestHandlerInterface
|
||||
{
|
||||
public ?string $serializer = ForumSerializer::class;
|
||||
public function __construct(
|
||||
protected JsonApi $api
|
||||
) {
|
||||
}
|
||||
|
||||
public array $include = ['groups', 'actor', 'actor.groups'];
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): array
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
return [
|
||||
'groups' => Group::whereVisibleTo($actor)->get(),
|
||||
'actor' => $actor->isGuest() ? null : $actor
|
||||
];
|
||||
return $this->api
|
||||
->forResource(ForumResource::class)
|
||||
->forEndpoint('show')
|
||||
->handle($request);
|
||||
}
|
||||
}
|
||||
|
@@ -1,38 +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\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\GroupSerializer;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Group\GroupRepository;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class ShowGroupController extends AbstractShowController
|
||||
{
|
||||
public ?string $serializer = GroupSerializer::class;
|
||||
|
||||
public function __construct(
|
||||
protected GroupRepository $groups
|
||||
) {
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): Group
|
||||
{
|
||||
$id = Arr::get($request->getQueryParams(), 'id');
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
$group = $this->groups->findOrFail($id, $actor);
|
||||
|
||||
return $group;
|
||||
}
|
||||
}
|
@@ -9,36 +9,24 @@
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@@ -1,48 +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\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\PostSerializer;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Post\PostRepository;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class ShowPostController extends AbstractShowController
|
||||
{
|
||||
public ?string $serializer = PostSerializer::class;
|
||||
|
||||
public array $include = [
|
||||
'user',
|
||||
'user.groups',
|
||||
'editedUser',
|
||||
'hiddenUser',
|
||||
'discussion'
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected PostRepository $posts
|
||||
) {
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): Post
|
||||
{
|
||||
$post = $this->posts->findOrFail(Arr::get($request->getQueryParams(), 'id'), RequestUtil::getActor($request));
|
||||
|
||||
$include = $this->extractInclude($request);
|
||||
|
||||
$this->loadRelations(new Collection([$post]), $include, $request);
|
||||
|
||||
return $post;
|
||||
}
|
||||
}
|
@@ -1,51 +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\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\CurrentUserSerializer;
|
||||
use Flarum\Api\Serializer\UserSerializer;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Http\SlugManager;
|
||||
use Flarum\User\User;
|
||||
use Flarum\User\UserRepository;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class ShowUserController extends AbstractShowController
|
||||
{
|
||||
public ?string $serializer = UserSerializer::class;
|
||||
|
||||
public array $include = ['groups'];
|
||||
|
||||
public function __construct(
|
||||
protected SlugManager $slugManager,
|
||||
protected UserRepository $users
|
||||
) {
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): User
|
||||
{
|
||||
$id = Arr::get($request->getQueryParams(), 'id');
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
if (Arr::get($request->getQueryParams(), 'bySlug', false)) {
|
||||
$user = $this->slugManager->forResource(User::class)->fromSlug($id, $actor);
|
||||
} else {
|
||||
$user = $this->users->findOrFail($id, $actor);
|
||||
}
|
||||
|
||||
if ($actor->id === $user->id) {
|
||||
$this->serializer = CurrentUserSerializer::class;
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
@@ -1,74 +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\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\DiscussionSerializer;
|
||||
use Flarum\Discussion\Command\EditDiscussion;
|
||||
use Flarum\Discussion\Command\ReadDiscussion;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Post\Post;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class UpdateDiscussionController extends AbstractShowController
|
||||
{
|
||||
public ?string $serializer = DiscussionSerializer::class;
|
||||
|
||||
public function __construct(
|
||||
protected Dispatcher $bus
|
||||
) {
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): Discussion
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
$discussionId = (int) Arr::get($request->getQueryParams(), 'id');
|
||||
$data = Arr::get($request->getParsedBody(), 'data', []);
|
||||
|
||||
/** @var Discussion $discussion */
|
||||
$discussion = $this->bus->dispatch(
|
||||
new EditDiscussion($discussionId, $actor, $data)
|
||||
);
|
||||
|
||||
// TODO: Refactor the ReadDiscussion (state) command into EditDiscussion?
|
||||
// That's what extensions will do anyway.
|
||||
if ($readNumber = Arr::get($data, 'attributes.lastReadPostNumber')) {
|
||||
$state = $this->bus->dispatch(
|
||||
new ReadDiscussion($discussionId, $actor, $readNumber)
|
||||
);
|
||||
|
||||
$discussion = $state->discussion;
|
||||
}
|
||||
|
||||
if ($posts = $discussion->getModifiedPosts()) {
|
||||
/** @var Collection<int, Post> $posts */
|
||||
$posts = (new Collection($posts))->load('discussion', 'user');
|
||||
$discussionPosts = $discussion->posts()->whereVisibleTo($actor)->oldest()->pluck('id')->all();
|
||||
|
||||
foreach ($discussionPosts as &$id) {
|
||||
foreach ($posts as $post) {
|
||||
if ($id == $post->id) {
|
||||
$id = $post;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$discussion->setRelation('posts', $discussionPosts);
|
||||
|
||||
$this->include = array_merge($this->include, ['posts', 'posts.discussion', 'posts.user']);
|
||||
}
|
||||
|
||||
return $discussion;
|
||||
}
|
||||
}
|
@@ -1,40 +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\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\GroupSerializer;
|
||||
use Flarum\Group\Command\EditGroup;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class UpdateGroupController extends AbstractShowController
|
||||
{
|
||||
public ?string $serializer = GroupSerializer::class;
|
||||
|
||||
public function __construct(
|
||||
protected Dispatcher $bus
|
||||
) {
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): Group
|
||||
{
|
||||
$id = Arr::get($request->getQueryParams(), 'id');
|
||||
$actor = RequestUtil::getActor($request);
|
||||
$data = Arr::get($request->getParsedBody(), 'data', []);
|
||||
|
||||
return $this->bus->dispatch(
|
||||
new EditGroup($id, $actor, $data)
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,39 +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\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\NotificationSerializer;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Notification\Command\ReadNotification;
|
||||
use Flarum\Notification\Notification;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class UpdateNotificationController extends AbstractShowController
|
||||
{
|
||||
public ?string $serializer = NotificationSerializer::class;
|
||||
|
||||
public function __construct(
|
||||
protected Dispatcher $bus
|
||||
) {
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): Notification
|
||||
{
|
||||
$id = Arr::get($request->getQueryParams(), 'id');
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
return $this->bus->dispatch(
|
||||
new ReadNotification($id, $actor)
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,49 +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\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\PostSerializer;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Post\Command\EditPost;
|
||||
use Flarum\Post\Post;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class UpdatePostController extends AbstractShowController
|
||||
{
|
||||
public ?string $serializer = PostSerializer::class;
|
||||
|
||||
public array $include = [
|
||||
'editedUser',
|
||||
'discussion'
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected Dispatcher $bus
|
||||
) {
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): Post
|
||||
{
|
||||
$id = Arr::get($request->getQueryParams(), 'id');
|
||||
$actor = RequestUtil::getActor($request);
|
||||
$data = Arr::get($request->getParsedBody(), 'data', []);
|
||||
|
||||
$post = $this->bus->dispatch(
|
||||
new EditPost($id, $actor, $data)
|
||||
);
|
||||
|
||||
$this->loadRelations($post->newCollection([$post]), $this->extractInclude($request), $request);
|
||||
|
||||
return $post;
|
||||
}
|
||||
}
|
@@ -1,58 +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\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\CurrentUserSerializer;
|
||||
use Flarum\Api\Serializer\UserSerializer;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\User\Command\EditUser;
|
||||
use Flarum\User\Exception\NotAuthenticatedException;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class UpdateUserController extends AbstractShowController
|
||||
{
|
||||
public ?string $serializer = UserSerializer::class;
|
||||
|
||||
public array $include = ['groups'];
|
||||
|
||||
public function __construct(
|
||||
protected Dispatcher $bus
|
||||
) {
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): User
|
||||
{
|
||||
$id = Arr::get($request->getQueryParams(), 'id');
|
||||
$actor = RequestUtil::getActor($request);
|
||||
$data = Arr::get($request->getParsedBody(), 'data', []);
|
||||
|
||||
if ($actor->id == $id) {
|
||||
$this->serializer = CurrentUserSerializer::class;
|
||||
}
|
||||
|
||||
// Require the user's current password if they are attempting to change
|
||||
// their own email address.
|
||||
if (isset($data['attributes']['email']) && $actor->id == $id) {
|
||||
$password = (string) Arr::get($request->getParsedBody(), 'meta.password');
|
||||
|
||||
if (! $actor->checkPassword($password)) {
|
||||
throw new NotAuthenticatedException;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->bus->dispatch(
|
||||
new EditUser($id, $actor, $data)
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,40 +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\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\UserSerializer;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\User\Command\UploadAvatar;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class UploadAvatarController extends AbstractShowController
|
||||
{
|
||||
public ?string $serializer = UserSerializer::class;
|
||||
|
||||
public function __construct(
|
||||
protected Dispatcher $bus
|
||||
) {
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): User
|
||||
{
|
||||
$id = Arr::get($request->getQueryParams(), 'id');
|
||||
$actor = RequestUtil::getActor($request);
|
||||
$file = Arr::get($request->getUploadedFiles(), 'avatar');
|
||||
|
||||
return $this->bus->dispatch(
|
||||
new UploadAvatar($id, $file, $actor)
|
||||
);
|
||||
}
|
||||
}
|
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace Flarum\Api\Controller;
|
||||
|
||||
use Flarum\Api\JsonApi;
|
||||
use Flarum\Foundation\ValidationException;
|
||||
use Flarum\Locale\TranslatorInterface;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
@@ -23,12 +24,13 @@ class UploadFaviconController extends UploadImageController
|
||||
protected string $filenamePrefix = 'favicon';
|
||||
|
||||
public function __construct(
|
||||
JsonApi $api,
|
||||
SettingsRepositoryInterface $settings,
|
||||
Factory $filesystemFactory,
|
||||
protected TranslatorInterface $translator,
|
||||
protected ImageManager $imageManager
|
||||
) {
|
||||
parent::__construct($settings, $filesystemFactory);
|
||||
parent::__construct($api, $settings, $filesystemFactory);
|
||||
}
|
||||
|
||||
protected function makeImage(UploadedFileInterface $file): EncodedImageInterface
|
||||
|
@@ -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;
|
||||
|
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace Flarum\Api\Controller;
|
||||
|
||||
use Flarum\Api\JsonApi;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Illuminate\Contracts\Filesystem\Factory;
|
||||
use Intervention\Image\ImageManager;
|
||||
@@ -21,11 +22,12 @@ class UploadLogoController extends UploadImageController
|
||||
protected string $filenamePrefix = 'logo';
|
||||
|
||||
public function __construct(
|
||||
JsonApi $api,
|
||||
SettingsRepositoryInterface $settings,
|
||||
Factory $filesystemFactory,
|
||||
protected ImageManager $imageManager
|
||||
) {
|
||||
parent::__construct($settings, $filesystemFactory);
|
||||
parent::__construct($api, $settings, $filesystemFactory);
|
||||
}
|
||||
|
||||
protected function makeImage(UploadedFileInterface $file): EncodedImageInterface
|
||||
|
@@ -0,0 +1,126 @@
|
||||
<?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\Api\Endpoint\Concerns;
|
||||
|
||||
use Closure;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Tobyz\JsonApiServer\Context;
|
||||
use Tobyz\JsonApiServer\Resource\AbstractResource;
|
||||
use Tobyz\JsonApiServer\Schema\Sort;
|
||||
|
||||
trait ExtractsListingParams
|
||||
{
|
||||
protected ?Closure $extractFilterCallback = null;
|
||||
protected ?Closure $extractSortCallback = null;
|
||||
protected ?Closure $extractLimitCallback = null;
|
||||
protected ?Closure $extractOffsetCallback = null;
|
||||
|
||||
public ?int $limit = null;
|
||||
public int $maxLimit = 50;
|
||||
public ?string $defaultSort = null;
|
||||
|
||||
public function limit(int $limit): static
|
||||
{
|
||||
$this->limit = $limit;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function maxLimit(int $maxLimit): static
|
||||
{
|
||||
$this->maxLimit = $maxLimit;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function extractFilter(Closure $callback): self
|
||||
{
|
||||
$this->extractFilterCallback = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function extractSort(Closure $callback): self
|
||||
{
|
||||
$this->extractSortCallback = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function extractLimit(Closure $callback): self
|
||||
{
|
||||
$this->extractLimitCallback = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function extractOffset(Closure $callback): self
|
||||
{
|
||||
$this->extractOffsetCallback = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function extractFilterValue(Context $context, array $defaultExtracts): array
|
||||
{
|
||||
return $this->extractFilterCallback
|
||||
? ($this->extractFilterCallback)($context, $defaultExtracts)
|
||||
: $defaultExtracts['filter'];
|
||||
}
|
||||
|
||||
public function extractSortValue(Context $context, array $defaultExtracts): ?array
|
||||
{
|
||||
$visibleSorts = $this->getAvailableSorts($context);
|
||||
|
||||
return $this->extractSortCallback
|
||||
? ($this->extractSortCallback)($context, $defaultExtracts, $visibleSorts)
|
||||
: $defaultExtracts['sort'];
|
||||
}
|
||||
|
||||
public function extractLimitValue(Context $context, array $defaultExtracts): ?int
|
||||
{
|
||||
return $this->extractLimitCallback
|
||||
? ($this->extractLimitCallback)($context, $defaultExtracts)
|
||||
: $defaultExtracts['limit'];
|
||||
}
|
||||
|
||||
public function extractOffsetValue(Context $context, array $defaultExtracts): int
|
||||
{
|
||||
return $this->extractOffsetCallback
|
||||
? ($this->extractOffsetCallback)($context, $defaultExtracts)
|
||||
: $defaultExtracts['offset'];
|
||||
}
|
||||
|
||||
public function defaultExtracts(Context $context): array
|
||||
{
|
||||
return [
|
||||
'filter' => RequestUtil::extractFilter($context->request),
|
||||
'sort' => RequestUtil::extractSort($context->request, $this->defaultSort, $this->getAvailableSorts($context)),
|
||||
'limit' => $limit = (RequestUtil::extractLimit($context->request, $this->limit, $this->maxLimit) ?? null),
|
||||
'offset' => RequestUtil::extractOffset($context->request, $limit),
|
||||
];
|
||||
}
|
||||
|
||||
public function getAvailableSorts(Context $context): array
|
||||
{
|
||||
if (! $context->collection instanceof AbstractResource) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$asc = collect($context->collection->resolveSorts())
|
||||
->filter(fn (Sort $field) => $field->isVisible($context))
|
||||
->pluck('name')
|
||||
->toArray();
|
||||
|
||||
$desc = array_map(fn ($field) => "-$field", $asc);
|
||||
|
||||
return array_merge($asc, $desc);
|
||||
}
|
||||
}
|
@@ -0,0 +1,91 @@
|
||||
<?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\Api\Endpoint\Concerns;
|
||||
|
||||
use Closure;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\User\Exception\NotAuthenticatedException;
|
||||
use Flarum\User\Exception\PermissionDeniedException;
|
||||
use Tobyz\JsonApiServer\Context;
|
||||
|
||||
trait HasAuthorization
|
||||
{
|
||||
protected bool|Closure $authenticated = false;
|
||||
|
||||
protected null|string|Closure $ability = null;
|
||||
|
||||
protected bool $admin = false;
|
||||
|
||||
public function authenticated(bool|Closure $condition = true): self
|
||||
{
|
||||
$this->authenticated = $condition;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function can(null|string|Closure $ability): self
|
||||
{
|
||||
$this->ability = $ability;
|
||||
|
||||
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)) {
|
||||
return $this->authenticated;
|
||||
}
|
||||
|
||||
return (bool) (isset($context->model)
|
||||
? ($this->authenticated)($context->model, $context)
|
||||
: ($this->authenticated)($context));
|
||||
}
|
||||
|
||||
public function getAuthorized(Context $context): string|null
|
||||
{
|
||||
if (! is_callable($this->ability)) {
|
||||
return $this->ability;
|
||||
}
|
||||
|
||||
return isset($context->model)
|
||||
? ($this->ability)($context->model, $context)
|
||||
: ($this->ability)($context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NotAuthenticatedException
|
||||
* @throws PermissionDeniedException
|
||||
*/
|
||||
public function isVisible(Context $context): bool
|
||||
{
|
||||
$actor = RequestUtil::getActor($context->request);
|
||||
|
||||
if ($this->getAuthenticated($context)) {
|
||||
$actor->assertRegistered();
|
||||
}
|
||||
|
||||
if ($this->admin) {
|
||||
$actor->assertAdmin();
|
||||
}
|
||||
|
||||
if ($ability = $this->getAuthorized($context)) {
|
||||
$actor->assertCan($ability, $context->model);
|
||||
}
|
||||
|
||||
return parent::isVisible($context);
|
||||
}
|
||||
}
|
25
framework/core/src/Api/Endpoint/Concerns/HasCustomHooks.php
Normal file
25
framework/core/src/Api/Endpoint/Concerns/HasCustomHooks.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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\Api\Endpoint\Concerns;
|
||||
|
||||
use Flarum\Foundation\ContainerUtil;
|
||||
use Tobyz\JsonApiServer\Context;
|
||||
|
||||
trait HasCustomHooks
|
||||
{
|
||||
protected function resolveCallable(callable|string $callable, Context $context): callable
|
||||
{
|
||||
if (is_string($callable)) {
|
||||
return ContainerUtil::wrapCallback($callable, $context->api->getContainer());
|
||||
}
|
||||
|
||||
return $callable;
|
||||
}
|
||||
}
|
25
framework/core/src/Api/Endpoint/Create.php
Normal file
25
framework/core/src/Api/Endpoint/Create.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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\Api\Endpoint;
|
||||
|
||||
use Flarum\Api\Endpoint\Concerns\HasAuthorization;
|
||||
use Flarum\Api\Endpoint\Concerns\HasCustomHooks;
|
||||
use Tobyz\JsonApiServer\Endpoint\Create as BaseCreate;
|
||||
|
||||
class Create extends BaseCreate implements EndpointInterface
|
||||
{
|
||||
use HasAuthorization;
|
||||
use HasCustomHooks;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
}
|
||||
}
|
20
framework/core/src/Api/Endpoint/Delete.php
Normal file
20
framework/core/src/Api/Endpoint/Delete.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?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\Api\Endpoint;
|
||||
|
||||
use Flarum\Api\Endpoint\Concerns\HasAuthorization;
|
||||
use Flarum\Api\Endpoint\Concerns\HasCustomHooks;
|
||||
use Tobyz\JsonApiServer\Endpoint\Delete as BaseDelete;
|
||||
|
||||
class Delete extends BaseDelete implements EndpointInterface
|
||||
{
|
||||
use HasAuthorization;
|
||||
use HasCustomHooks;
|
||||
}
|
22
framework/core/src/Api/Endpoint/Endpoint.php
Normal file
22
framework/core/src/Api/Endpoint/Endpoint.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?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\Api\Endpoint;
|
||||
|
||||
use Flarum\Api\Endpoint\Concerns\ExtractsListingParams;
|
||||
use Flarum\Api\Endpoint\Concerns\HasAuthorization;
|
||||
use Flarum\Api\Endpoint\Concerns\HasCustomHooks;
|
||||
use Tobyz\JsonApiServer\Endpoint\Endpoint as BaseEndpoint;
|
||||
|
||||
class Endpoint extends BaseEndpoint implements EndpointInterface
|
||||
{
|
||||
use HasAuthorization;
|
||||
use HasCustomHooks;
|
||||
use ExtractsListingParams;
|
||||
}
|
18
framework/core/src/Api/Endpoint/EndpointInterface.php
Normal file
18
framework/core/src/Api/Endpoint/EndpointInterface.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?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\Api\Endpoint;
|
||||
|
||||
/**
|
||||
* @mixin \Tobyz\JsonApiServer\Endpoint\Endpoint
|
||||
*/
|
||||
interface EndpointInterface
|
||||
{
|
||||
//
|
||||
}
|
86
framework/core/src/Api/Endpoint/Index.php
Normal file
86
framework/core/src/Api/Endpoint/Index.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?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\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\Search\SearchCriteria;
|
||||
use Flarum\Search\SearchManager;
|
||||
use Illuminate\Contracts\Database\Eloquent\Builder;
|
||||
use Tobyz\JsonApiServer\Endpoint\Index as BaseIndex;
|
||||
use Tobyz\JsonApiServer\Pagination\OffsetPagination;
|
||||
use Tobyz\JsonApiServer\Pagination\Pagination;
|
||||
|
||||
class Index extends BaseIndex implements EndpointInterface
|
||||
{
|
||||
use HasAuthorization;
|
||||
use ExtractsListingParams;
|
||||
use HasCustomHooks;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this
|
||||
->query(function ($query, ?Pagination $pagination, Context $context): Context {
|
||||
// This model has a searcher API, so we'll use that instead of the default.
|
||||
// The searcher API allows swapping the default search engine for a custom one.
|
||||
$search = $context->api->getContainer()->make(SearchManager::class);
|
||||
$modelClass = $query->getModel()::class;
|
||||
|
||||
if ($query instanceof Builder && $search->searchable($modelClass)) {
|
||||
$actor = $context->getActor();
|
||||
|
||||
$extracts = $this->defaultExtracts($context);
|
||||
|
||||
$filters = $this->extractFilterValue($context, $extracts);
|
||||
$sort = $this->extractSortValue($context, $extracts);
|
||||
$limit = $this->extractLimitValue($context, $extracts);
|
||||
$offset = $this->extractOffsetValue($context, $extracts);
|
||||
|
||||
$sortIsDefault = ! $context->queryParam('sort');
|
||||
|
||||
$results = $search->query(
|
||||
$modelClass,
|
||||
new SearchCriteria($actor, $filters, $limit, $offset, $sort, $sortIsDefault),
|
||||
);
|
||||
|
||||
$context = $context->withSearchResults($results);
|
||||
}
|
||||
// If the model doesn't have a searcher API, we'll just use the default logic.
|
||||
else {
|
||||
$context = $context->withQuery($query);
|
||||
|
||||
$this->applySorts($query, $context);
|
||||
$this->applyFilters($query, $context);
|
||||
|
||||
$pagination?->apply($query);
|
||||
}
|
||||
|
||||
return $context;
|
||||
});
|
||||
}
|
||||
|
||||
public function paginate(int $defaultLimit = 20, int $maxLimit = 50): static
|
||||
{
|
||||
$this->limit = $defaultLimit;
|
||||
$this->maxLimit = $maxLimit;
|
||||
|
||||
$this->paginationResolver = fn (Context $context) => new OffsetPagination(
|
||||
$context,
|
||||
$this->limit,
|
||||
$this->maxLimit,
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
27
framework/core/src/Api/Endpoint/Show.php
Normal file
27
framework/core/src/Api/Endpoint/Show.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?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\Api\Endpoint;
|
||||
|
||||
use Flarum\Api\Endpoint\Concerns\ExtractsListingParams;
|
||||
use Flarum\Api\Endpoint\Concerns\HasAuthorization;
|
||||
use Flarum\Api\Endpoint\Concerns\HasCustomHooks;
|
||||
use Tobyz\JsonApiServer\Endpoint\Show as BaseShow;
|
||||
|
||||
class Show extends BaseShow implements EndpointInterface
|
||||
{
|
||||
use HasAuthorization;
|
||||
use ExtractsListingParams;
|
||||
use HasCustomHooks;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
}
|
||||
}
|
25
framework/core/src/Api/Endpoint/Update.php
Normal file
25
framework/core/src/Api/Endpoint/Update.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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\Api\Endpoint;
|
||||
|
||||
use Flarum\Api\Endpoint\Concerns\HasAuthorization;
|
||||
use Flarum\Api\Endpoint\Concerns\HasCustomHooks;
|
||||
use Tobyz\JsonApiServer\Endpoint\Update as BaseUpdate;
|
||||
|
||||
class Update extends BaseUpdate implements EndpointInterface
|
||||
{
|
||||
use HasAuthorization;
|
||||
use HasCustomHooks;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
}
|
||||
}
|
165
framework/core/src/Api/JsonApi.php
Normal file
165
framework/core/src/Api/JsonApi.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?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\Api;
|
||||
|
||||
use Flarum\Api\Endpoint\EndpointInterface;
|
||||
use Flarum\Api\Resource\AbstractDatabaseResource;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
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;
|
||||
use Tobyz\JsonApiServer\Resource\Resource;
|
||||
|
||||
class JsonApi extends BaseJsonApi
|
||||
{
|
||||
protected string $resourceClass;
|
||||
protected string $endpointName;
|
||||
protected ?Request $baseRequest = null;
|
||||
protected ?Container $container = null;
|
||||
|
||||
public function forResource(string $resourceClass): self
|
||||
{
|
||||
$this->resourceClass = $resourceClass;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function forEndpoint(string $endpointName): self
|
||||
{
|
||||
$this->endpointName = $endpointName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function makeContext(Request $request): Context
|
||||
{
|
||||
if (! $this->endpointName || ! $this->resourceClass || ! class_exists($this->resourceClass)) {
|
||||
throw new BadRequestException('No resource or endpoint specified');
|
||||
}
|
||||
|
||||
$collection = $this->getCollection($this->resourceClass);
|
||||
|
||||
return (new Context($this, $request))
|
||||
->withCollection($collection)
|
||||
->withEndpoint($this->findEndpoint($collection));
|
||||
}
|
||||
|
||||
protected function findEndpoint(?Collection $collection): Endpoint&EndpointInterface
|
||||
{
|
||||
/** @var Endpoint&EndpointInterface $endpoint */
|
||||
foreach ($collection->resolveEndpoints() as $endpoint) {
|
||||
if ($endpoint->name === $this->endpointName) {
|
||||
return $endpoint;
|
||||
}
|
||||
}
|
||||
|
||||
throw new BadRequestException('Invalid endpoint specified');
|
||||
}
|
||||
|
||||
public function withRequest(Request $request): self
|
||||
{
|
||||
$this->baseRequest = $request;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$context = $this->makeContext($request);
|
||||
|
||||
return $context->endpoint->handle($context);
|
||||
}
|
||||
|
||||
public function process(array $body, array $internal = [], array $options = []): mixed
|
||||
{
|
||||
$request = $this->baseRequest ?? ServerRequestFactory::fromGlobals();
|
||||
|
||||
if (! empty($options['actor'])) {
|
||||
$request = RequestUtil::withActor($request, $options['actor']);
|
||||
}
|
||||
|
||||
$resource = $this->getCollection($this->resourceClass);
|
||||
|
||||
$request = $request
|
||||
->withParsedBody([
|
||||
...$body,
|
||||
'data' => [
|
||||
...($request->getParsedBody()['data'] ?? []),
|
||||
...($body['data'] ?? []),
|
||||
'type' => $resource instanceof Resource
|
||||
? $resource->type()
|
||||
: $resource->name(),
|
||||
],
|
||||
]);
|
||||
|
||||
$context = $this->makeContext($request)
|
||||
->withModelId($body['data']['id'] ?? null);
|
||||
|
||||
foreach ($internal as $key => $value) {
|
||||
$context = $context->withInternal($key, $value);
|
||||
}
|
||||
|
||||
$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
|
||||
{
|
||||
foreach ($request->getQueryParams() as $key => $value) {
|
||||
if (
|
||||
! preg_match('/[^a-z]/', $key) &&
|
||||
! in_array($key, ['include', 'fields', 'filter', 'page', 'sort'])
|
||||
) {
|
||||
throw (new BadRequestException("Invalid query parameter: $key"))->setSource([
|
||||
'parameter' => $key,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function typeForModel(string $modelClass): ?string
|
||||
{
|
||||
foreach ($this->resources as $resource) {
|
||||
if ($resource instanceof AbstractDatabaseResource && $resource->model() === $modelClass) {
|
||||
return $resource->type();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function typesForModels(array $modelClasses): array
|
||||
{
|
||||
return array_values(array_unique(array_map(fn ($modelClass) => $this->typeForModel($modelClass), $modelClasses)));
|
||||
}
|
||||
|
||||
public function container(Container $container): static
|
||||
{
|
||||
$this->container = $container;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getContainer(): ?Container
|
||||
{
|
||||
return $this->container;
|
||||
}
|
||||
}
|
@@ -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';
|
||||
|
||||
|
205
framework/core/src/Api/Resource/AbstractDatabaseResource.php
Normal file
205
framework/core/src/Api/Resource/AbstractDatabaseResource.php
Normal file
@@ -0,0 +1,205 @@
|
||||
<?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\Api\Resource;
|
||||
|
||||
use Flarum\Api\Context as FlarumContext;
|
||||
use Flarum\Api\Resource\Concerns\Bootable;
|
||||
use Flarum\Api\Resource\Concerns\Extendable;
|
||||
use Flarum\Api\Resource\Concerns\HasSortMap;
|
||||
use Flarum\Foundation\DispatchEventsTrait;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use RuntimeException;
|
||||
use Tobyz\JsonApiServer\Context;
|
||||
use Tobyz\JsonApiServer\Laravel\EloquentResource as BaseResource;
|
||||
|
||||
/**
|
||||
* @template M of Model
|
||||
* @extends BaseResource<M, FlarumContext>
|
||||
*/
|
||||
abstract class AbstractDatabaseResource extends BaseResource
|
||||
{
|
||||
use Bootable;
|
||||
use Extendable;
|
||||
use HasSortMap;
|
||||
use DispatchEventsTrait {
|
||||
dispatchEventsFor as traitDispatchEventsFor;
|
||||
}
|
||||
|
||||
abstract public function model(): string;
|
||||
|
||||
/** @inheritDoc */
|
||||
public function newModel(Context $context): object
|
||||
{
|
||||
return new ($this->model());
|
||||
}
|
||||
|
||||
public function resource(object $model, Context $context): ?string
|
||||
{
|
||||
$baseModel = $this->model();
|
||||
|
||||
if ($model instanceof $baseModel) {
|
||||
return $this->type();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function filters(): array
|
||||
{
|
||||
throw new RuntimeException('Not supported in Flarum, please use a model searcher instead https://docs.flarum.org/extend/search.');
|
||||
}
|
||||
|
||||
public function createAction(object $model, Context $context): object
|
||||
{
|
||||
$model = parent::createAction($model, $context);
|
||||
|
||||
$this->dispatchEventsFor($model, $context->getActor());
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
public function updateAction(object $model, Context $context): object
|
||||
{
|
||||
$model = parent::updateAction($model, $context);
|
||||
|
||||
$this->dispatchEventsFor($model, $context->getActor());
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
public function deleteAction(object $model, Context $context): void
|
||||
{
|
||||
$this->deleting($model, $context);
|
||||
|
||||
$this->delete($model, $context);
|
||||
|
||||
$this->deleted($model, $context);
|
||||
|
||||
$this->dispatchEventsFor($model, $context->getActor());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
* @return M|null
|
||||
*/
|
||||
public function creating(object $model, Context $context): ?object
|
||||
{
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
* @return M|null
|
||||
*/
|
||||
public function updating(object $model, Context $context): ?object
|
||||
{
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
* @return M|null
|
||||
*/
|
||||
public function saving(object $model, Context $context): ?object
|
||||
{
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
* @return M|null
|
||||
*/
|
||||
public function saved(object $model, Context $context): ?object
|
||||
{
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
* @return M|null
|
||||
*/
|
||||
public function created(object $model, Context $context): ?object
|
||||
{
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
* @return M|null
|
||||
*/
|
||||
public function updated(object $model, Context $context): ?object
|
||||
{
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
*/
|
||||
public function deleting(object $model, Context $context): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* @param M $model
|
||||
* @param FlarumContext $context
|
||||
*/
|
||||
public function deleted(object $model, Context $context): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function dispatchEventsFor(mixed $entity, User $actor = null): void
|
||||
{
|
||||
if (method_exists($entity, 'releaseEvents')) {
|
||||
$this->traitDispatchEventsFor($entity, $actor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FlarumContext $context
|
||||
*/
|
||||
public function mutateDataBeforeValidation(Context $context, array $data): array
|
||||
{
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FlarumContext $context
|
||||
*/
|
||||
public function results(object $query, Context $context): iterable
|
||||
{
|
||||
if ($results = $context->getSearchResults()) {
|
||||
return $results->getResults();
|
||||
}
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FlarumContext $context
|
||||
*/
|
||||
public function count(object $query, Context $context): ?int
|
||||
{
|
||||
if ($results = $context->getSearchResults()) {
|
||||
return $results->getTotalResults();
|
||||
}
|
||||
|
||||
return parent::count($query, $context);
|
||||
}
|
||||
}
|
27
framework/core/src/Api/Resource/AbstractResource.php
Normal file
27
framework/core/src/Api/Resource/AbstractResource.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?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\Api\Resource;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Resource\Concerns\Bootable;
|
||||
use Flarum\Api\Resource\Concerns\Extendable;
|
||||
use Flarum\Api\Resource\Concerns\HasSortMap;
|
||||
use Tobyz\JsonApiServer\Resource\AbstractResource as BaseResource;
|
||||
|
||||
/**
|
||||
* @template M of object
|
||||
* @extends BaseResource<M, Context>
|
||||
*/
|
||||
abstract class AbstractResource extends BaseResource
|
||||
{
|
||||
use Bootable;
|
||||
use Extendable;
|
||||
use HasSortMap;
|
||||
}
|
143
framework/core/src/Api/Resource/AccessTokenResource.php
Normal file
143
framework/core/src/Api/Resource/AccessTokenResource.php
Normal file
@@ -0,0 +1,143 @@
|
||||
<?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\Api\Resource;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Endpoint;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Http\AccessToken;
|
||||
use Flarum\Http\DeveloperAccessToken;
|
||||
use Flarum\Http\Event\DeveloperTokenCreated;
|
||||
use Flarum\Http\RememberAccessToken;
|
||||
use Flarum\Http\SessionAccessToken;
|
||||
use Flarum\Locale\TranslatorInterface;
|
||||
use Flarum\User\Exception\PermissionDeniedException;
|
||||
use Illuminate\Contracts\Session\Session;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Jenssegers\Agent\Agent;
|
||||
|
||||
/**
|
||||
* @extends AbstractDatabaseResource<AccessToken>
|
||||
*/
|
||||
class AccessTokenResource extends AbstractDatabaseResource
|
||||
{
|
||||
public function __construct(
|
||||
protected TranslatorInterface $translator
|
||||
) {
|
||||
}
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return 'access-tokens';
|
||||
}
|
||||
|
||||
public function model(): string
|
||||
{
|
||||
return AccessToken::class;
|
||||
}
|
||||
|
||||
public function scope(Builder $query, \Tobyz\JsonApiServer\Context $context): void
|
||||
{
|
||||
$query->whereVisibleTo($context->getActor());
|
||||
}
|
||||
|
||||
public function newModel(\Tobyz\JsonApiServer\Context $context): object
|
||||
{
|
||||
if ($context->creating(self::class)) {
|
||||
$token = DeveloperAccessToken::make($context->getActor()->id);
|
||||
$token->last_activity_at = null;
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
return parent::newModel($context);
|
||||
}
|
||||
|
||||
public function endpoints(): array
|
||||
{
|
||||
return [
|
||||
Endpoint\Create::make()
|
||||
->authenticated()
|
||||
->can('createAccessToken'),
|
||||
Endpoint\Delete::make()
|
||||
->authenticated(),
|
||||
Endpoint\Index::make()
|
||||
->authenticated()
|
||||
->paginate(),
|
||||
];
|
||||
}
|
||||
|
||||
public function fields(): array
|
||||
{
|
||||
return [
|
||||
Schema\Str::make('token')
|
||||
->visible(function (AccessToken $token, Context $context) {
|
||||
return $context->getActor()->id === $token->user_id && ! in_array('token', $token->getHidden(), true);
|
||||
}),
|
||||
Schema\Integer::make('userId'),
|
||||
Schema\DateTime::make('createdAt'),
|
||||
Schema\DateTime::make('lastActivityAt'),
|
||||
Schema\Boolean::make('isCurrent')
|
||||
->get(function (AccessToken $token, Context $context) {
|
||||
return $token->token === $context->request->getAttribute('session')->get('access_token');
|
||||
}),
|
||||
Schema\Boolean::make('isSessionToken')
|
||||
->get(function (AccessToken $token) {
|
||||
return in_array($token->type, [SessionAccessToken::$type, RememberAccessToken::$type], true);
|
||||
}),
|
||||
Schema\Str::make('title')
|
||||
->writableOnCreate()
|
||||
->requiredOnCreate()
|
||||
->maxLength(255),
|
||||
Schema\Str::make('lastIpAddress'),
|
||||
Schema\Str::make('device')
|
||||
->get(function (AccessToken $token) {
|
||||
$agent = new Agent();
|
||||
$agent->setUserAgent($token->last_user_agent);
|
||||
|
||||
return $this->translator->trans('core.forum.security.browser_on_operating_system', [
|
||||
'browser' => $agent->browser(),
|
||||
'os' => $agent->platform(),
|
||||
]);
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public function created(object $model, \Tobyz\JsonApiServer\Context $context): ?object
|
||||
{
|
||||
$this->events->dispatch(new DeveloperTokenCreated($model));
|
||||
|
||||
return parent::created($model, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AccessToken $model
|
||||
* @param \Flarum\Api\Context $context
|
||||
* @throws PermissionDeniedException
|
||||
*/
|
||||
public function delete(object $model, \Tobyz\JsonApiServer\Context $context): void
|
||||
{
|
||||
/** @var Session|null $session */
|
||||
$session = $context->request->getAttribute('session');
|
||||
|
||||
// Current session should only be terminated through logout.
|
||||
if ($session && $model->token === $session->get('access_token')) {
|
||||
throw new PermissionDeniedException();
|
||||
}
|
||||
|
||||
// Don't give away the existence of the token.
|
||||
if ($context->getActor()->cannot('revoke', $model)) {
|
||||
throw new ModelNotFoundException();
|
||||
}
|
||||
|
||||
$model->delete();
|
||||
}
|
||||
}
|
41
framework/core/src/Api/Resource/Concerns/Bootable.php
Normal file
41
framework/core/src/Api/Resource/Concerns/Bootable.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?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\Api\Resource\Concerns;
|
||||
|
||||
use Flarum\Api\JsonApi;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Contracts\Validation\Factory;
|
||||
|
||||
trait Bootable
|
||||
{
|
||||
protected JsonApi $api;
|
||||
protected Dispatcher $events;
|
||||
protected Factory $validation;
|
||||
|
||||
/**
|
||||
* Avoids polluting the constructor of the resource with dependencies.
|
||||
*/
|
||||
public function boot(JsonApi $api): static
|
||||
{
|
||||
$this->api = $api;
|
||||
$this->events = $api->getContainer()->make(Dispatcher::class);
|
||||
$this->validation = $api->getContainer()->make(Factory::class);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the JSON:API server package to resolve the validation factory.
|
||||
*/
|
||||
public function validationFactory(): Factory
|
||||
{
|
||||
return $this->validation;
|
||||
}
|
||||
}
|
93
framework/core/src/Api/Resource/Concerns/Extendable.php
Normal file
93
framework/core/src/Api/Resource/Concerns/Extendable.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?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\Api\Resource\Concerns;
|
||||
|
||||
trait Extendable
|
||||
{
|
||||
protected static array $endpointModifiers = [];
|
||||
protected static array $fieldModifiers = [];
|
||||
protected static array $sortModifiers = [];
|
||||
|
||||
protected ?array $cachedEndpoints = null;
|
||||
protected ?array $cachedFields = null;
|
||||
protected ?array $cachedSorts = null;
|
||||
|
||||
public static function mutateEndpoints(callable $modifier): void
|
||||
{
|
||||
static::$endpointModifiers[static::class][] = $modifier;
|
||||
}
|
||||
|
||||
public static function mutateFields(callable $modifier): void
|
||||
{
|
||||
static::$fieldModifiers[static::class][] = $modifier;
|
||||
}
|
||||
|
||||
public static function mutateSorts(callable $modifier): void
|
||||
{
|
||||
static::$sortModifiers[static::class][] = $modifier;
|
||||
}
|
||||
|
||||
public function resolveEndpoints(bool $earlyResolution = false): array
|
||||
{
|
||||
if (! is_null($this->cachedEndpoints) && ! $earlyResolution) {
|
||||
return $this->cachedEndpoints;
|
||||
}
|
||||
|
||||
$endpoints = $this->endpoints();
|
||||
|
||||
foreach (array_reverse(array_merge([static::class], class_parents($this))) as $class) {
|
||||
if (isset(static::$endpointModifiers[$class])) {
|
||||
foreach (static::$endpointModifiers[$class] as $modifier) {
|
||||
$endpoints = $modifier($endpoints, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->cachedEndpoints = $endpoints;
|
||||
}
|
||||
|
||||
public function resolveFields(): array
|
||||
{
|
||||
if (! is_null($this->cachedFields)) {
|
||||
return $this->cachedFields;
|
||||
}
|
||||
|
||||
$fields = $this->fields();
|
||||
|
||||
foreach (array_reverse(array_merge([static::class], class_parents($this))) as $class) {
|
||||
if (isset(static::$fieldModifiers[$class])) {
|
||||
foreach (static::$fieldModifiers[$class] as $modifier) {
|
||||
$fields = $modifier($fields, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->cachedFields = $fields;
|
||||
}
|
||||
|
||||
public function resolveSorts(): array
|
||||
{
|
||||
if (! is_null($this->cachedSorts)) {
|
||||
return $this->cachedSorts;
|
||||
}
|
||||
|
||||
$sorts = $this->sorts();
|
||||
|
||||
foreach (array_reverse(array_merge([static::class], class_parents($this))) as $class) {
|
||||
if (isset(static::$sortModifiers[$class])) {
|
||||
foreach (static::$sortModifiers[$class] as $modifier) {
|
||||
$sorts = $modifier($sorts, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->cachedSorts = $sorts;
|
||||
}
|
||||
}
|
29
framework/core/src/Api/Resource/Concerns/HasSortMap.php
Normal file
29
framework/core/src/Api/Resource/Concerns/HasSortMap.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?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\Api\Resource\Concerns;
|
||||
|
||||
use Flarum\Api\Sort\SortColumn;
|
||||
|
||||
trait HasSortMap
|
||||
{
|
||||
public function sortMap(): array
|
||||
{
|
||||
/** @var SortColumn[] $sorts */
|
||||
$sorts = $this->resolveSorts();
|
||||
|
||||
$map = [];
|
||||
|
||||
foreach ($sorts as $sort) {
|
||||
$map = array_merge($map, $sort->sortMap());
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
}
|
368
framework/core/src/Api/Resource/DiscussionResource.php
Normal file
368
framework/core/src/Api/Resource/DiscussionResource.php
Normal file
@@ -0,0 +1,368 @@
|
||||
<?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\Api\Resource;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Endpoint;
|
||||
use Flarum\Api\JsonApi;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Api\Sort\SortColumn;
|
||||
use Flarum\Bus\Dispatcher;
|
||||
use Flarum\Discussion\Command\ReadDiscussion;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Discussion\Event\Deleting;
|
||||
use Flarum\Discussion\Event\Saving;
|
||||
use Flarum\Discussion\Event\Started;
|
||||
use Flarum\Http\SlugManager;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Post\PostRepository;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
/**
|
||||
* @extends AbstractDatabaseResource<Discussion>
|
||||
*/
|
||||
class DiscussionResource extends AbstractDatabaseResource
|
||||
{
|
||||
public function __construct(
|
||||
protected Dispatcher $bus,
|
||||
protected SlugManager $slugManager,
|
||||
protected PostRepository $posts
|
||||
) {
|
||||
}
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return 'discussions';
|
||||
}
|
||||
|
||||
public function model(): string
|
||||
{
|
||||
return Discussion::class;
|
||||
}
|
||||
|
||||
public function scope(Builder $query, \Tobyz\JsonApiServer\Context $context): void
|
||||
{
|
||||
$query->whereVisibleTo($context->getActor());
|
||||
}
|
||||
|
||||
public function find(string $id, \Tobyz\JsonApiServer\Context $context): ?object
|
||||
{
|
||||
$actor = $context->getActor();
|
||||
|
||||
if (Arr::get($context->request->getQueryParams(), 'bySlug', false)) {
|
||||
$discussion = $this->slugManager->forResource(Discussion::class)->fromSlug($id, $actor);
|
||||
} else {
|
||||
$discussion = $this->query($context)->findOrFail($id);
|
||||
}
|
||||
|
||||
return $discussion;
|
||||
}
|
||||
|
||||
public function endpoints(): array
|
||||
{
|
||||
return [
|
||||
Endpoint\Create::make()
|
||||
->authenticated()
|
||||
->can('startDiscussion')
|
||||
->defaultInclude([
|
||||
'posts',
|
||||
'user',
|
||||
'lastPostedUser',
|
||||
'firstPost',
|
||||
'lastPost'
|
||||
]),
|
||||
Endpoint\Update::make()
|
||||
->authenticated(),
|
||||
Endpoint\Delete::make()
|
||||
->authenticated()
|
||||
->can('delete'),
|
||||
Endpoint\Show::make()
|
||||
->defaultInclude([
|
||||
'user',
|
||||
'posts',
|
||||
'posts.discussion',
|
||||
'posts.user',
|
||||
'posts.user.groups',
|
||||
'posts.editedUser',
|
||||
'posts.hiddenUser'
|
||||
]),
|
||||
Endpoint\Index::make()
|
||||
->defaultInclude([
|
||||
'user',
|
||||
'lastPostedUser',
|
||||
'mostRelevantPost',
|
||||
'mostRelevantPost.user'
|
||||
])
|
||||
->defaultSort('-lastPostedAt')
|
||||
->eagerLoad('state')
|
||||
->paginate(),
|
||||
];
|
||||
}
|
||||
|
||||
public function fields(): array
|
||||
{
|
||||
return [
|
||||
Schema\Str::make('title')
|
||||
->requiredOnCreate()
|
||||
->writable(function (Discussion $discussion, Context $context) {
|
||||
return $context->creating()
|
||||
|| $context->getActor()->can('rename', $discussion);
|
||||
})
|
||||
->minLength(3)
|
||||
->maxLength(80),
|
||||
Schema\Str::make('content')
|
||||
->writableOnCreate()
|
||||
->requiredOnCreate()
|
||||
->visible(false)
|
||||
->maxLength(63000)
|
||||
// set nothing...
|
||||
->set(fn () => null),
|
||||
Schema\Str::make('slug')
|
||||
->get(function (Discussion $discussion) {
|
||||
return $this->slugManager->forResource(Discussion::class)->toSlug($discussion);
|
||||
}),
|
||||
Schema\Integer::make('commentCount'),
|
||||
Schema\Integer::make('participantCount'),
|
||||
Schema\DateTime::make('createdAt'),
|
||||
Schema\DateTime::make('lastPostedAt'),
|
||||
Schema\Integer::make('lastPostNumber'),
|
||||
Schema\Boolean::make('canReply')
|
||||
->get(function (Discussion $discussion, Context $context) {
|
||||
return $context->getActor()->can('reply', $discussion);
|
||||
}),
|
||||
Schema\Boolean::make('canRename')
|
||||
->get(function (Discussion $discussion, Context $context) {
|
||||
return $context->getActor()->can('rename', $discussion);
|
||||
}),
|
||||
Schema\Boolean::make('canDelete')
|
||||
->get(function (Discussion $discussion, Context $context) {
|
||||
return $context->getActor()->can('delete', $discussion);
|
||||
}),
|
||||
Schema\Boolean::make('canHide')
|
||||
->get(function (Discussion $discussion, Context $context) {
|
||||
return $context->getActor()->can('hide', $discussion);
|
||||
}),
|
||||
Schema\Boolean::make('isHidden')
|
||||
->visible(fn (Discussion $discussion) => $discussion->hidden_at !== null)
|
||||
->writable(function (Discussion $discussion, Context $context) {
|
||||
return $context->updating()
|
||||
&& $context->getActor()->can('hide', $discussion);
|
||||
})
|
||||
->set(function (Discussion $discussion, bool $value, Context $context) {
|
||||
if ($value) {
|
||||
$discussion->hide($context->getActor());
|
||||
} else {
|
||||
$discussion->restore();
|
||||
}
|
||||
}),
|
||||
Schema\DateTime::make('hiddenAt')
|
||||
->visible(fn (Discussion $discussion) => $discussion->hidden_at !== null),
|
||||
Schema\DateTime::make('lastReadAt')
|
||||
->visible(fn (Discussion $discussion) => $discussion->state !== null)
|
||||
->get(function (Discussion $discussion) {
|
||||
return $discussion->state->last_read_at;
|
||||
}),
|
||||
Schema\Integer::make('lastReadPostNumber')
|
||||
->visible(fn (Discussion $discussion) => $discussion->state !== null)
|
||||
->get(function (Discussion $discussion) {
|
||||
return $discussion->state?->last_read_post_number;
|
||||
})
|
||||
->writable(function (Discussion $discussion, Context $context) {
|
||||
return $context->updating();
|
||||
})
|
||||
->set(function (Discussion $discussion, int $value, Context $context) {
|
||||
if ($readNumber = Arr::get($context->body(), 'data.attributes.lastReadPostNumber')) {
|
||||
$discussion->afterSave(function (Discussion $discussion) use ($readNumber, $context) {
|
||||
$this->bus->dispatch(
|
||||
new ReadDiscussion($discussion->id, $context->getActor(), $readNumber)
|
||||
);
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
Schema\Relationship\ToOne::make('user')
|
||||
->writableOnCreate()
|
||||
->includable(),
|
||||
Schema\Relationship\ToOne::make('firstPost')
|
||||
->includable()
|
||||
->inverse('discussion')
|
||||
->type('posts'),
|
||||
Schema\Relationship\ToOne::make('lastPostedUser')
|
||||
->includable()
|
||||
->type('users'),
|
||||
Schema\Relationship\ToOne::make('lastPost')
|
||||
->includable()
|
||||
->inverse('discussion')
|
||||
->type('posts'),
|
||||
Schema\Relationship\ToMany::make('posts')
|
||||
->withLinkage(function (Context $context) {
|
||||
return $context->showing(self::class);
|
||||
})
|
||||
->includable()
|
||||
->get(function (Discussion $discussion, Context $context) {
|
||||
$showingDiscussion = $context->showing(self::class);
|
||||
|
||||
if (! $showingDiscussion) {
|
||||
return fn () => $discussion->posts->all();
|
||||
}
|
||||
|
||||
/** @var Endpoint\Show $endpoint */
|
||||
$endpoint = $context->endpoint;
|
||||
|
||||
$actor = $context->getActor();
|
||||
|
||||
$limit = PostResource::$defaultLimit;
|
||||
|
||||
if (($near = Arr::get($context->request->getQueryParams(), 'page.near')) > 1) {
|
||||
$offset = $this->posts->getIndexForNumber($discussion->id, $near, $actor);
|
||||
$offset = max(0, $offset - $limit / 2);
|
||||
} else {
|
||||
$offset = $endpoint->extractOffsetValue($context, $endpoint->defaultExtracts($context));
|
||||
}
|
||||
|
||||
$posts = $discussion->posts()
|
||||
->whereVisibleTo($actor)
|
||||
->with($context->endpoint->getEagerLoadsFor('posts', $context))
|
||||
->with($context->endpoint->getWhereEagerLoadsFor('posts', $context))
|
||||
->orderBy('number')
|
||||
->skip($offset)
|
||||
->take($limit)
|
||||
->get();
|
||||
|
||||
/** @var Post $post */
|
||||
foreach ($posts as $post) {
|
||||
$post->setRelation('discussion', $discussion);
|
||||
}
|
||||
|
||||
$allPosts = $discussion->posts()->whereVisibleTo($actor)->orderBy('number')->pluck('id')->all();
|
||||
$loadedPosts = $posts->all();
|
||||
|
||||
array_splice($allPosts, $offset, $limit, $loadedPosts);
|
||||
|
||||
return $allPosts;
|
||||
}),
|
||||
Schema\Relationship\ToOne::make('mostRelevantPost')
|
||||
->visible(fn (Discussion $model, Context $context) => $context->listing())
|
||||
->includable()
|
||||
->inverse('discussion')
|
||||
->type('posts'),
|
||||
Schema\Relationship\ToOne::make('hideUser')
|
||||
->type('users'),
|
||||
];
|
||||
}
|
||||
|
||||
public function sorts(): array
|
||||
{
|
||||
return [
|
||||
SortColumn::make('lastPostedAt')
|
||||
->descendingAlias('latest'),
|
||||
SortColumn::make('commentCount')
|
||||
->descendingAlias('top'),
|
||||
SortColumn::make('createdAt')
|
||||
->ascendingAlias('oldest')
|
||||
->descendingAlias('newest'),
|
||||
];
|
||||
}
|
||||
|
||||
/** @param Discussion $model */
|
||||
public function creating(object $model, \Tobyz\JsonApiServer\Context $context): ?object
|
||||
{
|
||||
$actor = $context->getActor();
|
||||
|
||||
$model->created_at = Carbon::now();
|
||||
$model->user_id = $actor->id;
|
||||
|
||||
$model->setRelation('user', $actor);
|
||||
|
||||
$model->raise(new Started($model));
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
/** @param Discussion $model */
|
||||
public function created(object $model, \Tobyz\JsonApiServer\Context $context): ?object
|
||||
{
|
||||
$actor = $context->getActor();
|
||||
|
||||
if ($actor->exists) {
|
||||
$this->bus->dispatch(
|
||||
new ReadDiscussion($model->id, $actor, 1)
|
||||
);
|
||||
}
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
/** @param Discussion $model */
|
||||
protected function saveModel(Model $model, \Tobyz\JsonApiServer\Context $context): void
|
||||
{
|
||||
if ($context->creating()) {
|
||||
$model->newQuery()->getConnection()->transaction(function () use ($model, $context) {
|
||||
$model->save();
|
||||
|
||||
/** @var JsonApi $api */
|
||||
$api = $context->api;
|
||||
|
||||
// Now that the discussion has been created, we can add the first post.
|
||||
// We will do this by running the PostReply command.
|
||||
/** @var Post $post */
|
||||
$post = $api->forResource(PostResource::class)
|
||||
->forEndpoint('create')
|
||||
->withRequest($context->request)
|
||||
->process([
|
||||
'data' => [
|
||||
'attributes' => [
|
||||
'content' => Arr::get($context->body(), 'data.attributes.content'),
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => [
|
||||
'data' => [
|
||||
'type' => 'discussions',
|
||||
'id' => (string) $model->id,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
], ['isFirstPost' => true]);
|
||||
|
||||
// Before we dispatch events, refresh our discussion instance's
|
||||
// attributes as posting the reply will have changed some of them (e.g.
|
||||
// last_time.)
|
||||
$model->setRawAttributes($post->discussion->getAttributes(), true);
|
||||
$model->setFirstPost($post);
|
||||
$model->setLastPost($post);
|
||||
|
||||
$model->save();
|
||||
});
|
||||
}
|
||||
|
||||
parent::saveModel($model, $context);
|
||||
}
|
||||
|
||||
/** @param Discussion $model */
|
||||
public function deleting(object $model, \Tobyz\JsonApiServer\Context $context): void
|
||||
{
|
||||
$this->events->dispatch(
|
||||
new Deleting($model, $context->getActor(), [])
|
||||
);
|
||||
}
|
||||
|
||||
public function saving(object $model, \Tobyz\JsonApiServer\Context $context): ?object
|
||||
{
|
||||
$this->events->dispatch(
|
||||
new Saving($model, $context->getActor(), Arr::get($context->body(), 'data', []))
|
||||
);
|
||||
|
||||
return $model;
|
||||
}
|
||||
}
|
64
framework/core/src/Api/Resource/ExtensionReadmeResource.php
Normal file
64
framework/core/src/Api/Resource/ExtensionReadmeResource.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?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\Api\Resource;
|
||||
|
||||
use Flarum\Api\Endpoint;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Extension\Extension;
|
||||
use Flarum\Extension\ExtensionManager;
|
||||
use Tobyz\JsonApiServer\Context;
|
||||
use Tobyz\JsonApiServer\Resource\Findable;
|
||||
|
||||
/**
|
||||
* @todo: change to a simple ExtensionResource with readme field.
|
||||
*
|
||||
* @extends AbstractResource<Extension>
|
||||
*/
|
||||
class ExtensionReadmeResource extends AbstractResource implements Findable
|
||||
{
|
||||
public function __construct(
|
||||
protected ExtensionManager $extensions
|
||||
) {
|
||||
}
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return 'extension-readmes';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Extension $model
|
||||
*/
|
||||
public function getId(object $model, Context $context): string
|
||||
{
|
||||
return $model->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()),
|
||||
];
|
||||
}
|
||||
}
|
168
framework/core/src/Api/Resource/ForumResource.php
Normal file
168
framework/core/src/Api/Resource/ForumResource.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?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\Api\Resource;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Endpoint;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Foundation\Application;
|
||||
use Flarum\Foundation\Config;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Http\UrlGenerator;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Illuminate\Contracts\Filesystem\Cloud;
|
||||
use Illuminate\Contracts\Filesystem\Factory;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem;
|
||||
use stdClass;
|
||||
use Tobyz\JsonApiServer\Resource\Findable;
|
||||
|
||||
/**
|
||||
* @extends AbstractResource<stdClass>
|
||||
*/
|
||||
class ForumResource extends AbstractResource implements Findable
|
||||
{
|
||||
/**
|
||||
* @var Filesystem&Cloud
|
||||
*/
|
||||
protected Filesystem $assetsFilesystem;
|
||||
|
||||
public function __construct(
|
||||
protected UrlGenerator $url,
|
||||
protected SettingsRepositoryInterface $settings,
|
||||
protected Config $config,
|
||||
Factory $filesystemFactory
|
||||
) {
|
||||
$this->assetsFilesystem = $filesystemFactory->disk('flarum-assets');
|
||||
}
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return 'forums';
|
||||
}
|
||||
|
||||
public function getId(object $model, \Tobyz\JsonApiServer\Context $context): string
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
public function endpoints(): array
|
||||
{
|
||||
return [
|
||||
Endpoint\Show::make()
|
||||
->defaultInclude(['groups', 'actor.groups'])
|
||||
->route('GET', '/'),
|
||||
];
|
||||
}
|
||||
|
||||
public function fields(): array
|
||||
{
|
||||
$forumUrl = $this->url->to('forum')->base();
|
||||
$path = parse_url($forumUrl, PHP_URL_PATH) ?: '';
|
||||
|
||||
return [
|
||||
Schema\Str::make('title')
|
||||
->get(fn () => $this->settings->get('forum_title')),
|
||||
Schema\Str::make('description')
|
||||
->get(fn () => $this->settings->get('forum_description')),
|
||||
Schema\Boolean::make('showLanguageSelector')
|
||||
->get(fn () => $this->settings->get('show_language_selector', true)),
|
||||
Schema\Str::make('baseUrl')
|
||||
->get(fn () => $forumUrl),
|
||||
Schema\Str::make('basePath')
|
||||
->get(fn () => $path),
|
||||
Schema\Str::make('baseOrigin')
|
||||
->get(fn () => substr($forumUrl, 0, strlen($forumUrl) - strlen($path))),
|
||||
Schema\Str::make('debug')
|
||||
->get(fn () => $this->config->inDebugMode()),
|
||||
Schema\Str::make('apiUrl')
|
||||
->get(fn () => $this->url->to('api')->base()),
|
||||
Schema\Str::make('welcomeTitle')
|
||||
->get(fn () => $this->settings->get('welcome_title')),
|
||||
Schema\Str::make('welcomeMessage')
|
||||
->get(fn () => $this->settings->get('welcome_message')),
|
||||
Schema\Str::make('themePrimaryColor')
|
||||
->get(fn () => $this->settings->get('theme_primary_color')),
|
||||
Schema\Str::make('themeSecondaryColor')
|
||||
->get(fn () => $this->settings->get('theme_secondary_color')),
|
||||
Schema\Str::make('logoUrl')
|
||||
->get(fn () => $this->getLogoUrl()),
|
||||
Schema\Str::make('faviconUrl')
|
||||
->get(fn () => $this->getFaviconUrl()),
|
||||
Schema\Str::make('headerHtml')
|
||||
->get(fn () => $this->settings->get('custom_header')),
|
||||
Schema\Str::make('footerHtml')
|
||||
->get(fn () => $this->settings->get('custom_footer')),
|
||||
Schema\Boolean::make('allowSignUp')
|
||||
->get(fn () => $this->settings->get('allow_sign_up')),
|
||||
Schema\Str::make('defaultRoute')
|
||||
->get(fn () => $this->settings->get('default_route')),
|
||||
Schema\Boolean::make('canViewForum')
|
||||
->get(fn ($model, Context $context) => $context->getActor()->can('viewForum')),
|
||||
Schema\Boolean::make('canStartDiscussion')
|
||||
->get(fn ($model, Context $context) => $context->getActor()->can('startDiscussion')),
|
||||
Schema\Boolean::make('canSearchUsers')
|
||||
->get(fn ($model, Context $context) => $context->getActor()->can('searchUsers')),
|
||||
Schema\Boolean::make('canCreateAccessToken')
|
||||
->get(fn ($model, Context $context) => $context->getActor()->can('createAccessToken')),
|
||||
Schema\Boolean::make('moderateAccessTokens')
|
||||
->get(fn ($model, Context $context) => $context->getActor()->can('moderateAccessTokens')),
|
||||
Schema\Boolean::make('canEditUserCredentials')
|
||||
->get(fn ($model, Context $context) => $context->getActor()->hasPermission('user.editCredentials')),
|
||||
Schema\Str::make('assetsBaseUrl')
|
||||
->get(fn () => rtrim($this->assetsFilesystem->url(''), '/')),
|
||||
Schema\Str::make('jsChunksBaseUrl')
|
||||
->get(fn () => $this->assetsFilesystem->url('js')),
|
||||
|
||||
Schema\Str::make('adminUrl')
|
||||
->visible(fn ($model, Context $context) => $context->getActor()->can('administrate'))
|
||||
->get(fn () => $this->url->to('admin')->base()),
|
||||
Schema\Str::make('version')
|
||||
->visible(fn ($model, Context $context) => $context->getActor()->can('administrate'))
|
||||
->get(fn () => Application::VERSION),
|
||||
|
||||
Schema\Relationship\ToMany::make('groups')
|
||||
->includable()
|
||||
->get(fn ($model, Context $context) => Group::whereVisibleTo($context->getActor())->get()->all()),
|
||||
Schema\Relationship\ToOne::make('actor')
|
||||
->type('users')
|
||||
->includable()
|
||||
->get(fn ($model, Context $context) => $context->getActor()->isGuest() ? null : $context->getActor()),
|
||||
];
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
136
framework/core/src/Api/Resource/GroupResource.php
Normal file
136
framework/core/src/Api/Resource/GroupResource.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?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\Api\Resource;
|
||||
|
||||
use Flarum\Api\Endpoint;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Api\Sort\SortColumn;
|
||||
use Flarum\Group\Event\Deleting;
|
||||
use Flarum\Group\Event\Saving;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Locale\TranslatorInterface;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Arr;
|
||||
use Tobyz\JsonApiServer\Context;
|
||||
|
||||
/**
|
||||
* @extends AbstractDatabaseResource<Group>
|
||||
*/
|
||||
class GroupResource extends AbstractDatabaseResource
|
||||
{
|
||||
public function __construct(
|
||||
protected TranslatorInterface $translator
|
||||
) {
|
||||
}
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return 'groups';
|
||||
}
|
||||
|
||||
public function model(): string
|
||||
{
|
||||
return Group::class;
|
||||
}
|
||||
|
||||
public function scope(Builder $query, Context $context): void
|
||||
{
|
||||
$query->whereVisibleTo($context->getActor());
|
||||
}
|
||||
|
||||
public function endpoints(): array
|
||||
{
|
||||
return [
|
||||
Endpoint\Create::make()
|
||||
->authenticated()
|
||||
->can('createGroup'),
|
||||
Endpoint\Update::make()
|
||||
->authenticated()
|
||||
->can('edit'),
|
||||
Endpoint\Delete::make()
|
||||
->authenticated()
|
||||
->can('delete'),
|
||||
Endpoint\Show::make(),
|
||||
Endpoint\Index::make(),
|
||||
];
|
||||
}
|
||||
|
||||
public function fields(): array
|
||||
{
|
||||
return [
|
||||
Schema\Str::make('nameSingular')
|
||||
->requiredOnCreate()
|
||||
->get(function (Group $group) {
|
||||
return $this->translateGroupName($group->name_singular);
|
||||
})
|
||||
->set(function (Group $group, $value) {
|
||||
$group->rename($value, null);
|
||||
})
|
||||
->writable()
|
||||
->required(),
|
||||
Schema\Str::make('namePlural')
|
||||
->requiredOnCreate()
|
||||
->get(function (Group $group) {
|
||||
return $this->translateGroupName($group->name_plural);
|
||||
})
|
||||
->set(function (Group $group, $value) {
|
||||
$group->rename(null, $value);
|
||||
})
|
||||
->writable()
|
||||
->required(),
|
||||
Schema\Str::make('color')
|
||||
->nullable()
|
||||
->writable(),
|
||||
Schema\Str::make('icon')
|
||||
->nullable()
|
||||
->writable(),
|
||||
Schema\Boolean::make('isHidden')
|
||||
->writable(),
|
||||
];
|
||||
}
|
||||
|
||||
public function sorts(): array
|
||||
{
|
||||
return [
|
||||
SortColumn::make('nameSingular'),
|
||||
SortColumn::make('namePlural'),
|
||||
SortColumn::make('isHidden'),
|
||||
];
|
||||
}
|
||||
|
||||
private function translateGroupName(string $name): string
|
||||
{
|
||||
$translation = $this->translator->trans($key = 'core.group.'.strtolower($name));
|
||||
|
||||
if ($translation !== $key) {
|
||||
return $translation;
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
public function saving(object $model, Context $context): ?object
|
||||
{
|
||||
$this->events->dispatch(
|
||||
new Saving($model, $context->getActor(), Arr::get($context->body(), 'data', []))
|
||||
);
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
public function deleting(object $model, Context $context): void
|
||||
{
|
||||
$this->events->dispatch(
|
||||
new Deleting($model, $context->getActor(), [])
|
||||
);
|
||||
|
||||
parent::deleting($model, $context);
|
||||
}
|
||||
}
|
88
framework/core/src/Api/Resource/MailSettingResource.php
Normal file
88
framework/core/src/Api/Resource/MailSettingResource.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?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\Api\Resource;
|
||||
|
||||
use Flarum\Api\Endpoint;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Mail\DriverInterface;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Illuminate\Contracts\Validation\Factory;
|
||||
use stdClass;
|
||||
use Tobyz\JsonApiServer\Context;
|
||||
use Tobyz\JsonApiServer\Resource\Findable;
|
||||
|
||||
/**
|
||||
* @extends AbstractResource<object>
|
||||
*/
|
||||
class MailSettingResource extends AbstractResource implements Findable
|
||||
{
|
||||
public function __construct(
|
||||
protected SettingsRepositoryInterface $settings,
|
||||
protected Factory $validator,
|
||||
protected Container $container
|
||||
) {
|
||||
}
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return 'mail-settings';
|
||||
}
|
||||
|
||||
public function getId(object $model, Context $context): string
|
||||
{
|
||||
return '1';
|
||||
}
|
||||
|
||||
public function id(Context $context): ?string
|
||||
{
|
||||
return '1';
|
||||
}
|
||||
|
||||
public function find(string $id, Context $context): ?object
|
||||
{
|
||||
return new stdClass();
|
||||
}
|
||||
|
||||
public function endpoints(): array
|
||||
{
|
||||
return [
|
||||
Endpoint\Show::make()
|
||||
->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);
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
114
framework/core/src/Api/Resource/NotificationResource.php
Normal file
114
framework/core/src/Api/Resource/NotificationResource.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?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\Api\Resource;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Endpoint;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Bus\Dispatcher;
|
||||
use Flarum\Notification\Command\ReadNotification;
|
||||
use Flarum\Notification\Notification;
|
||||
use Flarum\Notification\NotificationRepository;
|
||||
use Tobyz\JsonApiServer\Pagination\OffsetPagination;
|
||||
|
||||
/**
|
||||
* @extends AbstractDatabaseResource<Notification>
|
||||
*/
|
||||
class NotificationResource extends AbstractDatabaseResource
|
||||
{
|
||||
protected bool $initialized = false;
|
||||
|
||||
public function __construct(
|
||||
protected Dispatcher $bus,
|
||||
protected NotificationRepository $notifications,
|
||||
) {
|
||||
$this->initialized = true;
|
||||
}
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return 'notifications';
|
||||
}
|
||||
|
||||
public function model(): string
|
||||
{
|
||||
return Notification::class;
|
||||
}
|
||||
|
||||
public function query(\Tobyz\JsonApiServer\Context $context): object
|
||||
{
|
||||
if ($context->listing(self::class)) {
|
||||
/** @var Endpoint\Index $endpoint */
|
||||
$endpoint = $context->endpoint;
|
||||
/** @var OffsetPagination $pagination */
|
||||
$pagination = ($endpoint->paginationResolver)($context);
|
||||
|
||||
return $this->notifications->query($context->getActor(), $pagination->limit, $pagination->offset);
|
||||
}
|
||||
|
||||
return parent::query($context);
|
||||
}
|
||||
|
||||
public function endpoints(): array
|
||||
{
|
||||
return [
|
||||
Endpoint\Update::make()
|
||||
->authenticated(),
|
||||
Endpoint\Index::make()
|
||||
->authenticated()
|
||||
->before(function (Context $context) {
|
||||
$context->getActor()->markNotificationsAsRead()->save();
|
||||
})
|
||||
->defaultInclude(array_filter([
|
||||
'fromUser',
|
||||
'subject',
|
||||
$this->initialized && count($this->subjectTypes()) > 1
|
||||
? 'subject.discussion'
|
||||
: null,
|
||||
]))
|
||||
->paginate(),
|
||||
];
|
||||
}
|
||||
|
||||
public function fields(): array
|
||||
{
|
||||
return [
|
||||
Schema\Str::make('contentType')
|
||||
->property('type'),
|
||||
Schema\Arr::make('content')
|
||||
->property('data'),
|
||||
Schema\DateTime::make('createdAt'),
|
||||
Schema\Boolean::make('isRead')
|
||||
->writable()
|
||||
->get(fn (Notification $notification) => (bool) $notification->read_at)
|
||||
->set(function (Notification $notification, bool $value, Context $context) {
|
||||
$this->bus->dispatch(
|
||||
new ReadNotification($notification->id, $context->getActor())
|
||||
);
|
||||
}),
|
||||
|
||||
Schema\Relationship\ToOne::make('user')
|
||||
->includable(),
|
||||
Schema\Relationship\ToOne::make('fromUser')
|
||||
->type('users')
|
||||
->includable(),
|
||||
Schema\Relationship\ToOne::make('subject')
|
||||
->collection($this->subjectTypes())
|
||||
->includable(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function subjectTypes(): array
|
||||
{
|
||||
return $this->api->typesForModels(
|
||||
(new Notification())->getSubjectModels()
|
||||
);
|
||||
}
|
||||
}
|
309
framework/core/src/Api/Resource/PostResource.php
Normal file
309
framework/core/src/Api/Resource/PostResource.php
Normal file
@@ -0,0 +1,309 @@
|
||||
<?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\Api\Resource;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Endpoint;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Api\Sort\SortColumn;
|
||||
use Flarum\Bus\Dispatcher;
|
||||
use Flarum\Discussion\Command\ReadDiscussion;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Foundation\ErrorHandling\LogReporter;
|
||||
use Flarum\Locale\TranslatorInterface;
|
||||
use Flarum\Post\CommentPost;
|
||||
use Flarum\Post\Event\Deleting;
|
||||
use Flarum\Post\Event\Saving;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Post\PostRepository;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Arr;
|
||||
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
||||
|
||||
/**
|
||||
* @extends AbstractDatabaseResource<Post>
|
||||
*/
|
||||
class PostResource extends AbstractDatabaseResource
|
||||
{
|
||||
public static int $defaultLimit = 20;
|
||||
|
||||
public function __construct(
|
||||
protected PostRepository $posts,
|
||||
protected TranslatorInterface $translator,
|
||||
protected LogReporter $log,
|
||||
protected Dispatcher $bus
|
||||
) {
|
||||
}
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return 'posts';
|
||||
}
|
||||
|
||||
public function model(): string
|
||||
{
|
||||
return Post::class;
|
||||
}
|
||||
|
||||
public function scope(Builder $query, \Tobyz\JsonApiServer\Context $context): void
|
||||
{
|
||||
$query->whereVisibleTo($context->getActor());
|
||||
}
|
||||
|
||||
public function newModel(\Tobyz\JsonApiServer\Context $context): object
|
||||
{
|
||||
if ($context->creating(self::class)) {
|
||||
$post = new CommentPost();
|
||||
|
||||
$post->user_id = $context->getActor()->id;
|
||||
$post->ip_address = $context->request->getAttribute('ipAddress');
|
||||
|
||||
return $post;
|
||||
}
|
||||
|
||||
return parent::newModel($context);
|
||||
}
|
||||
|
||||
public function endpoints(): array
|
||||
{
|
||||
return [
|
||||
Endpoint\Create::make()
|
||||
->authenticated()
|
||||
->visible(function (Context $context): bool {
|
||||
$discussionId = (int) Arr::get($context->body(), 'data.relationships.discussion.data.id');
|
||||
|
||||
// Make sure the user has permission to reply to this discussion. First,
|
||||
// make sure the discussion exists and that the user has permission to
|
||||
// view it; if not, fail with a ModelNotFound exception so we don't give
|
||||
// away the existence of the discussion. If the user is allowed to view
|
||||
// it, check if they have permission to reply.
|
||||
$discussion = Discussion::query()
|
||||
->whereVisibleTo($context->getActor())
|
||||
->findOrFail($discussionId);
|
||||
|
||||
// If this is the first post in the discussion, it's technically not a
|
||||
// "reply", so we won't check for that permission.
|
||||
if (! $context->internal('isFirstPost')) {
|
||||
return $context->getActor()->can('reply', $discussion);
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
->defaultInclude([
|
||||
'user',
|
||||
'discussion',
|
||||
'discussion.posts',
|
||||
'discussion.lastPostedUser'
|
||||
]),
|
||||
Endpoint\Update::make()
|
||||
->authenticated()
|
||||
->defaultInclude([
|
||||
'editedUser',
|
||||
'discussion'
|
||||
]),
|
||||
Endpoint\Delete::make()
|
||||
->authenticated()
|
||||
->can('delete'),
|
||||
Endpoint\Show::make()
|
||||
->defaultInclude([
|
||||
'user',
|
||||
'user.groups',
|
||||
'editedUser',
|
||||
'hiddenUser',
|
||||
'discussion'
|
||||
]),
|
||||
Endpoint\Index::make()
|
||||
->extractOffset(function (Context $context, array $defaultExtracts): int {
|
||||
$queryParams = $context->request->getQueryParams();
|
||||
|
||||
if (($near = Arr::get($queryParams, 'page.near')) > 1) {
|
||||
$sort = $defaultExtracts['sort'];
|
||||
$filter = $defaultExtracts['filter'];
|
||||
|
||||
if (count($filter) > 1 || ! isset($filter['discussion']) || $sort) {
|
||||
throw new BadRequestException(
|
||||
'You can only use page[near] with filter[discussion] and the default sort order'
|
||||
);
|
||||
}
|
||||
|
||||
$limit = $defaultExtracts['limit'];
|
||||
$offset = $this->posts->getIndexForNumber((int) $filter['discussion'], $near, $context->getActor());
|
||||
|
||||
return max(0, $offset - $limit / 2);
|
||||
}
|
||||
|
||||
return $defaultExtracts['offset'];
|
||||
})
|
||||
->defaultInclude([
|
||||
'user',
|
||||
'user.groups',
|
||||
'editedUser',
|
||||
'hiddenUser',
|
||||
'discussion'
|
||||
])
|
||||
->paginate(static::$defaultLimit),
|
||||
];
|
||||
}
|
||||
|
||||
public function fields(): array
|
||||
{
|
||||
return [
|
||||
Schema\Integer::make('number'),
|
||||
Schema\DateTime::make('createdAt')
|
||||
->writable(function (Post $post, Context $context) {
|
||||
return $context->creating()
|
||||
&& $context->getActor()->isAdmin();
|
||||
})
|
||||
->default(fn () => Carbon::now()),
|
||||
Schema\Str::make('contentType')
|
||||
->property('type'),
|
||||
|
||||
Schema\Str::make('content')
|
||||
->requiredOnCreate()
|
||||
->writable(function (Post $post, Context $context) {
|
||||
return $context->creating() || (
|
||||
$post instanceof CommentPost
|
||||
&& $context->updating()
|
||||
&& $context->getActor()->can('edit', $post)
|
||||
);
|
||||
})
|
||||
->maxLength(63000) // 65535 is without the text formatter XML generated after parsing. So we use 63000 to try being safer.
|
||||
->visible(function (Post $post, Context $context) {
|
||||
return ! ($post instanceof CommentPost)
|
||||
|| $context->getActor()->can('edit', $post);
|
||||
})
|
||||
->set(function (Post $post, string $value, Context $context) {
|
||||
if ($post instanceof CommentPost) {
|
||||
if ($context->creating()) {
|
||||
$post->setContentAttribute($value, $context->getActor());
|
||||
} elseif ($context->updating()) {
|
||||
$post->revise($value, $context->getActor());
|
||||
}
|
||||
}
|
||||
})
|
||||
->serialize(function (null|string|array $value, Context $context) {
|
||||
/**
|
||||
* Prevent the string type from trying to convert array content (for event posts) to a string.
|
||||
* @var Schema\Str $field
|
||||
*/
|
||||
$field = $context->field;
|
||||
$field->type = null;
|
||||
|
||||
return $value;
|
||||
}),
|
||||
Schema\Str::make('contentHtml')
|
||||
->visible(function (Post $post) {
|
||||
return $post instanceof CommentPost;
|
||||
})
|
||||
->get(function (CommentPost $post, Context $context) {
|
||||
try {
|
||||
$rendered = $post->formatContent($context->request);
|
||||
$post->setAttribute('renderFailed', false);
|
||||
} catch (\Exception $e) {
|
||||
$rendered = $this->translator->trans('core.lib.error.render_failed_message');
|
||||
$this->log->report($e);
|
||||
$post->setAttribute('renderFailed', true);
|
||||
}
|
||||
|
||||
return $rendered;
|
||||
}),
|
||||
Schema\Boolean::make('renderFailed')
|
||||
->visible(function (Post $post) {
|
||||
return $post instanceof CommentPost;
|
||||
}),
|
||||
|
||||
Schema\Str::make('ipAddress')
|
||||
->visible(function (Post $post, Context $context) {
|
||||
return $post instanceof CommentPost
|
||||
&& $context->getActor()->can('viewIps', $post);
|
||||
}),
|
||||
Schema\DateTime::make('editedAt'),
|
||||
Schema\Boolean::make('isHidden')
|
||||
->visible(fn (Post $post) => $post->hidden_at !== null)
|
||||
->writable(function (Post $post, Context $context) {
|
||||
return $context->updating()
|
||||
&& $context->getActor()->can('hide', $post);
|
||||
})
|
||||
->set(function (Post $post, bool $value, Context $context) {
|
||||
if ($post instanceof CommentPost) {
|
||||
if ($value) {
|
||||
$post->hide($context->getActor());
|
||||
} else {
|
||||
$post->restore();
|
||||
}
|
||||
}
|
||||
}),
|
||||
Schema\DateTime::make('hiddenAt')
|
||||
->visible(fn (Post $post) => $post->hidden_at !== null),
|
||||
|
||||
Schema\Boolean::make('canEdit')
|
||||
->get(fn (Post $post, Context $context) => $context->getActor()->can('edit', $post)),
|
||||
Schema\Boolean::make('canDelete')
|
||||
->get(fn (Post $post, Context $context) => $context->getActor()->can('delete', $post)),
|
||||
Schema\Boolean::make('canHide')
|
||||
->get(fn (Post $post, Context $context) => $context->getActor()->can('hide', $post)),
|
||||
|
||||
Schema\Relationship\ToOne::make('user')
|
||||
->includable(),
|
||||
Schema\Relationship\ToOne::make('discussion')
|
||||
->includable()
|
||||
->writableOnCreate(),
|
||||
Schema\Relationship\ToOne::make('editedUser')
|
||||
->type('users')
|
||||
->includable(),
|
||||
Schema\Relationship\ToOne::make('hiddenUser')
|
||||
->type('users')
|
||||
->includable(),
|
||||
];
|
||||
}
|
||||
|
||||
public function sorts(): array
|
||||
{
|
||||
return [
|
||||
SortColumn::make('number'),
|
||||
SortColumn::make('createdAt'),
|
||||
];
|
||||
}
|
||||
|
||||
/** @param Post $model */
|
||||
public function created(object $model, \Tobyz\JsonApiServer\Context $context): ?object
|
||||
{
|
||||
$actor = $context->getActor();
|
||||
|
||||
// After replying, we assume that the user has seen all of the posts
|
||||
// in the discussion; thus, we will mark the discussion as read if
|
||||
// they are logged in.
|
||||
if ($actor->exists) {
|
||||
$this->bus->dispatch(
|
||||
new ReadDiscussion($model->discussion_id, $actor, $model->number)
|
||||
);
|
||||
}
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
/** @param Post $model */
|
||||
public function deleting(object $model, \Tobyz\JsonApiServer\Context $context): void
|
||||
{
|
||||
$this->events->dispatch(
|
||||
new Deleting($model, $context->getActor(), [])
|
||||
);
|
||||
}
|
||||
|
||||
public function saving(object $model, \Tobyz\JsonApiServer\Context $context): ?object
|
||||
{
|
||||
$this->events->dispatch(
|
||||
new Saving($model, $context->getActor(), Arr::get($context->body(), 'data', []))
|
||||
);
|
||||
|
||||
return $model;
|
||||
}
|
||||
}
|
456
framework/core/src/Api/Resource/UserResource.php
Normal file
456
framework/core/src/Api/Resource/UserResource.php
Normal file
@@ -0,0 +1,456 @@
|
||||
<?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\Api\Resource;
|
||||
|
||||
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\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;
|
||||
use Flarum\User\Event\Saving;
|
||||
use Flarum\User\Exception\NotAuthenticatedException;
|
||||
use Flarum\User\RegistrationToken;
|
||||
use Flarum\User\User;
|
||||
use GuzzleHttp\Client;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Intervention\Image\ImageManager;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @extends AbstractDatabaseResource<User>
|
||||
*/
|
||||
class UserResource extends AbstractDatabaseResource
|
||||
{
|
||||
public function __construct(
|
||||
protected TranslatorInterface $translator,
|
||||
protected SlugManager $slugManager,
|
||||
protected SettingsRepositoryInterface $settings,
|
||||
protected ImageManager $imageManager,
|
||||
protected AvatarUploader $avatarUploader,
|
||||
protected Dispatcher $bus,
|
||||
) {
|
||||
}
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return 'users';
|
||||
}
|
||||
|
||||
public function model(): string
|
||||
{
|
||||
return User::class;
|
||||
}
|
||||
|
||||
public function scope(Builder $query, \Tobyz\JsonApiServer\Context $context): void
|
||||
{
|
||||
$query->whereVisibleTo($context->getActor());
|
||||
}
|
||||
|
||||
public function find(string $id, \Tobyz\JsonApiServer\Context $context): ?object
|
||||
{
|
||||
$actor = $context->getActor();
|
||||
|
||||
if (Arr::get($context->request->getQueryParams(), 'bySlug', false)) {
|
||||
$user = $this->slugManager->forResource(User::class)->fromSlug($id, $actor);
|
||||
} else {
|
||||
$user = $this->query($context)->findOrFail($id);
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function endpoints(): array
|
||||
{
|
||||
return [
|
||||
Endpoint\Create::make()
|
||||
->visible(function (Context $context) {
|
||||
if (! $this->settings->get('allow_sign_up')) {
|
||||
return $context->getActor()->isAdmin();
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
Endpoint\Update::make()
|
||||
->visible(function (User $user, Context $context) {
|
||||
$actor = $context->getActor();
|
||||
$body = $context->body();
|
||||
|
||||
// Require the user's current password if they are attempting to change
|
||||
// their own email address.
|
||||
|
||||
if (isset($body['data']['attributes']['email']) && $actor->id === $user->id) {
|
||||
$password = (string) Arr::get($body, 'meta.password');
|
||||
|
||||
if (! $actor->checkPassword($password)) {
|
||||
throw new NotAuthenticatedException;
|
||||
}
|
||||
}
|
||||
|
||||
$actor->assertRegistered();
|
||||
|
||||
return true;
|
||||
})
|
||||
->defaultInclude(['groups']),
|
||||
Endpoint\Delete::make()
|
||||
->authenticated()
|
||||
->can('delete'),
|
||||
Endpoint\Show::make()
|
||||
->defaultInclude(['groups']),
|
||||
Endpoint\Index::make()
|
||||
->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())
|
||||
);
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public function fields(): array
|
||||
{
|
||||
$translator = $this->translator;
|
||||
|
||||
return [
|
||||
Schema\Str::make('username')
|
||||
->requiredOnCreateWithout(['token'])
|
||||
->unique('users', 'username', true)
|
||||
->regex('/^[a-z0-9_-]+$/i')
|
||||
->validationMessages([
|
||||
'username.regex' => $translator->trans('core.api.invalid_username_message'),
|
||||
'username.required_without' => $translator->trans('validation.required', ['attribute' => $translator->trans('validation.attributes.username')])
|
||||
])
|
||||
->minLength(3)
|
||||
->maxLength(30)
|
||||
->writable(function (User $user, Context $context) {
|
||||
return $context->creating()
|
||||
|| $context->getActor()->can('editCredentials', $user);
|
||||
})
|
||||
->set(function (User $user, string $value) {
|
||||
if ($user->exists) {
|
||||
$user->rename($value);
|
||||
} else {
|
||||
$user->username = $value;
|
||||
}
|
||||
}),
|
||||
Schema\Str::make('email')
|
||||
->requiredOnCreateWithout(['token'])
|
||||
->validationMessages([
|
||||
'email.required_without' => $translator->trans('validation.required', ['attribute' => $translator->trans('validation.attributes.email')])
|
||||
])
|
||||
->email(['filter'])
|
||||
->unique('users', 'email', true)
|
||||
->visible(function (User $user, Context $context) {
|
||||
return $context->getActor()->can('editCredentials', $user)
|
||||
|| $context->getActor()->id === $user->id;
|
||||
})
|
||||
->writable(function (User $user, Context $context) {
|
||||
return $context->creating()
|
||||
|| $context->getActor()->can('editCredentials', $user)
|
||||
|| $context->getActor()->id === $user->id;
|
||||
})
|
||||
->set(function (User $user, string $value, Context $context) {
|
||||
if ($user->exists) {
|
||||
$isSelf = $context->getActor()->id === $user->id;
|
||||
|
||||
if ($isSelf) {
|
||||
$user->requestEmailChange($value);
|
||||
} else {
|
||||
$context->getActor()->assertCan('editCredentials', $user);
|
||||
$user->changeEmail($value);
|
||||
}
|
||||
} else {
|
||||
$user->email = $value;
|
||||
}
|
||||
}),
|
||||
Schema\Boolean::make('isEmailConfirmed')
|
||||
->visible(function (User $user, Context $context) {
|
||||
return $context->getActor()->can('editCredentials', $user)
|
||||
|| $context->getActor()->id === $user->id;
|
||||
})
|
||||
->writable(fn (User $user, Context $context) => $context->getActor()->isAdmin())
|
||||
->set(function (User $user, $value, Context $context) {
|
||||
if (! empty($value) && ($context->updating() || $context->getActor()->isAdmin())) {
|
||||
$user->activate();
|
||||
}
|
||||
}),
|
||||
Schema\Str::make('password')
|
||||
->requiredOnCreateWithout(['token'])
|
||||
->validationMessages([
|
||||
'password.required_without' => $translator->trans('validation.required', ['attribute' => $translator->trans('validation.attributes.password')])
|
||||
])
|
||||
->minLength(8)
|
||||
->visible(false)
|
||||
->writable(function (User $user, Context $context) {
|
||||
return $context->creating()
|
||||
|| $context->getActor()->can('editCredentials', $user);
|
||||
})
|
||||
->set(function (User $user, ?string $value) {
|
||||
$user->exists && $user->changePassword($value);
|
||||
}),
|
||||
// Registration token.
|
||||
Schema\Str::make('token')
|
||||
->visible(false)
|
||||
->writable(function (User $user, Context $context) {
|
||||
return $context->creating();
|
||||
})
|
||||
->set(function (User $user, ?string $value, Context $context) {
|
||||
if ($value) {
|
||||
/** @var RegistrationToken $token */
|
||||
$token = RegistrationToken::validOrFail($value);
|
||||
|
||||
$context->setParam('token', $token);
|
||||
$user->password ??= Str::random(20);
|
||||
|
||||
$this->applyToken($user, $token);
|
||||
}
|
||||
})
|
||||
->save(fn () => null),
|
||||
Schema\Str::make('displayName'),
|
||||
Schema\Str::make('avatarUrl'),
|
||||
Schema\Str::make('slug')
|
||||
->get(function (User $user) {
|
||||
return $this->slugManager->forResource(User::class)->toSlug($user);
|
||||
}),
|
||||
Schema\DateTime::make('joinTime')
|
||||
->property('joined_at'),
|
||||
Schema\Integer::make('discussionCount'),
|
||||
Schema\Integer::make('commentCount'),
|
||||
Schema\DateTime::make('lastSeenAt')
|
||||
->visible(function (User $user, Context $context) {
|
||||
return $user->getPreference('discloseOnline') || $context->getActor()->can('viewLastSeenAt', $user);
|
||||
}),
|
||||
|
||||
Schema\DateTime::make('markedAllAsReadAt')
|
||||
->visible(fn (User $user, Context $context) => ($context->collection instanceof self || $context->collection instanceof ForumResource) && $context->getActor()->id === $user->id)
|
||||
->writable(fn (User $user, Context $context) => $context->getActor()->id === $user->id)
|
||||
->set(function (User $user, $value) {
|
||||
if (! empty($value)) {
|
||||
$user->markAllAsRead();
|
||||
}
|
||||
}),
|
||||
|
||||
Schema\Integer::make('unreadNotificationCount')
|
||||
->visible(fn (User $user, Context $context) => ($context->collection instanceof self || $context->collection instanceof ForumResource) && $context->getActor()->id === $user->id)
|
||||
->get(function (User $user): int {
|
||||
return $user->getUnreadNotificationCount();
|
||||
}),
|
||||
Schema\Integer::make('newNotificationCount')
|
||||
->visible(fn (User $user, Context $context) => ($context->collection instanceof self || $context->collection instanceof ForumResource) && $context->getActor()->id === $user->id)
|
||||
->get(function (User $user): int {
|
||||
return $user->getNewNotificationCount();
|
||||
}),
|
||||
Schema\Arr::make('preferences')
|
||||
->visible(fn (User $user, Context $context) => ($context->collection instanceof self || $context->collection instanceof ForumResource) && $context->getActor()->id === $user->id)
|
||||
->writable(fn (User $user, Context $context) => $context->getActor()->id === $user->id)
|
||||
->set(function (User $user, array $value) {
|
||||
foreach ($value as $k => $v) {
|
||||
$user->setPreference($k, $v);
|
||||
}
|
||||
}),
|
||||
|
||||
Schema\Boolean::make('isAdmin')
|
||||
->visible(fn (User $user, Context $context) => ($context->collection instanceof self || $context->collection instanceof ForumResource) && $context->getActor()->id === $user->id)
|
||||
->get(fn (User $user, Context $context) => $context->getActor()->isAdmin()),
|
||||
|
||||
Schema\Boolean::make('canEdit')
|
||||
->get(function (User $user, Context $context) {
|
||||
return $context->getActor()->can('edit', $user);
|
||||
}),
|
||||
Schema\Boolean::make('canEditCredentials')
|
||||
->get(function (User $user, Context $context) {
|
||||
return $context->getActor()->can('editCredentials', $user);
|
||||
}),
|
||||
Schema\Boolean::make('canEditGroups')
|
||||
->get(function (User $user, Context $context) {
|
||||
return $context->getActor()->can('editGroups', $user);
|
||||
}),
|
||||
Schema\Boolean::make('canDelete')
|
||||
->get(function (User $user, Context $context) {
|
||||
return $context->getActor()->can('delete', $user);
|
||||
}),
|
||||
|
||||
Schema\Relationship\ToMany::make('groups')
|
||||
->writable(fn (User $user, Context $context) => $context->updating() && $context->getActor()->can('editGroups', $user))
|
||||
->includable()
|
||||
->set(function (User $user, $value, Context $context) {
|
||||
$actor = $context->getActor();
|
||||
|
||||
$oldGroups = $user->groups()->get()->all();
|
||||
$oldGroupIds = Arr::pluck($oldGroups, 'id');
|
||||
|
||||
$newGroupIds = [];
|
||||
foreach ($value as $group) {
|
||||
if ($id = Arr::get($group, 'id')) {
|
||||
$newGroupIds[] = $id;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure non-admins aren't adding/removing admins
|
||||
$adminChanged = in_array('1', array_diff($oldGroupIds, $newGroupIds)) || in_array('1', array_diff($newGroupIds, $oldGroupIds));
|
||||
$actor->assertPermission(! $adminChanged || $actor->isAdmin());
|
||||
|
||||
$user->raise(
|
||||
new GroupsChanged($user, $oldGroups)
|
||||
);
|
||||
|
||||
$user->afterSave(function (User $user) use ($newGroupIds) {
|
||||
$user->groups()->sync($newGroupIds);
|
||||
$user->unsetRelation('groups');
|
||||
});
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public function sorts(): array
|
||||
{
|
||||
return [
|
||||
SortColumn::make('username'),
|
||||
SortColumn::make('commentCount'),
|
||||
SortColumn::make('discussionCount'),
|
||||
SortColumn::make('lastSeenAt')
|
||||
->visible(function (Context $context) {
|
||||
return $context->getActor()->hasPermission('user.viewLastSeenAt');
|
||||
}),
|
||||
SortColumn::make('joinedAt'),
|
||||
];
|
||||
}
|
||||
|
||||
/** @param User $model */
|
||||
public function saved(object $model, \Tobyz\JsonApiServer\Context $context): ?object
|
||||
{
|
||||
if (($token = $context->getParam('token')) instanceof RegistrationToken) {
|
||||
$this->fulfillToken($model, $token);
|
||||
}
|
||||
|
||||
return parent::saved($model, $context);
|
||||
}
|
||||
|
||||
public function deleting(object $model, \Tobyz\JsonApiServer\Context $context): void
|
||||
{
|
||||
$this->events->dispatch(
|
||||
new Deleting($model, $context->getActor(), [])
|
||||
);
|
||||
}
|
||||
|
||||
public function saving(object $model, \Tobyz\JsonApiServer\Context $context): ?object
|
||||
{
|
||||
$this->events->dispatch(
|
||||
new Saving($model, $context->getActor(), Arr::get($context->body(), 'data', []))
|
||||
);
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
private function applyToken(User $user, RegistrationToken $token): void
|
||||
{
|
||||
foreach ($token->user_attributes as $k => $v) {
|
||||
if ($k === 'avatar_url') {
|
||||
$this->uploadAvatarFromUrl($user, $v);
|
||||
continue;
|
||||
}
|
||||
|
||||
$user->$k = $v;
|
||||
|
||||
if ($k === 'email') {
|
||||
$user->activate();
|
||||
}
|
||||
}
|
||||
|
||||
$this->events->dispatch(
|
||||
new RegisteringFromProvider($user, $token->provider, $token->payload)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function uploadAvatarFromUrl(User $user, string $url): void
|
||||
{
|
||||
$urlValidator = $this->validation->make(compact('url'), [
|
||||
'url' => 'required|active_url',
|
||||
]);
|
||||
|
||||
if ($urlValidator->fails()) {
|
||||
throw new ValidationException([
|
||||
'avatar_url' => 'Provided avatar URL must be a valid URI.',
|
||||
]);
|
||||
}
|
||||
|
||||
$scheme = parse_url($url, PHP_URL_SCHEME);
|
||||
|
||||
if (! in_array($scheme, ['http', 'https'])) {
|
||||
throw new ValidationException([
|
||||
'avatar_url' => "Provided avatar URL must have scheme http or https. Scheme provided was $scheme.",
|
||||
]);
|
||||
}
|
||||
|
||||
$urlContents = $this->retrieveAvatarFromUrl($url);
|
||||
|
||||
if ($urlContents !== null) {
|
||||
$image = $this->imageManager->read($urlContents);
|
||||
|
||||
$this->avatarUploader->upload($user, $image);
|
||||
}
|
||||
}
|
||||
|
||||
private function retrieveAvatarFromUrl(string $url): ?string
|
||||
{
|
||||
$client = new Client();
|
||||
|
||||
try {
|
||||
$response = $client->get($url);
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($response->getStatusCode() !== 200) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $response->getBody()->getContents();
|
||||
}
|
||||
|
||||
private function fulfillToken(User $user, RegistrationToken $token): void
|
||||
{
|
||||
$token->delete();
|
||||
|
||||
if ($token->provider && $token->identifier) {
|
||||
$user->loginProviders()->create([
|
||||
'provider' => $token->provider,
|
||||
'identifier' => $token->identifier
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
23
framework/core/src/Api/Schema/Arr.php
Normal file
23
framework/core/src/Api/Schema/Arr.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?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\Api\Schema;
|
||||
|
||||
/**
|
||||
* @todo validation rules for the array items.
|
||||
*/
|
||||
class Arr extends Attribute
|
||||
{
|
||||
public static function make(string $name): static
|
||||
{
|
||||
return (new static($name))
|
||||
->type(Type\Arr::make())
|
||||
->rule('array');
|
||||
}
|
||||
}
|
@@ -7,15 +7,11 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Group\Command;
|
||||
namespace Flarum\Api\Schema;
|
||||
|
||||
use Flarum\User\User;
|
||||
use Tobyz\JsonApiServer\Schema\Field\Attribute as BaseAttribute;
|
||||
|
||||
class CreateGroup
|
||||
class Attribute extends BaseAttribute
|
||||
{
|
||||
public function __construct(
|
||||
public User $actor,
|
||||
public array $data
|
||||
) {
|
||||
}
|
||||
//
|
||||
}
|
20
framework/core/src/Api/Schema/Boolean.php
Normal file
20
framework/core/src/Api/Schema/Boolean.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?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\Api\Schema;
|
||||
|
||||
class Boolean extends Attribute
|
||||
{
|
||||
public static function make(string $name): static
|
||||
{
|
||||
return (new static($name))
|
||||
->type(\Tobyz\JsonApiServer\Schema\Type\Boolean::make())
|
||||
->rule('boolean');
|
||||
}
|
||||
}
|
20
framework/core/src/Api/Schema/Date.php
Normal file
20
framework/core/src/Api/Schema/Date.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?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\Api\Schema;
|
||||
|
||||
class Date extends DateTime
|
||||
{
|
||||
public static function make(string $name): static
|
||||
{
|
||||
return (new static($name))
|
||||
->type(\Tobyz\JsonApiServer\Schema\Type\Date::make())
|
||||
->rule('date');
|
||||
}
|
||||
}
|
45
framework/core/src/Api/Schema/DateTime.php
Normal file
45
framework/core/src/Api/Schema/DateTime.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?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\Api\Schema;
|
||||
|
||||
class DateTime extends Attribute
|
||||
{
|
||||
public static function make(string $name): static
|
||||
{
|
||||
return (new static($name))
|
||||
->type(\Tobyz\JsonApiServer\Schema\Type\DateTime::make())
|
||||
->rule('date');
|
||||
}
|
||||
|
||||
public function before(string $date, bool|callable $condition = true): static
|
||||
{
|
||||
return $this->rule('before:'.$date, $condition);
|
||||
}
|
||||
|
||||
public function after(string $date, bool|callable $condition = true): static
|
||||
{
|
||||
return $this->rule('after:'.$date, $condition);
|
||||
}
|
||||
|
||||
public function beforeOrEqual(string $date, bool|callable $condition = true): static
|
||||
{
|
||||
return $this->rule('before_or_equal:'.$date, $condition);
|
||||
}
|
||||
|
||||
public function afterOrEqual(string $date, bool|callable $condition = true): static
|
||||
{
|
||||
return $this->rule('after_or_equal:'.$date, $condition);
|
||||
}
|
||||
|
||||
public function format(string $format, bool|callable $condition = true): static
|
||||
{
|
||||
return $this->rule('date_format:'.$format, $condition);
|
||||
}
|
||||
}
|
20
framework/core/src/Api/Schema/Integer.php
Normal file
20
framework/core/src/Api/Schema/Integer.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?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\Api\Schema;
|
||||
|
||||
class Integer extends Number
|
||||
{
|
||||
public static function make(string $name): static
|
||||
{
|
||||
return (new static($name))
|
||||
->type(\Tobyz\JsonApiServer\Schema\Type\Integer::make())
|
||||
->rule('integer');
|
||||
}
|
||||
}
|
35
framework/core/src/Api/Schema/Number.php
Normal file
35
framework/core/src/Api/Schema/Number.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?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\Api\Schema;
|
||||
|
||||
use Tobyz\JsonApiServer\Schema\Concerns\GetsRelationAggregates;
|
||||
use Tobyz\JsonApiServer\Schema\Contracts\RelationAggregator;
|
||||
|
||||
class Number extends Attribute implements RelationAggregator
|
||||
{
|
||||
use GetsRelationAggregates;
|
||||
|
||||
public static function make(string $name): static
|
||||
{
|
||||
return (new static($name))
|
||||
->type(\Tobyz\JsonApiServer\Schema\Type\Number::make())
|
||||
->rule('numeric');
|
||||
}
|
||||
|
||||
public function min(int $min, bool|callable $condition = true): static
|
||||
{
|
||||
return $this->rule("min:$min", $condition);
|
||||
}
|
||||
|
||||
public function max(int $max, bool|callable $condition = true): static
|
||||
{
|
||||
return $this->rule("max:$max", $condition);
|
||||
}
|
||||
}
|
@@ -7,15 +7,11 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\User\Command;
|
||||
namespace Flarum\Api\Schema\Relationship;
|
||||
|
||||
use Flarum\User\User;
|
||||
use Tobyz\JsonApiServer\Schema\Field\ToMany as BaseToMany;
|
||||
|
||||
class RegisterUser
|
||||
class ToMany extends BaseToMany
|
||||
{
|
||||
public function __construct(
|
||||
public User $actor,
|
||||
public array $data
|
||||
) {
|
||||
}
|
||||
//
|
||||
}
|
17
framework/core/src/Api/Schema/Relationship/ToOne.php
Normal file
17
framework/core/src/Api/Schema/Relationship/ToOne.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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\Api\Schema\Relationship;
|
||||
|
||||
use Tobyz\JsonApiServer\Schema\Field\ToOne as BaseToOne;
|
||||
|
||||
class ToOne extends BaseToOne
|
||||
{
|
||||
//
|
||||
}
|
46
framework/core/src/Api/Schema/Str.php
Normal file
46
framework/core/src/Api/Schema/Str.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?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\Api\Schema;
|
||||
|
||||
class Str extends Attribute
|
||||
{
|
||||
public static function make(string $name): static
|
||||
{
|
||||
return (new static($name))
|
||||
->type(\Tobyz\JsonApiServer\Schema\Type\Str::make())
|
||||
->rule('string');
|
||||
}
|
||||
|
||||
public function minLength(int $length, bool|callable $condition = true): static
|
||||
{
|
||||
return $this->rule('min:'.$length, $condition);
|
||||
}
|
||||
|
||||
public function maxLength(int $length, bool|callable $condition = true): static
|
||||
{
|
||||
return $this->rule('max:'.$length, $condition);
|
||||
}
|
||||
|
||||
public function email(array $validators = [], bool|callable $condition = true): static
|
||||
{
|
||||
$validators = implode(',', $validators);
|
||||
|
||||
if (! empty($validators)) {
|
||||
$validators = ':'.$validators;
|
||||
}
|
||||
|
||||
return $this->rule("email$validators", $condition);
|
||||
}
|
||||
|
||||
public function regex(string $pattern, bool|callable $condition = true): static
|
||||
{
|
||||
return $this->rule("regex:$pattern", $condition);
|
||||
}
|
||||
}
|
44
framework/core/src/Api/Schema/Type/Arr.php
Normal file
44
framework/core/src/Api/Schema/Type/Arr.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?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\Api\Schema\Type;
|
||||
|
||||
use Tobyz\JsonApiServer\Schema\Type\Type;
|
||||
|
||||
class Arr implements Type
|
||||
{
|
||||
public static function make(): static
|
||||
{
|
||||
return new static();
|
||||
}
|
||||
|
||||
public function serialize(mixed $value): array
|
||||
{
|
||||
return (array) $value;
|
||||
}
|
||||
|
||||
public function deserialize(mixed $value): array
|
||||
{
|
||||
return (array) $value;
|
||||
}
|
||||
|
||||
public function validate(mixed $value, callable $fail): void
|
||||
{
|
||||
if (! is_array($value)) {
|
||||
$fail('must be an array');
|
||||
}
|
||||
}
|
||||
|
||||
public function schema(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'array',
|
||||
];
|
||||
}
|
||||
}
|
@@ -1,234 +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\Api\Serializer;
|
||||
|
||||
use Closure;
|
||||
use DateTime;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Illuminate\Support\Arr;
|
||||
use InvalidArgumentException;
|
||||
use LogicException;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Tobscure\JsonApi\AbstractSerializer as BaseAbstractSerializer;
|
||||
use Tobscure\JsonApi\Collection;
|
||||
use Tobscure\JsonApi\Relationship;
|
||||
use Tobscure\JsonApi\Resource;
|
||||
use Tobscure\JsonApi\SerializerInterface;
|
||||
|
||||
abstract class AbstractSerializer extends BaseAbstractSerializer
|
||||
{
|
||||
protected Request $request;
|
||||
protected User $actor;
|
||||
protected static Container $container;
|
||||
|
||||
/**
|
||||
* @var array<string, callable[]>
|
||||
*/
|
||||
protected static array $attributeMutators = [];
|
||||
|
||||
/**
|
||||
* @var array<string, array<string, callable>>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
@@ -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\Api\Serializer;
|
||||
|
||||
use Flarum\Http\AccessToken;
|
||||
use Flarum\Locale\TranslatorInterface;
|
||||
use InvalidArgumentException;
|
||||
use Jenssegers\Agent\Agent;
|
||||
|
||||
class AccessTokenSerializer extends AbstractSerializer
|
||||
{
|
||||
protected $type = 'access-tokens';
|
||||
|
||||
public function __construct(
|
||||
protected TranslatorInterface $translator
|
||||
) {
|
||||
}
|
||||
|
||||
protected function getDefaultAttributes(object|array $model): array
|
||||
{
|
||||
if (! ($model instanceof AccessToken)) {
|
||||
throw new InvalidArgumentException(
|
||||
$this::class.' can only serialize instances of '.AccessToken::class
|
||||
);
|
||||
}
|
||||
|
||||
$session = $this->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;
|
||||
}
|
||||
}
|
@@ -1,77 +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\Api\Serializer;
|
||||
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Http\SlugManager;
|
||||
use InvalidArgumentException;
|
||||
use Tobscure\JsonApi\Relationship;
|
||||
|
||||
class BasicDiscussionSerializer extends AbstractSerializer
|
||||
{
|
||||
protected $type = 'discussions';
|
||||
|
||||
public function __construct(
|
||||
protected SlugManager $slugManager
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function getDefaultAttributes(object|array $model): array
|
||||
{
|
||||
if (! ($model instanceof Discussion)) {
|
||||
throw new InvalidArgumentException(
|
||||
$this::class.' can only serialize instances of '.Discussion::class
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'title' => $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);
|
||||
}
|
||||
}
|
@@ -1,72 +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\Api\Serializer;
|
||||
|
||||
use Exception;
|
||||
use Flarum\Foundation\ErrorHandling\LogReporter;
|
||||
use Flarum\Locale\TranslatorInterface;
|
||||
use Flarum\Post\CommentPost;
|
||||
use Flarum\Post\Post;
|
||||
use InvalidArgumentException;
|
||||
use Tobscure\JsonApi\Relationship;
|
||||
|
||||
class BasicPostSerializer extends AbstractSerializer
|
||||
{
|
||||
protected $type = 'posts';
|
||||
|
||||
public function __construct(
|
||||
protected LogReporter $log,
|
||||
protected TranslatorInterface $translator
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function getDefaultAttributes(object|array $model): array
|
||||
{
|
||||
if (! ($model instanceof Post)) {
|
||||
throw new InvalidArgumentException(
|
||||
$this::class.' can only serialize instances of '.Post::class
|
||||
);
|
||||
}
|
||||
|
||||
$attributes = [
|
||||
'number' => (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);
|
||||
}
|
||||
}
|
@@ -1,53 +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\Api\Serializer;
|
||||
|
||||
use Flarum\Http\SlugManager;
|
||||
use Flarum\User\User;
|
||||
use InvalidArgumentException;
|
||||
use Tobscure\JsonApi\Relationship;
|
||||
|
||||
class BasicUserSerializer extends AbstractSerializer
|
||||
{
|
||||
protected $type = 'users';
|
||||
|
||||
public function __construct(
|
||||
protected SlugManager $slugManager
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function getDefaultAttributes(object|array $model): array
|
||||
{
|
||||
if (! ($model instanceof User)) {
|
||||
throw new InvalidArgumentException(
|
||||
$this::class.' can only serialize instances of '.User::class
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'username' => $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');
|
||||
}
|
||||
}
|
@@ -1,39 +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\Api\Serializer;
|
||||
|
||||
use Flarum\User\User;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class CurrentUserSerializer extends UserSerializer
|
||||
{
|
||||
protected function getDefaultAttributes(object|array $model): array
|
||||
{
|
||||
if (! ($model instanceof User)) {
|
||||
throw new InvalidArgumentException(
|
||||
$this::class.' can only serialize instances of '.User::class
|
||||
);
|
||||
}
|
||||
|
||||
$attributes = parent::getDefaultAttributes($model);
|
||||
|
||||
$attributes += [
|
||||
'isEmailConfirmed' => (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;
|
||||
}
|
||||
}
|
@@ -1,49 +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\Api\Serializer;
|
||||
|
||||
use Flarum\Discussion\Discussion;
|
||||
|
||||
class DiscussionSerializer extends BasicDiscussionSerializer
|
||||
{
|
||||
/**
|
||||
* @param Discussion $model
|
||||
*/
|
||||
protected function getDefaultAttributes(object|array $model): array
|
||||
{
|
||||
$attributes = parent::getDefaultAttributes($model) + [
|
||||
'commentCount' => (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;
|
||||
}
|
||||
}
|
@@ -1,35 +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\Api\Serializer;
|
||||
|
||||
use Flarum\Extension\Extension;
|
||||
|
||||
class ExtensionReadmeSerializer extends AbstractSerializer
|
||||
{
|
||||
/**
|
||||
* @param Extension $model
|
||||
*/
|
||||
protected function getDefaultAttributes(object|array $model): array
|
||||
{
|
||||
return [
|
||||
'content' => $model->getReadme()
|
||||
];
|
||||
}
|
||||
|
||||
public function getId($extension)
|
||||
{
|
||||
return $extension->getId();
|
||||
}
|
||||
|
||||
public function getType($extension)
|
||||
{
|
||||
return 'extension-readmes';
|
||||
}
|
||||
}
|
@@ -1,133 +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\Api\Serializer;
|
||||
|
||||
use Flarum\Foundation\Application;
|
||||
use Flarum\Foundation\Config;
|
||||
use Flarum\Http\UrlGenerator;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Illuminate\Contracts\Filesystem\Cloud;
|
||||
use Illuminate\Contracts\Filesystem\Factory;
|
||||
use Tobscure\JsonApi\Relationship;
|
||||
|
||||
class ForumSerializer extends AbstractSerializer
|
||||
{
|
||||
protected $type = 'forums';
|
||||
|
||||
/**
|
||||
* @var Config
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* @var SettingsRepositoryInterface
|
||||
*/
|
||||
protected $settings;
|
||||
|
||||
/**
|
||||
* @var UrlGenerator
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* @var Cloud
|
||||
*/
|
||||
protected $assetsFilesystem;
|
||||
|
||||
/**
|
||||
* @param Config $config
|
||||
* @param Factory $filesystemFactory
|
||||
* @param SettingsRepositoryInterface $settings
|
||||
* @param UrlGenerator $url
|
||||
*/
|
||||
public function __construct(Config $config, Factory $filesystemFactory, SettingsRepositoryInterface $settings, UrlGenerator $url)
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
@@ -1,55 +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\Api\Serializer;
|
||||
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Locale\TranslatorInterface;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class GroupSerializer extends AbstractSerializer
|
||||
{
|
||||
protected $type = 'groups';
|
||||
|
||||
public function __construct(
|
||||
protected TranslatorInterface $translator
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function getDefaultAttributes(object|array $model): array
|
||||
{
|
||||
if (! ($model instanceof Group)) {
|
||||
throw new InvalidArgumentException(
|
||||
$this::class.' can only serialize instances of '.Group::class
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'nameSingular' => $this->translateGroupName($model->name_singular),
|
||||
'namePlural' => $this->translateGroupName($model->name_plural),
|
||||
'color' => $model->color,
|
||||
'icon' => $model->icon,
|
||||
'isHidden' => $model->is_hidden
|
||||
];
|
||||
}
|
||||
|
||||
private function translateGroupName(string $name): string
|
||||
{
|
||||
$translation = $this->translator->trans($key = 'core.group.'.strtolower($name));
|
||||
|
||||
if ($translation !== $key) {
|
||||
return $translation;
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
}
|
@@ -1,40 +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\Api\Serializer;
|
||||
|
||||
use Flarum\Mail\DriverInterface;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class MailSettingsSerializer extends AbstractSerializer
|
||||
{
|
||||
protected $type = 'mail-settings';
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function getDefaultAttributes(object|array $model): array
|
||||
{
|
||||
return [
|
||||
'fields' => 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';
|
||||
}
|
||||
}
|
@@ -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\Api\Serializer;
|
||||
|
||||
use Flarum\Notification\Notification;
|
||||
use InvalidArgumentException;
|
||||
use Tobscure\JsonApi\Relationship;
|
||||
|
||||
class NotificationSerializer extends AbstractSerializer
|
||||
{
|
||||
protected $type = 'notifications';
|
||||
|
||||
/**
|
||||
* A map of notification types (key) to the serializer that should be used
|
||||
* to output the notification's subject (value).
|
||||
*/
|
||||
protected static array $subjectSerializers = [];
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function getDefaultAttributes(object|array $model): array
|
||||
{
|
||||
if (! ($model instanceof Notification)) {
|
||||
throw new InvalidArgumentException(
|
||||
$this::class.' can only serialize instances of '.Notification::class
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'contentType' => $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;
|
||||
}
|
||||
}
|
@@ -1,77 +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\Api\Serializer;
|
||||
|
||||
use Flarum\Post\CommentPost;
|
||||
use Flarum\Post\Post;
|
||||
use Tobscure\JsonApi\Relationship;
|
||||
|
||||
class PostSerializer extends BasicPostSerializer
|
||||
{
|
||||
/**
|
||||
* @param Post $model
|
||||
*/
|
||||
protected function getDefaultAttributes(object|array $model): array
|
||||
{
|
||||
$attributes = parent::getDefaultAttributes($model);
|
||||
|
||||
unset($attributes['content']);
|
||||
|
||||
$canEdit = $this->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);
|
||||
}
|
||||
}
|
@@ -1,48 +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\Api\Serializer;
|
||||
|
||||
use Flarum\User\User;
|
||||
|
||||
class UserSerializer extends BasicUserSerializer
|
||||
{
|
||||
/**
|
||||
* @param User $model
|
||||
*/
|
||||
protected function getDefaultAttributes(object|array $model): array
|
||||
{
|
||||
$attributes = parent::getDefaultAttributes($model);
|
||||
|
||||
$attributes += [
|
||||
'joinTime' => $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;
|
||||
}
|
||||
}
|
48
framework/core/src/Api/Sort/SortColumn.php
Normal file
48
framework/core/src/Api/Sort/SortColumn.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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\Api\Sort;
|
||||
|
||||
use Tobyz\JsonApiServer\Laravel\Sort\SortColumn as BaseSortColumn;
|
||||
|
||||
class SortColumn extends BaseSortColumn
|
||||
{
|
||||
protected array $alias = [
|
||||
'asc' => null,
|
||||
'desc' => null,
|
||||
];
|
||||
|
||||
public function ascendingAlias(?string $alias): static
|
||||
{
|
||||
$this->alias['asc'] = $alias;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function descendingAlias(?string $alias): static
|
||||
{
|
||||
$this->alias['desc'] = $alias;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function sortMap(): array
|
||||
{
|
||||
$map = [];
|
||||
|
||||
foreach ($this->alias as $direction => $alias) {
|
||||
if ($alias) {
|
||||
$sort = ($direction === 'asc' ? '' : '-').$this->name;
|
||||
$map[$alias] = $sort;
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
}
|
@@ -19,27 +19,6 @@ return function (RouteCollection $map, RouteHandlerFactory $route) {
|
||||
$route->toController(Controller\ShowForumController::class)
|
||||
);
|
||||
|
||||
// List access tokens
|
||||
$map->get(
|
||||
'/access-tokens',
|
||||
'access-tokens.index',
|
||||
$route->toController(Controller\ListAccessTokensController::class)
|
||||
);
|
||||
|
||||
// Create access token
|
||||
$map->post(
|
||||
'/access-tokens',
|
||||
'access-tokens.create',
|
||||
$route->toController(Controller\CreateAccessTokenController::class)
|
||||
);
|
||||
|
||||
// Delete access token
|
||||
$map->delete(
|
||||
'/access-tokens/{id}',
|
||||
'access-tokens.delete',
|
||||
$route->toController(Controller\DeleteAccessTokenController::class)
|
||||
);
|
||||
|
||||
// Create authentication token
|
||||
$map->post(
|
||||
'/token',
|
||||
@@ -67,55 +46,6 @@ return function (RouteCollection $map, RouteHandlerFactory $route) {
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// List users
|
||||
$map->get(
|
||||
'/users',
|
||||
'users.index',
|
||||
$route->toController(Controller\ListUsersController::class)
|
||||
);
|
||||
|
||||
// Register a user
|
||||
$map->post(
|
||||
'/users',
|
||||
'users.create',
|
||||
$route->toController(Controller\CreateUserController::class)
|
||||
);
|
||||
|
||||
// Get a single user
|
||||
$map->get(
|
||||
'/users/{id}',
|
||||
'users.show',
|
||||
$route->toController(Controller\ShowUserController::class)
|
||||
);
|
||||
|
||||
// Edit a user
|
||||
$map->patch(
|
||||
'/users/{id}',
|
||||
'users.update',
|
||||
$route->toController(Controller\UpdateUserController::class)
|
||||
);
|
||||
|
||||
// Delete a user
|
||||
$map->delete(
|
||||
'/users/{id}',
|
||||
'users.delete',
|
||||
$route->toController(Controller\DeleteUserController::class)
|
||||
);
|
||||
|
||||
// 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',
|
||||
@@ -129,13 +59,6 @@ return function (RouteCollection $map, RouteHandlerFactory $route) {
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// List notifications for the current user
|
||||
$map->get(
|
||||
'/notifications',
|
||||
'notifications.index',
|
||||
$route->toController(Controller\ListNotificationsController::class)
|
||||
);
|
||||
|
||||
// Mark all notifications as read
|
||||
$map->post(
|
||||
'/notifications/read',
|
||||
@@ -143,13 +66,6 @@ return function (RouteCollection $map, RouteHandlerFactory $route) {
|
||||
$route->toController(Controller\ReadAllNotificationsController::class)
|
||||
);
|
||||
|
||||
// Mark a single notification as read
|
||||
$map->patch(
|
||||
'/notifications/{id}',
|
||||
'notifications.update',
|
||||
$route->toController(Controller\UpdateNotificationController::class)
|
||||
);
|
||||
|
||||
// Delete all notifications for the current user.
|
||||
$map->delete(
|
||||
'/notifications',
|
||||
@@ -157,129 +73,6 @@ return function (RouteCollection $map, RouteHandlerFactory $route) {
|
||||
$route->toController(Controller\DeleteAllNotificationsController::class)
|
||||
);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Discussions
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// List discussions
|
||||
$map->get(
|
||||
'/discussions',
|
||||
'discussions.index',
|
||||
$route->toController(Controller\ListDiscussionsController::class)
|
||||
);
|
||||
|
||||
// Create a discussion
|
||||
$map->post(
|
||||
'/discussions',
|
||||
'discussions.create',
|
||||
$route->toController(Controller\CreateDiscussionController::class)
|
||||
);
|
||||
|
||||
// Show a single discussion
|
||||
$map->get(
|
||||
'/discussions/{id}',
|
||||
'discussions.show',
|
||||
$route->toController(Controller\ShowDiscussionController::class)
|
||||
);
|
||||
|
||||
// Edit a discussion
|
||||
$map->patch(
|
||||
'/discussions/{id}',
|
||||
'discussions.update',
|
||||
$route->toController(Controller\UpdateDiscussionController::class)
|
||||
);
|
||||
|
||||
// Delete a discussion
|
||||
$map->delete(
|
||||
'/discussions/{id}',
|
||||
'discussions.delete',
|
||||
$route->toController(Controller\DeleteDiscussionController::class)
|
||||
);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Posts
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// List posts, usually for a discussion
|
||||
$map->get(
|
||||
'/posts',
|
||||
'posts.index',
|
||||
$route->toController(Controller\ListPostsController::class)
|
||||
);
|
||||
|
||||
// Create a post
|
||||
$map->post(
|
||||
'/posts',
|
||||
'posts.create',
|
||||
$route->toController(Controller\CreatePostController::class)
|
||||
);
|
||||
|
||||
// Show a single or multiple posts by ID
|
||||
$map->get(
|
||||
'/posts/{id}',
|
||||
'posts.show',
|
||||
$route->toController(Controller\ShowPostController::class)
|
||||
);
|
||||
|
||||
// Edit a post
|
||||
$map->patch(
|
||||
'/posts/{id}',
|
||||
'posts.update',
|
||||
$route->toController(Controller\UpdatePostController::class)
|
||||
);
|
||||
|
||||
// Delete a post
|
||||
$map->delete(
|
||||
'/posts/{id}',
|
||||
'posts.delete',
|
||||
$route->toController(Controller\DeletePostController::class)
|
||||
);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Groups
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// List groups
|
||||
$map->get(
|
||||
'/groups',
|
||||
'groups.index',
|
||||
$route->toController(Controller\ListGroupsController::class)
|
||||
);
|
||||
|
||||
// Create a group
|
||||
$map->post(
|
||||
'/groups',
|
||||
'groups.create',
|
||||
$route->toController(Controller\CreateGroupController::class)
|
||||
);
|
||||
|
||||
// Show a single group
|
||||
$map->get(
|
||||
'/groups/{id}',
|
||||
'groups.show',
|
||||
$route->toController(Controller\ShowGroupController::class)
|
||||
);
|
||||
|
||||
// Edit a group
|
||||
$map->patch(
|
||||
'/groups/{id}',
|
||||
'groups.update',
|
||||
$route->toController(Controller\UpdateGroupController::class)
|
||||
);
|
||||
|
||||
// Delete a group
|
||||
$map->delete(
|
||||
'/groups/{id}',
|
||||
'groups.delete',
|
||||
$route->toController(Controller\DeleteGroupController::class)
|
||||
);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Administration
|
||||
@@ -300,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)
|
||||
);
|
||||
|
||||
// Extension bisect
|
||||
$map->post(
|
||||
'/extension-bisect',
|
||||
|
@@ -11,10 +11,8 @@ namespace Flarum\Database;
|
||||
|
||||
use Flarum\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model as Eloquent;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use LogicException;
|
||||
|
||||
/**
|
||||
* Base model class, building on Eloquent.
|
||||
@@ -46,11 +44,6 @@ abstract class AbstractModel extends Eloquent
|
||||
*/
|
||||
protected array $afterDeleteCallbacks = [];
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static array $customRelations = [];
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@@ -118,47 +111,6 @@ abstract class AbstractModel extends Eloquent
|
||||
return $casts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an attribute from the model. If nothing is found, attempt to load
|
||||
* a custom relation method with this key.
|
||||
*/
|
||||
public function getAttribute($key)
|
||||
{
|
||||
if (! is_null($value = parent::getAttribute($key))) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// If a custom relation with this key has been set up, then we will load
|
||||
// and return results from the query and hydrate the relationship's
|
||||
// value on the "relationships" array.
|
||||
if (! $this->relationLoaded($key) && ($relation = $this->getCustomRelation($key))) {
|
||||
if (! $relation instanceof Relation) {
|
||||
throw new LogicException(
|
||||
'Relationship method must return an object of type '.Relation::class
|
||||
);
|
||||
}
|
||||
|
||||
return $this->relations[$key] = $relation->getResults();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a custom relation object.
|
||||
*/
|
||||
protected function getCustomRelation(string $name): mixed
|
||||
{
|
||||
foreach (array_merge([static::class], class_parents($this)) as $class) {
|
||||
$relation = Arr::get(static::$customRelations, $class.".$name");
|
||||
if (! is_null($relation)) {
|
||||
return $relation($this);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback to be run once after the model is saved.
|
||||
*/
|
||||
@@ -199,15 +151,6 @@ abstract class AbstractModel extends Eloquent
|
||||
return $callbacks;
|
||||
}
|
||||
|
||||
public function __call($method, $parameters)
|
||||
{
|
||||
if ($relation = $this->getCustomRelation($method)) {
|
||||
return $relation;
|
||||
}
|
||||
|
||||
return parent::__call($method, $parameters);
|
||||
}
|
||||
|
||||
public function newModelQuery()
|
||||
{
|
||||
$query = parent::newModelQuery();
|
||||
|
@@ -1,22 +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\Discussion\Command;
|
||||
|
||||
use Flarum\User\User;
|
||||
|
||||
class DeleteDiscussion
|
||||
{
|
||||
public function __construct(
|
||||
public int $discussionId,
|
||||
public User $actor,
|
||||
public array $data = []
|
||||
) {
|
||||
}
|
||||
}
|
@@ -1,46 +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\Discussion\Command;
|
||||
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Discussion\DiscussionRepository;
|
||||
use Flarum\Discussion\Event\Deleting;
|
||||
use Flarum\Foundation\DispatchEventsTrait;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
|
||||
class DeleteDiscussionHandler
|
||||
{
|
||||
use DispatchEventsTrait;
|
||||
|
||||
public function __construct(
|
||||
protected Dispatcher $events,
|
||||
protected DiscussionRepository $discussions
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(DeleteDiscussion $command): Discussion
|
||||
{
|
||||
$actor = $command->actor;
|
||||
|
||||
$discussion = $this->discussions->findOrFail($command->discussionId, $actor);
|
||||
|
||||
$actor->assertCan('delete', $discussion);
|
||||
|
||||
$this->events->dispatch(
|
||||
new Deleting($discussion, $actor, $command->data)
|
||||
);
|
||||
|
||||
$discussion->delete();
|
||||
|
||||
$this->dispatchEventsFor($discussion, $actor);
|
||||
|
||||
return $discussion;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user