Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework the front end preview #989

Merged
merged 30 commits into from
Jan 6, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
414b966
Rework frontend preview
richardhj Nov 25, 2019
4bad3d6
Collapse toolbar
richardhj Nov 25, 2019
2b856c1
Check for frontend request
richardhj Nov 25, 2019
a3a5630
Apply suggestions from code review
richardhj Nov 25, 2019
2098eba
Remove the contao_backend_switch route
richardhj Nov 25, 2019
8eacd71
Merge remote-tracking branch 'richardhj/feature/preview' into feature…
richardhj Nov 25, 2019
3315cc9
Apply suggestions from code review
richardhj Nov 26, 2019
b04c925
Inject js before </body>
richardhj Nov 26, 2019
9ebe83d
Remove non-present route
richardhj Nov 26, 2019
6fae43a
Move logic to PreviewUrlConvertEvent
richardhj Nov 28, 2019
280d16a
Add tests
richardhj Nov 29, 2019
2336e22
Apply suggestions from code review
richardhj Dec 2, 2019
2974a20
Do not inject preview bar if no preview entrypoint defined
richardhj Dec 3, 2019
dfe0d2a
Add ROLE_ALLOWED_TO_SWITCH_MEMBER
richardhj Dec 3, 2019
519124d
Further styling of toolbar
richardhj Dec 6, 2019
0732765
Fix safari incompatibilites
richardhj Dec 6, 2019
cfd83d9
Merge branch 'master' into feature/preview
richardhj Dec 11, 2019
80d06fd
Fix QS
richardhj Dec 11, 2019
f2f36f4
Fix the coding style
leofeyer Dec 19, 2019
0e3ae2e
Merge remote-tracking branch 'upstream/master' into feature/preview
richardhj Dec 30, 2019
4a969b2
Serialize amg in BackendUser.php
richardhj Dec 30, 2019
206d929
Remove CDATA as we use HTML
richardhj Dec 30, 2019
2c1c97d
Do not initialize framework.
richardhj Dec 30, 2019
550a561
Test service registration.
richardhj Dec 30, 2019
836d3c3
Fix the coding style
leofeyer Jan 6, 2020
1371041
Fix the CSS code
leofeyer Jan 6, 2020
8cbf016
Simplify event listener registration.
richardhj Jan 6, 2020
7666ac6
Remove legacy preview script code.
richardhj Jan 6, 2020
b225e1d
Add BackendPreviewSwitchControllerTest.php
richardhj Jan 6, 2020
973a979
Adjust the test method names
leofeyer Jan 6, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 0 additions & 19 deletions core-bundle/src/Controller/BackendController.php
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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);
richardhj marked this conversation as resolved.
Show resolved Hide resolved

if (!$this->authorizationChecker->isGranted('ROLE_USER')) {
richardhj marked this conversation as resolved.
Show resolved Hide resolved
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')) {
richardhj marked this conversation as resolved.
Show resolved Hide resolved
$targetUrl = $request->getBaseUrl().'/'.$request->query->get('url');
throw new RedirectResponseException($targetUrl);
}

if ($request->query->get('page') && null !== $page = PageModel::findWithDetails($request->query->get('page'))) {
richardhj marked this conversation as resolved.
Show resolved Hide resolved
$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);
richardhj marked this conversation as resolved.
Show resolved Hide resolved
}

throw new RedirectResponseException(
$this->router->generate('contao_root', [], UrlGeneratorInterface::ABSOLUTE_URL)
);
}
}
206 changes: 206 additions & 0 deletions core-bundle/src/Controller/BackendPreviewSwitchController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
<?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);
richardhj marked this conversation as resolved.
Show resolved Hide resolved

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

if (!($user instanceof BackendUser) || !$request->isXmlHttpRequest()) {
throw new PageNotFoundException('Bad response');
richardhj marked this conversation as resolved.
Show resolved Hide resolved
}

if ($request->isMethod('GET')) {
try {
$toolbar = $this->renderToolbar($user);
} catch (TwigError $e) {
return Response::create('', Response::HTTP_INTERNAL_SERVER_ERROR);
richardhj marked this conversation as resolved.
Show resolved Hide resolved
}

return Response::create($toolbar);
}

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

return Response::create();
richardhj marked this conversation as resolved.
Show resolved Hide resolved
}

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

return JsonResponse::create($data);
}

return Response::create('', Response::HTTP_INTERNAL_SERVER_ERROR);
richardhj marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* @throws TwigError
*/
private function renderToolbar(BackendUser $user): string
{
$canSwitchUser = ($user->isAdmin || (!empty($user->amg) && \is_array($user->amg)));
richardhj marked this conversation as resolved.
Show resolved Hide resolved
$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));
}
}
Loading