<?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-2009 Ouest Systemes Informatiques (OSInet)
 * @author Frederic G. MARAND
 * @license Licensed under the CeCILL 2.0 and the General Public Licence version 2 or later
 * @package QBF
 */

// $Id: qbf.module,v 1.9.4.11 2009-03-22 14:30:41 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.
 *
 * It MUST be a single component path, without a "/", otherwise qbf_menu() will
 * need to be changed.
 *
 * @ingroup paths
 * @see qbf_menu()
 */
define('QBF_PATH_MAIN',              'qbf');
/**
 * The QBF autocomplete path for search fields
 * @ingroup paths
 */
define('QBF_PATH_AC',                'qbf/ac');
/**
 * The path to the QBF settings page
 */
define('QBF_PATH_SETTINGS',          'admin/settings/qbf');

/**
 * 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');

/**
 * Notify owner about saved query deletions, variable name.
 *
 * @ingroup vars
 */
define('QBF_VAR_NOTIFY_DELETE',      'qbf_notify_delete');

/**
 * Name of the profile category under which the list of saved queries will be
 * displayed.
 *
 * @ingroup vars
 *
 * @see qbf_admin_settings(), qbf_profile_alter()
 */
define('QBF_VAR_PROFILE_CATEGORY',   'qbf_profile_category');

/**
 * Notify owner about saved query deletions, default value.
 *
 * @ingroup vars
 */
define('QBF_DEF_NOTIFY_DELETE',      FALSE);

/**
 * Default value for the profile category
 *
 * @ingroup vars
 *
 * See QBF_VAR_PROFILE_CATEGORY
 */
define('QBF_DEF_PROFILE_CATEGORY',     'Saved queries');

/**
 * A class wrapper for saved QBF queries
 */
class Qbf_Query
  {
  public $qid;
  public $uid;
  public $name;
  public $type;
  public $query;
  public $created;
  public $updated;

  /**
   * Constructor
   *
   * @param string $name
   * @param array $ar_values
   * @return void
   */
  public function __construct($type, $name = NULL, $ar_values = NULL)
    {
    global $user;
    $this->qid = 0; // will be autoset by the DB serial
    $this->uid = $user->uid;
    $this->type = $type;
    $this->name = $name;
    if (!empty($ar_values))
      {
      $this->query = serialize($ar_values);
      }
    $this->created = $this->updated = time();
    }

  /**
   * Save a named query to the DB, erasing previous homonym queries is any exists.
   *
   * @return int
   */
  public function save()
    {
    // Avoid duplicates
    if (!empty($this->name))
      {
      $sq = "DELETE FROM {%s} WHERE name = '%s' AND uid = '%d' ";
      db_query($sq, QBF_TABLE_NAME, $this->name, $this->uid);
      // $n = db_affected_rows(); // Know how many homonym queries we deleted
      }

    $ret = drupal_write_record(QBF_TABLE_NAME, $this); // no update param: we just deleted the previous version
    if ($ret) // has to be SAVED_NEW, by construction
      {
      $ret = $this->qid; // from serial
      }
    return $ret;
    }
  }

/**
 * 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($element_id, $form, $form_values)
  {
  // Elements which are unnamed (form), removed, or display-only have no place in the query
  if (!empty($element_id) && array_key_exists('#qbf', $form) && array_key_exists('#level', $form['#qbf'])
    && $form['#qbf']['#level'] >= QBF_LEVEL_OPTIONAL)
    {
    $ret = array($element_id => $form_values[$element_id]);
    }
  else
    {
    $ret = array();
    }

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

  return $ret;
  }

/**
 * Submit handler for qbf_form, Perform search button.
 *
 * @param array $form
 * @param array $form_state
 */
function _qbf_form_perform_submit($form, &$form_state)
  {
  // dsm($form);
  // dsm($form_state);
  $callback = $form_state['values']['qbf_query'];
  if (function_exists(($callback)))
    {
    $ar_query = _qbf_extract_query(NULL, $form, $form_state['values']);
    $form_state['qbf_results'] = $callback($ar_query);
    }
  $form_state['rebuild'] = TRUE;
  }

/**
 * Validate handler for qbf_form, Perform search button.
 *
 * @param array $form
 * @param array $form_state
 */
//function _qbf_form_perform_validate($form, &$form_state)
//  {
//  // @todo validate searches: checkboxes sets needs at least one value checked, otherwise there won't be any result
//  }

/**
 * Submit handler for qbf_form, Save search button.
 *
 * @param array $form
 * @param array $form_state
 * @return integer
 *   The id of the saved query.
 */
function _qbf_form_save_submit($form, &$form_state)
  {
  $qid = _qbf_save($form_state['values']['form_id'], $form_state);
  drupal_set_message(t('Your query was saved as "@name".',
    array('@name' => $form_state['values']['qbf_save_name'])));
  global $user;
  $form_state['redirect'] = "user/$user->uid/edit/qbf";
  return $qid;
  }

/**
 * Validate handler for qbf_form, Save search button.
 *
 * @param array $form
 * @param array $form_state
 */
//function _qbf_form_save_validate($form, &$form_state)
//  {
//  // @todo validate saves. Check whether any validation is necessary.
//  }

/**
 * Return the human-readable for a query type.
 *
 * @param string $query_type
 * @return string
 */
function _qbf_get_name_from_type($query_type)
  {
  static $labels = array();

  if (empty($labels) || empty($labels[$query_type]))
    {
    $ar_forms = qbf_forms();
    foreach ($ar_forms as /* $qbf_form_id => */ $form_info)
      {
      if ($query_type == $form_info['callback arguments'][0]['form'])
        {
        $labels[$query_type] = $form_info['callback arguments'][0]['label'];
        break;
        }
      }
    }
  return $labels[$query_type];
  }

/**
 * Delete a query by qid
 *
 * In the qbf/<qid>/delete case, $query has been tested for validity and access
 * in qbf_query_load(), so it is safe and accessible.
 *
 * Outside this context, the function can also be invoken with just a qid, and
 * the same check via qbf_query_load() will be performed.
 *
 * @param mixed $query
 *   int or object
 */
function _qbf_query_delete($query)
  {
  global $user;

  if (is_int($query))
    {
    $query = qbf_query_load($query);
    }

  if ($query) // access already checked in explicit or implicit qbf_query_load
    {
    $qid = $query->qid;
    $sq = 'DELETE FROM %s WHERE qid = %d ';
    db_query($sq, QBF_TABLE_NAME, $qid);
    $message = t('Query @id "@name" has been deleted.', array
      (
      '@id'   => $qid,
      '@name' => $query->name,
      ));
    drupal_set_message($message, 'status');
    $link = l($qid, QBF_PATH_MAIN .'/'. $qid .'/delete');
    $notify = variable_get(QBF_VAR_NOTIFY_DELETE, QBF_DEF_NOTIFY_DELETE);

    watchdog('qbf', $message, NULL, WATCHDOG_NOTICE, $link);
    // access check: we only send the message to the query owner, so access is
    // granted without an additional check
    if ($notify /* && $query->uid != $user->uid */)
      {
      $owner = user_load(array('uid' => $query->uid));
      $account = user_load(array('uid' => $query->uid));
      $language = user_preferred_language($account);
      $params = array
        (
        'query'    => $query,
        'owner'    => $owner, // unused by default, but can be used in a hook_mail_alter() implementation
        'deletor'  => $user,
        'language' => $language,
        );
      /* $ret = */ drupal_mail('qbf', __FUNCTION__, $user->mail, $language, $params, $user->mail);
      drupal_set_message(t('User !link has been informed', array
        (
        '!link' => l($account->name, 'user/'. $query->uid),
        )));
      // dsm(array("QQD, ret" => $ret));
      }
    }
  else {
    $message = t('Failed attempt to delete query @qid. Administrator has been alerted.', array
      (
      '@qid' => $qid,
      ));
    drupal_set_message($message, 'error');
    watchdog('qbf', $message, NULL, WATCHDOG_ERROR, $link);
    }
  drupal_goto();
}

/**
 * Main query page.
 *
 * This returns the query form if a valid query id or query type is specified,
 * or the list of available query types if several exisit, or jumps to the single
 * available query type if only one exists.
 *
 * @param object $query
 *   Valid query, loaded by qbf_query_load().
 * @return string
 */
function _qbf_query_form($query = NULL )
  {
  if (!empty($query))
    {
    $qbf_form_id = 'qbf_' . $query->type;
    $ret = drupal_get_form($qbf_form_id, $query);
    }
  else
    {
    $ar_forms = qbf_forms();
    $arRet = array();
    foreach ($ar_forms as $qbf_form_id => $form_info)
      {
      $form_id = $form_info['callback arguments'][0]['form'];
      $arRet[QBF_PATH_MAIN . "/$form_id"] = l($form_info['callback arguments'][0]['label'],
          QBF_PATH_MAIN . "/$form_id");
      }

    // If there is only one form type, no need to ask the user.
    if (count($arRet) == 1)
      {
      reset($arRet);
      drupal_goto(key($arRet));
      }
    else
      {
      $ret = theme('item_list', $arRet, t('Choose a query type'));
      }
    }
  return $ret;
  }

/**
 * Save a query and return its qid.
 *
 * This is not a hook_save() implementation, hence the "_".
 *
 * @ingroup forms
 *
 * @param $form_id string
 * @param $form_state array
 * @return int
 */
function _qbf_save($form_id, $form_state)
  {
  if (user_is_anonymous())
    {
    $warning = t('Attempt by anonymous user to save a QBF query. Should not happen.');
    drupal_set_message($warning, 'error');
    watchdog('qbf', $warning, NULL, WATCHDOG_WARNING);
    $ret = 0;
    }
  else
    {
    // @FIXME check whether form_state is now needed. It wasn't in QBF for D5
    $form = drupal_retrieve_form($form_id, $form_state);
    // dsm($form, "retrieve");
    drupal_prepare_form($form_id, $form, $form_state);
    // dsm($form, "prepare");
    $name = $form_state['values']['qbf_save_name'];
    $type = $form_state['values']['qbf_save_type'];
    // dsm($form_state);
    $form_values = _qbf_extract_query(NULL, $form, $form_state['values']);
    // dsm($form_values);
    $ar_values = array();
    foreach ($form_values as $key => $value)
      {
      if (empty($value))
        {
        continue;
        }
      $ar_values[$key] = $value;
      }

    $query = new Qbf_Query($type, $name, $ar_values);
    $ret = $query->save();
    }

  return $ret;
  }

/**
 * 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($key, $element, $form_state, $query)
  {
  // dsm(array('key' => $key, 'element' => $element));

  /**
   * List default type transformations applied to widget by FAPI.
   * Types without a default transformation are not transformed
   */
  static $ar_default_type_transformations = array
    (
    'button'         => NULL, // no content
    'file'           => NULL, // non-querable (yet ?)
    'image_button'   => NULL, // new in D6
    'markup'         => NULL, // no content
    'password'       => NULL, // forbidden
    'radio'          => NULL, // single radio is useless, unlike a set of them
    'submit'         => NULL, // no content
    'textarea'       => 'textfield', // reduce text for searches

    // Don't transform these:
    // 'checkbox'       => NULL,
    // 'checkboxes'     => NULL,
    // 'date'           => NULL,
    // 'fieldset'       => NULL, // useful visually
    // 'form'           => NULL, // removing it would delete the whole shebang
    // 'hidden'         => NULL, // non-querable visually, but may be useful
    // 'item'           => NULL,
    // 'radios'         => NULL,
    // 'select'         => NULL,
    // 'textfield'      => NULL,
    // 'value'          => 'value',
    // 'weight'         => NULL,
    );

    /**
     * List default property transformations applied to widget by FAPI property.
     *
     * Properties without a default transformation are not transformed
     */
    static $ar_default_property_transformations = array
      (
      // Standard properties
      '#action'        => NULL,
      '#after_build'   => NULL,
      // '#base'          => NULL, // gone in D6
      '#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 $ar_killer_properties = array
      (
      '#disabled'      => TRUE,
      );

    // Transform type
    $source_type = $element['#type'];

    // .. Default transformation
    $dest_type = array_key_exists($source_type, $ar_default_type_transformations)
      ? $ar_default_type_transformations[$source_type]
      : $source_type;

    // .. Apply form-defined type override
    if (isset($element['#qbf']['#type']))
      {
      $dest_type = $element['#qbf']['#type'];
      }

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

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

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

      if (isset($form_state['values'][$key]))
        {
        $ret['#default_value'] = $form_state['values'][$key];
        }
      elseif (isset($query->query[$key]))
        {
        $ret['#default_value'] = $query->query[$key];
        }

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

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

/**
 * Implement the former hook_settings().
 *
 * @return array
 */
function qbf_admin_settings()
  {
  $form = array();
  $form[QBF_VAR_NOTIFY_DELETE] = array
    (
    '#type'          => 'checkbox',
    '#default_value' => variable_get(QBF_VAR_NOTIFY_DELETE, QBF_DEF_NOTIFY_DELETE),
    '#title'         => t('Notify users when one of their saved searches has been deleted'),
    );

  $form['queries'][QBF_VAR_PROFILE_CATEGORY] = array
    (
    '#type'          => 'textfield',
    '#title'         => t('Name of profile category'),
    '#description'   => t('Choose a title for the section of the user profiles where the list of search queries will be displayed. It may match an existing profile category.'),
    '#default_value' => variable_get(QBF_VAR_PROFILE_CATEGORY, QBF_DEF_PROFILE_CATEGORY),
    );

  return system_settings_form($form);
  }

/**
 * The QBF form builder.
 *
 * @param array $form_state
 * @param array $query_info
 *   The query structure array
 * @param string $qbf_form_id
 *   The name of the QBF form
 * @param string $query
 *   The saved query.
 */
function qbf_form(&$form_state, $query_info, $qbf_form_id, $query = NULL)
  {
  $form_id = $query_info['form'];

  // Fetch the basic form and rename it, passing it the previous values
  $node = new stdClass();

  $form = $form_id($node, $form_state);

  $qbf_form = array();
  $qbf_form['#qbf_source_form_id'] = $form_id;

  // On the form element itself, only keep the QBF properties and the children
  if (in_array('#qbf', element_properties($form)))
    {
    $qbf_form += $form['#qbf'];
    }

  foreach (element_children($form) as $key)
    {
    $new_element = _qbf_transform($key, $form[$key], $form_state, $query);
    if (!is_null($new_element))
      {
      $qbf_form[$key] = $new_element;
      }
    }

  $qbf_form['#id'] = $qbf_form_id;

  $qbf_form['qbf'] = array
    (
    '#type'   => 'fieldset',
    '#title'  => t('Query'),
    );

  if (isset($form_state['values']) && !empty($form_state['values']))
    {
    if (isset($form_state['qbf_results']))
      {
      $qbf_form['qbf']['qbf_results'] = array
        (
        '#type'    => 'markup',
        '#prefix'  => '<p>',
        '#value'   => $form_state['qbf_results'],
        '#suffix'  => '</p>',
        );
      }
    }

  $qbf_form['qbf']['qbf_save_type'] = array
    (
    '#type'          => 'hidden',
    '#value'         => $query_info['form'],
    );

  $qbf_form['qbf']['qbf_query'] = array
    (
    '#type'          => 'hidden',
    '#value'         => $query_info['callback'],
    );

  $qbf_form['qbf']['qbf_save_name'] = array
    (
    '#title'         => t('Name of query in your save list'),
    '#type'          => 'textfield',
    '#required'      => TRUE,
    '#default_value' => empty($query->name)
      ? t('@label - @time', array('@label' => $query_info['label'], '@time' => format_date(time(), 'large')))
      : $query->name,
    );

  $qbf_form['qbf']['qbf_perform'] = array
    (
    '#submit'        => array('_qbf_form_perform_submit'),
    '#validate'      => array('_qbf_form_perform_validate'),
    '#type'          => 'submit',
    '#value'         => t('Perform query'),
    );

  $qbf_form['qbf']['qbf_save'] = array
    (
    '#submit'        => array('_qbf_form_save_submit'),
    '#validate'      => array('_qbf_form_save_validate'),
    '#type'          => 'submit',
    '#value'         => t('Save query'),
    );

  return $qbf_form;
  }

/**
 * Implement hook_forms().
 *
 * @link http://drupal.org/node/144132#hook-forms @endlink
 *
 * hook_qbf_register() returns an array of QBF-able node types, indexed by the
 * node type, with the following properties:
 * - form: the name of the hook_form() implementation (a $form_id)
 * - label: the human-readable type name under which the queries are saved by QBF
 * - callback: the function QBF must invoke to query the node type. It will
 *   receive the query type and a filtered version of $form_state['values']
 *   containing only valid node fields, and must return a themed grid of query
 *   results, which will be displayed as a #markup FAPI element. In advanced
 *   uses, a single callback can be used for several query types by using the
 *   query type parameter to know what the values apply to.
 *
 * @ingroup forms
 * @ingroup hooks
 *
 * @param array $args
 * @return array
 */
function qbf_forms(/* $args = NULL */)
  {
  static $forms = array();

  if (empty($forms))
    {
    $hook_name = 'qbf_register';
    // dsm(array("QBF_forms $qbf_form_id" => $args));

    // More efficient than using module_invoke_all: we avoid array-merging + re-looping
    foreach (module_implements($hook_name) as $module)
      {
      $arImplementations = module_invoke($module, $hook_name);
      // dsm($arImplementations);
      foreach ($arImplementations as /* $node_type => */ $query_info)
        {
        $qbf_form_id = 'qbf_' . $query_info['form'];
        $forms[$qbf_form_id] = array
          (
          'callback'           => 'qbf_form',
          'callback arguments' => array($query_info, $qbf_form_id),
          );
        } // foreach implementation
      } // foreach module
    } // if empty

  return $forms;
  }

/**
 * List queries owned by a given user.
 *
 * @param int $uid > 0
 * @return array
 */
function qbf_get_queries_by_user($uid = NULL)
  {
  if (is_null($uid))
    {
    global $user;
    $uid = $user->uid;
    }

  $sq = 'SELECT qq.qid, qq.uid, qq.type, qq.name, qq.query, qq.updated '
      . 'FROM {%s} qq '
      . 'WHERE qq.uid = %d '
      . 'ORDER BY qq.type, qq.name ';
  // no db_rewrite_sql: this function is not in a menu callback, so it is up to
  // the caller to check access
  $q = db_query($sq, QBF_TABLE_NAME, $uid);
  $ret = array();
  while (is_object($o = db_fetch_object($q)))
    {
    $ret[$o->qid] = $o; // qid is the PK, so it is present and unique
    }
  return $ret;
  }

/**
 * Implement hook_mail().
 *
 * @param string $key
 * @param array $message
 * @param array $params
 * @return void
 */
function qbf_mail($key, &$message, $params)
  {
  // dsm(array('QBF_mail key' => $key, 'message' => $message, 'params' => $params));

  $deletor_tokens = user_mail_tokens($params['deletor'], $params['language']->language);
  $tokens = array_merge($deletor_tokens, array
    (
    '!qname' => $params['query']->name,
    '!qid'   => $params['query']->qid,
    ));

  $message['subject'] = t('Effacement d\'une recherche !site enregistrée', $tokens);
  $message['body'] = t("!date\n\nVotre recherche !qid: !qname\nsur le site !site vient d'être effacée par !username.", $tokens);
  }

/**
 * Implement hook_menu().
 *
 * @return array
 */
function qbf_menu()
  {
  $items = array();

  $items[QBF_PATH_SETTINGS] = array
    (
    'title'            => 'Query-By-Form',
    'access arguments' => array(QBF_PERM_ADMIN),
    'page callback'    => 'drupal_get_form',
    'page arguments'   => array('qbf_admin_settings'),
    'type'             => MENU_NORMAL_ITEM,
    );

  $items[QBF_PATH_MAIN] = array
    (
    'type'             => MENU_CALLBACK,
    'access arguments' => array(QBF_PERM_QUERY),
    'page callback'    => '_qbf_query_form',
    );

  $items[QBF_PATH_MAIN . '/%qbf_query'] = array
    (
    'type'             => MENU_CALLBACK,
    'access arguments' => array(QBF_PERM_QUERY),
    'page callback'    => '_qbf_query_form',
    'page arguments'   => array(1),
    );

  $items[QBF_PATH_MAIN . '/%qbf_query/delete'] = array
    (
    'type'             => MENU_CALLBACK,
    'access arguments' => array(QBF_PERM_QUERY),
    'page callback'    => '_qbf_query_delete',
    'page arguments'   => array(1),
    );

  return $items;
  }

/**
 * Implement hook_perm().
 *
 * @todo D7: Format will change
 * @see http://drupal.org/node/224333#descriptions-permissions
 *
 * @ingroup hooks
 * @return array
 */
function qbf_perm()
  {
  $ret = array
    (
    QBF_PERM_ADMIN,
    QBF_PERM_QUERY,
    );

  return $ret;
  }

/**
 * Load a saved QBF query, or an empty query by type
 *
 * @link http://drupal.org/node/109153#load @endlink
 *
 * @param int $us_qid
 * @return array
 *   A form_values array
 */
function qbf_query_load($us_qid)
  {
  static $query = NULL;

  // Only allow query loading by logged-in users
  if (user_is_anonymous())
    {
    return FALSE;
    }

  // Filter out visibly invalid values
  $qid = (is_numeric($us_qid) && ($us_qid > 0))
    ? $us_qid
    : 0;

  // If this is not a saved query, it may be a QBF query type
  if ($qid === 0)
    {
    $ar_forms = qbf_forms();
    foreach ($ar_forms as /* $qbf_form_id => */ $form_info)
      {
      if ($us_qid === $form_info['callback arguments'][0]['form'])
        {
        $query = new Qbf_Query($us_qid);
        break;
        }
      }
    }

  if (is_null($query) && $qid)
    {
    $sq = 'SELECT qq.qid, qq.uid, qq.type, qq.name, qq.query '
        . 'FROM {%s} qq '
        . 'WHERE qq.qid = %d ';
    // db_rewrite_sql does not apply here: access control is further down
    $q = db_query($sq, QBF_TABLE_NAME, $qid);
    $query = db_fetch_object($q); // 0 or 1 row: we are querying on the primary key
    if ($query !== FALSE)
      {
      $query->query = unserialize($query->query);
      // dsm($query);
      }
    }

  global $user;
  $ret = (isset($query) && (($query->uid == $user->uid) || user_access(QBF_PERM_ADMIN)))
    ? $query
    : FALSE;

  return $ret;
  }

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

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

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

/**
 * Implement hook_user().
 *
 * Display job searchs as an account form category
 *
 * Edit and account could be passed by reference, but are currently not modified.
 *
 * @ingroup hooks
 *
 * @param string $op
 * @param array &$edit
 * @param array $account
 * @param string $category
 * @return array|void
 */
function qbf_user($op, $edit, $account, $category = NULL)
  {
  $qbf_category = variable_get(QBF_VAR_PROFILE_CATEGORY, QBF_DEF_PROFILE_CATEGORY);

  // dsm("hook user($op, edit, $account->uid = $account->name, $category)");

  switch ($op)
    {
    case 'categories':
      // dsm("hook user($op)");
      $ret = array();
      $ret[] = array
        (
        'name'    => 'qbf',
        'title'   => $qbf_category,
        'weight'  => 2,
        );
      break;

//    case 'view':
//      // Only allow field to QBF admins and own user
//      if ($user->uid != $account->uid && !user_access(QBF_PERM_ADMIN))
//        {
//        return;
//        }
//
//      $account->content['queries'] = array
//        (
//        '#type'    => 'user_profile_category',
//        '#title'   => t('Saved job/internship queries'),
//        // '#class'   => "job-user-$category",
//        );
//      $account->content['queries']['list'] = array
//        (
//        '#type'    => 'user_profile_item',
//        '#title'   => t('List of searches'),
//        '#value'   => '<p>Would appear here</p>',
//        );
//      $none_message = ($account->uid == $user->uid)
//        ? t('None yet. !newQuery', array('!newQuery' => $new_query_link))
//        : t('None yet.');
//      $saved = ($count > 0)
//        ? format_plural($count, 'One saved query. ', '@count saved queries. ')
//          . l(t('View/edit'), "user/$account->uid/edit/qbf")
//        : $none_message;
//      dsm($account->content);
//      break;

    case 'form':
      // dsm("hook user($op, $account->uid = $account->name, $category)");
      if ($category != 'qbf')
        {
        $ret = NULL;
        break;
        }

      // No access control: it is already controlled by the
      // "administer users" permission in user.module
      $ar_queries = qbf_get_queries_by_user($account->uid);
      $count = count($ar_queries);

      $ar_header = array
        (
        t('Query type'),
        t('Query title'),
        t('Saved on'),
        // t('# results'),
        t('Actions'),
        );
      $ar_data = array();
      foreach ($ar_queries as $query)
        {
        $ar_data[] = array
          (
          _qbf_get_name_from_type($query->type),
          l($query->name, QBF_PATH_MAIN . "/$query->qid"),
          format_date($query->updated, 'small'),
          // t('n.a.'),
          l(t('Delete'), QBF_PATH_MAIN ."/$query->qid/delete", array('query' => "destination=user/$account->uid/edit/qbf")),
          );
        }
      $data = theme('table', $ar_header, $ar_data);

      $max_count = variable_get(JOB_VAR_MAX_QUERIES, JOB_DEF_MAX_QUERIES);
      if ($count < $max_count)
        {
        $new_query_link = l(t('Create new query'), QBF_PATH_MAIN);
        $data .= format_plural($max_count - $count,
          '<p>You may still save one more query.',
          '<p>You may still save @count more queries.');
        $data .= " $new_query_link.</p>";
        }
      else
        {
        $data .= t("<p>You have reached the maximum number of saved queries (@count).</p>",
          array('@count' => $count));
        }

      /**
       * A first-level form element is needed by contrib module profile_privacy,
       * at least in version 5.x-1.2 and 6.x-1-2. This mimics the
       * user_profile_category/user_profile_item scheme provided by profile.module.
       * We do not use these types, in order to use the full display area width
       * for the queries table.
       */
      $ret['job'] = array
        (
        '#type' => 'markup', // in profile, usually user_profile_category
        '#title' => NULL,
        );
      $ret['job']['queries'] = array
        (
        '#type'  => 'markup', // in profile, usually user_profile_item
        '#value' => $data,
        );
      break;
    }

  return $ret;
  }

error_reporting($_qbf_er);