]> Raphaƫl G. Git Repositories - airbundle/blob - Controller/DefaultController.php
Add register template
[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 }