* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Rapsys\TreeBundle\Controller; use Doctrine\Persistence\ManagerRegistry; use Psr\Container\ContainerInterface; use Rapsys\PackBundle\Util\FacebookUtil; use Rapsys\TreeBundle\Entity\Album; use Rapsys\TreeBundle\Entity\Element; use Rapsys\TreeBundle\RapsysTreeBundle; use Rapsys\UserBundle\RapsysUserBundle; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Contracts\Translation\TranslatorInterface; use Twig\Environment; /** * {@inheritdoc} */ class TreeController extends AbstractController { /** * Alias string */ protected string $alias; /** * Config array */ protected array $config; /** * Context array */ protected array $context = []; /** * Count integer */ protected int $count; /** * Locale string */ protected string $locale; /** * Page integer */ protected int $page; /** * Request instance */ protected Request $request; /** * Route string */ protected string $route; /** * Route params array */ protected array $routeParams; /** * Creates a new tree controller * * @param AuthorizationCheckerInterface $checker The container instance * @param ContainerInterface $container The ContainerInterface instance * @param ManagerRegistry $doctrine The doctrine instance * @param FacebookUtil $facebook The facebook instance * @param RouterInterface $router The router instance * @param Security $security The security instance * @param RequestStack $stack The stack instance * @param TranslatorInterface $translator The translator instance * @param Environment $twig The twig environment instance * @param integer $limit The page limit */ 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) { //Retrieve config $this->config = $container->getParameter($this->alias = RapsysTreeBundle::getAlias()); //Get main request $this->request = $this->stack->getMainRequest(); //Get current locale $this->locale = $this->request->getLocale(); //Set canonical $canonical = null; //Set alternates $alternates = []; //Get current page $this->page = (int) $this->request->query->get('page'); //With negative page if ($this->page < 0) { $this->page = 0; } //Set route //TODO: default to not found route ??? //TODO: pour une url not found, cet attribut n'est pas défini, comment on fait ??? //XXX: on génère une route bidon par défaut ??? $this->route = $this->request->attributes->get('_route'); //Set route params $this->routeParams = $this->request->attributes->get('_route_params'); //With route and routeParams if ($this->route !== null && $this->routeParams !== null) { //Set canonical $canonical = $this->router->generate($this->route, $this->routeParams, UrlGeneratorInterface::ABSOLUTE_URL); //Set alternates $alternates = [ substr($this->locale, 0, 2) => [ 'absolute' => $canonical ] ]; } //Set the context $this->context = [ 'alternates' => $alternates, 'canonical' => $canonical, 'contact' => [ 'address' => $this->config['contact']['address'], 'name' => $this->translator->trans($this->config['contact']['name']) ], 'copy' => [ 'by' => $this->translator->trans($this->config['copy']['by']), 'link' => $this->config['copy']['link'], 'long' => $this->translator->trans($this->config['copy']['long']), 'short' => $this->translator->trans($this->config['copy']['short']), 'title' => $this->config['copy']['title'] ], 'description' => null, 'donate' => $this->config['donate'], 'facebook' => [ 'og:type' => 'article', 'og:site_name' => $title = $this->translator->trans($this->config['title']), 'og:url' => $canonical, #'fb:admins' => $this->config['facebook']['admins'], 'fb:app_id' => $this->config['facebook']['apps'] ], //XXX: TODO: only generate it when fb robot request the url ??? 'fbimage' => [ 'texts' => [ $title => [ 'font' => 'irishgrover', 'size' => 110 ] ] ], 'icon' => $this->config['icon'], 'keywords' => null, 'locale' => str_replace('_', '-', $this->locale), 'logo' => $this->config['logo'], 'forms' => [], 'root' => $this->router->generate($this->config['root']), 'title' => [ 'page' => null, 'section' => null, 'site' => $title ] ]; } /** * The album page * * Display album * * @param Request $request The request instance * @param int $id The album id * @param string $path The album path * @param string $slug The album slug * @return Response The rendered view */ public function album(Request $request, int $id, string $path, string $slug): Response { //Get user $user = $this->security->getUser(); //Check admin role if (!$this->checker->isGranted('ROLE_'.strtoupper($this->container->getParameter(RapsysUserBundle::getAlias().'.default.admin')))) { //Throw access denied //XXX: prevent slugger reverse engineering by not displaying decoded mail throw $this->createAccessDeniedException($this->translator->trans('Unable to access album', [], $this->alias)); } //Without album if (!($this->context['album'] = $this->doctrine->getRepository(Album::class)->findOneByIdPathAsArray($id, $path))) { //Throw 404 throw $this->createNotFoundException($this->translator->trans('Unable to find album')); } //With slug not matching if ($this->context['album']['slug'] !== $slug) { //Redirect on clean slug return $this->redirectToRoute($this->route, [ 'slug' => $this->context['album']['slug'] ]+$this->routeParams); } //Set modified $this->modified = $this->context['album']['modified']; //Create response $response = new Response(); //With logged user if ($this->checker->isGranted('IS_AUTHENTICATED_REMEMBERED')) { //Set last modified $response->setLastModified(new \DateTime('-1 year')); //Set as private $response->setPrivate(); //Without logged user } else { //Set etag //XXX: only for public to force revalidation by last modified $response->setEtag(md5(serialize($this->context['album']))); //Set last modified $response->setLastModified($this->modified); //Set as public $response->setPublic(); //Without role and modification if ($response->isNotModified($request)) { //Return 304 response return $response; } } //Set keywords /*$this->context['head']['keywords'] = implode( ', ', //Use closure to extract each unique article keywords sorted (function ($t) { //Return array $r = []; //Iterate on articles foreach($t as $a) { //Non empty keywords if (!empty($a['keywords'])) { //Iterate on keywords foreach($a['keywords'] as $k) { //Set keyword $r[$k['title']] = $k['title']; } } } //Sort array sort($r); //Return array return $r; })($this->context['articles']) ); //Get albums $this->context['albums'] = $this->config['albums'];*/ //Return rendered response return $this->render('@RapsysTree/album.html.twig', $this->context, $response); } /** * The element page * * Display element * * @param Request $request The request instance * @param int $id The element id * @param ?string $path The element path * @return Response The rendered view */ public function element(Request $request, int $id, string $path): Response { //Get user $user = $this->security->getUser(); //Without element if (!($this->context['element'] = $this->doctrine->getRepository(Element::class)->findOneByUidIdPathAsArray($user?->getId(), $id, $path))) { //Throw 404 throw $this->createNotFoundException($this->translator->trans('Unable to find element')); } //With realpath not matching path //XXX: extra slashes removed by rewrite rule if (($this->context['path'] = substr($this->context['element']['realpath'], strlen($this->context['element']['album']['path'])+1)) !== $path) { //Redirect on clean path return $this->redirectToRoute($this->route, [ 'path' => $this->context['path'] ]+$this->routeParams); } //Set modified $this->modified = $this->context['element']['modified']; //Create response $response = new Response(); //With logged user if ($this->checker->isGranted('IS_AUTHENTICATED_REMEMBERED')) { //Set last modified $response->setLastModified(new \DateTime('-1 year')); //Set as private $response->setPrivate(); //Without logged user } else { //Set etag //XXX: only for public to force revalidation by last modified $response->setEtag(md5(serialize($this->context['element']))); //Set last modified $response->setLastModified($this->modified); //Set as public $response->setPublic(); //Without role and modification if ($response->isNotModified($request)) { //Return 304 response return $response; } } //TODO: move that in a shared function to use in album member function too /* //With directory if (is_dir($realpath)) { //Set element directories $this->context['element'] += [ //Directories 'directories' => [], //Files 'files' => [] ]; //Iterate on directory foreach(array_diff(scandir($realpath), ['.', '..']) as $item) { //Check item if ( //Without item realpath !($itempath = realpath($realpath.'/'.$item)) || //With item realpath not matching element path ( $this->context['element']['album']['path'].$this->context['element']['path'] !== substr($itempath, 0, strlen($this->context['element']['album']['path'].$this->context['element']['path'])) ) ) { //Skip continue; } //With directory if (is_dir($itempath)) { //Append directory $this->context['element']['directories'][$item] = $this->router->generate($this->route, [ 'path' => $this->context['path'].'/'.$item ]+$this->routeParams); //With file } elseif (is_file($itempath)) { //Append file $this->context['element']['files'][$item] = [ //Set mime 'mime' => mime_content_type($itempath), //Set size 'size' => filesize($itempath), //Set link 'link' => $this->router->generate($this->route, [ 'path' => $this->context['path'].'/'.$item ]+$this->routeParams) ]; //With unknown type } else { //Throw 404 throw $this->createNotFoundException($this->translator->trans('Unable to process element')); } } //With file } elseif (is_file($realpath)) { //Append file $this->context['element']['file'] = [ //Set mime 'mime' => mime_content_type($realpath), //Set size 'size' => filesize($realpath), //TODO: extra fields ? (preview, miniature, etc ?) ]; //TODO: mimetype decided extra fields ? (.pdf for doc, webm preview, img preview, etc ?) //TODO: XXX: finish this !!! //With unknown type } else { //Throw 404 throw $this->createNotFoundException($this->translator->trans('Unable to process element')); } */ //Set keywords /*$this->context['head']['keywords'] = implode( ', ', //Use closure to extract each unique article keywords sorted (function ($t) { //Return array $r = []; //Iterate on articles foreach($t as $a) { //Non empty keywords if (!empty($a['keywords'])) { //Iterate on keywords foreach($a['keywords'] as $k) { //Set keyword $r[$k['title']] = $k['title']; } } } //Sort array sort($r); //Return array return $r; })($this->context['articles']) ); //Get albums $this->context['albums'] = $this->config['albums'];*/ #header('Content-Type: text/plain'); #var_dump($this->context['element']); #exit; //Return rendered response return $this->render('@RapsysTree/element.html.twig', $this->context, $response); } /** * The index page * * Display index * * @param Request $request The request instance * @return Response The rendered view */ public function index(Request $request): Response { //Get user $user = $this->security->getUser(); //With not enough albums if (($this->count = $this->doctrine->getRepository(Album::class)->countByUidAsInt($user?->getId())) < $this->page * $this->limit) { //Throw 404 throw $this->createNotFoundException($this->translator->trans('Unable to find albums')); } //Get albums if ($this->context['albums'] = $this->doctrine->getRepository(Album::class)->findByUidAsArray($user?->getId(), $this->page, $this->limit)) { //Set modified $this->modified = max(array_map(function ($v) { return $v['modified']; }, $this->context['albums'])); //Without albums } else { //Set empty modified $this->modified = new \DateTime('-1 year'); } //Create response $response = new Response(); //With logged user if ($this->checker->isGranted('IS_AUTHENTICATED_REMEMBERED')) { //Set last modified $response->setLastModified(new \DateTime('-1 year')); //Set as private $response->setPrivate(); //Without logged user } else { //Set etag //XXX: only for public to force revalidation by last modified $response->setEtag(md5(serialize($this->context['albums']))); //Set last modified $response->setLastModified($this->modified); //Set as public $response->setPublic(); //Without role and modification if ($response->isNotModified($request)) { //Return 304 response return $response; } } //Set keywords /*$this->context['head']['keywords'] = implode( ', ', //Use closure to extract each unique article keywords sorted (function ($t) { //Return array $r = []; //Iterate on articles foreach($t as $a) { //Non empty keywords if (!empty($a['keywords'])) { //Iterate on keywords foreach($a['keywords'] as $k) { //Set keyword $r[$k['title']] = $k['title']; } } } //Sort array sort($r); //Return array return $r; })($this->context['articles']) ); //Get albums $this->context['albums'] = $this->config['albums'];*/ //Return rendered response return $this->render('@RapsysTree/index.html.twig', $this->context, $response); } /** * The directory page * * Display directory * * @param Request $request The request instance * @param string $path The directory path * @return Response The rendered view */ public function directory(Request $request, string $path): Response { header('Content-Type: text/plain'); var_dump($path); exit; //Render template $response = $this->render('@RapsysTree/directory.html.twig', $this->context); $response->setEtag(md5($response->getContent())); $response->setPublic(); $response->isNotModified($request); //Return response return $response; } /** * The document page * * Display document * * @param Request $request The request instance * @param string $path The directory path * @return Response The rendered view */ public function document(Request $request, string $path): Response { //Render template $response = $this->render('@RapsysTree/document.html.twig', $this->context); $response->setEtag(md5($response->getContent())); $response->setPublic(); $response->isNotModified($request); //Return response return $response; } /** * Renders a view * * {@inheritdoc} */ protected function render(string $view, array $parameters = [], Response $response = null): Response { //Create response when null $response ??= new Response(); //Without alternates if (count($parameters['alternates']) <= 1) { //Set routeParams $routeParams = $this->routeParams; //Iterate on locales excluding current one foreach($this->config['locales'] as $locale) { //With current locale if ($locale !== $this->locale) { //Set titles $titles = []; //Set route params locale $routeParams['_locale'] = $locale; //Iterate on other locales foreach(array_diff($this->config['locales'], [$locale]) as $other) { //Set other locale title $titles[$other] = $this->translator->trans($this->config['languages'][$locale], [], null, $other); } //Set locale locales context $parameters['alternates'][str_replace('_', '-', $locale)] = [ 'absolute' => $this->router->generate($this->route, $routeParams, UrlGeneratorInterface::ABSOLUTE_URL), 'relative' => $this->router->generate($this->route, $routeParams), 'title' => implode('/', $titles), 'translated' => $this->translator->trans($this->config['languages'][$locale], [], null, $locale) ]; //Add shorter locale if (empty($parameters['alternates'][$shortCurrent = substr($locale, 0, 2)])) { //Set locale locales context $parameters['alternates'][$shortCurrent] = $parameters['alternates'][str_replace('_', '-', $locale)]; } } } } //With canonical if (!empty($parameters['canonical'])) { //Set facebook url $parameters['facebook']['og:url'] = $parameters['canonical']; } //With empty facebook title and title if (empty($parameters['facebook']['og:title']) && !empty($parameters['title']['page'])) { //Set facebook title $parameters['facebook']['og:title'] = $parameters['title']['page']; } //With empty facebook description and description if (empty($parameters['facebook']['og:description']) && !empty($parameters['description'])) { //Set facebook description $parameters['facebook']['og:description'] = $parameters['description']; } //With locale if (!empty($this->locale)) { //Set facebook locale $parameters['facebook']['og:locale'] = $this->locale; //With alternates //XXX: locale change when fb_locale=xx_xx is provided is done in FacebookSubscriber //XXX: see https://stackoverflow.com/questions/20827882/in-open-graph-markup-whats-the-use-of-oglocalealternate-without-the-locati if (!empty($parameters['alternates'])) { //Iterate on alternates foreach($parameters['alternates'] as $lang => $alternate) { if (strlen($lang) == 5) { //Set facebook locale alternate $parameters['facebook']['og:locale:alternate'] = str_replace('-', '_', $lang); } } } } //Without facebook image defined and texts if (empty($parameters['facebook']['og:image']) && !empty($this->request) && !empty($parameters['fbimage']['texts']) && !empty($this->modified)) { //Get facebook image $parameters['facebook'] += $this->facebook->getImage($this->request->getPathInfo(), $parameters['fbimage']['texts'], $this->modified->getTimestamp()); } //Call twig render method $content = $this->twig->render($view, $parameters); //Invalidate OK response on invalid form if (200 === $response->getStatusCode()) { foreach ($parameters as $v) { if ($v instanceof FormInterface && $v->isSubmitted() && !$v->isValid()) { $response->setStatusCode(422); break; } } } //Store content in response $response->setContent($content); //Return response return $response; } }