<?php

// $Id: ec_useracc.module,v 1.1.2.12 2007/03/01 13:26:14 gordon Exp $

/*
TODO: Member-only discounts available on other products purchased when a user account 
   is purchased.
*/

/**
 * Enables user account provision when products are purchased.
 * @author Sammy Spets (thanks to Synerger Pty Ltd, Australia)
 */

define('ECMAIL_TYPE_EMAIL_CONFIRM', 'ec_useracc confirm');
define('ECMAIL_TYPE_SITE_WELCOME', 'ec_useracc welcome');

define('MAILVAR_EMAIL_CONFIRM', 'ec_mail_email_confirm');
define('MAILVAR_SITE_WELCOME', 'ec_mail_site_welcome');

define('ECUSERACC_DEFAULT_CONFIRM_EXPIRY', 2); // number of days a confirmation stays valid
define('ECUSERACC_CODE_NCHARS', 32); // number of chars in the confirmation code
define('ECUSERACC_SECONDS_PER_DAY', 86400);

/**
 * Implementation of hook_cron()
 */
function ec_useracc_cron() {
  $t = time();
  if ($t > variable_get('ec_useracc_last_cron', 0) + ECUSERACC_SECONDS_PER_DAY) {
    $result = db_query("SELECT * FROM {ec_useracc_confirm} c, {users} u, {ec_transaction} t WHERE c.uid = u.uid AND t.uid = u.uid AND expiry <= %d", $t);
    while ($row = db_fetch_array($result)) {
      // remove spam accounts from the system
      if (!$row['status']) {
        db_query("DELETE FROM {users} WHERE uid = %d", $row['uid']);
        db_query('DELETE FROM {sessions} WHERE uid = %d', $row['uid']);
        db_query('DELETE FROM {users_roles} WHERE uid = %d', $row['uid']);
        db_query('DELETE FROM {authmap} WHERE uid = %d', $row['uid']);
        $edit = array();
        module_invoke_all('user', 'delete', $edit, (object)$row);

        // TODO: mark the transaction as spam or cancelled

        watchdog(t('Removed SPAM user account'), WATCHDOG_NOTICE); 
      }
    }
    db_query("DELETE FROM {ec_useracc_confirm} WHERE expiry <= %d", $t);

    variable_set('ec_useracc_last_cron', $t);
  }
}

/**
 * Implementation of hook_help().
 */
function ec_useracc_help($section) {
  switch ($section) {
    case 'admin/modules#description':
      return '<b>EC:</b> '.t('Enables user account provision when products are purchased (requires: ec_mail, ec_anon).');
  }
}

/**
 * Implementation of hook_menu().
 */
function ec_useracc_menu($may_cache) {
  $items = array();

  if ($may_cache) {
    $items[] = array(
      'path' => 'ec_useracc',
      'type' => MENU_CALLBACK,
      'callback' => 'ec_useracc_blank',
      'access' => 1
    );
    $items[] = array(
      'path' => 'ec_useracc/confirm', 
      'title' => t('email confirmation'),
      'type' => MENU_CALLBACK,
      'callback' => 'ec_useracc_confirmation',
      'access' => 1
    );
    $items[] = array(
      'path' => 'admin/store/settings/ec_useracc',
      'title' => 'user account creation',
      'callback' => 'ec_useracc_ec_settings',
      'access' => user_access('administer store'),
      'type' => MENU_NORMAL_ITEM,
    );
  }

  return $items;
}

/**
 * Used to display a blank page.
 */
function ec_useracc_blank() {
  return '';
}

/**
 * Implementation of hook_settings()
 */
function ec_useracc_ec_settings() {
  $form = ec_mail_selection_form_for_variable('ec_useracc_settings_form', ECMAIL_TYPE_EMAIL_CONFIRM, t('Confirmation e-mail'), MAILVAR_EMAIL_CONFIRM, t("This mail will be sent to confirm a new user's email address. If you want to edit this mail or add more mails see the %url page.", array('%url' => l(t('ec_mail settings'), 'admin/store/settings/ec_mail'))));
  $form[MAILVAR_SITE_WELCOME] =  ec_mail_selection_form_for_variable('ec_useracc_settings_form', ECMAIL_TYPE_SITE_WELCOME, t('Welcome mail'), MAILVAR_SITE_WELCOME, t("This mail will be sent to confirm a new user's email address. If you want to edit this mail or add more mails see the %url page.", array('%url' => l(t('ec_mail settings'), 'admin/store/settings/ec_mail'))));

  $form['useracc_confirm_expiry'] = array(
    '#type' => 'select',
    '#title' => t('Days to confirmation expiry'),
    '#default_value' => variable_get('useracc_confirm_expiry', ECUSERACC_DEFAULT_CONFIRM_EXPIRY),
    '#options' => drupal_map_assoc(range(1, 31)),
    '#description' => t('This is the number of days an email confirmation is valid for. If the confirmation is not made within this time the cron system removes them along with the unused user account.')
  );
  
  return system_settings_form('ec_useracc_ec_settings_form', $form);
}

/**
 * Implementation of hook_user().
 */
function ec_useracc_user($op, &$edit, &$account, $category = NULL) {
  $confirm = db_fetch_array(db_query("SELECT * FROM {ec_useracc_confirm} WHERE uid = %d", $account->uid));
  if (!$confirm || ($op != 'categories' && $category != 'useracc_confirm')) {
    return;
  }

  switch ($op) {
    case 'categories':
      return array(
        array('name' => 'useracc_confirm', 'title' => t('email confirmation'), 'weight' => 10)
      );
    case 'form':
      return ec_useracc_confirm_form();
      break;
    case 'update':
      ec_useracc_confirmation($confirm['code']);
      drupal_goto("user/{$confirm['uid']}/edit");
      break;
  }
}

/**
 * Produces the confirmation form page in the user account editing section.
 */
function ec_useracc_confirm_form() {
  $form['confirm'] = array(
    '#type' => 'checkbox',
    '#title' => t('Confirm the account of this user?'),
    '#default_value' => FALSE,
    '#description' => t('Check this box to confirm the user account.')
  );

  return $form;
}

/**
 * Implementation of hook_nodeapi().
 */
function ec_useracc_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) {
  switch ($op) {
    case 'load':
      ec_useracc_load($node);
      break;
    case 'validate':
      ec_useracc_validate($node);
      break;
    case 'insert':
    case 'update':
      ec_useracc_save($node);
      break;
    case 'view':
      if (user_access('administer products')) {
        return theme('ec_useracc_view', $node);
      }
      break;
  }
}

/**
 * Loads the user account provision information into the product node.
 *
 * @param Product node.
 */
function ec_useracc_load(&$node) {
  if (!isset($node->useracc['create'])) {
    $ua = db_fetch_array(db_query("SELECT * FROM {ec_useracc} WHERE vid = %d", $node->vid));
    if ($ua) {
      $node->useracc = array('create' => $ua['creat'], 'block' => $ua['block']);
      $node->allow_blocked_buyer = $ua['creat'];
    }
  }
}

/**
 * Validates the node form.
 *
 * @param $node Product node of the form data.
 */
function ec_useracc_validate(&$node) {
  if (module_exist('ec_recurring') && !ec_recurring_is_schedule_selected($node) && $node->useracc['block']) {
    form_set_error('sid', t('A recurring schedule must be selected for accounts to be blocked at product expiry.'));
  }
}

/**
 * Saves the user account provision information contained in the given node.
 * Returns TRUE if the save was successful. Otherwise it returns FALSE.
 *
 * @param $node Product containing user account provision fields.
 * @param $force_block Forces the value of useracc_block to be saved even if
 *   the ec_recurring module isn't installed.
 * @return TRUE if the save was successful.
 */
function ec_useracc_save(&$node, $force_block = FALSE) {
  $creat = ($node->useracc_create ? 1 : 0);
  $block = ($node->useracc_block ? 1 : 0);
 
  if (!$force_block && !$node->sid && $block) {
    $block = 0;
    drupal_set_message(t('Setting for blocking user accounts at expiry has been cleared because no recurring schedule was set.'));
  }

  $affected = db_affected_rows(db_query("UPDATE {ec_useracc} SET creat = %d, block = %d WHERE vid = %d", $creat, $block, $node->vid));
  if (!$affected) {
    return db_affected_rows(db_query("INSERT INTO {ec_useracc} (vid, creat, block) VALUES (%d, %d, %d)", $node->vid, $creat, $block));
  }

  return $affected;
}

/**
 * Implementation of hook_form_alter().
 */
function ec_useracc_form_alter($form_id, &$form) {
  if (product_form_is_product($form_id, $form)) {
    $form['product']['useracc'] = array(
      '#type' => 'fieldset',
      '#title' => t('User account provision'),
      '#weight' => -14,
      '#collapsible' => TRUE
    );
    
    $form['product']['useracc']['useracc_create'] = array(
      '#type' => 'checkbox',
      '#title' => t('Create an account for the user when this product is purchased.'),
      '#default_value' => $form['#node']->useracc['create'],
      '#description' => t('Check this box when you want a user account created for the customer when they buy this product. It will only create the account if the user does not have one already.')
    );
      
    // this will only be accessible when the product is a recurring one
    // TODO: make this section of the form appear when the user checks the
    // recurring payments checkbox
    $disabled = (!is_array($form['#node']->schedule));
    if ($disabled) {
      $desc = t('This functionality can be enabled by installing the !link module.', array('!link' => l('ec_recurring', 'admin/modules')));
    } else {
      $desc = t("Check this box if you want the customer's user account blocked when this product expires. It will only block the account if the user has no other unexpired products at the time of expiry.");
    }
    $form['product']['useracc']['useracc_block'] = array(
      '#type' => 'checkbox',
      '#title' => t("Block the user's account when this product expires."),
      '#default_value' => ($disabled ? FALSE : $form['#node']->useracc['block']),
      '#attributes' => ($disabled ? array('disabled' => TRUE) : array()),
      '#description' => $desc 
    );
  }
}

function theme_ec_useracc_review_form(&$form) {
  global $user;

  $screens = checkout_get_screens();
  $screen = array_search('ec_useracc', $screens);
  
  if (!$user->uid) {
    $changeurl = ($screen ? ' '. l(t('(change)'), 'cart/checkout', array(), "op={$screen}&". drupal_get_destination()) : '');
  }

  return theme('box', t('Email address'), form_render($form['mail']) . $changeurl);
}

/**
 * Implementation of hook_transactionapi()
 */
function ec_useracc_ec_transactionapi(&$txn, $op, $a3 = NULL, $a4 = NULL) {
  if ($op == 'load') {
    $items =& $txn->items;
    foreach  ($items as $key => $val) {
      ec_useracc_load($items[$key]);
      if ($items[$key]->useracc_create) {
        $txn->is_creational = TRUE;
      }
    }
  }
}

/**
 * Implementation of hook_ecommerceapi()
 */
function ec_useracc_ecommerceapi(&$txn, $op) {
  global $user;

  switch ($op) {
    case 'on payment completion':
      if ($user->uid == $txn->uid) {
        return;
      }
      
      // Go through each of the products purchased
      for ($i = 0; $i < count($txn['items']); $i++) {      
        $node =& $txn['items'][$i]; // easy referencing
        
        if ($node->useracc['create']) {
          if (!$_SESSION['blocked_uid'] && (!is_numeric($txn['uid']) || $txn['uid'] == 0)) {
            ec_useracc_create($txn, $node);
          } else {
            if ($_SESSION['blocked_uid']) {
              ec_useracc_login_blocked_user($txn);
            }
            
            ec_useracc_unblock($txn['uid']);
          }
          break;
        }
      }
      break;
  }
}

/**
 * Logs in a blocked user. This function expects that the caller
 * has checked $_SESSION['blocked_uid'] for a valid value.
 * 
 * @param $txn
 *   Transaction causing the login (array)
 */
function ec_useracc_login_blocked_user(&$txn) {
  global $user;
  
  $txn['uid'] = $_SESSION['blocked_uid'];
  unset($_SESSION['blocked_uid']);
  db_query("UPDATE {ec_transaction} SET uid = %d WHERE txnid = %d", $txn['uid'], $txn['txnid']);
  module_invoke_all('useraccapi', $txn, 'account activated');
  
  // now log the user in
  $user = user_load(array('uid' => $txn['uid']));
  
  // NOTE: The following code is extracted out of user_login_submit(). There were
  // problems with the session regeneration. The session data was migrated to the new
  // session but a cookie wasn't sent to the browser to report this and the old
  // cookie ended up being used. Would be handy to get this working to reduce code repetition.
  $message = t('Session opened for %name.', array('%name' => theme('placeholder', $user->name)));
  watchdog('user', $message);
  
  // Update the user table timestamp noting user has logged in.
  db_query("UPDATE {users} SET login = %d WHERE uid = %d", time(), $user->uid);

  user_module_invoke('login', $user, $user);

  db_query("UPDATE {sessions} SET uid = %d WHERE sid = '%s'", $user->uid, session_id());
}

/**
 * Creates a user account for the given transaction and product. This function
 * assumes the product given is a creational one. If the current user is logged
 * in this function does nothing.
 *
 * @param $txn Transaction
 * @param $node Product
 */
function ec_useracc_create(&$txn, &$node) {
  global $user;
  
  if (empty($txn['mail'])) {
    return;
  }
  $newacc['mail'] = $txn['mail'];
  $newacc['name'] = user_password(); // temp username
  $newacc['pass'] = user_password();
  $newacc['status'] = FALSE;

  // create the new account
  $account = user_save('', $newacc);

  // update the name to be customer_N with the possibility of extending the '_' section
  // if duplicates exist
  $affected = 0;
  $account->name = 'customer_'.$account->uid;
  while (!$affected) {
    $affected = db_affected_rows(db_query("UPDATE {users} SET name = '%s' WHERE uid = %d", $account->name, $account->uid));
    if (!$affected) {
      $account->name = preg_replace("/(_+)/", '_$1', $account->name);
    } 
  }
  $newacc['name'] = $account->name;

  watchdog('user', t('New user: %name %email.', array('%name' => theme('placeholder', $account->name), '%email' => theme('placeholder', '<'. $account->mail .'>'))), WATCHDOG_NOTICE, l(t('edit'), 'user/'. $account->uid .'/edit'));

  // migrate the transaction to the new user account
  $txn['uid'] = $account->uid;
  db_query("UPDATE {ec_transaction} SET uid = %d WHERE txnid = %d", $account->uid, $txn['txnid']);

  module_invoke_all('useraccapi', $txn, 'account activated');

  if ($txn['gross'] != 0) {
    ec_useracc_send_welcome($account, $newacc['pass']);
  } else {
    ec_useracc_send_confirm($account, $newacc['pass']);
    drupal_set_title(t('Email address confirmation'));
    drupal_set_message(t('An email has been sent to the email address you specified during the checkout process. Inside it there are instructions on confirming your email address. The instructions in that email will only work for a limited time. See the email for more details. Thank you for registering with us.'));
    $_REQUEST['destination'] = url('ec_useracc');
    return;
  }
 
  if (!$user->uid) {
    // unblock the account then log the user in
    ec_useracc_unblock($account->uid);
    $user = user_authenticate($account->name, $newacc['pass']);
    $newacc['uid'] = $account->uid;
    user_login_submit('', $newacc);

    // redirect user to their account page
    $_REQUEST['destination'] = url('user');
  }
} 

/**
 * Unblocks the given user account.
 *
 * @param User ID of the user to unblock
 */
function ec_useracc_unblock($uid) {
  if (empty($uid)) {
    echo __FUNCTION__."() ERROR: uid is invalid and cannot unblock account";
    exit(1);
  }

  db_query("UPDATE {users} SET status = 1 WHERE uid = %d", $uid);
}

/**
 * Sends a confirmation mail to the given user.
 *
 * @param $account User's account
 * @param $password User's clear text password
 */
function ec_useracc_send_confirm($account, $password) {
  // generate a confirmation code
  $account->code = user_password(ECUSERACC_CODE_NCHARS);

  // save the confirmation code, uid, expiry timestamp and cleartext password
  $account->confexpiry = time() + variable_get('useracc_confirm_expiry', ECUSERACC_DEFAULT_CONFIRM_EXPIRY) * 86400;
  db_query("INSERT INTO {ec_useracc_confirm} (code, uid, pass, expiry) VALUES ('%s', %d, '%s', %d)", $account->code, $account->uid, $password, $account->confexpiry);

  // send the confirmation email to the user
  $mid = variable_get(ECMAIL_TYPE_EMAIL_CONFIRM, 0);
  ec_mail_send_mid($mid, $account->mail, $account);
}

/**
 * Processes an email address confirmation.
 *
 * @param $code Confirmation code
 */
function ec_useracc_confirmation($code) {
  global $user;
  
  if (FALSE) {//empty($code) || strlen($code) != ECUSERACC_CODE_NCHARS) {
    return drupal_not_found();
  }
  $confirm = db_fetch_array(db_query("SELECT uid, pass FROM {ec_useracc_confirm} WHERE code = '%s'", $code));

  if (!$confirm) {
    return drupal_not_found();
  } else {
    // remove the entry from the confirm table
    db_query("DELETE FROM {ec_useracc_confirm} WHERE uid = %d", $confirm['uid']);

    // unblock the account
    ec_useracc_unblock($confirm['uid']);
  }
 
  $account = user_load(array('uid' => $confirm['uid']));

  if (!$user->uid) {
    drupal_set_message(t('Thank you for confirming your email address.'));
  }

  ec_useracc_send_welcome($account, $confirm['pass']);

  return '';
}

/**
 * Sends a welcome email to the given user's email address.
 *
 * @param $account User account. NOT A REFERENCE because the
 *  clear text password is added to the $account temporarily.
 * @param $password User's clear text password
 */
function ec_useracc_send_welcome($account, $password) {
  global $base_url;
  
  $account->password = $password;

  $mid = variable_get(ECMAIL_TYPE_SITE_WELCOME, 0);
  ec_mail_send_mid($mid, $account->mail, $account);

  drupal_set_message(t('Your password and further instructions have been sent to your e-mail address.'));
}

/**
 * Process account blocks at expiry time.
 *
 * This function increments $GLOBALS['expirations']['accounts_blocked'] each time
 * it removes a role.
 *
 * @param $node Product for which account blocking are processed. This function expects
 * $node->expired_schedule to be set to the expiring entry from
 * ec_recurring_expiration and $node->uid to be the user that purchased the
 * product.
 */
function ec_useracc_atexpiry($node) {
  // Do nothing if the product doesn't block or was previously renewed
  if (!$node->useracc['block'] || $node->expiry_schedule['status'] == ECRECURRING_STATUS_RENEWED) {
    return;
  }

  // Also do nothing if the user has unexpired creational products
  if (db_num_rows(db_query("SELECT * FROM {ec_recurring_expiration} e, {ec_transaction} t WHERE e.txnid = t.txnid AND t.uid = %d AND e.status <> %d AND e.rid <= %d", $node->uid, ECRECURRING_STATUS_EXPIRED, ECRECURRING_UNLIMITED))) {
    return;
  }
  
  if (db_affected_rows(db_query("UPDATE {users} SET status = 0 WHERE uid = %d", $node->uid))) {
    $GLOBALS['expirations']['accounts_blocked']++;
  }
}

/**
 * Implementation of hook_recurringapi().
 */
function ec_useracc_recurringapi($op, &$obj, $val1 = NULL, $val2 = NULL) {
  switch ($op) {
    case 'on expiry':
      ec_useracc_atexpiry($obj);
      break;
    case 'cron report':
      print "Accounts blocked: ".$GLOBALS['expirations']['accounts_blocked']."\n\n";
      break;
  }
}

/**
 * Returns TRUE if the given transaction contains a creational product.
 *
 * @param $txn Transaction to check
 * @return TRUE if the given transaction contains a creational product.
 */
function ec_useracc_is_creational_txn(&$txn) {
  $items =& $txn->items;
  foreach ($items as $key => $val) {
    // can't use $val here because it's not a reference in PHP4
    ec_useracc_load($items[$key]);

    if ($items[$key]->useracc_create) {
      return TRUE;
    }
  }
}

/**
 * Implements hook_token_list() from the token module.
 */
function ec_useracc_token_list($type = 'all') {
  $tokens = array();
  if (in_array($type, array(ECMAIL_TYPE_EMAIL_CONFIRM, ECMAIL_TYPE_SITE_WELCOME))) {
    $tokens = token_list('user');
  }
  
  if ($type == 'all' || $type == ECMAIL_TYPE_EMAIL_CONFIRM) {
    $tokens[ECMAIL_TYPE_EMAIL_CONFIRM]['mail-confirm-url']            = t('Confirmation URL');
    $tokens[ECMAIL_TYPE_EMAIL_CONFIRM]['mail-confirm-time-to-expire'] = t('Time to confirmation expiry');
  } else if ($type == 'all' || $type == ECMAIL_TYPE_SITE_WELCOME) {
    $tokens[ECMAIL_TYPE_SITE_WELCOME]['user-clear-password']          = t("User's clear text password");
  }
  
  return $tokens;
}

/**
 * Implements hook_token_values() from the token module.
 */
function ec_useracc_token_values($type, $object = NULL) {
  $values = FALSE;
  
  $account = (object) $object;
  
  if (in_array($type, array(ECMAIL_TYPE_EMAIL_CONFIRM, ECMAIL_TYPE_SITE_WELCOME))) {
    $values = token_values('user', $account);
  }
  
  switch ($type) {
    case ECMAIL_TYPE_EMAIL_CONFIRM:
      $values['mail-confirm-url']            = url("ec_useracc/confirm/{$account->code}", NULL, NULL, TRUE);
      $values['mail-confirm-time-to-expire'] = format_interval($account->confexpiry - time());
      break;
    case ECMAIL_TYPE_SITE_WELCOME:
      $values['user-clear-password']         = $account->password;
      break;
  }

  return $values;
}

/**
 * Implementation of hook_mail_types() from ec_mail module.
 */
function ec_useracc_mail_types() {
  return array(
    ECMAIL_TYPE_EMAIL_CONFIRM => t('Email address confirmation'),
    ECMAIL_TYPE_SITE_WELCOME => t('Site welcome email'),
  );
}

/**
 * Implementation of hook_mail_reset() from ec_mail module.
 */
function ec_useracc_mail_reset($type) {
  $mids = array();
  $var = NULL;
  switch ($type) {
    case ECMAIL_TYPE_EMAIL_CONFIRM:
      $defsub = t('%site: Confirmation of your email address');
      $defbody = t("Dear %firstname!\n\nThank you for registering at %site. Before you can access the site you need to visit the following URL to confirm your email address:\n\n%confirmuri\n\nAfter that, you will be sent your login details via email.\n\n%site team\n%uri");
      $mid = ec_mail_import_old_mail('Default email confirmation', $type, 'useracc', $defsub, 'useracc', $defbody);
      $var = MAILVAR_EMAIL_CONFIRM;
      break;
    case ECMAIL_TYPE_SITE_WELCOME:
      $defsub = t('Welcome to %site!');
      $defbody = t("Dear %firstname,\n\nThank you for registering at %site. You may now log in to %login_uri using the following username and password:\n\nusername: %username\npassword: %password\n\nYou may also log in by clicking on this link or copying and pasting it in your browser:\n\n%reset_uri\n\nThis is a one-time login, so it can be used only once.\n\nAfter logging in, you will be redirected to %edit_uri so you can change your password.\n\nYour new %site membership also enables to you to login to other Drupal powered websites (e.g. http://drupal.org/) without registering. Just use the following Drupal ID and password:\n\nDrupal ID: %username@%uri_brief\npassword: %password\n\n%site team\n%uri");
      $mid = ec_mail_import_old_mail('Default welcome mail', $type, 'useracc', $defsub, 'useracc', $defbody);
      $var = MAILVAR_SITE_WELCOME;
      break;
  }
  
  if ($var) {
    ec_mail_variable_change($var, $mid, TRUE);
  }
  
  return $mids;
}
