Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
35 / 35
Controller
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
4 / 4
21
100.00% covered (success)
100.00%
35 / 35
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 __invoke
n/a
0 / 0
2
n/a
0 / 0
 onKernelController
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
7 / 7
 onKernelView
100.00% covered (success)
100.00%
1 / 1
7
100.00% covered (success)
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% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
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
31namespace App\Core;
32
33use function App\Core\I18n\_m;
34use App\Util\Common;
35use App\Util\Exception\ClientException;
36use App\Util\Exception\RedirectException;
37use Exception;
38use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
39use Symfony\Component\EventDispatcher\EventSubscriberInterface;
40use Symfony\Component\HttpFoundation\JsonResponse;
41use Symfony\Component\HttpFoundation\RedirectResponse;
42use Symfony\Component\HttpFoundation\Request;
43use Symfony\Component\HttpFoundation\RequestStack;
44use Symfony\Component\HttpFoundation\Response;
45use Symfony\Component\HttpKernel\Event\ControllerEvent;
46use Symfony\Component\HttpKernel\Event\ExceptionEvent;
47use Symfony\Component\HttpKernel\Event\ViewEvent;
48use Symfony\Component\HttpKernel\KernelEvents;
49
50class 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}