sqrl-2.0.0-rc1/src/Nut.php

src/Nut.php
<?php

namespace Drupal\sqrl;

use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\sqrl\Exception\NutException;
use Drupal\user\Entity\User;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;

/**
 * Provides nut services.
 */
final class Nut implements ContainerInjectionInterface {

  use StringManipulation;
  use StringTranslationTrait;

  public const MODE_BUILD = 'build';
  public const MODE_FETCH = 'fetch';
  public const IS_COOKIE  = FALSE;

  public const STATUS_INITED  = 'inited';
  public const STATUS_INVALID = 'invalid';
  public const STATUS_BUILT   = 'built';
  public const STATUS_FETCHED = 'fetched';

  /**
   * The mode.
   *
   * @var string
   */
  private string $mode = self::MODE_BUILD;

  /**
   * The status.
   *
   * @var string
   */
  private string $status = self::STATUS_INITED;

  /**
   * The expiry status.
   *
   * @var bool
   */
  private bool $expired;

  /**
   * The public nut.
   *
   * @var string
   */
  protected string $nutPublic;

  /**
   * The client time.
   *
   * @var int
   */
  protected int $clientTime;

  /**
   * The client IP address.
   *
   * @var string
   */
  protected string $clientIP;

  /**
   * The client operation.
   *
   * @var string
   */
  protected string $clientOperation;

  /**
   * The client operation parameters.
   *
   * @var array
   */
  protected array $clientOperationParams = [];

  /**
   * The client messages.
   *
   * @var array
   */
  protected array $clientMessages = [];

  /**
   * The client UID.
   *
   * @var int
   */
  protected int $clientUid;

  /**
   * The client login token.
   *
   * @var string
   */
  protected string $clientLoginToken;

  /**
   * The client cancel token.
   *
   * @var string
   */
  protected string $clientCancelToken;

  /**
   * The time service.
   *
   * @var \Drupal\Component\Datetime\TimeInterface
   */
  protected TimeInterface $time;

  /**
   * The request.
   *
   * @var \Symfony\Component\HttpFoundation\Request
   */
  protected Request $request;

  /**
   * The state.
   *
   * @var \Drupal\sqrl\State
   */
  protected State $state;

  /**
   * The sqrl service.
   *
   * @var \Drupal\sqrl\Sqrl
   */
  protected Sqrl $sqrl;

  /**
   * The log channel.
   *
   * @var \Drupal\sqrl\Log
   */
  protected Log $log;

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected AccountInterface $currentUser;

  /**
   * The messenger service.
   *
   * @var \Drupal\Core\Messenger\MessengerInterface
   */
  protected MessengerInterface $messenger;

  /**
   * Constructs the nut service.
   *
   * @param \Drupal\Core\Session\AccountInterface $current_user
   *   The current user.
   * @param \Drupal\Component\Datetime\TimeInterface $time
   *   The time service.
   * @param \Symfony\Component\HttpFoundation\RequestStack $request
   *   The request stack.
   * @param \Drupal\sqrl\State $state
   *   The state.
   * @param \Drupal\sqrl\Sqrl $sqrl
   *   The sqrl service.
   * @param \Drupal\sqrl\Log $log
   *   The log channel.
   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
   *   The messenger service.
   */
  public function __construct(AccountInterface $current_user, TimeInterface $time, RequestStack $request, State $state, Sqrl $sqrl, Log $log, MessengerInterface $messenger) {
    $this->currentUser = $current_user;
    $this->time = $time;
    $this->request = $request->getCurrentRequest();
    $this->state = $state;
    $this->sqrl = $sqrl;
    $this->log = $log;
    $this->messenger = $messenger;
    $this->clientLoginToken = $this->base64Encode($this->randomBytes(8));
    $this->clientCancelToken = $this->base64Encode($this->randomBytes(8));
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container): Nut {
    return new Nut(
      $container->get('current_user'),
      $container->get('datetime.time'),
      $container->get('request_stack'),
      $container->get('sqrl.state'),
      $container->get('sqrl.handler'),
      $container->get('sqrl.log'),
      $container->get('messenger')
    );
  }

  /**
   * Sets the client operation.
   *
   * @param string $op
   *   The client operation.
   *
   * @return self
   *   This nut service.
   */
  public function setClientOperation(string $op): Nut {
    $this->clientOperation = $op;
    return $this;
  }

  /**
   * Gets the client operation.
   *
   * @return string
   *   The client operation.
   */
  public function getClientOperation(): string {
    return $this->clientOperation;
  }

  /**
   * Gets the client UID.
   *
   * @return int
   *   The client UID.
   */
  public function getClientUid(): int {
    return $this->clientUid;
  }

  /**
   * Gets the login token.
   *
   * @return string
   *   The login token.
   */
  public function getLoginToken(): string {
    return $this->clientLoginToken;
  }

  /**
   * Gets the cancel token.
   *
   * @return string
   *   The cancel token.
   */
  public function getCancelToken(): string {
    return $this->clientCancelToken;
  }

  /**
   * Gets the public nut.
   *
   * @return string
   *   The public nut.
   *
   * @throws \JsonException
   */
  public function getPublicNut(): string {
    $this->build();
    return $this->nutPublic;
  }

  /**
   * Determines if the nut is valid.
   *
   * @return bool
   *   TRUE, if the nut is valid, FALSE otherwise.
   */
  public function isValid(): bool {
    return ($this->status !== self::STATUS_INVALID);
  }

  /**
   * Determines if the nut is expired.
   *
   * @return bool
   *   TRUE, if the nut is expired, FALSE otherwise.
   */
  public function isExpired(): bool {
    return $this->expired ?? FALSE;
  }

  /**
   * Determines if the client IP is valid.
   *
   * @return bool
   *   TRUE, if the client IP is valid, FALSE otherwise.
   */
  public function isIpValid(): bool {
    return ($this->clientIP === $this->request->getClientIp());
  }

  /**
   * Gets all the parameters.
   *
   * @return array
   *   Key values pairs of all parameters.
   */
  private function getParams(): array {
    return [
      'time' => $this->time->getRequestTime(),
      'op' => $this->getClientOperation(),
      'ip' => $this->request->getClientIp(),
      'params' => $this->clientOperationParams,
      'messages to browser' => $this->clientMessages,
      'uid' => $this->currentUser->id(),
      'login token' => $this->clientLoginToken,
      'cancel token' => $this->clientCancelToken,
    ];
  }

  /**
   * Builds the nut.
   *
   * @throws \JsonException
   */
  private function build(): void {
    if ($this->mode !== self::MODE_BUILD || $this->status === self::STATUS_BUILT) {
      return;
    }
    $this->status = self::STATUS_BUILT;
    $this->nutPublic = $this->base64Encode($this->randomBytes(8));

    $this->state->setNut($this->nutPublic, $this->getParams());
  }

  /**
   * Gets the nut as a string.
   *
   * @return string
   *   The nut.
   *
   * @throws \JsonException
   */
  public function __toString(): string {
    return $this->getPublicNut();
  }

  /**
   * Receives and validates the nut.
   */
  public function fetch(): void {
    $this->mode = self::MODE_FETCH;
    if ($this->status === self::STATUS_FETCHED) {
      return;
    }
    try {
      $this->nutPublic = $this->fetchNut();
      $this->load();
      $this->validateExpiration();
      $this->status = self::STATUS_FETCHED;
    }
    catch (NutException | \JsonException $e) {
      // @todo Logging.
      $this->status = self::STATUS_INVALID;
      $this->log->debug('Fetch NUT error: ' . $e->getMessage());
    }
  }

  /**
   * Gets a list of possible accounts for a form select widget.
   *
   * @return array
   *   The list of possible accounts.
   *
   * @throws \JsonException
   */
  public function getAccountsForSelect(): array {
    $result = [];
    foreach ($this->state->getAuth($this->getPublicNut(), FALSE) as $uid) {
      /** @var \Drupal\user\UserInterface $user */
      $user = User::load($uid);
      if ($user->isActive()) {
        $result[$uid] = $user->label();
      }
    }
    return $result;
  }

  /**
   * Callback to poll for the public nut while the browser is waiting.
   *
   * @param string|null $token
   *   The token.
   *
   * @return \Drupal\Core\Url|null
   *   The redirect url, if the nut has been received and validated, NULL
   *   otherwise.
   *
   * @throws \JsonException
   */
  public function poll(?string $token = NULL): ?Url {
    $uids = $this->state->getAuth($this->getPublicNut());
    if (!empty($uids)) {
      $op = $this->getClientOperation();
      $route = 'user.page';

      switch ($op) {
        case 'login':
        case 'register':
          /** @var \Drupal\user\UserInterface[] $users */
          $users = [];
          foreach ($uids as $uid) {
            /** @var \Drupal\user\UserInterface $user */
            $user = User::load($uid);
            if ($user->isActive()) {
              $users[] = $user;
            }
          }
          $count = count($users);

          if ($count === 0) {
            // None of the user accounts linked to the SQRL ID is active.
            $this->messenger->addError($this->t('No active user account, you can not login with this SQRL identity.'));
            return NULL;
          }
          if ($count === 1) {
            // Exactly one active user account is linked to the SQRL ID, let
            // them log in.
            $account = reset($users);
            user_login_finalize($account);
          }
          else {
            // More than one active user accounts are linked to the SQRL ID,
            // the user needs to select which one to log into.
            $this->state->setAuth($this->getPublicNut(), $users);
            $this->sqrl->setNut($this);
            return $this->sqrl->getUrl('sqrl.ident.select', ['token' => $token]);
          }
          break;

        case 'link':
          // User linked their account to their SQRL identity.
          break;

        case 'unlink':
          // User unlinked their account from their SQRL identity.
          break;

        case 'profile':
          // More than one active user accounts are linked to the SQRL ID,
          // the user needs to select which one to log into.
          $this->state->setAuth($this->getPublicNut(), $this->getClientUid());
          $this->sqrl->setNut($this);
          return $this->sqrl->getUrl('sqrl.profile.edit', [
            'user' => $this->getClientUid(),
            'token' => $token,
          ]);

      }
      foreach ($this->state->getMessages($this->getPublicNut()) as $message) {
        $this->messenger->addMessage($message['message'], $message['type']);
      }

      return Url::fromRoute($route);
    }
    return NULL;
  }

  /**
   * Extract nut from the get request.
   *
   * @return string
   *   The nut.
   *
   * @throws \Drupal\sqrl\Exception\NutException
   */
  private function fetchNut(): string {
    $nut = $this->request->query->get('nut');
    if (!$nut) {
      throw new NutException('Nut missing from GET request');
    }
    return $nut;
  }

  /**
   * Validates the expiration time.
   */
  private function validateExpiration(): void {
    $this->expired = FALSE;
    if ($this->clientTime + State::EXPIRE_NUT < $this->time->getRequestTime()) {
      $this->expired = TRUE;
    }
  }

  /**
   * Extract all arguments from the nut.
   *
   * @throws \Drupal\sqrl\Exception\NutException
   * @throws \JsonException
   */
  private function load(): void {
    $params = $this->state->getNut($this->nutPublic);
    if (empty($params)) {
      throw new NutException('No params received from implementing framework');
    }
    if (empty($params['time'])) {
      throw new NutException('Wrong params received from implementing framework');
    }
    if (empty($params['op'])) {
      throw new NutException('Wrong params received from implementing framework');
    }
    if (empty($params['ip'])) {
      throw new NutException('Wrong params received from implementing framework');
    }
    if (!isset($params['params']) || !is_array($params['params'])) {
      throw new NutException('Wrong params received from implementing framework');
    }
    if (!isset($params['messages to browser']) || !is_array($params['messages to browser'])) {
      throw new NutException('Wrong params received from implementing framework');
    }
    if (!isset($params['uid'])) {
      throw new NutException('Wrong params received from implementing framework');
    }
    if (!isset($params['login token'])) {
      throw new NutException('Wrong params received from implementing framework');
    }
    if (!isset($params['cancel token'])) {
      throw new NutException('Wrong params received from implementing framework');
    }

    $this->clientTime = $params['time'];
    $this->clientOperation = $params['op'];
    $this->clientIP = $params['ip'];
    $this->clientOperationParams = $params['params'];
    $this->clientMessages = $params['messages to browser'];
    $this->clientUid = $params['uid'];
    $this->clientLoginToken = $params['login token'];
    $this->clientCancelToken = $params['cancel token'];
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc