. // }}} /** * Base class for controllers * * @package GNUsocial * @category Controller * * @author Hugo Sales * @author Diogo Peralta Cordeiro <@diogo.site> * @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later */ namespace App\Core; use function App\Core\I18n\_m; use App\Util\Common; use App\Util\Exception\ClientException; use App\Util\Exception\RedirectException; use Exception; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Event\ViewEvent; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Validator\Exception\ValidatorException; /** * @method int int(string $param) * @method bool bool(string $param) * @method string string(string $param) * @method string params(string $param) * @method mixed handle(Request $request, mixed ...$extra) */ abstract class Controller extends AbstractController implements EventSubscriberInterface { private array $vars = []; protected ?Request $request = null; public function __construct(RequestStack $requestStack) { $this->request = $requestStack->getCurrentRequest(); } /** * TODO: Not currently used, so not tested, but should be * * @codeCoverageIgnore */ public function __invoke(Request $request) { $this->request = $request; $class = static::class; $method = 'on' . ucfirst(mb_strtolower($request->getMethod())); $attributes = array_diff_key($request->attributes->get('_route_params'), array_flip(['_format', '_fragment', '_locale', 'template', 'accept', 'is_system_path'])); if (method_exists($class, $method)) { return $this->{$method}($request, ...$attributes); } else { return $this->handle($request, ...$attributes); } } /** * Symfony event when it's searching for which controller to use */ public function onKernelController(ControllerEvent $event) { $controller = $event->getController(); $request = $event->getRequest(); $this->request = $request; $this->vars = ['controller' => $controller, 'request' => $request]; $user = Common::user(); if ($user !== null) { $this->vars['current_actor'] = $user->getActor(); } $event->stopPropagation(); return $event; } /** * Symfony event when the controller result is not a Response object */ public function onKernelView(ViewEvent $event) { $request = $event->getRequest(); $response = $event->getControllerResult(); if (!\is_array($response)) { // This means it's not one of our custom format responses, nothing to do // @codeCoverageIgnoreStart return $event; // @codeCoverageIgnoreEnd } $this->vars = array_merge_recursive($this->vars, $response); $template = \array_key_exists('_template', $this->vars) ? $this->vars['_template'] : null; unset($this->vars['_template'], $response['_template']); // Respond in the most preferred acceptable content type $accept = $request->getAcceptableContentTypes() ?: ['text/html']; $format = $request->getFormat($accept[0]); $potential_response = null; if (Event::handle('ControllerResponseInFormat', [ 'route' => $request->get('_route'), 'accept_header' => $accept, 'vars' => $this->vars, 'response' => &$potential_response, ]) !== Event::stop) { switch ($format) { case 'json': $event->setResponse(new JsonResponse($response)); break; case 'html': if ($template !== null) { $event->setResponse($this->render($template, $this->vars)); break; } else { // no break, goto default } // no break default: throw new ClientException(_m('Unsupported format: {format}', ['format' => $format]), 406); // 406 Not Acceptable } } else { $event->setResponse($potential_response); } Event::handle('CleanupModule'); return $event; } /** * Symfony event when the controller throws an exception * * @codeCoverageIgnore */ public function onKernelException(ExceptionEvent $event) { $except = $event->getThrowable(); if ($_ENV['APP_ENV'] !== 'dev') { // TODO: This is where our custom exception pages could go // $event->setResponse((new Response())->setStatusCode(455)); } do { if ($except instanceof RedirectException) { if (($redir = $except->redirect_response) != null) { $event->setResponse($redir); } else { $event->setResponse(new RedirectResponse($event->getRequest()->getPathInfo())); } } } while ($except != null && ($except = $except->getPrevious()) != null); Event::handle('CleanupModule'); return $event; } /** * @codeCoverageIgnore */ public static function getSubscribedEvents() { return [ KernelEvents::CONTROLLER => 'onKernelController', KernelEvents::EXCEPTION => 'onKernelException', KernelEvents::VIEW => 'onKernelView', ]; } /** * Get and convert GET parameters. Can be called with `int`, `bool`, `string`, etc * * @throws Exception * @throws ValidatorException * * @return null|array|bool|int|string the value or null if no parameter exists */ public function __call(string $method, array $args): array|bool|int|string|null { switch ($method) { case 'int': return $this->request->query->getInt($args[0]); case 'bool': return $this->request->query->getBoolean($args[0]); case 'string': return $this->request->query->get($args[0]); case 'params': return $this->request->query->all(); default: // @codeCoverageIgnoreStart Log::critical($m = "Method '{$method}' on class App\\Core\\Controller not found (__call)"); throw new Exception($m); // @codeCoverageIgnoreEnd } } }