You are here

BootSubscriber.php in Bakery Single Sign-On System 8.2

For Boot event subscribe.

File

src/EventSubscriber/BootSubscriber.php
View 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

Namesort descending Description
BootSubscriber For handling chocolatechip cookie on boot.