1 <?php 
declare(strict_types
=1); 
   4  * This file is part of the Rapsys AirBundle package. 
   6  * (c) Raphaël Gertz <symfony@rapsys.eu> 
   8  * For the full copyright and license information, please view the LICENSE 
   9  * file that was distributed with this source code. 
  12 namespace Rapsys\AirBundle\Controller
; 
  14 use Doctrine\Persistence\ManagerRegistry
; 
  15 use Psr\Log\LoggerInterface
; 
  16 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController 
as BaseAbstractController
; 
  17 use Symfony\Bundle\FrameworkBundle\Controller\ControllerTrait
; 
  18 use Symfony\Component\Asset\PackageInterface
; 
  19 use Symfony\Component\DependencyInjection\ContainerInterface
; 
  20 use Symfony\Component\Filesystem\Exception\IOExceptionInterface
; 
  21 use Symfony\Component\Filesystem\Filesystem
; 
  22 use Symfony\Component\HttpFoundation\RequestStack
; 
  23 use Symfony\Component\HttpFoundation\Response
; 
  24 use Symfony\Component\Routing\Generator\UrlGeneratorInterface
; 
  25 use Symfony\Component\Routing\RouterInterface
; 
  26 use Symfony\Component\Translation\TranslatorInterface
; 
  27 use Symfony\Contracts\Service\ServiceSubscriberInterface
; 
  29 use Rapsys\AirBundle\Entity\Dance
; 
  30 use Rapsys\AirBundle\Entity\Location
; 
  31 use Rapsys\AirBundle\Entity\Slot
; 
  32 use Rapsys\AirBundle\Entity\User
; 
  33 use Rapsys\AirBundle\RapsysAirBundle
; 
  36  * Provides common features needed in controllers. 
  40 abstract class AbstractController 
extends BaseAbstractController 
implements ServiceSubscriberInterface 
{ 
  42                 //Rename render as baseRender 
  43                 render 
as protected baseRender
; 
  49         ///ContainerInterface instance 
  58         ///Translator instance 
  59         protected $translator; 
  64          * Stores container, router and translator interfaces 
  66          * Prepares context tree 
  68          * @param ContainerInterface $container The container instance 
  70         public function __construct(ContainerInterface 
$container) { 
  72                 $this->config 
= $container->getParameter(RapsysAirBundle
::getAlias()); 
  75                 $this->container 
= $container; 
  78                 $this->router 
= $container->get('router'); 
  81                 $this->translator 
= $container->get('translator'); 
  85                         'description' => null, 
  89                                 'title' => $this->translator
->trans($this->config
['contact']['title']), 
  90                                 'mail' => $this->config
['contact']['mail'] 
  93                                 'by' => $this->translator
->trans($this->config
['copy']['by']), 
  94                                 'link' => $this->config
['copy']['link'], 
  95                                 'long' => $this->translator
->trans($this->config
['copy']['long']), 
  96                                 'short' => $this->translator
->trans($this->config
['copy']['short']), 
  97                                 'title' => $this->config
['copy']['title'] 
 100                                 'donate' => $this->config
['site']['donate'], 
 101                                 'ico' => $this->config
['site']['ico'], 
 102                                 'logo' => $this->config
['site']['logo'], 
 103                                 'png' => $this->config
['site']['png'], 
 104                                 'svg' => $this->config
['site']['svg'], 
 105                                 'title' => $this->translator
->trans($this->config
['site']['title']), 
 106                                 'url' => $this->router
->generate($this->config
['site']['url']) 
 112                                         'og' => 'http://ogp.me/ns#', 
 113                                         'fb' => 'http://ogp.me/ns/fb#' 
 116                                         'og:type' => 'article', 
 117                                         'og:site_name' => $this->translator
->trans($this->config
['site']['title']), 
 118                                         #'fb:admins' => $this->config['facebook']['admins'], 
 119                                         'fb:app_id' => $this->config
['facebook']['apps'] 
 128          * Return the facebook image 
 130          * @desc Generate image in jpeg format or load it from cache 
 132          * @param string $pathInfo The request path info 
 133          * @param array $parameters The image parameters 
 134          * @return array The image array 
 136         protected function getFacebookImage(string $pathInfo, array $parameters = []): array { 
 138                 //XXX: require asset package to be public 
 139                 $package = $this->container
->get('rapsys_pack.path_package'); 
 142                 $texts = $parameters['texts'] ?? []; 
 145                 $source = $parameters['source'] ?? 'png/facebook.png'; 
 148                 $updated = $parameters['updated'] ?? strtotime('last week'); 
 151                 $src = $this->config
['path']['public'].'/'.$source; 
 154                 //XXX: remove extension and store as png anyway 
 155                 $cache = $this->config
['path']['cache'].'/facebook/'.substr($source, 0, strrpos($source, '.')).'.'.$this->config
['facebook']['width'].'x'.$this->config
['facebook']['height'].'.png'; 
 157                 //Set destination path 
 158                 //XXX: format <public>/facebook<pathinfo>.jpeg 
 159                 //XXX: was <public>/facebook/<controller>/<action>.<locale>.jpeg 
 160                 $dest = $this->config
['path']['public'].'/facebook'.$pathInfo.'.jpeg'; 
 162                 //With up to date generated image 
 165                         ($stat = stat($dest)) && 
 166                         $stat['mtime'] >= $updated 
 169                         list ($width, $height) = getimagesize($dest); 
 172                         foreach($texts as $text => $data) { 
 173                                 //With canonical text 
 174                                 if (!empty($data['canonical'])) { 
 175                                         //Prevent canonical to finish in alt 
 176                                         unset($texts[$text]); 
 182                                 'og:image' => $package->getAbsoluteUrl('@RapsysAir/facebook/'.$stat['mtime'].$pathInfo.'.jpeg'), 
 183                                 'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))), 
 184                                 'og:image:height' => $height, 
 185                                 'og:image:width' => $width 
 187                 //With image candidate 
 188                 } elseif (is_file($src)) { 
 189                         //Create image object 
 190                         $image = new \
Imagick(); 
 193                         if (is_file($cache)) { 
 195                                 $image->readImage($cache); 
 196                         //Without we generate it 
 198                                 //Check target directory 
 199                                 if (!is_dir($dir = dirname($cache))) { 
 200                                         //Create filesystem object 
 201                                         $filesystem = new Filesystem(); 
 205                                                 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755) 
 206                                                 $filesystem->mkdir($dir, 0775); 
 207                                         } catch (IOExceptionInterface 
$e) { 
 209                                                 throw new \
Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e); 
 214                                 $image->readImage($src); 
 216                                 //Crop using aspect ratio 
 217                                 //XXX: for better result upload image directly in aspect ratio :) 
 218                                 $image->cropThumbnailImage($this->config
['facebook']['width'], $this->config
['facebook']['height']); 
 220                                 //Strip image exif data and properties 
 221                                 $image->stripImage(); 
 224                                 if (!$image->writeImage($cache)) { 
 226                                         throw new \
Exception(sprintf('Unable to write image "%s"', $cache)); 
 229                         //Check target directory 
 230                         if (!is_dir($dir = dirname($dest))) { 
 231                                 //Create filesystem object 
 232                                 $filesystem = new Filesystem(); 
 236                                         //XXX: set as 0775, symfony umask (0022) will reduce rights (0755) 
 237                                         $filesystem->mkdir($dir, 0775); 
 238                                 } catch (IOExceptionInterface 
$e) { 
 240                                         throw new \
Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e); 
 245                         $width = $image->getImageWidth(); 
 248                         $height = $image->getImageHeight(); 
 251                         $draw = new \
ImagickDraw(); 
 253                         //Set stroke antialias 
 254                         $draw->setStrokeAntialias(true); 
 257                         $draw->setTextAntialias(true); 
 261                                 'irishgrover' => $this->config
['path']['public'].'/ttf/irishgrover.v10.ttf', 
 262                                 'droidsans' => $this->config
['path']['public'].'/ttf/droidsans.regular.ttf', 
 263                                 'dejavusans' => $this->config
['path']['public'].'/ttf/dejavusans.2.37.ttf', 
 264                                 'labelleaurore' => $this->config
['path']['public'].'/ttf/labelleaurore.v10.ttf' 
 269                                 'left' => \Imagick
::ALIGN_LEFT
, 
 270                                 'center' => \Imagick
::ALIGN_CENTER
, 
 271                                 'right' => \Imagick
::ALIGN_RIGHT
 
 275                         $defaultFont = 'dejavusans'; 
 278                         $defaultAlign = 'center'; 
 284                         $defaultStroke = '#00c3f9'; 
 290                         $defaultFill = 'white'; 
 296                         $count = count($texts); 
 298                         //Draw each text stroke 
 299                         foreach($texts as $text => $data) { 
 301                                 $draw->setFont($fonts[$data['font']??$defaultFont]); 
 304                                 $draw->setFontSize($data['size']??$defaultSize); 
 307                                 $draw->setStrokeWidth($data['width']??$defaultWidth); 
 310                                 $draw->setTextAlignment($align = ($aligns[$data['align']??$defaultAlign])); 
 313                                 $metrics = $image->queryFontMetrics($draw, $text); 
 316                                 if (empty($data['y'])) { 
 317                                         //Position verticaly each text evenly 
 318                                         $texts[$text]['y'] = $data['y'] = (($height + 
100) / (count($texts) + 
1) * $i) - 50; 
 322                                 if (empty($data['x'])) { 
 323                                         if ($align == \Imagick
::ALIGN_CENTER
) { 
 324                                                 $texts[$text]['x'] = $data['x'] = $width/2; 
 325                                         } elseif ($align == \Imagick
::ALIGN_LEFT
) { 
 326                                                 $texts[$text]['x'] = $data['x'] = 50; 
 327                                         } elseif ($align == \Imagick
::ALIGN_RIGHT
) { 
 328                                                 $texts[$text]['x'] = $data['x'] = $width - 50; 
 333                                 //XXX: add ascender part then center it back by half of textHeight 
 334                                 //TODO: maybe add a boundingbox ??? 
 335                                 $texts[$text]['y'] = $data['y'] +
= $metrics['ascender'] - $metrics['textHeight']/2; 
 338                                 $draw->setStrokeColor(new \
ImagickPixel($data['stroke']??$defaultStroke)); 
 341                                 $draw->setFillColor(new \
ImagickPixel($data['stroke']??$defaultStroke)); 
 344                                 $draw->annotation($data['x'], $data['y'], $text); 
 350                         //Create stroke object 
 351                         $stroke = new \
Imagick(); 
 354                         $stroke->newImage($width, $height, new \
ImagickPixel('transparent')); 
 357                         $stroke->drawImage($draw); 
 360                         //XXX: blur the stroke canvas only 
 361                         $stroke->blurImage(5,3); 
 364                         //XXX: see https://www.php.net/manual/en/image.evaluateimage.php 
 365                         $stroke->evaluateImage(\Imagick
::EVALUATE_DIVIDE
, 1.5, \Imagick
::CHANNEL_ALPHA
); 
 368                         $image->compositeImage($stroke, \Imagick
::COMPOSITE_OVER
, 0, 0); 
 380                         $draw->setTextAntialias(true); 
 383                         foreach($texts as $text => $data) { 
 385                                 $draw->setFont($fonts[$data['font']??$defaultFont]); 
 388                                 $draw->setFontSize($data['size']??$defaultSize); 
 391                                 $draw->setTextAlignment($aligns[$data['align']??$defaultAlign]); 
 394                                 $draw->setFillColor(new \
ImagickPixel($data['fill']??$defaultFill)); 
 397                                 $draw->annotation($data['x'], $data['y'], $text); 
 399                                 //With canonical text 
 400                                 if (!empty($data['canonical'])) { 
 401                                         //Prevent canonical to finish in alt 
 402                                         unset($texts[$text]); 
 407                         $image->drawImage($draw); 
 409                         //Strip image exif data and properties 
 410                         $image->stripImage(); 
 413                         $image->setImageFormat('jpeg'); 
 416                         if (!$image->writeImage($dest)) { 
 418                                 throw new \
Exception(sprintf('Unable to write image "%s"', $dest)); 
 426                                 'og:image' => $package->getAbsoluteUrl('@RapsysAir/facebook/'.$stat['mtime'].$pathInfo.'.jpeg'), 
 427                                 'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))), 
 428                                 'og:image:height' => $height, 
 429                                 'og:image:width' => $width 
 433                 //Return empty array without image 
 442         protected function render(string $view, array $parameters = [], Response 
$response = null): Response 
{ 
 444                 $stack = $this->container
->get('request_stack'); 
 446                 //Get current request 
 447                 $request = $stack->getCurrentRequest(); 
 450                 $locale = $request->getLocale(); 
 453                 $parameters['locale'] = str_replace('_', '-', $locale); 
 456                 $pathInfo = $this->router
->getContext()->getPathInfo(); 
 458                 //Iterate on locales excluding current one 
 459                 foreach($this->config
['locales'] as $current) { 
 463                         //Iterate on other locales 
 464                         foreach(array_diff($this->config
['locales'], [$current]) as $other) { 
 465                                 $titles[$other] = $this->translator
->trans($this->config
['languages'][$current], [], null, $other); 
 468                         //Retrieve route matching path 
 469                         $route = $this->router
->match($pathInfo); 
 472                         $name = $route['_route']; 
 475                         unset($route['_route']); 
 477                         //With current locale 
 478                         if ($current == $locale) { 
 479                                 //Set locale locales context 
 480                                 $parameters['canonical'] = $this->router
->generate($name, ['_locale' => $current]+
$route, UrlGeneratorInterface
::ABSOLUTE_URL
); 
 482                                 //Set locale locales context 
 483                                 $parameters['alternates'][str_replace('_', '-', $current)] = [ 
 484                                         'absolute' => $this->router
->generate($name, ['_locale' => $current]+
$route, UrlGeneratorInterface
::ABSOLUTE_URL
), 
 485                                         'relative' => $this->router
->generate($name, ['_locale' => $current]+
$route), 
 486                                         'title' => implode('/', $titles), 
 487                                         'translated' => $this->translator
->trans($this->config
['languages'][$current], [], null, $current) 
 492                         if (empty($parameters['alternates'][$shortCurrent = substr($current, 0, 2)])) { 
 493                                 //Set locale locales context 
 494                                 $parameters['alternates'][$shortCurrent] = [ 
 495                                         'absolute' => $this->router
->generate($name, ['_locale' => $current]+
$route, UrlGeneratorInterface
::ABSOLUTE_URL
), 
 496                                         'relative' => $this->router
->generate($name, ['_locale' => $current]+
$route), 
 497                                         'title' => implode('/', $titles), 
 498                                         'translated' => $this->translator
->trans($this->config
['languages'][$current], [], null, $current) 
 503                 //Create application form for role_guest 
 504                 if ($this->isGranted('ROLE_GUEST')) { 
 505                         //Without application form 
 506                         if (empty($parameters['forms']['application'])) { 
 508                                 $doctrine = $this->get('doctrine'); 
 510                                 //Get favorites dances 
 511                                 $danceFavorites = $doctrine->getRepository(Dance
::class)->findByUserId($this->getUser()->getId()); 
 514                                 $danceDefault = !empty($danceFavorites)?current($danceFavorites):null; 
 516                                 //Get favorites locations 
 517                                 $locationFavorites = $doctrine->getRepository(Location
::class)->findByUserId($this->getUser()->getId()); 
 519                                 //Set location default 
 520                                 $locationDefault = !empty($locationFavorites)?current($locationFavorites):null; 
 523                                 if ($this->isGranted('ROLE_ADMIN')) { 
 525                                         $dances = $doctrine->getRepository(Dance
::class)->findAll(); 
 528                                         $locations = $doctrine->getRepository(Location
::class)->findAll(); 
 531                                         //Restrict to favorite dances 
 532                                         $dances = $danceFavorites; 
 535                                         $danceFavorites = []; 
 537                                         //Restrict to favorite locations 
 538                                         $locations = $locationFavorites; 
 541                                         $locationFavorites = []; 
 544                                 //With session location id 
 545                                 //XXX: set in session controller 
 546                                 if (!empty($parameters['session']['location']['id'])) { 
 547                                         //Iterate on each location 
 548                                         foreach($locations as $location) { 
 550                                                 if ($location->getId() == $parameters['session']['location']['id']) { 
 551                                                         //Set location as default 
 552                                                         $locationDefault = $location; 
 560                                 //Create ApplicationType form 
 561                                 $application = $this->createForm('Rapsys\AirBundle\Form\ApplicationType', null, [ 
 563                                         'action' => $this->generateUrl('rapsys_air_application_add'), 
 564                                         //Set the form attribute 
 565                                         'attr' => [ 'class' => 'col' ], 
 567                                         'dance_choices' => $dances, 
 569                                         'dance_default' => $danceDefault, 
 570                                         //Set dance favorites 
 571                                         'dance_favorites' => $danceFavorites, 
 572                                         //Set location choices 
 573                                         'location_choices' => $locations, 
 574                                         //Set location default 
 575                                         'location_default' => $locationDefault, 
 576                                         //Set location favorites 
 577                                         'location_favorites' => $locationFavorites, 
 579                                         'user' => $this->isGranted('ROLE_ADMIN'), 
 581                                         'user_choices' => $doctrine->getRepository(User
::class)->findAllWithTranslatedGroupAndCivility($this->translator
), 
 582                                         //Set default user to current 
 583                                         'user_default' => $this->getUser()->getId(), 
 584                                         //Set to session slot or evening by default 
 585                                         //XXX: default to Evening (3) 
 586                                         'slot_default' => $doctrine->getRepository(Slot
::class)->findOneById($parameters['session']['slot']['id']??3) 
 589                                 //Add form to context 
 590                                 $parameters['forms']['application'] = $application->createView(); 
 592                 //Create login form for anonymous 
 593                 } elseif (!$this->isGranted('IS_AUTHENTICATED_REMEMBERED')) { 
 594                         //Create LoginType form 
 595                         $login = $this->createForm('Rapsys\UserBundle\Form\LoginType', null, [ 
 597                                 'action' => $this->generateUrl('rapsys_user_login'), 
 598                                 //Disable password repeated 
 599                                 'password_repeated' => false, 
 600                                 //Set the form attribute 
 601                                 'attr' => [ 'class' => 'col' ] 
 604                         //Add form to context 
 605                         $parameters['forms']['login'] = $login->createView(); 
 614                                 'pseudonym' => false, 
 628                         $slugger = $this->container
->get('rapsys_pack.slugger_util'); 
 630                         //Create RegisterType form 
 631                         $register = $this->createForm('Rapsys\AirBundle\Form\RegisterType', null, $field+
[ 
 633                                 'action' => $this->generateUrl( 
 634                                         'rapsys_user_register', 
 636                                                 'mail' => $smail = $slugger->short(''), 
 637                                                 'field' => $sfield = $slugger->serialize($field), 
 638                                                 'hash' => $slugger->hash($smail.$sfield) 
 641                                 //Set the form attribute 
 642                                 'attr' => [ 'class' => 'col' ] 
 645                         //Add form to context 
 646                         $parameters['forms']['register'] = $register->createView(); 
 649                 //With page infos and without facebook texts 
 650                 if (empty($parameters['facebook']['texts']) && !empty($parameters['site']['title']) && !empty($parameters['title']) && !empty($parameters['canonical'])) { 
 652                         $parameters['facebook']['texts'] = [ 
 653                                 $parameters['site']['title'] => [ 
 654                                         'font' => 'irishgrover', 
 657                                 $parameters['title'] => [ 
 660                                 $parameters['canonical'] => [ 
 663                                         'font' => 'labelleaurore', 
 670                 if (!empty($parameters['canonical'])) { 
 672                         $parameters['facebook']['metas']['og:url'] = $parameters['canonical']; 
 675                 //With empty facebook title and title 
 676                 if (empty($parameters['facebook']['metas']['og:title']) && !empty($parameters['title'])) { 
 678                         $parameters['facebook']['metas']['og:title'] = $parameters['title']; 
 681                 //With empty facebook description and description 
 682                 if (empty($parameters['facebook']['metas']['og:description']) && !empty($parameters['description'])) { 
 683                         //Set facebook description 
 684                         $parameters['facebook']['metas']['og:description'] = $parameters['description']; 
 688                 if (!empty($locale)) { 
 689                         //Set facebook locale 
 690                         $parameters['facebook']['metas']['og:locale'] = $locale; 
 693                         //XXX: locale change when fb_locale=xx_xx is provided is done in FacebookSubscriber 
 694                         //XXX: see https://stackoverflow.com/questions/20827882/in-open-graph-markup-whats-the-use-of-oglocalealternate-without-the-locati 
 695                         if (!empty($parameters['alternates'])) { 
 696                                 //Iterate on alternates 
 697                                 foreach($parameters['alternates'] as $lang => $alternate) { 
 698                                         if (strlen($lang) == 5) { 
 699                                                 //Set facebook locale alternate 
 700                                                 $parameters['facebook']['metas']['og:locale:alternate'] = str_replace('-', '_', $lang); 
 706                 //Without facebook image defined and texts 
 707                 if (empty($parameters['facebook']['metas']['og:image']) && !empty($parameters['facebook']['texts'])) { 
 709                         $parameters['facebook']['metas'] +
= $this->getFacebookImage($pathInfo, $parameters['facebook']); 
 713                 return $this->baseRender($view, $parameters, $response); 
 719          * @see vendor/symfony/framework-bundle/Controller/AbstractController.php 
 721         public static function getSubscribedServices(): array { 
 722                 //Return subscribed services 
 724                         //'logger' => LoggerInterface::class, 
 725                         'doctrine' => ManagerRegistry
::class, 
 726                         'rapsys_pack.path_package' => PackageInterface
::class, 
 727                         'request_stack' => RequestStack
::class, 
 728                         'router' => RouterInterface
::class, 
 729                         'translator' => TranslatorInterface
::class