5 use CuyZ\Valinor\Mapper\MappingError
;
6 use FastRoute\Dispatcher\Result\MethodNotAllowed
;
7 use FastRoute\Dispatcher\Result\NotMatched
;
8 use FastRoute\RouteCollector
;
9 use Laminas\Diactoros\Response\JsonResponse
;
10 use Psr\Http\Message\ServerRequestInterface
;
11 use Psr\Http\Message\ResponseInterface
;
12 use Psr\Http\Server\RequestHandlerInterface
;
13 use wcf\http\attribute\AllowHttpMethod
;
14 use wcf\system\endpoint\event\ControllerCollecting
;
15 use wcf\system\endpoint\IController
;
16 use wcf\system\endpoint\RequestFailure
;
17 use wcf\system\endpoint\RequestType
;
18 use wcf\system\event\EventHandler
;
19 use wcf\system\exception\PermissionDeniedException
;
20 use wcf\system\exception\UserInputException
;
21 use wcf\system\request\RouteHandler
;
23 use function FastRoute\simpleDispatcher
;
25 #[AllowHttpMethod('DELETE')]
26 final class ApiAction
implements RequestHandlerInterface
29 public function handle(ServerRequestInterface
$request): ResponseInterface
31 $isSupportedVerb = match ($request->getMethod()) {
32 'DELETE', 'GET', 'POST' => true,
36 if (!$isSupportedVerb) {
37 return $this->toErrorResponse(RequestFailure
::MethodNotAllowed
, 'unacceptable_method');
40 $endpoint = $this->getEndpointFromPathInfo(RouteHandler
::getPathInfo());
41 if ($endpoint === null) {
42 return $this->toErrorResponse(RequestFailure
::UnknownEndpoint
, 'missing_endpoint');
45 // TODO: This is currently very inefficient and should be cached in some
46 // way, maybe even use a combined cache for both?
47 $event = new ControllerCollecting();
48 EventHandler
::getInstance()->fire($event);
50 $dispatcher = simpleDispatcher(
51 static function (RouteCollector
$r) use ($event) {
52 foreach ($event->getControllers() as $controller) {
53 $reflectionClass = new \
ReflectionClass($controller);
54 $attribute = current($reflectionClass->getAttributes(RequestType
::class, \ReflectionAttribute
::IS_INSTANCEOF
));
55 \assert
($attribute !== false);
57 $apiController = $attribute->newInstance();
59 $r->addRoute($apiController->method
->toString(), $apiController->uri
, $controller);
64 'cacheDisabled' => true,
68 $result = $dispatcher->dispatch($request->getMethod(), $endpoint);
70 if ($result instanceof NotMatched
) {
71 return $this->toErrorResponse(RequestFailure
::UnknownEndpoint
, 'unknown_endpoint');
74 if ($result instanceof MethodNotAllowed
) {
75 return $this->toErrorResponse(RequestFailure
::MethodNotAllowed
, 'endpoint_does_not_allow_method');
78 /** @var IController */
79 $controller = $result->handler
;
82 return $controller($request, $result->variables
);
83 } catch (MappingError
$e) {
84 return $this->toErrorResponse(RequestFailure
::ValidationFailed
, 'mapping_error', $e->getMessage());
85 } catch (PermissionDeniedException
) {
86 return $this->toErrorResponse(RequestFailure
::PermissionDenied
, 'permission_denied');
87 } catch (UserInputException
$e) {
88 return $this->toErrorResponse(RequestFailure
::ValidationFailed
, $e->getType(), $e->getMessage(), $e->getField());
89 } catch (\Throwable
$e) {
90 return $this->toErrorResponse(RequestFailure
::InternalError
, 'unknown_exception', $e->getMessage());
94 private function getEndpointFromPathInfo(string $pathInfo): ?
string
96 if (!\
str_starts_with($pathInfo, 'api/rpc/')) {
100 $endpoint = \
mb_substr($pathInfo, \
strlen('api/rpc/') - 1);
102 // The namespace and the primary object are always required.
103 if (\
substr_count($endpoint, '/') < 2 || \
str_ends_with($endpoint, '/')) {
110 private function toErrorResponse(
111 RequestFailure
$reason,
113 string $message = '',
115 ): ResponseInterface
{
116 return new JsonResponse([
117 'type' => $reason->toString(),
119 'message' => $message,
121 ], $reason->toStatusCode());