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
Bug
introduced
by
![]() |
|||
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 |