entity_legal-4.0.x-dev/src/Plugin/EntityLegal/Redirect.php
src/Plugin/EntityLegal/Redirect.php
<?php
declare(strict_types=1);
namespace Drupal\entity_legal\Plugin\EntityLegal;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Messenger\MessengerTrait;
use Drupal\Core\Routing\RedirectDestinationTrait;
use Drupal\Core\Routing\ResettableStackedRouteMatchInterface;
use Drupal\Core\Routing\TrustedRedirectResponse;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Drupal\entity_legal\EntityLegalDocumentInterface;
use Drupal\entity_legal\EntityLegalPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Method class for redirecting existing users to accept a legal document.
*
* @EntityLegal(
* id = "redirect",
* label = @Translation("Redirect every page load to legal document until accepted"),
* type = "existing_users",
* )
*/
class Redirect extends EntityLegalPluginBase {
use MessengerTrait;
use RedirectDestinationTrait;
use StringTranslationTrait;
/**
* Constructs a new plugin instance.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager service.
* @param \Drupal\Core\Session\AccountProxyInterface $currentUser
* The current user.
* @param \Drupal\Core\Routing\ResettableStackedRouteMatchInterface $routeMatch
* The current route match service.
* @param \Drupal\Core\TempStore\PrivateTempStoreFactory $privateTempStoreFactory
* The private temp store factory service.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
protected EntityTypeManagerInterface $entityTypeManager,
protected AccountProxyInterface $currentUser,
protected ResettableStackedRouteMatchInterface $routeMatch,
protected PrivateTempStoreFactory $privateTempStoreFactory,
) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $this->entityTypeManager, $this->currentUser);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('current_user'),
$container->get('current_route_match'),
$container->get('tempstore.private')
);
}
/**
* {@inheritdoc}
*/
public function execute(array &$context = []): void {
foreach ($this->getDocumentsForMethod() as $document) {
/** @var \Symfony\Component\HttpKernel\Event\ResponseEvent $event */
$event = $context['event'];
$request = $event->getRequest();
// The acceptance of a legal document is applicable only to humans.
if ($request->getRequestFormat() !== 'html') {
return;
}
// Don't redirect on POST requests.
if (!$request->isMethodSafe()) {
return;
}
if (!$routeName = $this->routeMatch->getRouteName()) {
// Unrouted?
return;
}
if ($this->isExcludedRoute($routeName, $document)) {
return;
}
// Do not redirect password reset.
if ($this->isPasswordReset($event->getRequest())) {
return;
}
if ($messages = $this->messenger()->all()) {
// Save any messages set for the destination page.
// @see \Drupal\entity_legal\Form\EntityLegalDocumentAcceptanceForm::submitForm()
$this->privateTempStoreFactory->get('entity_legal')->set('postponed_messages', $messages);
$this->messenger()->deleteAll();
}
$this->messenger()->addWarning($this->t('You must accept this agreement before continuing.'));
$entityUrl = $document->toUrl()
->setOption('query', $this->getDestinationArray())
->setAbsolute(TRUE)
->toString();
$event->setResponse(new TrustedRedirectResponse($entityUrl));
// Remove destination cause the RedirectResponseSubscriber redirects, and
// in some cases, it brings redirect loops.
$request->query->remove('destination');
$request->request->remove('destination');
}
}
/**
* Checks if the current route is excluded.
*
* @param string $routeName
* The route name.
* @param \Drupal\entity_legal\EntityLegalDocumentInterface $document
* The legal document entity.
*
* @return bool
* If the current route is excluded.
*/
protected function isExcludedRoute(string $routeName, EntityLegalDocumentInterface $document): bool {
$excludedRoutes = [
'system.csrftoken',
'user.logout',
'user.logout.confirm',
'system.js_asset',
'system.css_asset',
$document->toUrl()->getRouteName(),
];
return in_array($routeName, $excludedRoutes);
}
/**
* Check if this is a valid password reset request.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The HTTP request object.
*
* @return bool
* If this is a valid password reset request.
*/
protected function isPasswordReset(Request $request): bool {
// Unblock only the current user account edit form.
if ($this->routeMatch->getRouteName() !== 'entity.user.edit_form' && $this->routeMatch->getRawParameter('user') != $this->currentUser->id()) {
return FALSE;
}
// The password reset token should be present.
if (!$passResetToken = $request->get('pass-reset-token')) {
return FALSE;
}
// Now we check if it's a valid token.
// @see \Drupal\user\Controller\UserController::resetPassLogin()
// @see \Drupal\user\AccountForm::form()
$sessionKey = "pass_reset_{$this->currentUser->id()}";
if (!isset($_SESSION[$sessionKey]) || !hash_equals($_SESSION[$sessionKey], $passResetToken)) {
return FALSE;
}
return TRUE;
}
}
