. // }}} /** * 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 App\Util\Exception\ServerException; 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\HttpFoundation\Response; 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 * * @throws ClientException * @throws ServerException */ 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 = $this->vars['_template'] ?? null; Event::handle('OverrideTemplate', [$this->vars, &$template]); // Allow plugins to replace the template used for anything unset($this->vars['_template'], $response['_template']); // Respond in the most preferred acceptable content type $route = $request->get('_route'); $accept = $request->getAcceptableContentTypes() ?: ['text/html']; $format = $request->getFormat($accept[0]); $potential_response = null; if (Event::handle('ControllerResponseInFormat', [ 'route' => $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)); /* // Setting the Content-Security-Policy response header $policy = "default-src 'self';" . "script-src 'strict-dynamic' https: http:;" . "object-src 'none'; base-uri 'none'"; $potential_response = $event->getResponse(); $potential_response->headers->set('Content-Security-Policy', $policy); $potential_response->headers->set('X-Content-Security-Policy', $policy); $potential_response->headers->set('X-WebKit-CSP', $policy);*/ break; } else { // no break, goto default } // no break default: throw new ClientException(_m('Unsupported format: {format}', ['format' => $format]), 406); // 406 Not Acceptable } } else { if (\is_null($potential_response)) { // TODO BugFoundException Log::critical($m = "ControllerResponseInFormat for route '{$route}' returned Event::stop but didn't provide a response"); throw new ServerException(_m($m, ['route' => $route])); } $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 } } }