Skip to content

Commit

Permalink
Rework frontend preview
Browse files Browse the repository at this point in the history
  • Loading branch information
richardhj committed Nov 25, 2019
1 parent 0feddd8 commit 414b966
Show file tree
Hide file tree
Showing 12 changed files with 668 additions and 430 deletions.
19 changes: 0 additions & 19 deletions core-bundle/src/Controller/BackendController.php
Expand Up @@ -21,7 +21,6 @@
use Contao\BackendPage;
use Contao\BackendPassword;
use Contao\BackendPopup;
use Contao\BackendPreview;
use Contao\BackendSwitch;
use Contao\CoreBundle\Framework\ContaoFramework;
use Contao\CoreBundle\Picker\PickerBuilderInterface;
Expand Down Expand Up @@ -94,24 +93,6 @@ public function passwordAction(): Response
return $controller->run();
}

/**
* @Route("/contao/preview", name="contao_backend_preview")
*/
public function previewAction(Request $request): Response
{
$previewScript = $this->getParameter('contao.preview_script');

if ($request->getScriptName() !== $previewScript) {
return $this->redirect($previewScript.$request->getRequestUri());
}

$this->get('contao.framework')->initialize();

$controller = new BackendPreview();

return $controller->run();
}

/**
* @Route("/contao/confirm", name="contao_backend_confirm")
*/
Expand Down
118 changes: 118 additions & 0 deletions core-bundle/src/Controller/BackendPreviewController.php
@@ -0,0 +1,118 @@
<?php

declare(strict_types=1);

/*
* This file is part of Contao.
*
* (c) Leo Feyer
*
* @license LGPL-3.0-or-later
*/

namespace Contao\CoreBundle\Controller;

use Contao\ArticleModel;
use Contao\CoreBundle\Event\PreviewUrlConvertEvent;
use Contao\CoreBundle\Exception\AccessDeniedException;
use Contao\CoreBundle\Exception\RedirectResponseException;
use Contao\CoreBundle\Framework\ContaoFramework;
use Contao\CoreBundle\Security\Authentication\FrontendPreviewAuthenticator;
use Contao\PageModel;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;

/**
* This controller handles the back end preview call and redirects to the requested front end page while ensuring the
* /preview.php entry point is used. When requested, the front end user gets authenticated.
*
* @Route(defaults={"_scope" = "backend"})
*/
class BackendPreviewController
{
private $contaoFramework;

private $previewScript;

private $frontendPreviewAuthenticator;

private $dispatcher;

private $router;

private $authorizationChecker;

public function __construct(
ContaoFramework $contaoFramework,
string $previewScript,
FrontendPreviewAuthenticator $frontendPreviewAuthenticator,
EventDispatcherInterface $dispatcher,
RouterInterface $router,
AuthorizationCheckerInterface $authorizationChecker
) {
$this->contaoFramework = $contaoFramework;
$this->previewScript = $previewScript;
$this->frontendPreviewAuthenticator = $frontendPreviewAuthenticator;
$this->dispatcher = $dispatcher;
$this->router = $router;
$this->authorizationChecker = $authorizationChecker;
}

/**
* @Route("/contao/preview", name="contao_backend_preview")
*/
public function __invoke(Request $request): Response
{
if ($request->getScriptName() !== $this->previewScript) {
throw new RedirectResponseException($this->previewScript.$request->getRequestUri());
}

$this->contaoFramework->initialize(false);

if (!$this->authorizationChecker->isGranted('ROLE_USER')) {
throw new AccessDeniedException('Access denied');
}

// Switch to a particular member (see contao/core#6546)
if (($frontendUser = $request->query->get('user'))
&& !$this->frontendPreviewAuthenticator->authenticateFrontendUser($frontendUser, false)) {
$this->frontendPreviewAuthenticator->removeFrontendAuthentication();
}

if ($request->query->get('url')) {
$targetUrl = $request->getBaseUrl().'/'.$request->query->get('url');
throw new RedirectResponseException($targetUrl);
}

if ($request->query->get('page') && null !== $page = PageModel::findWithDetails($request->query->get('page'))) {
$params = null;

// Add the /article/ fragment (see contao/core-bundle#673)
if (null !== ($article = ArticleModel::findByAlias($request->query->get('article')))) {
$params = sprintf(
'/articles/%s%s',
('main' !== $article->inColumn) ? $article->inColumn.':' : '',
$article->id
);
}

throw new RedirectResponseException($page->getPreviewUrl($params));
}

$urlConvertEvent = new PreviewUrlConvertEvent();
$this->dispatcher->dispatch($urlConvertEvent);

if (null !== $targetUrl = $urlConvertEvent->getUrl()) {
throw new RedirectResponseException($targetUrl);
}

throw new RedirectResponseException(
$this->router->generate('contao_root', [], UrlGeneratorInterface::ABSOLUTE_URL)
);
}
}
202 changes: 202 additions & 0 deletions core-bundle/src/Controller/BackendPreviewSwitchController.php
@@ -0,0 +1,202 @@
<?php

declare(strict_types=1);

/*
* This file is part of Contao.
*
* (c) Leo Feyer
*
* @license LGPL-3.0-or-later
*/

namespace Contao\CoreBundle\Controller;

use Contao\BackendUser;
use Contao\CoreBundle\Exception\PageNotFoundException;
use Contao\CoreBundle\Framework\ContaoFramework;
use Contao\CoreBundle\Security\Authentication\FrontendPreviewAuthenticator;
use Contao\CoreBundle\Security\Authentication\Token\TokenChecker;
use Contao\Date;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\FetchMode;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Twig\Environment as TwigEnvironment;
use Twig\Error\Error as TwigError;

/**
* This controller serves for the back end preview toolbar by providing the following ajax endpoints:
* a) Return the toolbar html (dispatched in an ajax request to allow lazy loading and force back end scope)
* b) Provide members' usernames for the datalist
* c) Process the switch action (i.e. log in a specific front end user).
*
* @Route(defaults={"_scope" = "backend"})
*/
class BackendPreviewSwitchController
{
private $contaoFramework;

private $frontendPreviewAuthenticator;

private $tokenChecker;

private $connection;

private $security;

private $twig;

private $tokenManager;

private $csrfTokenName;

private $router;

public function __construct(
ContaoFramework $contaoFramework,
FrontendPreviewAuthenticator $frontendPreviewAuthenticator,
TokenChecker $tokenChecker,
Connection $connection,
Security $security,
TwigEnvironment $twig,
RouterInterface $router,
CsrfTokenManagerInterface $tokenManager,
string $csrfTokenName
) {
$this->contaoFramework = $contaoFramework;
$this->frontendPreviewAuthenticator = $frontendPreviewAuthenticator;
$this->tokenChecker = $tokenChecker;
$this->connection = $connection;
$this->security = $security;
$this->twig = $twig;
$this->router = $router;
$this->tokenManager = $tokenManager;
$this->csrfTokenName = $csrfTokenName;
}

/**
* @Route("/contao/preview_switch", name="contao_backend_preview_switch")
*/
public function __invoke(Request $request): Response
{
$this->contaoFramework->initialize(false);

$user = $this->security->getUser();

if (!($user instanceof BackendUser) || !$request->isXmlHttpRequest()) {
throw new PageNotFoundException('Bad response');
}

if ($request->isMethod('GET')) {
$toolbar = $this->renderToolbar($user);

return Response::create($toolbar);
}

if ('tl_switch' === $request->request->get('FORM_SUBMIT')) {
$this->authenticatePreview($user, $request);

return Response::create();
}

if ('datalist_members' === $request->request->get('FORM_SUBMIT')) {
$data = $this->getMembersDataList($user, $request);

return JsonResponse::create($data);
}

return Response::create('', 404);
}

/**
* @throws TwigError
*/
private function renderToolbar(BackendUser $user): string
{
$canSwitchUser = ($user->isAdmin || (!empty($user->amg) && \is_array($user->amg)));
$frontendUsername = $this->tokenChecker->getFrontendUsername();
$showUnpublished = $this->tokenChecker->isPreviewMode();

return $this->twig->render(
'@ContaoCore/Frontend/preview_toolbar_base.html.twig',
[
'request_token' => $this->tokenManager->getToken($this->csrfTokenName)->getValue(),
'action' => $this->router->generate('contao_backend_preview_switch'),
'canSwitchUser' => $canSwitchUser,
'user' => $frontendUsername,
'show' => $showUnpublished,
]
);
}

private function authenticatePreview(BackendUser $user, Request $request): void
{
$canSwitchUser = $this->isAllowedToAccessMembers($user);
$frontendUsername = $this->tokenChecker->getFrontendUsername();
$showUnpublished = 'hide' !== $request->request->get('unpublished');

if ($canSwitchUser) {
$frontendUsername = $request->request->get('user') ?: null;
}

if (null !== $frontendUsername) {
$this->frontendPreviewAuthenticator->authenticateFrontendUser($frontendUsername, $showUnpublished);
} else {
$this->frontendPreviewAuthenticator->authenticateFrontendGuest($showUnpublished);
}
}

private function getMembersDataList(BackendUser $user, Request $request): array
{
$andWhereGroups = '';

if (!$this->isAllowedToAccessMembers($user)) {
return [];
}

if (!$user->isAdmin) {
$groups = array_map(
static function ($groupId) {
return '%"'.(int) $groupId.'"%';
},
$user->amg
);

$andWhereGroups = "AND (groups LIKE '".implode("' OR GROUPS LIKE '", $groups)."')";
}

$time = Date::floorToMinute();

// Get the active front end users
$result = $this->connection->executeQuery(
sprintf(
<<<'SQL'
SELECT username
FROM tl_member
WHERE username LIKE ?
%s
AND login='1' AND disable!='1' AND (start='' OR start<='%s') AND (stop='' OR stop>'%d')
ORDER BY username
SQL
,
$andWhereGroups,
$time,
$time + 60
),
[str_replace('%', '', $request->request->get('value')).'%']
);

return $result->fetchAll(FetchMode::COLUMN);
}

private function isAllowedToAccessMembers(BackendUser $user): bool
{
return $user->isAdmin || (!empty($user->amg) && \is_array($user->amg));
}
}

0 comments on commit 414b966

Please sign in to comment.