<?php

/**
 * @file
 * This class represents a ldap_user module's configuration
 * It is extended by LdapUserConfAdmin for configuration and other admin functions.
 */

require_once 'ldap_user.module';
/**
 *
 */
class LdapUserConf {

  /**
   * Server providing Drupal account provisioning.
   *
   * @var string
   *
   * @see LdapServer::sid
   */
  public $drupalAcctProvisionServer = LDAP_USER_NO_SERVER_SID;

  /**
   * Server providing LDAP entry provisioning.
   *
   * @var string
   *
   * @see LdapServer::sid
   */
  public $ldapEntryProvisionServer = LDAP_USER_NO_SERVER_SID;

  /**
   * Associative array mapping synch directions to ldap server instances.
   *
   * @var array
   */
  public $provisionSidFromDirection = [
    LDAP_USER_PROV_DIRECTION_TO_DRUPAL_USER => LDAP_USER_NO_SERVER_SID,
    LDAP_USER_PROV_DIRECTION_TO_LDAP_ENTRY => LDAP_USER_NO_SERVER_SID,
  ];

  /**
   * Array of events that trigger provisioning of Drupal Accounts
   * Valid constants are:
   *   LDAP_USER_DRUPAL_USER_PROV_ON_AUTHENTICATE
   *   LDAP_USER_DRUPAL_USER_PROV_ON_USER_UPDATE_CREATE
   *   LDAP_USER_DRUPAL_USER_PROV_ON_ALLOW_MANUAL_CREATE.
   *
   * @var array
   */
  public $drupalAcctProvisionTriggers = [LDAP_USER_DRUPAL_USER_PROV_ON_AUTHENTICATE, LDAP_USER_DRUPAL_USER_PROV_ON_USER_UPDATE_CREATE, LDAP_USER_DRUPAL_USER_PROV_ON_ALLOW_MANUAL_CREATE];

  /**
   * Array of events that trigger provisioning of LDAP Entries
   * Valid constants are:
   *   LDAP_USER_LDAP_ENTRY_PROV_ON_USER_UPDATE_CREATE
   *   LDAP_USER_LDAP_ENTRY_PROV_ON_AUTHENTICATE
   *   LDAP_USER_LDAP_ENTRY_DELETE_ON_USER_DELETE.
   *
   * @var array
   */
  public $ldapEntryProvisionTriggers = [];

  /**
   * Server providing LDAP entry provisioning.
   *
   * @var string
   *
   * @see LdapServer::sid
   */
  public $userConflictResolve = LDAP_USER_CONFLICT_RESOLVE_DEFAULT;

  /**
   * Whether to allow/disallow provisioning accounts that have the same email.
   * Depending on whether the "sharedemail" module is enabled, this variable
   * will (by default) be set accordingly.  It can be overridden by an admin.
   *
   * @var int
   *    LDAP_USER_ACCOUNTS_WITH_SAME_EMAIL_DISABLED (0)
   *    LDAP_USER_ACCOUNTS_WITH_SAME_EMAIL_ENABLED (1)
   */
  public $accountsWithSameEmail = LDAP_USER_ACCOUNTS_WITH_SAME_EMAIL_DISABLED;

  /**
   * Drupal account creation model.
   *
   * @var int
   *   LDAP_USER_ACCT_CREATION_LDAP_BEHAVIOR   /admin/config/people/accounts/settings do not affect "LDAP Associated" Drupal accounts.
   *   LDAP_USER_ACCT_CREATION_USER_SETTINGS_FOR_LDAP  use Account creation settings at /admin/config/people/accounts/settings
   */
  public $acctCreation = LDAP_USER_ACCT_CREATION_LDAP_BEHAVIOR_DEFAULT;

  /**
   * Has current object been saved to the database?
   *
   * @var bool
   */
  public $inDatabase = FALSE;

  /**
   * What to do when an ldap provisioned username conflicts with existing drupal user?
   *
   * @var int
   *   LDAP_USER_CONFLICT_LOG - log the conflict
   *   LDAP_USER_CONFLICT_RESOLVE - LDAP associate the existing drupal user
   */
  public $manualAccountConflict = LDAP_USER_MANUAL_ACCT_CONFLICT_REJECT;

  /**
   * @todo default to FALSE and check for mapping to set to true
   */
  public $setsLdapPassword = TRUE;

  public $loginConflictResolve = FALSE;

  public $disableAdminPasswordField = FALSE;
  /**
   * Array of field synch mappings provided by all modules (via hook_ldap_user_attrs_list_alter())
   * array of the form: array(
   * LDAP_USER_PROV_DIRECTION_TO_DRUPAL_USER | LDAP_USER_PROV_DIRECTION_TO_LDAP_ENTRY => array(
   *   <server_id> => array(
   *     'sid' => <server_id> (redundant)
   *     'ldap_attr' => e.g. [sn]
   *     'user_attr'  => e.g. [field.field_user_lname] (when this value is set to 'user_tokens', 'user_tokens' value is used.)
   *     'user_tokens' => e.g. [field.field_user_lname], [field.field_user_fname]
   *     'convert' => 1|0 boolean indicating need to covert from binary
   *     'direction' => LDAP_USER_PROV_DIRECTION_TO_DRUPAL_USER | LDAP_USER_PROV_DIRECTION_TO_LDAP_ENTRY (redundant)
   *     'config_module' => 'ldap_user'
   *     'prov_module' => 'ldap_user'
   *     'enabled' => 1|0 boolean
   *      prov_events' => array( of LDAP_USER_EVENT_* constants indicating during which synch actions field should be synched)
   *         - four permutations available
   *            to ldap:   LDAP_USER_EVENT_CREATE_LDAP_ENTRY,  LDAP_USER_EVENT_SYNCH_TO_LDAP_ENTRY,
   *            to drupal: LDAP_USER_EVENT_CREATE_DRUPAL_USER, LDAP_USER_EVENT_SYNCH_TO_DRUPAL_USER
   *    )
   *  )
   */
  /**
   * Array of field synching directions for each operation.  should include ldapUserSynchMappings.
   */
  public $synchMapping = NULL;
  // Keyed on direction => property, ldap, or field token such as '[field.field_lname] with brackets in them.
  /**
   * Synch mappings configured in ldap user module (not in other modules)
   *   array of the form: array(
   * LDAP_USER_PROV_DIRECTION_TO_DRUPAL_USER | LDAP_USER_PROV_DIRECTION_TO_LDAP_ENTRY => array(
   * 'sid' => <server_id> (redundant)
   * 'ldap_attr' => e.g. [sn]
   * 'user_attr'  => e.g. [field.field_user_lname] (when this value is set to 'user_tokens', 'user_tokens' value is used.)
   * 'user_tokens' => e.g. [field.field_user_lname], [field.field_user_fname]
   * 'convert' => 1|0 boolean indicating need to covert from binary
   * 'direction' => LDAP_USER_PROV_DIRECTION_TO_DRUPAL_USER | LDAP_USER_PROV_DIRECTION_TO_LDAP_ENTRY (redundant)
   * 'config_module' => 'ldap_user'
   * 'prov_module' => 'ldap_user'
   * 'enabled' => 1|0 boolean
   * prov_events' => array( of LDAP_USER_EVENT_* constants indicating during which synch actions field should be synched)
   * - four permutations available
   * to ldap:   LDAP_USER_EVENT_CREATE_LDAP_ENTRY,  LDAP_USER_EVENT_SYNCH_TO_LDAP_ENTRY,
   * to drupal: LDAP_USER_EVENT_CREATE_DRUPAL_USER, LDAP_USER_EVENT_SYNCH_TO_DRUPAL_USER
   * )
   * )
   * )
   */
  public $ldapUserSynchMappings = NULL;
  /**
   * Keyed on property, ldap, or field token such as '[field.field_lname] with brackets in them.
   */
  public $detailedWatchdog = FALSE;
  public $provisionsDrupalAccountsFromLdap = FALSE;
  public $provisionsLdapEntriesFromDrupalUsers = FALSE;

  /**
   * What should be done with ldap provisioned accounts that no longer have associated drupal accounts.
   */
  public $orphanedDrupalAcctBehavior = 'ldap_user_orphan_email';
  /**
   * Options are partially derived from user module account cancel options:.
   *
   * 'ldap_user_orphan_do_not_check' => Do not check for orphaned Drupal accounts.)
   * 'ldap_user_orphan_email' => Perform no action, but email list of orphaned accounts. (All the other options will send email summaries also.)
   * 'user_cancel_block' => Disable the account and keep its content.
   * 'user_cancel_block_unpublish' => Disable the account and unpublish its content.
   * 'user_cancel_reassign' => Delete the account and make its content belong to the Anonymous user.
   * 'user_cancel_delete' => Delete the account and its content.
   */

  public $orphanedCheckQty = 100;

  public $provisionsLdapEvents = [];
  public $provisionsDrupalEvents = [];

  public $saveable = [
    'drupalAcctProvisionServer',
    'ldapEntryProvisionServer',
    'drupalAcctProvisionTriggers',
    'ldapEntryProvisionTriggers',
    'orphanedDrupalAcctBehavior',
    'orphanedCheckQty',
    'userConflictResolve',
    'accountsWithSameEmail',
    'manualAccountConflict',
    'acctCreation',
    'ldapUserSynchMappings',
    'disableAdminPasswordField',
  ];

  /**
   *
   */
  public function __construct() {
    $this->load();

    $this->provisionSidFromDirection[LDAP_USER_PROV_DIRECTION_TO_DRUPAL_USER] = $this->drupalAcctProvisionServer;
    $this->provisionSidFromDirection[LDAP_USER_PROV_DIRECTION_TO_LDAP_ENTRY] = $this->ldapEntryProvisionServer;

    $this->provisionsLdapEvents = [
      LDAP_USER_EVENT_CREATE_LDAP_ENTRY => t('On LDAP Entry Creation'),
      LDAP_USER_EVENT_SYNCH_TO_LDAP_ENTRY => t('On Synch to LDAP Entry'),
    ];

    $this->provisionsDrupalEvents = [
      LDAP_USER_EVENT_CREATE_DRUPAL_USER => t('On Drupal User Creation'),
      LDAP_USER_EVENT_SYNCH_TO_DRUPAL_USER => t('On Synch to Drupal User'),
    ];

    $this->provisionsDrupalAccountsFromLdap = (
      $this->drupalAcctProvisionServer &&
      $this->drupalAcctProvisionServer &&
      (count(array_filter(array_values($this->drupalAcctProvisionTriggers))) > 0)
    );

    $this->provisionsLdapEntriesFromDrupalUsers = (
      $this->ldapEntryProvisionServer
      && $this->ldapEntryProvisionServer
      && (count(array_filter(array_values($this->ldapEntryProvisionTriggers))) > 0)
      );

    $this->setSynchMapping(TRUE);
    $this->detailedWatchdog = variable_get('ldap_help_watchdog_detail', 0);
  }

  /**
   *
   */
  public function load() {

    if ($saved = variable_get("ldap_user_conf", FALSE)) {
      $this->inDatabase = TRUE;
      foreach ($this->saveable as $property) {
        if (isset($saved[$property])) {
          $this->{$property} = $saved[$property];
        }
      }
    }
    else {
      $this->inDatabase = FALSE;
      // By default this variable should be 0 if the "sharedemail" module
      // is not enabled, or 1 if the module is.
      $this->accountsWithSameEmail = (int) module_exists('sharedemail');
    }
    // Determine account creation configuration.
    $user_register = variable_get('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL);
    if ($this->acctCreation == LDAP_USER_ACCT_CREATION_LDAP_BEHAVIOR_DEFAULT || $user_register == USER_REGISTER_VISITORS) {
      $this->createLDAPAccounts = TRUE;
      $this->createLDAPAccountsAdminApproval = FALSE;
    }
    elseif ($user_register == USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL) {
      $this->createLDAPAccounts = FALSE;
      $this->createLDAPAccountsAdminApproval = TRUE;
    }
    else {
      $this->createLDAPAccounts = FALSE;
      $this->createLDAPAccountsAdminApproval = FALSE;
    }
  }

  /**
   * Destructor Method.
   */
  public function __destruct() {}

  /**
   * Util to fetch mappings for a given direction.
   *
   * @param string $sid
   *   The server id.
   * @param string $direction
   *   LDAP_USER_PROV_DIRECTION_* constant.
   * @param array $prov_events
   *
   * @return arraybool
   *   Array of mappings (may be empty array)
   */
  public function getSynchMappings($direction = LDAP_USER_PROV_DIRECTION_ALL, $prov_events = NULL) {
    if (!$prov_events) {
      $prov_events = ldap_user_all_events();
    }

    $mappings = [];
    if ($direction == LDAP_USER_PROV_DIRECTION_ALL) {
      $directions = [LDAP_USER_PROV_DIRECTION_TO_DRUPAL_USER, LDAP_USER_PROV_DIRECTION_TO_LDAP_ENTRY];
    }
    else {
      $directions = [$direction];
    }
    foreach ($directions as $direction) {
      if (!empty($this->ldapUserSynchMappings[$direction])) {
        foreach ($this->ldapUserSynchMappings[$direction] as $attribute => $mapping) {
          if (!empty($mapping['prov_events'])) {
            $result = count(array_intersect($prov_events, $mapping['prov_events']));
            if ($result) {
              $mappings[$attribute] = $mapping;
            }
          }
        }
      }
    }
    return $mappings;
  }

  /**
   *
   */
  public function isDrupalAcctProvisionServer($sid) {
    if (!$sid || !$this->drupalAcctProvisionServer) {
      return FALSE;
    }
    elseif ($this->drupalAcctProvisionServer == $sid) {
      return TRUE;
    }
    else {
      return FALSE;
    }
  }

  /**
   *
   */
  public function isLdapEntryProvisionServer($sid) {
    if (!$sid || !$this->ldapEntryProvisionServer) {
      return FALSE;
    }
    elseif ($this->ldapEntryProvisionServer == $sid) {
      return TRUE;
    }
    else {
      return FALSE;
    }
  }

  /**
   * Util to fetch attributes required for this user conf, not other modules.
   *
   * @param enum $direction
   *   LDAP_USER_PROV_DIRECTION_* constants.
   * @param string $ldap_context
   */
  public function getLdapUserRequiredAttributes($direction = LDAP_USER_PROV_DIRECTION_ALL, $ldap_context = NULL) {

    $attributes_map = [];
    $required_attributes = [];
    if ($this->drupalAcctProvisionServer) {
      $prov_events = $this->ldapContextToProvEvents($ldap_context);
      $attributes_map = $this->getSynchMappings($direction, $prov_events);
      $required_attributes = [];
      foreach ($attributes_map as $detail) {
        if (count(array_intersect($prov_events, $detail['prov_events']))) {
          // Add the attribute to our array.
          if ($detail['ldap_attr']) {
            ldap_servers_token_extract_attributes($required_attributes, $detail['ldap_attr']);
          }
        }
      }
    }
    return $required_attributes;
  }

  /**
   * Converts the more general ldap_context string to its associated ldap user event.
   */
  public function ldapContextToProvEvents($ldap_context = NULL) {

    switch ($ldap_context) {

      case 'ldap_user_prov_to_drupal':
        $result = [LDAP_USER_EVENT_SYNCH_TO_DRUPAL_USER, LDAP_USER_EVENT_CREATE_DRUPAL_USER, LDAP_USER_EVENT_LDAP_ASSOCIATE_DRUPAL_ACCT];
        break;

      case 'ldap_user_prov_to_ldap':
        $result = [LDAP_USER_EVENT_SYNCH_TO_LDAP_ENTRY, LDAP_USER_EVENT_CREATE_LDAP_ENTRY];
        break;

      default:
        $result = ldap_user_all_events();

    }

    return $result;

  }

  /**
   * Converts the more general ldap_context string to its associated ldap user prov direction.
   */
  public function ldapContextToProvDirection($ldap_context = NULL) {

    switch ($ldap_context) {

      case 'ldap_user_prov_to_drupal':
        $result = LDAP_USER_PROV_DIRECTION_TO_DRUPAL_USER;
        break;

      case 'ldap_user_prov_to_ldap':
      case 'ldap_user_delete_drupal_user':
        $result = LDAP_USER_PROV_DIRECTION_TO_LDAP_ENTRY;
        break;

      // Provisioning is can hapen 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_insert_drupal_user':
      case 'ldap_user_disable_drupal_user':
        $result = LDAP_USER_PROV_DIRECTION_ALL;
        break;

      default:
        $result = LDAP_USER_PROV_DIRECTION_ALL;

    }

    return $result;
  }

  /**
   * Derive mapping array from ldap user configuration and other configurations.
   * if this becomes a resource hungry function should be moved to ldap_user functions
   * and stored with static variable. should be cached also.
   *
   * This should be cached and modules implementing ldap_user_synch_mapping_alter
   * should know when to invalidate cache.
   */

  /**
   * @todo change default to false after development
   */
  public function setSynchMapping($reset = TRUE) {

    $synch_mapping_cache = cache_get('ldap_user_synch_mapping');
    if (!$reset && $synch_mapping_cache) {
      $this->synchMapping = $synch_mapping_cache->data;
    }
    else {
      $available_user_attrs = [];
      foreach ([LDAP_USER_PROV_DIRECTION_TO_DRUPAL_USER, LDAP_USER_PROV_DIRECTION_TO_LDAP_ENTRY] as $direction) {
        $sid = $this->provisionSidFromDirection[$direction];
        $available_user_attrs[$direction] = [];
        $ldap_server = ($sid) ? ldap_servers_get_servers($sid, NULL, TRUE) : FALSE;

        $params = [
          'ldap_server' => $ldap_server,
          'ldap_user_conf' => $this,
          'direction' => $direction,
        ];

        drupal_alter('ldap_user_attrs_list', $available_user_attrs[$direction], $params);
      }
    }
    $this->synchMapping = $available_user_attrs;

    cache_set('ldap_user_synch_mapping', $this->synchMapping);
  }

  /**
   * Given a $prov_event determine if ldap user configuration supports it.
   *   this is overall, not per field synching configuration.
   *
   * @param enum $direction
   *   LDAP_USER_PROV_DIRECTION_TO_DRUPAL_USER or LDAP_USER_PROV_DIRECTION_TO_LDAP_ENTRY.
   *
   * @param enum $prov_event
   *   LDAP_USER_EVENT_SYNCH_TO_DRUPAL_USER, LDAP_USER_EVENT_CREATE_DRUPAL_USER
   *   LDAP_USER_EVENT_SYNCH_TO_LDAP_ENTRY LDAP_USER_EVENT_CREATE_LDAP_ENTRY
   *   LDAP_USER_EVENT_LDAP_ASSOCIATE_DRUPAL_ACCT
   *   LDAP_USER_EVENT_ALL.
   *
   * @param enum $action
   *   'synch', 'provision', 'delete_ldap_entry', 'delete_drupal_entry', 'cancel_drupal_entry'.
   *
   * @return bool
   */
  public function provisionEnabled($direction, $provision_trigger) {
    $result = FALSE;

    if ($direction == LDAP_USER_PROV_DIRECTION_TO_LDAP_ENTRY) {

      if (!$this->ldapEntryProvisionServer) {
        $result = FALSE;
      }
      else {
        $result = in_array($provision_trigger, $this->ldapEntryProvisionTriggers);
      }

    }
    elseif ($direction == LDAP_USER_PROV_DIRECTION_TO_DRUPAL_USER) {
      if (!$this->drupalAcctProvisionServer) {
        $result = FALSE;
      }
      else {
        $result = in_array($provision_trigger, $this->drupalAcctProvisionTriggers);
      }
    }

    return $result;
  }

  /**
   * Given a drupal account, provision an ldap entry if none exists.  if one exists do nothing.
   *
   * @param object $account
   *   drupal account object with minimum of name property.
   * @param array $ldap_user
   *   as prepopulated ldap entry.  usually not provided.
   *
   * @return array of form:
   *   array('status' => 'success', 'fail', or 'conflict'),
   *     array('ldap_server' => ldap server object),
   *     array('proposed' => proposed ldap entry),
   *     array('existing' => existing ldap entry),
   *     array('description' = > blah blah)
   */
  public function provisionLdapEntry($account, $ldap_user = NULL, $test_query = FALSE) {
    $watchdog_tokens = [];
    $result = [
      'status' => NULL,
      'ldap_server' => NULL,
      'proposed' => NULL,
      'existing' => NULL,
      'description' => NULL,
    ];

    if (is_scalar($account)) {
      $username = $account;
      $account = new stdClass();
      $account->name = $username;
    }

    list($account, $user_entity) = ldap_user_load_user_acct_and_entity($account->name);

    if (is_object($account) && property_exists($account, 'uid') && $account->uid == 1) {
      $result['status'] = 'fail';
      $result['error_description'] = 'can not provision drupal user 1';
      // Do not provision or synch user 1.
      return $result;
    }

    if ($account == FALSE || $account->uid == 0) {
      $result['status'] = 'fail';
      $result['error_description'] = 'can not provision ldap user unless corresponding drupal account exists first.';
      return $result;
    }

    if (!$this->ldapEntryProvisionServer || !$this->ldapEntryProvisionServer) {
      $result['status'] = 'fail';
      $result['error_description'] = 'no provisioning server enabled';
      return $result;
    }

    $ldap_server = ldap_servers_get_servers($this->ldapEntryProvisionServer, NULL, TRUE);
    $params = [
      'direction' => LDAP_USER_PROV_DIRECTION_TO_LDAP_ENTRY,
      'prov_events' => [LDAP_USER_EVENT_CREATE_LDAP_ENTRY],
      'module' => 'ldap_user',
      'function' => 'provisionLdapEntry',
      'include_count' => FALSE,
    ];

    list($proposed_ldap_entry, $error) = $this->drupalUserToLdapEntry($account, $ldap_server, $params, $ldap_user);
    $proposed_dn = (is_array($proposed_ldap_entry) && isset($proposed_ldap_entry['dn']) && $proposed_ldap_entry['dn']) ? $proposed_ldap_entry['dn'] : NULL;
    $proposed_dn_lcase = drupal_strtolower($proposed_dn);
    $existing_ldap_entry = ($proposed_dn) ? $ldap_server->dnExists($proposed_dn, 'ldap_entry') : NULL;

    if ($error == LDAP_USER_PROV_RESULT_NO_PWD) {
      $result['status'] = 'fail';
      $result['description'] = 'Can not provision ldap account without user provided password.';
      $result['existing'] = $existing_ldap_entry;
      $result['proposed'] = $proposed_ldap_entry;
      $result['ldap_server'] = $ldap_server;
    }
    elseif (!$proposed_dn) {
      $result['status'] = 'fail';
      $result['description'] = t('failed to derive dn and or mappings');
      return $result;
    }
    elseif ($existing_ldap_entry) {
      $result['status'] = 'conflict';
      $result['description'] = 'can not provision ldap entry because exists already';
      $result['existing'] = $existing_ldap_entry;
      $result['proposed'] = $proposed_ldap_entry;
      $result['ldap_server'] = $ldap_server;
    }
    elseif ($test_query) {
      $result['status'] = 'fail';
      $result['description'] = 'not created because flagged as test query';
      $result['proposed'] = $proposed_ldap_entry;
      $result['ldap_server'] = $ldap_server;
    }
    else {
      // Stick $proposed_ldap_entry in $ldap_entries array for drupal_alter call.
      $ldap_entries = [$proposed_dn_lcase => $proposed_ldap_entry];
      $context = [
        'action' => 'add',
        'corresponding_drupal_data' => [$proposed_dn_lcase => $account],
        'corresponding_drupal_data_type' => 'user',
      ];
      drupal_alter('ldap_entry_pre_provision', $ldap_entries, $ldap_server, $context);
      // Remove altered $proposed_ldap_entry from $ldap_entries array.
      $proposed_ldap_entry = $ldap_entries[$proposed_dn_lcase];

      $ldap_entry_created = $ldap_server->createLdapEntry($proposed_ldap_entry, $proposed_dn);
      if ($ldap_entry_created) {
        module_invoke_all('ldap_entry_post_provision', $ldap_entries, $ldap_server, $context);
        $result['status'] = 'success';
        $result['description'] = 'ldap account created';
        $result['proposed'] = $proposed_ldap_entry;
        $result['created'] = $ldap_entry_created;
        $result['ldap_server'] = $ldap_server;

        // Need to store <sid>|<dn> in ldap_user_prov_entries field, which may contain more than one.
        $ldap_user_prov_entry = $ldap_server->sid . '|' . $proposed_ldap_entry['dn'];
        if (!isset($user_entity->ldap_user_prov_entries[LANGUAGE_NONE])) {
          $user_entity->ldap_user_prov_entries = [LANGUAGE_NONE => []];
        }
        $ldap_user_prov_entry_exists = FALSE;
        foreach ($user_entity->ldap_user_prov_entries[LANGUAGE_NONE] as $i => $field_value_instance) {
          if ($field_value_instance == $ldap_user_prov_entry) {
            $ldap_user_prov_entry_exists = TRUE;
          }
        }
        if (!$ldap_user_prov_entry_exists) {
          $user_entity->ldap_user_prov_entries[LANGUAGE_NONE][] = [
            'value' => $ldap_user_prov_entry,
          ];

          // Save the field without calling user_save()
          field_attach_presave('user', $user_entity);
          field_attach_update('user', $user_entity);
        }

      }
      else {
        $result['status'] = 'fail';
        $result['proposed'] = $proposed_ldap_entry;
        $result['created'] = $ldap_entry_created;
        $result['ldap_server'] = $ldap_server;
        $result['existing'] = NULL;
      }
    }

    $tokens = [
      '%dn' => isset($result['proposed']['dn']) ? $result['proposed']['dn'] : NULL,
      '%sid' => (isset($result['ldap_server']) && $result['ldap_server']) ? $result['ldap_server']->sid : 0,
      '%username' => @$account->name,
      '%uid' => @$account->uid,
      '%description' => @$result['description'],
    ];
    if (!$test_query && isset($result['status'])) {
      if ($result['status'] == 'success') {
        if ($this->detailedWatchdog) {
          watchdog('ldap_user', 'LDAP entry on server %sid created dn=%dn.  %description. username=%username, uid=%uid', $tokens, WATCHDOG_INFO);
        }
      }
      elseif ($result['status'] == 'conflict') {
        if ($this->detailedWatchdog) {
          watchdog('ldap_user', 'LDAP entry on server %sid not created because of existing ldap entry. %description. username=%username, uid=%uid', $tokens, WATCHDOG_WARNING);
        }
      }
      elseif ($result['status'] == 'fail') {
        watchdog('ldap_user', 'LDAP entry on server %sid not created because error.  %description. username=%username, uid=%uid', $tokens, WATCHDOG_ERROR);
      }
    }
    return $result;
  }

  /**
   * Given a drupal account, synch to related ldap entry.
   *
   * @param drupal user object $account
   *   Drupal user object.
   * @param array $user_edit
   *   Edit array for user_save.  generally null unless user account is being created or modified in same synching.
   * @param array $ldap_user
   *   current ldap data of user. @see README.developers.txt for structure.
   *
   * @return TRUE on success or FALSE on fail.
   */
  public function synchToLdapEntry($account, $user_edit = NULL, $ldap_user = [], $test_query = FALSE) {

    if (is_object($account) && property_exists($account, 'uid') && $account->uid == 1) {
      // Do not provision or synch user 1.
      return FALSE;
    }

    $watchdog_tokens = [];
    $result = FALSE;
    $proposed_ldap_entry = FALSE;

    if ($this->ldapEntryProvisionServer) {
      $ldap_server = ldap_servers_get_servers($this->ldapEntryProvisionServer, NULL, TRUE);

      $params = [
        'direction' => LDAP_USER_PROV_DIRECTION_TO_LDAP_ENTRY,
        'prov_events' => [LDAP_USER_EVENT_SYNCH_TO_LDAP_ENTRY],
        'module' => 'ldap_user',
        'function' => 'synchToLdapEntry',
        'include_count' => FALSE,
      ];

      list($proposed_ldap_entry, $error) = $this->drupalUserToLdapEntry($account, $ldap_server, $params, $ldap_user);
      if ($error != LDAP_USER_PROV_RESULT_NO_ERROR) {
        $result = FALSE;
      }
      elseif (is_array($proposed_ldap_entry) && isset($proposed_ldap_entry['dn'])) {
        $existing_ldap_entry = $ldap_server->dnExists($proposed_ldap_entry['dn'], 'ldap_entry');
        // This array represents attributes to be modified; not comprehensive list of attributes.
        $attributes = [];
        foreach ($proposed_ldap_entry as $attr_name => $attr_values) {
          if ($attr_name != 'dn') {
            if (isset($attr_values['count'])) {
              unset($attr_values['count']);
            }
            if (count($attr_values) == 1) {
              $attributes[$attr_name] = $attr_values[0];
            }
            else {
              $attributes[$attr_name] = $attr_values;
            }
          }
        }

        if ($test_query) {
          $proposed_ldap_entry = $attributes;
          $result = [
            'proposed' => $proposed_ldap_entry,
            'server' => $ldap_server,
          ];
        }
        else {
          // Stick $proposed_ldap_entry in $ldap_entries array for drupal_alter call.
          $proposed_dn_lcase = drupal_strtolower($proposed_ldap_entry['dn']);
          $ldap_entries = [$proposed_dn_lcase => $attributes];
          $context = [
            'action' => 'update',
            'corresponding_drupal_data' => [$proposed_dn_lcase => $attributes],
            'corresponding_drupal_data_type' => 'user',
          ];
          drupal_alter('ldap_entry_pre_provision', $ldap_entries, $ldap_server, $context);
          // Remove altered $proposed_ldap_entry from $ldap_entries array.
          $attributes = $ldap_entries[$proposed_dn_lcase];
          $result = $ldap_server->modifyLdapEntry($proposed_ldap_entry['dn'], $attributes);
          // Success.
          if ($result) {
            module_invoke_all('ldap_entry_post_provision', $ldap_entries, $ldap_server, $context);
          }
        }
      }
      // Failed to get acceptable proposed ldap entry.
      else {
        $result = FALSE;
      }
    }

    $tokens = [
      '%dn' => isset($proposed_ldap_entry['dn']) ? $proposed_ldap_entry['dn'] : NULL,
      '%sid' => $this->ldapEntryProvisionServer,
      '%username' => $account->name,
      '%uid' => ($test_query || !property_exists($account, 'uid')) ? '' : $account->uid,
    ];

    if ($result) {
      watchdog('ldap_user', 'LDAP entry on server %sid synched dn=%dn. username=%username, uid=%uid', $tokens, WATCHDOG_INFO);
    }
    else {
      watchdog('ldap_user', 'LDAP entry on server %sid not synched because error. username=%username, uid=%uid', $tokens, WATCHDOG_ERROR);
    }

    return $result;

  }

  /**
   * Given a drupal account, query ldap and get all user fields and create user account.
   *
   * @param array $account
   *   drupal account array with minimum of name.
   * @param array $user_edit
   *   drupal edit array in form user_save($account, $user_edit) would take,
   *   generally empty unless overriding synchToDrupalAccount derived values.
   * @param array $ldap_user
   *   as user's ldap entry.  passed to avoid requerying ldap in cases where already present.
   * @param bool $save
   *   indicating if drupal user should be saved.  generally depends on where function is called from.
   *
   * @return bool|object
   *   Result of user_save() function is $save is true, otherwise return TRUE
   *   $user_edit data returned by reference
   */
  public function synchToDrupalAccount($drupal_user, &$user_edit, $prov_event = LDAP_USER_EVENT_SYNCH_TO_DRUPAL_USER, $ldap_user = NULL, $save = FALSE) {

    $debug = [
      'account' => $drupal_user,
      'user_edit' => $user_edit,
      'ldap_user' => $ldap_user,
    ];

    if (
        (!$ldap_user  && !isset($drupal_user->name)) ||
        (!$drupal_user && $save) ||
        ($ldap_user && !isset($ldap_user['sid']))
    ) {
      // Should throw watchdog error also.
      return FALSE;
    }

    if (!$ldap_user && $this->drupalAcctProvisionServer) {
      $ldap_user = ldap_servers_get_user_ldap_data($drupal_user->name, $this->drupalAcctProvisionServer, 'ldap_user_prov_to_drupal');
    }

    if (!$ldap_user) {
      return FALSE;
    }

    if ($this->drupalAcctProvisionServer) {
      $ldap_server = ldap_servers_get_servers($this->drupalAcctProvisionServer, NULL, TRUE);
      $this->entryToUserEdit($ldap_user, $user_edit, $ldap_server, LDAP_USER_PROV_DIRECTION_TO_DRUPAL_USER, [$prov_event]);
    }

    if ($save) {
      $account = user_load($drupal_user->uid);
      $result = user_save($account, $user_edit, 'ldap_user');
      return $result;
    }
    else {
      return TRUE;
    }
  }

  /**
   * Given a drupal account, delete user account.
   *
   * @param string $username
   *   drupal account name.
   *
   * @return TRUE or FALSE.  FALSE indicates failed or action not enabled in ldap user configuration
   */
  public function deleteDrupalAccount($username) {
    $user = user_load_by_name($username);
    if (is_object($user)) {
      user_delete($user->uid);
      return TRUE;
    }
    else {
      return FALSE;
    }
  }

  /**
   * Given a drupal account, find the related ldap entry.
   *
   * @param drupal user object $account
   *
   * @return FALSE or ldap entry
   */
  public function getProvisionRelatedLdapEntry($account, $prov_events = NULL) {
    if (!$prov_events) {
      $prov_events = ldap_user_all_events();
    }
    $sid = $this->ldapEntryProvisionServer;
    if (!$sid) {
      return FALSE;
    }
    // $user_entity->ldap_user_prov_entries,.
    $ldap_server = ldap_servers_get_servers($sid, NULL, TRUE);
    $params = [
      'direction' => LDAP_USER_PROV_DIRECTION_TO_LDAP_ENTRY,
      'prov_events' => $prov_events,
      'module' => 'ldap_user',
      'function' => 'getProvisionRelatedLdapEntry',
      'include_count' => FALSE,
    ];
    list($proposed_ldap_entry, $error) = $this->drupalUserToLdapEntry($account, $ldap_server, $params);
    if (!(is_array($proposed_ldap_entry) && isset($proposed_ldap_entry['dn']) && $proposed_ldap_entry['dn'])) {
      return FALSE;
    }
    $ldap_entry = $ldap_server->dnExists($proposed_ldap_entry['dn'], 'ldap_entry', []);
    return $ldap_entry;

  }

  /**
   * Given a drupal account, delete ldap entry that was provisioned based on it
   *   normally this will be 0 or 1 entry, but the ldap_user_provisioned_ldap_entries
   *   field attached to the user entity track each ldap entry provisioned.
   *
   * @param object $account
   *   drupal account.
   *
   * @return TRUE or FALSE.  FALSE indicates failed or action not enabled in ldap user configuration
   */
  public function deleteProvisionedLdapEntries($account) {
    // Determine server that is associated with user.
    $boolean_result = FALSE;
    if (isset($account->ldap_user_prov_entries[LANGUAGE_NONE][0])) {
      foreach ($account->ldap_user_prov_entries[LANGUAGE_NONE] as $i => $field_instance) {
        $parts = explode('|', $field_instance['value']);
        if (count($parts) == 2) {

          list($sid, $dn) = $parts;
          $ldap_server = ldap_servers_get_servers($sid, NULL, TRUE);
          if (is_object($ldap_server) && $dn) {
            $boolean_result = $ldap_server->delete($dn);
            $tokens = ['%sid' => $sid, '%dn' => $dn, '%username' => $account->name, '%uid' => $account->uid];
            if ($boolean_result) {
              watchdog('ldap_user', 'LDAP entry on server %sid deleted dn=%dn. username=%username, uid=%uid', $tokens, WATCHDOG_INFO);
            }
            else {
              watchdog('ldap_user', 'LDAP entry on server %sid not deleted because error. username=%username, uid=%uid', $tokens, WATCHDOG_ERROR);
            }
          }
          else {
            $boolean_result = FALSE;
          }
        }
      }
    }
    return $boolean_result;

  }

  /**
   * Populate ldap entry array for provisioning.
   *
   * @param array $account
   *   drupal account.
   * @param \LdapServer $ldap_server
   * @param array $ldap_user
   *   ldap entry of user, returned by reference.
   * @param array $params
   *   with the following key values:
   *   'ldap_context' =>
   *   'module' => module calling function, e.g. 'ldap_user'
   *   'function' => function calling function, e.g. 'provisionLdapEntry'
   *   'include_count' => should 'count' array key be included
   *   'direction' => LDAP_USER_PROV_DIRECTION_TO_LDAP_ENTRY || LDAP_USER_PROV_DIRECTION_TO_DRUPAL_USER.
   *
   * @return array(ldap entry, $result) in ldap extension array format.!THIS IS NOT THE ACTUAL LDAP ENTRY
   */
  public function drupalUserToLdapEntry($account, $ldap_server, $params, $ldap_user_entry = NULL) {
    $provision = (isset($params['function']) && $params['function'] == 'provisionLdapEntry');
    $result = LDAP_USER_PROV_RESULT_NO_ERROR;
    if (!$ldap_user_entry) {
      $ldap_user_entry = [];
    }

    if (!is_object($account) || !is_object($ldap_server)) {
      return [NULL, LDAP_USER_PROV_RESULT_BAD_PARAMS];
    }
    $watchdog_tokens = [
      '%drupal_username' => $account->name,
    ];
    $include_count = (isset($params['include_count']) && $params['include_count']);

    $direction = isset($params['direction']) ? $params['direction'] : LDAP_USER_PROV_DIRECTION_ALL;
    $prov_events = empty($params['prov_events']) ? ldap_user_all_events() : $params['prov_events'];

    $mappings = $this->getSynchMappings($direction, $prov_events);
    foreach ($mappings as $field_key => $field_detail) {
      list($ldap_attr_name, $ordinal, $conversion) = ldap_servers_token_extract_parts($field_key, TRUE);
      $ordinal = (!$ordinal) ? 0 : $ordinal;
      if ($ldap_user_entry && isset($ldap_user_entry[$ldap_attr_name]) && is_array($ldap_user_entry[$ldap_attr_name]) && isset($ldap_user_entry[$ldap_attr_name][$ordinal])) {
        // don't override values passed in.
        continue;
      }

      $synched = $this->isSynched($field_key, $params['prov_events'], LDAP_USER_PROV_DIRECTION_TO_LDAP_ENTRY);
      if ($synched) {
        $token = ($field_detail['user_attr'] == 'user_tokens') ? $field_detail['user_tokens'] : $field_detail['user_attr'];
        $value = ldap_servers_token_replace($account, $token, 'user_account');

        // Deal with empty/unresolved password.
        if (substr($token, 0, 10) == '[password.' && (!$value || $value == $token)) {
          if (!$provision) {
            // don't overwrite password on synch if no value provided.
            continue;
          }
        }

        if ($ldap_attr_name == 'dn' && $value) {
          $ldap_user_entry['dn'] = $value;
        }
        elseif ($value) {
          if (!isset($ldap_user_entry[$ldap_attr_name]) || !is_array($ldap_user_entry[$ldap_attr_name])) {
            $ldap_user_entry[$ldap_attr_name] = [];
          }
          $ldap_user_entry[$ldap_attr_name][$ordinal] = $value;
          if ($include_count) {
            $ldap_user_entry[$ldap_attr_name]['count'] = count($ldap_user_entry[$ldap_attr_name]);
          }

        }

      }

    }

    /**
     * 4. call drupal_alter() to allow other modules to alter $ldap_user
     */
    $params['account'] = $account;
    drupal_alter('ldap_entry', $ldap_user_entry, $params);

    return [$ldap_user_entry, $result];

  }

  /**
   * Given a drupal account, query ldap and get all user fields and save user account
   * (note: parameters are in odd order to match synchDrupalAccount handle)
   *
   * @param array $account
   *   drupal account object or null.
   * @param array $user_edit
   *   drupal edit array in form user_save($account, $user_edit) would take.
   * @param array $ldap_user
   *   as user's ldap entry.  passed to avoid requerying ldap in cases where already present.
   * @param bool $save
   *   indicating if drupal user should be saved.  generally depends on where function is called from and if the.
   *
   * @return bool
   *   Resultof user_save() function is $save is true, otherwise return TRUE on
   *   success or FALSE on any problem
   *
   *   $user_edit data returned by reference
   */
  public function provisionDrupalAccount($account = FALSE, &$user_edit, $ldap_user = NULL, $save = TRUE) {

    $watchdog_tokens = [];
    /**
     * @todo
     * -- add error catching for conflicts, conflicts should be checked before calling this function.
     *
     */

    if (!$account) {
      $account = new stdClass();
    }
    $account->is_new = TRUE;

    if (!$ldap_user && !isset($user_edit['name'])) {
      return FALSE;
    }

    if (!$ldap_user) {
      $watchdog_tokens['%username'] = $user_edit['name'];
      if ($this->drupalAcctProvisionServer) {
        $ldap_user = ldap_servers_get_user_ldap_data($user_edit['name'], $this->drupalAcctProvisionServer, 'ldap_user_prov_to_drupal');
      }
      if (!$ldap_user) {
        if ($this->detailedWatchdog) {
          watchdog('ldap_user', '%username : failed to find associated ldap entry for username in provision.', $watchdog_tokens, WATCHDOG_DEBUG);
        }
        return FALSE;
      }
    }

    if (!isset($user_edit['name']) && isset($account->name)) {
      $user_edit['name'] = $account->name;
      $watchdog_tokens['%username'] = $user_edit['name'];
    }
    // When using the multi-domain last authentication option
    // $ldap_server breaks beacause $this->drupalAcctProvisionServer is set on LDAP_USER_AUTH_SERVER_SID
    // So we need to check it's not the case before using ldap_servers_get_servers.
    if ($this->drupalAcctProvisionServer && $this->drupalAcctProvisionServer != LDAP_USER_AUTH_SERVER_SID) {

      /** @var \LdapServer $ldap_server */
      // $ldap_user['sid'].
      $ldap_server = ldap_servers_get_servers($this->drupalAcctProvisionServer, 'enabled', TRUE);

      $params = [
        'account' => $account,
        'user_edit' => $user_edit,
        'prov_event' => LDAP_USER_EVENT_CREATE_DRUPAL_USER,
        'module' => 'ldap_user',
        'function' => 'provisionDrupalAccount',
        'direction' => LDAP_USER_PROV_DIRECTION_TO_DRUPAL_USER,
      ];

      drupal_alter('ldap_entry', $ldap_user, $params);

      // Look for existing drupal account with same puid.  if so update username and attempt to synch in current context.
      $puid = $ldap_server->userPuidFromLdapEntry($ldap_user['attr']);
      $existing_account_from_puid = ($puid) ? $ldap_server->userUserEntityFromPuid($puid) : FALSE;

      // Synch drupal account, since drupal account exists.
      if ($existing_account_from_puid) {
        // 1. correct username and authmap.
        $this->entryToUserEdit($ldap_user, $user_edit, $ldap_server, LDAP_USER_PROV_DIRECTION_TO_DRUPAL_USER, [LDAP_USER_EVENT_SYNCH_TO_DRUPAL_USER]);
        $account = user_save($existing_account_from_puid, $user_edit, 'ldap_user');
        user_set_authmaps($account, ["authname_ldap_user" => $user_edit['name']]);
        // 2. attempt synch if appropriate for current context.
        if ($account) {
          $account = $this->synchToDrupalAccount($account, $user_edit, LDAP_USER_EVENT_SYNCH_TO_DRUPAL_USER, $ldap_user, TRUE);
        }
        return $account;
      }
      // Create drupal account.
      else {
        $this->entryToUserEdit($ldap_user, $user_edit, $ldap_server, LDAP_USER_PROV_DIRECTION_TO_DRUPAL_USER, [LDAP_USER_EVENT_CREATE_DRUPAL_USER]);
        if ($save) {
          $watchdog_tokens = ['%drupal_username' => $user_edit['name']];
          if (empty($user_edit['name'])) {
            drupal_set_message(t('User account creation failed because of invalid, empty derived Drupal username.'), 'error');
            watchdog('ldap_user',
              'Failed to create Drupal account %drupal_username because drupal username could not be derived.',
              $watchdog_tokens,
              WATCHDOG_ERROR
            );
            return FALSE;
          }
          if (!isset($user_edit['mail']) || !$user_edit['mail']) {
            drupal_set_message(t('User account creation failed because of invalid, empty derived email address.'), 'error');
            watchdog('ldap_user',
              'Failed to create Drupal account %drupal_username because email address could not be derived by LDAP User module',
              $watchdog_tokens,
              WATCHDOG_ERROR
            );
            return FALSE;
          }
          if (($this->accountsWithSameEmail == LDAP_USER_ACCOUNTS_WITH_SAME_EMAIL_DISABLED) && ($account_with_same_email = user_load_by_mail($user_edit['mail']))) {
            $watchdog_tokens['%email'] = $user_edit['mail'];
            $watchdog_tokens['%duplicate_name'] = $account_with_same_email->name;
            watchdog('ldap_user', 'LDAP user %drupal_username has email address
              (%email) conflict with a drupal user %duplicate_name', $watchdog_tokens, WATCHDOG_ERROR);
            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;
          }
          $account = user_save(NULL, $user_edit, 'ldap_user');
          if (!$account) {
            drupal_set_message(t('User account creation failed because of system problems.'), 'error');
          }
          else {
            user_set_authmaps($account, ['authname_ldap_user' => $account->name]);
            ldap_user_ldap_provision_semaphore('drupal_created', 'set', $account->name);
          }
          return $account;
        }
        return TRUE;
      }
    }
  }

  /**
   * Set ldap associations of a drupal account by altering user fields.
   *
   * @param string $drupal_username
   *
   * @return bool
   *   TRUE on success, FALSE on error or failure because of invalid user or ldap accounts
   */
  public function ldapAssociateDrupalAccount($drupal_username) {

    if ($this->drupalAcctProvisionServer) {
      $prov_events = [LDAP_USER_EVENT_LDAP_ASSOCIATE_DRUPAL_ACCT];
      // $ldap_user['sid'].
      $ldap_server = ldap_servers_get_servers($this->drupalAcctProvisionServer, 'enabled', TRUE);
      $account = user_load_by_name($drupal_username);
      $ldap_user = ldap_servers_get_user_ldap_data($drupal_username, $this->drupalAcctProvisionServer, 'ldap_user_prov_to_drupal');
      if (!$account) {
        watchdog(
          'ldap_user',
          'Failed to LDAP associate drupal account %drupal_username because account not found',
          ['%drupal_username' => $drupal_username],
          WATCHDOG_ERROR
        );
        return FALSE;
      }
      elseif (!$ldap_user) {
        watchdog(
          'ldap_user',
          'Failed to LDAP associate drupal account %drupal_username because corresponding LDAP entry not found',
          ['%drupal_username' => $drupal_username],
          WATCHDOG_ERROR
        );
        return FALSE;
      }
      else {
        $user_edit = [];
        $user_edit['data']['ldap_user']['init'] = [
          'sid'  => $ldap_user['sid'],
          'dn'   => $ldap_user['dn'],
          'mail'   => $account->mail,
        ];
        $ldap_user_puid = $ldap_server->userPuidFromLdapEntry($ldap_user['attr']);
        if ($ldap_user_puid) {
          $user_edit['ldap_user_puid'][LANGUAGE_NONE][0]['value'] = $ldap_user_puid;
        }
        $user_edit['ldap_user_puid_property'][LANGUAGE_NONE][0]['value'] = $ldap_server->unique_persistent_attr;
        $user_edit['ldap_user_puid_sid'][LANGUAGE_NONE][0]['value'] = $ldap_server->sid;
        $user_edit['ldap_user_current_dn'][LANGUAGE_NONE][0]['value'] = $ldap_user['dn'];
        $account = user_save($account, $user_edit, 'ldap_user');
        return (boolean) $account;
      }
    }
    else {
      return FALSE;
    }
  }

  /**
   * Populate $user edit array (used in hook_user_save, hook_user_update, etc)
   * ... should not assume all attribues are present in ldap entry.
   *
   * @param $ldap_user
   * @param array $edit
   *   see hook_user_save, hook_user_update, etc.
   * @param \LdapServer $ldap_server
   * @param int $direction
   * @param array $prov_events
   */
  public function entryToUserEdit($ldap_user, &$edit, $ldap_server, $direction = LDAP_USER_PROV_DIRECTION_TO_DRUPAL_USER, $prov_events = NULL) {

    // Need array of user fields and which direction and when they should be synched.
    if (!$prov_events) {
      $prov_events = ldap_user_all_events();
    }
    $mail_synched = $this->isSynched('[property.mail]', $prov_events, $direction);
    if (!isset($edit['mail']) && $mail_synched) {
      $derived_mail = $ldap_server->userEmailFromLdapEntry($ldap_user['attr']);
      if ($derived_mail) {
        $edit['mail'] = $derived_mail;
      }
    }

    $drupal_username = $ldap_server->userUsernameFromLdapEntry($ldap_user['attr']);

    if ($this->isSynched('[property.name]', $prov_events, $direction) && !isset($edit['name']) && $drupal_username) {
      $edit['name'] = $drupal_username;
    }

    if ($direction == LDAP_USER_PROV_DIRECTION_TO_DRUPAL_USER && in_array(LDAP_USER_EVENT_CREATE_DRUPAL_USER, $prov_events)) {
      $edit['mail'] = isset($edit['mail']) ? $edit['mail'] : $ldap_user['mail'];
      if (!isset($edit['pass'])) {
        $edit['pass'] = user_password(20);
        watchdog('ldap_user', '20 character random password generated for the %username account that has been created.', ['%username' => $drupal_username], WATCHDOG_INFO);
      }
      $edit['init'] = isset($edit['init']) ? $edit['init'] : $edit['mail'];
      $edit['status'] = isset($edit['status']) ? $edit['status'] : 1;
      $edit['signature'] = isset($edit['signature']) ? $edit['signature'] : '';

      $edit['data']['ldap_user']['init'] = [
        'sid'  => $ldap_user['sid'],
        'dn'   => $ldap_user['dn'],
        'mail' => $edit['mail'],
      ];
    }

    /*
     * Make sure the user account has the latest ldap_user settings
     * when syncing the profile.
     */
    if ($direction == LDAP_USER_PROV_DIRECTION_TO_DRUPAL_USER && in_array(LDAP_USER_EVENT_SYNCH_TO_DRUPAL_USER, $prov_events)) {
      $edit['data']['ldap_user']['init'] = [
        'sid'  => $ldap_user['sid'],
        'dn'   => $ldap_user['dn'],
        'mail' => isset($edit['mail']) && !empty($edit['mail']) ? $edit['mail'] : $ldap_user['mail'],
      ];
    }

    if ($this->isSynched('[property.picture]', $prov_events, $direction)) {
      $picture = $ldap_server->userPictureFromLdapEntry($ldap_user['attr'], $drupal_username);
      if ($picture) {
        if (in_array(LDAP_USER_EVENT_CREATE_DRUPAL_USER, $prov_events)) {
          $edit['picture'] = $picture->fid;
        }
        else {
          $edit['picture'] = $picture;
        }
        $edit['data']['ldap_user']['init']['thumb5md'] = $picture->md5Sum;
      }
    }

    /**
     * basic $user ldap fields
     */
    if ($this->isSynched('[field.ldap_user_puid]', $prov_events, $direction)) {
      $ldap_user_puid = $ldap_server->userPuidFromLdapEntry($ldap_user['attr']);
      if ($ldap_user_puid) {
        $edit['ldap_user_puid'][LANGUAGE_NONE][0]['value'] = $ldap_user_puid;
      }
    }
    if ($this->isSynched('[field.ldap_user_puid_property]', $prov_events, $direction)) {
      $edit['ldap_user_puid_property'][LANGUAGE_NONE][0]['value'] = $ldap_server->unique_persistent_attr;
    }
    if ($this->isSynched('[field.ldap_user_puid_sid]', $prov_events, $direction)) {
      $edit['ldap_user_puid_sid'][LANGUAGE_NONE][0]['value'] = $ldap_server->sid;
    }
    if ($this->isSynched('[field.ldap_user_current_dn]', $prov_events, $direction)) {
      $edit['ldap_user_current_dn'][LANGUAGE_NONE][0]['value'] = $ldap_user['dn'];
    }

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

    // Loop over the mappings.
    foreach ($mappings as $user_attr_key => $field_detail) {

      // Make sure this mapping is relevant to the sync context.
      if (!$this->isSynched($user_attr_key, $prov_events, $direction)) {
        continue;
      }
      /**
        * if "convert from binary is selected" and no particular method is in token,
        * default to ldap_servers_binary() function
        */
      if ($field_detail['convert'] && strpos($field_detail['ldap_attr'], ';') === FALSE) {
        $field_detail['ldap_attr'] = str_replace(']', ';binary]', $field_detail['ldap_attr']);
      }
      $value = ldap_servers_token_replace($ldap_user['attr'], $field_detail['ldap_attr'], 'ldap_entry');
      list($value_type, $value_name, $value_instance) = ldap_servers_parse_user_attr_name($user_attr_key);

      // $value_instance not used, may have future use case.
      // Are we dealing with a field?
      if ($value_type == 'field') {
        // Field api field - first we get the field.
        $field = field_info_field($value_name);
        // Then the columns for the field in the schema.
        $columns = array_keys($field['columns']);
        // Then we convert the value into an array if it's scalar.
        $values = $field['cardinality'] == 1 ? [$value] : (array) $value;

        $items = [];
        // Loop over the values and set them in our $items array.
        foreach ($values as $delta => $value) {
          if (isset($value)) {
            // We set the first column value only, this is consistent with
            // the Entity Api (@see entity_metadata_field_property_set).
            $items[$delta][$columns[0]] = $value;
          }
        }
        // Add them to our edited item.
        $edit[$value_name][LANGUAGE_NONE] = $items;
      }
      elseif ($value_type == 'property') {
        // Straight property.
        $edit[$value_name] = $value;
      }
    }

    // Allow other modules to have a say.
    drupal_alter('ldap_user_edit_user', $edit, $ldap_user, $ldap_server, $prov_events);
    // don't let empty 'name' value pass for user.
    if (isset($edit['name']) && $edit['name'] == '') {
      unset($edit['name']);
    }

  }

  /**
   * Given configuration of synching, determine is a given synch should occur.
   *
   * @param string $attr_token
   *   e.g. [property.mail], [field.ldap_user_puid_property].
   * @param array $prov_events
   *   e.g. array(LDAP_USER_EVENT_CREATE_DRUPAL_USER).  typically array with 1
   *   element.
   * @param scalar $direction
   *   LDAP_USER_PROV_DIRECTION_TO_DRUPAL_USER or
   *   LDAP_USER_PROV_DIRECTION_TO_LDAP_ENTRY.
   *
   * @return bool
   */
  public function isSynched($attr_token, $prov_events, $direction) {
    $result = (boolean) (
      isset($this->synchMapping[$direction][$attr_token]['prov_events']) &&
      count(array_intersect($prov_events, $this->synchMapping[$direction][$attr_token]['prov_events']))
    );
    if (!$result) {
      if (isset($this->synchMapping[$direction][$attr_token])) {
      }
      else {
      }
    }
    return $result;
  }

}
