Skip to content

Commit

Permalink
Merge branch '4.7'
Browse files Browse the repository at this point in the history
# Conflicts:
#	CHANGELOG.md
#	core-bundle/src/Resources/contao/config/constants.php
#	core-bundle/src/Resources/contao/languages/cs/default.xlf
#	core-bundle/src/Resources/contao/languages/cs/tl_opt_in.xlf
#	core-bundle/src/Resources/contao/languages/de/default.xlf
#	core-bundle/src/Resources/contao/languages/de/tl_opt_in.xlf
#	core-bundle/src/Resources/contao/languages/es/default.xlf
#	core-bundle/src/Resources/contao/languages/es/tl_opt_in.xlf
#	core-bundle/src/Resources/contao/languages/fa/default.xlf
#	core-bundle/src/Resources/contao/languages/fa/tl_opt_in.xlf
#	core-bundle/src/Resources/contao/languages/fr/default.xlf
#	core-bundle/src/Resources/contao/languages/fr/tl_opt_in.xlf
#	core-bundle/src/Resources/contao/languages/it/default.xlf
#	core-bundle/src/Resources/contao/languages/it/tl_opt_in.xlf
#	core-bundle/src/Resources/contao/languages/ja/default.xlf
#	core-bundle/src/Resources/contao/languages/ja/tl_opt_in.xlf
#	core-bundle/src/Resources/contao/languages/nl/default.xlf
#	core-bundle/src/Resources/contao/languages/nl/tl_opt_in.xlf
#	core-bundle/src/Resources/contao/languages/pl/default.xlf
#	core-bundle/src/Resources/contao/languages/pl/tl_opt_in.xlf
#	core-bundle/src/Resources/contao/languages/ru/default.xlf
#	core-bundle/src/Resources/contao/languages/ru/tl_opt_in.xlf
#	core-bundle/src/Resources/contao/languages/sl/default.xlf
#	core-bundle/src/Resources/contao/languages/sl/tl_opt_in.xlf
#	core-bundle/src/Resources/contao/languages/sr/default.xlf
#	core-bundle/src/Resources/contao/languages/sr/tl_opt_in.xlf
#	core-bundle/src/Resources/contao/languages/zh/default.xlf
#	core-bundle/src/Resources/contao/languages/zh/tl_opt_in.xlf
  • Loading branch information
leofeyer committed Apr 9, 2019
2 parents c0fc631 + 8da9e02 commit b479b71
Show file tree
Hide file tree
Showing 35 changed files with 847 additions and 222 deletions.
Expand Up @@ -220,7 +220,7 @@
(
array('tl_calendar_events', 'loadTime')
),
'sql' => "int(10) unsigned NULL"
'sql' => "int(10) NULL"
),
'endTime' => array
(
Expand All @@ -237,7 +237,7 @@
(
array('tl_calendar_events', 'setEmptyEndTime')
),
'sql' => "int(10) unsigned NULL"
'sql' => "int(10) NULL"
),
'startDate' => array
(
Expand Down
22 changes: 18 additions & 4 deletions comments-bundle/src/Resources/contao/classes/Comments.php
Expand Up @@ -584,7 +584,7 @@ public static function addCommentsSubscription(CommentsModel $objComment)

/** @var OptIn $optIn */
$optIn = System::getContainer()->get('contao.opt-in');
$optInToken = $optIn->create('com-', $objComment->email, array('tl_comments_notify'=>array($objNotify->id)));
$optInToken = $optIn->create('com', $objComment->email, array('tl_comments_notify'=>array($objNotify->id)));

// Send the token
$optInToken->send(sprintf($GLOBALS['TL_LANG']['MSC']['com_optInSubject'], Idna::decode(Environment::get('host'))), sprintf($GLOBALS['TL_LANG']['MSC']['com_optInMessage'], $objComment->name, $strUrl, $strUrl . $strConnector . 'token=' . $optInToken->getIdentifier(), $strUrl . $strConnector . 'token=' . $objNotify->tokenRemove));
Expand All @@ -603,9 +603,23 @@ public static function changeSubscriptionStatus(FrontendTemplate $objTemplate)
$optIn = System::getContainer()->get('contao.opt-in');

// Find an unconfirmed token with only one related record
if ((!$optInToken = $optIn->find(Input::get('token'))) || $optInToken->isConfirmed() || \count($arrRelated = $optInToken->getRelatedRecords()) != 1 || key($arrRelated) != 'tl_comments_notify' || \count($arrIds = current($arrRelated)) != 1 || (!$objNotify = CommentsNotifyModel::findByPk($arrIds[0])))
if ((!$optInToken = $optIn->find(Input::get('token'))) || !$optInToken->isValid() || \count($arrRelated = $optInToken->getRelatedRecords()) != 1 || key($arrRelated) != 'tl_comments_notify' || \count($arrIds = current($arrRelated)) != 1 || (!$objNotify = CommentsNotifyModel::findByPk($arrIds[0])))
{
$objTemplate->confirm = $GLOBALS['TL_LANG']['MSC']['invalidTokenUrl'];
$objTemplate->confirm = $GLOBALS['TL_LANG']['MSC']['invalidToken'];

return;
}

if ($optInToken->isConfirmed())
{
$objTemplate->confirm = $GLOBALS['TL_LANG']['MSC']['tokenConfirmed'];

return;
}

if ($optInToken->getEmail() != $objNotify->email)
{
$objTemplate->confirm = $GLOBALS['TL_LANG']['MSC']['tokenEmailMismatch'];

return;
}
Expand All @@ -623,7 +637,7 @@ public static function changeSubscriptionStatus(FrontendTemplate $objTemplate)

if ($objNotify === null)
{
$objTemplate->confirm = $GLOBALS['TL_LANG']['MSC']['invalidTokenUrl'];
$objTemplate->confirm = $GLOBALS['TL_LANG']['MSC']['invalidToken'];

return;
}
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Expand Up @@ -50,7 +50,7 @@
"firebase/php-jwt": "^4.0",
"friendsofsymfony/http-cache": "^2.5",
"friendsofsymfony/http-cache-bundle": "^2.6",
"imagine/imagine": "^0.7",
"imagine/imagine": "^0.7 || ^1.0",
"knplabs/knp-menu-bundle": "^2.1",
"knplabs/knp-time-bundle": "^1.5.2",
"leafo/scssphp": "^0.7.1",
Expand Down
2 changes: 1 addition & 1 deletion core-bundle/composer.json
Expand Up @@ -49,7 +49,7 @@
"firebase/php-jwt": "^4.0",
"friendsofsymfony/http-cache": "^2.5",
"friendsofsymfony/http-cache-bundle": "^2.6",
"imagine/imagine": "^0.7",
"imagine/imagine": "^0.7 || ^1.0",
"knplabs/knp-menu-bundle": "^2.1",
"knplabs/knp-time-bundle": "^1.5.2",
"leafo/scssphp": "^0.7.1",
Expand Down
4 changes: 4 additions & 0 deletions core-bundle/src/DataContainer/PaletteManipulator.php
Expand Up @@ -193,6 +193,10 @@ private function explode(string $palette): array
$groups = StringUtil::trimsplit(';', $palette);

foreach ($groups as $group) {
if ('' === $group) {
continue;
}

$hide = false;
$fields = StringUtil::trimsplit(',', $group);

Expand Down
111 changes: 111 additions & 0 deletions core-bundle/src/EventListener/RequestTokenListener.php
@@ -0,0 +1,111 @@
<?php

declare(strict_types=1);

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

namespace Contao\CoreBundle\EventListener;

use Contao\Config;
use Contao\CoreBundle\Exception\InvalidRequestTokenException;
use Contao\CoreBundle\Framework\ContaoFramework;
use Contao\CoreBundle\Routing\ScopeMatcher;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;

/**
* Validates the request token if the request is a Contao request.
*/
class RequestTokenListener
{
/**
* @var ContaoFramework
*/
private $framework;

/**
* @var ScopeMatcher
*/
private $scopeMatcher;

/**
* @var CsrfTokenManagerInterface
*/
private $csrfTokenManager;

/**
* @var string
*/
private $csrfTokenName;

public function __construct(ContaoFramework $framework, ScopeMatcher $scopeMatcher, CsrfTokenManagerInterface $csrfTokenManager, string $csrfTokenName)
{
$this->framework = $framework;
$this->scopeMatcher = $scopeMatcher;
$this->csrfTokenManager = $csrfTokenManager;
$this->csrfTokenName = $csrfTokenName;
}

/**
* @throws InvalidRequestTokenException
*/
public function onKernelRequest(GetResponseEvent $event): void
{
$request = $event->getRequest();

// Only check the request token if a) the request is a POST request, b)
// the request is not an Ajax request, c) the _token_check attribute is
// not false and d) the _token_check attribute is set or the request is
// a Contao request
if (
'POST' !== $request->getRealMethod()
|| $request->isXmlHttpRequest()
|| false === $request->attributes->get('_token_check')
|| (!$request->attributes->has('_token_check') && !$this->scopeMatcher->isContaoRequest($request))
) {
return;
}

/** @var Config $config */
$config = $this->framework->getAdapter(Config::class);

if (\defined('BYPASS_TOKEN_CHECK')) {
@trigger_error('Defining the BYPASS_TOKEN_CHECK constant has been deprecated and will no longer work in Contao 5.0.', E_USER_DEPRECATED);

return;
}

if ($config->get('disableRefererCheck')) {
@trigger_error('Using the "disableRefererCheck" setting has been deprecated and will no longer work in Contao 5.0.', E_USER_DEPRECATED);

return;
}

if ($config->get('requestTokenWhitelist')) {
@trigger_error('Using the "requestTokenWhitelist" setting has been deprecated and will no longer work in Contao 5.0.', E_USER_DEPRECATED);

$hostname = gethostbyaddr($request->getClientIp());

foreach ($config->get('requestTokenWhitelist') as $domain) {
if ($domain === $hostname || preg_match('/\.' . preg_quote($domain, '/') . '$/', $hostname)) {
return;
}
}
}

$token = new CsrfToken($this->csrfTokenName, $request->request->get('REQUEST_TOKEN'));

if ($this->csrfTokenManager->isTokenValid($token)) {
return;
}

throw new InvalidRequestTokenException('Invalid CSRF token. Please reload the page and try again.');
}
}
20 changes: 0 additions & 20 deletions core-bundle/src/Framework/ContaoFramework.php
Expand Up @@ -15,7 +15,6 @@
use Contao\ClassLoader;
use Contao\Config;
use Contao\CoreBundle\Exception\IncompleteInstallationException;
use Contao\CoreBundle\Exception\InvalidRequestTokenException;
use Contao\CoreBundle\Routing\ScopeMatcher;
use Contao\CoreBundle\Security\Authentication\Token\TokenChecker;
use Contao\CoreBundle\Session\LazySessionAccess;
Expand Down Expand Up @@ -383,9 +382,6 @@ private function triggerInitializeSystemHook(): void
}
}

/**
* @throws InvalidRequestTokenException
*/
private function handleRequestToken(): void
{
/** @var RequestToken $requestToken */
Expand All @@ -395,12 +391,6 @@ private function handleRequestToken(): void
if (!\defined('REQUEST_TOKEN')) {
\define('REQUEST_TOKEN', 'cli' === \PHP_SAPI ? null : $requestToken->get());
}

if ($this->canSkipTokenCheck() || $requestToken->validate($this->request->request->get('REQUEST_TOKEN'))) {
return;
}

throw new InvalidRequestTokenException('Invalid request token. Please reload the page and try again.');
}

private function iniSet(string $key, string $value): void
Expand All @@ -419,16 +409,6 @@ private function getSession(): ?SessionInterface
return $this->request->getSession();
}

private function canSkipTokenCheck(): bool
{
return null === $this->request
|| 'POST' !== $this->request->getRealMethod()
|| $this->request->isXmlHttpRequest()
|| !$this->request->attributes->has('_token_check')
|| false === $this->request->attributes->get('_token_check')
;
}

private function registerHookListeners(): void
{
foreach ($this->hookListeners as $hookName => $priorities) {
Expand Down
6 changes: 5 additions & 1 deletion core-bundle/src/OptIn/OptIn.php
Expand Up @@ -33,14 +33,18 @@ public function __construct(ContaoFramework $framework)
*/
public function create(string $prefix, string $email, array $related): OptInTokenInterface
{
if ($prefix) {
$prefix = rtrim($prefix, '-');
}

if (\strlen($prefix) > 6) {
throw new \InvalidArgumentException('The token prefix must not be longer than 6 characters');
}

$token = bin2hex(random_bytes(12));

if ($prefix) {
$token = $prefix.substr($token, \strlen($prefix));
$token = $prefix.'-'.substr($token, \strlen($prefix) + 1);
}

/** @var OptInModel $optIn */
Expand Down
40 changes: 39 additions & 1 deletion core-bundle/src/OptIn/OptInToken.php
Expand Up @@ -55,7 +55,7 @@ public function getEmail(): string
*/
public function isValid(): bool
{
return $this->model->createdOn > strtotime('-24 hours');
return !$this->model->invalidatedThrough && $this->model->createdOn > strtotime('-24 hours');
}

/**
Expand All @@ -75,6 +75,44 @@ public function confirm(): void
$this->model->confirmedOn = time();
$this->model->removeOn = strtotime('+3 years');
$this->model->save();

$related = $this->model->getRelatedRecords();

if (empty($related)) {
return;
}

/** @var OptInModel $adapter */
$adapter = $this->framework->getAdapter(OptInModel::class);
$prefix = strtok($this->getIdentifier(), '-');

// Invalidate other tokens that relate to the same records
foreach ($related as $table => $ids) {
if (!$models = $adapter->findByRelatedTableAndIds($table, $ids)) {
continue;
}

foreach ($models as $model) {
if (
$model->confirmedOn > 0
|| $model->invalidatedThrough
|| $model->token === $this->getIdentifier()
|| 0 !== strncmp($model->token, $prefix.'-', \strlen($prefix) + 1)
) {
continue;
}

$token = new OptInToken($model, $this->framework);

// The related records must match exactly
if ($token->getRelatedRecords() !== $related) {
continue;
}

$model->invalidatedThrough = $this->model->token;
$model->save();
}
}
}

/**
Expand Down
11 changes: 11 additions & 0 deletions core-bundle/src/Resources/config/listener.yml
Expand Up @@ -133,6 +133,17 @@ services:
# The priority must be lower than the one of the Symfony route listener (defaults to 32)
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 20 }

contao.listener.request_token:
class: Contao\CoreBundle\EventListener\RequestTokenListener
arguments:
- "@contao.framework"
- "@contao.routing.scope_matcher"
- "@contao.csrf.token_manager"
- "%contao.csrf_token_name%"
tags:
# The priority must be lower than the one of the Symfony route listener (defaults to 32)
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 30 }

contao.listener.response_exception:
class: Contao\CoreBundle\EventListener\ResponseExceptionListener
tags:
Expand Down
Expand Up @@ -145,14 +145,14 @@ public function run()
$objTemplate->link = StringUtil::specialchars($url);
$objTemplate->info = $arrInfo;
$objTemplate->labels = $GLOBALS['TL_LANG']['CONFIRM'];
$objTemplate->explain = $GLOBALS['TL_LANG']['ERR']['invalidTokenUrl'];
$objTemplate->explain = $GLOBALS['TL_LANG']['MSC']['invalidTokenUrl'];
$objTemplate->cancel = $GLOBALS['TL_LANG']['MSC']['cancelBT'];
$objTemplate->continue = $GLOBALS['TL_LANG']['MSC']['continue'];
$objTemplate->theme = Backend::getTheme();
$objTemplate->base = Environment::get('base');
$objTemplate->language = $GLOBALS['TL_LANGUAGE'];
$objTemplate->h1 = $GLOBALS['TL_LANG']['MSC']['invalidTokenUrl'];
$objTemplate->title = StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['invalidTokenUrl']);
$objTemplate->h1 = $GLOBALS['TL_LANG']['MSC']['invalidToken'];
$objTemplate->title = StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['invalidToken']);
$objTemplate->host = Environment::get('host');
$objTemplate->charset = Config::get('characterSet');

Expand Down
Expand Up @@ -216,6 +216,9 @@ public function renderPage($pageModel)
}
}

// Inherit the settings from the parent pages
$objPage->loadDetails();

// Trigger the 404 page if the page is not published and the front end preview is not active (see #374)
if (!BE_USER_LOGGED_IN && !$objPage->isPublic)
{
Expand All @@ -230,12 +233,6 @@ public function renderPage($pageModel)
$objHandler->generate($objPage->id);
}

// Inherit the settings from the parent pages if it has not been done yet
if (!\is_bool($objPage->protected))
{
$objPage->loadDetails();
}

// Set the admin e-mail address
if ($objPage->adminEmail != '')
{
Expand Down

0 comments on commit b479b71

Please sign in to comment.