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);