initial import from onelab svn codebase
[plewww.git] / modules / statistics.module
1 <?php
2 // $Id: statistics.module 144 2007-03-28 07:52:20Z thierry $
3
4 /**
5  * @file
6  * Logs access statistics for your site.
7  */
8
9 /**
10  * Implementation of hook_help().
11  */
12 function statistics_help($section) {
13   switch ($section) {
14     case 'admin/help#statistics':
15       $output = '<p>'. t('The statistics module keeps track of numerous statistics of site usage. It counts how many times, and from where each of your posts is viewed. The statistics module can be used to learn many useful things about how users are interacting with each other and with your site.') .'</p>';
16       $output .= t('<p>Statistics module features</p>
17 <ul>
18 <li>Logs show statistics for how many times your site and specific content on your site has been accessed.</li>
19 <li>Referrers tells you from where visitors came from (referrer URL).</li>
20 <li>Top pages shows you what\'s hot, what is the most popular content on your site.</li>
21 <li>Top users shows you the most active users for your site.</li>
22 <li>Recent hits displays information about the latest activity on your site.</li>
23 <li>Node count displays the number of times a node has been accessed in the node\'s link section next to <em># comments</em>.</li>
24 <li>Popular content block creates a block that can display the day\'s top viewed content, the all time top viewed content, and the last content viewed.</li>
25 </ul>
26 ');
27       $output .= t('<p>Configuring the statistics module</p>
28 <ul>
29 <li>Enable access log allows you to turn the access log on and off. This log is used to store data about every page accessed, such as the remote host\'s IP address, where they came from (referrer), what node they\'ve viewed, and their user name. Enabling the log adds one database call per page displayed by Drupal.</li>
30 <li>Discard access logs older than allows you to configure how long an access log entry is saved, after which time it is deleted from the database table. To use this you need to run <em>cron.php</em></li>
31 <li>Enable node view counter allows you to turn on and off the node-counting functionality of this module. If it is turned on, an extra database query is added for each node displayed, which increments a counter.</li>
32 </ul>
33 ');
34       $output .= t('<p>You can</p>
35 <ul>
36 <li>administer statistics <a href="%admin-settings-statistics">administer &gt;&gt; settings &gt;&gt; statistics</a>.</li>
37 <li>access statistics logs <a href="%admin-logs">administer &gt;&gt; logs</a>.</li>
38 <li>view recent hits <a href="%admin-logs-hits">administer &gt;&gt; logs &gt;&gt; recent hits</a>.</li>
39 <li>enable \'popular content\' block in block administration <a href="%admin-block">administer &gt;&gt; blocks </a> but only after you have enabled \'Count content views\' in settings.</li>
40 </ul>
41 ', array('%admin-settings-statistics' => url('admin/settings/statistics'), '%admin-logs' => url('admin/logs'), '%admin-logs-hits' => url('admin/logs/hits'), '%admin-block' => url('admin/block')));
42       $output .= '<p>'. t('For more information please read the configuration and customization handbook <a href="%statistics">Statistics page</a>.', array('%statistics' => 'http://drupal.org/handbook/modules/statistics/')) .'</p>';
43       return $output;
44     case 'admin/modules#description':
45       return t('Logs access statistics for your site.');
46     case 'admin/settings/statistics':
47       return t('<p>Settings for the statistical information that Drupal will keep about the site. See <a href="%statistics">site statistics</a> for the actual information.</p>', array('%statistics' => url('admin/logs/hits')));
48     case 'admin/logs/hits':
49       return t('<p>This page shows you the most recent hits.</p>');
50     case 'admin/logs/referrers':
51       return t('<p>This page shows you all external referrers. These are links pointing to your web site from outside your web site.</p>');
52     case 'admin/logs/visitors':
53       return t('<p>When you ban a visitor, you prevent his IP address from accessing your site. Unlike blocking a user, banning a visitor works even for anonymous users. The most common use for this is to block bots/web crawlers that are consuming too many resources.</p>');
54   }
55 }
56
57 /**
58  * Implementation of hook_exit().
59  *
60  * This is where statistics are gathered on page accesses.
61  */
62 function statistics_exit() {
63   global $user, $recent_activity;
64
65   drupal_bootstrap(DRUPAL_BOOTSTRAP_PATH);
66
67   if (variable_get('statistics_count_content_views', 0)) {
68     // We are counting content views.
69     if ((arg(0) == 'node') && is_numeric(arg(1)) && arg(2) == '') {
70       // A node has been viewed, so update the node's counters.
71       db_query('UPDATE {node_counter} SET daycount = daycount + 1, totalcount = totalcount + 1, timestamp = %d WHERE nid = %d', time(), arg(1));
72       // If we affected 0 rows, this is the first time viewing the node.
73       if (!db_affected_rows()) {
74         // We must create a new row to store counters for the new node.
75         db_query('INSERT INTO {node_counter} (nid, daycount, totalcount, timestamp) VALUES (%d, 1, 1, %d)', arg(1), time());
76       }
77     }
78   }
79   if ((variable_get('statistics_enable_access_log', 0)) && (module_invoke('throttle', 'status') == 0)) {
80     // Log this page access.
81     db_query("INSERT INTO {accesslog} (title, path, url, hostname, uid, sid, timer, timestamp) values('%s', '%s', '%s', '%s', %d, '%s', %d, %d)", strip_tags(drupal_get_title()), $_GET['q'], referer_uri(), $_SERVER['REMOTE_ADDR'], $user->uid, session_id(), timer_read('page'), time());
82   }
83 }
84
85 /**
86  * Implementation of hook_perm().
87  */
88 function statistics_perm() {
89   return array('access statistics', 'view post access counter');
90 }
91
92 /**
93  * Implementation of hook_link().
94  */
95 function statistics_link($type, $node = 0, $main = 0) {
96   global $id;
97   $links = array();
98
99   if ($type != 'comment' && user_access('view post access counter')) {
100     $statistics = statistics_get($node->nid);
101     if ($statistics) {
102       $links[] = format_plural($statistics['totalcount'], '1 read', '%count reads');
103     }
104   }
105
106   return $links;
107 }
108
109 /**
110  * Implementation of hook_menu().
111  */
112 function statistics_menu($may_cache) {
113   $items = array();
114
115   $access = user_access('access statistics');
116   if ($may_cache) {
117     $items[] = array('path' => 'admin/logs/hits', 'title' => t('recent hits'),
118       'callback' => 'statistics_recent_hits', 'access' => $access,
119       'weight' => 3);
120     $items[] = array('path' => 'admin/logs/pages', 'title' => t('top pages'),
121       'callback' => 'statistics_top_pages', 'access' => $access,
122       'weight' => 1);
123     $items[] = array('path' => 'admin/logs/visitors', 'title' => t('top visitors'),
124       'callback' => 'statistics_top_visitors', 'access' => $access,
125       'weight' => 2);
126     $items[] = array('path' => 'admin/logs/referrers', 'title' => t('referrers'),
127       'callback' => 'statistics_top_referrers', 'access' => $access);
128     $items[] = array('path' => 'admin/logs/access', 'title' => t('details'),
129       'callback' => 'statistics_access_log', 'access' => $access,
130       'type' => MENU_CALLBACK);
131   }
132   else {
133     if (arg(0) == 'user' && is_numeric(arg(1)) && variable_get('statistics_enable_access_log', 0)) {
134       $items[] = array('path' => 'user/'. arg(1) .'/track/navigation', 'title' => t('track page visits'),
135         'callback' => 'statistics_user_tracker', 'access' => $access,
136         'type' => MENU_LOCAL_TASK, 'weight' => 2);
137     }
138     if (arg(0) == 'node' && is_numeric(arg(1)) && variable_get('statistics_enable_access_log', 0)) {
139       $items[] = array('path' => 'node/'. arg(1) .'/track', 'title' => t('track'),
140         'callback' => 'statistics_node_tracker', 'access' => $access,
141         'type' => MENU_LOCAL_TASK, 'weight' => 2);
142     }
143   }
144
145   return $items;
146 }
147
148 /**
149  * Implementation of hook_user().
150  */
151 function statistics_user($op, &$edit, &$user) {
152   if ($op == 'delete') {
153     db_query('UPDATE {accesslog} SET uid = 0 WHERE uid = %d', $user->uid);
154   }
155 }
156
157 function statistics_access_log($aid) {
158   $result = db_query('SELECT a.*, u.name FROM {accesslog} a LEFT JOIN {users} u ON a.uid = u.uid WHERE aid = %d', $aid);
159   if ($access = db_fetch_object($result)) {
160     $output  = '<table border="1" cellpadding="2" cellspacing="2">';
161     $output .= ' <tr><th>'. t('URL') ."</th><td>". l(url($access->path, NULL, NULL, TRUE), $access->path) ."</td></tr>";
162     $output .= ' <tr><th>'. t('Title') .'</th><td>'. $access->title .'</td></tr>'; // safe because it comes from drupal_get_title()
163     $output .= ' <tr><th>'. t('Referrer') ."</th><td>". ($access->url ? l($access->url, $access->url) : '') ."</td></tr>";
164     $output .= ' <tr><th>'. t('Date') .'</th><td>'. format_date($access->timestamp, 'large') .'</td></tr>';
165     $output .= ' <tr><th>'. t('User') .'</th><td>'. theme('username', $access) .'</td></tr>';
166     $output .= ' <tr><th>'. t('Hostname') .'</th><td>'. check_plain($access->hostname) .'</td></tr>';
167     $output .= '</table>';
168     return $output;
169   }
170   else {
171     drupal_not_found();
172   }
173 }
174
175 function statistics_node_tracker() {
176   if ($node = node_load(arg(1))) {
177
178     $header = array(
179         array('data' => t('Time'), 'field' => 'a.timestamp', 'sort' => 'desc'),
180         array('data' => t('Referrer'), 'field' => 'a.url'),
181         array('data' => t('User'), 'field' => 'u.name'),
182         array('data' => t('Operations')));
183
184     $result = pager_query('SELECT a.aid, a.timestamp, a.url, a.uid, u.name FROM {accesslog} a LEFT JOIN {users} u ON a.uid = u.uid WHERE a.path LIKE \'node/%d%%\'' . tablesort_sql($header), 30, 0, NULL, $node->nid);
185     while ($log = db_fetch_object($result)) {
186       $rows[] = array(
187         array('data' => format_date($log->timestamp, 'small'), 'class' => 'nowrap'),
188         _statistics_link($log->url),
189         theme('username', $log),
190         l(t('details'), "admin/logs/access/$log->aid"));
191     }
192
193     drupal_set_title(check_plain($node->title));
194     $output = theme('table', $header, $rows);
195     $output .= theme('pager', NULL, 30, 0);
196     return $output;
197   }
198   else {
199     drupal_not_found();
200   }
201 }
202
203 function statistics_user_tracker() {
204   if ($account = user_load(array('uid' => arg(1)))) {
205
206     $header = array(
207         array('data' => t('Timestamp'), 'field' => 'timestamp', 'sort' => 'desc'),
208         array('data' => t('Page'), 'field' => 'path'),
209         array('data' => t('Operations')));
210
211     $result = pager_query('SELECT aid, timestamp, path, title FROM {accesslog} WHERE uid = %d' . tablesort_sql($header), 30, 0, NULL, $account->uid);
212     while ($log = db_fetch_object($result)) {
213       $rows[] = array(
214         array('data' => format_date($log->timestamp, 'small'), 'class' => 'nowrap'),
215         _statistics_format_item($log->title, $log->path),
216         l(t('details'), "admin/logs/access/$log->aid"));
217     }
218
219     drupal_set_title($account->name);
220     $output = theme('table', $header, $rows);
221     $output .= theme('pager', NULL, 30, 0);
222     return $output;
223   }
224   else {
225     drupal_not_found();
226   }
227 }
228
229 /**
230  * Menu callback; presents the "recent hits" page.
231  */
232 function statistics_recent_hits() {
233   $header = array(
234     array('data' => t('Timestamp'), 'field' => 'a.timestamp', 'sort' => 'desc'),
235     array('data' => t('Page'), 'field' => 'a.path'),
236     array('data' => t('User'), 'field' => 'u.name'),
237     array('data' => t('Operations'))
238   );
239
240   $sql = 'SELECT a.aid, a.path, a.title, a.uid, u.name, a.timestamp FROM {accesslog} a LEFT JOIN {users} u ON u.uid = a.uid' . tablesort_sql($header);
241
242   $result = pager_query($sql, 30);
243   while ($log = db_fetch_object($result)) {
244     $rows[] = array(
245       array('data' => format_date($log->timestamp, 'small'), 'class' => 'nowrap'),
246       _statistics_format_item($log->title, $log->path),
247       theme('username', $log),
248       l(t('details'), "admin/logs/access/$log->aid"));
249   }
250
251   $output = theme('table', $header, $rows);
252   $output .= theme('pager', NULL, 30, 0);
253   return $output;
254 }
255
256 /**
257  * Menu callback; presents the "top pages" page.
258  */
259 function statistics_top_pages() {
260   $sql = "SELECT COUNT(path) AS hits, path, title, AVG(timer) AS average_time, SUM(timer) AS total_time FROM {accesslog} GROUP BY path, title";
261   $sql_cnt = "SELECT COUNT(DISTINCT(path)) FROM {accesslog}";
262
263   $header = array(
264     array('data' => t('Hits'), 'field' => 'hits', 'sort' => 'desc'),
265     array('data' => t('Page'), 'field' => 'path'),
266     array('data' => t('Average page generation time'), 'field' => 'average_time'),
267     array('data' => t('Total page generation time'), 'field' => 'total_time')
268   );
269   $sql .= tablesort_sql($header);
270   $result = pager_query($sql, 30, 0, $sql_cnt);
271
272   while ($page = db_fetch_object($result)) {
273     $rows[] = array($page->hits, _statistics_format_item($page->title, $page->path), t('%time ms', array('%time' => round($page->average_time))), format_interval(round($page->total_time / 1000)));
274   }
275
276   drupal_set_title(t('Top pages in the past %interval', array('%interval' => format_interval(variable_get('statistics_flush_accesslog_timer', 259200)))));
277   $output = theme('table', $header, $rows);
278   $output .= theme('pager', NULL, 30, 0);
279   return $output;
280 }
281
282 /**
283  * Menu callback; presents the "top visitors" page.
284  */
285 function statistics_top_visitors() {
286
287   $header = array(
288     array('data' => t('Hits'), 'field' => 'hits', 'sort' => 'desc'),
289     array('data' => t('Visitor'), 'field' => 'u.name'),
290     array('data' => t('Total page generation time'), 'field' => 'total'),
291     array('data' => t('Operations'))
292   );
293
294   $sql = "SELECT COUNT(a.uid) AS hits, a.uid, u.name, a.hostname, SUM(a.timer) AS total, ac.aid FROM {accesslog} a LEFT JOIN {access} ac ON ac.type = 'host' AND LOWER(a.hostname) LIKE (ac.mask) LEFT JOIN {users} u ON a.uid = u.uid GROUP BY a.hostname, a.uid, u.name, ac.aid". tablesort_sql($header);
295   $sql_cnt = "SELECT COUNT(DISTINCT(uid)) FROM {accesslog}";
296   $result = pager_query($sql, 30, 0, $sql_cnt);
297
298   while ($account = db_fetch_object($result)) {
299     $qs = drupal_get_destination();
300     $ban_link = $account->aid ? l(t('unban'), "admin/access/rules/delete/$account->aid", array(), $qs) : l(t('ban'), "admin/access/rules/add/$account->hostname/host", array(), $qs);
301     $rows[] = array($account->hits, ($account->uid ? theme('username', $account) : $account->hostname), format_interval(round($account->total / 1000)), $ban_link);
302   }
303
304   drupal_set_title(t('Top visitors in the past %interval', array('%interval' => format_interval(variable_get('statistics_flush_accesslog_timer', 259200)))));
305   $output = theme('table', $header, $rows);
306   $output .= theme('pager', NULL, 30, 0);
307   return $output;
308 }
309
310 /**
311  * Menu callback; presents the "referrer" page.
312  */
313 function statistics_top_referrers() {
314   $query = "SELECT url, COUNT(url) AS hits, MAX(timestamp) AS last FROM {accesslog} WHERE url NOT LIKE '%%%s%%' AND url <> '' GROUP BY url";
315   $query_cnt = "SELECT COUNT(DISTINCT(url)) FROM {accesslog} WHERE url <> '' AND url NOT LIKE '%%%s%%'";
316   drupal_set_title(t('Top referrers in the past %interval', array('%interval' => format_interval(variable_get('statistics_flush_accesslog_timer', 259200)))));
317
318   $header = array(
319     array('data' => t('Hits'), 'field' => 'hits', 'sort' => 'desc'),
320     array('data' => t('Url'), 'field' => 'url'),
321     array('data' => t('Last visit'), 'field' => 'last'),
322   );
323
324   $query .= tablesort_sql($header);
325   $result = pager_query($query, 30, 0, $query_cnt, $_SERVER['HTTP_HOST']);
326
327   while ($referrer = db_fetch_object($result)) {
328     $rows[] = array($referrer->hits, _statistics_link($referrer->url), t('%time ago', array('%time' => format_interval(time() - $referrer->last))));
329   }
330
331   $output = theme('table', $header, $rows);
332   $output .= theme('pager', NULL, 30, 0);
333   return $output;
334 }
335
336 /**
337  * Implementation of hook_settings().
338  */
339 function statistics_settings() {
340   // access log settings:
341   $options = array('1' => t('Enabled'), '0' => t('Disabled'));
342   $form['access'] = array('#type' => 'fieldset', '#title' => t('Access log settings'));
343   $form['access']['statistics_enable_access_log'] = array('#type' => 'radios', '#title' => t('Enable access log'), '#default_value' =>  variable_get('statistics_enable_access_log', 0), '#options' => $options, '#description' => t('Log each page access.  Required for referrer statistics.'));
344   $period = drupal_map_assoc(array(3600, 10800, 21600, 32400, 43200, 86400, 172800, 259200, 604800, 1209600, 2419200, 4838400, 9676800), 'format_interval');
345   $form['access']['statistics_flush_accesslog_timer'] = array('#type' => 'select', '#title' => t('Discard access logs older than'), '#default_value'   => variable_get('statistics_flush_accesslog_timer', 259200), '#options' => $period, '#description' => t('Older access log entries (including referrer statistics) will be automatically discarded.  Requires crontab.'));
346
347   // count content views settings
348   $form['content'] = array('#type' => 'fieldset', '#title' => t('Content viewing counter settings'));
349   $form['content']['statistics_count_content_views'] = array('#type' => 'radios', '#title' => t('Count content views'), '#default_value' =>  variable_get('statistics_count_content_views', 0), '#options' => $options, '#description' => t('Increment a counter each time content is viewed.'));
350
351   return $form;
352 }
353
354 /**
355  * Implementation of hook_cron().
356  */
357 function statistics_cron() {
358   $statistics_timestamp = variable_get('statistics_day_timestamp', '');
359
360   if ((time() - $statistics_timestamp) >= 86400) {
361     /* reset day counts */
362     db_query('UPDATE {node_counter} SET daycount = 0');
363     variable_set('statistics_day_timestamp', time());
364   }
365
366   /* clean expired access logs */
367   db_query('DELETE FROM {accesslog} WHERE timestamp < %d', time() - variable_get('statistics_flush_accesslog_timer', 259200));
368 }
369
370 /**
371  * Returns all time or today top or last viewed node(s).
372  *
373  * @param $dbfield
374  *   one of
375  *   - 'totalcount': top viewed content of all time.
376  *   - 'daycount': top viewed content for today.
377  *   - 'timestamp': last viewed node.
378  *
379  * @param $dbrows
380  *   number of rows to be returned.
381  *
382  * @return
383  *   A query result containing n.nid, n.title, u.uid, u.name of the selected node(s)
384  *   or FALSE if the query could not be executed correctly.
385  */
386 function statistics_title_list($dbfield, $dbrows) {
387   return db_query_range(db_rewrite_sql("SELECT n.nid, n.title, u.uid, u.name FROM {node} n INNER JOIN {node_counter} s ON n.nid = s.nid INNER JOIN {users} u ON n.uid = u.uid WHERE %s <> '0' AND n.status = 1 ORDER BY %s DESC"), 's.'. $dbfield, 's.'. $dbfield, 0, $dbrows);
388 }
389
390
391 /**
392  * Retrieves a node's "view statistics".
393  *
394  * @param $nid
395  *   node ID
396  *
397  * @return
398  *   An array with three entries: [0]=totalcount, [1]=daycount, [2]=timestamp
399  *   - totalcount: count of the total number of times that node has been viewed.
400  *   - daycount: count of the total number of times that node has been viewed "today".
401  *     For the daycount to be reset, cron must be enabled.
402  *   - timestamp: timestamp of when that node was last viewed.
403  */
404 function statistics_get($nid) {
405
406   if ($nid > 0) {
407     /* retrieves an array with both totalcount and daycount */
408     $statistics = db_fetch_array(db_query('SELECT totalcount, daycount, timestamp FROM {node_counter} WHERE nid = %d', $nid));
409   }
410
411   return $statistics;
412 }
413
414 /**
415  * Implementation of hook_block().
416  */
417 function statistics_block($op = 'list', $delta = 0, $edit = array()) {
418   switch ($op) {
419     case 'list':
420       if (variable_get('statistics_count_content_views', 0)) {
421         $blocks[0]['info'] = t('Popular content');
422       }
423       return $blocks;
424
425     case 'configure':
426       // Popular content block settings
427       $numbers = array('0' => t('Disabled')) + drupal_map_assoc(array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30, 40));
428       $form['statistics_block_top_day_num'] = array('#type' => 'select', '#title' => t("Number of day's top views to display"), '#default_value' => variable_get('statistics_block_top_day_num', 0), '#options' => $numbers, '#description' => t('How many content items to display in "day" list.'));
429       $form['statistics_block_top_all_num'] = array('#type' => 'select', '#title' => t('Number of all time views to display'), '#default_value' => variable_get('statistics_block_top_all_num', 0), '#options' => $numbers, '#description' => t('How many content items to display in "all time" list.'));
430       $form['statistics_block_top_last_num'] = array('#type' => 'select', '#title' => t('Number of most recent views to display'), '#default_value' => variable_get('statistics_block_top_last_num', 0), '#options' => $numbers, '#description' => t('How many content items to display in "recently viewed" list.'));
431       return $form;
432
433     case 'save':
434       variable_set('statistics_block_top_day_num', $edit['statistics_block_top_day_num']);
435       variable_set('statistics_block_top_all_num', $edit['statistics_block_top_all_num']);
436       variable_set('statistics_block_top_last_num', $edit['statistics_block_top_last_num']);
437       break;
438
439     case 'view':
440       if (user_access('access content')) {
441         $content = array();
442
443         $daytop = variable_get('statistics_block_top_day_num', 0);
444         if ($daytop && ($result = statistics_title_list('daycount', $daytop)) && db_num_rows($result)) {
445           $content[] = node_title_list($result, t("Today's:"));
446         }
447
448         $alltimetop = variable_get('statistics_block_top_all_num', 0);
449         if ($alltimetop && ($result = statistics_title_list('totalcount', $alltimetop)) && db_num_rows($result)) {
450           $content[] = node_title_list($result, t('All time:'));
451         }
452
453         $lasttop = variable_get('statistics_block_top_last_num', 0);
454         if ($lasttop && ($result = statistics_title_list('timestamp', $lasttop)) && db_num_rows($result)) {
455           $content[] = node_title_list($result, t('Last viewed:'));
456         }
457
458         if (count($content)) {
459           $block['content'] = implode('<br />', $content);
460           $block['subject'] = t('Popular content');
461           return $block;
462         }
463       }
464   }
465 }
466
467 /**
468  * It is possible to adjust the width of columns generated by the
469  * statistics module.
470  */
471 function _statistics_link($path, $width = 35) {
472   $title = drupal_get_path_alias($path);
473   $title = truncate_utf8($title, $width, FALSE, TRUE);
474   return l($title, $path);
475 }
476
477 function _statistics_format_item($title, $path) {
478   $path = ($path ? $path : '/');
479   $output  = ($title ? "$title<br />" : '');
480   $output .= _statistics_link($path);
481   return $output;
482 }
483
484 /**
485  * Implementation of hook_nodeapi().
486  */
487 function statistics_nodeapi(&$node, $op, $arg = 0) {
488   switch ($op) {
489     case 'delete':
490       // clean up statistics table when node is deleted
491       db_query('DELETE FROM {node_counter} WHERE nid = %d', $node->nid);
492   }
493 }
494
495