1 <?php 
declare(strict_types
=1); 
   4  * This file is part of the Rapsys TreeBundle 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\TreeBundle\Controller
; 
  14 use Doctrine\Persistence\ManagerRegistry
; 
  16 use Psr\Container\ContainerInterface
; 
  18 use Rapsys\PackBundle\Util\FacebookUtil
; 
  19 use Rapsys\TreeBundle\Entity\Album
; 
  20 use Rapsys\TreeBundle\Entity\Element
; 
  21 use Rapsys\TreeBundle\RapsysTreeBundle
; 
  22 use Rapsys\UserBundle\RapsysUserBundle
; 
  24 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController
; 
  25 use Symfony\Bundle\SecurityBundle\Security
; 
  26 use Symfony\Component\HttpFoundation\Request
; 
  27 use Symfony\Component\HttpFoundation\RequestStack
; 
  28 use Symfony\Component\HttpFoundation\Response
; 
  29 use Symfony\Component\Routing\Generator\UrlGeneratorInterface
; 
  30 use Symfony\Component\Routing\RouterInterface
; 
  31 use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface
; 
  33 use Symfony\Contracts\Translation\TranslatorInterface
; 
  40 class TreeController 
extends AbstractController 
{ 
  44         protected string $alias; 
  49         protected array $config; 
  54         protected array $context = []; 
  64         protected string $locale; 
  74         protected Request 
$request; 
  79         protected string $route; 
  84         protected array $routeParams; 
  87          * Creates a new tree controller 
  89          * @param AuthorizationCheckerInterface $checker The container instance 
  90          * @param ContainerInterface $container The ContainerInterface instance 
  91          * @param ManagerRegistry $doctrine The doctrine instance 
  92          * @param FacebookUtil $facebook The facebook instance 
  93          * @param RouterInterface $router The router instance 
  94          * @param Security $security The security instance 
  95          * @param RequestStack $stack The stack instance 
  96          * @param TranslatorInterface $translator The translator instance 
  97          * @param Environment $twig The twig environment instance 
  98          * @param integer $limit The page limit 
 100         function __construct(protected AuthorizationCheckerInterface 
$checker, protected ContainerInterface 
$container, protected ManagerRegistry 
$doctrine, protected FacebookUtil 
$facebook, protected RouterInterface 
$router, protected Security 
$security, protected RequestStack 
$stack, protected TranslatorInterface 
$translator, protected Environment 
$twig, protected int $limit = 5) { 
 102                 $this->config 
= $container->getParameter($this->alias 
= RapsysTreeBundle
::getAlias()); 
 105                 $this->request 
= $this->stack
->getMainRequest(); 
 108                 $this->locale 
= $this->request
->getLocale(); 
 117                 $this->page 
= (int) $this->request
->query
->get('page'); 
 120                 if ($this->page 
< 0) { 
 125                 //TODO: default to not found route ??? 
 126                 //TODO: pour une url not found, cet attribut n'est pas défini, comment on fait ??? 
 127                 //XXX: on génère une route bidon par défaut ??? 
 128                 $this->route 
= $this->request
->attributes
->get('_route'); 
 131                 $this->routeParams 
= $this->request
->attributes
->get('_route_params'); 
 133                 //With route and routeParams 
 134                 if ($this->route 
!== null && $this->routeParams 
!== null) { 
 136                         $canonical = $this->router
->generate($this->route
, $this->routeParams
, UrlGeneratorInterface
::ABSOLUTE_URL
); 
 140                                 substr($this->locale
, 0, 2) => [ 
 141                                         'absolute' => $canonical 
 148                         'alternates' => $alternates, 
 149                         'canonical' => $canonical, 
 151                                 'address' => $this->config
['contact']['address'], 
 152                                 'name' => $this->translator
->trans($this->config
['contact']['name']) 
 155                                 'by' => $this->translator
->trans($this->config
['copy']['by']), 
 156                                 'link' => $this->config
['copy']['link'], 
 157                                 'long' => $this->translator
->trans($this->config
['copy']['long']), 
 158                                 'short' => $this->translator
->trans($this->config
['copy']['short']), 
 159                                 'title' => $this->config
['copy']['title'] 
 161                         'description' => null, 
 162                         'donate' => $this->config
['donate'], 
 164                                 'og:type' => 'article', 
 165                                 'og:site_name' => $title = $this->translator
->trans($this->config
['title']), 
 166                                 'og:url' => $canonical, 
 167                                 #'fb:admins' => $this->config['facebook']['admins'], 
 168                                 'fb:app_id' => $this->config
['facebook']['apps'] 
 170                         //XXX: TODO: only generate it when fb robot request the url ??? 
 174                                                 'font' => 'irishgrover', 
 179                         'icon' => $this->config
['icon'], 
 181                         'locale' => str_replace('_', '-', $this->locale
), 
 182                         'logo' => $this->config
['logo'], 
 184                         'root' => $this->router
->generate($this->config
['root']), 
 198          * @param Request $request The request instance 
 199          * @param int $id The album id 
 200          * @param string $path The album path 
 201          * @param string $slug The album slug 
 202          * @return Response The rendered view 
 204         public function album(Request 
$request, int $id, string $path, string $slug): Response 
{ 
 206                 $user = $this->security
->getUser(); 
 209                 if (!$this->checker
->isGranted('ROLE_'.strtoupper($this->container
->getParameter(RapsysUserBundle
::getAlias().'.default.admin')))) { 
 210                         //Throw access denied 
 211                         //XXX: prevent slugger reverse engineering by not displaying decoded mail 
 212                         throw $this->createAccessDeniedException($this->translator
->trans('Unable to access album', [], $this->alias
)); 
 216                 if (!($this->context
['album'] = $this->doctrine
->getRepository(Album
::class)->findOneByIdPathAsArray($id, $path))) { 
 218                         throw $this->createNotFoundException($this->translator
->trans('Unable to find album')); 
 221                 //With slug not matching 
 222                 if ($this->context
['album']['slug'] !== $slug) { 
 223                         //Redirect on clean slug 
 224                         return $this->redirectToRoute($this->route
, [ 'slug' => $this->context
['album']['slug'] ]+
$this->routeParams
); 
 228                 $this->modified 
= $this->context
['album']['modified']; 
 231                 $response = new Response(); 
 234                 if ($this->checker
->isGranted('IS_AUTHENTICATED_REMEMBERED')) { 
 236                         $response->setLastModified(new \
DateTime('-1 year')); 
 239                         $response->setPrivate(); 
 240                 //Without logged user 
 243                         //XXX: only for public to force revalidation by last modified 
 244                         $response->setEtag(md5(serialize($this->context
['album']))); 
 247                         $response->setLastModified($this->modified
); 
 250                         $response->setPublic(); 
 252                         //Without role and modification 
 253                         if ($response->isNotModified($request)) { 
 254                                 //Return 304 response 
 260                 /*$this->context['head']['keywords'] = implode( 
 262                         //Use closure to extract each unique article keywords sorted 
 267                                 //Iterate on articles 
 270                                         if (!empty($a['keywords'])) { 
 271                                                 //Iterate on keywords 
 272                                                 foreach($a['keywords'] as $k) { 
 274                                                         $r[$k['title']] = $k['title']; 
 284                         })($this->context['articles']) 
 287                 $this->context['albums'] = $this->config['albums'];*/ 
 289                 //Return rendered response 
 290                 return $this->render('@RapsysTree/album.html.twig', $this->context
, $response); 
 299          * @param Request $request The request instance 
 300          * @param int $id The element id 
 301          * @param ?string $path The element path 
 302          * @return Response The rendered view 
 304         public function element(Request 
$request, int $id, string $path): Response 
{ 
 306                 $user = $this->security
->getUser(); 
 309                 if (!($this->context
['element'] = $this->doctrine
->getRepository(Element
::class)->findOneByUidIdPathAsArray($user?->getId(), $id, $path))) { 
 311                         throw $this->createNotFoundException($this->translator
->trans('Unable to find element')); 
 314                 //With realpath not matching path 
 315                 //XXX: extra slashes removed by rewrite rule 
 316                 if (($this->context
['path'] = substr($this->context
['element']['realpath'], strlen($this->context
['element']['album']['path'])+
1)) !== $path) { 
 317                         //Redirect on clean path 
 318                         return $this->redirectToRoute($this->route
, [ 'path' => $this->context
['path'] ]+
$this->routeParams
); 
 322                 $this->modified 
= $this->context
['element']['modified']; 
 325                 $response = new Response(); 
 328                 if ($this->checker
->isGranted('IS_AUTHENTICATED_REMEMBERED')) { 
 330                         $response->setLastModified(new \
DateTime('-1 year')); 
 333                         $response->setPrivate(); 
 334                 //Without logged user 
 337                         //XXX: only for public to force revalidation by last modified 
 338                         $response->setEtag(md5(serialize($this->context
['element']))); 
 341                         $response->setLastModified($this->modified
); 
 344                         $response->setPublic(); 
 346                         //Without role and modification 
 347                         if ($response->isNotModified($request)) { 
 348                                 //Return 304 response 
 353                 //TODO: move that in a shared function to use in album member function too 
 357                 if (is_dir($realpath)) { 
 358                         //Set element directories 
 359                         $this->context['element'] += [ 
 366                         //Iterate on directory 
 367                         foreach(array_diff(scandir($realpath), ['.', '..']) as $item) { 
 370                                         //Without item realpath 
 371                                         !($itempath = realpath($realpath.'/'.$item)) || 
 372                                         //With item realpath not matching element path 
 374                                                 $this->context['element']['album']['path'].$this->context['element']['path'] !== 
 375                                                 substr($itempath, 0, strlen($this->context['element']['album']['path'].$this->context['element']['path'])) 
 383                                 if (is_dir($itempath)) { 
 385                                         $this->context['element']['directories'][$item] = $this->router->generate($this->route, [ 'path' => $this->context['path'].'/'.$item ]+$this->routeParams); 
 387                                 } elseif (is_file($itempath)) { 
 389                                         $this->context['element']['files'][$item] = [ 
 391                                                 'mime' => mime_content_type($itempath), 
 393                                                 'size' => filesize($itempath), 
 395                                                 'link' => $this->router->generate($this->route, [ 'path' => $this->context['path'].'/'.$item ]+$this->routeParams) 
 400                                         throw $this->createNotFoundException($this->translator->trans('Unable to process element')); 
 404                 } elseif (is_file($realpath)) { 
 406                         $this->context['element']['file'] = [ 
 408                                 'mime' => mime_content_type($realpath), 
 410                                 'size' => filesize($realpath), 
 411                                 //TODO: extra fields ? (preview, miniature, etc ?) 
 413                         //TODO: mimetype decided extra fields ? (.pdf for doc, webm preview, img preview, etc ?) 
 414                         //TODO: XXX: finish this !!! 
 418                         throw $this->createNotFoundException($this->translator->trans('Unable to process element')); 
 423                 /*$this->context['head']['keywords'] = implode( 
 425                         //Use closure to extract each unique article keywords sorted 
 430                                 //Iterate on articles 
 433                                         if (!empty($a['keywords'])) { 
 434                                                 //Iterate on keywords 
 435                                                 foreach($a['keywords'] as $k) { 
 437                                                         $r[$k['title']] = $k['title']; 
 447                         })($this->context['articles']) 
 450                 $this->context['albums'] = $this->config['albums'];*/ 
 452                 #header('Content-Type: text/plain'); 
 453                 #var_dump($this->context['element']); 
 456                 //Return rendered response 
 457                 return $this->render('@RapsysTree/element.html.twig', $this->context
, $response); 
 465          * @param Request $request The request instance 
 466          * @return Response The rendered view 
 468         public function index(Request 
$request): Response 
{ 
 470                 $user = $this->security
->getUser(); 
 472                 //With not enough albums 
 473                 if (($this->count 
= $this->doctrine
->getRepository(Album
::class)->countByUidAsInt($user?->getId())) < $this->page 
* $this->limit
) { 
 475                         throw $this->createNotFoundException($this->translator
->trans('Unable to find albums')); 
 479                 if ($this->context
['albums'] = $this->doctrine
->getRepository(Album
::class)->findByUidAsArray($user?->getId(), $this->page
, $this->limit
)) { 
 481                         $this->modified 
= max(array_map(function ($v) { return $v
['modified']; }, $this->context
['albums'])); 
 485                         $this->modified 
= new \
DateTime('-1 year'); 
 489                 $response = new Response(); 
 492                 if ($this->checker
->isGranted('IS_AUTHENTICATED_REMEMBERED')) { 
 494                         $response->setLastModified(new \
DateTime('-1 year')); 
 497                         $response->setPrivate(); 
 498                 //Without logged user 
 501                         //XXX: only for public to force revalidation by last modified 
 502                         $response->setEtag(md5(serialize($this->context
['albums']))); 
 505                         $response->setLastModified($this->modified
); 
 508                         $response->setPublic(); 
 510                         //Without role and modification 
 511                         if ($response->isNotModified($request)) { 
 512                                 //Return 304 response 
 518                 /*$this->context['head']['keywords'] = implode( 
 520                         //Use closure to extract each unique article keywords sorted 
 525                                 //Iterate on articles 
 528                                         if (!empty($a['keywords'])) { 
 529                                                 //Iterate on keywords 
 530                                                 foreach($a['keywords'] as $k) { 
 532                                                         $r[$k['title']] = $k['title']; 
 542                         })($this->context['articles']) 
 545                 $this->context['albums'] = $this->config['albums'];*/ 
 547                 //Return rendered response 
 548                 return $this->render('@RapsysTree/index.html.twig', $this->context
, $response); 
 556          * @param Request $request The request instance 
 557          * @param string $path The directory path 
 558          * @return Response The rendered view 
 560         public function directory(Request 
$request, string $path): Response 
{ 
 561                 header('Content-Type: text/plain'); 
 566                 $response = $this->render('@RapsysTree/directory.html.twig', $this->context
); 
 568                 $response->setEtag(md5($response->getContent())); 
 569                 $response->setPublic(); 
 570                 $response->isNotModified($request); 
 581          * @param Request $request The request instance 
 582          * @param string $path The directory path 
 583          * @return Response The rendered view 
 585         public function document(Request 
$request, string $path): Response 
{ 
 587                 $response = $this->render('@RapsysTree/document.html.twig', $this->context
); 
 589                 $response->setEtag(md5($response->getContent())); 
 590                 $response->setPublic(); 
 591                 $response->isNotModified($request); 
 602         protected function render(string $view, array $parameters = [], Response 
$response = null): Response 
{ 
 603                 //Create response when null 
 604                 $response ??= new Response(); 
 607                 if (count($parameters['alternates']) <= 1) { 
 609                         $routeParams = $this->routeParams
; 
 611                         //Iterate on locales excluding current one 
 612                         foreach($this->config
['locales'] as $locale) { 
 613                                 //With current locale 
 614                                 if ($locale !== $this->locale
) { 
 618                                         //Set route params locale 
 619                                         $routeParams['_locale'] = $locale; 
 621                                         //Iterate on other locales 
 622                                         foreach(array_diff($this->config
['locales'], [$locale]) as $other) { 
 623                                                 //Set other locale title 
 624                                                 $titles[$other] = $this->translator
->trans($this->config
['languages'][$locale], [], null, $other); 
 627                                         //Set locale locales context 
 628                                         $parameters['alternates'][str_replace('_', '-', $locale)] = [ 
 629                                                 'absolute' => $this->router
->generate($this->route
, $routeParams, UrlGeneratorInterface
::ABSOLUTE_URL
), 
 630                                                 'relative' => $this->router
->generate($this->route
, $routeParams), 
 631                                                 'title' => implode('/', $titles), 
 632                                                 'translated' => $this->translator
->trans($this->config
['languages'][$locale], [], null, $locale) 
 636                                         if (empty($parameters['alternates'][$shortCurrent = substr($locale, 0, 2)])) { 
 637                                                 //Set locale locales context 
 638                                                 $parameters['alternates'][$shortCurrent] = $parameters['alternates'][str_replace('_', '-', $locale)]; 
 645                 if (!empty($parameters['canonical'])) { 
 647                         $parameters['facebook']['og:url'] = $parameters['canonical']; 
 650                 //With empty facebook title and title 
 651                 if (empty($parameters['facebook']['og:title']) && !empty($parameters['title']['page'])) { 
 653                         $parameters['facebook']['og:title'] = $parameters['title']['page']; 
 656                 //With empty facebook description and description 
 657                 if (empty($parameters['facebook']['og:description']) && !empty($parameters['description'])) { 
 658                         //Set facebook description 
 659                         $parameters['facebook']['og:description'] = $parameters['description']; 
 663                 if (!empty($this->locale
)) { 
 664                         //Set facebook locale 
 665                         $parameters['facebook']['og:locale'] = $this->locale
; 
 668                         //XXX: locale change when fb_locale=xx_xx is provided is done in FacebookSubscriber 
 669                         //XXX: see https://stackoverflow.com/questions/20827882/in-open-graph-markup-whats-the-use-of-oglocalealternate-without-the-locati 
 670                         if (!empty($parameters['alternates'])) { 
 671                                 //Iterate on alternates 
 672                                 foreach($parameters['alternates'] as $lang => $alternate) { 
 673                                         if (strlen($lang) == 5) { 
 674                                                 //Set facebook locale alternate 
 675                                                 $parameters['facebook']['og:locale:alternate'] = str_replace('-', '_', $lang); 
 681                 //Without facebook image defined and texts 
 682                 if (empty($parameters['facebook']['og:image']) && !empty($this->request
) && !empty($parameters['fbimage']['texts']) && !empty($this->modified
)) { 
 684                         $parameters['facebook'] +
= $this->facebook
->getImage($this->request
->getPathInfo(), $parameters['fbimage']['texts'], $this->modified
->getTimestamp()); 
 687                 //Call twig render method 
 688                 $content = $this->twig
->render($view, $parameters); 
 690                 //Invalidate OK response on invalid form 
 691                 if (200 === $response->getStatusCode()) { 
 692                         foreach ($parameters as $v) { 
 693                                 if ($v instanceof FormInterface 
&& $v->isSubmitted() && !$v->isValid()) { 
 694                                         $response->setStatusCode(422); 
 700                 //Store content in response 
 701                 $response->setContent($content);