]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/plugin/SystemInfo.php
New FSF address
[SourceForge/phpwiki.git] / lib / plugin / SystemInfo.php
1 <?php // -*-php-*-
2 // $Id$
3 /**
4  * Copyright (C) 1999, 2000, 2001, 2002 $ThePhpWikiProgrammingTeam
5  * Copyright 2008-2009 Marc-Etienne Vargenau, Alcatel-Lucent
6  *
7  * This file is part of PhpWiki.
8  *
9  * PhpWiki is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by
11  * the Free Software Foundation; either version 2 of the License, or
12  * (at your option) any later version.
13  *
14  * PhpWiki is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  * GNU General Public License for more details.
18  *
19  * You should have received a copy of the GNU General Public License along
20  * with PhpWiki; if not, write to the Free Software Foundation, Inc.,
21  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22  */
23
24 /**
25  * Usage: <<SystemInfo all >>
26  *        or <<SystemInfo pagestats cachestats discspace hitstats >>
27  *        or <<SystemInfo version >>
28  *        or <<SystemInfo current_theme >>
29  *        or <<SystemInfo PHPWIKI_DIR >>
30  *
31  * Provide access to phpwiki's lower level system information.
32  *
33  *   version, charset, pagestats, SERVER_NAME, database, discspace,
34  *   cachestats, userstats, linkstats, accessstats, hitstats,
35  *   revisionstats, interwikilinks, imageextensions, wikiwordregexp,
36  *   availableplugins, downloadurl  or any other predefined CONSTANT
37  *
38  * In spirit to http://www.ecyrd.com/JSPWiki/SystemInfo.jsp
39  *
40  * Done: Some calculations are heavy (~5-8 secs), so we should cache
41  *       the result. In the page or with WikiPluginCached?
42  */
43
44 require_once "lib/WikiPluginCached.php";
45 class WikiPlugin_SystemInfo
46 extends WikiPluginCached
47 {
48     function getPluginType() {
49         return PLUGIN_CACHED_HTML;
50     }
51     function getName() {
52         return _("SystemInfo");
53     }
54
55     function getDescription() {
56         return _("Provides access to PhpWiki's lower level system information.");
57     }
58
59     /* From lib/WikiPlugin.php:
60      * If the plugin can deduce a modification time, or equivalent
61      * sort of tag for it's content, then the plugin should
62      * call $request->appendValidators() with appropriate arguments,
63      * and should override this method to return true.
64      */
65     function managesValidators() {
66         return true;
67     }
68     function getExpire($dbi, $argarray, $request) {
69         return '+1800'; // 30 minutes
70     }
71     function getHtml($dbi, $argarray, $request, $basepage) {
72         $loader = new WikiPluginLoader;
73         return $loader->expandPI('<<SystemInfo '
74                                  . WikiPluginCached::glueArgs($argarray) // all
75                                  . ' ?>', $request, $this, $basepage);
76     }
77
78     function getDefaultArguments() {
79         return array(
80                      // 'seperator' => ' ', // on multiple args
81                      );
82     }
83
84     function database() {
85         global $request;
86         $s  = "DATABASE_TYPE: " . DATABASE_TYPE . ", ";
87         switch (DATABASE_TYPE) {
88         case 'SQL':     // pear
89         case 'ADODB':
90         case 'PDO':
91             $dsn = DATABASE_DSN;
92             $s .= "DATABASE BACKEND:" . " ";
93             $s .= (DATABASE_TYPE == 'SQL') ? 'PearDB' : 'ADODB';
94             if (preg_match('/^(\w+):/', $dsn, $m)) {
95                 $backend = $m[1];
96                 $s .= " $backend";
97             }
98             $s .= ", DATABASE_PREFIX: \"".DATABASE_PREFIX."\", ";
99             break;
100         case 'dba':
101             $s .= "DATABASE_DBA_HANDLER: ".DATABASE_DBA_HANDLER.", ";
102             $s .= "DATABASE_DIRECTORY: \"".DATABASE_DIRECTORY."\", ";
103             break;
104         case 'cvs':
105             $s .= "DATABASE_DIRECTORY: \"".DATABASE_DIRECTORY."\", ";
106             // $s .= "cvs stuff: , ";
107             break;
108         case 'flatfile':
109             $s .= "DATABASE_DIRECTORY: ".DATABASE_DIRECTORY.", ";
110             break;
111         }
112         // hack: suppress error when using sql, so no timeout
113         @$s .= "DATABASE_TIMEOUT: " . DATABASE_TIMEOUT;
114         return $s;
115     }
116     function cachestats() {
117         global $request;
118         if (! defined('USECACHE') or !USECACHE)
119             return _("no cache used");
120         $dbi =& $this->_dbi;
121         $cache = $dbi->_cache;
122         $s  = _("cached pagedata:") . " " . count($cache->_pagedata_cache);
123         $s .= ", " . _("cached versiondata:");
124         $s .= " " . count($cache->_versiondata_cache);
125         //$s .= ", glv size: " . count($cache->_glv_cache);
126         //$s .= ", cache hits: ?";
127         //$s .= ", cache misses: ?";
128         return $s;
129     }
130     function ExpireParams() {
131         global $ExpireParams;
132         $s  = sprintf(_("Keep up to %d major edits, but keep them no longer than %d days."),
133                       $ExpireParams['major']['keep'],
134                       $ExpireParams['major']['max_age']);
135         $s .= sprintf(_(" Keep up to %d minor edits, but keep them no longer than %d days."),
136                       $ExpireParams['minor']['keep'],
137                       $ExpireParams['minor']['max_age']);
138         $s .= sprintf(_(" Keep the latest contributions of the last %d authors up to %d days."),
139                       $ExpireParams['author']['keep'], $ExpireParams['author']['max_age']);
140         $s .= sprintf(_(" Additionally, try to keep the latest contributions of all authors in the last %d days (even if there are more than %d of them,) but in no case keep more than %d unique author revisions."),
141                       $ExpireParams['author']['min_age'],
142                       $ExpireParams['author']['keep'],
143                       $ExpireParams['author']['max_keep']);
144         return $s;
145     }
146     function pagestats() {
147         global $request;
148         $dbi = $request->getDbh();
149         $s  = sprintf(_("%d pages"), $dbi->numPages(true));
150         $s  .= ", " . sprintf(_("%d not-empty pages"), $dbi->numPages(false));
151         // more bla....
152         // $s  .= ", " . sprintf(_("earliest page from %s"), $earliestdate);
153         // $s  .= ", " . sprintf(_("latest page from %s"), $latestdate);
154         // $s  .= ", " . sprintf(_("latest pagerevision from %s"), $latestrevdate);
155         return $s;
156     }
157     //What kind of link statistics?
158     //  total links in, total links out, mean links per page, ...
159     //  Any useful numbers similar to a VisualWiki interestmap?
160     function linkstats() {
161         $s  = _("not yet");
162         return $s;
163     }
164     // number of homepages: easy
165     // number of anonymous users?
166     //   calc this from accesslog info?
167     // number of anonymous edits?
168     //   easy. related to the view/edit rate in accessstats.
169     function userstats() {
170         global $request;
171         $dbi =& $this->_dbi;
172         $h = 0;
173         $page_iter = $dbi->getAllPages(true);
174         while ($page = $page_iter->next()) {
175             if ($page->isUserPage(true)) // check if the admin is there. if not add him to the authusers.
176                 $h++;
177         }
178         $s  = sprintf(_("%d homepages"), $h);
179         // $s  .= ", " . sprintf(_("%d anonymous users"), $au); // ??
180         // $s  .= ", " . sprintf(_("%d anonymous edits"), $ae); // see recentchanges
181         // $s  .= ", " . sprintf(_("%d authenticated users"), $auth); // users with password set
182         // $s  .= ", " . sprintf(_("%d externally authenticated users"), $extauth); // query AuthDB?
183         return $s;
184     }
185
186     //only from logging info possible. = hitstats per time.
187     // total hits per day/month/year
188     // view/edit rate
189     // TODO: see WhoIsOnline hit stats, and sql accesslogs
190     function accessstats() {
191         $s  = _("not yet");
192         return $s;
193     }
194
195     // numeric array
196     function _stats($hits, $treshold = 10.0) {
197         sort($hits);
198         reset($hits);
199         $n = count($hits);
200         $max = 0; $min = 9999999999999; $sum = 0;
201         foreach ($hits as $h) {
202             $sum += $h;
203             $max = max($h, $max);
204             $min = min($h, $min);
205         }
206         $median_i = (int) $n / 2;
207         if (! ($n / 2))
208             $median = $hits[$median_i];
209         else
210             $median = $hits[$median_i];
211         $treshold = 10;
212         $mintreshold = $max * $treshold / 100.0;   // lower than 10% of the hits
213         reset($hits);
214         $nmin = $hits[0] < $mintreshold ? 1 : 0;
215         while (next($hits) < $mintreshold)
216             $nmin++;
217         $maxtreshold = $max - $mintreshold; // more than 90% of the hits
218         end($hits);
219         $nmax = 1;
220         while (prev($hits) > $maxtreshold)
221             $nmax++;
222         return array('n'     => $n,
223                      'sum'   => $sum,
224                      'min'   => $min,
225                      'max'   => $max,
226                      'mean'  => $sum / $n,
227                      'median'=> $median,
228                      'stddev'=> stddev($hits, $sum),
229                      'treshold'    => $treshold,
230                      'nmin'        => $nmin,
231                      'mintreshold' => $mintreshold,
232                      'nmax'        => $nmax,
233                      'maxtreshold' => $maxtreshold);
234     }
235
236     // only absolute numbers, not for any time interval. see accessstats
237     //  some useful number derived from the curve of the hit stats.
238     //  total, max, mean, median, stddev;
239     //  %d pages less than 3 hits (<10%)    <10% percent of the leastpopular
240     //  %d pages more than 100 hits (>90%)  >90% percent of the mostpopular
241     function hitstats() {
242         global $request;
243         $dbi =& $this->_dbi;
244         $hits = array();
245         $page_iter = $dbi->getAllPages(true);
246         while ($page = $page_iter->next()) {
247             if (($current = $page->getCurrentRevision())
248                 && (! $current->hasDefaultContents())) {
249                 $hits[] = $page->get('hits');
250             }
251         }
252         $treshold = 10.0;
253         $stats = $this->_stats($hits, $treshold);
254
255         $s  = sprintf(_("total hits: %d"), $stats['sum']);
256         $s .= ", " . sprintf(_("max: %d"), $stats['max']);
257         $s .= ", " . sprintf(_("mean: %2.3f"), $stats['mean']);
258         $s .= ", " . sprintf(_("median: %d"), $stats['median']);
259         $s .= ", " . sprintf(_("stddev: %2.3f"), $stats['stddev']);
260         $s .= "; " . sprintf(_("%d pages with less than %d hits (<%d%%)."),
261                              $stats['nmin'], $stats['mintreshold'], $treshold);
262         $s .= " " . sprintf(_("%d page(s) with more than %d hits (>%d%%)."),
263                             $stats['nmax'], $stats['maxtreshold'], 100 - $treshold);
264         return $s;
265     }
266     /* not yet ready
267      */
268     function revisionstats() {
269         global $request, $LANG;
270
271         include_once("lib/WikiPluginCached.php");
272         $cache = WikiPluginCached::newCache();
273         $id = $cache->generateId('SystemInfo::revisionstats_' . $LANG);
274         $cachedir = 'plugincache';
275         $content = $cache->get($id, $cachedir);
276
277         if (!empty($content))
278             return $content;
279
280         $dbi =& $this->_dbi;
281         $stats = array();
282         $page_iter = $dbi->getAllPages(true);
283         $stats['empty'] = $stats['latest']['major'] = $stats['latest']['minor'] = 0;
284         while ($page = $page_iter->next()) {
285             if (!$page->exists()) {
286                 $stats['empty']++;
287                 continue;
288             }
289             $current = $page->getCurrentRevision();
290             // is the latest revision a major or minor one?
291             //   latest revision: numpages 200 (100%) / major (60%) / minor (40%)
292             if ($current->get('is_minor_edit'))
293                 $stats['latest']['major']++;
294             else
295                 $stats['latest']['minor']++;
296 /*
297             // FIXME: This needs much too long to be acceptable.
298             // overall:
299             //   number of revisions: all (100%) / major (60%) / minor (40%)
300             // revs per page:
301             //   per page: mean 20 / major (60%) / minor (40%)
302             $rev_iter = $page->getAllRevisions();
303             while ($rev = $rev_iter->next()) {
304                 if ($rev->get('is_minor_edit'))
305                     $stats['page']['major']++;
306                 else
307                     $stats['page']['minor']++;
308             }
309             $rev_iter->free();
310             $stats['page']['all'] = $stats['page']['major'] + $stats['page']['minor'];
311             $stats['perpage'][]       = $stats['page']['all'];
312             $stats['perpage_major'][] = $stats['page']['major'];
313             $stats['sum']['all'] += $stats['page']['all'];
314             $stats['sum']['major'] += $stats['page']['major'];
315             $stats['sum']['minor'] += $stats['page']['minor'];
316             $stats['page'] = array();
317 */
318         }
319         $page_iter->free();
320         $stats['numpages'] = $stats['latest']['major'] + $stats['latest']['minor'];
321         $stats['latest']['major_perc'] = $stats['latest']['major'] * 100.0 / $stats['numpages'];
322         $stats['latest']['minor_perc'] = $stats['latest']['minor'] * 100.0 / $stats['numpages'];
323         $empty = sprintf("empty pages: %d (%02.1f%%) / %d (100%%)\n",
324                          $stats['empty'], $stats['empty'] * 100.0 / $stats['numpages'],
325                          $stats['numpages']);
326         $latest = sprintf("latest revision: major %d (%02.1f%%) / minor %d (%02.1f%%) / all %d (100%%)\n",
327                           $stats['latest']['major'], $stats['latest']['major_perc'],
328                           $stats['latest']['minor'], $stats['latest']['minor_perc'], $stats['numpages']);
329 /*
330         $stats['sum']['major_perc'] = $stats['sum']['major'] * 100.0 / $stats['sum']['all'];
331         $stats['sum']['minor_perc'] = $stats['sum']['minor'] * 100.0 / $stats['sum']['all'];
332         $sum = sprintf("number of revisions: major %d (%02.1f%%) / minor %d (%02.1f%%) / all %d (100%%)\n",
333                        $stats['sum']['major'], $stats['sum']['major_perc'],
334                        $stats['sum']['minor'], $stats['sum']['minor_perc'], $stats['sum']['all']);
335
336         $stats['perpage']       = $this->_stats($stats['perpage']);
337         $stats['perpage_major'] = $this->_stats($stats['perpage_major']);
338         $stats['perpage']['major_perc'] = $stats['perpage_major']['sum'] * 100.0 / $stats['perpage']['sum'];
339         $stats['perpage']['minor_perc'] = 100 - $stats['perpage']['major_perc'];
340         $stats['perpage_minor']['sum']  = $stats['perpage']['sum'] - $stats['perpage_major']['sum'];
341         $stats['perpage_minor']['mean'] = $stats['perpage_minor']['sum'] / ($stats['perpage']['n'] - $stats['perpage_major']['n']);
342         $perpage = sprintf("revisions per page: all %d, mean %02.1f / major %d (%02.1f%%) / minor %d (%02.1f%%)\n",
343                            $stats['perpage']['sum'], $stats['perpage']['mean'],
344                            $stats['perpage_major']['mean'], $stats['perpage']['major_perc'],
345                            $stats['perpage_minor']['mean'], $stats['perpage']['minor_perc']);
346         $perpage .= sprintf("  %d page(s) with less than %d revisions (<%d%%)\n",
347                             $stats['perpage']['nmin'], $stats['perpage']['maintreshold'], $treshold);
348         $perpage .= sprintf("  %d page(s) with more than %d revisions (>%d%%)\n",
349                             $stats['perpage']['nmax'], $stats['perpage']['maxtreshold'], 100 - $treshold);
350         $content = $empty . $latest . $sum . $perpage;
351 */
352         $content = $empty . $latest;
353
354         // regenerate cache every 30 minutes
355         $cache->save($id, $content, '+1800', $cachedir);
356         return $content;
357     }
358     // Size of databases/files/cvs are possible plus the known size of the app.
359     // Cache this costly operation.
360     // Even if the whole plugin call is stored internally, we cache this
361     // seperately with a seperate key.
362     function discspace() {
363         global $DBParams, $request;
364
365         include_once("lib/WikiPluginCached.php");
366         $cache = WikiPluginCached::newCache();
367         $id = $cache->generateId('SystemInfo::discspace');
368         $cachedir = 'plugincache';
369         $content = $cache->get($id, $cachedir);
370
371         if (empty($content)) {
372             $dir = defined('PHPWIKI_DIR') ? PHPWIKI_DIR : '.';
373             //TODO: windows only (no cygwin)
374             $appsize = `du -s $dir | cut -f1`;
375
376             if (in_array($DBParams['dbtype'], array('SQL', 'ADODB'))) {
377                 //TODO: where is the data is actually stored? see phpMyAdmin
378                 $pagesize = 0;
379             } elseif ($DBParams['dbtype'] == 'dba') {
380                 $pagesize = 0;
381                 $dbdir = $DBParams['directory'];
382                 if ($DBParams['dba_handler'] == 'db3')
383                     $pagesize = filesize($DBParams['directory']
384                                          . "/wiki_pagedb.db3") / 1024;
385                 // if issubdirof($dbdir, $dir) $appsize -= $pagesize;
386             } else { // flatfile, cvs
387                 $dbdir = $DBParams['directory'];
388                 $pagesize = `du -s $dbdir`;
389                 // if issubdirof($dbdir, $dir) $appsize -= $pagesize;
390             }
391             $content = array('appsize' => $appsize,
392                              'pagesize' => $pagesize);
393             // regenerate cache every 30 minutes
394             $cache->save($id, $content, '+1800', $cachedir);
395         } else {
396             $appsize = $content['appsize'];
397             $pagesize = $content['pagesize'];
398         }
399
400         $s  = sprintf(_("Application size: %d KiB"), $appsize);
401         if ($pagesize)
402             $s  .= ", " . sprintf(_("Pagedata size: %d KiB", $pagesize));
403         return $s;
404     }
405
406     function inlineimages () {
407         return implode(' ', explode('|', INLINE_IMAGES));
408     }
409     function wikinameregexp () {
410         return $GLOBALS['WikiNameRegexp'];
411     }
412     function allowedprotocols () {
413         return implode(' ', explode('|', ALLOWED_PROTOCOLS));
414     }
415     function available_plugins () {
416         $fileset = new FileSet(FindFile('lib/plugin'), '*.php');
417         $list = $fileset->getFiles();
418         natcasesort($list);
419         reset($list);
420         return sprintf(_("Total %d plugins: "), count($list))
421             . implode(', ', array_map(create_function('$f',
422                                                       'return substr($f,0,-4);'),
423                                       $list));
424     }
425     function supported_languages () {
426         $available_languages = listAvailableLanguages();
427         natcasesort($available_languages);
428
429         return sprintf(_("Total of %d languages: "),
430                        count($available_languages))
431             . implode(', ', $available_languages) . ". "
432             . sprintf(_("Current language: '%s'"), $GLOBALS['LANG'])
433             . ((DEFAULT_LANGUAGE != $GLOBALS['LANG'])
434                ? ". " . sprintf(_("Default language: '%s'"), DEFAULT_LANGUAGE)
435                : '');
436     }
437
438     function supported_themes () {
439         global $WikiTheme;
440         $available_themes = listAvailableThemes();
441         natcasesort($available_themes);
442         return sprintf(_("Total of %d themes: "), count($available_themes))
443             . implode(', ',$available_themes) . ". "
444             . sprintf(_("Current theme: '%s'"), $WikiTheme->_name)
445             . ((THEME != $WikiTheme->_name)
446                ? ". " . sprintf(_("Default theme: '%s'"), THEME)
447                : '');
448     }
449
450     function call ($arg, &$availableargs) {
451         if (!empty($availableargs[$arg]))
452             return $availableargs[$arg]();
453         elseif (method_exists($this, $arg)) // any defined SystemInfo->method()
454             return call_user_func_array(array(&$this, $arg), array());
455         elseif (defined($arg) && // any defined constant
456                 !in_array($arg,array('ADMIN_PASSWD','DATABASE_DSN','DBAUTH_AUTH_DSN')))
457             return constant($arg);
458         else
459             return $this->error(sprintf(_("unknown argument '%s' to SystemInfo"), $arg));
460     }
461
462     function run($dbi, $argstr, &$request, $basepage) {
463         // don't parse argstr for name=value pairs. instead we use just 'name'
464         //$args = $this->getArgs($argstr, $request);
465         $this->_dbi =& $dbi;
466         $args['seperator'] = ' ';
467         $availableargs = // name => callback + 0 args
468             array ('appname' => create_function('',"return 'PhpWiki';"),
469                    'version' => create_function('',"return sprintf('%s', PHPWIKI_VERSION);"),
470                    'LANG'    => create_function('','return $GLOBALS["LANG"];'),
471                    'LC_ALL'  => create_function('','return setlocale(LC_ALL, 0);'),
472                    'current_language' => create_function('','return $GLOBALS["LANG"];'),
473                    'system_language' => create_function('','return DEFAULT_LANGUAGE;'),
474                    'current_theme' => create_function('','return $GLOBALS["WikiTheme"]->_name;'),
475                    'system_theme'  => create_function('','return THEME;'),
476                    // more here or as method.
477                    '' => create_function('',"return 'dummy';")
478                    );
479         // split the argument string by any number of commas or space
480         // characters, which include " ", \r, \t, \n and \f
481         $allargs = preg_split("/[\s,]+/", $argstr, -1, PREG_SPLIT_NO_EMPTY);
482         if (in_array('all', $allargs) || in_array('table', $allargs)) {
483             $allargs = array('appname'          => _("Application name"),
484                              'version'          => _("PhpWiki engine version"),
485                              'database'         => _("Database"),
486                              'cachestats'       => _("Cache statistics"),
487                              'pagestats'        => _("Page statistics"),
488                              //'revisionstats'    => _("Page revision statistics"),
489                              //'linkstats'        => _("Link statistics"),
490                              'userstats'        => _("User statistics"),
491                              //'accessstats'      => _("Access statistics"),
492                              'hitstats'         => _("Hit statistics"),
493                              'discspace'        => _("Harddisc usage"),
494                              'expireparams'     => _("Expiry parameters"),
495                              'wikinameregexp'   => _("Wikiname regexp"),
496                              'allowedprotocols' => _("Allowed protocols"),
497                              'inlineimages'     => _("Inline images"),
498                              'available_plugins'   => _("Available plugins"),
499                              'supported_languages' => _("Supported languages"),
500                              'supported_themes'    => _("Supported themes"),
501 //                           '' => _(""),
502                              '' => ""
503                              );
504             $table = HTML::table(array('class' => 'bordered'));
505             foreach ($allargs as $arg => $desc) {
506                 if (!$arg)
507                     continue;
508                 if (!$desc)
509                     $desc = _($arg);
510                 $table->pushContent(HTML::tr(HTML::th(array('style' => "white-space:nowrap"), $desc),
511                                              HTML::td(HTML($this->call($arg, $availableargs)))));
512             }
513             return $table;
514         } else {
515             $output = '';
516             foreach ($allargs as $arg) {
517                 $o = $this->call($arg, $availableargs);
518                 if (is_object($o))
519                     return $o;
520                 else
521                     $output .= ($o . $args['seperator']);
522             }
523             // if more than one arg, remove the trailing seperator
524             if ($output) $output = substr($output, 0,
525                                           - strlen($args['seperator']));
526             return HTML($output);
527         }
528     }
529 }
530
531 function median($hits) {
532     sort($hits);
533     reset($hits);
534     $n = count($hits);
535     $median = (int) $n / 2;
536     if (! ($n % 2)) // proper rounding on even length
537         return ($hits[$median] + $hits[$median-1]) * 0.5;
538     else
539         return $hits[$median];
540 }
541
542 function rsum($a, $b) {
543     $a += $b;
544     return $a;
545 }
546 function mean(&$hits, $total = false) {
547     $n = count($hits);
548     if (!$total)
549         $total = array_reduce($hits, 'rsum');
550     return (float) $total / ($n * 1.0);
551 }
552
553 function stddev(&$hits, $total = false) {
554     $n = count($hits);
555     if (!$total) $total = array_reduce($hits, 'rsum');
556     $GLOBALS['mean'] = $total / $n;
557     $r = array_map(create_function('$i', 'global $mean; return ($i-$mean)*($i-$mean);'),
558                    $hits);
559     unset($GLOBALS['mean']);
560     return (float)sqrt(mean($r, $total) * ($n / (float)($n -1)));
561 }
562
563 // Local Variables:
564 // mode: php
565 // tab-width: 8
566 // c-basic-offset: 4
567 // c-hanging-comment-ender-p: nil
568 // indent-tabs-mode: nil
569 // End:
570 ?>