﻿Index: modules/statistics/statistics.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/statistics/statistics.module,v
retrieving revision 1.300
diff -u -r1.300 statistics.module
--- modules/statistics/statistics.module	13 Apr 2009 10:40:13 -0000	1.300
+++ modules/statistics/statistics.module	25 Apr 2009 22:35:28 -0000
@@ -38,6 +38,22 @@
 }
 
 /**
+ * Implementation of hook_theme().
+ */
+function statistics_theme() {
+  return array(
+    'statistics_filter_form' => array(
+      'arguments' => array('form' => NULL),
+      'file' => 'statistics.admin.inc',
+    ),
+    'statistics_filters' => array(
+      'arguments' => array('form' => NULL),
+      'file' => 'statistics.admin.inc',
+    ),
+  );
+}
+
+/**
  * Implementation of hook_exit().
  *
  * This is where statistics are gathered on page accesses.
@@ -346,6 +362,105 @@
 }
 
 /**
+ * List statistics filters that can be applied.
+ */
+function statistics_filters() {
+  // Regular filters
+  $filters = array();
+
+  $filters['hits'] = array(
+    '#title' => t('recent hits'),
+    '#filters' => array(
+      'page' => array(
+        'title' => t('page'),
+        'where' => array('field' => 'a.path', 'operator' => 'NOT LIKE'),
+        'join' => '',
+      ),
+      'uid' => array(
+        'title' => t('userid'),
+        'where' => array('field' => 'a.uid', 'operator' => '!='),
+        'join' => '',
+      ),
+      'user' => array(
+        'title' => t('username'),
+        'where' => array('field' => 'u.name', 'operator' => 'NOT LIKE'),
+        'join' => '',
+      ),
+    ),
+  );
+  $filters['pages'] = array(
+    '#title' => t('top pages'),
+    '#filters' => array(
+      'path' => array(
+        'title' => t('path'),
+        'where' => array('field' => 'path', 'operator' => 'NOT LIKE'),
+        'join' => '',
+      ),
+      'title' => array(
+        'title' => t('title'),
+        'where' => array('field' => 'title', 'operator' => 'NOT LIKE'),
+        'join' => '',
+      ),
+    ),
+  );
+  $filters['referrers'] = array(
+    '#title' => t('top referrers'),
+    '#filters' => array(
+      'url' => array(
+        'title' => t('url'),
+        'where' => array('field' => 'url', 'operator' => 'NOT LIKE'),
+        'join' => '',
+      ),
+    ),
+  );
+  $filters['visitors'] = array(
+    '#title' => t('top visitors'),
+    '#filters' => array(
+      'path' => array(
+        'title' => t('username'),
+        'where' => array('field' => 'u.name', 'operator' => 'NOT LIKE'),
+        'join' => '',
+      ),
+      'hostname' => array(
+        'title' => t('hostname'),
+        'where' => array('field' => 'a.hostname', 'operator' => 'NOT LIKE'),
+        'join' => '',
+      ),
+    ),
+  );
+
+  return $filters;
+}
+
+/**
+ * Build query for statistic filters based on session and statistic type.
+ * 
+ * @param $type 
+ *  Type of filter.
+ */
+function statistics_build_filter_query($type) {
+  $filters = statistics_filters();
+  // get type specific filters.
+  $filters = $filters[$type]['#filters'];
+
+  if (!isset($_SESSION['statistics_filter'][$type])) {
+    return array('where' => array(), 'join' => '', 'args' => array());
+  }
+
+  // Build query.
+  $where = $args = $join = array();
+  foreach ($_SESSION['statistics_filter'][$type] as $filter) {
+    list($key, $value) = $filter;
+    $where[] = $filters[$key]['where'];
+    $join[] = $filters[$key]['join'];
+    $args[] = $value;
+  }
+  $join = !empty($join) ? ' ' . implode(' ', array_unique($join)) : '';
+
+  return array('where' => $where, 'join' => $join, 'args' => $args);
+}
+
+/**
  * It is possible to adjust the width of columns generated by the
  * statistics module.
  */
Index: modules/statistics/statistics.admin.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/statistics/statistics.admin.inc,v
retrieving revision 1.21
diff -u -r1.21 statistics.admin.inc
--- modules/statistics/statistics.admin.inc	13 Apr 2009 10:40:13 -0000	1.21
+++ modules/statistics/statistics.admin.inc	25 Apr 2009 22:35:27 -0000
@@ -10,6 +10,9 @@
  * Menu callback; presents the "recent hits" page.
  */
 function statistics_recent_hits() {
+  $filtertype = 'hits';
+  $filter = statistics_build_filter_query($filtertype);
+
   $header = array(
     array('data' => t('Timestamp'), 'field' => 'a.timestamp', 'sort' => 'desc'),
     array('data' => t('Page'), 'field' => 'a.path'),
@@ -25,6 +28,11 @@
     ->limit(30)
     ->setHeader($header);
 
+  // Build query condition.
+  foreach ($filter['where'] as $key => $where) {
+    $query->condition($where['field'], $filter['args'][$key], $where['operator']);
+  }
+
   $result = $query->execute();
   $rows = array();
   foreach ($result as $log) {
@@ -39,7 +47,8 @@
     $rows[] = array(array('data' => t('No statistics available.'), 'colspan' => 4));
   }
 
-  $output = theme('table', $header, $rows);
+  $output = drupal_get_form('statistics_filter_form', 'hits');
+  $output .= theme('table', $header, $rows);
   $output .= theme('pager', NULL, 30, 0);
   return $output;
 }
@@ -48,6 +57,9 @@
  * Menu callback; presents the "top pages" page.
  */
 function statistics_top_pages() {
+  $filtertype = 'pages';
+  $filter = statistics_build_filter_query($filtertype);
+
   $header = array(
     array('data' => t('Hits'), 'field' => 'hits', 'sort' => 'desc'),
     array('data' => t('Page'), 'field' => 'path'),
@@ -68,6 +80,15 @@
     ->limit(30)
     ->setHeader($header);
 
+  // Build query condition.
+  if (count($filter['where'])) {
+    $condition = db_and();
+    foreach ($filter['where'] as $key => $where) {
+      $condition = $condition->condition($where['field'], $filter['args'][$key], $where['operator']);
+    }
+    $query->condition($condition);
+  }
+
   $count_query = db_select('accesslog');
   $count_query->addExpression('COUNT(DISTINCT path)');
   $query->setCountQuery($count_query);
@@ -83,7 +104,8 @@
   }
 
   drupal_set_title(t('Top pages in the past %interval', array('%interval' => format_interval(variable_get('statistics_flush_accesslog_timer', 259200)))), PASS_THROUGH);
-  $output = theme('table', $header, $rows);
+  $output = drupal_get_form('statistics_filter_form', $filtertype);
+  $output .= theme('table', $header, $rows);
   $output .= theme('pager', NULL, 30, 0);
   return $output;
 }
@@ -92,6 +114,8 @@
  * Menu callback; presents the "top visitors" page.
  */
 function statistics_top_visitors() {
+  $filtertype = 'visitors';
+  $filter = statistics_build_filter_query($filtertype);
 
   $header = array(
     array('data' => t('Hits'), 'field' => 'hits', 'sort' => 'desc'),
@@ -116,6 +140,15 @@
     ->limit(30)
     ->setHeader($header);
 
+  // Build query condition.
+  if (count($filter['where'])) {
+    $condition = db_and();
+    foreach ($filter['where'] as $key => $where) {
+      $condition = $condition->condition($where['field'], $filter['args'][$key], $where['operator']);
+    }
+    $query->condition($condition);
+  }
+
   $count_query = db_select('accesslog');
   $count_query->addExpression('COUNT(DISTINCT CONCAT(CAST(uid AS char), hostname))');
   $query->setCountQuery($count_query);
@@ -133,7 +166,8 @@
   }
 
   drupal_set_title(t('Top visitors in the past %interval', array('%interval' => format_interval(variable_get('statistics_flush_accesslog_timer', 259200)))), PASS_THROUGH);
-  $output = theme('table', $header, $rows);
+  $output = drupal_get_form('statistics_filter_form', $filtertype);
+  $output .= theme('table', $header, $rows);
   $output .= theme('pager', NULL, 30, 0);
   return $output;
 }
@@ -144,6 +178,9 @@
 function statistics_top_referrers() {
   drupal_set_title(t('Top referrers in the past %interval', array('%interval' => format_interval(variable_get('statistics_flush_accesslog_timer', 259200)))), PASS_THROUGH);
 
+  $filtertype = 'referrers';
+  $filter = statistics_build_filter_query($filtertype);
+
   $header = array(
     array('data' => t('Hits'), 'field' => 'hits', 'sort' => 'desc'),
     array('data' => t('Url'), 'field' => 'url'),
@@ -161,6 +198,15 @@
     ->limit(30)
     ->setHeader($header);
 
+  // Build query condition.
+  if (count($filter['where'])) {
+    $condition = db_and();
+    foreach ($filter['where'] as $key => $where) {
+      $condition = $condition->condition($where['field'], $filter['args'][$key], $where['operator']);
+    }
+    $query->condition($condition);
+  }
+
   $count_query = db_select('accesslog');
   $count_query->addExpression('COUNT(DISTINCT url)');
   $count_query
@@ -178,7 +224,8 @@
     $rows[] = array(array('data' => t('No statistics available.'), 'colspan' => 3));
   }
 
-  $output = theme('table', $header, $rows);
+  $output = drupal_get_form('statistics_filter_form', $filtertype);
+  $output .= theme('table', $header, $rows);
   $output .= theme('pager', NULL, 30, 0);
   return $output;
 }
@@ -224,6 +271,111 @@
 }
 
 /**
+ * Form builder; Return form for logging filters.
+ *
+ * @param $type Type of logging.
+ *
+ * @ingroup forms
+ * @see statistics_filter_form_submit()
+ */
+function statistics_filter_form($form_state, $type) {
+  $session = isset($_SESSION['statistics_filter']) ? $_SESSION['statistics_filter'] : array();
+  $session = (is_array($session) && isset($session[$type]) ) ? $session[$type] : array();
+  $filters = statistics_filters();
+  
+  $i = 0;
+  $form['filters'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Hide !type where', array('!type' => $filters[$type]['#title'])),
+    '#theme' => 'statistics_filters',
+    '#collapsible' => TRUE,
+    '#collapsed' => FALSE,
+  );
+
+  $form['filters']['type'] = array(
+    '#type' => 'value', 
+    '#value' => $type,
+  );
+
+  // get only type specific filters.
+  $filters = $filters[$type]['#filters'];
+  
+  foreach ($session as $filter) {
+    list($filtertype, $filtervalue) = $filter;
+    $params = array('%property' => $filters[$filtertype]['title'] , '%value' => $filtervalue);
+    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' => 'textfield',
+      '#size' => 50,
+    );
+  }
+
+  $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 statistics filter form.
+ */
+function statistics_filter_form_submit($form, &$form_state) {
+  $op = $form_state['values']['op'];
+  $type = $form_state['values']['type'];
+  $filters = statistics_filters();
+  // get only type specific filters.
+  $filters = $filters[$type]['#filters'];
+  switch ($op) {
+    case t('Filter'): 
+    case t('Refine'):
+      if (isset($form_state['values']['filter'])) {
+        $filter = $form_state['values']['filter'];
+        if (isset($form_state['values'][$filter])) {
+          $_SESSION['statistics_filter'][$type][] = array($filter, $form_state['values'][$filter]);
+        }
+      }
+      break;
+    case t('Undo'):
+      array_pop($_SESSION['statistics_filter'][$type]);
+      break;
+    case t('Reset'):
+      $_SESSION['statistics_filter'][$type] = array();
+      break;
+  }
+
+  $form_state['redirect'] = 'admin/reports/' . $type;
+  return;
+}
+
+/**
  * Form builder; Configure access logging.
  *
  * @ingroup forms
@@ -262,3 +414,48 @@
 
   return system_settings_form($form, TRUE);
 }
+
+/**
+ * Theme user administration filter form.
+ *
+ * @ingroup themeable
+ */
+function theme_statistics_filter_form(&$form) {
+  $output = '<div id="statistics-admin-filter">';
+  $output .= drupal_render($form['filters']);
+  $output .= '</div>';
+  $output .= drupal_render_children($form);
+  return $output;
+}
+
+/**
+ * Theme statistics filter selector.
+ *
+ * @ingroup themeable
+ */
+function theme_statistics_filters(&$form) {
+  $output = '<ul class="clear-block">';
+  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="c">';
+  foreach (element_children($form['status']) as $key) {
+    $output .= drupal_render($form['status'][$key]);
+  }
+  $output .= '</dd>';
+
+  $output .= '</dl>';
+  $output .= '<div class="container-inline" id="statistics-filter-buttons">' . drupal_render($form['buttons']) . '</div>';
+  $output .= '</li></ul>';
+
+  return $output;
+}
Index: modules/statistics/statistics.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/statistics/statistics.test,v
retrieving revision 1.8
diff -u -r1.8 statistics.test
--- modules/statistics/statistics.test	13 Apr 2009 10:40:13 -0000	1.8
+++ modules/statistics/statistics.test	25 Apr 2009 22:35:28 -0000
@@ -70,3 +70,72 @@
     $this->assertRaw(t('The IP address %ip was deleted.', array('%ip' => $test_ip_address)), t('IP address deleted.'));
   }
 }
+
+class StatisticsAdminTestCase extends DrupalWebTestCase {
+  function getInfo() {
+    return array(
+      'name' => t('Statistics administration'),
+      'description' => t('Test admininstration page functionality.'),
+      'group' => t('Statistics')
+    );
+  }
+
+  function setUp() {
+    parent::setUp('statistics');
+  }
+
+  /**
+   * Add some entries to access/referrer/recent hits/top pages and filter them.
+   */
+  function testStatisticsAdmin() {
+    // Create some log entries.
+    // access log
+    $access_text1 = $this->randomName();
+    $access_text2 = $this->randomName();
+    db_insert('accesslog')
+      ->fields(array(
+        'title' => $access_text1,
+        'path' => $access_text1,
+        'url' => 'http://example.com',
+        'hostname' => '192.168.1.1',
+        'uid' => 0,
+        'sid' => 10,
+        'timer' => 10,
+        'timestamp' => REQUEST_TIME,
+      ))
+      ->execute();
+    db_insert('accesslog')
+      ->fields(array(
+        'title' => $access_text2,
+        'path' => $access_text2,
+        'url' => 'http://sub.example.com',
+        'hostname' => '192.168.1.2',
+        'uid' => 0,
+        'sid' => 10,
+        'timer' => 10,
+        'timestamp' => REQUEST_TIME,
+      ))
+      ->execute();
+
+    // Create admin user to filter the logs.
+    $admin_user = $this->drupalCreateUser(array('access statistics'));
+    $this->drupalLogin($admin_user);
+    $this->drupalGet('admin/reports/hits');
+    $this->assertText($access_text1, t("Found '%text' on recent hits page", array('%text' => $access_text1)));
+    $this->assertText($access_text2, t("Found '%text' on recent hits page", array('%text' => $access_text2)));
+
+    // Filter the access log by page.
+    $edit = array();
+    $edit['filter'] = 'page';
+    $edit['page'] = $access_text1;
+
+    $this->drupalPost('admin/reports/hits', $edit, t('Filter'));
+
+    // Check if the correct log items show up.
+    // Use assertNoRaw and check for text within a link because the text is also 
+    // displayed in the filter form.
+    $this->assertNoRaw('>' . $access_text1 . '</a>', t("Found '%text' not on filtered by page on recent hits page", array('%text' => $access_text1)));
+    $this->assertText($access_text2, t("Found '%text' on filtered by page on recent hits page", array('%text' => $access_text2)));
+  }
+
+}
Index: misc/form.js
===================================================================
RCS file: /cvs/drupal/drupal/misc/form.js,v
retrieving revision 1.7
diff -u -r1.7 form.js
--- misc/form.js	11 Apr 2009 22:19:44 -0000	1.7
+++ misc/form.js	25 Apr 2009 22:35:27 -0000
@@ -66,6 +66,11 @@
         $('.multiselect input:radio[value="'+ this.id.substr(5) +'"]')
           .attr('checked', true);
     });
+    $('.multiselect input:not(.multiselectSelector-processed)', context)
+      .addClass('multiselectSelector-processed').change(function() {
+        $('.multiselect input:radio[value="'+ this.id.substr(5) +'"]')
+          .attr('checked', true);
+    });
   }
 };
 
