You are here

class DrupalUserProcessor in Lightweight Directory Access Protocol (LDAP) 8.3

Same name and namespace in other branches
  1. 8.4 ldap_user/src/Processor/DrupalUserProcessor.php \Drupal\ldap_user\Processor\DrupalUserProcessor

Handles processing of a user from LDAP to Drupal.

Hierarchy

Expanded class hierarchy of DrupalUserProcessor

4 files declare their use of DrupalUserProcessor
IntegrationTests.php in ldap_user/tests/src/Kernel/IntegrationTests.php
LdapUserTestForm.php in ldap_user/src/Form/LdapUserTestForm.php
ldap_user.module in ldap_user/ldap_user.module
Module for the LDAP User Entity.
LoginValidator.php in ldap_authentication/src/Controller/LoginValidator.php

File

ldap_user/src/Processor/DrupalUserProcessor.php, line 21

Namespace

Drupal\ldap_user\Processor
View source
class DrupalUserProcessor implements LdapUserAttributesInterface {
  private $config;

  /**
   * The Drupal user account.
   *
   * @var \Drupal\user\Entity\User
   */
  private $account;

  /**
   * The server interacting with.
   *
   * @var \Drupal\ldap_servers\Entity\Server
   */
  private $server;

  /**
   * LDAP server factory.
   *
   * @var \Drupal\ldap_servers\ServerFactory
   */
  private $factory;

  /**
   * LDAP Details logger.
   *
   * @var \Drupal\ldap_servers\Logger\LdapDetailLog
   */
  private $detailLog;

  /**
   * Token processor.
   *
   * @var \Drupal\ldap_servers\Processor\TokenProcessor
   */
  protected $tokenProcessor;

  /**
   * Constructor.
   */
  public function __construct() {
    $this->config = \Drupal::config('ldap_user.settings');
    $this->factory = \Drupal::service('ldap.servers');
    $this->detailLog = \Drupal::service('ldap.detail_log');
    $this->tokenProcessor = \Drupal::service('ldap.token_processor');
  }

  /**
   * Get the user account.
   *
   * @return \Drupal\user\Entity\User
   *   User account.
   */
  public function getUserAccount() {
    return $this->account;
  }

  /**
   * Set LDAP associations of a Drupal account by altering user fields.
   *
   * @param string $drupalUsername
   *   The Drupal username.
   *
   * @return bool
   *   Returns FALSE on invalid user or LDAP accounts.
   */
  public function ldapAssociateDrupalAccount($drupalUsername) {
    if ($this->config
      ->get('drupalAcctProvisionServer')) {
      $ldapServer = $this->factory
        ->getServerByIdEnabled($this->config
        ->get('drupalAcctProvisionServer'));
      $this->account = user_load_by_name($drupalUsername);
      if (!$this->account) {
        \Drupal::logger('ldap_user')
          ->error('Failed to LDAP associate Drupal account %drupal_username because account not found', [
          '%drupal_username' => $drupalUsername,
        ]);
        return FALSE;
      }
      $ldap_user = $ldapServer
        ->matchUsernameToExistingLdapEntry($drupalUsername);
      if (!$ldap_user) {
        \Drupal::logger('ldap_user')
          ->error('Failed to LDAP associate Drupal account %drupal_username because corresponding LDAP entry not found', [
          '%drupal_username' => $drupalUsername,
        ]);
        return FALSE;
      }
      $persistentUid = $ldapServer
        ->userPuidFromLdapEntry($ldap_user['attr']);
      if ($persistentUid) {
        $this->account
          ->set('ldap_user_puid', $persistentUid);
      }
      $this->account
        ->set('ldap_user_puid_property', $ldapServer
        ->get('unique_persistent_attr'));
      $this->account
        ->set('ldap_user_puid_sid', $ldapServer
        ->id());
      $this->account
        ->set('ldap_user_current_dn', $ldap_user['dn']);
      $this->account
        ->set('ldap_user_last_checked', time());
      $this->account
        ->set('ldap_user_ldap_exclude', 0);
      $this
        ->saveAccount();
      $this
        ->syncToDrupalAccount(self::EVENT_CREATE_DRUPAL_USER, $ldap_user);
      return TRUE;
    }
    else {
      return FALSE;
    }
  }

  /**
   * Provision a Drupal user account.
   *
   * Given user data, create a user and apply LDAP attributes or assign to
   * correct user if name has changed through PUID.
   *
   * @param array $userData
   *   A keyed array normally containing 'name' and optionally more.
   *
   * @return bool|\Drupal\user\Entity\User
   *   Return the user on success or FALSE on any problem.
   */
  public function provisionDrupalAccount(array $userData) {
    $this->account = User::create($userData);
    $ldapUser = FALSE;

    // Get an LDAP user from the LDAP server.
    if ($this->config
      ->get('drupalAcctProvisionServer')) {
      $ldapUser = $this->factory
        ->getUserDataFromServerByIdentifier($userData['name'], $this->config
        ->get('drupalAcctProvisionServer'));
    }

    // Still no LDAP user.
    if (!$ldapUser) {
      \Drupal::service('ldap.detail_log')
        ->log('@username: Failed to find associated LDAP entry for username in provision.', [
        '@username' => $userData['name'],
      ], 'ldap-user');
      return FALSE;
    }
    $this->server = $this->factory
      ->getServerByIdEnabled($this->config
      ->get('drupalAcctProvisionServer'));

    // If we don't have an account name already we should set one.
    if (!$this->account
      ->getAccountName()) {
      $this->account
        ->set('name', $ldapUser[$this->server
        ->get('user_attr')]);
    }

    // Can we get details from an LDAP server?
    $params = [
      'account' => $this->account,
      'user_values' => $userData,
      'prov_event' => self::EVENT_CREATE_DRUPAL_USER,
      'module' => 'ldap_user',
      'function' => 'provisionDrupalAccount',
      'direction' => self::PROVISION_TO_DRUPAL,
    ];
    \Drupal::moduleHandler()
      ->alter('ldap_entry', $ldapUser, $params);

    // Look for existing Drupal account with the same PUID. If found, update
    // that user instead of creating a new user.
    $persistentUid = $this->server
      ->userPuidFromLdapEntry($ldapUser['attr']);
    $accountFromPuid = $persistentUid ? $this->server
      ->userAccountFromPuid($persistentUid) : FALSE;
    if ($accountFromPuid) {
      $this
        ->updateExistingAccountByPersistentUid($ldapUser, $accountFromPuid);
    }
    else {
      if (!$this
        ->createDrupalUser($ldapUser)) {
        return FALSE;
      }
    }
    return TRUE;
  }

  /**
   * Set flag to exclude user from LDAP association.
   *
   * @param string $drupalUsername
   *   The account username.
   *
   * @return bool
   *   TRUE on success, FALSE on error or failure because of invalid user.
   */
  public function ldapExcludeDrupalAccount($drupalUsername) {
    $account = User::load($drupalUsername);
    if (!$account) {
      \Drupal::logger('ldap_user')
        ->error('Failed to exclude user from LDAP association because Drupal account %username was not found', [
        '%username' => $drupalUsername,
      ]);
      return FALSE;
    }
    $account
      ->set('ldap_user_ldap_exclude', 1);
    $account
      ->save();
    return (bool) $account;
  }

  /**
   * Alter the user's attributes.
   *
   * @param array $attributes
   *   Attributes to change.
   * @param array $params
   *   Parameters.
   *
   * @return array
   *   Altered attributes.
   */
  public function alterUserAttributes(array $attributes, array $params) {

    // Puid attributes are server specific.
    if (isset($params['sid']) && $params['sid']) {
      if (is_scalar($params['sid'])) {
        $ldapServer = $this->factory
          ->getServerByIdEnabled($params['sid']);
      }
      else {
        $ldapServer = $params['sid'];
      }
      if ($ldapServer) {
        if (!isset($attributes['dn'])) {
          $attributes['dn'] = [];
        }

        // Force dn "attribute" to exist.
        $attributes['dn'] = ConversionHelper::setAttributeMap($attributes['dn']);

        // Add the attributes required by the user configuration when
        // provisioning Drupal users.
        switch ($params['ldap_context']) {
          case 'ldap_user_insert_drupal_user':
          case 'ldap_user_update_drupal_user':
          case 'ldap_user_ldap_associate':
            if ($ldapServer
              ->get('user_attr')) {
              $attributes[$ldapServer
                ->get('user_attr')] = ConversionHelper::setAttributeMap(@$attributes[$ldapServer
                ->get('user_attr')]);
            }
            if ($ldapServer
              ->get('mail_attr')) {
              $attributes[$ldapServer
                ->get('mail_attr')] = ConversionHelper::setAttributeMap(@$attributes[$ldapServer
                ->get('mail_attr')]);
            }
            if ($ldapServer
              ->get('picture_attr')) {
              $attributes[$ldapServer
                ->get('picture_attr')] = ConversionHelper::setAttributeMap(@$attributes[$ldapServer
                ->get('picture_attr')]);
            }
            if ($ldapServer
              ->get('unique_persistent_attr')) {
              $attributes[$ldapServer
                ->get('unique_persistent_attr')] = ConversionHelper::setAttributeMap(@$attributes[$ldapServer
                ->get('unique_persistent_attr')]);
            }
            if ($ldapServer
              ->get('mail_template')) {
              ConversionHelper::extractTokenAttributes($attributes, $ldapServer
                ->get('mail_template'));
            }
            break;
        }
        $ldapContext = empty($params['ldap_context']) ? NULL : $params['ldap_context'];
        $direction = empty($params['direction']) ? $this
          ->ldapContextToProvDirection($ldapContext) : $params['direction'];
        $helper = new SyncMappingHelper();
        $attributesRequiredByOtherModuleMappings = $helper
          ->getLdapUserRequiredAttributes($direction, $ldapContext);
        $attributes = array_merge($attributesRequiredByOtherModuleMappings, $attributes);
        return $attributes;
      }
    }
    return $attributes;
  }

  /**
   * LDAP attributes to alter.
   *
   * @param array $availableUserAttributes
   *   Available attributes.
   * @param array $params
   *   Parameters.
   *
   * @return array
   *   Altered attributes.
   */
  public function alterLdapUserAttributes(array $availableUserAttributes, array $params) {
    if (isset($params['direction'])) {
      $direction = $params['direction'];
    }
    else {
      $direction = self::PROVISION_TO_NONE;
    }
    if ($direction == self::PROVISION_TO_LDAP) {
      $availableUserAttributes['[property.name]'] = [
        'name' => 'Property: Username',
        'source' => '',
        'direction' => self::PROVISION_TO_LDAP,
        'enabled' => TRUE,
        'prov_events' => [
          self::EVENT_CREATE_DRUPAL_USER,
          self::EVENT_SYNC_TO_DRUPAL_USER,
        ],
        'config_module' => 'ldap_user',
        'prov_module' => 'ldap_user',
        'configurable_to_ldap' => TRUE,
      ];
      $availableUserAttributes['[property.mail]'] = [
        'name' => 'Property: Email',
        'source' => '',
        'direction' => self::PROVISION_TO_LDAP,
        'enabled' => TRUE,
        'prov_events' => [
          self::EVENT_CREATE_DRUPAL_USER,
          self::EVENT_SYNC_TO_DRUPAL_USER,
        ],
        'config_module' => 'ldap_user',
        'prov_module' => 'ldap_user',
        'configurable_to_ldap' => TRUE,
      ];
      $availableUserAttributes['[property.picture]'] = [
        'name' => 'Property: picture',
        'source' => '',
        'direction' => self::PROVISION_TO_LDAP,
        'enabled' => TRUE,
        'prov_events' => [
          self::EVENT_CREATE_DRUPAL_USER,
          self::EVENT_SYNC_TO_DRUPAL_USER,
        ],
        'config_module' => 'ldap_user',
        'prov_module' => 'ldap_user',
        'configurable_to_ldap' => TRUE,
      ];
      $availableUserAttributes['[property.uid]'] = [
        'name' => 'Property: Drupal User Id (uid)',
        'source' => '',
        'direction' => self::PROVISION_TO_LDAP,
        'enabled' => TRUE,
        'prov_events' => [
          self::EVENT_CREATE_DRUPAL_USER,
          self::EVENT_SYNC_TO_DRUPAL_USER,
        ],
        'config_module' => 'ldap_user',
        'prov_module' => 'ldap_user',
        'configurable_to_ldap' => TRUE,
      ];
    }

    // 1. Drupal user properties
    // 1.a make sure empty array are present so array + function works.
    foreach ([
      'property.status',
      'property.timezone',
      'property.signature',
    ] as $property_id) {
      $property_token = '[' . $property_id . ']';
      if (!isset($availableUserAttributes[$property_token]) || !is_array($availableUserAttributes[$property_token])) {
        $availableUserAttributes[$property_token] = [];
      }
    }

    // @todo make these merges so they don't override saved values such as 'enabled'
    $availableUserAttributes['[property.status]'] = $availableUserAttributes['[property.status]'] + [
      'name' => 'Property: Account Status',
      'configurable_to_drupal' => 1,
      'configurable_to_ldap' => 1,
      'user_tokens' => '1=enabled, 0=blocked.',
      'enabled' => FALSE,
      'config_module' => 'ldap_user',
      'prov_module' => 'ldap_user',
    ];
    $availableUserAttributes['[property.timezone]'] = $availableUserAttributes['[property.timezone]'] + [
      'name' => 'Property: User Timezone',
      'configurable_to_drupal' => 1,
      'configurable_to_ldap' => 1,
      'enabled' => FALSE,
      'config_module' => 'ldap_user',
      'prov_module' => 'ldap_user',
    ];
    $availableUserAttributes['[property.signature]'] = $availableUserAttributes['[property.signature]'] + [
      'name' => 'Property: User Signature',
      'configurable_to_drupal' => 1,
      'configurable_to_ldap' => 1,
      'enabled' => FALSE,
      'config_module' => 'ldap_user',
      'prov_module' => 'ldap_user',
    ];

    // 2. Drupal user fields.
    $user_fields = \Drupal::entityManager()
      ->getFieldStorageDefinitions('user');
    foreach ($user_fields as $field_name => $field_instance) {
      $field_id = "[field.{$field_name}]";
      if (!isset($availableUserAttributes[$field_id]) || !is_array($availableUserAttributes[$field_id])) {
        $availableUserAttributes[$field_id] = [];
      }
      $availableUserAttributes[$field_id] = $availableUserAttributes[$field_id] + [
        'name' => t('Field: @label', [
          '@label' => $field_instance
            ->getLabel(),
        ]),
        'configurable_to_drupal' => 1,
        'configurable_to_ldap' => 1,
        'enabled' => FALSE,
        'config_module' => 'ldap_user',
        'prov_module' => 'ldap_user',
      ];
    }
    if (!LdapConfiguration::provisionsDrupalAccountsFromLdap()) {
      $availableUserAttributes['[property.mail]']['config_module'] = 'ldap_user';
      $availableUserAttributes['[property.name]']['config_module'] = 'ldap_user';
      $availableUserAttributes['[property.picture]']['config_module'] = 'ldap_user';
    }
    if ($direction == self::PROVISION_TO_LDAP) {
      $availableUserAttributes['[password.random]'] = [
        'name' => 'Password: Random password',
        'source' => '',
        'direction' => self::PROVISION_TO_LDAP,
        'enabled' => TRUE,
        'prov_events' => [
          self::EVENT_CREATE_DRUPAL_USER,
          self::EVENT_SYNC_TO_DRUPAL_USER,
        ],
        'config_module' => 'ldap_user',
        'prov_module' => 'ldap_user',
        'configurable_to_ldap' => TRUE,
      ];

      // Use user password when available fall back to random pwd.
      $availableUserAttributes['[password.user-random]'] = [
        'name' => 'Password: Plain user password or random',
        'source' => '',
        'direction' => self::PROVISION_TO_LDAP,
        'enabled' => TRUE,
        'prov_events' => [
          self::EVENT_CREATE_DRUPAL_USER,
          self::EVENT_SYNC_TO_DRUPAL_USER,
        ],
        'config_module' => 'ldap_user',
        'prov_module' => 'ldap_user',
        'configurable_to_ldap' => TRUE,
      ];

      // Use user password, do not modify if unavailable.
      $availableUserAttributes['[password.user-only]'] = [
        'name' => 'Password: Plain user password',
        'source' => '',
        'direction' => self::PROVISION_TO_LDAP,
        'enabled' => TRUE,
        'prov_events' => [
          self::EVENT_CREATE_DRUPAL_USER,
          self::EVENT_SYNC_TO_DRUPAL_USER,
        ],
        'config_module' => 'ldap_user',
        'prov_module' => 'ldap_user',
        'configurable_to_ldap' => TRUE,
      ];
    }

    // TODO: This is possibly an overlap with SyncMappingHelper.
    $mappings = $this->config
      ->get('ldapUserSyncMappings');

    // This is where need to be added to arrays.
    if (!empty($mappings[$direction])) {
      $availableUserAttributes = $this
        ->applyUserAttributes($availableUserAttributes, $mappings, $direction);
    }
    return [
      $availableUserAttributes,
      $params,
    ];
  }

  /**
   * Test if the user is LDAP associated.
   *
   * @param \Drupal\user\UserInterface $account
   *   The Drupal user.
   *
   * @return bool
   *   Whether the user is LDAP associated.
   */
  public function isUserLdapAssociated(UserInterface $account) {
    $associated = FALSE;
    if (property_exists($account, 'ldap_user_current_dn') && !empty($account
      ->get('ldap_user_current_dn')->value)) {
      $associated = TRUE;
    }
    elseif ($account
      ->id()) {
      $authmap = ExternalAuthenticationHelper::getUserIdentifierFromMap($account
        ->id());
      if (!empty($authmap)) {
        $associated = TRUE;
      }
    }
    return $associated;
  }

  /**
   * Callback for hook_ENTITY_TYPE_insert().
   *
   * Perform any actions required, due to possibly not being the module creating
   * the user.
   *
   * @param \Drupal\user\UserInterface $account
   *   The Drupal user.
   */
  public function newDrupalUserCreated(UserInterface $account) {
    $this->account = $account;
    if (ExternalAuthenticationHelper::excludeUser($account)) {
      return;
    }
    if (is_object($account) && $account
      ->getAccountName()) {

      // Check for first time user.
      if (SemaphoreStorage::get('provision', $account
        ->getAccountName()) || SemaphoreStorage::get('sync', $account
        ->getAccountName()) || $this
        ->newAccountRequest($account)) {
        return;
      }
    }

    // The account is already created, so do not provisionDrupalAccount(), just
    // syncToDrupalAccount(), even if action is 'provision'.
    if ($account
      ->isActive() && LdapConfiguration::provisionAvailableToDrupal(self::PROVISION_DRUPAL_USER_ON_USER_UPDATE_CREATE)) {
      $this
        ->syncToDrupalAccount(self::EVENT_CREATE_DRUPAL_USER);
    }
    $this
      ->provisionLdapEntryOnUserCreation($account);
  }

  /**
   * Callback for hook_ENTITY_TYPE_update().
   *
   * @param \Drupal\user\UserInterface $account
   *   The Drupal user.
   */
  public function drupalUserUpdated(UserInterface $account) {

    // For some reason cloning was only necessary on the update hook.
    $this->account = clone $account;
    if (ExternalAuthenticationHelper::excludeUser($this->account)) {
      return;
    }

    // Check for provisioning to LDAP; this will normally occur on
    // hook_user_insert or other event when Drupal user is created.
    if ($this
      ->provisionsLdapEntriesFromDrupalUsers() && LdapConfiguration::provisionAvailableToLdap(self::PROVISION_LDAP_ENTRY_ON_USER_ON_USER_UPDATE_CREATE)) {
      $this
        ->provisionLdapEntryOnUserUpdateCreateEvent();
    }
    if (SemaphoreStorage::get('sync_drupal', $this->account
      ->getAccountName())) {
      return;
    }
    else {
      SemaphoreStorage::set('sync_drupal', $this->account
        ->getAccountName());
      if (LdapConfiguration::provisionsDrupalAccountsFromLdap() && in_array(self::EVENT_SYNC_TO_DRUPAL_USER, array_keys(LdapConfiguration::provisionsDrupalEvents()))) {
        $this
          ->syncToDrupalAccount(self::EVENT_SYNC_TO_DRUPAL_USER);
      }
    }
  }

  /**
   * Handle Drupal user login.
   *
   * @param \Drupal\user\UserInterface $account
   *   The Drupal user.
   */
  public function drupalUserLogsIn(UserInterface $account) {
    $this->account = $account;
    if (ExternalAuthenticationHelper::excludeUser($this->account)) {
      return;
    }
    $this
      ->loginDrupalAccountProvisioning();
    $this
      ->loginLdapEntryProvisioning();
  }

  /**
   * Handle deletion of Drupal user.
   *
   * @param \Drupal\user\UserInterface $account
   *   The Drupal user account.
   */
  public function drupalUserDeleted(UserInterface $account) {

    // Drupal user account is about to be deleted.
    $this
      ->deleteProvisionedLdapEntry($account);
    ExternalAuthenticationHelper::deleteUserIdentifier($account
      ->id());
  }

  /**
   * Apply user attributes.
   *
   * @param array $availableUserAttributes
   *   Available attributes.
   * @param array $mappings
   *   Mappings.
   * @param string $direction
   *   Synchronization direction.
   *
   * @return array
   *   All attributes applied.
   */
  private function applyUserAttributes(array $availableUserAttributes, array $mappings, $direction) {
    foreach ($mappings[$direction] as $target_token => $mapping) {
      if ($direction == self::PROVISION_TO_DRUPAL && isset($mapping['user_attr'])) {
        $key = $mapping['user_attr'];
      }
      elseif ($direction == self::PROVISION_TO_LDAP && isset($mapping['ldap_attr'])) {
        $key = $mapping['ldap_attr'];
      }
      else {
        continue;
      }
      $keys = [
        'ldap_attr',
        'user_attr',
        'convert',
        'direction',
        'enabled',
        'prov_events',
      ];
      foreach ($keys as $subKey) {
        if (isset($mapping[$subKey])) {
          $availableUserAttributes[$key][$subKey] = $mapping[$subKey];
        }
        else {
          $availableUserAttributes[$key][$subKey] = NULL;
        }
        $availableUserAttributes[$key]['config_module'] = 'ldap_user';
        $availableUserAttributes[$key]['prov_module'] = 'ldap_user';
      }
      if ($mapping['user_attr'] == 'user_tokens') {
        $availableUserAttributes['user_attr'] = $mapping['user_tokens'];
      }
    }
    return $availableUserAttributes;
  }

  /**
   * Create a Drupal user.
   *
   * @param array $ldap_user
   *   The LDAP user.
   */
  private function createDrupalUser(array $ldap_user) {
    $this->account
      ->enforceIsNew();
    $this
      ->applyAttributesToAccount($ldap_user, self::PROVISION_TO_DRUPAL, [
      self::EVENT_CREATE_DRUPAL_USER,
    ]);
    $tokens = [
      '%drupal_username' => $this->account
        ->getAccountName(),
    ];
    if (empty($this->account
      ->getAccountName())) {
      drupal_set_message(t('User account creation failed because of invalid, empty derived Drupal username.'), 'error');
      \Drupal::logger('ldap_user')
        ->error('Failed to create Drupal account %drupal_username because Drupal username could not be derived.', $tokens);
      return FALSE;
    }
    if (!($mail = $this->account
      ->getEmail())) {
      drupal_set_message(t('User account creation failed because of invalid, empty derived email address.'), 'error');
      \Drupal::logger('ldap_user')
        ->error('Failed to create Drupal account %drupal_username because email address could not be derived by LDAP User module', $tokens);
      return FALSE;
    }
    if ($account_with_same_email = user_load_by_mail($mail)) {
      \Drupal::logger('ldap_user')
        ->error('LDAP user %drupal_username has email address (%email) conflict with a Drupal user %duplicate_name', [
        '%drupal_username' => $this->account
          ->getAccountName(),
        '%email' => $mail,
        '%duplicate_name' => $account_with_same_email
          ->getAccountName(),
      ]);
      drupal_set_message(t('Another user already exists in the system with the same email address. You should contact the system administrator in order to solve this conflict.'), 'error');
      return FALSE;
    }
    $this
      ->saveAccount();
    if (!$this->account) {
      drupal_set_message(t('User account creation failed because of system problems.'), 'error');
    }
    else {
      ExternalAuthenticationHelper::setUserIdentifier($this->account, $this->account
        ->getAccountName());
      return TRUE;
    }
  }

  /**
   * Update Drupal user from PUID.
   *
   * @param array $ldap_user
   *   The LDAP user.
   * @param \Drupal\user\UserInterface $accountFromPuid
   *   The account from the PUID.
   */
  private function updateExistingAccountByPersistentUid(array $ldap_user, UserInterface $accountFromPuid) {
    $this->account = $accountFromPuid;
    ExternalAuthenticationHelper::setUserIdentifier($this->account, $this->account
      ->getAccountName());
    $this
      ->syncToDrupalAccount(self::EVENT_SYNC_TO_DRUPAL_USER, $ldap_user);
  }

  /**
   * Process user picture from LDAP entry.
   *
   * @param array $ldap_entry
   *   The LDAP entry.
   *
   * @return bool|\Drupal\file\Entity\File
   *   Drupal file object image user's thumbnail or FALSE if none present or
   *   an error occurs.
   */
  private function userPictureFromLdapEntry(array $ldap_entry) {
    if ($ldap_entry && $this->server
      ->get('picture_attr')) {

      // Check if LDAP entry has been provisioned.
      if (isset($ldap_entry[$this->server
        ->get('picture_attr')][0])) {
        $ldapUserPicture = $ldap_entry[$this->server
          ->get('picture_attr')][0];
      }
      else {

        // No picture present.
        return FALSE;
      }

      // @TODO 2914053.
      if (!$this->account || $this->account
        ->isAnonymous() || $this->account
        ->id() == 1) {
        return FALSE;
      }
      $currentUserPicture = $this->account
        ->get('user_picture')
        ->getValue();
      if (empty($currentUserPicture)) {
        return $this
          ->saveUserPicture($this->account
          ->get('user_picture'), $ldapUserPicture);
      }
      else {
        $file = File::load($currentUserPicture[0]['target_id']);
        if ($file && md5(file_get_contents($file
          ->getFileUri())) == md5($ldapUserPicture)) {

          // Same image, do nothing.
          return FALSE;
        }
        else {
          return $this
            ->saveUserPicture($this->account
            ->get('user_picture'), $ldapUserPicture);
        }
      }
    }
  }

  /**
   * Save the user's picture.
   *
   * @param \Drupal\Core\Field\FieldItemListInterface $field
   *   The field attached to the user.
   * @param string $ldapUserPicture
   *   The picture itself.
   *
   * @return array|bool
   *   Returns file ID wrapped in target or false.
   */
  private function saveUserPicture(FieldItemListInterface $field, $ldapUserPicture) {

    // Create tmp file to get image format and derive extension.
    $fileName = uniqid();
    $unmanagedFile = file_directory_temp() . '/' . $fileName;
    file_put_contents($unmanagedFile, $ldapUserPicture);
    $image_type = exif_imagetype($unmanagedFile);
    $extension = image_type_to_extension($image_type, FALSE);
    unlink($unmanagedFile);
    $fieldSettings = $field
      ->getFieldDefinition()
      ->getItemDefinition()
      ->getSettings();
    $tokenService = \Drupal::token();
    $directory = $tokenService
      ->replace($fieldSettings['file_directory']);
    $fullDirectoryPath = $fieldSettings['uri_scheme'] . '://' . $directory;
    if (!is_dir(\Drupal::service('file_system')
      ->realpath($fullDirectoryPath))) {
      \Drupal::service('file_system')
        ->mkdir($fullDirectoryPath, NULL, TRUE);
    }
    $managed_file = file_save_data($ldapUserPicture, $fullDirectoryPath . '/' . $fileName . '.' . $extension);
    $validators = [
      'file_validate_is_image' => [],
      'file_validate_image_resolution' => [
        $fieldSettings['max_resolution'],
      ],
      'file_validate_size' => [
        $fieldSettings['max_filesize'],
      ],
    ];
    $errors = file_validate($managed_file, $validators);
    if ($managed_file && empty(file_validate($managed_file, $validators))) {
      return [
        'target_id' => $managed_file
          ->id(),
      ];
    }
    else {

      // Todo: Verify file garbage collection.
      foreach ($errors as $error) {
        $this->detailLog
          ->log('File upload error for user image with validation error @error', [
          '@error' => $error,
        ]);
      }
      return FALSE;
    }
  }

  /**
   * TODO: Remove redundancy in LdapConfiguration.
   */
  private function provisionsLdapEntriesFromDrupalUsers() {
    if ($this->config
      ->get('ldapEntryProvisionServer') && count(array_filter(array_values($this->config
      ->get('ldapEntryProvisionTriggers')))) > 0) {
      return TRUE;
    }
    else {
      return FALSE;
    }
  }

  /**
   * Return context to provision direction.
   *
   * Converts the more general ldap_context string to its associated LDAP user
   * prov direction.
   *
   * @param string|null $ldapContext
   *   The relevant context.
   *
   * @return string
   *   The provisioning direction.
   */
  private function ldapContextToProvDirection($ldapContext = NULL) {
    switch ($ldapContext) {
      case 'ldap_user_prov_to_drupal':
        $result = self::PROVISION_TO_DRUPAL;
        break;
      case 'ldap_user_prov_to_ldap':
      case 'ldap_user_delete_drupal_user':
        $result = self::PROVISION_TO_LDAP;
        break;

      // Provisioning is can happen in both directions in most contexts.
      case 'ldap_user_insert_drupal_user':
      case 'ldap_user_update_drupal_user':
      case 'ldap_authentication_authenticate':
      case 'ldap_user_disable_drupal_user':
        $result = self::PROVISION_TO_ALL;
        break;
      default:
        $result = self::PROVISION_TO_ALL;
        break;
    }
    return $result;
  }

  /**
   * Handle account deletion with LDAP entry provisioning.
   *
   * @param \Drupal\user\UserInterface $account
   *   Drupal account.
   */
  private function deleteProvisionedLdapEntry(UserInterface $account) {
    if ($this
      ->provisionsLdapEntriesFromDrupalUsers() && LdapConfiguration::provisionAvailableToLdap(self::PROVISION_LDAP_ENTRY_ON_USER_ON_USER_DELETE)) {
      $ldapProcessor = new LdapUserProcessor();
      $ldapProcessor
        ->deleteProvisionedLdapEntries($account);
    }
  }

  /**
   * Handle account login with LDAP entry provisioning.
   */
  private function loginLdapEntryProvisioning() {
    if ($this
      ->provisionsLdapEntriesFromDrupalUsers() && LdapConfiguration::provisionAvailableToLdap(self::PROVISION_LDAP_ENTRY_ON_USER_ON_USER_AUTHENTICATION)) {
      $ldapUserProcessor = new LdapUserProcessor();

      // Provision entry.
      if (SemaphoreStorage::get('provision', $this->account
        ->getAccountName()) == FALSE && !$ldapUserProcessor
        ->getProvisionRelatedLdapEntry($this->account)) {
        $provisionResult = $ldapUserProcessor
          ->provisionLdapEntry($this->account);
        if ($provisionResult['status'] == 'success') {
          SemaphoreStorage::set('provision', $this->account
            ->getAccountName());
        }
      }

      // Sync entry if not just provisioned.
      if (SemaphoreStorage::get('provision', $this->account
        ->getAccountName()) == FALSE && SemaphoreStorage::get('sync', $this->account
        ->getAccountName()) == FALSE) {
        $result = $ldapUserProcessor
          ->syncToLdapEntry($this->account);
        if ($result) {
          SemaphoreStorage::set('sync', $this->account
            ->getAccountName());
        }
      }
    }
  }

  /**
   * Handle account login with Drupal provisioning.
   */
  private function loginDrupalAccountProvisioning() {
    if (LdapConfiguration::provisionsDrupalAccountsFromLdap() && in_array(self::EVENT_SYNC_TO_DRUPAL_USER, array_keys(LdapConfiguration::provisionsDrupalEvents()))) {
      $ldap_user = $this->factory
        ->getUserDataFromServerByAccount($this->account, $this->config
        ->get('drupalAcctProvisionServer'), 'ldap_user_prov_to_drupal');
      if ($ldap_user) {
        $this->server = $this->factory
          ->getServerById($this->config
          ->get('drupalAcctProvisionServer'));
        $this
          ->applyAttributesToAccount($ldap_user, self::PROVISION_TO_DRUPAL, [
          self::EVENT_SYNC_TO_DRUPAL_USER,
        ]);
      }
      $this
        ->saveAccount();
    }
  }

  /**
   * Handle the user update/create event with LDAP entry provisioning.
   */
  private function provisionLdapEntryOnUserUpdateCreateEvent() {
    if (SemaphoreStorage::get('provision', $this->account
      ->getAccountName()) || SemaphoreStorage::get('sync', $this->account
      ->getAccountName())) {
      return;
    }
    $processor = new LdapUserProcessor();

    // Check if provisioning to LDAP has already occurred this page load.
    if (!$processor
      ->getProvisionRelatedLdapEntry($this->account)) {
      $provisionResult = $processor
        ->provisionLdapEntry($this->account);
      if ($provisionResult['status'] == 'success') {
        SemaphoreStorage::set('provision', $this->account
          ->getAccountName());
      }
    }

    // Sync if not just provisioned and enabled.
    if (SemaphoreStorage::get('provision', $this->account
      ->getAccountName()) == FALSE) {

      // Check if provisioning to LDAP has already occurred this page load.
      if (LdapConfiguration::provisionAvailableToLdap(self::PROVISION_LDAP_ENTRY_ON_USER_ON_USER_UPDATE_CREATE) && $processor
        ->getProvisionRelatedLdapEntry($this->account)) {
        $ldapProcessor = new LdapUserProcessor();
        if ($ldapProcessor
          ->syncToLdapEntry($this->account)) {
          SemaphoreStorage::set('sync', $this->account
            ->getAccountName());
        }
      }
    }
  }

  /**
   * Saves the account, separated to make this testable.
   */
  private function saveAccount() {
    $this->account
      ->save();
  }

  /**
   * Apply field values to user account.
   *
   * One should not assume all attributes are present in the LDAP entry.
   *
   * @param array $ldapUser
   *   LDAP entry.
   * @param string $direction
   *   The provisioning direction.
   * @param array $prov_events
   *   The provisioning events.
   */
  private function applyAttributesToAccount(array $ldapUser, $direction = NULL, array $prov_events = NULL) {
    if ($direction == NULL) {
      $direction = self::PROVISION_TO_DRUPAL;
    }
    if (!$prov_events) {
      $prov_events = LdapConfiguration::getAllEvents();
    }
    $this
      ->setLdapBaseFields($ldapUser, $direction, $prov_events);
    if ($direction == self::PROVISION_TO_DRUPAL && in_array(self::EVENT_CREATE_DRUPAL_USER, $prov_events)) {

      // If empty, set initial mail, status active, generate a random password.
      $this
        ->setFieldsOnDrupalUserCreation($ldapUser);
    }
    $this
      ->setUserDefinedMappings($ldapUser, $direction, $prov_events);
    $context = [
      'ldap_server' => $this->server,
      'prov_events' => $prov_events,
    ];
    \Drupal::moduleHandler()
      ->alter('ldap_user_edit_user', $this->account, $ldapUser, $context);

    // Set ldap_user_last_checked.
    $this->account
      ->set('ldap_user_last_checked', time());
  }

  /**
   * For a Drupal account, query LDAP, get all user fields and save.
   *
   * @param int $provisioningEvent
   *   The provisioning event.
   * @param array $ldapUser
   *   A user's LDAP entry. Passed to avoid re-querying LDAP in cases where
   *   already present.
   *
   * @return bool
   *   Attempts to sync, reports failure if unsuccessful.
   */
  private function syncToDrupalAccount($provisioningEvent = NULL, array $ldapUser = NULL) {
    if ($provisioningEvent == NULL) {
      $provisioningEvent = self::EVENT_SYNC_TO_DRUPAL_USER;
    }
    if (!$ldapUser && !method_exists($this->account, 'getAccountName') || !$this->account) {
      \Drupal::logger('ldap_user')
        ->notice('Invalid selection passed to syncToDrupalAccount.');
      return FALSE;
    }
    if (!$ldapUser && $this->config
      ->get('drupalAcctProvisionServer')) {
      $ldapUser = $this->factory
        ->getUserDataFromServerByAccount($this->account, $this->config
        ->get('drupalAcctProvisionServer'), 'ldap_user_prov_to_drupal');
    }
    if (!$ldapUser) {
      return FALSE;
    }
    if ($this->config
      ->get('drupalAcctProvisionServer')) {
      $this->server = Server::load($this->config
        ->get('drupalAcctProvisionServer'));
      $this
        ->applyAttributesToAccount($ldapUser, self::PROVISION_TO_DRUPAL, [
        $provisioningEvent,
      ]);
    }
    $this
      ->saveAccount();
    return TRUE;
  }

  /**
   * Handle LDAP entry provision on user creation.
   *
   * @param \Drupal\user\UserInterface $account
   *   The Drupal user account.
   */
  private function provisionLdapEntryOnUserCreation(UserInterface $account) {
    if ($this
      ->provisionsLdapEntriesFromDrupalUsers()) {
      $processor = new LdapUserProcessor();
      if (LdapConfiguration::provisionAvailableToLdap(self::PROVISION_LDAP_ENTRY_ON_USER_ON_USER_UPDATE_CREATE)) {
        if (!$processor
          ->getProvisionRelatedLdapEntry($account)) {
          $provision_result = $processor
            ->provisionLdapEntry($account);
          if ($provision_result['status'] == 'success') {
            SemaphoreStorage::set('provision', $account
              ->getAccountName());
          }
        }
        else {
          if ($processor
            ->syncToLdapEntry($account)) {
            SemaphoreStorage::set('sync', $account
              ->getAccountName());
          }
        }
      }
    }
  }

  /**
   * Determine if this a user registration process.
   *
   * @param \Drupal\user\UserInterface $account
   *   Drupal user account.
   *
   * @return bool
   *   It is a registration process.
   */
  private function newAccountRequest(UserInterface $account) {
    if (\Drupal::currentUser()
      ->isAnonymous() && $account
      ->isNew()) {
      return TRUE;
    }
    else {
      return FALSE;
    }
  }

  /**
   * Sets the fields for initial users.
   *
   * @param array $ldapUser
   *   Ldap user data.
   */
  private function setFieldsOnDrupalUserCreation(array $ldapUser) {
    $derived_mail = $this->server
      ->userEmailFromLdapEntry($ldapUser['attr']);
    if (!$this->account
      ->getEmail()) {
      $this->account
        ->set('mail', $derived_mail);
    }
    if (!$this->account
      ->getPassword()) {
      $this->account
        ->set('pass', user_password(20));
    }
    if (!$this->account
      ->getInitialEmail()) {
      $this->account
        ->set('init', $derived_mail);
    }
    if (!$this->account
      ->isBlocked()) {
      $this->account
        ->set('status', 1);
    }
  }

  /**
   * Sets the fields required by LDAP.
   *
   * @param array $ldapUser
   *   Ldap user data.
   * @param string $direction
   *   Provision direction.
   * @param array $prov_events
   *   Provisioning event.
   */
  private function setLdapBaseFields(array $ldapUser, $direction, array $prov_events) {

    // Basic $user LDAP fields.
    $mappingHelper = new SyncMappingHelper();
    if ($mappingHelper
      ->isSynced('[property.name]', $prov_events, $direction)) {
      $this->account
        ->set('name', $this->server
        ->userUsernameFromLdapEntry($ldapUser['attr']));
    }
    if ($mappingHelper
      ->isSynced('[property.mail]', $prov_events, $direction)) {
      $derived_mail = $this->server
        ->userEmailFromLdapEntry($ldapUser['attr']);
      if ($derived_mail) {
        $this->account
          ->set('mail', $derived_mail);
      }
    }
    if ($mappingHelper
      ->isSynced('[property.picture]', $prov_events, $direction)) {
      $picture = $this
        ->userPictureFromLdapEntry($ldapUser['attr']);
      if ($picture) {
        $this->account
          ->set('user_picture', $picture);
      }
    }
    if ($mappingHelper
      ->isSynced('[field.ldap_user_puid]', $prov_events, $direction)) {
      $ldap_user_puid = $this->server
        ->userPuidFromLdapEntry($ldapUser['attr']);
      if ($ldap_user_puid) {
        $this->account
          ->set('ldap_user_puid', $ldap_user_puid);
      }
    }
    if ($mappingHelper
      ->isSynced('[field.ldap_user_puid_property]', $prov_events, $direction)) {
      $this->account
        ->set('ldap_user_puid_property', $this->server
        ->get('unique_persistent_attr'));
    }
    if ($mappingHelper
      ->isSynced('[field.ldap_user_puid_sid]', $prov_events, $direction)) {
      $this->account
        ->set('ldap_user_puid_sid', $this->server
        ->id());
    }
    if ($mappingHelper
      ->isSynced('[field.ldap_user_current_dn]', $prov_events, $direction)) {
      $this->account
        ->set('ldap_user_current_dn', $ldapUser['dn']);
    }
  }

  /**
   * Sets the additional, user-defined fields.
   *
   * The greyed out user mappings are not passed to this function.
   *
   * @param array $ldapUser
   *   Ldap user data.
   * @param string $direction
   *   Provision direction.
   * @param array $prov_events
   *   Provisioning event.
   */
  private function setUserDefinedMappings(array $ldapUser, $direction, array $prov_events) {
    $mappingHelper = new SyncMappingHelper();

    // Get any additional mappings.
    $mappings = $mappingHelper
      ->getSyncMappings($direction, $prov_events);

    // Loop over the mappings.
    foreach ($mappings as $key => $fieldDetails) {

      // Make sure this mapping is relevant to the sync context.
      if ($mappingHelper
        ->isSynced($key, $prov_events, $direction)) {

        // If "convert from binary is selected" and no particular method is in
        // token default to binaryConversionToString() function.
        if ($fieldDetails['convert'] && strpos($fieldDetails['ldap_attr'], ';') === FALSE) {
          $fieldDetails['ldap_attr'] = str_replace(']', ';binary]', $fieldDetails['ldap_attr']);
        }
        $value = $this->tokenProcessor
          ->tokenReplace($ldapUser['attr'], $fieldDetails['ldap_attr'], 'ldap_entry');

        // The ordinal $value_instance is not used and could probably be
        // removed.
        list($value_type, $value_name, $value_instance) = $this
          ->parseUserAttributeNames($key);
        if ($value_type == 'field' || $value_type == 'property') {
          $this->account
            ->set($value_name, $value);
        }
      }
    }
  }

  /**
   * Parse user attribute names.
   *
   * @param string $user_attr_key
   *   A string in the form of <attr_type>.<attr_name>[:<instance>] such as
   *   field.lname, property.mail, field.aliases:2.
   *
   * @return array
   *   An array such as array('field','field_user_lname', NULL).
   */
  private function parseUserAttributeNames($user_attr_key) {

    // Make sure no [] are on attribute.
    $user_attr_key = trim($user_attr_key, TokenProcessor::PREFIX . TokenProcessor::SUFFIX);
    $parts = explode('.', $user_attr_key);
    $attr_type = $parts[0];
    $attr_name = isset($parts[1]) ? $parts[1] : FALSE;
    $attr_ordinal = FALSE;
    if ($attr_name) {
      $attr_name_parts = explode(':', $attr_name);
      if (isset($attr_name_parts[1])) {
        $attr_name = $attr_name_parts[0];
        $attr_ordinal = $attr_name_parts[1];
      }
    }
    return [
      $attr_type,
      $attr_name,
      $attr_ordinal,
    ];
  }

}

Members

Namesort descending Modifiers Type Description Overrides
DrupalUserProcessor::$account private property The Drupal user account.
DrupalUserProcessor::$config private property
DrupalUserProcessor::$detailLog private property LDAP Details logger.
DrupalUserProcessor::$factory private property LDAP server factory.
DrupalUserProcessor::$server private property The server interacting with.
DrupalUserProcessor::$tokenProcessor protected property Token processor.
DrupalUserProcessor::alterLdapUserAttributes public function LDAP attributes to alter.
DrupalUserProcessor::alterUserAttributes public function Alter the user's attributes.
DrupalUserProcessor::applyAttributesToAccount private function Apply field values to user account.
DrupalUserProcessor::applyUserAttributes private function Apply user attributes.
DrupalUserProcessor::createDrupalUser private function Create a Drupal user.
DrupalUserProcessor::deleteProvisionedLdapEntry private function Handle account deletion with LDAP entry provisioning.
DrupalUserProcessor::drupalUserDeleted public function Handle deletion of Drupal user.
DrupalUserProcessor::drupalUserLogsIn public function Handle Drupal user login.
DrupalUserProcessor::drupalUserUpdated public function Callback for hook_ENTITY_TYPE_update().
DrupalUserProcessor::getUserAccount public function Get the user account.
DrupalUserProcessor::isUserLdapAssociated public function Test if the user is LDAP associated.
DrupalUserProcessor::ldapAssociateDrupalAccount public function Set LDAP associations of a Drupal account by altering user fields.
DrupalUserProcessor::ldapContextToProvDirection private function Return context to provision direction.
DrupalUserProcessor::ldapExcludeDrupalAccount public function Set flag to exclude user from LDAP association.
DrupalUserProcessor::loginDrupalAccountProvisioning private function Handle account login with Drupal provisioning.
DrupalUserProcessor::loginLdapEntryProvisioning private function Handle account login with LDAP entry provisioning.
DrupalUserProcessor::newAccountRequest private function Determine if this a user registration process.
DrupalUserProcessor::newDrupalUserCreated public function Callback for hook_ENTITY_TYPE_insert().
DrupalUserProcessor::parseUserAttributeNames private function Parse user attribute names.
DrupalUserProcessor::provisionDrupalAccount public function Provision a Drupal user account.
DrupalUserProcessor::provisionLdapEntryOnUserCreation private function Handle LDAP entry provision on user creation.
DrupalUserProcessor::provisionLdapEntryOnUserUpdateCreateEvent private function Handle the user update/create event with LDAP entry provisioning.
DrupalUserProcessor::provisionsLdapEntriesFromDrupalUsers private function TODO: Remove redundancy in LdapConfiguration.
DrupalUserProcessor::saveAccount private function Saves the account, separated to make this testable.
DrupalUserProcessor::saveUserPicture private function Save the user's picture.
DrupalUserProcessor::setFieldsOnDrupalUserCreation private function Sets the fields for initial users.
DrupalUserProcessor::setLdapBaseFields private function Sets the fields required by LDAP.
DrupalUserProcessor::setUserDefinedMappings private function Sets the additional, user-defined fields.
DrupalUserProcessor::syncToDrupalAccount private function For a Drupal account, query LDAP, get all user fields and save.
DrupalUserProcessor::updateExistingAccountByPersistentUid private function Update Drupal user from PUID.
DrupalUserProcessor::userPictureFromLdapEntry private function Process user picture from LDAP entry.
DrupalUserProcessor::__construct public function Constructor.
LdapUserAttributesInterface::ACCOUNT_CREATION_LDAP_BEHAVIOUR constant
LdapUserAttributesInterface::ACCOUNT_CREATION_USER_SETTINGS_FOR_LDAP constant
LdapUserAttributesInterface::EVENT_CREATE_DRUPAL_USER constant
LdapUserAttributesInterface::EVENT_CREATE_LDAP_ENTRY constant
LdapUserAttributesInterface::EVENT_LDAP_ASSOCIATE_DRUPAL_USER constant
LdapUserAttributesInterface::EVENT_SYNC_TO_DRUPAL_USER constant
LdapUserAttributesInterface::EVENT_SYNC_TO_LDAP_ENTRY constant
LdapUserAttributesInterface::MANUAL_ACCOUNT_CONFLICT_LDAP_ASSOCIATE constant
LdapUserAttributesInterface::MANUAL_ACCOUNT_CONFLICT_NO_LDAP_ASSOCIATE constant
LdapUserAttributesInterface::MANUAL_ACCOUNT_CONFLICT_REJECT constant
LdapUserAttributesInterface::MANUAL_ACCOUNT_CONFLICT_SHOW_OPTION_ON_FORM constant
LdapUserAttributesInterface::PROVISION_DRUPAL_USER_ON_USER_AUTHENTICATION constant
LdapUserAttributesInterface::PROVISION_DRUPAL_USER_ON_USER_ON_MANUAL_CREATION constant
LdapUserAttributesInterface::PROVISION_DRUPAL_USER_ON_USER_UPDATE_CREATE constant
LdapUserAttributesInterface::PROVISION_LDAP_ENTRY_ON_USER_ON_USER_AUTHENTICATION constant
LdapUserAttributesInterface::PROVISION_LDAP_ENTRY_ON_USER_ON_USER_DELETE constant
LdapUserAttributesInterface::PROVISION_LDAP_ENTRY_ON_USER_ON_USER_UPDATE_CREATE constant
LdapUserAttributesInterface::PROVISION_TO_ALL constant
LdapUserAttributesInterface::PROVISION_TO_DRUPAL constant
LdapUserAttributesInterface::PROVISION_TO_LDAP constant
LdapUserAttributesInterface::PROVISION_TO_NONE constant
LdapUserAttributesInterface::USER_CONFLICT_ATTEMPT_RESOLVE constant
LdapUserAttributesInterface::USER_CONFLICT_LOG constant