]> Raphaƫl G. Git Repositories - airbundle/blob - Controller/DefaultController.php
Add todo
[airbundle] / Controller / DefaultController.php
1 <?php
2
3 namespace Rapsys\AirBundle\Controller;
4
5 use Symfony\Bridge\Twig\Mime\TemplatedEmail;
6 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
7 use Symfony\Bundle\FrameworkBundle\Controller\ControllerTrait;
8 use Symfony\Component\Asset\Packages;
9 use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
10 use Symfony\Component\Filesystem\Filesystem;
11 use Symfony\Component\DependencyInjection\ContainerAwareTrait;
12 use Symfony\Component\DependencyInjection\ContainerInterface;
13 use Symfony\Component\Form\FormError;
14 use Symfony\Component\HttpFoundation\Request;
15 use Symfony\Component\HttpFoundation\RequestStack;
16 use Symfony\Component\HttpFoundation\Response;
17 use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
18 use Symfony\Component\Mailer\MailerInterface;
19 use Symfony\Component\Mime\Address;
20 use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
21 use Symfony\Component\Routing\RouterInterface;
22 use Symfony\Component\Translation\TranslatorInterface;
23
24 use Rapsys\AirBundle\Entity\Application;
25 use Rapsys\AirBundle\Entity\Location;
26 use Rapsys\AirBundle\Entity\Session;
27 use Rapsys\AirBundle\Entity\Slot;
28 use Rapsys\AirBundle\Entity\User;
29 use Rapsys\AirBundle\Pdf\DisputePdf;
30 use Rapsys\UserBundle\Utils\Slugger;
31
32
33 class DefaultController {
34 use ControllerTrait {
35 //Rename render as _render
36 render as protected _render;
37 }
38
39 ///Config array
40 protected $config;
41
42 ///Context array
43 protected $context;
44
45 ///Router instance
46 protected $router;
47
48 ///Translator instance
49 protected $translator;
50
51 ///Packages instance
52 protected $asset;
53
54 ///RequestStack instance
55 protected $stack;
56
57 ///Request instance
58 protected $request;
59
60 ///Locale instance
61 protected $locale;
62
63 /**
64 * @var ContainerInterface
65 */
66 protected $container;
67
68 ///Facebook image array
69 protected $facebookImage = [];
70
71 /**
72 * Inject container and translator interface
73 *
74 * @param ContainerInterface $container The container instance
75 * @param RouterInterface $router The router instance
76 * @param RequestStack $stack The request stack
77 * @param TranslatorInterface $translator The translator instance
78 */
79 public function __construct(ContainerInterface $container, RouterInterface $router, RequestStack $stack, TranslatorInterface $translator, Packages $asset) {
80 //Retrieve config
81 $this->config = $container->getParameter($this->getAlias());
82
83 //Set the container
84 $this->container = $container;
85
86 //Set the router
87 $this->router = $router;
88
89 //Set the translator
90 $this->translator = $translator;
91
92 //Set the asset
93 $this->asset = $asset;
94
95 //Set the request stack
96 $this->stack = $stack;
97
98 //Set the context
99 $this->context = [
100 'contact' => [
101 'title' => $translator->trans($this->config['contact']['title']),
102 'mail' => $this->config['contact']['mail']
103 ],
104 'copy' => [
105 'by' => $translator->trans($this->config['copy']['by']),
106 'link' => $this->config['copy']['link'],
107 'long' => $translator->trans($this->config['copy']['long']),
108 'short' => $translator->trans($this->config['copy']['short']),
109 'title' => $this->config['copy']['title']
110 ],
111 'page' => [
112 'description' => null,
113 'section' => null,
114 'title' => null
115 ],
116 'site' => [
117 'donate' => $this->config['site']['donate'],
118 'ico' => $this->config['site']['ico'],
119 'logo' => $this->config['site']['logo'],
120 'png' => $this->config['site']['png'],
121 'svg' => $this->config['site']['svg'],
122 'title' => $translator->trans($this->config['site']['title']),
123 'url' => $router->generate($this->config['site']['url'])
124 ],
125 'canonical' => null,
126 'alternates' => [],
127 'ogps' => [
128 'type' => 'article',
129 'site_name' => $this->translator->trans($this->config['site']['title'])
130 ],
131 'facebooks' => [
132 #'admins' => $this->config['facebook']['admins'],
133 'app_id' => $this->config['facebook']['apps']
134 ],
135 'forms' => []
136 ];
137
138 //Get current request
139 $this->request = $stack->getCurrentRequest();
140
141 //Get current locale
142 #$this->locale = $router->getContext()->getParameters()['_locale'];
143 $this->locale = $this->request->getLocale();
144
145 //Set translator locale
146 //XXX: allow LocaleSubscriber on the fly locale change for first page
147 $this->translator->setLocale($this->locale);
148
149 //Iterate on locales excluding current one
150 foreach($this->config['locales'] as $locale) {
151 //Set titles
152 $titles = [];
153
154 //Iterate on other locales
155 foreach(array_diff($this->config['locales'], [$locale]) as $other) {
156 $titles[$other] = $translator->trans($this->config['languages'][$locale], [], null, $other);
157 }
158
159 //Get context path
160 $path = $router->getContext()->getPathInfo();
161
162 //Retrieve route matching path
163 $route = $router->match($path);
164
165 //Get route name
166 $name = $route['_route'];
167
168 //Unset route name
169 unset($route['_route']);
170
171 //With current locale
172 if ($locale == $this->locale) {
173 //Set locale locales context
174 $this->context['canonical'] = $router->generate($name, ['_locale' => $locale]+$route, UrlGeneratorInterface::ABSOLUTE_URL);
175 } else {
176 //Set locale locales context
177 $this->context['alternates'][$locale] = [
178 'absolute' => $router->generate($name, ['_locale' => $locale]+$route, UrlGeneratorInterface::ABSOLUTE_URL),
179 'relative' => $router->generate($name, ['_locale' => $locale]+$route),
180 'title' => implode('/', $titles),
181 'translated' => $translator->trans($this->config['languages'][$locale], [], null, $locale)
182 ];
183 }
184
185 //Add shorter locale
186 if (empty($this->context['alternates'][$shortLocale = substr($locale, 0, 2)])) {
187 //Set locale locales context
188 $this->context['alternates'][$shortLocale] = [
189 'absolute' => $router->generate($name, ['_locale' => $locale]+$route, UrlGeneratorInterface::ABSOLUTE_URL),
190 'relative' => $router->generate($name, ['_locale' => $locale]+$route),
191 'title' => implode('/', $titles),
192 'translated' => $translator->trans($this->config['languages'][$locale], [], null, $locale)
193 ];
194 }
195 }
196 }
197
198 /**
199 * The about page
200 *
201 * @desc Display the about informations
202 *
203 * @return Response The rendered view
204 */
205 public function about(): Response {
206 //Set page
207 $this->context['page']['title'] = $this->translator->trans('About');
208
209 //Set description
210 $this->context['page']['description'] = $this->translator->trans('Libre Air about');
211
212 //Set keywords
213 $this->context['keywords'] = [
214 $this->translator->trans('about'),
215 $this->translator->trans('Libre Air')
216 ];
217
218 //Render template
219 $response = $this->render('@RapsysAir/default/about.html.twig', $this->context);
220 $response->setEtag(md5($response->getContent()));
221 $response->setPublic();
222 $response->isNotModified($this->request);
223
224 //Return response
225 return $response;
226 }
227
228 /**
229 * The contact page
230 *
231 * @desc Send a contact mail to configured contact
232 *
233 * @param Request $request The request instance
234 * @param MailerInterface $mailer The mailer instance
235 *
236 * @return Response The rendered view or redirection
237 */
238 public function contact(Request $request, MailerInterface $mailer): Response {
239 //Set page
240 $this->context['page']['title'] = $this->translator->trans('Contact');
241
242 //Set description
243 $this->context['page']['description'] = $this->translator->trans('Contact Libre Air');
244
245 //Set keywords
246 $this->context['keywords'] = [
247 $this->translator->trans('contact'),
248 $this->translator->trans('Libre Air'),
249 $this->translator->trans('outdoor'),
250 $this->translator->trans('Argentine Tango'),
251 $this->translator->trans('calendar')
252 ];
253
254 //Create the form according to the FormType created previously.
255 //And give the proper parameters
256 $form = $this->createForm('Rapsys\AirBundle\Form\ContactType', null, [
257 'action' => $this->generateUrl('rapsys_air_contact'),
258 'method' => 'POST'
259 ]);
260
261 if ($request->isMethod('POST')) {
262 // Refill the fields in case the form is not valid.
263 $form->handleRequest($request);
264
265 if ($form->isValid()) {
266 //Get data
267 $data = $form->getData();
268
269 //Create message
270 $message = (new TemplatedEmail())
271 //Set sender
272 ->from(new Address($data['mail'], $data['name']))
273 //Set recipient
274 ->to(new Address($this->context['contact']['mail'], $this->context['contact']['title']))
275 //Set subject
276 ->subject($data['subject'])
277
278 //Set path to twig templates
279 ->htmlTemplate('@RapsysAir/mail/contact.html.twig')
280 ->textTemplate('@RapsysAir/mail/contact.text.twig')
281
282 //Set context
283 ->context(
284 [
285 'subject' => $data['subject'],
286 'message' => strip_tags($data['message']),
287 ]+$this->context
288 );
289
290 //Try sending message
291 //XXX: mail delivery may silently fail
292 try {
293 //Send message
294 $mailer->send($message);
295
296 //Redirect on the same route with sent=1 to cleanup form
297 return $this->redirectToRoute($request->get('_route'), ['sent' => 1]+$request->get('_route_params'));
298 //Catch obvious transport exception
299 } catch(TransportExceptionInterface $e) {
300 if ($message = $e->getMessage()) {
301 //Add error message mail unreachable
302 $form->get('mail')->addError(new FormError($this->translator->trans('Unable to contact: %mail%: %message%', ['%mail%' => $this->context['contact']['mail'], '%message%' => $this->translator->trans($message)])));
303 } else {
304 //Add error message mail unreachable
305 $form->get('mail')->addError(new FormError($this->translator->trans('Unable to contact: %mail%', ['%mail%' => $this->context['contact']['mail']])));
306 }
307 }
308 }
309 }
310
311 //Render template
312 return $this->render('@RapsysAir/form/contact.html.twig', ['form' => $form->createView(), 'sent' => $request->query->get('sent', 0)]+$this->context);
313 }
314
315 /**
316 * The dispute page
317 *
318 * @desc Generate a dispute document
319 *
320 * @param Request $request The request instance
321 * @param MailerInterface $mailer The mailer instance
322 *
323 * @return Response The rendered view or redirection
324 */
325 public function dispute(Request $request, MailerInterface $mailer): Response {
326 //Prevent non-guest to access here
327 $this->denyAccessUnlessGranted('ROLE_USER', null, $this->translator->trans('Unable to access this page without role %role%!', ['%role%' => $this->translator->trans('User')]));
328
329 //Set page
330 $this->context['page']['title'] = $this->translator->trans('Dispute');
331
332 //Set description
333 $this->context['page']['description'] = $this->translator->trans('Libre Air dispute');
334
335 //Set keywords
336 $this->context['keywords'] = [
337 $this->translator->trans('dispute'),
338 $this->translator->trans('Libre Air'),
339 $this->translator->trans('outdoor'),
340 $this->translator->trans('Argentine Tango'),
341 $this->translator->trans('calendar')
342 ];
343
344 //Create the form according to the FormType created previously.
345 //And give the proper parameters
346 $form = $this->createForm('Rapsys\AirBundle\Form\DisputeType', ['court' => 'Paris', 'abstract' => 'Pour constater cette prĆ©tendue infraction, les agents verbalisateurs ont pĆ©nĆ©trĆ© dans un jardin privatif, sans visibilitĆ© depuis la voie publique, situĆ© derriĆØre un batiment privĆ©, pour ce faire ils ont franchi au moins un grillage de chantier ou des potteaux mĆ©talliques sĆ©parant le terrain privĆ© de la voie publique de l\'autre cĆ“tĆ© du batiment.'], [
347 'action' => $this->generateUrl('rapsys_air_dispute'),
348 'method' => 'POST'
349 ]);
350
351 if ($request->isMethod('POST')) {
352 // Refill the fields in case the form is not valid.
353 $form->handleRequest($request);
354
355 if ($form->isValid()) {
356 //Get data
357 $data = $form->getData();
358
359 //Gathering offense
360 if (!empty($data['offense']) && $data['offense'] == 'gathering') {
361 //Add gathering
362 $output = DisputePdf::genGathering($data['court'], $data['notice'], $data['agent'], $data['service'], $data['abstract'], $this->translator->trans($this->getUser()->getCivility()->getTitle()), $this->getUser()->getForename(), $this->getUser()->getSurname());
363 //Traffic offense
364 } elseif (!empty($data['offense'] && $data['offense'] == 'traffic')) {
365 //Add traffic
366 $output = DisputePdf::genTraffic($data['court'], $data['notice'], $data['agent'], $data['service'], $data['abstract'], $this->translator->trans($this->getUser()->getCivility()->getTitle()), $this->getUser()->getForename(), $this->getUser()->getSurname());
367 //Unsupported offense
368 } else {
369 header('Content-Type: text/plain');
370 die('TODO');
371 exit;
372 }
373
374 //Send common headers
375 header('Content-Type: application/pdf');
376
377 //Send remaining headers
378 header('Cache-Control: private, max-age=0, must-revalidate');
379 header('Pragma: public');
380
381 //Send content-length
382 header('Content-Length: '.strlen($output));
383
384 //Display the pdf
385 echo $output;
386
387 //Die for now
388 exit;
389
390 # //Create message
391 # $message = (new TemplatedEmail())
392 # //Set sender
393 # ->from(new Address($data['mail'], $data['name']))
394 # //Set recipient
395 # //XXX: remove the debug set in vendor/symfony/mime/Address.php +46
396 # ->to(new Address($this->config['contact']['mail'], $this->config['contact']['title']))
397 # //Set subject
398 # ->subject($data['subject'])
399 #
400 # //Set path to twig templates
401 # ->htmlTemplate('@RapsysAir/mail/contact.html.twig')
402 # ->textTemplate('@RapsysAir/mail/contact.text.twig')
403 #
404 # //Set context
405 # ->context(
406 # [
407 # 'subject' => $data['subject'],
408 # 'message' => strip_tags($data['message']),
409 # ]+$this->context
410 # );
411 #
412 # //Try sending message
413 # //XXX: mail delivery may silently fail
414 # try {
415 # //Send message
416 # $mailer->send($message);
417 #
418 # //Redirect on the same route with sent=1 to cleanup form
419 # return $this->redirectToRoute($request->get('_route'), ['sent' => 1]+$request->get('_route_params'));
420 # //Catch obvious transport exception
421 # } catch(TransportExceptionInterface $e) {
422 # if ($message = $e->getMessage()) {
423 # //Add error message mail unreachable
424 # $form->get('mail')->addError(new FormError($this->translator->trans('Unable to contact: %mail%: %message%', ['%mail%' => $this->config['contact']['mail'], '%message%' => $this->translator->trans($message)])));
425 # } else {
426 # //Add error message mail unreachable
427 # $form->get('mail')->addError(new FormError($this->translator->trans('Unable to contact: %mail%', ['%mail%' => $this->config['contact']['mail']])));
428 # }
429 # }
430 }
431 }
432
433 //Render template
434 return $this->render('@RapsysAir/default/dispute.html.twig', ['form' => $form->createView(), 'sent' => $request->query->get('sent', 0)]+$this->context);
435 }
436
437 /**
438 * The index page
439 *
440 * @desc Display all granted sessions with an application or login form
441 *
442 * @param Request $request The request instance
443 *
444 * @return Response The rendered view
445 */
446 public function index(Request $request): Response {
447 //Fetch doctrine
448 $doctrine = $this->getDoctrine();
449
450 //Set page
451 $this->context['page']['title'] = $this->translator->trans('Argentine Tango in Paris');
452
453 //Set description
454 $this->context['page']['description'] = $this->translator->trans('Outdoor Argentine Tango session calendar in Paris');
455
456 //Set keywords
457 $this->context['keywords'] = [
458 $this->translator->trans('Argentine Tango'),
459 $this->translator->trans('Paris'),
460 $this->translator->trans('outdoor'),
461 $this->translator->trans('calendar'),
462 $this->translator->trans('Libre Air')
463 ];
464
465 //Set type
466 //XXX: only valid for home page
467 $this->context['ogps']['type'] = 'website';
468
469 //Compute period
470 $period = new \DatePeriod(
471 //Start from first monday of week
472 new \DateTime('Monday this week'),
473 //Iterate on each day
474 new \DateInterval('P1D'),
475 //End with next sunday and 4 weeks
476 new \DateTime(
477 $this->isGranted('IS_AUTHENTICATED_REMEMBERED')?'Monday this week + 3 week':'Monday this week + 2 week'
478 )
479 );
480
481 //Fetch calendar
482 $calendar = $doctrine->getRepository(Session::class)->fetchCalendarByDatePeriod($this->translator, $period, null, $request->get('session'), !$this->isGranted('IS_AUTHENTICATED_REMEMBERED'));
483
484 //Fetch locations
485 //XXX: we want to display all active locations anyway
486 $locations = $doctrine->getRepository(Location::class)->findTranslatedSortedByPeriod($this->translator, $period);
487
488 //Render the view
489 return $this->render('@RapsysAir/default/index.html.twig', ['calendar' => $calendar, 'locations' => $locations]+$this->context);
490
491 //Set Cache-Control must-revalidate directive
492 //TODO: add a javascript forced refresh after 1h ? or header refresh ?
493 #$response->setPublic(true);
494 #$response->setMaxAge(300);
495 #$response->mustRevalidate();
496 ##$response->setCache(['public' => true, 'max_age' => 300]);
497
498 //Return the response
499 #return $response;
500 }
501
502 /**
503 * The organizer regulation page
504 *
505 * @desc Display the organizer regulation policy
506 *
507 * @return Response The rendered view
508 */
509 public function organizerRegulation(): Response {
510 //Set page
511 $this->context['page']['title'] = $this->translator->trans('Organizer regulation');
512
513 //Set description
514 $this->context['page']['description'] = $this->translator->trans('Libre Air organizer regulation');
515
516 //Set keywords
517 $this->context['keywords'] = [
518 $this->translator->trans('organizer regulation'),
519 $this->translator->trans('Libre Air')
520 ];
521
522 //Render template
523 $response = $this->render('@RapsysAir/default/organizer_regulation.html.twig', $this->context);
524
525 //Set as cachable
526 $response->setEtag(md5($response->getContent()));
527 $response->setPublic();
528 $response->isNotModified($this->request);
529
530 //Return response
531 return $response;
532 }
533
534 /**
535 * The terms of service page
536 *
537 * @desc Display the terms of service policy
538 *
539 * @return Response The rendered view
540 */
541 public function termsOfService(): Response {
542 //Set page
543 $this->context['page']['title'] = $this->translator->trans('Terms of service');
544
545 //Set description
546 $this->context['page']['description'] = $this->translator->trans('Libre Air terms of service');
547
548 //Set keywords
549 $this->context['keywords'] = [
550 $this->translator->trans('terms of service'),
551 $this->translator->trans('Libre Air')
552 ];
553
554 //Render template
555 $response = $this->render('@RapsysAir/default/terms_of_service.html.twig', $this->context);
556
557 //Set as cachable
558 $response->setEtag(md5($response->getContent()));
559 $response->setPublic();
560 $response->isNotModified($this->request);
561
562 //Return response
563 return $response;
564 }
565
566 /**
567 * The frequently asked questions page
568 *
569 * @desc Display the frequently asked questions
570 *
571 * @return Response The rendered view
572 */
573 public function frequentlyAskedQuestions(): Response {
574 //Set page
575 $this->context['page']['title'] = $this->translator->trans('Frequently asked questions');
576
577 //Set description
578 $this->context['page']['description'] = $this->translator->trans('Libre Air frequently asked questions');
579
580 //Set keywords
581 $this->context['keywords'] = [
582 $this->translator->trans('frequently asked questions'),
583 $this->translator->trans('faq'),
584 $this->translator->trans('Libre Air')
585 ];
586
587 //Render template
588 $response = $this->render('@RapsysAir/default/frequently_asked_questions.html.twig', $this->context);
589
590 //Set as cachable
591 $response->setEtag(md5($response->getContent()));
592 $response->setPublic();
593 $response->isNotModified($this->request);
594
595 //Return response
596 return $response;
597 }
598
599 /**
600 * Return the bundle alias
601 *
602 * {@inheritdoc}
603 */
604 public function getAlias(): string {
605 return 'rapsys_air';
606 }
607
608 /**
609 * Return the facebook image
610 *
611 * @desc Generate image in jpeg format or load it from cache
612 *
613 * @return array The image array
614 */
615 protected function getFacebookImage(): array {
616 //Set texts
617 $texts = $this->facebookImage['texts'] ?? [];
618
619 //Set default source
620 $source = $this->facebookImage['source'] ?? 'png/facebook.png';
621
622 //Set default source
623 $updated = $this->facebookImage['updated'] ?? strtotime('last week');
624
625 //Set default destination
626 //XXX: format facebook<pathinfo>.jpeg
627 //XXX: was facebook/<controller>/<action>.<locale>.jpeg
628 $destination = $this->facebookImage['destination'] ?? 'facebook'.$this->request->getPathInfo().'.jpeg';
629
630 //Set source path
631 $src = $this->config['path']['public'].'/'.$source;
632
633 //Set cache path
634 //XXX: remove extension and store as png anyway
635 $cache = $this->config['path']['cache'].'/facebook/'.substr($source, 0, strrpos($source, '.')).'.'.$this->config['facebook']['width'].'x'.$this->config['facebook']['height'].'.png';
636
637 //Set destination path
638 $dest = $this->config['path']['public'].'/'.$destination;
639
640 //Set asset
641 $asset = '@RapsysAir/'.$destination;
642
643 //With up to date generated image
644 if (
645 is_file($dest) &&
646 ($stat = stat($dest)) &&
647 $stat['mtime'] >= $updated
648 ) {
649 //Get image size
650 list ($width, $height) = getimagesize($dest);
651
652 //With canonical in texts
653 if (!empty($texts[$this->context['canonical']])) {
654 //Prevent canonical to finish in alt
655 unset($texts[$this->context['canonical']]);
656 }
657
658 //Return image data
659 return [
660 #'image' => $this->stack->getCurrentRequest()->getUriForPath($this->asset->getUrl($asset), true),#.'?fbrefresh='.$stat['mtime'],
661 'image:url' => $this->stack->getCurrentRequest()->getUriForPath($this->asset->getUrl($asset), true),#.'?fbrefresh='.$stat['mtime'],
662 #'image:secure_url' => $this->stack->getCurrentRequest()->getUriForPath($this->asset->getUrl($asset), true),#.'?fbrefresh='.$stat['mtime'],
663 'image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))),
664 'image:height' => $height,
665 'image:width' => $width
666 ];
667 //With image candidate
668 } elseif (is_file($src)) {
669 //Create image object
670 $image = new \Imagick();
671
672 //With cache image
673 if (is_file($cache)) {
674 //Read image
675 $image->readImage($cache);
676 //Without we generate it
677 } else {
678 //Check target directory
679 if (!is_dir($dir = dirname($cache))) {
680 //Create filesystem object
681 $filesystem = new Filesystem();
682
683 try {
684 //Create dir
685 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
686 $filesystem->mkdir($dir, 0775);
687 } catch (IOExceptionInterface $e) {
688 //Throw error
689 throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
690 }
691 }
692
693 //Read image
694 $image->readImage($src);
695
696 //Crop using aspect ratio
697 //XXX: for better result upload image directly in aspect ratio :)
698 $image->cropThumbnailImage($this->config['facebook']['width'], $this->config['facebook']['height']);
699
700 //Strip image exif data and properties
701 $image->stripImage();
702
703 //Save cache image
704 if (!$image->writeImage($cache)) {
705 //Throw error
706 throw new \Exception(sprintf('Unable to write image "%s"', $cache));
707 }
708 }
709 //Check target directory
710 if (!is_dir($dir = dirname($dest))) {
711 //Create filesystem object
712 $filesystem = new Filesystem();
713
714 try {
715 //Create dir
716 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
717 $filesystem->mkdir($dir, 0775);
718 } catch (IOExceptionInterface $e) {
719 //Throw error
720 throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
721 }
722 }
723
724 //Get image width
725 $width = $image->getImageWidth();
726
727 //Get image height
728 $height = $image->getImageHeight();
729
730 //Create draw
731 $draw = new \ImagickDraw();
732
733 //Set stroke antialias
734 $draw->setStrokeAntialias(true);
735
736 //Set text antialias
737 $draw->setTextAntialias(true);
738
739 //Set stroke width
740 $draw->setStrokeWidth($this->facebookImage['stroke']??15);
741
742 //Set font aliases
743 $fonts = [
744 'irishgrover' => $this->config['path']['public'].'/ttf/irishgrover.v10.ttf',
745 'droidsans' => $this->config['path']['public'].'/ttf/droidsans.regular.ttf',
746 'dejavusans' => $this->config['path']['public'].'/ttf/dejavusans.2.37.ttf',
747 'labelleaurore' => $this->config['path']['public'].'/ttf/labelleaurore.v10.ttf'
748 ];
749
750 //Set align aliases
751 $aligns = [
752 'left' => \Imagick::ALIGN_LEFT,
753 'center' => \Imagick::ALIGN_CENTER,
754 'right' => \Imagick::ALIGN_RIGHT
755 ];
756
757 //Set default font
758 $defaultFont = 'dejavusans';
759
760 //Set default align
761 $defaultAlign = 'center';
762
763 //Set default size
764 $defaultSize = 60;
765
766 //Set default stroke
767 $defaultStroke = '#00c3f9';
768
769 //Set default fill
770 $defaultFill = 'white';
771
772 //Init counter
773 $i = 1;
774
775 //Set text count
776 $count = count($texts);
777
778 //Draw each text stroke
779 foreach($texts as $text => $data) {
780 //Set font
781 $draw->setFont($fonts[$data['font']??$defaultFont]);
782
783 //Set font size
784 $draw->setFontSize($data['size']??$defaultSize);
785
786 //Set text alignment
787 $draw->setTextAlignment($align = ($aligns[$data['align']??$defaultAlign]));
788
789 //Get font metrics
790 $metrics = $image->queryFontMetrics($draw, $text);
791
792 //Without y
793 if (empty($data['y'])) {
794 //Position verticaly each text evenly
795 $texts[$text]['y'] = $data['y'] = (($height + 100) / (count($texts) + 1) * $i) - 50;
796 }
797
798 //Without x
799 if (empty($data['x'])) {
800 if ($align == \Imagick::ALIGN_CENTER) {
801 $texts[$text]['x'] = $data['x'] = $width/2;
802 } elseif ($align == \Imagick::ALIGN_LEFT) {
803 $texts[$text]['x'] = $data['x'] = 50;
804 } elseif ($align == \Imagick::ALIGN_RIGHT) {
805 $texts[$text]['x'] = $data['x'] = $width - 50;
806 }
807 }
808
809 //Center verticaly
810 //XXX: add ascender part then center it back by half of textHeight
811 //TODO: maybe add a boundingbox ???
812 $texts[$text]['y'] = $data['y'] += $metrics['ascender'] - $metrics['textHeight']/2;
813
814 //Set stroke color
815 $draw->setStrokeColor(new \ImagickPixel($data['stroke']??$defaultStroke));
816
817 //Set fill color
818 $draw->setFillColor(new \ImagickPixel($data['stroke']??$defaultStroke));
819
820 //Add annotation
821 $draw->annotation($data['x'], $data['y'], $text);
822
823 //Increase counter
824 $i++;
825 }
826
827 //Create stroke object
828 $stroke = new \Imagick();
829
830 //Add new image
831 $stroke->newImage($width, $height, new \ImagickPixel('transparent'));
832
833 //Draw on image
834 $stroke->drawImage($draw);
835
836 //Blur image
837 //XXX: blur the stroke canvas only
838 $stroke->blurImage(5,3);
839
840 //Set opacity to 0.5
841 //XXX: see https://www.php.net/manual/en/image.evaluateimage.php
842 $stroke->evaluateImage(\Imagick::EVALUATE_DIVIDE, 1.5, \Imagick::CHANNEL_ALPHA);
843
844 //Compose image
845 $image->compositeImage($stroke, \Imagick::COMPOSITE_OVER, 0, 0);
846
847 //Clear stroke
848 $stroke->clear();
849
850 //Destroy stroke
851 unset($stroke);
852
853 //Clear draw
854 $draw->clear();
855
856 //Set text antialias
857 $draw->setTextAntialias(true);
858
859 //Draw each text
860 foreach($texts as $text => $data) {
861 //Set font
862 $draw->setFont($fonts[$data['font']??$defaultFont]);
863
864 //Set font size
865 $draw->setFontSize($data['size']??$defaultSize);
866
867 //Set text alignment
868 $draw->setTextAlignment($aligns[$data['align']??$defaultAlign]);
869
870 //Set fill color
871 $draw->setFillColor(new \ImagickPixel($data['fill']??$defaultFill));
872
873 //Add annotation
874 $draw->annotation($data['x'], $data['y'], $text);
875 }
876
877 //Draw on image
878 $image->drawImage($draw);
879
880 //Strip image exif data and properties
881 $image->stripImage();
882
883 //Set image format
884 $image->setImageFormat('jpeg');
885
886 //Save image
887 if (!$image->writeImage($dest)) {
888 //Throw error
889 throw new \Exception(sprintf('Unable to write image "%s"', $dest));
890 }
891
892 //Get dest stat
893 //TODO: see if it works every time
894 $stat = stat($dest);
895
896 //With canonical in texts
897 if (!empty($texts[$this->context['canonical']])) {
898 //Prevent canonical to finish in alt
899 unset($texts[$this->context['canonical']]);
900 }
901
902 //Return image data
903 return [
904 //TODO: see if it works every time
905 #'image' => $this->stack->getCurrentRequest()->getUriForPath($this->asset->getUrl($asset), true),#.'?fbrefresh='.$stat['mtime'],
906 'image:url' => $this->stack->getCurrentRequest()->getUriForPath($this->asset->getUrl($asset), true),#.'?fbrefresh='.$stat['mtime'],
907 #'image:secure_url' => $this->stack->getCurrentRequest()->getUriForPath($this->asset->getUrl($asset), true),#.'?fbrefresh='.$stat['mtime'],
908 'image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))),
909 'image:height' => $height,
910 'image:width' => $width
911 ];
912 }
913
914 //Return empty array without image
915 return [];
916 }
917
918 /**
919 * Renders a view
920 *
921 * {@inheritdoc}
922 */
923 protected function render(string $view, array $parameters = [], Response $response = null): Response {
924 //Create application form for role_guest
925 if ($this->isGranted('ROLE_GUEST')) {
926 //Without application form
927 if (empty($parameters['forms']['application'])) {
928 //Fetch doctrine
929 $doctrine = $this->getDoctrine();
930
931 //Create ApplicationType form
932 $application = $this->createForm('Rapsys\AirBundle\Form\ApplicationType', null, [
933 //Set the action
934 'action' => $this->generateUrl('rapsys_air_application_add'),
935 //Set the form attribute
936 'attr' => [ 'class' => 'col' ],
937 //Set admin
938 'admin' => $this->isGranted('ROLE_ADMIN'),
939 //Set default user to current
940 'user' => $this->getUser()->getId(),
941 //Set default slot to evening
942 //XXX: default to Evening (3)
943 'slot' => $doctrine->getRepository(Slot::class)->findOneById(3)
944 ]);
945
946 //Add form to context
947 $parameters['forms']['application'] = $application->createView();
948 }
949 //Create login form for anonymous
950 } elseif (!$this->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
951 //Create ApplicationType form
952 $login = $this->createForm('Rapsys\UserBundle\Form\LoginType', null, [
953 //Set the action
954 'action' => $this->generateUrl('rapsys_user_login'),
955 //Set the form attribute
956 'attr' => [ 'class' => 'col' ]
957 ]);
958
959 //Add form to context
960 $parameters['forms']['login'] = $login->createView();
961 }
962
963 //With page infos and without facebook image
964 if (empty($this->facebookImage) && !empty($parameters['site']['title']) && !empty($parameters['page']['title']) && !empty($parameters['canonical'])) {
965 //Set facebook image
966 $this->facebookImage = [
967 'texts' => [
968 $parameters['site']['title'] => [
969 'font' => 'irishgrover',
970 'size' => 110
971 ],
972 $parameters['page']['title'] => [
973 'align' => 'left'
974 ],
975 $parameters['canonical'] => [
976 'align' => 'right',
977 'font' => 'labelleaurore',
978 'size' => 50
979 ]
980 ]
981 ];
982 }
983
984 //With canonical
985 if (!empty($parameters['canonical'])) {
986 //Set facebook url
987 $parameters['ogps']['url'] = $parameters['canonical'];
988 }
989
990 //With page title
991 if (!empty($parameters['page']['title'])) {
992 //Set facebook title
993 $parameters['ogps']['title'] = $parameters['page']['title'];
994 }
995
996 //With page description
997 if (!empty($parameters['page']['description'])) {
998 //Set facebook description
999 $parameters['ogps']['description'] = $parameters['page']['description'];
1000 }
1001
1002 //With locale
1003 if (!empty($this->locale)) {
1004 //Set facebook locale
1005 $parameters['ogps']['locale'] = str_replace('-', '_', $this->locale);
1006
1007 //With alternates
1008 //XXX: disabled as we don't support fb_locale=xx_xx
1009 //XXX: see https://stackoverflow.com/questions/20827882/in-open-graph-markup-whats-the-use-of-oglocalealternate-without-the-locati
1010 #if (!empty($parameters['alternates'])) {
1011 # //Iterate on alternates
1012 # foreach($parameters['alternates'] as $lang => $alternate) {
1013 # if (strlen($lang) == 5) {
1014 # //Set facebook locale alternate
1015 # $parameters['ogps']['locale:alternate'] = str_replace('-', '_', $lang);
1016 # }
1017 # }
1018 #}
1019 }
1020
1021 //With facebook image defined
1022 if (!empty($this->facebookImage)) {
1023 //Get facebook image
1024 $parameters['ogps'] += $this->getFacebookImage();
1025 }
1026
1027 //Call parent method
1028 return $this->_render($view, $parameters, $response);
1029 }
1030 }