Jelajahi Sumber

- porting nodes from Deadwood module inserted in-place
- new Qbf_Query class to wrap query saves: properties, __construct(), save()
- restored OSInet coding style to ported functions
- _qbf_query_delete(), _qbf_transform_element(), qbf_query_mapper() ported
- qbf_admin_settings(), qbf_forms(), qbf_get_queries_by_user(), qbf_menu(), qbf_perm(),
- new qbf_mail() introduced for _qbf_query_delete() notifications
- qbf_query_load() ported and now doubles as a path component loader
- _qbf_save() modified to use Qbf_Query class for storage, still needs work on the form part

Frederic G. Marand 15 tahun lalu
induk
melakukan
abb70643b5
1 mengubah file dengan 472 tambahan dan 443 penghapusan
  1. 472 443
      qbf.module

+ 472 - 443
qbf.module

@@ -12,100 +12,7 @@
  * @package QBF
  */
 
-// $Id: qbf.module,v 1.9.4.2 2009-03-17 12:59:28 marand Exp $
-
-// ======== D6 LIMIT ==================================================================================================
-/* TODO New hook_mail implementation
-   Because of changes to drupal_mail function, you need to move the variables
-   setup and string replace commands into the hook_mail implementation and then
-   call drupal_mail with the name of the module which contains this
-   implementation, the mailkey, the recipient, the language of the user the mail
-   goes to and some arbitrary parameters. */
-
-/* TODO db_next_id() is gone, and replaced as db_last_insert_id()
-   Since db_next_id() introduce some problems, and the use of this function
-   can be replaced by database level auto increment handling, db_next_id()
-   is now gone and replaced as db_last_insert_id() with help of serial type
-   under Schema API (check out http://drupal.org/node/149176 for more details).
-   Please refer to drupal_write_record() as demonstration. */
-
-/* TODO FormAPI image buttons are now supported.
-   FormAPI now offers the 'image_button' element type, allowing developers to
-   use icons or other custom images in place of traditional HTML submit buttons.
-
-$form['my_image_button'] = array(
-  '#type'         => 'image_button',
-  '#title'        => t('My button'),
-  '#return_value' => 'my_data',
-  '#src'          => 'my/image/path.jpg',
-); */
-
-/* TODO Check node access before emailing content
-   Modules like Organic Groups and Project Issue send the same content as an
-   email notifications to many users. They should now be using the new 3rd
-   parameter to node_access() to check access on the content before emailing it.
-   Note that db_rewrite_sql() provodes no protection because the recipient is not
-   the logged in user who is receiving the content. */
-
-/* TODO hook_user('view')
-   The return value of hook_user('view') has changed, to match the process that
-   nodes use for rendering. Modules should add their custom HTML to
-   $account->content element. Further, this HTML should be in the form that
-   drupal_render() recognizes. */
-
-/* TODO Node previews and adding form fields to the node form.
-   There is a subtle but important difference in the way node previews (and other
-   such operations) are carried out when adding or editing a node. With the new
-   Forms API, the node form is handled as a multi-step form. When the node form
-   is previewed, all the form values are submitted, and the form is rebuilt with
-   those form values put into $form['#node']. Thus, form elements that are added
-   to the node form will lose any user input unless they set their '#default_value'
-   elements using this embedded node object. */
-
-/* TODO New user_mail_tokens() method may be useful.
-   user.module now provides a user_mail_tokens() function to return an array
-   of the tokens available for the email notification messages it sends when
-   accounts are created, activated, blocked, etc. Contributed modules that
-   wish to make use of the same tokens for their own needs are encouraged
-   to use this function. */
-
-/* TODO
-   There is a new hook_watchdog in core. This means that contributed modules
-   can implement hook_watchdog to log Drupal events to custom destinations.
-   Two core modules are included, dblog.module (formerly known as watchdog.module),
-   and syslog.module. Other modules in contrib include an emaillog.module,
-   included in the logging_alerts module. See syslog or emaillog for an
-   example on how to implement hook_watchdog.
-function example_watchdog($log = array()) {
-  if ($log['severity'] == WATCHDOG_ALERT) {
-    mysms_send($log['user']->uid,
-      $log['type'],
-      $log['message'],
-      $log['variables'],
-      $log['severity'],
-      $log['referer'],
-      $log['ip'],
-      format_date($log['timestamp']));
-  }
-} */
-
-/* TODO Implement the hook_theme registry. Combine all theme registry entries
-   into one hook_theme function in each corresponding module file.
-function qbf_theme() {
-  return array(
-  );
-}; */
-
-
-/* TODO
-   An argument for replacements has been added to format_plural(),
-   escaping and/or theming the values just as done with t().*/
-
-/* TODO $form['#base'] is gone
-   In FormAPI, many forms with different form_ids can share the same validate,
-   submit, and theme handlers. This can be done by manually populating the
-   $form['#submit'], $form['#validate'], and $form['#theme'] elements with
-   the proper function names. */
+// $Id: qbf.module,v 1.9.4.3 2009-03-17 18:48:53 marand Exp $
 
 /**
  * Saved error reporting level.
@@ -181,52 +88,155 @@ define('QBF_VAR_NOTIFY_DELETE',      'qbf_notify_delete');
  */
 define('QBF_DEF_NOTIFY_DELETE',      FALSE);
 
+class Qbf_Query
+  {
+  public $qid;
+  public $uid;
+  public $name;
+  public $query;
+  public $created;
+  public $updated;
+
+  /**
+   * Constructor
+   *
+   * @param string $name
+   * @param array $ar_values
+   * @return void
+   */
+  public function __construct($name, $ar_values)
+    {
+    global $user;
+    $this->qid = 0; // will be autoset by the DB serial
+    $this->uid = $user->uid;
+    $this->name = $name;
+    $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'";
+      db_query($sq, QBF_TABLE_NAME, $name);
+      }
+
+    $ret = drupal_write_record(QBF_TABLE_NAME, $this); // no update param: we just deleted the previous version
+    dsm($this);
+    if ($ret) // has to be SAVED_NEW, by construction
+      {
+      $ret = $this->qid; // from serial
+      }
+    return $ret;
+    }
+  }
+
 /**
- * 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.
+ * Recursively build a query array from the form and its values
  *
- * Any additional parameter passed to the function is transmitted to the form
- * generating function.
+ * In the current version, element names are supposed to be unique, even at
+ * different levels in the tree.
  *
  * @ingroup forms
- * @param string $form_id
- * @return array
+ * @param array $form
+ * @param array $form_values
  */
-function qbf_transform_form($form_id) {
-// @todo GW:  function qbf_transform_form(&$form_state, $form_id) {
+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();
+    }
 
-  $ar_args = func_get_args();
-//dsm(array('qtf' => $ar_args));
-  // Fetch the basic form and rename it, passing it the caller's arguments
-  $form = call_user_func_array('drupal_retrieve_form', $ar_args);
-  $new_form_id = "qbf_$form_id";
+  // QBF level is not inherited, so this loop is outside the "if" above
+  foreach (element_children($form) as $child_name)
+    {
+    $ret += _qbf_extract_query($form[$child_name], $form_values);
+    }
 
-  // Only keep the children of the form and QBF properties on the form itself
-  $elements = array();
-  $new_form = array();
-  $new_form['#qbf_source_form_id'] = $form_id;
-  if (in_array('#qbf', element_properties($form))) {
-    $new_form += $form['#qbf'];
+  return $ret;
   }
 
-  foreach (element_children($form) as $key) {
-    // dsm("Transforming $key, type " . $form[$key]['#type']);
-    $new_element = _qbf_transform_element($key, $form[$key]);
-    if (!is_null($new_element)) {
-      $new_form[$key] = $new_element;
+/**
+ * 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);
     }
-  }
 
-  $new_form['#id'] = $new_form_id;
-  $new_form['#multistep'] = TRUE;
-  // Do not set #redirect, even to FALSE (submit handlers)
-  // $new_form['#redirect']  = FALSE;
-  $new_form['#after_build'][] = 'qbf_after_build';
-  $new_form['#submit'] = array('qbf_submit' => array());
-// dsm($new_form);
-  return $new_form;
+  if ($query) // access already checked in explicit or implicit qbf_query_load
+    {
+    $qid = $query->qid;
+    $sq = 'DELETE FROM %s WHERE qid = %d ';
+    $q = 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));
+      $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();
 }
 
 /**
@@ -253,15 +263,28 @@ function _qbf_transform_element($key, $element) {
    */
   static $ar_default_type_transformations = array
     (
-    'button'         => NULL,
-    'file'           => NULL,
-    // 'hidden'         => NULL,
-    'markup'         => NULL,
-    'password'       => NULL,
-    'radio'          => NULL,
-    'submit'         => NULL,
-    'textarea'       => 'textfield',
+    '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,
     );
 
   /**
@@ -274,7 +297,7 @@ function _qbf_transform_element($key, $element) {
     // Standard properties
     '#action'        => NULL,
     '#after_build'   => NULL,
-    '#base'          => NULL,
+    // '#base'          => NULL, // gone in D6
     '#button_type'   => NULL,
     '#built'         => NULL,
     '#description'   => NULL,
@@ -306,76 +329,91 @@ function _qbf_transform_element($key, $element) {
     ? $ar_default_type_transformations[$source_type]
     : $source_type;
   // .. Apply form-defined type override
-  if (isset($element['#qbf']['#type'])) {
+  if (isset($element['#qbf']['#type']))
+    {
     $dest_type = $element['#qbf']['#type'];
-  }
+    }
 
-  if (is_null($dest_type)) {
+  if (is_null($dest_type))
+    {
     $ret = NULL;
-  }
-  else {
+    }
+  else
+    {
     $ret = $element;
     $ret['#type'] = $dest_type;
-    if (!array_key_exists('#qbf', $element) || $element['#qbf']['#level'] == QBF_LEVEL_REMOVE) {
+    if (!array_key_exists('#qbf', $element) || $element['#qbf']['#level'] == QBF_LEVEL_REMOVE)
+      {
       $ret = NULL;
-    }
-    else {
-      foreach (element_properties($element) as $property_name) {
+      }
+    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])) {
+          && ($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)) {
+        if (array_key_exists($property_name, $ar_default_property_transformations))
+          {
           $ret[$property_name] = $ar_default_property_transformations[$property_name];
-        }
-        else {
+          }
+        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) {
+        if ($property_name == '#qbf')
+          {
+          foreach ($element[$property_name] as $override_name => $override_value)
+            {
             $ret[$override_name] = $override_value;
+            }
           }
         }
-      }
 
       // Recursively transform children
-      foreach (element_children($element) as $child_name) {
+      foreach (element_children($element) as $child_name)
+        {
         $child = _qbf_transform_element($child_name, $element[$child_name]);
-        if (is_null($child)) {
+        if (is_null($child))
+          {
           unset($ret[$child_name]);
-        }
-        else {
+          }
+        else
+          {
           $ret[$child_name] = $child;
+          }
         }
       }
     }
-  }
 
   //dsm(array('key' => $key, 'transformed element' => $ret));
   return $ret;
-}
+  }
 
 /**
- * Implement hook_perm().
+ * Implement the former hook_settings().
  *
- * @ingroup hooks
  * @return array
  */
-function qbf_perm() {
-  $ret = array
+function qbf_admin_settings()
+  {
+  $form = array();
+  $form[QBF_VAR_NOTIFY_DELETE] = array
     (
-    QBF_PERM_ADMIN,
-    QBF_PERM_QUERY,
+    '#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'),
     );
-
-  return $ret;
-}
+  return system_settings_form($form);
+  }
 
 /**
  * Implement hook_forms().
@@ -384,86 +422,163 @@ function qbf_perm() {
  * @ingroup hooks
  * @return array
  */
-function qbf_forms() {
+function qbf_forms()
+  {
   $hook_name = 'qbf_register';
 
-  foreach (module_implements($hook_name) as $module) {
-    foreach (module_invoke($module, $hook_name) as $form_name) {
+  // More efficient than using module_invoke_all: we avoid array-merging + re-looping
+  foreach (module_implements($hook_name) as $module)
+    {
+    foreach (module_invoke($module, $hook_name) as $form_name)
+      {
       $forms["qbf_$form_name"] = array
         (
         'callback'           => 'qbf_transform_form',
         'callback arguments' => array($form_name),
         );
+      }
     }
-  }
 
   return $forms;
-}
+  }
 
 /**
- * Insert the query results at the bottom of the query form.
+ * List queries owned by a given user.
  *
- * @ingroup forms
- * @param array $form
- * @param array $form_values
+ * @param int $uid > 0
  * @return array
  */
-function qbf_after_build($form, $form_values) {
-  if (empty($form['#post'])) {
-    return $form;
+function qbf_get_queries_by_user($uid = NULL)
+  {
+  if (is_null($uid))
+    {
+    global $user;
+    $uid = $user->uid;
+    }
+
+  $sq = 'SELECT qq.qid, qq.uid, qq.name, qq.query, qq.updated '
+      . 'FROM {%s} qq '
+      . 'WHERE qq.uid = %d '
+      . 'ORDER BY 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 ($o = db_fetch_object($q))
+    {
+    $ret[$o->qid] = $o; // qid is the PK, so it is present and unique
+    }
+  return $ret;
   }
 
-  // If #post is not empty, we are indeed querying
-  $ar_query = _qbf_extract_query($form, $form_values);
+/**
+ * Implement hook_menu().
+ *
+ * @return array
+ */
+function qbf_menu()
+  {
+  $items = array();
 
-  /* 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($ar_query);
+  $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 . '/%qbf_query/delete'] = array
+    (
+    'type'             => MENU_CALLBACK,
+    'access arguments' => array(QBF_PERM_QUERY),
+    'page callback'    => '_qbf_query_delete',
+    'page arguments'   => array(1),
+    );
+
+  return $items;
   }
-  else {
-    drupal_set_message(t('QBF: incorrect callback function for search'), 'error');
+
+/**
+ * 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);
   }
-  $form['qbf_query_results'] = array
+
+/**
+ * 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
     (
-    '#type'    => 'markup',
-    '#value'   => $results,
-    '#weight'  => 10,
+    QBF_PERM_ADMIN,
+    QBF_PERM_QUERY,
     );
-  return $form;
-}
+
+  return $ret;
+  }
 
 /**
- * Recursively build a query array from the form and its values
+ * Load a saved QBF query.
  *
- * In the current version, element names are supposed to be unique, even at
- * different levels in the tree.
+ * @see qbf_import_values()
+ * @link http://drupal.org/node/109153#load @endlink
  *
- * @ingroup forms
- * @param array $form
- * @param array $form_values
+ * @param int $qid
+ * @return array A form_values array usable by qbf_import_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();
-  }
+function qbf_query_load($qid)
+  {
+  static $query = NULL;
+
+  if (is_null($query))
+    {
+    $sq = 'SELECT qq.qid, qq.uid, qq.query, qq.name '
+        . '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
+    // FALSE does not happen: only NULL or a value can be here
+    if ($query !== NULL)
+      {
+      $query->query = unserialize($query->query);
+      //dsm($query);
+      }
+    }
 
-  // QBF level is not inherited, so this loop is outside the "if" above
-  foreach (element_children($form) as $child_name) {
-    $ret += _qbf_extract_query($form[$child_name], $form_values);
-  }
+  global $user;
+  $ret = (isset($query) && isset($query->uid) && (($query->uid == $user->uid) || user_access(QBF_PERM_ADMIN)))
+    ? $query
+    : FALSE;
 
   return $ret;
-}
+  }
 
 /**
  * Provide an optional automatic mapping mechanism for query building.
@@ -472,31 +587,133 @@ function _qbf_extract_query($form, $form_values) {
  * array to complete it in $ar_defaults, and returns a fully built query array
  * ready to be used for querying.
  *
- * @param array $ar_query
+ * @param array $ar_query_map
  * @param array $ar_defaults
  * @return array
  */
-function qbf_query_mapper($ar_queryMap = array(), $ar_defaults = array()) {
+function qbf_query_mapper($ar_query_map = array(), $ar_defaults = array())
+  {
   $ret = array();
 
-  foreach ($ar_queryMap as $name => $value) {
+  foreach ($ar_query_map as $name => $value)
+    {
     // accept NULL, empty strings...
-    if (!is_array($value)) {
+    if (!is_array($value))
+      {
       $value = array();
-    }
+      }
     $item = $value;
 
-    foreach ($ar_defaults as $default_key => $default_value) {
-      if (!array_key_exists($default_key, $item)) {
+    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;
+  }
+
+error_reporting($_qbf_er);
+
+// ======== D6 LIMIT ==================================================================================================
+
+/* TODO Node previews and adding form fields to the node form.
+   There is a subtle but important difference in the way node previews (and other
+   such operations) are carried out when adding or editing a node. With the new
+   Forms API, the node form is handled as a multi-step form. When the node form
+   is previewed, all the form values are submitted, and the form is rebuilt with
+   those form values put into $form['#node']. Thus, form elements that are added
+   to the node form will lose any user input unless they set their '#default_value'
+   elements using this embedded node object. */
+
+/**
+ * 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) {
+// @todo GW:  function qbf_transform_form(&$form_state, $form_id) {
+
+  $ar_args = func_get_args();
+//dsm(array('qtf' => $ar_args));
+  // Fetch the basic form and rename it, passing it the caller's arguments
+  $form = call_user_func_array('drupal_retrieve_form', $ar_args);
+  $new_form_id = "qbf_$form_id";
+
+  // Only keep the children of the form and QBF properties on the form itself
+  $elements = array();
+  $new_form = array();
+  $new_form['#qbf_source_form_id'] = $form_id;
+  if (in_array('#qbf', element_properties($form))) {
+    $new_form += $form['#qbf'];
+  }
+
+  foreach (element_children($form) as $key) {
+    // dsm("Transforming $key, type " . $form[$key]['#type']);
+    $new_element = _qbf_transform_element($key, $form[$key]);
+    if (!is_null($new_element)) {
+      $new_form[$key] = $new_element;
+    }
+  }
+
+  $new_form['#id'] = $new_form_id;
+  $new_form['#multistep'] = TRUE;
+  // Do not set #redirect, even to FALSE (submit handlers)
+  // $new_form['#redirect']  = FALSE;
+  $new_form['#after_build'][] = 'qbf_after_build';
+  $new_form['#submit'] = array('qbf_submit' => array());
+// dsm($new_form);
+  return $new_form;
+}
+
+/**
+ * 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 empty, we are indeed querying
+  $ar_query = _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($ar_query);
+  }
+  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;
 }
 
 /**
@@ -522,35 +739,6 @@ function qbf_import_values($element, $form_values) {
   return $element;
 }
 
-/**
- * Load a saved QBF query.
- *
- * It is not named qbf_load() although this would seem more natural, because a
- * hook_load() exists and this is not an implementation of this hook.
- *
- * @see qbf_import_values()
- *
- * @param int $qid
- * @return array A form_values array usable by qbf_import_values
- */
-function qbf_query_load($qid) {
-  $sq = 'SELECT qq.qid, qq.uid, qq.query, qq.name '
-      .'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
-  // FALSE does not happen
-  if ($ret === NULL) {
-    $ret = NULL;
-  }
-  else {
-    $ret->query = unserialize($ret->query);
-    //dsm($ret);
-  }
-  return $ret;
-}
-
 /**
  * Submit handler for query save form.
  *
@@ -594,205 +782,46 @@ function qbf_submit($form, &$form_state) {
 */
 }
 
-/**
- * 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.name, qq.query, qq.updated '
-      .'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
+ * @param $form_state array
  * @return int
  */
-function _qbf_save($form_id, $form_values) {
-  global $user;
-
-  if ($user->uid == 0) {
+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, WATCHDOG_WARNING);
+    watchdog('qbf', $warning, NULL, WATCHDOG_WARNING);
     $ret = 0;
-  }
-  else {
-/* @TODO GW drupal_retrieve_form() now accepts a form_state parameter.
+    }
+  else
+    {
+    // @FIXME check whether form_state is now needed. It wasn't in QBF for D5
     $form = drupal_retrieve_form($form_id, $form_state);
-*/
-    $form = drupal_retrieve_form($form_id);
-    drupal_prepare_form($form_id, $form);
-    $name = $form_values['save-name'];
-    $form_values = _qbf_extract_query($form, $form_values);
+    dsm($form, "retrieve");
+    drupal_prepare_form($form_id, $form, $form_state);
+    dsm($form, "prepare");
+    $name = $form_state['post']['save-name'];
+    $form_values = _qbf_extract_query($form, $form_state['post']);
     $ar_values = array();
-    foreach ($form_values as $key => $value) {
-      if (empty($value)) {
+    foreach ($form_values as $key => $value)
+      {
+      if (empty($value))
+        {
         continue;
-      }
+        }
       $ar_values[$key] = $value;
-    }
+      }
 
-    // Avoid duplicates
-    if (!empty($name)) {
-      $sq = "DELETE FROM {%s} WHERE name = '%s'";
-      db_query($sq, QBF_TABLE_NAME, $name);
+    $query = new Qbf_Query($name, $ar_values);
+    $ret = $query->save();
     }
 
-    $sq = 'INSERT INTO {%s} (qid, uid, name, query, updated) '
-         ."VALUES           (%d,  %d,  '%s', '%s',  '%d' ) ";
-    $ret = db_next_id('qbf_qid');
-    $q = db_query($sq, QBF_TABLE_NAME, $ret, $user->uid, $name, serialize($ar_values), time());
-  }
-
   return $ret;
-}
-
-/**
- * Implement hook_menu().
- *
- * @param $may_cache boolean
- * @return array
- */
-function qbf_menu()
-  {
-/* TODO
-   Non menu code that was placed in hook_menu under the '!$may_cache' block
-   so that it could be run during initialization, should now be moved to hook_init.
-   Previously we called hook_init twice, once early in the bootstrap process, second
-   just after the bootstrap has finished. The first instance is now called boot
-   instead of init.
-
-   In Drupal 6, there are now two hooks that can be used by modules to execute code
-   at the beginning of a page request. hook_boot() replaces hook_boot() in Drupal 5
-   and runs on each page request, even for cached pages. hook_boot() now only runs
-   for non-cached pages and thus can be used for code that was previously placed in
-   hook_menu() with $may_cache = FALSE:
-
-   Dynamic menu items under a '!$may_cache' block can often be simplified
-   to remove references to arg(n) and use of '%<function-name>' to check
-   conditions. See http://drupal.org/node/103114.
-
-   The title and description arguments should not have strings wrapped in t(),
-   because translation of these happen in a later stage in the menu system.
-*/
-  $items = array();
-  $admin_access  = array(QBF_PERM_ADMIN);
-  $queror_access = array(QBF_PERM_QUERY);
-
-  $items[QBF_PATH_SETTINGS] = array
-    (
-    'title'              => t('Query-By-Form'),
-    'access arguments'   => $admin_access,
-    'page callback'      => 'drupal_get_form',
-    'page arguments'     => array('qbf_admin_settings'),
-    'type'               => MENU_NORMAL_ITEM,
-    );
-
-  $items[QBF_PATH_MAIN . '/%qbf_query/delete'] = array
-    (
-    'type'               => MENU_CALLBACK,
-    'access arguments'   => $queror_access,
-    'page callback'      => '_qbf_query_delete',
-    'page arguments'     => array(1),
-    );
-
-  return $items;
-  }
-
-/**
- * Delete a query by qid
- *
- * $qid has been tested in qbf_menu() to be a positive integer, so it is a safe
- * number, but we still need to know more about it.
- *
- * @param $qid integer
- */
-function _qbf_query_delete($qid) {
-
-  global $user;
-
-  $query = qbf_query_load($qid);
-  $notify = variable_get(QBF_VAR_NOTIFY_DELETE, QBF_DEF_NOTIFY_DELETE);
-  $link = l($qid, QBF_PATH_MAIN .'/'. $qid .'/delete');
-
-  // only valid if valid query, and owner or admin
-  if (isset($query->uid) && (($query->uid == $user->uid) || user_access(QBF_PERM_ADMIN))) {
-    $sq = 'DELETE FROM %s WHERE qid = %d ';
-    $q = 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');
-    watchdog('qbf', $message, WATCHDOG_NOTICE, $link);
-    if (variable_get(QBF_VAR_NOTIFY_DELETE, QBF_DEF_NOTIFY_DELETE) && $query->uid != $user->uid) {
-      $ret = drupal_mail(__FUNCTION__, $user->mail, $message, $message, $user->mail);
-/* @todo GW
-//      $ret = drupal_mail(__FUNCTION__, $user->mail, $message, $message, $user->mail);
-      $ret = /* TODO Create a hook_mail($key, &$message, $params) function to generate
-      $ret = the message body when called by drupal_mail.
-      $ret = $account = array(); // Set this as needed
-      $ret = $language = user_preferred_language($account);
-      $ret = $object = array(); // Replace this as needed
-      $ret = $context['subject'] = $subject;
-      $ret = $context['body'] = $body;
-      $ret = $params = array('account' => $account, 'object' => $object, 'context' => $context);
-      $ret = drupal_mail('qbf', __FUNCTION__, $user->mail, $language, $params, $user->mail);
-*/
-      $account = user_load(array('uid' => $query->uid));
-      drupal_set_message(t('User !link has been informed', array
-        (
-        '!link' => l($account->name, 'user/'. $query->uid),
-        )));
-      dsm($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, WATCHDOG_ERROR, $link);
-    }
-  drupal_goto();
-}
-
-/**
- * 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'),
-    );
-  return system_settings_form($form);
-}
-
-error_reporting($_qbf_er);