<?php
// $Id: ldapdata.module,v 1.31 2009/10/27 14:29:17 miglius Exp $

/**
 * @file
 * ldapdata provides data maping against ldap server.
 */

//////////////////////////////////////////////////////////////////////////////

define('LDAPDATA_SYNC',            variable_get('ldapdata_sync', 2));
define('LDAPDATA_PROFILE',        'LDAP attributes');
define('LDAPDATA_PROFILE_WEIGHT', 5);
define('LDAPDATA_USER_TAB',       'LDAP entry');
define('LDAPDATA_USER_DATA',      'ldapdata_user_data');

// Changed the values to be more unix-line. 6 = rw, 4 = ro, 2 = nothing.
define('LDAPDATA_MAP_ATTRIBUTES',           6);
define('LDAPDATA_MAP_ATTRIBUTES_READ_ONLY', 4);
define('LDAPDATA_MAP_NOTHING',              2);

//////////////////////////////////////////////////////////////////////////////
// Core API hooks

/**
 * Implements hook_init()
 */
function ldapdata_init() {
  require_once(drupal_get_path('module', 'ldapdata') .'/includes/LDAPInterface.inc');
}

/**
 * Implements hook_theme().
 */
function ldapdata_theme() {
  return array(
    'ldapdata_admin_edit' => array(
      'arguments' => array('form' => NULL),
      'file' => 'ldapdata.theme.inc'
    ),
    'ldapdata_ldap_attribute' => array(
      'arguments' => array('value' => NULL, 'type' => NULL),
      'file' => 'ldapdata.theme.inc'
    ),
  );
}

/**
 * Implements hook_menu().
 */
function ldapdata_menu() {
  return array(
    'admin/settings/ldap/ldapdata' => array(
      'title' => 'Data',
      'description' => 'Configure LDAP data to Drupal profiles synchronization settings.',
      'page callback' => 'drupal_get_form',
      'page arguments' => array('ldapdata_admin_settings'),
      'access arguments' => array('administer ldap modules'),
      'file' => 'ldapdata.admin.inc',
    ),
    'admin/settings/ldap/ldapdata/edit' => array(
      'title' => 'Data',
      'page callback' => 'drupal_get_form',
      'page arguments' => array('ldapdata_admin_edit', 4, 5),
      'type' => MENU_CALLBACK,
      'access arguments' => array('administer ldap modules'),
      'file' => 'ldapdata.admin.inc',
    ),
    'admin/settings/ldap/ldapdata/edit/%/test' => array(
      'title' => 'Test LDAP Server',
      'page callback' => '_ldapdata_ajax_test',
      'page arguments' => array(5),
      'type' => MENU_CALLBACK,
      'access arguments' => array('administer ldap modules'),
      'file' => 'ldapdata.admin.inc',
    ),
    'admin/settings/ldap/ldapdata/reset' => array(
      'title' => 'Data',
      'page callback' => 'drupal_get_form',
      'page arguments' => array('ldapdata_admin_edit', 4, 5),
      'type' => MENU_CALLBACK,
      'access arguments' => array('administer ldap modules'),
      'file' => 'ldapdata.admin.inc',
    ),
  );
}

/**
 * Implements hook_user().
 */
function ldapdata_user($op, &$edit, &$account, $category = NULL) {
  switch ($op) {
    case 'categories':
      return _ldapdata_user_categories();
    case 'form':
      return _ldapdata_user_form($account, $category);
    case 'load':
      _ldapdata_user_load($account);
      break;
    case 'login':
      _ldapdata_user_login($account);
      break;
    case 'submit':
      _ldapdata_user_submit($edit, $account, $category);
      break;
    case 'view':
      return _ldapdata_user_view($account);
  }
}

//////////////////////////////////////////////////////////////////////////////
// hook_user() functions

/**
 * Implements hook_user() categories operation.
 */
function _ldapdata_user_categories() {
  return array(
    array(
      'name' => LDAPDATA_USER_DATA,
      'title' => t(LDAPDATA_USER_TAB),
      'weight' => 50,
      'access callback' => 'ldapdata_category_access',
      'access arguments' => array(1)
    )
  );
}

/**
 * Checks if LDAP data category should be printed.
 */
function ldapdata_category_access($account) {
  global $user;

  if (!($user->uid > 0 && $user->uid == $account->uid || user_access('administer users')))
    return FALSE;

  if (!isset($account->ldap_authentified))
    return FALSE;

  return (_ldapdata_ldap_info($account, 'mapping_type') == LDAPDATA_MAP_ATTRIBUTES && count(_ldapdata_ldap_info($account, 'ldapdata_rwattrs')) > 0) ? TRUE : FALSE;
}

/**
 * Implements hook_user() categories operation.
 * Only used for editable LDAP attributes with no Drupal equivalents.
 */
function _ldapdata_user_form(&$user, $category) {
  global $_ldapdata_ldap;

  // Force LDAP sync.
  _ldapdata_user_load($user, TRUE);

  $attributes = _ldapdata_ldap_info($user, 'ldapdata_rwattrs');

  if (!isset($user->ldap_dn) || $category != LDAPDATA_USER_DATA || _ldapdata_ldap_info($user, 'mapping_type') != LDAPDATA_MAP_ATTRIBUTES || !$attributes)
    return;

  $bind_info = _ldapdata_edition($user);
  if (!$_ldapdata_ldap->connect($bind_info['dn'], $bind_info['pass'])) {
    watchdog('ldapdata', "User form: user %name's data could not be read in the LDAP directory", array('%name' => $user->name), WATCHDOG_WARNING);
    return;
  }

  $entry = $_ldapdata_ldap->retrieveAttributes($user->ldap_dn);

  $form['ldap_attributes'] = array(
    '#title' => t(LDAPDATA_PROFILE),
    '#type' => 'fieldset',
  );

  foreach (_ldapdata_ldap_info($user, 'ldapdata_attrs') as $attr_name => $attr_info) {
    if (in_array($attr_name, $attributes)) {
      array_shift($attr_info);
      $value = isset($entry[drupal_strtolower($attr_name)]) ? $entry[drupal_strtolower($attr_name)][0] : '';
      $form['ldap_attributes']['ldap_'. $attr_name] = _ldapdata_attribute_form($value, $attr_info);
    }
  }

  $_ldapdata_ldap->disconnect();

  return $form;
}

/**
 * Implements hook_user() load operation.
 */
function _ldapdata_user_load(&$account, $sync = FALSE) {
  global $user, $_ldapdata_ldap;

  // Setup the global $_ldapdata_ldap object.
  if (!_ldapdata_init($account))
    return;

  if (!$sync && (LDAPDATA_SYNC < 1 || LDAPDATA_SYNC < 2 && $user->uid != $account->uid))
    return;

  static $accounts_synced = array();
  if (isset($accounts_synced[$account->uid])) {
    return;
  }

  // See http://drupal.org/node/91786 about user_node().
  // User can be edited by the user or by other authorized users.
  $authmap = user_get_authmaps($account->name);
  if (!isset($authmap['ldapauth']) || (_ldapdata_ldap_info($account, 'mapping_type') == LDAPDATA_MAP_NOTHING)) {
    return;
  }

  $accounts_synced[$account->uid] = TRUE;
  $bind_info = _ldapdata_edition($account);
  if (!$_ldapdata_ldap->connect($bind_info['dn'], $bind_info['pass'])) {
    watchdog('ldapdata', "User load: user %name's data could not be read in the LDAP directory", array('%name' => $account->name), WATCHDOG_WARNING);
    return;
  }

  if ($entry = $_ldapdata_ldap->retrieveAttributes($account->ldap_dn)) {
    $ldap_drupal_reverse_mappings = _ldapdata_reverse_mappings($account->ldap_config);

    // Retrieve profile fields list.
    $profile_fields = _ldapdata_retrieve_profile_fields();

    $drupal_fields = array();
    foreach (_ldapdata_reverse_mappings($account->ldap_config) as $drupal_field => $ldap_attr) {
      $value = isset($entry[strtolower($ldap_attr)]) ? $entry[strtolower($ldap_attr)][0] : '';

      // Is it a profile field?
      if (is_numeric($drupal_field)) {
        if ($profile_field = isset($profile_fields[$drupal_field]) ? $profile_fields[$drupal_field] : NULL) {
          if ($row = db_fetch_array(db_query("SELECT value FROM {profile_values} WHERE fid = '%d' AND uid = '%d'", $drupal_field, $account->uid))) {
            if ($row['value'] != $value)
              db_query("UPDATE {profile_values} SET value = '%s' WHERE fid = '%d' AND uid = '%d'", $value, $drupal_field, $account->uid);
          }
          else {
            db_query("INSERT INTO {profile_values} (value, fid, uid) VALUES ('%s', '%d', '%d')", $value, $drupal_field, $account->uid);
          }
          $account->$drupal_field = $value;
        }
      }
      // Then it might be a Drupal field.
      else if (isset($account->$drupal_field) && !in_array($drupal_field, array('pass'))) {
        $drupal_fields = array_merge($drupal_fields, array($drupal_field => $value));
      }
    }
    if (!empty($drupal_fields))
      $account = user_save($account, $drupal_fields);
  }
  $_ldapdata_ldap->disconnect();
}

/**
 * Implements hook_user() login operation.
 */
function _ldapdata_user_login(&$user) {
  global $_ldapdata_ldap;

  // Force LDAP sync.
  if (LDAPDATA_SYNC == 0)
    _ldapdata_user_load($user, TRUE);
}

/**
 * Implements hook_user() submit operation.
 */
function _ldapdata_user_submit(&$edit, &$user, $category) {
  global $_ldapdata_ldap;

  // Setup the global $_ldapdata_ldap object.
  if (!_ldapdata_init($user))
    return;

  $authmap = user_get_authmaps($user->name);
  if (!isset($authmap['ldapauth']))
    return;

  // Three cases here:
  //   1. User logged on and editing his LDAP entry attributes ($category == LDAPDATA_USER_DATA).
  //   2. User logged on and editing his Drupal account settings ($category == 'account').
  //   3. OBSOLETE FROM 4.7: Password lost and being updated (category == 'account').
  // Additionally:
  //   4. User logged on and editing his profile.module fields ($category == *any*).
  $writeout = array();

  $editables = _ldapdata_ldap_info($user, 'ldapdata_rwattrs');
  if ($category == LDAPDATA_USER_DATA && $editables) {
    // Case 1:
    $writeout = array_merge($writeout, _ldapdata_user_update_ldap_attributes($edit, $user));
  }
  else if ($category == 'account') {
    // Cases 2 && 3:
    $writeout = array_merge($writeout, _ldapdata_user_update_drupal_account($edit, $user));
  }

  // And now, case 4:
  $writeout = array_merge($writeout, _ldapdata_user_update_profile($edit, $user));
  if ($writeout) {
    $bind_info = _ldapdata_edition($user);
    if (!$_ldapdata_ldap->connect($bind_info['dn'], $bind_info['pass'])) {
      watchdog('ldapdata', "User update: user %name's data could not be updated in the LDAP directory", array('%name' => $user->name), WATCHDOG_NOTICE);
      return;
    }
    if (!($_ldapdata_ldap->writeAttributes($user->ldap_dn, $writeout))) {
      drupal_set_message(t('The data was not written to LDAP.'), 'error');
    }
  }
  $_ldapdata_ldap->disconnect();
}

/**
 * Implements hook_user() view operation.
 */
function _ldapdata_user_view(&$user) {
  global $_ldapdata_ldap;

  // Setup the global $_ldapdata_ldap object.
  if (!_ldapdata_init($user))
    return;

  $authmap = user_get_authmaps($user->name);
  if (!isset($authmap['ldapauth']))
    return;

  $bind_info = _ldapdata_edition($user);
  if (!$_ldapdata_ldap->connect($bind_info['dn'], $bind_info['pass'])) {
    watchdog('ldapdata', "User view: user %name's data could not be read in the LDAP directory", array('%name' => $user->name), WATCHDOG_WARNING);
    return;
  }

  $entry = $_ldapdata_ldap->retrieveAttributes($user->ldap_dn);
  $allowed_attrs = _ldapdata_ldap_info($user, 'ldapdata_roattrs');
  $items = array();
  $i = 0;
  foreach (_ldapdata_ldap_info($user, 'ldapdata_attrs') as $attr_name => $attr_info) {
    if (in_array($attr_name, $allowed_attrs)) {
      $item = array(
        '#type' => 'user_profile_item',
        '#title' => t($attr_info[2]),
        '#value' => theme('ldapdata_ldap_attribute', $entry[drupal_strtolower($attr_name)][0], $attr_info[0]),
        '#weight' => $i++,
      );
      $items[$attr_name] = $item;
    }
  }
  if (!empty($items)) {
    $user->content[t(LDAPDATA_PROFILE)] = array_merge(array(
      '#type' => 'user_profile_category',
      '#title' => t(LDAPDATA_PROFILE),
      '#attributes' => array('class' => 'ldapdata-entry'),
      '#weight' => LDAPDATA_PROFILE_WEIGHT,
    ), $items);
  }
}

//////////////////////////////////////////////////////////////////////////////
// Auxiliary functions

/**
 * Find out which LDAP attributes should be synced back to LDAP..
 *
 * @param $edit
 *   A submitted form data.
 * @param $user
 *   A user object.
 *
 * @return
 *   An associated array of attributes to write to LDAP.
 */
function _ldapdata_user_update_ldap_attributes(&$edit, &$user) {
  $writeout = array();
  $editables = _ldapdata_ldap_info($user, 'ldapdata_rwattrs');

  foreach ($edit as $edit_attr => $value) {
    // Preventing a POST data injection: we check allowance to write value.
    if (($ldap_attr = preg_replace('/^ldap_(.*)$/', '$1', $edit_attr)) && in_array($ldap_attr, $editables))
      $writeout[$ldap_attr] = $value;
      unset($edit[$edit_attr]);
  }

  return $writeout;
}

/**
 * Find out which Drupal attributes should be synced back to LDAP..
 *
 * @param $edit
 *   A submitted form data.
 * @param $user
 *   A user object.
 *
 * @return
 *   An associated array of attributes to write to LDAP.
 */
function _ldapdata_user_update_drupal_account(&$edit, &$user) {
  global $_ldapdata_ldap;

  $writeout = array();
  if (isset($user->ldap_dn) && _ldapdata_ldap_info($user, 'mapping_type') == LDAPDATA_MAP_ATTRIBUTES) {
    // Case 2: updating account data.
    $d2l_map = _ldapdata_reverse_mappings($user->ldap_config);
    foreach ($edit as $key => $value) {
      if ($ldap_attr = isset($d2l_map[$key]) ? $d2l_map[$key] : NULL) {
        if ($key == 'pass') {
          if ($value) {
            $pw = $_ldapdata_ldap->getOption('encrypted') ? '{md5}'. base64_encode(pack('H*', md5($value))) : $value;
            $writeout[$ldap_attr] = $pw;
          }
        }
        else {
          $writeout[$ldap_attr] = $value;
        }
      }
    }
  }
  return $writeout;
}

/**
 * Find out which profile attributes should be synced back to LDAP.
 *
 * @param $edit
 *   A submitted form data.
 * @param $user
 *   A user object.
 *
 * @return
 *   An associated array of attributes to write to LDAP.
 */
function _ldapdata_user_update_profile(&$edit, &$user) {
  if (_ldapdata_ldap_info($user, 'mapping_type') != LDAPDATA_MAP_ATTRIBUTES)
    return array();

  $ldap_drupal_reverse_mappings = _ldapdata_reverse_mappings($user->ldap_config);

  // Retrieve profile fields list.
  $profile_fields = _ldapdata_retrieve_profile_fields();

  // Compare against $edit list.
  $writeout = array();
  foreach ($profile_fields as $key => $field) {
    if (isset($edit[$field]) && isset($ldap_drupal_reverse_mappings[$key])) {
      $writeout[$ldap_drupal_reverse_mappings[$key]] = $edit[$field];
    }
  }
  return $writeout;
}

/**
 * Create HTML form element for the writtable LDAP attribute.
 *
 * @param $value
 *   A current value in LDAP.
 * @param $info
 *   An array with the HTML from element definitions.
 *
 * @return
 *   An array of the form element.
 */
function _ldapdata_attribute_form($value, $info) {
  switch (array_shift($info)) {
    case 'textfield':
      $form = array(
        '#type' => 'textfield',
        '#title' => array_shift($info),
        '#default_value' => $value,
        '#size' => array_shift($info),
        '#maxlength' => array_shift($info),
        '#description' => array_shift($info),
        '#attributes' => array_shift($info),
        '#required' => array_shift($info),
      );
      break;
    case 'password':
      $form = array(
        '#type' => 'password',
        '#title' => array_shift($info),
        '#default_value' => $value,
        '#size' => array_shift($info),
        '#maxlength' => array_shift($info),
        '#description' => array_shift($info),
      );
      break;
  }
  return $form;
}

/**
 * Retrieve profile fields.
 *
 * @return
 *   An array of the form element.
 */
function _ldapdata_retrieve_profile_fields() {
  $fields = array();
  if (module_exists('profile')) {
    $result = db_query("SELECT * FROM {profile_fields}");
    while ($row = db_fetch_object($result)) {
      $fields[$row->fid] = $row->name;
    }
  }
  return $fields;
}

/**
 * Retrieve drupal user fields which can be synced with LDAP.
 *
 * @return
 *   An array of user fields.
 */
function _ldapdata_retrieve_standard_user_fields() {

  // pablom -
  // This commented code below would return all possible values,
  // but maybe that's not appropriate.
  //
  // $fields = array();
  // $result = db_query('SHOW COLUMNS FROM {users}');
  // while ($row = db_fetch_object($result)) {
  //   $fields[] = $row->Field;
  // }

  //  Rather, I'll use my benevolent dictator powers
  //  to return only sensible ones.
  $fields = array(
    'mail' => 'mail',
    'pass' => 'pass',
    'signature' => 'signature',
  );
  return $fields;
}

/**
 * Retrieve reverse LDAP to drupal mappings.
 *
 * @return
 *   An array of drupal keys pointing to LDAP attributes.
 */
function _ldapdata_reverse_mappings($sid) {
  $map = array();
  foreach (_ldapdata_ldap_info($sid, 'ldapdata_mappings') as $key => $value) {
    if (($drupal_key = preg_replace('/^ldap_amap-(.*)$/', '$1', $key)) && !in_array($drupal_key, array('access', 'status')))
      $map[$drupal_key] = $value;
  }
  return $map;
}

/**
 * Retrieve LDAP write credentials.
 *
 * @param $sid
 *   A server ID or user object.
 *
 * @return
 *   An array with the LDAP write username and password.
 */
function _ldapdata_edition($sid) {
  if (!($sid = is_object($sid) ? (isset($sid->ldap_config) ? $sid->ldap_config : NULL) : $sid))
    return;

  $row = db_fetch_object(db_query("SELECT ldapdata_binddn, ldapdata_bindpw FROM {ldapauth} WHERE sid = %d", $sid));

  return array(
    'dn' => $row->ldapdata_binddn ? $row->ldapdata_binddn : (isset($_SESSION['ldap_login']['dn']) ? $_SESSION['ldap_login']['dn'] : ''),
    'pass' => $row->ldapdata_bindpw ? $row->ldapdata_bindpw : (isset($_SESSION['ldap_login']['pass']) ? $_SESSION['ldap_login']['pass'] : ''),
  );
}

/**
 * Filter LDAP attributes.
 *
 * @param $sid
 *   A LDAP server ID.
 * @param $attributes
 *   An array of LDAP attributes.
 *
 * @return
 *   A filtered array of LDAP attributes.
 */
function _ldapdata_attribute_filter($sid, $attributes) {
  if ($code = _ldapdata_ldap_info($sid, 'ldapdata_filter_php'))
    $attributes = eval($code);

  return $attributes;
}

/**
 * Initiates the LDAPInterfase class.
 *
 * @param $sid
 *   A server ID or user object.
 *
 * @return
 */
function _ldapdata_init($sid) {
  global $_ldapdata_ldap;

  if (!($sid = is_object($sid) ? (isset($sid->ldap_config) ? $sid->ldap_config : NULL) : $sid))
    return;

  static $servers = array();
  if (!isset($servers[$sid]))
    $servers[$sid] = db_fetch_object(db_query("SELECT * FROM {ldapauth} WHERE status = 1 AND sid = %d", $sid));

  if ($servers[$sid]) {
    // Other modules can invoke user load from hook_init() before ldapdata.
    require_once(drupal_get_path('module', 'ldapdata') .'/includes/LDAPInterface.inc');

    $_ldapdata_ldap = new LDAPInterface();
    $_ldapdata_ldap->setOption('sid', $sid);
    $_ldapdata_ldap->setOption('name', $servers[$sid]->name);
    $_ldapdata_ldap->setOption('server', $servers[$sid]->server);
    $_ldapdata_ldap->setOption('port', $servers[$sid]->port);
    $_ldapdata_ldap->setOption('tls', $servers[$sid]->tls);
    $_ldapdata_ldap->setOption('encrypted', $servers[$sid]->encrypted);
    $_ldapdata_ldap->setOption('basedn', $servers[$sid]->basedn);
    $_ldapdata_ldap->setOption('user_attr', $servers[$sid]->user_attr);
    $_ldapdata_ldap->setOption('attr_filter', '_ldapdata_attribute_filter');
    return $_ldapdata_ldap;
  }
  return FALSE;
}

/**
 * Retrieve the saved ldapdata saved setting.
 *
 * @param $sid
 *   A server ID or user object.
 * @param $req
 *   An attribute name.
 *
 * @return
 *   The attribute value.
 */
function _ldapdata_ldap_info($sid, $req) {
  if (!($sid = is_object($sid) ? (isset($sid->ldap_config) ? $sid->ldap_config : NULL) : $sid))
    return;

  static $servers = array();
  if (!isset($servers[$sid]))
    $servers[$sid] = db_fetch_object(db_query("SELECT * FROM {ldapauth} WHERE sid = %d", $sid));

  switch ($req) {
    case 'mapping_type':
      $ldapdata_mappings = !empty($servers[$sid]->ldapdata_mappings) ? unserialize($servers[$sid]->ldapdata_mappings) : array();
      return isset($ldapdata_mappings['access']) ? $ldapdata_mappings['access'] : LDAPDATA_MAP_NOTHING;
    case 'ldapdata_mappings':
      return !empty($servers[$sid]->ldapdata_mappings) ? unserialize($servers[$sid]->ldapdata_mappings) : array();
    case 'ldapdata_roattrs':
      return !empty($servers[$sid]->ldapdata_roattrs) ? unserialize($servers[$sid]->ldapdata_roattrs) : array();
    case 'ldapdata_rwattrs':
      return !empty($servers[$sid]->ldapdata_rwattrs) ? unserialize($servers[$sid]->ldapdata_rwattrs) : array();
    case 'ldapdata_binddn':
      return $servers[$sid]->ldapdata_binddn;
    case 'ldapdata_bindpw':
      return $servers[$sid]->ldapdata_bindpw;
    case 'ldapdata_attrs':
      return !empty($servers[$sid]->ldapdata_attrs) ? unserialize($servers[$sid]->ldapdata_attrs) : array();
    case 'ldapdata_filter_php':
      return $servers[$sid]->ldapdata_filter_php;
  }
}

