<?php
/**
 * @file
 * Query By Form
 *
 * This module allows node modules to add a query by form tab for their node
 * types to the default search form
 *
 * @copyright 2008 Ouest Systemes Informatiques (OSInet)
 * @author Frederic G. MARAND
 * @license CeCILL 2.0
 * @package QBF
 */

// $Id: qbf.module,v 1.9.2.1 2008-09-19 13:27:52 marand Exp $

/**
 * Saved error reporting level.
 *
 * QBF module is supposed to pass parsing at E_ALL|E_STRICT, but other modules
 * may not be so strict, so we save the level at the start of the module and
 * restore it at the end of the module.
 */
global $_qbf_er;

$_qbf_er = error_reporting(E_ALL | E_STRICT);

/**
 * Remove this element from the generated form
 */
define('QBF_LEVEL_REMOVE',           0);
/**
 * This element is only for display in the generated form: do not include it
 * in the query vector.
 */
define('QBF_LEVEL_DISPLAY',          1);
/**
 * Include this element in the generated form and in the query vector, but do
 * not mark it as required.
 */
define('QBF_LEVEL_OPTIONAL',         2);
/**
 * Include this element in the generated form and in the query vector, and
 * mark it as required.
 */
define('QBF_LEVEL_REQUIRED',         3);

/**
 * The main QBF path
 * @ingroup paths
 */
define('QBF_PATH_MAIN',              'qbf');
/**
 * The QBF autocomplete path for search fields
 * @ingroup paths
 */
define('QBF_PATH_AC',                'qbf/ac');

/**
 * Authorize use of QBF searches
 */
define('QBF_PERM_QUERY',             'use QBF search functions');
/**
 * Authorize QBF administration
 */
define('QBF_PERM_ADMIN',             'administer QBF');

/**
 * The name of the table used to store queries
 */
define('QBF_TABLE_NAME',             'qbf_queries');

/**
 * Transform a form array for QBF.
 *
 * This function obtains the form array using Forms API, and transforms it by
 * modifying widgets to other types where needed.
 *
 * Any additional parameter passed to the function is transmitted to the form
 * generating function.
 *
 * @ingroup forms
 * @param string $form_id
 * @return array
 */
function qbf_transform_form($form_id) {
  $arArgs = func_get_args();
//dsm(array('qtf' => $arArgs));
  // Fetch the basic form and rename it, passing it the caller's arguments
  $form = call_user_func_array('drupal_retrieve_form', $arArgs);
  $newFormId = "qbf_$form_id";

  // Only keep the children of the form and QBF properties on the form itself
  $elements = array();
  $newForm = array();
  $newForm['#qbf_source_form_id'] = $form_id;
  if (in_array('#qbf', element_properties($form)))
    {
    $newForm += $form['#qbf'];
    }

  foreach (element_children($form) as $key)
    {
    // dsm("Transforming $key, type " . $form[$key]['#type']);
    $newElement = _qbf_transform_element($key, $form[$key]);
    if (!is_null($newElement))
      {
      $newForm[$key] = $newElement;
      }
    }

  $newForm['#id'] = $newFormId;
  $newForm['#multistep'] = TRUE;
  // Do not set #redirect, even to FALSE (submit handlers)
  // $newForm['#redirect']  = FALSE;
  $newForm['#after_build'][] = 'qbf_after_build';
  $newForm['#submit'] = array('qbf_submit' => array());
// dsm($newForm);
  return $newForm;
}

/**
 * Transform a form element for QBF.
 *
 * QBF-specific properties are:
 * - #qbf : array of properties
 * - #level: only within #qbf
 *
 * See QBF_* constants
 *
 * @ingroup forms
 * @param string $key
 * @param array $element
 * @return void
 */
function _qbf_transform_element($key, $element) {
  // dsm(array('key' => $key, 'element' => $element));

  /**
   * List default type transformations applied to widget by FAPI.
   *
   * Types without a default transformation are not transformed
   */
  static $arDefaultTypeTransformations = array
    (
    'button'         => NULL,
    'file'           => NULL,
    // 'hidden'         => NULL,
    'markup'         => NULL,
    'password'       => NULL,
    'radio'          => NULL,
    'submit'         => NULL,
    'textarea'       => 'textfield',
    // 'value'          => 'value',
    );

  /**
   * List default property transformations applied to widget by FAPI property.
   *
   * Properties without a default transformation are not transformed
   */
  static $arDefaultPropertyTransformations = array
    (
    // Standard properties
    '#action'        => NULL,
    '#after_build'   => NULL,
    '#base'          => NULL,
    '#button_type'   => NULL,
    '#built'         => NULL,
    '#description'   => NULL,
    '#method'        => NULL,
    '#parents'       => NULL,
    '#redirect'      => NULL,
    '#ref'           => NULL,
    '#required'      => NULL,
    '#rows'          => NULL,
    '#submit'        => NULL,
    '#tree'          => NULL,
    '#validate'      => NULL,
    );

  /**
   * List properties causing causing element removal.
   *
   * The key is the property name, the value is the one causing removal.
   */
  static $arKillerProperties = array
    (
    '#disabled'      => TRUE,
    );

  // Transform type
  $sourceType = $element['#type'];
  // .. Default transformation
  $destType = array_key_exists($sourceType, $arDefaultTypeTransformations)
    ? $arDefaultTypeTransformations[$sourceType]
    : $sourceType;
  // .. Apply form-defined type override
  if (isset($element['#qbf']['#type']))
    {
    $destType = $element['#qbf']['#type'];
    }

  if (is_null($destType))
    {
    $ret = NULL;
    }
  else
    {
    $ret = $element;
    $ret['#type'] = $destType;
    if (!array_key_exists('#qbf', $element) || $element['#qbf']['#level'] == QBF_LEVEL_REMOVE)
      {
      $ret = NULL;
      }
    else
      {
      foreach (element_properties($element) as $propertyName)
        {
        // Apply killer properties first to avoid useless work
        if (array_key_exists($propertyName, $arKillerProperties)
          && ($element[$propertyName] = $arKillerProperties[$propertyName]))
          {
          $ret = NULL;
          break;
          }

        // Now transform or copy remaining properties
        if (array_key_exists($propertyName, $arDefaultPropertyTransformations))
          {
          $ret[$propertyName] = $arDefaultPropertyTransformations[$propertyName];
          }
        else
          {
          $ret[$propertyName] = $element[$propertyName];
          }

        // And apply form-defined property overrides
        if ($propertyName == '#qbf')
          {
          foreach ($element[$propertyName] as $overrideName => $overrideValue)
            {
            $ret[$overrideName] = $overrideValue;
            }
          }
        }

      // Recursively transform children
      foreach (element_children($element) as $childName)
        {
        $child = _qbf_transform_element($childName, $element[$childName]);
        if (is_null($child))
          {
          unset($ret[$childName]);
          }
        else
          {
          $ret[$childName] = $child;
          }
        }
      }
    }

  //dsm(array('key' => $key, 'transformed element' => $ret));
  return $ret;
}

/**
 * Implement hook_perm().
 *
 * @return array
 */
function qbf_perm() {
  $ret = array
    (
    QBF_PERM_QUERY,
    );

  return $ret;
}

/**
 * Implement hook_forms().
 *
 * @ingroup forms
 * @return array
 */
function qbf_forms() {
  $hookName = 'qbf_register';

  foreach (module_implements($hookName) as $module)
    {
    foreach (module_invoke($module, $hookName) as $formName)
      {
      $forms["qbf_$formName"] = array
        (
        'callback'       => 'qbf_transform_form',
        'callback arguments' => array($formName),
        );
      }
    }

  return $forms;
}

/**
 * Insert the query results at the bottom of the query form.
 *
 * @ingroup forms
 * @param array $form
 * @param array $form_values
 * @return array
 */
function qbf_after_build($form, $form_values) {
  if (empty($form['#post']))
    {
    return $form;
    }

  // If #post is not emtpy, we are indeed querying
  $arQuery = _qbf_extract_query($form, $form_values);

  /* This function is called at the end of the form building process, which
   * means that child properties of #qbf have already been upgraded to element
   * properties. So we look for $form['#callback'] and not
   * $form['#qbf']['#callback']
   */
  if (isset($form['#callback']) && function_exists($function = $form['#callback']))
    {
    $results = $function($arQuery);
    }
  else
    {
    drupal_set_message(t('QBF: incorrect callback function for search'), 'error');
    }
  $form['qbf_query_results'] = array
    (
    '#type'    => 'markup',
    '#value'   => $results,
    '#weight'  => 10,
    );
  return $form;
}

/**
 * Recursively build a query array from the form and its values
 *
 * In the current version, element names are supposed to be unique, even at
 * different levels in the tree.
 *
 * @ingroup forms
 * @param array $form
 * @param array $form_values
 */
function _qbf_extract_query($form, $form_values) {
  $name = $form['#parents'][0];
  // Elements which are removed or display-only have no place in the query
  if (array_key_exists('#qbf', $form) && array_key_exists('#level', $form['#qbf'])
    && $form['#qbf']['#level'] >= QBF_LEVEL_OPTIONAL)
    {
    $ret = array($name => $form_values[$name]);
    }
  else
    {
    $ret = array();
    }

  // QBF level is not inherited, so this loop is outside the "if" above
  foreach (element_children($form) as $childName)
    {
    $ret += _qbf_extract_query($form[$childName], $form_values);
    }

  return $ret;
}

/**
 * Provide an optional automatic mapping mechanism for query building.
 *
 * This function takes a partly built query map $arQueryMap, and a defaults
 * array to complete it in $arDefaults, and returns a fully built query array
 * ready to be used for querying.
 *
 * @param array $arQuery
 * @param array $arDefaults
 * @return array
 */
function qbf_query_mapper($arQueryMap = array(), $arDefaults = array()) {
  $ret = array();

  foreach ($arQueryMap as $name => $value)
    {
    if (!is_array($value)) // accept NULL, empty strings...
      {
      $value = array();
      }
    $item = $value;

    foreach ($arDefaults as $defaultKey => $defaultValue)
      {
      if (!array_key_exists($defaultKey, $item))
        {
        $item[$defaultKey] = is_null($defaultValue)
          ? $name
          : $defaultValue;
        }
      // else if is already in $item, so we don't touch it
      }
    $ret[$name] = $item;
    }
  return $ret;
}

/**
 * Load a form_values array into a form used by QBF.
 *
 * This is typically useful when loading saved queries using qbf_load().
 * For other cases, the mechanisms built within FAPI should be used instead.
 *
 * @see qbf_load()
 *
 * @ingroup forms
 * @param array $form
 * @param array $form_values
 * @return array The modified form
 */
function qbf_import_values($element, $form_values) {
  foreach (element_children($element) as $childName)
    {
    if (!empty($form_values[$childName]))
      {
      $element[$childName]['#qbf']['#default_value'] = $form_values[$childName];
      }
    $element[$childName] = qbf_import_values($element[$childName], $form_values);
    }
  return $element;
}

/**
 * Load a saved QBF query.
 *
 * @see qbf_import_values()
 *
 * @param int $qid
 * @return array A form_values array usable by qbf_import_values
 */
function qbf_load($qid) {
  $sq = 'SELECT qq.qid, qq.uid, qq.query '
      . 'FROM {%s} qq '
      . 'WHERE qq.qid = %d ';
  // db_rewrite_sql does not apply here until we add more advanced support for access control
  $q = db_query($sq, QBF_TABLE_NAME, $qid);
  $ret = db_fetch_object($q); // 0 or 1 row: we are querying on the primary key
  if ($ret === FALSE)
    {
    $ret = NULL;
    }
  else
    {
    $ret->query = unserialize($ret->query);
    }
  return $ret;
}

/**
 * Submit handler for query save form.
 *
 * @ingroup forms
 * @param $form_id string
 * @param $form_values array
 * @return string
 */
function qbf_submit($form_id, $form_values) {
  switch ($form_values['op'])
    {
    case t('Search'):
      $ret = FALSE;
      break;
    case t('Save query'):
      _qbf_save($form_id, $form_values);
      drupal_set_message(t('Your query was saved as "@name".',
        array('@name' => $form_values['save-name'])));
      global $user;
      $ret = "user/$user->uid/edit/job";;
      break;
    }
  //dsm(array('QS' => $form_values));
  return $ret;
}

/**
 * List queries owned by a given user.
 *
 * @param int $uid > 0
 * @return array
 */
function qbf_get_queries_by_user($uid) {
  $sq = 'SELECT qq.qid, qq.uid, qq.name, qq.query '
      . 'FROM {%s} qq '
      . 'WHERE qq.uid = %d '
      . 'ORDER BY qq.name ';
  $q = db_query($sq, QBF_TABLE_NAME, $uid);
  $ret = array();
  while ($o = db_fetch_object($q))
    {
    $ret[$o->qid] = $o; // qid is the PK, so it is present and unique
    }
  return $ret;
}

/**
 * Save a query and return its qid.
 *
 * @ingroup forms
 * @param $form_id string
 * @param $form_values array
 * @return int
 */
function _qbf_save($form_id, $form_values) {
  global $user;

  if ($user->uid == 0)
    {
    $warning = t('Attempt by anonymous user to save a QBF query. Should not happen.');
    drupal_set_message($warning, 'error');
    watchdog('qbf', $warning, WATCHDOG_WARNING);
    $ret = 0;
    }
  else
    {
    $sq = 'INSERT INTO {%s} (qid, uid, name, query) '
         ."VALUES           (%d,  %d,  '%s', '%s' ) ";
    $ret = db_next_id('qbf_qid');
    $q = db_query($sq, QBF_TABLE_NAME, $ret, $user->uid, $form_values['save-name'], serialize($form_values));
    }

  return $ret;
}

error_reporting($_qbf_er);