BootSubscriber.php in Bakery Single Sign-On System 8.2
For Boot event subscribe.
Namespace
Drupal\bakery\EventSubscriberFile
src/EventSubscriber/BootSubscriber.phpView source
<?php
namespace Drupal\bakery\EventSubscriber;
/**
* @file
* For Boot event subscribe.
*/
use Drupal\bakery\BakeryService;
use Drupal\bakery\Cookies\ChocolateChip;
use Drupal\bakery\Exception\MissingKeyException;
use Drupal\bakery\Kitchen;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Logger\LoggerChannelTrait;
use Drupal\Core\Messenger\MessengerTrait;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* For handling chocolatechip cookie on boot.
*/
class BootSubscriber implements EventSubscriberInterface {
use LoggerChannelTrait;
use MessengerTrait;
use StringTranslationTrait;
/**
* @var \Drupal\bakery\BakeryService
*/
protected $bakeryService;
/**
* @var \Drupal\bakery\Kitchen
*/
protected $kitchen;
/**
* @var \Drupal\Core\Session\AccountProxyInterface
*/
protected $currentUser;
/**
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $userStorage;
protected $config;
/**
* Initialize bakeryService.
*
* @param \Drupal\bakery\BakeryService $bakeryService
* Bakery service used.
* @param \Drupal\bakery\Kitchen $kitchen
* Bakery kitchen service.
* @param \Drupal\Core\Session\AccountProxyInterface $currentUser
* The current user.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* Type manager for retrieving user storage.
*/
public function __construct(BakeryService $bakeryService, Kitchen $kitchen, AccountProxyInterface $currentUser, EntityTypeManagerInterface $entityTypeManager) {
$this->bakeryService = $bakeryService;
$this->kitchen = $kitchen;
$this->currentUser = $currentUser;
$this->userStorage = $entityTypeManager
->getStorage('user');
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
// Should be called on cached pages also.
return [
KernelEvents::REQUEST => [
'onEvent',
27,
],
];
}
/**
* On boot event we need to test the cookie.
*/
public function onEvent(GetResponseEvent $event) {
try {
$cookie = $this->kitchen
->taste(Kitchen::CHOCOLATE_CHIP);
} catch (MissingKeyException $e) {
// Continue below to clean up.
$cookie = FALSE;
}
// Continue if this is a valid cookie.
// That only happens for users who have a current valid session on the
// master site.
if ($cookie) {
// Detect SSO cookie mismatch if there is already a valid session and
// force logout.
if ($this->currentUser
->id() && $cookie['name'] !== $this->currentUser
->getAccountName()) {
user_logout();
$event
->setResponse(new RedirectResponse('/'));
return;
}
if ($this->bakeryService
->isMain()) {
if ($this->currentUser
->isAuthenticated()) {
// Bake a fresh cookie. Yum.
$this->kitchen
->bake(ChocolateChip::fromData($cookie));
}
else {
$this->kitchen
->eat(Kitchen::CHOCOLATE_CHIP);
}
}
elseif ($this->currentUser
->isAnonymous()) {
$this
->somethingAnonymous($event, $cookie);
}
}
else {
// Eat the bad cookie. Burp.
$this->kitchen
->eat(Kitchen::CHOCOLATE_CHIP);
// Log out users that have lost their SSO cookie, with the exception of
// UID 1 and any applied roles with permission to bypass.
if ($this->currentUser
->id() > 1 && !$this->currentUser
->hasPermission('bypass bakery')) {
$this
->getLogger('bakery')
->notice('Logging out the user with the bad cookie.', []);
user_logout();
// Maybe detect destinations and try to move them along?
$event
->setResponse(new RedirectResponse('/'));
}
}
}
private function somethingAnonymous(GetResponseEvent $event, array $cookie) {
// User is anonymous. If they do not have an account we'll create one by
// requesting their information from the master site. If they do have an
// account we may need to correct some disparant information.
/** @var \Drupal\user\UserInterface[] $account */
$account = $this->userStorage
->loadByProperties([
'name' => $cookie['name'],
'mail' => $cookie['mail'],
]);
$account = reset($account);
if ($this->bakeryService
->isChild()) {
// Fix out of sync users with valid init.
if (!$account && $cookie['master']) {
$account = $this
->repairInit($cookie);
}
// Create the account if it doesn't exist.
if (!$account && $cookie['master']) {
$account = $this
->bootstrapAccount($event, $cookie);
}
if ($account && $cookie['master'] && $account
->id() && $account
->get('init')->value != $cookie['init']) {
// User existed previously but init is wrong.
// Fix it to ensure account remains in sync.
// Make sure that there aren't any OTHER accounts with this init.
/** @var int $count */
$count = $this->userStorage
->getQuery()
->condition('init', $cookie['init'])
->count()
->execute();
if ($count == 0) {
$account
->set('init', $cookie['init'])
->save();
$this
->getLogger('bakery')
->notice('uid %uid out of sync. Changed init field from %oldinit to %newinit', [
'%oldinit' => $account
->getInitialEmail(),
'%newinit' => $cookie['init'],
'%uid' => $account
->id(),
]);
}
else {
// Username and email matched,
// but init belonged to a DIFFERENT account.
// Something got seriously tangled up.
$this
->getLogger('bakery')
->notice('Accounts mixed up! Username %user and init %init disagree with each other!', [
'%user' => $account
->getAccountName(),
'%init' => $cookie['init'],
]);
}
}
}
if ($account) {
// If the login attempt fails we need to destroy the cookie to prevent
// infinite redirects (with infinite failed login messages).
$login = $this->bakeryService
->userExternalLogin($account);
if ($login) {
// If an anonymous user has just been logged in, trigger a 'refresh'
// of the current page.
// TODO take into account destination query.
$event
->setResponse(new RedirectResponse(\Drupal::service('path.current')
->getPath()));
}
else {
$this->kitchen
->eat(Kitchen::CHOCOLATE_CHIP);
}
}
}
private function repairInit($cookie) {
/** @var int $count */
$count = $this->userStorage
->getQuery()
->condition('init', $cookie['init'])
->count()
->execute();
if ($count > 1) {
// Uh oh.
$this
->getLogger('bakery')
->notice('Account uniqueness problem: Multiple users found with init %init.', [
'%init' => $cookie['init'],
]);
$this
->messenger()
->addError($this
->t('Account uniqueness problem detected. <a href="@contact">Please contact the site administrator.</a>', [
'@contact' => $this
->getConfig()
->get('bakery_master') . 'contact',
]));
}
if ($count == 1) {
/** @var \Drupal\user\UserInterface[] $account */
$account = $this->userStorage
->loadByProperties([
'init' => $cookie['init'],
]);
if (is_array($account)) {
$account = reset($account);
}
if ($account) {
$this
->getLogger('bakery')
->notice('Fixing out of sync uid %uid. Changed name %name_old to %name_new, mail %mail_old to %mail_new.', [
'%uid' => $account
->id(),
'%name_old' => $account
->getAccountName(),
'%name_new' => $cookie['name'],
'%mail_old' => $account
->getEmail(),
'%mail_new' => $cookie['mail'],
]);
$account
->setEmail($cookie['mail']);
$account
->setUsername($cookie['name']);
$account
->save();
// Reload.
/** @var \Drupal\user\UserInterface[] $account */
$account = $this->userStorage
->loadByProperties([
'name' => $cookie['name'],
'mail' => $cookie['mail'],
]);
return reset($account);
}
}
return FALSE;
}
private function bootstrapAccount(GetResponseEvent $event, array $cookie) {
$checks = TRUE;
/** @var int $mail_count */
$mail_count = $this->userStorage
->getQuery()
->condition('uid', 0, '!=')
->condition('mail', '', '!=')
->condition('mail', $cookie['mail'], 'LIKE')
->count()
->execute();
if ($mail_count > 0) {
$checks = FALSE;
}
/** @var int $name_count */
$name_count = $this->userStorage
->getQuery()
->condition('uid', 0, '!=')
->condition('name', $cookie['name'], 'LIKE')
->count()
->execute();
if ($name_count > 0) {
$checks = FALSE;
}
/** @var int $init_count */
$init_count = $this->userStorage
->getQuery()
->condition('uid', 0, '!=')
->condition('init', $cookie['init'], '=')
->condition('name', $cookie['name'], 'LIKE')
->count()
->execute();
if ($init_count > 0) {
$checks = FALSE;
}
if ($checks) {
// Request information from master to keep data in sync.
$uid = $this->bakeryService
->requestAccount($cookie['name']);
// In case the account creation failed we want to make sure the user
// gets their bad cookie destroyed by not returning too early.
if ($uid) {
return $this->userStorage
->load($uid);
}
else {
$this->kitchen
->eat(Kitchen::CHOCOLATE_CHIP);
}
}
else {
$this
->messenger()
->addStatus(t('Your user account on %site appears to have problems. Would you like to try to <a href=":url">repair it yourself</a>?', [
'%site' => \Drupal::config('system.site')
->get('name'),
':url' => Url::fromRoute('bakery.repair')
->toString(),
]));
$this
->messenger()
->addStatus(Xss::filter($this
->getConfig()
->get('bakery_help_text')));
$event
->getRequest()
->getSession()
->set('BAKERY_CRUMBLED', TRUE);
}
return FALSE;
}
public function getConfig() {
if (!isset($this->config)) {
$this->config = \Drupal::config('bakery.settings');
}
return $this->config;
}
}
Classes
Name | Description |
---|---|
BootSubscriber | For handling chocolatechip cookie on boot. |