]> Raphaël G. Git Repositories - blogbundle/blob - Controller/AbstractController.php
Add security member
[blogbundle] / Controller / AbstractController.php
1 <?php declare(strict_types=1);
2
3 /*
4 * This file is part of the Rapsys BlogBundle package.
5 *
6 * (c) Raphaël Gertz <symfony@rapsys.eu>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace Rapsys\BlogBundle\Controller;
13
14 use Doctrine\ORM\EntityManagerInterface;
15 use Doctrine\Persistence\ManagerRegistry;
16 use Psr\Log\LoggerInterface;
17 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as BaseAbstractController;
18 use Symfony\Bundle\SecurityBundle\Security;
19 use Symfony\Component\Asset\PackageInterface;
20 use Symfony\Component\DependencyInjection\ContainerInterface;
21 use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
22 use Symfony\Component\Filesystem\Filesystem;
23 use Symfony\Component\Form\FormFactoryInterface;
24 use Symfony\Component\HttpFoundation\Request;
25 use Symfony\Component\HttpFoundation\RequestStack;
26 use Symfony\Component\HttpFoundation\Response;
27 use Symfony\Component\Mailer\MailerInterface;
28 use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
29 use Symfony\Component\Routing\RouterInterface;
30 use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
31 use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
32 use Symfony\Component\Security\Core\User\UserInterface;
33 use Symfony\Contracts\Service\ServiceSubscriberInterface;
34 use Symfony\Contracts\Translation\TranslatorInterface;
35 use Twig\Environment;
36
37 use Rapsys\BlogBundle\Entity\Dance;
38 use Rapsys\BlogBundle\Entity\Location;
39 use Rapsys\BlogBundle\Entity\Slot;
40 use Rapsys\BlogBundle\Entity\User;
41 use Rapsys\BlogBundle\RapsysBlogBundle;
42
43 use Rapsys\PackBundle\Util\FacebookUtil;
44 use Rapsys\PackBundle\Util\ImageUtil;
45 use Rapsys\PackBundle\Util\SluggerUtil;
46
47 /**
48 * Provides common features needed in controllers.
49 *
50 * {@inheritdoc}
51 */
52 abstract class AbstractController extends BaseAbstractController implements ServiceSubscriberInterface {
53 ///AuthorizationCheckerInterface instance
54 protected AuthorizationCheckerInterface $checker;
55
56 ///Config array
57 protected array $config;
58
59 ///Context array
60 protected array $context;
61
62 ///AccessDecisionManagerInterface instance
63 protected AccessDecisionManagerInterface $decision;
64
65 ///ManagerRegistry instance
66 protected ManagerRegistry $doctrine;
67
68 ///FacebookUtil instance
69 protected FacebookUtil $facebook;
70
71 ///FormFactoryInterface instance
72 protected FormFactoryInterface $factory;
73
74 ///Image util instance
75 protected ImageUtil $image;
76
77 ///Limit integer
78 protected int $limit;
79
80 ///Locale string
81 protected string $locale;
82
83 ///MailerInterface instance
84 protected MailerInterface $mailer;
85
86 ///EntityManagerInterface instance
87 protected EntityManagerInterface $manager;
88
89 ///Modified DateTime
90 protected \DateTime $modified;
91
92 ///PackageInterface instance
93 protected PackageInterface $package;
94
95 ///Request instance
96 protected Request $request;
97
98 ///Route string
99 protected string $route;
100
101 ///Route params array
102 protected array $routeParams;
103
104 ///Router instance
105 protected RouterInterface $router;
106
107 ///Slugger util instance
108 protected SluggerUtil $slugger;
109
110 ///Security instance
111 protected Security $security;
112
113 ///RequestStack instance
114 protected RequestStack $stack;
115
116 ///Translator instance
117 protected TranslatorInterface $translator;
118
119 ///Twig\Environment instance
120 protected Environment $twig;
121
122 /**
123 * Abstract constructor
124 *
125 * @param AuthorizationCheckerInterface $checker The container instance
126 * @param ContainerInterface $container The container instance
127 * @param AccessDecisionManagerInterface $decision The decision instance
128 * @param ManagerRegistry $doctrine The doctrine instance
129 * @param FacebookUtil $facebook The facebook instance
130 * @param FormFactoryInterface $factory The factory instance
131 * @param ImageUtil $image The image instance
132 * @param MailerInterface $mailer The mailer instance
133 * @param EntityManagerInterface $manager The manager instance
134 * @param PackageInterface $package The package instance
135 * @param RouterInterface $router The router instance
136 * @param SluggerUtil $slugger The slugger instance
137 * @param Security $security The security instance
138 * @param RequestStack $stack The stack instance
139 * @param TranslatorInterface $translator The translator instance
140 * @param Environment $twig The twig environment instance
141 * @param integer $limit The page limit
142 *
143 * @TODO move all that stuff to setSlugger('@slugger') setters with a calls: [ setSlugger: [ '@slugger' ] ] to unbload classes ???
144 * @TODO add a calls: [ ..., prepare: ['@???'] ] that do all the logic that can't be done in constructor because various things are not available
145 */
146 public function __construct(AuthorizationCheckerInterface $checker, ContainerInterface $container, AccessDecisionManagerInterface $decision, ManagerRegistry $doctrine, FacebookUtil $facebook, FormFactoryInterface $factory, ImageUtil $image, MailerInterface $mailer, EntityManagerInterface $manager, PackageInterface $package, RouterInterface $router, SluggerUtil $slugger, Security $security, RequestStack $stack, TranslatorInterface $translator, Environment $twig, int $limit = 5) {
147 //Set checker
148 $this->checker = $checker;
149
150 //Retrieve config
151 $this->config = $container->getParameter(RapsysBlogBundle::getAlias());
152
153 //Set the container
154 $this->container = $container;
155
156 //Set decision
157 $this->decision = $decision;
158
159 //Set doctrine
160 $this->doctrine = $doctrine;
161
162 //Set facebook
163 $this->facebook = $facebook;
164
165 //Set factory
166 $this->factory = $factory;
167
168 //Set image
169 $this->image = $image;
170
171 //Set limit
172 $this->limit = $limit;
173
174 //Set mailer
175 $this->mailer = $mailer;
176
177 //Set manager
178 $this->manager = $manager;
179
180 //Set package
181 $this->package = $package;
182
183 //Set router
184 $this->router = $router;
185
186 //Set slugger
187 $this->slugger = $slugger;
188
189 //Set security
190 $this->security = $security;
191
192 //Set stack
193 $this->stack = $stack;
194
195 //Set translator
196 $this->translator = $translator;
197
198 //Set twig
199 $this->twig = $twig;
200
201 //Get main request
202 $this->request = $this->stack->getMainRequest();
203
204 //Get current locale
205 $this->locale = $this->request->getLocale();
206
207 //Set canonical
208 $canonical = null;
209
210 //Set alternates
211 $alternates = [];
212
213 //Set route
214 //TODO: default to not found route ???
215 //TODO: when url not found, this attribute is not defined, how do we handle it ???
216 //XXX: do we generate a dummy default route by default ???
217 $this->route = $this->request->attributes->get('_route');
218
219 //Set route params
220 $this->routeParams = $this->request->attributes->get('_route_params');
221
222 //With route and routeParams
223 if ($this->route !== null && $this->routeParams !== null) {
224 //Set canonical
225 $canonical = $this->router->generate($this->route, $this->routeParams, UrlGeneratorInterface::ABSOLUTE_URL);
226
227 //Set alternates
228 $alternates = [];
229 }
230
231 //Set the context
232 $this->context = [
233 //TODO: review the structure
234 #'title' => $this->translator->trans($this->config['title']),
235 #'og:site_name' => $this->translator->trans($this->config['title']),
236 #'site' => [
237 # 'donate' => $this->config['donate'],
238 # 'title' => $title = $this->translator->trans($this->config['site']['title']),
239 #],
240 'head' => [
241 'alternates' => $alternates,
242 'canonical' => $canonical,
243 'icon' => $this->config['icon'],
244 'keywords' => null,
245 'locale' => str_replace('_', '-', $this->locale),
246 'logo' => [
247 'png' => $this->config['logo']['png'],
248 'svg' => $this->config['logo']['svg'],
249 'alt' => $this->translator->trans($this->config['logo']['alt'])
250 ],
251 'root' => $this->config['root'],
252 'title' => null,
253 'facebook' => [
254 'og:type' => 'article',
255 'og:site_name' => $this->translator->trans($this->config['title']),
256 'og:url' => $canonical,
257 //TODO: review this value
258 'fb:app_id' => $this->config['facebook']['apps']
259 ],
260 'fbimage' => [
261 'texts' => [
262 $this->translator->trans($this->config['title']) => [
263 'font' => 'irishgrover',
264 'size' => 110
265 ]
266 ]
267 ]
268 ],
269 'contact' => [
270 'name' => $this->translator->trans($this->config['contact']['name']),
271 'mail' => $this->config['contact']['mail']
272 ],
273 'copy' => [
274 'by' => $this->translator->trans($this->config['copy']['by']),
275 'link' => $this->config['copy']['link'],
276 'long' => $this->translator->trans($this->config['copy']['long']),
277 'short' => $this->translator->trans($this->config['copy']['short']),
278 'title' => $this->config['copy']['title']
279 ],
280 'forms' => [],
281 'title' => null,
282 'description' => null,
283 'section' => null,
284 'site' => [
285 'icon' => $this->config['icon'],
286 'logo' => [
287 'png' => $this->config['logo']['png'],
288 'svg' => $this->config['logo']['svg'],
289 'alt' => $this->translator->trans($this->config['logo']['alt'])
290 ],
291 'path' => $this->config['path'],
292 'root' => $this->config['root'],
293 'title' => $this->translator->trans($this->config['title'])
294 ]
295 ];
296 }
297
298 /**
299 * Renders a view
300 *
301 * {@inheritdoc}
302 */
303 protected function render(string $view, array $parameters = [], Response $response = null): Response {
304 //Create response when null
305 $response ??= new Response();
306
307 //Without alternates
308 if (empty($parameters['head']['alternates'])) {
309 //Iterate on locales excluding current one
310 foreach($this->config['locales'] as $locale) {
311 //Set routeParams
312 $routeParams = $this->routeParams;
313
314 //With current locale
315 if ($locale !== $this->locale) {
316 //Set titles
317 $titles = [];
318
319 //Set route params locale
320 $routeParams['_locale'] = $locale;
321
322 //Unset slug
323 //XXX: slug is in current locale, better use a simple redirect for invalid slug than implement a hard to get translation here
324 unset($routeParams['slug']);
325
326 //Iterate on other locales
327 foreach(array_diff($this->config['locales'], [$locale]) as $other) {
328 //Set other locale title
329 $titles[$other] = $this->translator->trans($this->config['languages'][$locale], [], null, $other);
330 }
331
332 //Set locale locales context
333 $parameters['head']['alternates'][str_replace('_', '-', $locale)] = [
334 'absolute' => $this->router->generate($this->route, $routeParams, UrlGeneratorInterface::ABSOLUTE_URL),
335 'relative' => $this->router->generate($this->route, $routeParams),
336 'title' => implode('/', $titles),
337 'translated' => $this->translator->trans($this->config['languages'][$locale], [], null, $locale)
338 ];
339
340 //Add shorter locale
341 if (empty($parameters['head']['alternates'][$shortCurrent = substr($locale, 0, 2)])) {
342 //Set locale locales context
343 $parameters['head']['alternates'][$shortCurrent] = $parameters['head']['alternates'][str_replace('_', '-', $locale)];
344 }
345 //Add shorter locale
346 } elseif (empty($parameters['head']['alternates'][$shortCurrent = substr($locale, 0, 2)])) {
347 //Set titles
348 $titles = [];
349
350 //Set route params locale
351 $routeParams['_locale'] = $locale;
352
353 //Iterate on other locales
354 foreach(array_diff($this->config['locales'], [$locale]) as $other) {
355 //Set other locale title
356 $titles[$other] = $this->translator->trans($this->config['languages'][$locale], [], null, $other);
357 }
358
359 //Set locale locales context
360 $parameters['head']['alternates'][$shortCurrent] = [
361 'absolute' => $this->router->generate($this->route, $routeParams, UrlGeneratorInterface::ABSOLUTE_URL),
362 'relative' => $this->router->generate($this->route, $routeParams),
363 'title' => implode('/', $titles),
364 'translated' => $this->translator->trans($this->config['languages'][$locale], [], null, $locale)
365 ];
366 }
367 }
368 }
369
370 //With empty head title and section
371 if (empty($parameters['head']['title']) && !empty($parameters['section'])) {
372 //Set head title
373 $parameters['head']['title'] = implode(' - ', [$parameters['title'], $parameters['section'], $this->translator->trans($this->config['title'])]);
374 //With empty head title
375 } elseif (empty($parameters['head']['title'])) {
376 //Set head title
377 $parameters['head']['title'] = implode(' - ', [$parameters['title'], $this->translator->trans($this->config['title'])]);
378 }
379
380 //With empty head description and description
381 if (empty($parameters['head']['description']) && !empty($parameters['description'])) {
382 //Set head description
383 $parameters['head']['description'] = $parameters['description'];
384 }
385
386 //With empty facebook title and title
387 if (empty($parameters['head']['facebook']['og:title']) && !empty($parameters['title'])) {
388 //Set facebook title
389 $parameters['head']['facebook']['og:title'] = $parameters['title'];
390 }
391
392 //With empty facebook description and description
393 if (empty($parameters['head']['facebook']['og:description']) && !empty($parameters['description'])) {
394 //Set facebook description
395 $parameters['head']['facebook']['og:description'] = $parameters['description'];
396 }
397
398 //With locale
399 if (!empty($this->locale)) {
400 //Set facebook locale
401 $parameters['head']['facebook']['og:locale'] = $this->locale;
402
403 //With alternates
404 //XXX: locale change when fb_locale=xx_xx is provided is done in FacebookSubscriber
405 //XXX: see https://stackoverflow.com/questions/20827882/in-open-graph-markup-whats-the-use-of-oglocalealternate-without-the-locati
406 if (!empty($parameters['head']['alternates'])) {
407 //Iterate on alternates
408 foreach($parameters['head']['alternates'] as $lang => $alternate) {
409 if (strlen($lang) == 5) {
410 //Set facebook locale alternate
411 $parameters['head']['facebook']['og:locale:alternate'] = str_replace('-', '_', $lang);
412 }
413 }
414 }
415 }
416
417 //Without facebook image defined and texts
418 if (empty($parameters['head']['facebook']['og:image']) && !empty($this->request) && !empty($parameters['head']['fbimage']['texts']) && !empty($this->modified)) {
419 //Get facebook image
420 $parameters['head']['facebook'] += $this->facebook->getImage($this->request->getPathInfo(), $parameters['head']['fbimage']['texts'], $this->modified->getTimestamp());
421 }
422
423 //Call twig render method
424 $content = $this->twig->render($view, $parameters);
425
426 //Invalidate OK response on invalid form
427 if (200 === $response->getStatusCode()) {
428 foreach ($parameters as $v) {
429 if ($v instanceof FormInterface && $v->isSubmitted() && !$v->isValid()) {
430 $response->setStatusCode(422);
431 break;
432 }
433 }
434 }
435
436 //Store content in response
437 $response->setContent($content);
438
439 //Return response
440 return $response;
441 }
442
443 /**
444 * {@inheritdoc}
445 *
446 * @see vendor/symfony/framework-bundle/Controller/AbstractController.php
447 */
448 public static function getSubscribedServices(): array {
449 //Return subscribed services
450 return [
451 'security.authorization_checker' => AuthorizationCheckerInterface::class,
452 'service_container' => ContainerInterface::class,
453 'rapsys_user.access_decision_manager' => AccessDecisionManagerInterface::class,
454 'doctrine' => ManagerRegistry::class,
455 'rapsys_pack.facebook_util' => FacebookUtil::class,
456 'form.factory' => FormFactoryInterface::class,
457 'rapsys_pack.image_util' => ImageUtil::class,
458 'mailer.mailer' => MailerInterface::class,
459 'doctrine.orm.default_entity_manager' => EntityManagerInterface::class,
460 'rapsys_pack.path_package' => PackageInterface::class,
461 'router' => RouterInterface::class,
462 'rapsys_pack.slugger_util' => SluggerUtil::class,
463 'security' => Security::class,
464 'stack' => RequestStack::class,
465 'translator' => TranslatorInterface::class,
466 'twig' => Environment::class,
467 ];
468 }
469
470 /**
471 * Get a user from the Security Helper.
472 *
473 * @throws \LogicException If SecurityBundle is not available
474 *
475 * @see TokenInterface::getUser()
476 * @see https://github.com/symfony/symfony/issues/44735
477 * @see vendor/symfony/framework-bundle/Controller/AbstractController.php
478 */
479 protected function getUser(): ?UserInterface {
480 //Without token
481 if (null === ($token = $this->security->getToken())) {
482 return null;
483 }
484
485 //With token
486 return $token->getUser();
487 }
488 }