3 namespace Rapsys\AirBundle\Controller
; 
   5 use Symfony\Component\Filesystem\Exception\IOExceptionInterface
; 
   6 use Symfony\Component\Filesystem\Filesystem
; 
   7 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController 
as BaseAbstractController
; 
   8 use Symfony\Bundle\FrameworkBundle\Controller\ControllerTrait
; 
   9 use Symfony\Component\HttpFoundation\Response
; 
  10 use Symfony\Component\Routing\Generator\UrlGeneratorInterface
; 
  12 use Rapsys\AirBundle\Entity\Slot
; 
  13 use Rapsys\UserBundle\Utils\Slugger
; 
  17  * Provides common features needed in controllers. 
  21 abstract class AbstractController 
extends BaseAbstractController 
{ 
  23                 //Rename render as baseRender 
  24                 render 
as protected baseRender
; 
  28          * Return the facebook image 
  30          * @desc Generate image in jpeg format or load it from cache 
  32          * @param string $pathInfo The request path info 
  33          * @param array $parameters The image parameters 
  34          * @return array The image array 
  36         protected function getFacebookImage(string $pathInfo, array $parameters = []): array { 
  38                 //XXX: require asset package to be public 
  39                 $package = $this->container
->get('rapsys_pack.path_package'); 
  42                 $texts = $parameters['texts'] ?? []; 
  45                 $source = $parameters['source'] ?? 'png/facebook.png'; 
  48                 $updated = $parameters['updated'] ?? strtotime('last week'); 
  51                 $src = $this->config
['path']['public'].'/'.$source; 
  54                 //XXX: remove extension and store as png anyway 
  55                 $cache = $this->config
['path']['cache'].'/facebook/'.substr($source, 0, strrpos($source, '.')).'.'.$this->config
['facebook']['width'].'x'.$this->config
['facebook']['height'].'.png'; 
  57                 //Set destination path 
  58                 //XXX: format <public>/facebook<pathinfo>.jpeg 
  59                 //XXX: was <public>/facebook/<controller>/<action>.<locale>.jpeg 
  60                 $dest = $this->config
['path']['public'].'/facebook'.$pathInfo.'.jpeg'; 
  62                 //With up to date generated image 
  65                         ($stat = stat($dest)) && 
  66                         $stat['mtime'] >= $updated 
  69                         list ($width, $height) = getimagesize($dest); 
  72                         foreach($texts as $text => $data) { 
  74                                 if (!empty($data['canonical'])) { 
  75                                         //Prevent canonical to finish in alt 
  82                                 'og:image' => $package->getAbsoluteUrl('@RapsysAir/facebook/'.$stat['mtime'].$pathInfo.'.jpeg'), 
  83                                 'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))), 
  84                                 'og:image:height' => $height, 
  85                                 'og:image:width' => $width 
  87                 //With image candidate 
  88                 } elseif (is_file($src)) { 
  90                         $image = new \
Imagick(); 
  93                         if (is_file($cache)) { 
  95                                 $image->readImage($cache); 
  96                         //Without we generate it 
  98                                 //Check target directory 
  99                                 if (!is_dir($dir = dirname($cache))) { 
 100                                         //Create filesystem object 
 101                                         $filesystem = new Filesystem(); 
 105                                                 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755) 
 106                                                 $filesystem->mkdir($dir, 0775); 
 107                                         } catch (IOExceptionInterface 
$e) { 
 109                                                 throw new \
Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e); 
 114                                 $image->readImage($src); 
 116                                 //Crop using aspect ratio 
 117                                 //XXX: for better result upload image directly in aspect ratio :) 
 118                                 $image->cropThumbnailImage($this->config
['facebook']['width'], $this->config
['facebook']['height']); 
 120                                 //Strip image exif data and properties 
 121                                 $image->stripImage(); 
 124                                 if (!$image->writeImage($cache)) { 
 126                                         throw new \
Exception(sprintf('Unable to write image "%s"', $cache)); 
 129                         //Check target directory 
 130                         if (!is_dir($dir = dirname($dest))) { 
 131                                 //Create filesystem object 
 132                                 $filesystem = new Filesystem(); 
 136                                         //XXX: set as 0775, symfony umask (0022) will reduce rights (0755) 
 137                                         $filesystem->mkdir($dir, 0775); 
 138                                 } catch (IOExceptionInterface 
$e) { 
 140                                         throw new \
Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e); 
 145                         $width = $image->getImageWidth(); 
 148                         $height = $image->getImageHeight(); 
 151                         $draw = new \
ImagickDraw(); 
 153                         //Set stroke antialias 
 154                         $draw->setStrokeAntialias(true); 
 157                         $draw->setTextAntialias(true); 
 161                                 'irishgrover' => $this->config
['path']['public'].'/ttf/irishgrover.v10.ttf', 
 162                                 'droidsans' => $this->config
['path']['public'].'/ttf/droidsans.regular.ttf', 
 163                                 'dejavusans' => $this->config
['path']['public'].'/ttf/dejavusans.2.37.ttf', 
 164                                 'labelleaurore' => $this->config
['path']['public'].'/ttf/labelleaurore.v10.ttf' 
 169                                 'left' => \Imagick
::ALIGN_LEFT
, 
 170                                 'center' => \Imagick
::ALIGN_CENTER
, 
 171                                 'right' => \Imagick
::ALIGN_RIGHT
 
 175                         $defaultFont = 'dejavusans'; 
 178                         $defaultAlign = 'center'; 
 184                         $defaultStroke = '#00c3f9'; 
 190                         $defaultFill = 'white'; 
 196                         $count = count($texts); 
 198                         //Draw each text stroke 
 199                         foreach($texts as $text => $data) { 
 201                                 $draw->setFont($fonts[$data['font']??$defaultFont]); 
 204                                 $draw->setFontSize($data['size']??$defaultSize); 
 207                                 $draw->setStrokeWidth($data['width']??$defaultWidth); 
 210                                 $draw->setTextAlignment($align = ($aligns[$data['align']??$defaultAlign])); 
 213                                 $metrics = $image->queryFontMetrics($draw, $text); 
 216                                 if (empty($data['y'])) { 
 217                                         //Position verticaly each text evenly 
 218                                         $texts[$text]['y'] = $data['y'] = (($height + 
100) / (count($texts) + 
1) * $i) - 50; 
 222                                 if (empty($data['x'])) { 
 223                                         if ($align == \Imagick
::ALIGN_CENTER
) { 
 224                                                 $texts[$text]['x'] = $data['x'] = $width/2; 
 225                                         } elseif ($align == \Imagick
::ALIGN_LEFT
) { 
 226                                                 $texts[$text]['x'] = $data['x'] = 50; 
 227                                         } elseif ($align == \Imagick
::ALIGN_RIGHT
) { 
 228                                                 $texts[$text]['x'] = $data['x'] = $width - 50; 
 233                                 //XXX: add ascender part then center it back by half of textHeight 
 234                                 //TODO: maybe add a boundingbox ??? 
 235                                 $texts[$text]['y'] = $data['y'] +
= $metrics['ascender'] - $metrics['textHeight']/2; 
 238                                 $draw->setStrokeColor(new \
ImagickPixel($data['stroke']??$defaultStroke)); 
 241                                 $draw->setFillColor(new \
ImagickPixel($data['stroke']??$defaultStroke)); 
 244                                 $draw->annotation($data['x'], $data['y'], $text); 
 250                         //Create stroke object 
 251                         $stroke = new \
Imagick(); 
 254                         $stroke->newImage($width, $height, new \
ImagickPixel('transparent')); 
 257                         $stroke->drawImage($draw); 
 260                         //XXX: blur the stroke canvas only 
 261                         $stroke->blurImage(5,3); 
 264                         //XXX: see https://www.php.net/manual/en/image.evaluateimage.php 
 265                         $stroke->evaluateImage(\Imagick
::EVALUATE_DIVIDE
, 1.5, \Imagick
::CHANNEL_ALPHA
); 
 268                         $image->compositeImage($stroke, \Imagick
::COMPOSITE_OVER
, 0, 0); 
 280                         $draw->setTextAntialias(true); 
 283                         foreach($texts as $text => $data) { 
 285                                 $draw->setFont($fonts[$data['font']??$defaultFont]); 
 288                                 $draw->setFontSize($data['size']??$defaultSize); 
 291                                 $draw->setTextAlignment($aligns[$data['align']??$defaultAlign]); 
 294                                 $draw->setFillColor(new \
ImagickPixel($data['fill']??$defaultFill)); 
 297                                 $draw->annotation($data['x'], $data['y'], $text); 
 299                                 //With canonical text 
 300                                 if (!empty($data['canonical'])) { 
 301                                         //Prevent canonical to finish in alt 
 302                                         unset($texts[$text]); 
 307                         $image->drawImage($draw); 
 309                         //Strip image exif data and properties 
 310                         $image->stripImage(); 
 313                         $image->setImageFormat('jpeg'); 
 316                         if (!$image->writeImage($dest)) { 
 318                                 throw new \
Exception(sprintf('Unable to write image "%s"', $dest)); 
 326                                 'og:image' => $package->getAbsoluteUrl('@RapsysAir/facebook/'.$stat['mtime'].$pathInfo.'.jpeg'), 
 327                                 'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))), 
 328                                 'og:image:height' => $height, 
 329                                 'og:image:width' => $width 
 333                 //Return empty array without image 
 343         protected function render(string $view, array $parameters = [], Response 
$response = null): Response 
{ 
 345                 $stack = $this->container
->get('request_stack'); 
 347                 //Get current request 
 348                 $request = $stack->getCurrentRequest(); 
 351                 $locale = $request->getLocale(); 
 354                 $parameters['locale'] = str_replace('_', '-', $locale); 
 357                 $router = $this->container
->get('router'); 
 360                 $pathInfo = $router->getContext()->getPathInfo(); 
 362                 //Iterate on locales excluding current one 
 363                 foreach($this->config
['locales'] as $current) { 
 367                         //Iterate on other locales 
 368                         foreach(array_diff($this->config
['locales'], [$current]) as $other) { 
 369                                 $titles[$other] = $this->translator
->trans($this->config
['languages'][$current], [], null, $other); 
 372                         //Retrieve route matching path 
 373                         $route = $router->match($pathInfo); 
 376                         $name = $route['_route']; 
 379                         unset($route['_route']); 
 381                         //With current locale 
 382                         if ($current == $locale) { 
 383                                 //Set locale locales context 
 384                                 $parameters['canonical'] = $router->generate($name, ['_locale' => $current]+
$route, UrlGeneratorInterface
::ABSOLUTE_URL
); 
 386                                 //Set locale locales context 
 387                                 $parameters['alternates'][str_replace('_', '-', $current)] = [ 
 388                                         'absolute' => $router->generate($name, ['_locale' => $current]+
$route, UrlGeneratorInterface
::ABSOLUTE_URL
), 
 389                                         'relative' => $router->generate($name, ['_locale' => $current]+
$route), 
 390                                         'title' => implode('/', $titles), 
 391                                         'translated' => $this->translator
->trans($this->config
['languages'][$current], [], null, $current) 
 396                         if (empty($parameters['alternates'][$shortCurrent = substr($current, 0, 2)])) { 
 397                                 //Set locale locales context 
 398                                 $parameters['alternates'][$shortCurrent] = [ 
 399                                         'absolute' => $router->generate($name, ['_locale' => $current]+
$route, UrlGeneratorInterface
::ABSOLUTE_URL
), 
 400                                         'relative' => $router->generate($name, ['_locale' => $current]+
$route), 
 401                                         'title' => implode('/', $titles), 
 402                                         'translated' => $this->translator
->trans($this->config
['languages'][$current], [], null, $current) 
 407                 //Create application form for role_guest 
 408                 if ($this->isGranted('ROLE_GUEST')) { 
 409                         //Without application form 
 410                         if (empty($parameters['forms']['application'])) { 
 412                                 $doctrine = $this->getDoctrine(); 
 414                                 //Create ApplicationType form 
 415                                 $application = $this->createForm('Rapsys\AirBundle\Form\ApplicationType', null, [ 
 417                                         'action' => $this->generateUrl('rapsys_air_application_add'), 
 418                                         //Set the form attribute 
 419                                         'attr' => [ 'class' => 'col' ], 
 421                                         'admin' => $this->isGranted('ROLE_ADMIN'), 
 422                                         //Set default user to current 
 423                                         'user' => $this->getUser()->getId(), 
 424                                         //Set default slot to evening 
 425                                         //XXX: default to Evening (3) 
 426                                         'slot' => $doctrine->getRepository(Slot
::class)->findOneById(3) 
 429                                 //Add form to context 
 430                                 $parameters['forms']['application'] = $application->createView(); 
 432                 //Create login form for anonymous 
 433                 } elseif (!$this->isGranted('IS_AUTHENTICATED_REMEMBERED')) { 
 434                         //Create LoginType form 
 435                         $login = $this->createForm('Rapsys\UserBundle\Form\LoginType', null, [ 
 437                                 'action' => $this->generateUrl('rapsys_user_login'), 
 438                                 //Disable password repeated 
 439                                 'password_repeated' => false, 
 440                                 //Set the form attribute 
 441                                 'attr' => [ 'class' => 'col' ] 
 444                         //Add form to context 
 445                         $parameters['forms']['login'] = $login->createView(); 
 454                                 'pseudonym' => false, 
 466                         $slugger = $this->container
->get('rapsys_pack.slugger_util'); 
 468                         //Create RegisterType form 
 469                         $register = $this->createForm('Rapsys\AirBundle\Form\RegisterType', null, $field+
[ 
 471                                 'action' => $this->generateUrl( 
 472                                         'rapsys_user_register', 
 474                                                 'mail' => $smail = $slugger->short(''), 
 475                                                 'field' => $sfield = $slugger->serialize($field), 
 476                                                 'hash' => $slugger->hash($smail.$sfield) 
 479                                 //Set the form attribute 
 480                                 'attr' => [ 'class' => 'col' ] 
 483                         //Add form to context 
 484                         $parameters['forms']['register'] = $register->createView(); 
 487                 //With page infos and without facebook texts 
 488                 if (empty($parameters['facebook']['texts']) && !empty($parameters['site']['title']) && !empty($parameters['page']['title']) && !empty($parameters['canonical'])) { 
 490                         $parameters['facebook']['texts'] = [ 
 491                                 $parameters['site']['title'] => [ 
 492                                         'font' => 'irishgrover', 
 495                                 $parameters['page']['title'] => [ 
 498                                 $parameters['canonical'] => [ 
 501                                         'font' => 'labelleaurore', 
 508                 if (!empty($parameters['canonical'])) { 
 510                         $parameters['facebook']['metas']['og:url'] = $parameters['canonical']; 
 514                 if (!empty($parameters['page']['title'])) { 
 516                         $parameters['facebook']['metas']['og:title'] = $parameters['page']['title']; 
 519                 //With page description 
 520                 if (!empty($parameters['page']['description'])) { 
 521                         //Set facebook description 
 522                         $parameters['facebook']['metas']['og:description'] = $parameters['page']['description']; 
 526                 if (!empty($locale)) { 
 527                         //Set facebook locale 
 528                         $parameters['facebook']['metas']['og:locale'] = $locale; 
 531                         //XXX: locale change when fb_locale=xx_xx is provided is done in FacebookSubscriber 
 532                         //XXX: see https://stackoverflow.com/questions/20827882/in-open-graph-markup-whats-the-use-of-oglocalealternate-without-the-locati 
 533                         if (!empty($parameters['alternates'])) { 
 534                                 //Iterate on alternates 
 535                                 foreach($parameters['alternates'] as $lang => $alternate) { 
 536                                         if (strlen($lang) == 5) { 
 537                                                 //Set facebook locale alternate 
 538                                                 $parameters['facebook']['metas']['og:locale:alternate'] = str_replace('-', '_', $lang); 
 544                 //Without facebook image defined and texts 
 545                 if (empty($parameters['facebook']['metas']['og:image']) && !empty($parameters['facebook']['texts'])) { 
 547                         $parameters['facebook']['metas'] +
= $this->getFacebookImage($pathInfo, $parameters['facebook']); 
 551                 return $this->baseRender($view, $parameters, $response);