You are here

SocialAuthUserManager.php in Social Auth 8

Namespace

Drupal\social_auth

File

src/SocialAuthUserManager.php
View source
<?php

namespace Drupal\social_auth;

use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Drupal\Core\Url;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\Routing\UrlGeneratorTrait;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Transliteration\PhpTransliteration;
use Drupal\Core\Utility\Token;
use Drupal\social_auth\Event\SocialAuthEvents;
use Drupal\social_auth\Event\SocialAuthUserEvent;
use Drupal\social_auth\Event\SocialAuthUserFieldsEvent;
use Drupal\user\Entity\User;
use Drupal\user\UserInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;

/**
 * Contains all logic that is related to Drupal user management.
 */
class SocialAuthUserManager {
  use UrlGeneratorTrait;
  use StringTranslationTrait;
  protected $configFactory;
  protected $loggerFactory;
  protected $eventDispatcher;
  protected $entityTypeManager;
  protected $entityFieldManager;
  protected $token;
  protected $transliteration;
  protected $languageManager;
  protected $routeProvider;

  /**
   * The implementer plugin id.
   *
   * @var string
   */
  protected $pluginId;

  /**
   * The session manager.
   *
   * @var \Symfony\Component\HttpFoundation\Session\SessionInterface
   */
  protected $session;

  /**
   * Session keys to nullify is user could not be logged in.
   *
   * @var array
   */
  protected $sessionKeys;

  /**
   * Constructor.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   Used for accessing Drupal configuration.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
   *   Used for logging errors.
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
   *   Used for dispatching social auth events.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   Used for loading and creating Drupal user objects.
   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
   *   Used for access Drupal user field definitions.
   * @param \Drupal\Core\Utility\Token $token
   *   Used for token support in Drupal user picture directory.
   * @param \Drupal\Core\Transliteration\PhpTransliteration $transliteration
   *   Used for user picture directory and file transliteration.
   * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
   *   Used to check if route path exists.
   * @param SessionInterface $session
   *   Used for reading data from and writing data to session.
   */
  public function __construct(ConfigFactoryInterface $config_factory, LoggerChannelFactoryInterface $logger_factory, EventDispatcherInterface $event_dispatcher, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, Token $token, PhpTransliteration $transliteration, LanguageManagerInterface $language_manager, RouteProviderInterface $route_provider, SessionInterface $session) {
    $this->configFactory = $config_factory;
    $this->loggerFactory = $logger_factory;
    $this->eventDispatcher = $event_dispatcher;
    $this->entityTypeManager = $entity_type_manager;
    $this->entityFieldManager = $entity_field_manager;
    $this->token = $token;
    $this->transliteration = $transliteration;
    $this->languageManager = $language_manager;
    $this->routeProvider = $route_provider;
    $this->session = $session;

    // Sets default plugin id.
    $this
      ->setPluginId('social_auth');
  }

  /**
   * Sets the implementer plugin id.
   *
   * This value is used to generate customized logs, drupal messages, and event
   * dispatchers.
   *
   * @param string $plugin_id
   *   The plugin id.
   */
  public function setPluginId($plugin_id) {
    $this->pluginId = $plugin_id;
  }

  /**
   * Gets the implementer plugin id.
   *
   * This value is used to generate customized logs, drupal messages, and events
   * dispatchers.
   *
   * @return string
   *   The plugin id.
   */
  public function getPluginId() {
    return $this->pluginId;
  }

  /**
   * Sets the session keys to nullify if user could not logged in.
   *
   * @param array $session_keys
   *   The session keys to nullify.
   */
  public function setSessionKeysToNullify(array $session_keys) {
    $this->sessionKeys = $session_keys;
  }

  /**
   * Creates and/or authenticates an user.
   *
   * @param string $email
   *   The user's email address.
   * @param string $name
   *   The user's name.
   * @param string $id
   *   The user's id in provider.
   * @param string|bool $picture_url
   *   The user's picture.
   *
   * @return \Symfony\Component\HttpFoundation\RedirectResponse
   *   A redirect response.
   */
  public function authenticateUser($email, $name, $id = NULL, $picture_url = FALSE) {

    // Tries to load the user by their email.
    $drupal_user = $this
      ->loadUserByProperty('mail', $email);

    // If user email has already an account in the site.
    if ($drupal_user) {

      // Authenticates and redirect existing user.
      return $this
        ->authenticateExistingUser($drupal_user);
    }
    $drupal_user = $this
      ->createUser($name, $email);

    // If the new user could be registered.
    if ($drupal_user) {

      // Download profile picture for the newly created user.
      if ($picture_url) {
        $this
          ->setProfilePic($drupal_user, $picture_url, $id);
      }

      // Authenticates and redirect new user.
      return $this
        ->authenticateNewUser($drupal_user);
    }
    drupal_set_message($this
      ->t('You could not be authenticated, please contact the administrator'), 'error');
    $this
      ->nullifySessionKeys();
    return $this
      ->redirect('user.login');
  }

  /**
   * Authenticates and redirects existing users in authentication process.
   *
   * @param \Drupal\user\UserInterface $drupal_user
   *   User object to authenticate.
   *
   * @return \Symfony\Component\HttpFoundation\RedirectResponse
   *   A redirect response.
   */
  public function authenticateExistingUser(UserInterface $drupal_user) {

    // If Admin (user 1) can not authenticate.
    if ($this
      ->isAdminDisabled($drupal_user)) {
      $this
        ->nullifySessionKeys();
      drupal_set_message($this
        ->t('Authentication for Admin (user 1) is disabled.'), 'error');
      return $this
        ->redirect('user.login');
    }

    // If user can not login because of their role.
    $disabled_role = $this
      ->isUserRoleDisabled($drupal_user);
    if ($disabled_role) {
      drupal_set_message($this
        ->t("Authentication for '@role' role is disabled.", array(
        '@role' => $disabled_role,
      )), 'error');
      return $this
        ->redirect('user.login');
    }

    // If user could be logged in.
    if ($this
      ->loginUser($drupal_user)) {
      return $this
        ->getLoginPostPath();
    }
    else {
      $this
        ->nullifySessionKeys();
      drupal_set_message($this
        ->t("Your account has not been approved yet or might have been canceled, please contact the administrator"), 'error');
      return $this
        ->redirect('user.login');
    }
  }

  /**
   * Authenticates and redirects new users in authentication process.
   *
   * @param \Drupal\user\UserInterface $drupal_user
   *   User object to login.
   *
   * @return \Symfony\Component\HttpFoundation\RedirectResponse
   *   A redirect response.
   */
  public function authenticateNewUser(UserInterface $drupal_user) {

    // If the account needs admin approval.
    if ($this
      ->isApprovalRequired()) {
      drupal_set_message($this
        ->t("Your account was created, but it needs administrator's approval"), 'warning');
      $this
        ->nullifySessionKeys();
      return $this
        ->redirect('user.login');
    }

    // If the new user could be logged in.
    if ($this
      ->loginUser($drupal_user)) {

      // User form redirection or false if option is not enabled.
      $redirect = $this
        ->redirectToUserForm($drupal_user);
      if ($redirect) {
        return $redirect;
      }
      else {
        return $this
          ->getLoginPostPath();
      }
    }
  }

  /**
   * Loads existing Drupal user object by given property and value.
   *
   * Note that first matching user is returned. Email address and account name
   * are unique so there can be only zero or one matching user when
   * loading users by these properties.
   *
   * @param string $field
   *   User entity field to search from.
   * @param string $value
   *   Value to search for.
   *
   * @return \Drupal\user\Entity\User|false
   *   Drupal user account if found
   *   False otherwise
   */
  public function loadUserByProperty($field, $value) {
    $users = $this->entityTypeManager
      ->getStorage('user')
      ->loadByProperties(array(
      $field => $value,
    ));
    if (!empty($users)) {
      return current($users);
    }

    // If user was not found, return FALSE.
    return FALSE;
  }

  /**
   * Create a new user account.
   *
   * @param string $name
   *   User's name on Provider.
   * @param string $email
   *   User's email address.
   *
   * @return \Drupal\user\Entity\User|false
   *   Drupal user account if user was created
   *   False otherwise
   */
  public function createUser($name, $email) {

    // Make sure we have everything we need.
    if (!$name || !$email) {
      $this->loggerFactory
        ->get($this
        ->getPluginId())
        ->error('Failed to create user. Name: @name, email: @email', array(
        '@name' => $name,
        '@email' => $email,
      ));
      return FALSE;
    }

    // Check if site configuration allows new users to register.
    if ($this
      ->registrationBlocked()) {
      $this->loggerFactory
        ->get($this
        ->getPluginId())
        ->warning('Failed to create user. User registration is disabled in Drupal account settings. Name: @name, email: @email.', array(
        '@name' => $name,
        '@email' => $email,
      ));
      drupal_set_message($this
        ->t('User registration is disabled, please contact the administrator.'), 'error');
      return FALSE;
    }

    // Get the current UI language.
    $langcode = $this->languageManager
      ->getCurrentLanguage()
      ->getId();

    // Initializes the user fields.
    $fields = $this
      ->getUserFields($name, $email, $langcode);

    // Create new user account.

    /** @var \Drupal\user\Entity\User $new_user */
    $new_user = $this->entityTypeManager
      ->getStorage('user')
      ->create($fields);

    // Try to save the new user account.
    try {
      $new_user
        ->save();
      $this->loggerFactory
        ->get($this
        ->getPluginId())
        ->notice('New user created. Username @username, UID: @uid', [
        '@username' => $new_user
          ->getAccountName(),
        '@uid' => $new_user
          ->id(),
      ]);

      // Dipatches SocialAuthEvents::USER_CREATED event.
      $event = new SocialAuthUserEvent($new_user, $this
        ->getPluginId());
      $this->eventDispatcher
        ->dispatch(SocialAuthEvents::USER_CREATED, $event);
      return $new_user;
    } catch (EntityStorageException $ex) {
      $this->loggerFactory
        ->get($this
        ->getPluginId())
        ->error('Could not create new user. Exception: @message', [
        '@message' => $ex
          ->getMessage(),
      ]);
    }
    return FALSE;
  }

  /**
   * Logs the user in.
   *
   * @param \Drupal\user\Entity\User $drupal_user
   *   User object.
   *
   * @return bool
   *   True if login was successful
   *   False if the login was blocked
   */
  public function loginUser(User $drupal_user) {

    // Check that the account is active and log the user in.
    if ($drupal_user
      ->isActive()) {
      $this
        ->userLoginFinalize($drupal_user);

      // Dipatches SocialAuthEvents::USER_LOGIN event.
      $event = new SocialAuthUserEvent($drupal_user, $this
        ->getPluginId());
      $this->eventDispatcher
        ->dispatch(SocialAuthEvents::USER_LOGIN, $event);
      return TRUE;
    }
    $this->loggerFactory
      ->get($this
      ->getPluginId())
      ->warning('Login for user @user prevented. Account is blocked.', array(
      '@user' => $drupal_user
        ->getAccountName(),
    ));
    return FALSE;
  }

  /**
   * Nullifies session keys if user could not logged in.
   */
  protected function nullifySessionKeys() {
    if (!empty($this->sessionKeys)) {
      array_walk($this->sessionKeys, function ($session_key) {
        $this->session
          ->set($session_key, NULL);
      });
    }
  }

  /**
   * Checks if user registration is blocked in Drupal account settings.
   *
   * @return bool
   *   True if registration is blocked
   *   False if registration is not blocked
   */
  protected function registrationBlocked() {

    // Check if Drupal account registration settings is Administrators only.
    if ($this->configFactory
      ->get('user.settings')
      ->get('register') == 'admin_only') {
      return TRUE;
    }
    return FALSE;
  }

  /**
   * Checks if admin approval is required for new users.
   *
   * @return bool
   *   True if approval is required
   *   False if approval is not required
   */
  protected function isApprovalRequired() {
    if ($this->configFactory
      ->get('user.settings')
      ->get('register') == 'visitors_admin_approval') {
      return TRUE;
    }
    return FALSE;
  }

  /**
   * Checks if Admin (user 1) can login.
   *
   * @param \Drupal\user\UserInterface $drupal_user
   *   User object to check if user is admin.
   *
   * @return bool
   *   True if user 1 can't login.
   *   False otherwise
   */
  protected function isAdminDisabled(UserInterface $drupal_user) {
    if ($this->configFactory
      ->get('social_auth.settings')
      ->get('disable_admin_login') && $drupal_user
      ->id() == 1) {
      return TRUE;
    }
    return FALSE;
  }

  /**
   * Checks if User with specific roles is allowed to login.
   *
   * @param \Drupal\user\UserInterface $drupal_user
   *   User object to check if user has a specific role.
   *
   * @return string|false
   *   The role that can't login.
   *   False if the user roles are not disabled.
   */
  protected function isUserRoleDisabled(UserInterface $drupal_user) {
    foreach ($this->configFactory
      ->get('social_auth.settings')
      ->get('disabled_roles') as $role) {
      if ($drupal_user
        ->hasRole($role)) {
        return $role;
      }
    }
    return FALSE;
  }

  /**
   * Returns the Post Login Path.
   *
   * @return string
   *   Post Login Path to which the user would be redirected after login.
   */
  protected function getLoginPostPath() {
    $post_login = $this->configFactory
      ->get('social_auth.settings')
      ->get('post_login');
    $routes = $this->routeProvider
      ->getRoutesByNames(array(
      $post_login,
    ));
    if (empty($routes)) {

      // Route does not exist so just redirect to path.
      return new RedirectResponse(Url::fromUserInput($post_login)
        ->toString());
    }
    else {
      return $this
        ->redirect($post_login);
    }
  }

  /**
   * Checks if User should be redirected to User Form after creation.
   *
   * @param \Drupal\user\UserInterface $drupal_user
   *   User object to get the id of user.
   *
   * @return \Symfony\Component\HttpFoundation\RedirectResponse|false
   *   A redirect response to user form, if option is enabled.
   *   False otherwise
   */
  protected function redirectToUserForm(UserInterface $drupal_user) {
    if ($this->configFactory
      ->get('social_auth.settings')
      ->get('redirect_user_form')) {
      return $this
        ->redirect('entity.user.edit_form', [
        'user' => $drupal_user
          ->id(),
      ]);
    }
    return FALSE;
  }

  /**
   * Ensures that Drupal usernames will be unique.
   *
   * Drupal usernames will be generated so that the user's full name on Provider
   * will become user's Drupal username. This method will check if the username
   * is already used and appends a number until it finds the first available
   * username.
   *
   * @param string $name
   *   User's full name on provider.
   *
   * @return string
   *   Unique drupal username.
   */
  protected function generateUniqueUsername($name) {
    $max_length = 60;
    $name = Unicode::substr($name, 0, $max_length);

    // Add a trailing number if needed to make username unique.
    $base = $name;
    $i = 1;
    $candidate = $base;
    while ($this
      ->loadUserByProperty('name', $candidate)) {

      // Calculate max length for $base and truncate if needed.
      $max_length_base = $max_length - strlen((string) $i) - 1;
      $base = Unicode::substr($base, 0, $max_length_base);
      $candidate = $base . ' ' . $i;
      $i++;
    }

    // Trim leading and trailing whitespace.
    $candidate = trim($candidate);
    return $candidate;
  }

  /**
   * Returns the status for new users.
   *
   * @return int
   *   Value 0 means that new accounts remain blocked and require approval.
   *   Value 1 means that visitors can register new accounts without approval.
   */
  protected function getNewUserStatus() {
    if ($this->configFactory
      ->get('user.settings')
      ->get('register') == 'visitors') {
      return 1;
    }
    return 0;
  }

  /**
   * Downloads and sets user profile picture.
   *
   * @param User $drupal_user
   *   User object to update the profile picture for.
   * @param string $picture_url
   *   Absolute URL where the picture will be downloaded from.
   * @param string $id
   *   User's ID.
   *
   * @return bool
   *   True if picture was successfully set.
   *   False otherwise.
   */
  public function setProfilePic(User $drupal_user, $picture_url, $id) {

    // Try to download the profile picture and add it to user fields.
    if ($this
      ->userPictureEnabled()) {
      if ($file = $this
        ->downloadProfilePic($picture_url, $id)) {
        $drupal_user
          ->set('user_picture', $file
          ->id());
        $drupal_user
          ->save();
        return TRUE;
      }
    }
    return FALSE;
  }

  /**
   * Downloads the profile picture to Drupal filesystem.
   *
   * @param string $picture_url
   *   Absolute URL where to download the profile picture.
   * @param string $id
   *   Social network ID of the user.
   *
   * @return \Drupal\file\FileInterface|false
   *   FileInterface object if file was succesfully downloaded
   *   False otherwise
   */
  protected function downloadProfilePic($picture_url, $id) {

    // Make sure that we have everything we need.
    if (!$picture_url || !$id) {
      return FALSE;
    }

    // Determine target directory.
    $scheme = $this->configFactory
      ->get('system.file')
      ->get('default_scheme');
    $file_directory = $this
      ->getPictureDirectory();
    if (!$file_directory) {
      return FALSE;
    }
    $directory = $scheme . '://' . $file_directory;

    // Replace tokens.
    $directory = $this->token
      ->replace($directory);

    // Transliterate directory name.
    $directory = $this->transliteration
      ->transliterate($directory, 'en', '_', 50);
    if (!$this
      ->filePrepareDirectory($directory, 1)) {
      $this->loggerFactory
        ->get($this
        ->getPluginId())
        ->error('Could not save @plugin_id\'s provider profile picture. Directory is not writable: @directory', [
        '@directory' => $directory,
        '@provider' => $this
          ->getPluginId(),
      ]);
      return FALSE;
    }

    // Generate filename and transliterate.
    $filename = $this->transliteration
      ->transliterate($this
      ->getPluginId() . '_' . $id . '.jpg', 'en', '_', 50);
    $destination = $directory . DIRECTORY_SEPARATOR . $filename;

    // Download the picture to local filesystem.
    if (!($file = $this
      ->systemRetrieveFile($picture_url, $destination, TRUE, 1))) {
      $this->loggerFactory
        ->get($this
        ->getPluginId())
        ->error('Could not download @plugin_id\'s provider profile picture from url: @url', [
        '@url' => $picture_url,
        '@plugin_id' => $this
          ->getPluginId(),
      ]);
      return FALSE;
    }
    return $file;
  }

  /**
   * Returns whether this site supports the default user picture feature.
   *
   * We use this method instead of the procedural user_pictures_enabled()
   * so that we can unit test our own methods.
   *
   * @return bool
   *   True if user pictures are enabled
   *   False otherwise
   */
  protected function userPictureEnabled() {
    $field_definitions = $this->entityFieldManager
      ->getFieldDefinitions('user', 'user');
    return isset($field_definitions['user_picture']);
  }

  /**
   * Returns picture directory if site supports the user picture feature.
   *
   * @return string|bool
   *   Directory for user pictures if site supports user picture feature.
   *   False otherwise.
   */
  protected function getPictureDirectory() {
    $field_definitions = $this->entityFieldManager
      ->getFieldDefinitions('user', 'user');
    if (isset($field_definitions['user_picture'])) {
      return $field_definitions['user_picture']
        ->getSetting('file_directory');
    }
    return FALSE;
  }

  /**
   * Wrapper for file_prepare_directory.
   *
   * We need to wrap the legacy procedural Drupal API functions so that we are
   * not using them directly in our own methods. This way we can unit test our
   * own methods.
   *
   * @see file_prepare_directory
   */
  protected function filePrepareDirectory(&$directory, $options) {
    return file_prepare_directory($directory, $options);
  }

  /**
   * Wrapper for system_retrieve_file.
   *
   * We need to wrap the legacy procedural Drupal API functions so that we are
   * not using them directly in our own methods. This way we can unit test our
   * own methods.
   *
   * @see system_retrieve_file
   */
  protected function systemRetrieveFile($url, $destination, $managed, $replace) {
    return system_retrieve_file($url, $destination, $managed, $replace);
  }

  /**
   * Wrapper for user_password.
   *
   * We need to wrap the legacy procedural Drupal API functions so that we are
   * not using them directly in our own methods. This way we can unit test our
   * own methods.
   *
   * @param int $length
   *   Length of the password.
   *
   * @return string
   *   The password.
   *
   * @see user_password
   */
  protected function userPassword($length) {
    return user_password($length);
  }

  /**
   * Wrapper for user_login_finalize.
   *
   * We need to wrap the legacy procedural Drupal API functions so that we are
   * not using them directly in our own methods. This way we can unit test our
   * own methods.
   *
   * @see user_password
   */
  protected function userLoginFinalize(UserInterface $account) {
    user_login_finalize($account);
  }

  /**
   * Returns an array of fields to initialize the creation of the user.
   *
   * @param string $name
   *   User's name on Provider.
   * @param string $email
   *   User's email address.
   * @param string $langcode
   *   The current UI language.
   *
   * @return array
   *   Fields to initialize for the user creation.
   */
  protected function getUserFields($name, $email, $langcode) {

    // - Password can be very long since the user doesn't see this.
    $fields = [
      'name' => $this
        ->generateUniqueUsername($name),
      'mail' => $email,
      'init' => $email,
      'pass' => $this
        ->userPassword(32),
      'status' => $this
        ->getNewUserStatus(),
      'langcode' => $langcode,
      'preferred_langcode' => $langcode,
      'preferred_admin_langcode' => $langcode,
    ];

    // Dispatches SocialAuthEvents::USER_FIELDS, so that other modules can
    // update this array before an user is saved.
    $event = new SocialAuthUserFieldsEvent($fields, $this
      ->getPluginId());
    $this->eventDispatcher
      ->dispatch(SocialAuthEvents::USER_FIELDS, $event);
    $fields = $event
      ->getUserFields();
    return $fields;
  }

}

Classes

Namesort descending Description
SocialAuthUserManager Contains all logic that is related to Drupal user management.