Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
| Total | |
100.00% |
1 / 1 |
|
100.00% |
4 / 4 |
CRAP | |
100.00% |
35 / 35 |
| Controller | |
100.00% |
1 / 1 |
|
100.00% |
4 / 4 |
21 | |
100.00% |
35 / 35 |
| __construct | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
| __invoke | n/a |
0 / 0 |
2 | n/a |
0 / 0 |
|||||
| onKernelController | |
100.00% |
1 / 1 |
1 | |
100.00% |
7 / 7 |
|||
| onKernelView | |
100.00% |
1 / 1 |
7 | |
100.00% |
19 / 19 |
|||
| onKernelException | n/a |
0 / 0 |
5 | n/a |
0 / 0 |
|||||
| getSubscribedEvents | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
| __call | |
100.00% |
1 / 1 |
4 | |
100.00% |
7 / 7 |
|||
| 1 | <?php |
| 2 | |
| 3 | // {{{ License |
| 4 | // This file is part of GNU social - https://www.gnu.org/software/social |
| 5 | // |
| 6 | // GNU social is free software: you can redistribute it and/or modify |
| 7 | // it under the terms of the GNU Affero General Public License as published by |
| 8 | // the Free Software Foundation, either version 3 of the License, or |
| 9 | // (at your option) any later version. |
| 10 | // |
| 11 | // GNU social is distributed in the hope that it will be useful, |
| 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 14 | // GNU Affero General Public License for more details. |
| 15 | // |
| 16 | // You should have received a copy of the GNU Affero General Public License |
| 17 | // along with GNU social. If not, see <http://www.gnu.org/licenses/>. |
| 18 | // }}} |
| 19 | |
| 20 | /** |
| 21 | * Base class for controllers |
| 22 | * |
| 23 | * @package GNUsocial |
| 24 | * @category Controller |
| 25 | * |
| 26 | * @author Hugo Sales <hugo@hsal.es> |
| 27 | * @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org |
| 28 | * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later |
| 29 | */ |
| 30 | |
| 31 | namespace App\Core; |
| 32 | |
| 33 | use function App\Core\I18n\_m; |
| 34 | use App\Util\Common; |
| 35 | use App\Util\Exception\ClientException; |
| 36 | use App\Util\Exception\RedirectException; |
| 37 | use Exception; |
| 38 | use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; |
| 39 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; |
| 40 | use Symfony\Component\HttpFoundation\JsonResponse; |
| 41 | use Symfony\Component\HttpFoundation\RedirectResponse; |
| 42 | use Symfony\Component\HttpFoundation\Request; |
| 43 | use Symfony\Component\HttpFoundation\RequestStack; |
| 44 | use Symfony\Component\HttpFoundation\Response; |
| 45 | use Symfony\Component\HttpKernel\Event\ControllerEvent; |
| 46 | use Symfony\Component\HttpKernel\Event\ExceptionEvent; |
| 47 | use Symfony\Component\HttpKernel\Event\ViewEvent; |
| 48 | use Symfony\Component\HttpKernel\KernelEvents; |
| 49 | |
| 50 | class Controller extends AbstractController implements EventSubscriberInterface |
| 51 | { |
| 52 | private array $vars = []; |
| 53 | private ?Request $request = null; |
| 54 | |
| 55 | public function __construct(RequestStack $requestStack) |
| 56 | { |
| 57 | $this->request = $requestStack->getCurrentRequest(); |
| 58 | } |
| 59 | |
| 60 | /** |
| 61 | * TODO: Not currently used, so not tested, but should be |
| 62 | * |
| 63 | * @codeCoverageIgnore |
| 64 | */ |
| 65 | public function __invoke(Request $request) |
| 66 | { |
| 67 | $this->request = $request; |
| 68 | $class = get_called_class(); |
| 69 | $method = 'on' . ucfirst(strtolower($request->getMethod())); |
| 70 | if (method_exists($class, $method)) { |
| 71 | return $class::$method($request, $this->vars); |
| 72 | } else { |
| 73 | return $class::handle($request, $this->vars); |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | /** |
| 78 | * Symfony event when it's searching for which controller to use |
| 79 | */ |
| 80 | public function onKernelController(ControllerEvent $event) |
| 81 | { |
| 82 | $controller = $event->getController(); |
| 83 | $request = $event->getRequest(); |
| 84 | |
| 85 | $this->request = $request; |
| 86 | $this->vars = ['controler' => $controller, 'request' => $request, 'have_user' => Common::user() !== null]; |
| 87 | Event::handle('StartTwigPopulateVars', [&$this->vars]); |
| 88 | |
| 89 | $event->stopPropagation(); |
| 90 | return $event; |
| 91 | } |
| 92 | |
| 93 | /** |
| 94 | * Symfony event when the controller result is not a Response object |
| 95 | */ |
| 96 | public function onKernelView(ViewEvent $event) |
| 97 | { |
| 98 | $request = $event->getRequest(); |
| 99 | $response = $event->getControllerResult(); |
| 100 | if (!is_array($response)) { |
| 101 | // This means it's not one of our custom format responses, nothing to do |
| 102 | // @codeCoverageIgnoreStart |
| 103 | return $event; |
| 104 | // @codeCoverageIgnoreEnd |
| 105 | } |
| 106 | |
| 107 | $this->vars = array_merge_recursive($this->vars, $response); |
| 108 | Event::handle('EndTwigPopulateVars', [&$this->vars]); |
| 109 | |
| 110 | $template = $this->vars['_template']; |
| 111 | unset($this->vars['_template'], $this->vars['request'], $response['_template']); |
| 112 | |
| 113 | // Respond in the the most preffered acceptable content type |
| 114 | $accept = $request->getAcceptableContentTypes() ?: ['text/html']; |
| 115 | $format = $request->getFormat($accept[0]); |
| 116 | switch ($format) { |
| 117 | case 'html': |
| 118 | $event->setResponse($this->render($template, $this->vars)); |
| 119 | break; |
| 120 | case 'json': |
| 121 | case 'jsonld': |
| 122 | $event->setResponse(new JsonResponse($response)); |
| 123 | break; |
| 124 | default: |
| 125 | throw new ClientException(_m('Unsupported format: {format}', ['format' => $format]), 406); // 406 Not Acceptable |
| 126 | } |
| 127 | |
| 128 | return $event; |
| 129 | } |
| 130 | |
| 131 | /** |
| 132 | * Symfony event when the controller throws an exception |
| 133 | * |
| 134 | * @codeCoverageIgnore |
| 135 | */ |
| 136 | public function onKernelException(ExceptionEvent $event) |
| 137 | { |
| 138 | $except = $event->getThrowable(); |
| 139 | if ($_ENV['APP_ENV'] !== 'dev') { |
| 140 | // TODO: This is where our custom exception pages could go |
| 141 | // $event->setResponse((new Response())->setStatusCode(455)); |
| 142 | } |
| 143 | do { |
| 144 | if ($except instanceof RedirectException) { |
| 145 | if (($redir = $except->redirect_response) != null) { |
| 146 | $event->setResponse($redir); |
| 147 | } else { |
| 148 | $event->setResponse(new RedirectResponse($event->getRequest()->getPathInfo())); |
| 149 | } |
| 150 | } |
| 151 | } while ($except != null && ($except = $except->getPrevious()) != null); |
| 152 | return $event; |
| 153 | } |
| 154 | |
| 155 | /** |
| 156 | * @codeCoverageIgnore |
| 157 | */ |
| 158 | public static function getSubscribedEvents() |
| 159 | { |
| 160 | return [ |
| 161 | KernelEvents::CONTROLLER => 'onKernelController', |
| 162 | KernelEvents::EXCEPTION => 'onKernelException', |
| 163 | KernelEvents::VIEW => 'onKernelView', |
| 164 | ]; |
| 165 | } |
| 166 | |
| 167 | /** |
| 168 | * Get and convert GET parameters. Can be called with `int`, `bool`, etc |
| 169 | * |
| 170 | * @param string $name |
| 171 | * |
| 172 | * @throws ValidatorException |
| 173 | */ |
| 174 | public function __call(string $method, array $args) |
| 175 | { |
| 176 | $name = $args[0]; |
| 177 | $value = $this->request->query->get($name); |
| 178 | switch ($method) { |
| 179 | case 'int': |
| 180 | return (int) $value; |
| 181 | case 'bool': |
| 182 | return (bool) $value; |
| 183 | default: |
| 184 | // @codeCoverageIgnoreStart |
| 185 | Log::critical($m = "Method '{$method}' on class App\\Core\\Controller not found (__call)"); |
| 186 | throw new Exception($m); |
| 187 | // @codeCoverageIgnoreEnd |
| 188 | } |
| 189 | } |
| 190 | } |