<?php

/**
 * @file
 *
 * Main module file for Encrypt Form API module.
 */

/**
 * Implements hook_form_alter().
 *
 * Loop through form, find elements with #encrypt, add them to an array, and
 * add our submit callback.
 */
function encryptfapi_form_alter(&$form, &$form_state, $form_id) {
  $encrypted_elements = array();
  $encrypted_elements = _encryptfapi_process_elements($form, $encrypted_elements);

  // If any elements are marked as #encrypt, store them and add our submit callback.
  if (!empty($encrypted_elements)) {
    $form['#encrypted_elements'] = $encrypted_elements;
    array_unshift($form['#submit'], '_encryptfapi_form_submit');
  }
}

/**
 * Submit handler for the form.
 *
 * This is where we replace values with the encrypted counterparts.
 */
function _encryptfapi_form_submit($form, &$form_state) {
  foreach ($form['#encrypted_elements'] as $element => $instructions) {
    // The the element's location in the form array, then get its value.
    $tree = _encryptfapi_get_array_key_path($form, $element);
    $value = drupal_array_get_nested_value($form_state['values'], $tree, $key_exists);

    // If #encrypt is an array, use the 'method' provided to encrypt. Otherwise, just use the default.
    if (is_array($instructions)) {
      $method = isset($instructions['method']) ? $instructions['method'] : NULL;
      $provider = isset($instructions['key_provider']) ? $instructions['key_provider'] : NULL;
      $encrypted = encrypt($value, array('base64' => TRUE), $method, $provider);
    }
    else {
      $encrypted = encrypt($value, array('base64' => TRUE));
    }
    form_set_value(array('#parents' => $tree), $encrypted, $form_state);
  }
}

/**
 * Implements hook_element_info_alter().
 *
 * Declare a callback for decrypting default values on elements marked as #encrypt.
 */
function encryptfapi_element_info_alter(&$type) {
  $form_elements = _encryptfapi_get_accepted_element_types();
  foreach ($form_elements as $element) {
    $type[$element]['#pre_render'][] = '_encryptfapi_element_prerender';
  }
}

/**
 * Helper function to traverse array and find all elements with #encrypt
 *
 * @param $form
 *  The form tree to parse.
 *
 * @param $encrypted_elements
 *  An array that stores the keys of items marked as #encrypt. Simply used for recursion.
 *
 * @return array
 *  An array of form elements that are marked as encrypted.
 */
function _encryptfapi_process_elements($form, &$encrypted_elements = array()) {
  // Only the following FAPI elements can be encrypted.
  $accepted_types = _encryptfapi_get_accepted_element_types();

  foreach (element_children($form) as $element) {
    if (isset($form[$element]['#type']) && in_array($form[$element]['#type'], $accepted_types) && isset($form[$element]['#encrypt']) && $form[$element]['#encrypt']) {
      $encrypted_elements[$element] = $form[$element]['#encrypt'];
    }
    elseif (is_array($form[$element])) {
      _encryptfapi_process_elements($form[$element], $encrypted_elements);
    }
  }

  return $encrypted_elements;
}

/**
 * Helper function to find the path to an array key.
 * TODO: There has to be a better way to do this.
 *
 * @param $array
 *  The array to search through.
 *
 * @param $lookup
 *  The key to find.
 *
 * @return mixed
 *  Returns an array, where each key is an element of the path to the key, or NULL is the key
 *  is not found.
 */
function _encryptfapi_get_array_key_path($array, $lookup) {
  if (array_key_exists($lookup, $array)) {
    return array($lookup);
  }
  else {
    foreach ($array as $key => $subarray) {
      if (is_array($subarray) && $ret = _encryptfapi_get_array_key_path($subarray, $lookup)) {
        $ret[] = $key;
        return array_reverse($ret);
      }
    }
  }
  return NULL;
}

/**
 * Helper function to return the types of FAPI elements that can be encrypted.
 */
function _encryptfapi_get_accepted_element_types() {
  return array('textfield', 'textarea', 'checkboxes', 'checkbox', 'radios', 'radio', 'select', 'password');
}

/**
 * Helper function to prerender form elements marked as #encrypt.
 *
 * Basically just wraps #default_value in a call to decrypt().
 */
function _encryptfapi_element_prerender($element) {
  if (isset($element['#encrypt']) && $element['#encrypt'] && isset($element['#default_value'])) {
    // TODO: Support for specified enc. method and key provider.
    $element['#default_value'] = decrypt($element['#default_value'], array('base64' => TRUE));
    $element['#value'] = decrypt($element['#value'], array('base64' => TRUE));
  }
  return $element;
}
