Issues (20)

src/Controllers/GvController.php (1 issue)

Labels
Severity
1
<?php namespace Gvera\Controllers;
2
3
use Exception;
4
use Gvera\Exceptions\InvalidCSRFException;
5
use Gvera\Exceptions\InvalidHttpMethodException;
6
use Gvera\Exceptions\InvalidMethodException;
7
use Gvera\Exceptions\InvalidViewException;
8
use Gvera\Exceptions\NotAllowedException;
9
use Gvera\Helpers\dependencyInjection\DIContainer;
10
use Gvera\Helpers\http\HttpRequest;
11
use Gvera\Helpers\http\HttpResponse;
12
use Gvera\Helpers\http\JSONResponse;
13
use Gvera\Helpers\http\PrintErrorResponse;
14
use Gvera\Helpers\http\Response;
15
use Gvera\Helpers\locale\Locale;
16
use Gvera\Helpers\security\BasicAuthenticationStrategy;
17
use Gvera\Helpers\security\CSRFToken;
18
use Gvera\Helpers\security\JWTTokenAuthenticationStrategy;
19
use Gvera\Helpers\security\SessionAuthenticationStrategy;
20
use Gvera\Helpers\security\AuthenticationContext;
21
use Gvera\Models\User;
22
use Gvera\Services\TwigService;
23
use ReflectionException;
24
use Twig\Environment;
25
use Twig\Error\LoaderError;
26
use Twig\Error\RuntimeError;
27
use Twig\Error\SyntaxError;
28
29
/**
30
 * Class GvController
31
 * @category Class
32
 * @package Gvera\Controllers
33
 * @author    Guido Vera
34
 * @license  http://www.gnu.org/copyleft/gpl.html GNU General Public License
35
 * @link     http://www.github.com/veraguido/gv
36
 * Base controller to be used as a parent of all controllers, manages http objects,
37
 * and the responsibility of loading twig or not.
38
 */
39
abstract class GvController
40
{
41
42
    private ?string $method;
43
    private ?string $name;
44
    protected array $viewParams = array();
45
    protected HttpResponse $httpResponse;
46
    protected HttpRequest $httpRequest;
47
    protected DIContainer $diContainer;
48
    protected bool $protectedController = false;
49
50
    const DEFAULT_CONTROLLER = "Index";
51
    const DEFAULT_METHOD = 'index';
52
    private TwigService $twigService;
53
54
    /**
55
     * GvController constructor.
56
     * @param DIContainer $diContainer
57
     * @param $controllerName
58
     * @param $method
59
     * @throws InvalidMethodException
60
     * @throws ReflectionException
61
     */
62
    public function __construct(DIContainer $diContainer, $controllerName, $method)
63
    {
64
        $this->diContainer = $diContainer;
65
        $this->method = $method;
66
        $this->name = $controllerName;
67
        $this->httpRequest = $this->diContainer->get('httpRequest');
68
        $this->httpResponse = $this->diContainer->get('httpResponse');
69
        $config = $this->diContainer->get('config');
70
        $this->twigService = new TwigService(
71
            $config,
72
            __DIR__ . '/../Views/',
73
            __DIR__ . '/../../var/cache/views/'
74
        );
75
76
        if (!method_exists($this, $method)) {
77
            throw new InvalidMethodException(
78
                'the method was not found on the controller',
79
                [
80
                    'method' => $method,
81
                    'controller' => $controllerName
82
                ]
83
            );
84
        }
85
    }
86
87
    /**
88
     * @param array $allowedHttpMethods
89
     * @throws InvalidHttpMethodException
90
     * @throws InvalidViewException
91
     * @throws LoaderError
92
     * @throws NotAllowedException
93
     * @throws ReflectionException
94
     * @throws RuntimeError
95
     * @throws SyntaxError
96
     */
97
    public function init(array $allowedHttpMethods = []):void
98
    {
99
        $this->preInit($allowedHttpMethods);
100
101
        $methodName = $this->method;
102
        $this->$methodName();
103
104
        $this->postInit();
105
    }
106
107
    /**
108
     * @param $allowedHttpMethods
109
     * @throws InvalidHttpMethodException
110
     * @throws ReflectionException
111
     * @throws NotAllowedException
112
     */
113
    protected function preInit($allowedHttpMethods)
114
    {
115
        $this->checkIfPassIsGranted();
116
        $annotationUtil = $this->diContainer->get('annotationUtil');
117
        $isHttpMethodValid = $annotationUtil->validateMethods(
118
            $allowedHttpMethods,
119
            $this->httpRequest
120
        );
121
122
        if (false === $isHttpMethodValid) {
123
            throw new InvalidHttpMethodException(
124
                'The http method used for this action is not supported',
125
                [
126
                    "httpMethod" => $this->httpRequest->getRequestType(),
127
                    "allowedMethods" => $allowedHttpMethods
128
                ]
129
            );
130
        }
131
132
        if ($this->twigService->needsTwig($this->name, $this->method)) {
133
            $this->twigService->loadTwig();
134
        }
135
    }
136
137
    /**
138
     * @throws InvalidViewException
139
     */
140
    protected function postInit()
141
    {
142
        if ($this->twigService->needsTwig($this->name, $this->method)) {
143
            $this->httpResponse->response(
144
                new Response($this->twigService->render($this->name, $this->method, $this->viewParams))
145
            );
146
            return;
147
        }
148
149
        if (count($this->viewParams) > 0) {
150
            throw new InvalidViewException(
151
                'view params was set, but view could not be found ',
152
                ['method' => $this->method,'controller ' => $this->name]
153
            );
154
        }
155
    }
156
157
    /**
158
     * @throws NotAllowedException
159
     */
160
    public function checkIfPassIsGranted()
161
    {
162
        if (!$this->protectedController) {
163
            return;
164
        }
165
166
        if (null !== $this->httpRequest->getAuthDetails()) {
167
            $this->mustPassBasicAuthentication();
168
            return;
169
        }
170
171
        if (null !== $this->httpRequest->getBearerToken()) {
172
            $this->mustPassTokenAuthentication();
173
        }
174
175
        $this->mustPassSessionAuthentication();
176
    }
177
178
    /**
179
     * @throws NotAllowedException
180
     * @throws Exception
181
     */
182
    protected function mustPassSessionAuthentication()
183
    {
184
        $sessionStrategy = new SessionAuthenticationStrategy(
185
            $this->getSession(),
186
            $this->getUserService(),
187
            $this->getEntityManager()
188
        );
189
        $context =new AuthenticationContext($sessionStrategy);
190
        if (!$context->isUserLoggedIn()) {
191
            throw new NotAllowedException(Locale::getLocale('user is not allowed'));
192
        }
193
    }
194
195
    /**
196
     * @throws NotAllowedException
197
     */
198
    protected function mustPassBasicAuthentication()
199
    {
200
        $basicAuthStrategy = new BasicAuthenticationStrategy(
201
            $this->getEntityManager(),
202
            $this->getUserService(),
203
            $this->httpRequest->getAuthDetails()
204
        );
205
        $context = new AuthenticationContext($basicAuthStrategy);
206
        if (!$context->isUserLoggedIn()) {
207
            throw new NotAllowedException(Locale::getLocale('user is not allowed'));
208
        }
209
    }
210
211
    /**
212
     * @throws NotAllowedException
213
     * @throws Exception
214
     */
215
    public function mustPassTokenAuthentication()
216
    {
217
        $token = $this->httpRequest->getBearerToken();
218
        $jwtTokenStrategy = new JWTTokenAuthenticationStrategy($token, $this->getEntityManager());
0 ignored issues
show
It seems like $token can also be of type null; however, parameter $tokenValue of Gvera\Helpers\security\J...Strategy::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

218
        $jwtTokenStrategy = new JWTTokenAuthenticationStrategy(/** @scrutinizer ignore-type */ $token, $this->getEntityManager());
Loading history...
219
        $context = new AuthenticationContext($jwtTokenStrategy);
220
        if (!$context->isUserLoggedIn()) {
221
            throw new NotAllowedException(Locale::getLocale('user is not allowed'));
222
        }
223
    }
224
225
    /**
226
     * @param int $errorCode
227
     * @param string $message
228
     */
229
    protected function badRequestWithError(int $errorCode, string $message):void
230
    {
231
        $this->httpResponse->response(
232
            new JSONResponse(
233
                ['error' => $errorCode,'message' => $message],
234
                Response::HTTP_RESPONSE_BAD_REQUEST
235
            )
236
        );
237
    }
238
239
    /**
240
     * @param int $errorCode
241
     * @param string $message
242
     */
243
    protected function unauthorizedWithError(int $errorCode, string $message):void
244
    {
245
        $this->httpResponse->response(
246
            new PrintErrorResponse(
247
                $errorCode,
248
                $message,
249
                Response::HTTP_RESPONSE_UNAUTHORIZED
250
            )
251
        );
252
    }
253
254
    protected function unauthorizedBasicAuth()
255
    {
256
        $this->httpResponse->response(
257
            new Response(
258
                '',
259
                Response::CONTENT_TYPE_HTML,
260
                Response::HTTP_RESPONSE_UNAUTHORIZED,
261
                Response::BASIC_AUTH_ACCESS_DENIED
262
            )
263
        );
264
    }
265
266
    /**
267
     * @param string $action
268
     * @return boolean
269
     */
270
    protected function isUserAllowed(string $action): bool
271
    {
272
        $repo = $this->getEntityManager()->getRepository(User::class);
273
        $session = $this->getSession();
274
        $user = $repo->findOneById($session->get('user')['id']);
275
        return $this->getUserService()->userCan($user, $action);
276
    }
277
278
    /**
279
     * @return string
280
     */
281
    protected function generateCSRFToken(): string
282
    {
283
        $session = $this->getSession();
284
        $token  = $this->getCsrfFactory()->createToken();
285
        $session->set(CSRFToken::ID, $token->getTokenValue());
286
287
        return $token->getTokenValue();
288
    }
289
290
    /**
291
     * @param $requestToken
292
     * @throws InvalidCSRFException
293
     */
294
    protected function validateCSRFToken($requestToken)
295
    {
296
        $session = $this->getSession();
297
        $sessionToken = $session->get(CSRFToken::ID);
298
        if (false === hash_equals($requestToken, $sessionToken)) {
299
            throw new InvalidCSRFException(
300
                "csrf tokens do not match",
301
                ['session token' => $sessionToken, 'form token' => $requestToken]
302
            );
303
        }
304
        $session->unsetByKey('csrf');
305
    }
306
307
    /**
308
     * @param $name
309
     * @param $arguments
310
     * @return object
311
     * @throws ReflectionException
312
     * using magic methods to retrieve from DIContainer
313
     */
314
    public function __call($name, $arguments): object
315
    {
316
        $id = lcfirst(str_replace('get', '', $name));
317
        return $this->diContainer->get($id);
318
    }
319
}
320