1<?php 2 3// phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps 4use dokuwiki\Extension\AdminPlugin; 5use dokuwiki\plugin\statistics\SearchEngines; 6 7/** 8 * statistics plugin 9 * 10 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 11 * @author Andreas Gohr <gohr@splitbrain.org> 12 */ 13class admin_plugin_statistics extends AdminPlugin 14{ 15 /** @var string the currently selected page */ 16 protected $opt = ''; 17 18 /** @var string from date in YYYY-MM-DD */ 19 protected $from = ''; 20 /** @var string to date in YYYY-MM-DD */ 21 protected $to = ''; 22 /** @var int Offset to use when displaying paged data */ 23 protected $start = 0; 24 25 /** @var helper_plugin_statistics */ 26 protected $hlp; 27 28 /** 29 * Available statistic pages 30 */ 31 protected $pages = [ 32 'dashboard' => 'printDashboard', 33 'content' => [ 34 'pages' => 'printTable', 35 'edits' => 'printTable', 36 'images' => 'printImages', 37 'downloads' => 'printDownloads', 38 'history' => 'printHistory', 39 ], 40 'users' => [ 41 'topdomain' => 'printTableAndPieGraph', 42 'topuser' => 'printTableAndPieGraph', 43 'topeditor' => 'printTableAndPieGraph', 44 'topgroup' => 'printTableAndPieGraph', 45 'topgroupedit' => 'printTableAndPieGraph', 46 'seenusers' => 'printTable', 47 ], 48 'links' => [ 49 'referer' => 'printReferer', 50 'newreferer' => 'printTable', 51 'outlinks' => 'printTable' 52 ], 53 'search' => [ 54 'searchengines' => 'printTableAndPieGraph', 55 'internalsearchphrases' => 'printTable', 56 'internalsearchwords' => 'printTable', 57 ], 58 'technology' => [ 59 'browsers' => 'printTableAndPieGraph', 60 'os' => 'printTableAndPieGraph', 61 'countries' => 'printTableAndPieGraph', 62 'resolution' => 'printTableAndScatterGraph', 63 'viewport' => 'printTableAndScatterGraph', 64 ] 65 ]; 66 67 /** @var array keeps a list of all real content pages, generated from above array */ 68 protected $allowedpages = []; 69 70 /** 71 * Initialize the helper 72 */ 73 public function __construct() 74 { 75 $this->hlp = plugin_load('helper', 'statistics'); 76 77 // remove pages that are not available because logging its data is disabled 78 if ($this->getConf('nolocation')) { 79 $this->pages['technology'] = array_diff($this->pages['technology'], ['countries']); 80 } 81 if ($this->getConf('nousers')) { 82 unset($this->pages['users']); 83 } 84 85 // build a list of pages 86 foreach ($this->pages as $key => $val) { 87 if (is_array($val)) { 88 $this->allowedpages = array_merge($this->allowedpages, $val); 89 } else { 90 $this->allowedpages[$key] = $val; 91 } 92 } 93 } 94 95 /** 96 * Access for managers allowed 97 */ 98 public function forAdminOnly() 99 { 100 return false; 101 } 102 103 /** 104 * return sort order for position in admin menu 105 */ 106 public function getMenuSort() 107 { 108 return 350; 109 } 110 111 /** 112 * handle user request 113 */ 114 public function handle() 115 { 116 global $INPUT; 117 $this->opt = preg_replace('/[^a-z]+/', '', $INPUT->str('opt')); 118 if (!isset($this->allowedpages[$this->opt])) $this->opt = 'dashboard'; 119 120 $this->start = $INPUT->int('s'); 121 $this->setTimeframe($INPUT->str('f', date('Y-m-d')), $INPUT->str('t', date('Y-m-d'))); 122 } 123 124 /** 125 * set limit clause 126 */ 127 public function setTimeframe($from, $to) 128 { 129 // swap if wrong order 130 if ($from > $to) [$from, $to] = [$to, $from]; 131 132 $this->hlp->getQuery()->setTimeFrame($from, $to); 133 $this->from = $from; 134 $this->to = $to; 135 } 136 137 /** 138 * Output the Statistics 139 */ 140 public function html() 141 { 142 echo '<script src="' . DOKU_BASE . 'lib/plugins/statistics/lib/chart.js"></script>'; 143 echo '<script src="' . DOKU_BASE . 'lib/plugins/statistics/lib/chartjs-plugin-datalabels.js"></script>'; 144 145 echo '<div id="plugin__statistics">'; 146 echo '<h1>' . $this->getLang('menu') . '</h1>'; 147 $this->html_timeselect(); 148 tpl_flush(); 149 150 151 $method = $this->allowedpages[$this->opt]; 152 if (method_exists($this, $method)) { 153 echo '<div class="plg_stats_' . $this->opt . '">'; 154 echo '<h2>' . $this->getLang($this->opt) . '</h2>'; 155 $this->$method($this->opt); 156 echo '</div>'; 157 } 158 echo '</div>'; 159 } 160 161 /** 162 * Return the TOC 163 * 164 * @return array 165 */ 166 public function getTOC() 167 { 168 $toc = []; 169 foreach ($this->pages as $key => $info) { 170 if (is_array($info)) { 171 $toc[] = html_mktocitem( 172 '', 173 $this->getLang($key), 174 1, 175 '' 176 ); 177 178 foreach (array_keys($info) as $page) { 179 $toc[] = html_mktocitem( 180 '?do=admin&page=statistics&opt=' . $page . 181 '&f=' . $this->from . 182 '&t=' . $this->to, 183 $this->getLang($page), 184 2, 185 '' 186 ); 187 } 188 } else { 189 $toc[] = html_mktocitem( 190 '?do=admin&page=statistics&opt=' . $key . 191 '&f=' . $this->from . 192 '&t=' . $this->to, 193 $this->getLang($key), 194 1, 195 '' 196 ); 197 } 198 } 199 return $toc; 200 } 201 202 /** 203 * @fixme instead of this, I would like the print* methods to call the Graph methods 204 */ 205 public function html_graph($name, $width, $height) 206 { 207 $this->hlp->getGraph($this->from, $this->to, $width, $height)->$name(); 208 } 209 210 /** 211 * Outputs pagination links 212 * 213 * @param int $limit 214 * @param int $next 215 */ 216 public function html_pager($limit, $next) 217 { 218 $params = [ 219 'do' => 'admin', 220 'page' => 'statistics', 221 'opt' => $this->opt, 222 'f' => $this->from, 223 't' => $this->to, 224 ]; 225 226 echo '<div class="plg_stats_pager">'; 227 if ($this->start > 0) { 228 $go = max($this->start - $limit, 0); 229 $params['s'] = $go; 230 echo '<a href="?' . buildURLparams($params) . '" class="prev button">' . $this->getLang('prev') . '</a>'; 231 } 232 233 if ($next) { 234 $go = $this->start + $limit; 235 $params['s'] = $go; 236 echo '<a href="?' . buildURLparams($params) . '" class="next button">' . $this->getLang('next') . '</a>'; 237 } 238 echo '</div>'; 239 } 240 241 /** 242 * Print the time selection menu 243 */ 244 public function html_timeselect() 245 { 246 $quick = [ 247 'today' => date('Y-m-d'), 248 'last1' => date('Y-m-d', time() - (60 * 60 * 24)), 249 'last7' => date('Y-m-d', time() - (60 * 60 * 24 * 7)), 250 'last30' => date('Y-m-d', time() - (60 * 60 * 24 * 30)), 251 ]; 252 253 254 echo '<div class="plg_stats_timeselect">'; 255 echo '<span>' . $this->getLang('time_select') . '</span> '; 256 257 echo '<form action="' . DOKU_SCRIPT . '" method="get">'; 258 echo '<input type="hidden" name="do" value="admin" />'; 259 echo '<input type="hidden" name="page" value="statistics" />'; 260 echo '<input type="hidden" name="opt" value="' . $this->opt . '" />'; 261 echo '<input type="date" name="f" value="' . $this->from . '" class="edit" />'; 262 echo '<input type="date" name="t" value="' . $this->to . '" class="edit" />'; 263 echo '<input type="submit" value="go" class="button" />'; 264 echo '</form>'; 265 266 echo '<ul>'; 267 foreach ($quick as $name => $time) { 268 // today is included only today 269 $to = $name == 'today' ? $quick['today'] : $quick['last1']; 270 271 $url = buildURLparams([ 272 'do' => 'admin', 273 'page' => 'statistics', 274 'opt' => $this->opt, 275 'f' => $time, 276 't' => $to, 277 ]); 278 279 echo '<li>'; 280 echo '<a href="?' . $url . '">'; 281 echo $this->getLang('time_' . $name); 282 echo '</a>'; 283 echo '</li>'; 284 } 285 echo '</ul>'; 286 287 echo '</div>'; 288 } 289 290 // region: Print functions for the different statistic pages 291 292 /** 293 * Print an introductionary screen 294 */ 295 public function printDashboard() 296 { 297 echo '<p>' . $this->getLang('intro_dashboard') . '</p>'; 298 299 // general info 300 echo '<div class="plg_stats_top">'; 301 $result = $this->hlp->getQuery()->aggregate(); 302 303 echo '<ul>'; 304 foreach (['pageviews', 'sessions', 'visitors', 'users', 'logins', 'current'] as $name) { 305 echo '<li><div class="li">' . sprintf($this->getLang('dash_' . $name), $result[$name]) . '</div></li>'; 306 } 307 echo '</ul>'; 308 309 echo '<ul>'; 310 foreach (['bouncerate', 'timespent', 'avgpages', 'newvisitors', 'registrations', 'last'] as $name) { 311 echo '<li><div class="li">' . sprintf($this->getLang('dash_' . $name), $result[$name]) . '</div></li>'; 312 } 313 echo '</ul>'; 314 315 $this->html_graph('dashboardviews', 700, 280); 316 $this->html_graph('dashboardwiki', 700, 280); 317 echo '</div>'; 318 319 $quickgraphs = [ 320 ['lbl' => 'dash_mostpopular', 'query' => 'pages', 'opt' => 'page'], 321 ['lbl' => 'dash_newincoming', 'query' => 'newreferer', 'opt' => 'newreferer'], 322 ['lbl' => 'dash_topsearch', 'query' => 'searchphrases', 'opt' => 'internalsearchphrases'], 323 ]; 324 325 foreach ($quickgraphs as $graph) { 326 $params = [ 327 'do' => 'admin', 328 'page' => 'statistics', 329 'f' => $this->from, 330 't' => $this->to, 331 'opt' => $graph['opt'], 332 ]; 333 334 echo '<div>'; 335 echo '<h2>' . $this->getLang($graph['lbl']) . '</h2>'; 336 $result = call_user_func([$this->hlp->getQuery(), $graph['query']]); 337 $this->html_resulttable($result); 338 echo '<p><a href="?' . buildURLparams($params) . '" class="more">' . $this->getLang('more') . '…</a></p>'; 339 echo '</div>'; 340 } 341 } 342 343 public function printHistory($name) 344 { 345 echo '<p>' . $this->getLang('intro_history') . '</p>'; 346 $this->html_graph('history_page_count', 600, 200); 347 $this->html_graph('history_page_size', 600, 200); 348 $this->html_graph('history_media_count', 600, 200); 349 $this->html_graph('history_media_size', 600, 200); 350 } 351 352 353 public function printTableAndPieGraph($name) { 354 echo '<p>' . $this->getLang("intro_$name") . '</p>'; 355 $this->html_graph($name, 300, 300); 356 $result = $this->hlp->getQuery()->$name(); 357 $this->html_resulttable($result, '', 150); 358 } 359 360 public function printTableAndScatterGraph() 361 { 362 echo '<p>' . $this->getLang('intro_resolution') . '</p>'; 363 $this->html_graph('resolution', 650, 490); 364 $result = $this->hlp->getQuery()->resolution(); 365 $this->html_resulttable($result, '', 150); 366 } 367 368 public function printTable($name) 369 { 370 echo '<p>' . $this->getLang("intro_$name") . '</p>'; 371 $result = $this->hlp->getQuery()->$name(); 372 $this->html_resulttable($result, '', 150); 373 } 374 375 376 public function printImages() 377 { 378 echo '<p>' . $this->getLang('intro_images') . '</p>'; 379 380 $result = $this->hlp->getQuery()->imagessum(); 381 echo '<p>'; 382 echo sprintf($this->getLang('trafficsum'), $result[0]['cnt'], filesize_h($result[0]['filesize'])); 383 echo '</p>'; 384 385 $result = $this->hlp->getQuery()->images(); 386 $this->html_resulttable($result, '', 150); 387 } 388 389 public function printDownloads() 390 { 391 echo '<p>' . $this->getLang('intro_downloads') . '</p>'; 392 393 $result = $this->hlp->getQuery()->downloadssum(); 394 echo '<p>'; 395 echo sprintf($this->getLang('trafficsum'), $result[0]['cnt'], filesize_h($result[0]['filesize'])); 396 echo '</p>'; 397 398 $result = $this->hlp->getQuery()->downloads(); 399 $this->html_resulttable($result, '', 150); 400 } 401 402 public function printReferer() 403 { 404 $result = $this->hlp->getQuery()->aggregate(); 405 406 if ($result['referers']) { 407 printf( 408 '<p>' . $this->getLang('intro_referer') . '</p>', 409 $result['referers'], 410 $result['direct'], 411 (100 * $result['direct'] / $result['referers']), 412 $result['search'], 413 (100 * $result['search'] / $result['referers']), 414 $result['external'], 415 (100 * $result['external'] / $result['referers']) 416 ); 417 } 418 419 $result = $this->hlp->getQuery()->referer(); 420 $this->html_resulttable($result, '', 150); 421 } 422 423 // endregion 424 425 426 /** 427 * Display a result in a HTML table 428 */ 429 public function html_resulttable($result, $header = '', $pager = 0) 430 { 431 echo '<table class="inline">'; 432 if (is_array($header)) { 433 echo '<tr>'; 434 foreach ($header as $h) { 435 echo '<th>' . hsc($h) . '</th>'; 436 } 437 echo '</tr>'; 438 } 439 440 $count = 0; 441 if (is_array($result)) foreach ($result as $row) { 442 echo '<tr>'; 443 foreach ($row as $k => $v) { 444 if ($k == 'res_x') continue; 445 if ($k == 'res_y') continue; 446 447 echo '<td class="plg_stats_X' . $k . '">'; 448 if ($k == 'page') { 449 echo '<a href="' . wl($v) . '" class="wikilink1">'; 450 echo hsc($v); 451 echo '</a>'; 452 } elseif ($k == 'media') { 453 echo '<a href="' . ml($v) . '" class="wikilink1">'; 454 echo hsc($v); 455 echo '</a>'; 456 } elseif ($k == 'filesize') { 457 echo filesize_h($v); 458 } elseif ($k == 'url') { 459 $url = hsc($v); 460 $url = preg_replace('/^https?:\/\/(www\.)?/', '', $url); 461 if (strlen($url) > 45) { 462 $url = substr($url, 0, 30) . ' … ' . substr($url, -15); 463 } 464 echo '<a href="' . $v . '" class="urlextern">'; 465 echo $url; 466 echo '</a>'; 467 } elseif ($k == 'ilookup') { 468 echo '<a href="' . wl('', ['id' => $v, 'do' => 'search']) . '">Search</a>'; 469 } elseif ($k == 'engine') { 470 $name = SearchEngines::getName($v); 471 $url = SearchEngines::getURL($v); 472 if ($url) { 473 echo '<a href="' . $url . '">' . hsc($name) . '</a>'; 474 } else { 475 echo hsc($name); 476 } 477 } elseif ($k == 'html') { 478 echo $v; 479 } else { 480 echo hsc($v); 481 } 482 echo '</td>'; 483 } 484 echo '</tr>'; 485 486 if ($pager && ($count == $pager)) break; 487 $count++; 488 } 489 echo '</table>'; 490 491 if ($pager) $this->html_pager($pager, count($result) > $pager); 492 } 493} 494