Index: includes/filter.inc
===================================================================
RCS file: includes/filter.inc
diff -N includes/filter.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ includes/filter.inc	15 May 2009 19:59:48 -0000
@@ -0,0 +1,310 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Filter query extender and helper functions.
+ */
+
+/**
+ * Query extender for filter queries.
+ *
+ * This creates a filter form and filters the query based on this form.
+ */
+class Filter extends SelectQueryExtender {
+  /**
+   * Currently active filters, loaded based on $name.
+   */
+  protected $filter = array();
+
+  /**
+   * Name of the used filter. This is used in the hooks and to load the data
+   * in $_SESSION.
+   */
+  protected $name;
+
+  /**
+   * The defined filter elements of that filter, loaded with the hook.
+   *
+   * @see hook_filter_elements_FILTERNAME().
+   */
+  protected $elements = array();
+
+  /**
+   * Set the name of the filter.
+   *
+   * Load the saved filter data in $_SESSION, execute the hook to load the
+   * filter elements. Then add a tag to the query object and store the loaded
+   * data as metadata in the query object.
+   *
+   * After that, the defined filters are applied to the object, as far as
+   * possible.
+   *
+   * @param $name
+   *   Name of the filter query. This should be a unique string to identify the
+   *   site/listing that should be filtered.
+   * 
+   * @return
+   *   The query object to allow fluent method calls.
+   */
+  public function filterName($name) {
+    $this->name = $name;
+    $this->initialize();
+
+    return $this;
+  }
+
+  protected function initialize() {
+    if (!isset($_SESSION['filter'][$this->name])) {
+      $_SESSION['filter'][$this->name] = array();
+    }
+    $this->filter =& $_SESSION['filter'][$this->name];
+    $this->elements = module_invoke_all('filter_elements_' . $this->name, $this);
+    $this->addTag('filter_' . $this->name);
+    $this->addMetaData('filter', $this->filter);
+    $this->addMetaData('elements', $this->elements);
+  }
+
+  public function execute() {
+    // Add simple conditions of the currently active filters to the query.
+    // @todo: Implement additional simple filter configurations.
+    foreach ($this->filter as $name => $values) {
+      foreach ($values as $value) {
+        if (isset($this->elements[$name]['field'])) {
+          // Check if we need to do a LIKE comparison.
+          if (preg_match('/([\*%]+)/', $value)) {
+            // Replace wildcards with PDO wildcards.
+            $this->condition($this->elements[$name]['field'], preg_replace('!\*+!', '%', $value), 'LIKE');
+          }
+          else {
+            $this->condition($this->elements[$name]['field'], $value);
+          }
+        }
+      }
+    }
+
+    return $this->query->execute();
+  }
+
+  /**
+   * Return the defined filter name.
+   *
+   * @return
+   *   The previously defined filter.
+   */
+  public function getFilterName() {
+    return $this->name;
+  }
+
+  /**
+   * Return the loaded, currently active filters for that query.
+   *
+   * @return
+   *   Array with active filters.
+   */
+  public function getFilter() {
+    return $this->filter;
+  }
+
+  /**
+   * Get the form definition of the active filter.
+   *
+   * @see filter_extender_form
+   *
+   * @param $title
+   *   Fieldset title that should be used for that form.
+   */
+  public function getForm($title = NULL) {
+
+    // Only display a form if filter elements were found.
+    if (empty($this->elements)) {
+      return array();
+    }
+
+    if ($title == NULL) {
+      $title = t('Only display items where');
+    }
+
+    // Note: #query is a workaround becase we can't directly call a object
+    // method for the form submit.
+    $form = array(
+      '#title'  => $title,
+      '#type'   => 'fieldset',
+      '#weight' => -50,
+      '#theme'  => 'filter_selection_form',
+      '#query'  => $this,
+    );
+
+    $i = 0;
+    foreach ($this->filter as $name => $values) {
+      $params = array(
+        '%property' => $this->elements[$name]['title'],
+        '%value' => join(t(' and '), $this->getFilterDescription($name, $values)
+      ));
+      if ($i++ > 0) {
+        $form['current'][] = array('#markup' => t('<em>and</em> where <strong>%property</strong> is <strong>%value</strong>', $params));
+      }
+      else {
+        $form['current'][] = array('#markup' => t('<strong>%property</strong> is <strong>%value</strong>', $params));
+      }
+    }
+
+    // Create a select for each filter element.
+    foreach ($this->elements as $name => $element) {
+      $names[$name] = $element['title'];
+
+      // Display a select if pre-defined options are available.
+      if (isset($element['options'])) {
+        $form['status'][$name] = array(
+          '#type' => 'select',
+          '#options' => $element['options'],
+        );
+      } else {
+        // If not, use a textfield, optionally with an autocomplete path.
+        $form['status'][$name] = array(
+          '#type' => 'textfield',
+          '#size' => 19,
+        );
+        if (isset($element['autocomplete_path'])) {
+          $form['status'][$name]['#autocomplete_path'] = $element['autocomplete_path'];
+        }
+      }
+    }
+
+    // Create list of filter elements.
+    $form['filter'] = array(
+      '#type' => 'radios',
+      '#options' => $names,
+    );
+
+    $form['buttons']['submit'] = array(
+      '#type' => 'submit',
+      '#value' => (count($this->filter) ? t('Refine') : t('Filter')),
+      '#submit' => array('filter_extender_form_submit'),
+    );
+    if (count($this->filter)) {
+      $form['buttons']['undo'] = array(
+        '#type' => 'submit',
+        '#value' => t('Undo'),
+      '#submit' => array('filter_extender_form_submit'),
+      );
+      $form['buttons']['reset'] = array(
+        '#type' => 'submit',
+        '#value' => t('Reset'),
+        '#submit' => array('filter_extender_form_submit'),
+      );
+    }
+    return $form;
+  }
+
+  /**
+   * Handle the filter form submit.
+   *
+   * Because this can't be called directly, it's called by the helper function
+   * filter_extender_form_submit.
+   *
+   * @see filter_extender_form_submit()
+   */
+  public function submitForm($form, &$form_state) {
+    switch ($form_state['values']['op']) {
+      case t('Filter'):
+      case t('Refine'):
+        if (!empty($form_state['values']['filter'])) {
+          $name = $form_state['values']['filter'];
+          $value = $form_state['values'][$name];
+
+          // Only add the new filter of multiple is enabled, override the
+          // current value if not.
+          if (isset($this->elements[$name]['multiple']) && $this->elements[$name]['multiple']) {
+            $this->filter[$name][$value] = $value;
+          }
+          else {
+            $this->filter[$name] = array($value => $value);
+          }
+        }
+        break;
+      case t('Undo'):
+        array_pop($this->filter);
+        break;
+      case t('Reset'):
+        $this->filter = array();
+        break;
+    }
+  }
+
+  /**
+   * Return an array of filter values that should be displayed in the form.
+   *
+   * @param $name
+   *   Name of the filter.
+   * @param $values
+   *   Array of values that should be displayed.
+   * @return
+   *   Array of filter values, either the values itself or the text of the
+   *   selected options.
+   */
+  protected function getFilterDescription($name, $values) {
+    if (isset($this->elements[$name]['options'])) {
+      $options = $this->elements[$name]['options'];
+      if (count(array_filter($this->elements[$name]['options'], 'is_array')) > 0) {
+        $options = call_user_func_array('array_merge', $this->elements[$name]['options']);
+      }
+      return array_intersect_key($options, $values);
+    } else {
+      return $values;
+    }
+  }
+}
+
+/**
+ * Theme filter selector form.
+ *
+ * @ingroup themeable
+ */
+function theme_filter_selection_form($form) {
+  $output = '';
+  if (!empty($form['current'])) {
+    $output .= '<p>';
+    foreach (element_children($form['current']) as $key) {
+      $output .= '' . drupal_render($form['current'][$key]) . '<br />';
+    }
+    $output .= '</p>';
+  }
+
+  $output .= '<p><dl class="multiselect">' . (!empty($form['current']) ? '<dt><em>' . t('and') . '</em> ' . t('where') . '</dt>' : '') . '<dd class="a">';
+  foreach (element_children($form['filter']) as $key) {
+    $output .= drupal_render($form['filter'][$key]);
+  }
+  $output .= '</dd><dt>' . t('is') . '</dt><dd class="b">';
+
+  foreach (element_children($form['status']) as $key) {
+    $output .= drupal_render($form['status'][$key]);
+  }
+  $output .= '</dd></dl>';
+  $output .= '<div class="container-inline" id="user-admin-buttons">' . drupal_render($form['buttons']) . '</div></p>';
+  return $output;
+}
+
+/**
+ * Form for the filter of the passed in $query object.
+ *
+ * This is a helper function because we can't directly call a object method with
+ * drupal_get_form(). Only use this if you don't already have a $form object
+ * that could be extended.
+ *
+ * @param $query
+ *   SelectQuery object that is already extend with Filter.
+ * @return <type>
+ */
+function filter_extender_form($query) {
+  return $query->getForm();
+}
+
+/**
+ * Helper function for submitting the filter form.
+ *
+ * @see filter_extender_form()
+ */
+function filter_extender_form_submit($form, &$form_state) {
+  $form['filter']['#query']->submitForm($form, $form_state);
+}
\ No newline at end of file
Index: modules/user/user.admin.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/user/user.admin.inc,v
retrieving revision 1.47
diff -u -p -r1.47 user.admin.inc
--- modules/user/user.admin.inc	12 May 2009 08:37:45 -0000	1.47
+++ modules/user/user.admin.inc	15 May 2009 19:59:49 -0000
@@ -19,7 +19,6 @@ function user_admin($callback_arg = '') 
         $build['user_multiple_cancel_confirm'] = drupal_get_form('user_multiple_cancel_confirm');
       }
       else {
-        $build['user_filter_form'] = drupal_get_form('user_filter_form');
         $build['user_admin_account'] = drupal_get_form('user_admin_account');
       }
   }
@@ -27,104 +26,6 @@ function user_admin($callback_arg = '') 
 }
 
 /**
- * Form builder; Return form for user administration filters.
- *
- * @ingroup forms
- * @see user_filter_form_submit()
- */
-function user_filter_form() {
-  if (!isset($_SESSION['user_overview_filter'])) {
-    drupal_set_session('user_overview_filter', array());
-  }
-  $session = &$_SESSION['user_overview_filter'];
-  $filters = user_filters();
-
-  $i = 0;
-  $form['filters'] = array(
-    '#type' => 'fieldset',
-    '#title' => t('Show only users where'),
-    '#theme' => 'user_filters',
-  );
-  foreach ($session as $filter) {
-    list($type, $value) = $filter;
-    // Merge an array of arrays into one if necessary.
-    $options = $type == 'permission' ? call_user_func_array('array_merge', $filters[$type]['options']) : $filters[$type]['options'];
-    $params = array('%property' => $filters[$type]['title'] , '%value' => $options[$value]);
-    if ($i++ > 0) {
-      $form['filters']['current'][] = array('#markup' => t('<em>and</em> where <strong>%property</strong> is <strong>%value</strong>', $params));
-    }
-    else {
-      $form['filters']['current'][] = array('#markup' => t('<strong>%property</strong> is <strong>%value</strong>', $params));
-    }
-  }
-
-  foreach ($filters as $key => $filter) {
-    $names[$key] = $filter['title'];
-    $form['filters']['status'][$key] = array(
-      '#type' => 'select',
-      '#options' => $filter['options'],
-    );
-  }
-
-  $form['filters']['filter'] = array(
-    '#type' => 'radios',
-    '#options' => $names,
-  );
-  $form['filters']['buttons']['submit'] = array(
-    '#type' => 'submit',
-    '#value' => (count($session) ? t('Refine') : t('Filter')),
-  );
-  if (count($session)) {
-    $form['filters']['buttons']['undo'] = array(
-      '#type' => 'submit',
-      '#value' => t('Undo'),
-    );
-    $form['filters']['buttons']['reset'] = array(
-      '#type' => 'submit',
-      '#value' => t('Reset'),
-    );
-  }
-
-  drupal_add_js('misc/form.js');
-
-  return $form;
-}
-
-/**
- * Process result from user administration filter form.
- */
-function user_filter_form_submit($form, &$form_state) {
-  $op = $form_state['values']['op'];
-  $filters = user_filters();
-  switch ($op) {
-    case t('Filter'): case t('Refine'):
-      if (isset($form_state['values']['filter'])) {
-        $filter = $form_state['values']['filter'];
-        // Merge an array of arrays into one if necessary.
-        $options = $filter == 'permission' ? call_user_func_array('array_merge', $filters[$filter]['options']) : $filters[$filter]['options'];
-        if (isset($options[$form_state['values'][$filter]])) {
-          if (!isset($_SESSION['user_overview_filter'])) {
-            drupal_set_session('user_overview_filter', array());
-          }
-          $_SESSION['user_overview_filter'][] = array($filter, $form_state['values'][$filter]);
-        }
-      }
-      break;
-    case t('Undo'):
-      array_pop($_SESSION['user_overview_filter']);
-      break;
-    case t('Reset'):
-      drupal_set_session('user_overview_filter', array());
-      break;
-    case t('Update'):
-      return;
-  }
-
-  $form_state['redirect'] = 'admin/user/user';
-  return;
-}
-
-/**
  * Form builder; User administration page.
  *
  * @ingroup forms
@@ -143,14 +44,16 @@ function user_admin_account() {
     t('Operations')
   );
 
-  $query = db_select('users', 'u');
-  $query->leftJoin('users_roles', 'ur', 'u.uid = ur.uid');
-  $query->condition('u.uid', 0, '<>');
-  user_build_filter_query($query);
+  // Extend with Filter early so that the count query gets the same filters applied.
+  $query = db_select('users', 'u')->extend('Filter');
+  $query
+    ->filterName('user_admin')
+    ->condition('u.uid', 0, '<>');
   
   $count_query = clone $query;
   $count_query->addExpression('COUNT(DISTINCT u.uid)');
 
+  // Extend with PagerDefault and Tablesort now, as the count query doesn't need them.
   $query = $query->extend('PagerDefault')->extend('TableSort');
   $query
     ->fields('u', array('uid', 'name', 'status', 'created', 'access'))
@@ -158,6 +61,8 @@ function user_admin_account() {
     ->setHeader($header)
     ->setCountQuery($count_query);
   $result = $query->execute();
+  // Load filter form.
+  $form['filter'] = $query->getForm(t('Only display users where...'));
 
   $form['options'] = array(
     '#type' => 'fieldset',
@@ -177,6 +82,9 @@ function user_admin_account() {
   $form['options']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Update'),
+    // Only run the update validate and submit function for the Update button.
+    '#submit' => array('user_admin_account_update'),
+    '#validate' => array('user_admin_account_update_validate'),
   );
 
   $destination = drupal_get_destination();
@@ -211,7 +119,7 @@ function user_admin_account() {
 /**
  * Submit the user administration update form.
  */
-function user_admin_account_submit($form, &$form_state) {
+function user_admin_account_update($form, &$form_state) {
   $operations = module_invoke_all('user_operations', $form_state);
   $operation = $operations[$form_state['values']['operation']];
   // Filter out unchecked accounts.
@@ -230,7 +138,10 @@ function user_admin_account_submit($form
   }
 }
 
-function user_admin_account_validate($form, &$form_state) {
+/**
+ * Validate the user administration update form.
+ */
+function user_admin_account_update_validate($form, &$form_state) {
   $form_state['values']['accounts'] = array_filter($form_state['values']['accounts']);
   if (count($form_state['values']['accounts']) == 0) {
     form_set_error('', t('No users selected.'));
@@ -799,7 +710,8 @@ function theme_user_admin_account($form)
     t('Operations')
   );
 
-  $output = drupal_render($form['options']);
+  $output = drupal_render($form['filter']);
+  $output .= drupal_render($form['options']);
   if (isset($form['name']) && is_array($form['name'])) {
     foreach (element_children($form['name']) as $key) {
       $rows[] = array(
@@ -850,49 +762,3 @@ function theme_user_admin_new_role($form
 
   return $output;
 }
-
-/**
- * Theme user administration filter form.
- *
- * @ingroup themeable
- */
-function theme_user_filter_form($form) {
-  $output = '<div id="user-admin-filter">';
-  $output .= drupal_render($form['filters']);
-  $output .= '</div>';
-  $output .= drupal_render_children($form);
-  return $output;
-}
-
-/**
- * Theme user administration filter selector.
- *
- * @ingroup themeable
- */
-function theme_user_filters($form) {
-  $output = '<ul class="clearfix">';
-  if (!empty($form['current'])) {
-    foreach (element_children($form['current']) as $key) {
-      $output .= '<li>' . drupal_render($form['current'][$key]) . '</li>';
-    }
-  }
-
-  $output .= '<li><dl class="multiselect">' . (!empty($form['current']) ? '<dt><em>' . t('and') . '</em> ' . t('where') . '</dt>' : '') . '<dd class="a">';
-  foreach (element_children($form['filter']) as $key) {
-    $output .= drupal_render($form['filter'][$key]);
-  }
-  $output .= '</dd>';
-
-  $output .= '<dt>' . t('is') . '</dt><dd class="b">';
-
-  foreach (element_children($form['status']) as $key) {
-    $output .= drupal_render($form['status'][$key]);
-  }
-  $output .= '</dd>';
-
-  $output .= '</dl>';
-  $output .= '<div class="container-inline" id="user-admin-buttons">' . drupal_render($form['buttons']) . '</div>';
-  $output .= '</li></ul>';
-
-  return $output;
-}
Index: modules/user/user.css
===================================================================
RCS file: /cvs/drupal/drupal/modules/user/user.css,v
retrieving revision 1.10
diff -u -p -r1.10 user.css
--- modules/user/user.css	9 Oct 2008 04:19:44 -0000	1.10
+++ modules/user/user.css	15 May 2009 19:59:49 -0000
@@ -12,12 +12,6 @@
 #user-login-form {
   text-align: center;
 }
-#user-admin-filter ul {
-  list-style-type: none;
-  padding: 0;
-  margin: 0;
-  width: 100%;
-}
 #user-admin-buttons {
   float: left; /* LTR */
   margin-left: 0.5em; /* LTR */
Index: modules/user/user.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/user/user.module,v
retrieving revision 1.986
diff -u -p -r1.986 user.module
--- modules/user/user.module	14 May 2009 08:23:15 -0000	1.986
+++ modules/user/user.module	15 May 2009 19:59:50 -0000
@@ -63,12 +63,6 @@ function user_theme() {
     'user_admin_account' => array(
       'arguments' => array('form' => NULL),
     ),
-    'user_filter_form' => array(
-      'arguments' => array('form' => NULL),
-    ),
-    'user_filters' => array(
-      'arguments' => array('form' => NULL),
-    ),
     'user_signature' => array(
       'arguments' => array('signature' => NULL),
     ),
@@ -2424,18 +2418,24 @@ function _user_sort($a, $b) {
 }
 
 /**
- * List user administration filters that can be applied.
+ * Implementation of hook_filter_elements_filtername().
  */
-function user_filters() {
+function user_filter_elements_user_admin() {
   // Regular filters
   $filters = array();
+  $filters['name'] = array(
+    'title' => t('username'),
+    'autocomplete_path' => 'user/autocomplete',
+    'field' => 'u.name',
+  );
+
   $roles = user_roles(TRUE);
   unset($roles[DRUPAL_AUTHENTICATED_RID]); // Don't list authorized role.
   if (count($roles)) {
     $filters['role'] = array(
       'title' => t('role'),
-      'field' => 'ur.rid',
       'options' => $roles,
+      'multiple' => TRUE,
     );
   }
 
@@ -2453,6 +2453,7 @@ function user_filters() {
   $filters['permission'] = array(
     'title' => t('permission'),
     'options' => $options,
+    'multiple' => TRUE,
   );
 
   $filters['status'] = array(
@@ -2464,32 +2465,36 @@ function user_filters() {
 }
 
 /**
- * Extends a query object for user administration filters based on session.
+ * Extends a query object for user permission filters.
  *
  * @param $query
  *   Query object that should be filtered.
  */
-function user_build_filter_query(SelectQuery $query) {
-  $filters = user_filters();
-
-  // Extend Query with filter conditions.
-  foreach ($_SESSION['user_overview_filter'] as $filter) {
-    list($key, $value) = $filter;
-    // This checks to see if this permission filter is an enabled permission for
-    // the authenticated role. If so, then all users would be listed, and we can
-    // skip adding it to the filter query.
-    if ($key == 'permission') {
+function user_query_filter_user_admin_alter($query) {
+  // Extend Query with permission conditions.
+  $filter = $query->getMetaData('filter');
+  if (isset($filter['permission'])) {
+    $i = 0;
+    $query->leftJoin('users_roles', 'ur', 'u.uid = ur.uid');
+    foreach ($filter['permission'] as $value) {
       $account = new stdClass();
       $account->uid = 'user_filter';
       $account->roles = array(DRUPAL_AUTHENTICATED_RID => 1);
       if (user_access($value, $account)) {
         continue;
       }
-      $query->leftJoin('role_permission', 'p', 'ur.rid = p.rid');
-      $query->condition(db_or()->condition('u.uid', 1)->condition('p.permission', $value));
+      $query->leftjoin('role_permission', "p$i", "ur.rid = p$i.rid");
+      $query->condition(db_or()->condition("u.uid", 1)->condition("p$i.permission", $value));
+      $i++;
     }
-    else {
-      $query->condition($filters[$key]['field'], $value);
+  }
+  
+  if (isset($filter['role'])) {
+    $i = 0;
+    foreach ($filter['role'] as $value) {
+      $table = $query->leftJoin('users_roles', "ur$i", "ur$i.uid = u.uid");
+      $query->condition("ur$i.rid", $value);
+      $i++;
     }
   }
 }
