]> Raphaël G. Git Repositories - treebundle/blob - Controller/TreeController.php
Add rapsyspack.file_util helper
[treebundle] / Controller / TreeController.php
1 <?php declare(strict_types=1);
2
3 /*
4 * This file is part of the Rapsys TreeBundle package.
5 *
6 * (c) Raphaël Gertz <symfony@rapsys.eu>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace Rapsys\TreeBundle\Controller;
13
14 use Doctrine\Persistence\ManagerRegistry;
15
16 use Psr\Container\ContainerInterface;
17
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;
23
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;
32
33 use Symfony\Contracts\Translation\TranslatorInterface;
34
35 use Twig\Environment;
36
37 /**
38 * {@inheritdoc}
39 */
40 class TreeController extends AbstractController {
41 /**
42 * Alias string
43 */
44 protected string $alias;
45
46 /**
47 * Config array
48 */
49 protected array $config;
50
51 /**
52 * Context array
53 */
54 protected array $context = [];
55
56 /**
57 * Count integer
58 */
59 protected int $count;
60
61 /**
62 * Locale string
63 */
64 protected string $locale;
65
66 /**
67 * Page integer
68 */
69 protected int $page;
70
71 /**
72 * Request instance
73 */
74 protected Request $request;
75
76 /**
77 * Route string
78 */
79 protected string $route;
80
81 /**
82 * Route params array
83 */
84 protected array $routeParams;
85
86 /**
87 * Creates a new tree controller
88 *
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
99 */
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) {
101 //Retrieve config
102 $this->config = $container->getParameter($this->alias = RapsysTreeBundle::getAlias());
103
104 //Get main request
105 $this->request = $this->stack->getMainRequest();
106
107 //Get current locale
108 $this->locale = $this->request->getLocale();
109
110 //Set canonical
111 $canonical = null;
112
113 //Set alternates
114 $alternates = [];
115
116 //Get current page
117 $this->page = (int) $this->request->query->get('page');
118
119 //With negative page
120 if ($this->page < 0) {
121 $this->page = 0;
122 }
123
124 //Set route
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');
129
130 //Set route params
131 $this->routeParams = $this->request->attributes->get('_route_params');
132
133 //With route and routeParams
134 if ($this->route !== null && $this->routeParams !== null) {
135 //Set canonical
136 $canonical = $this->router->generate($this->route, $this->routeParams, UrlGeneratorInterface::ABSOLUTE_URL);
137
138 //Set alternates
139 $alternates = [
140 substr($this->locale, 0, 2) => [
141 'absolute' => $canonical
142 ]
143 ];
144 }
145
146 //Set the context
147 $this->context = [
148 'alternates' => $alternates,
149 'canonical' => $canonical,
150 'contact' => [
151 'address' => $this->config['contact']['address'],
152 'name' => $this->translator->trans($this->config['contact']['name'])
153 ],
154 'copy' => [
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']
160 ],
161 'description' => null,
162 'donate' => $this->config['donate'],
163 'facebook' => [
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']
169 ],
170 //XXX: TODO: only generate it when fb robot request the url ???
171 'fbimage' => [
172 'texts' => [
173 $title => [
174 'font' => 'irishgrover',
175 'size' => 110
176 ]
177 ]
178 ],
179 'icon' => $this->config['icon'],
180 'keywords' => null,
181 'locale' => str_replace('_', '-', $this->locale),
182 'logo' => $this->config['logo'],
183 'forms' => [],
184 'root' => $this->router->generate($this->config['root']),
185 'title' => [
186 'page' => null,
187 'section' => null,
188 'site' => $title
189 ]
190 ];
191 }
192
193 /**
194 * The album page
195 *
196 * Display album
197 *
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
203 */
204 public function album(Request $request, int $id, string $path, string $slug): Response {
205 //Get user
206 $user = $this->security->getUser();
207
208 //Check admin role
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));
213 }
214
215 //Without album
216 if (!($this->context['album'] = $this->doctrine->getRepository(Album::class)->findOneByIdPathAsArray($id, $path))) {
217 //Throw 404
218 throw $this->createNotFoundException($this->translator->trans('Unable to find album'));
219 }
220
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);
225 }
226
227 //Set modified
228 $this->modified = $this->context['album']['modified'];
229
230 //Create response
231 $response = new Response();
232
233 //With logged user
234 if ($this->checker->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
235 //Set last modified
236 $response->setLastModified(new \DateTime('-1 year'));
237
238 //Set as private
239 $response->setPrivate();
240 //Without logged user
241 } else {
242 //Set etag
243 //XXX: only for public to force revalidation by last modified
244 $response->setEtag(md5(serialize($this->context['album'])));
245
246 //Set last modified
247 $response->setLastModified($this->modified);
248
249 //Set as public
250 $response->setPublic();
251
252 //Without role and modification
253 if ($response->isNotModified($request)) {
254 //Return 304 response
255 return $response;
256 }
257 }
258
259 //Set keywords
260 /*$this->context['head']['keywords'] = implode(
261 ', ',
262 //Use closure to extract each unique article keywords sorted
263 (function ($t) {
264 //Return array
265 $r = [];
266
267 //Iterate on articles
268 foreach($t as $a) {
269 //Non empty keywords
270 if (!empty($a['keywords'])) {
271 //Iterate on keywords
272 foreach($a['keywords'] as $k) {
273 //Set keyword
274 $r[$k['title']] = $k['title'];
275 }
276 }
277 }
278
279 //Sort array
280 sort($r);
281
282 //Return array
283 return $r;
284 })($this->context['articles'])
285 );
286 //Get albums
287 $this->context['albums'] = $this->config['albums'];*/
288
289 //Return rendered response
290 return $this->render('@RapsysTree/album.html.twig', $this->context, $response);
291 }
292
293
294 /**
295 * The element page
296 *
297 * Display element
298 *
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
303 */
304 public function element(Request $request, int $id, string $path): Response {
305 //Get user
306 $user = $this->security->getUser();
307
308 //Without element
309 if (!($this->context['element'] = $this->doctrine->getRepository(Element::class)->findOneByUidIdPathAsArray($user?->getId(), $id, $path))) {
310 //Throw 404
311 throw $this->createNotFoundException($this->translator->trans('Unable to find element'));
312 }
313
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);
319 }
320
321 //Set modified
322 $this->modified = $this->context['element']['modified'];
323
324 //Create response
325 $response = new Response();
326
327 //With logged user
328 if ($this->checker->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
329 //Set last modified
330 $response->setLastModified(new \DateTime('-1 year'));
331
332 //Set as private
333 $response->setPrivate();
334 //Without logged user
335 } else {
336 //Set etag
337 //XXX: only for public to force revalidation by last modified
338 $response->setEtag(md5(serialize($this->context['element'])));
339
340 //Set last modified
341 $response->setLastModified($this->modified);
342
343 //Set as public
344 $response->setPublic();
345
346 //Without role and modification
347 if ($response->isNotModified($request)) {
348 //Return 304 response
349 return $response;
350 }
351 }
352
353 //TODO: move that in a shared function to use in album member function too
354
355 /*
356 //With directory
357 if (is_dir($realpath)) {
358 //Set element directories
359 $this->context['element'] += [
360 //Directories
361 'directories' => [],
362 //Files
363 'files' => []
364 ];
365
366 //Iterate on directory
367 foreach(array_diff(scandir($realpath), ['.', '..']) as $item) {
368 //Check item
369 if (
370 //Without item realpath
371 !($itempath = realpath($realpath.'/'.$item)) ||
372 //With item realpath not matching element path
373 (
374 $this->context['element']['album']['path'].$this->context['element']['path'] !==
375 substr($itempath, 0, strlen($this->context['element']['album']['path'].$this->context['element']['path']))
376 )
377 ) {
378 //Skip
379 continue;
380 }
381
382 //With directory
383 if (is_dir($itempath)) {
384 //Append directory
385 $this->context['element']['directories'][$item] = $this->router->generate($this->route, [ 'path' => $this->context['path'].'/'.$item ]+$this->routeParams);
386 //With file
387 } elseif (is_file($itempath)) {
388 //Append file
389 $this->context['element']['files'][$item] = [
390 //Set mime
391 'mime' => mime_content_type($itempath),
392 //Set size
393 'size' => filesize($itempath),
394 //Set link
395 'link' => $this->router->generate($this->route, [ 'path' => $this->context['path'].'/'.$item ]+$this->routeParams)
396 ];
397 //With unknown type
398 } else {
399 //Throw 404
400 throw $this->createNotFoundException($this->translator->trans('Unable to process element'));
401 }
402 }
403 //With file
404 } elseif (is_file($realpath)) {
405 //Append file
406 $this->context['element']['file'] = [
407 //Set mime
408 'mime' => mime_content_type($realpath),
409 //Set size
410 'size' => filesize($realpath),
411 //TODO: extra fields ? (preview, miniature, etc ?)
412 ];
413 //TODO: mimetype decided extra fields ? (.pdf for doc, webm preview, img preview, etc ?)
414 //TODO: XXX: finish this !!!
415 //With unknown type
416 } else {
417 //Throw 404
418 throw $this->createNotFoundException($this->translator->trans('Unable to process element'));
419 }
420 */
421
422 //Set keywords
423 /*$this->context['head']['keywords'] = implode(
424 ', ',
425 //Use closure to extract each unique article keywords sorted
426 (function ($t) {
427 //Return array
428 $r = [];
429
430 //Iterate on articles
431 foreach($t as $a) {
432 //Non empty keywords
433 if (!empty($a['keywords'])) {
434 //Iterate on keywords
435 foreach($a['keywords'] as $k) {
436 //Set keyword
437 $r[$k['title']] = $k['title'];
438 }
439 }
440 }
441
442 //Sort array
443 sort($r);
444
445 //Return array
446 return $r;
447 })($this->context['articles'])
448 );
449 //Get albums
450 $this->context['albums'] = $this->config['albums'];*/
451
452 #header('Content-Type: text/plain');
453 #var_dump($this->context['element']);
454 #exit;
455
456 //Return rendered response
457 return $this->render('@RapsysTree/element.html.twig', $this->context, $response);
458 }
459
460 /**
461 * The index page
462 *
463 * Display index
464 *
465 * @param Request $request The request instance
466 * @return Response The rendered view
467 */
468 public function index(Request $request): Response {
469 //Get user
470 $user = $this->security->getUser();
471
472 //With not enough albums
473 if (($this->count = $this->doctrine->getRepository(Album::class)->countByUidAsInt($user?->getId())) < $this->page * $this->limit) {
474 //Throw 404
475 throw $this->createNotFoundException($this->translator->trans('Unable to find albums'));
476 }
477
478 //Get albums
479 if ($this->context['albums'] = $this->doctrine->getRepository(Album::class)->findByUidAsArray($user?->getId(), $this->page, $this->limit)) {
480 //Set modified
481 $this->modified = max(array_map(function ($v) { return $v['modified']; }, $this->context['albums']));
482 //Without albums
483 } else {
484 //Set empty modified
485 $this->modified = new \DateTime('-1 year');
486 }
487
488 //Create response
489 $response = new Response();
490
491 //With logged user
492 if ($this->checker->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
493 //Set last modified
494 $response->setLastModified(new \DateTime('-1 year'));
495
496 //Set as private
497 $response->setPrivate();
498 //Without logged user
499 } else {
500 //Set etag
501 //XXX: only for public to force revalidation by last modified
502 $response->setEtag(md5(serialize($this->context['albums'])));
503
504 //Set last modified
505 $response->setLastModified($this->modified);
506
507 //Set as public
508 $response->setPublic();
509
510 //Without role and modification
511 if ($response->isNotModified($request)) {
512 //Return 304 response
513 return $response;
514 }
515 }
516
517 //Set keywords
518 /*$this->context['head']['keywords'] = implode(
519 ', ',
520 //Use closure to extract each unique article keywords sorted
521 (function ($t) {
522 //Return array
523 $r = [];
524
525 //Iterate on articles
526 foreach($t as $a) {
527 //Non empty keywords
528 if (!empty($a['keywords'])) {
529 //Iterate on keywords
530 foreach($a['keywords'] as $k) {
531 //Set keyword
532 $r[$k['title']] = $k['title'];
533 }
534 }
535 }
536
537 //Sort array
538 sort($r);
539
540 //Return array
541 return $r;
542 })($this->context['articles'])
543 );
544 //Get albums
545 $this->context['albums'] = $this->config['albums'];*/
546
547 //Return rendered response
548 return $this->render('@RapsysTree/index.html.twig', $this->context, $response);
549 }
550
551 /**
552 * The directory page
553 *
554 * Display directory
555 *
556 * @param Request $request The request instance
557 * @param string $path The directory path
558 * @return Response The rendered view
559 */
560 public function directory(Request $request, string $path): Response {
561 header('Content-Type: text/plain');
562 var_dump($path);
563 exit;
564
565 //Render template
566 $response = $this->render('@RapsysTree/directory.html.twig', $this->context);
567
568 $response->setEtag(md5($response->getContent()));
569 $response->setPublic();
570 $response->isNotModified($request);
571
572 //Return response
573 return $response;
574 }
575
576 /**
577 * The document page
578 *
579 * Display document
580 *
581 * @param Request $request The request instance
582 * @param string $path The directory path
583 * @return Response The rendered view
584 */
585 public function document(Request $request, string $path): Response {
586 //Render template
587 $response = $this->render('@RapsysTree/document.html.twig', $this->context);
588
589 $response->setEtag(md5($response->getContent()));
590 $response->setPublic();
591 $response->isNotModified($request);
592
593 //Return response
594 return $response;
595 }
596
597 /**
598 * Renders a view
599 *
600 * {@inheritdoc}
601 */
602 protected function render(string $view, array $parameters = [], Response $response = null): Response {
603 //Create response when null
604 $response ??= new Response();
605
606 //Without alternates
607 if (count($parameters['alternates']) <= 1) {
608 //Set routeParams
609 $routeParams = $this->routeParams;
610
611 //Iterate on locales excluding current one
612 foreach($this->config['locales'] as $locale) {
613 //With current locale
614 if ($locale !== $this->locale) {
615 //Set titles
616 $titles = [];
617
618 //Set route params locale
619 $routeParams['_locale'] = $locale;
620
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);
625 }
626
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)
633 ];
634
635 //Add shorter 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)];
639 }
640 }
641 }
642 }
643
644 //With canonical
645 if (!empty($parameters['canonical'])) {
646 //Set facebook url
647 $parameters['facebook']['og:url'] = $parameters['canonical'];
648 }
649
650 //With empty facebook title and title
651 if (empty($parameters['facebook']['og:title']) && !empty($parameters['title']['page'])) {
652 //Set facebook title
653 $parameters['facebook']['og:title'] = $parameters['title']['page'];
654 }
655
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'];
660 }
661
662 //With locale
663 if (!empty($this->locale)) {
664 //Set facebook locale
665 $parameters['facebook']['og:locale'] = $this->locale;
666
667 //With alternates
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);
676 }
677 }
678 }
679 }
680
681 //Without facebook image defined and texts
682 if (empty($parameters['facebook']['og:image']) && !empty($this->request) && !empty($parameters['fbimage']['texts']) && !empty($this->modified)) {
683 //Get facebook image
684 $parameters['facebook'] += $this->facebook->getImage($this->request->getPathInfo(), $parameters['fbimage']['texts'], $this->modified->getTimestamp());
685 }
686
687 //Call twig render method
688 $content = $this->twig->render($view, $parameters);
689
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);
695 break;
696 }
697 }
698 }
699
700 //Store content in response
701 $response->setContent($content);
702
703 //Return response
704 return $response;
705 }
706 }