1<?php 2 3namespace dokuwiki\plugin\statistics; 4 5use dokuwiki\plugin\sqlite\SQLiteDB; 6use helper_plugin_statistics; 7 8/** 9 * This class defines a bunch of SQL queries to fetch various statistics from the database 10 */ 11class Query 12{ 13 protected helper_plugin_statistics $hlp; 14 protected SQLiteDB $db; 15 protected string $from; 16 protected string $to; 17 protected string $limit = ''; 18 19 /** 20 * @param helper_plugin_statistics $hlp 21 */ 22 public function __construct(helper_plugin_statistics $hlp) 23 { 24 $this->hlp = $hlp; 25 $this->db = $hlp->getDB(); 26 $today = date('Y-m-d'); 27 $this->setTimeFrame($today, $today); 28 $this->setPagination(0, 20); 29 } 30 31 /** 32 * Set the time frame for all queries 33 * 34 * @param string $from The start date as YYYY-MM-DD 35 * @param string $to The end date as YYYY-MM-DD 36 */ 37 public function setTimeFrame(string $from, string $to): void 38 { 39 try { 40 $from = new \DateTime($from); 41 $to = new \DateTime($to); 42 } catch (\Exception $e) { 43 $from = new \DateTime(); 44 $to = new \DateTime(); 45 } 46 $from->setTime(0, 0); 47 $to->setTime(23, 59, 59); 48 49 $this->from = $from->format('Y-m-d H:i:s'); 50 $this->to = $to->format('Y-m-d H:i:s'); 51 } 52 53 /** 54 * Set the pagination settings for some queries 55 * 56 * @param int $start The start offset 57 * @param int $limit The number of results. If one more is returned, there is another page 58 * @return void 59 */ 60 public function setPagination(int $start, int $limit) 61 { 62 // when a limit is set, one more is fetched to indicate when a next page exists 63 if ($limit) $limit += 1; 64 65 if ($limit) { 66 $this->limit = " LIMIT $start,$limit"; 67 } elseif ($start) { 68 $this->limit = " OFFSET $start"; 69 } 70 } 71 72 /** 73 * Return some aggregated statistics 74 */ 75 public function aggregate(): array 76 { 77 // init some values that might not be set 78 $data = [ 79 'referers' => 0, // total number of (external) referrers 80 'external' => 0, // external referrers 81 'search' => 0, // search engine referrers 82 'direct' => 0, // direct referrers 83 'internal' => 0, // internal referrers 84 'bouncerate' => 0, 85 'newvisitors' => 0, 86 ]; 87 88 // Count referrer types by joining with referers table 89 $sql = "SELECT 90 CASE 91 WHEN R.engine IS NOT NULL THEN 'search' 92 WHEN R.url = '' THEN 'direct' 93 WHEN R.url IS NOT NULL THEN 'external' 94 ELSE 'internal' 95 END as ref_type, 96 COUNT(*) as cnt 97 FROM pageviews as P 98 LEFT JOIN referers as R ON P.ref_id = R.id 99 LEFT JOIN sessions as S ON P.session = S.session 100 WHERE P.dt >= ? AND P.dt <= ? 101 AND S.ua_type = ? 102 GROUP BY ref_type"; 103 $result = $this->db->queryAll($sql, [$this->from, $this->to, 'browser']); 104 105 foreach ($result as $row) { 106 if ($row['ref_type'] == 'search') { 107 $data['search'] = $row['cnt']; 108 $data['referers'] += $row['cnt']; 109 } 110 if ($row['ref_type'] == 'direct') { 111 $data['direct'] = $row['cnt']; 112 $data['referers'] += $row['cnt']; 113 } 114 if ($row['ref_type'] == 'external') { 115 $data['external'] = $row['cnt']; 116 $data['referers'] += $row['cnt']; 117 } 118 if ($row['ref_type'] == 'internal') { 119 $data['internal'] = $row['cnt']; 120 } 121 } 122 123 // general user and session info 124 $sql = "SELECT COUNT(DISTINCT P.session) as sessions, 125 COUNT(P.session) as views, 126 COUNT(DISTINCT S.user) as users, 127 COUNT(DISTINCT S.uid) as visitors 128 FROM pageviews as P 129 LEFT JOIN sessions as S ON P.session = S.session 130 WHERE P.dt >= ? AND P.dt <= ? 131 AND S.ua_type = ?"; 132 $result = $this->db->queryRecord($sql, [$this->from, $this->to, 'browser']); 133 134 $data['users'] = $result['users']; 135 $data['sessions'] = $result['sessions']; 136 $data['pageviews'] = $result['views']; 137 $data['visitors'] = $result['visitors']; 138 139 // calculate bounce rate (sessions with only 1 page view) 140 if ($data['sessions']) { 141 $sql = "SELECT COUNT(*) as cnt 142 FROM ( 143 SELECT P.session, COUNT(*) as views 144 FROM pageviews as P 145 LEFT JOIN sessions as S ON P.session = S.session 146 WHERE P.dt >= ? AND P.dt <= ? 147 AND S.ua_type = ? 148 GROUP BY P.session 149 HAVING views = 1 150 )"; 151 $count = $this->db->queryValue($sql, [$this->from, $this->to, 'browser']); 152 $data['bouncerate'] = $count * 100 / $data['sessions']; 153 $data['newvisitors'] = $count * 100 / $data['sessions']; 154 } 155 156 // calculate avg. number of views per session 157 $sql = "SELECT AVG(views) as cnt 158 FROM ( 159 SELECT P.session, COUNT(*) as views 160 FROM pageviews as P 161 LEFT JOIN sessions as S ON P.session = S.session 162 WHERE P.dt >= ? AND P.dt <= ? 163 AND S.ua_type = ? 164 GROUP BY P.session 165 )"; 166 $data['avgpages'] = $this->db->queryValue($sql, [$this->from, $this->to, 'browser']); 167 168 // average time spent on the site 169 $sql = "SELECT AVG((julianday(end) - julianday(dt)) * 24 * 60) as time 170 FROM sessions as S 171 WHERE S.dt >= ? AND S.dt <= ? 172 AND S.dt != S.end 173 AND DATE(S.dt) = DATE(S.end) 174 AND S.ua_type = ?"; 175 $data['timespent'] = $this->db->queryValue($sql, [$this->from, $this->to, 'browser']); 176 177 // logins 178 $sql = "SELECT COUNT(*) as logins 179 FROM logins as A 180 WHERE A.dt >= ? AND A.dt <= ? 181 AND (type = ? OR type = ?)"; 182 $data['logins'] = $this->db->queryValue($sql, [$this->from, $this->to, 'l', 'p']); 183 184 // registrations 185 $sql = "SELECT COUNT(*) as registrations 186 FROM logins as A 187 WHERE A.dt >= ? AND A.dt <= ? 188 AND type = ?"; 189 $data['registrations'] = $this->db->queryValue($sql, [$this->from, $this->to, 'C']); 190 191 // current users (based on recent sessions) 192 $sql = "SELECT COUNT(DISTINCT uid) as current 193 FROM sessions 194 WHERE end >= datetime('now', '-10 minutes')"; 195 $data['current'] = $this->db->queryValue($sql); 196 197 return $data; 198 } 199 200 201 /** 202 * Return some trend data about visits and edits in the wiki 203 * 204 * @param bool $hours Use hour resolution rather than days 205 * @return array 206 */ 207 public function dashboardviews(bool $hours = false): array 208 { 209 if ($hours) { 210 $TIME = 'strftime(\'%H\', P.dt)'; 211 } else { 212 $TIME = 'DATE(P.dt)'; 213 } 214 215 $data = []; 216 217 // access trends 218 $sql = "SELECT $TIME as time, 219 COUNT(DISTINCT P.session) as sessions, 220 COUNT(P.session) as pageviews, 221 COUNT(DISTINCT S.uid) as visitors 222 FROM pageviews as P 223 LEFT JOIN sessions as S ON P.session = S.session 224 WHERE P.dt >= ? AND P.dt <= ? 225 AND S.ua_type = ? 226 GROUP BY $TIME 227 ORDER BY time"; 228 $result = $this->db->queryAll($sql, [$this->from, $this->to, 'browser']); 229 foreach ($result as $row) { 230 $data[$row['time']]['sessions'] = $row['sessions']; 231 $data[$row['time']]['pageviews'] = $row['pageviews']; 232 $data[$row['time']]['visitors'] = $row['visitors']; 233 } 234 return $data; 235 } 236 237 /** 238 * @param bool $hours Use hour resolution rather than days 239 * @return array 240 */ 241 public function dashboardwiki(bool $hours = false): array 242 { 243 if ($hours) { 244 $TIME = 'strftime(\'%H\', dt)'; 245 } else { 246 $TIME = 'DATE(dt)'; 247 } 248 249 $data = []; 250 251 // edit trends 252 foreach (['E', 'C', 'D'] as $type) { 253 $sql = "SELECT $TIME as time, 254 COUNT(*) as cnt 255 FROM edits as A 256 WHERE A.dt >= ? AND A.dt <= ? 257 AND type = ? 258 GROUP BY $TIME 259 ORDER BY time"; 260 $result = $this->db->queryAll($sql, [$this->from, $this->to, $type]); 261 foreach ($result as $row) { 262 $data[$row['time']][$type] = $row['cnt']; 263 } 264 } 265 ksort($data); 266 return $data; 267 } 268 269 /** 270 * @param string $info Which type of history to select (FIXME which ones are there?) 271 * @param string $interval Group data by this interval (days, weeks, months) 272 * @return array 273 */ 274 public function history(string $info, string $interval = 'day'): array 275 { 276 if ($interval == 'weeks') { 277 $TIME = 'strftime(\'%Y\', dt), strftime(\'%W\', dt)'; 278 } elseif ($interval == 'months') { 279 $TIME = 'strftime(\'%Y-%m\', dt)'; 280 } else { 281 $TIME = 'strftime(\'%d-%m\', dt)'; 282 } 283 284 $mod = 1; 285 if ($info == 'media_size' || $info == 'page_size') { 286 $mod = 1024 * 1024; 287 } 288 289 $sql = "SELECT $TIME as time, 290 AVG(value)/$mod as cnt 291 FROM history as A 292 WHERE A.dt >= ? AND A.dt <= ? 293 AND info = ? 294 GROUP BY $TIME 295 ORDER BY $TIME"; 296 return $this->db->queryAll($sql, [$this->from, $this->to, $info]); 297 } 298 299 /** 300 * @return array 301 */ 302 public function searchengines(): array 303 { 304 $sql = "SELECT COUNT(*) as cnt, R.engine 305 FROM pageviews as P 306 LEFT JOIN referers as R ON P.ref_id = R.id 307 WHERE P.dt >= ? AND P.dt <= ? 308 AND R.engine IS NOT NULL 309 AND R.engine != '' 310 GROUP BY R.engine 311 ORDER BY cnt DESC, R.engine" . 312 $this->limit; 313 return $this->db->queryAll($sql, [$this->from, $this->to]); 314 } 315 316 /** 317 * @return array 318 */ 319 public function searchphrases(): array 320 { 321 $sql = "SELECT COUNT(*) as cnt, query, query as ilookup 322 FROM search 323 WHERE dt >= ? AND dt <= ? 324 GROUP BY query 325 ORDER BY cnt DESC, query" . 326 $this->limit; 327 return $this->db->queryAll($sql, [$this->from, $this->to]); 328 } 329 330 /** 331 * @return array 332 */ 333 public function searchwords(): array 334 { 335 $sql = "SELECT COUNT(*) as cnt, SW.word, SW.word as ilookup 336 FROM search as S, 337 searchwords as SW 338 WHERE S.dt >= ? AND S.dt <= ? 339 AND S.id = SW.sid 340 GROUP BY SW.word 341 ORDER BY cnt DESC, SW.word" . 342 $this->limit; 343 return $this->db->queryAll($sql, [$this->from, $this->to]); 344 } 345 346 /** 347 * @return array 348 */ 349 public function outlinks(): array 350 { 351 $sql = "SELECT COUNT(*) as cnt, link as url 352 FROM outlinks as A 353 WHERE A.dt >= ? AND A.dt <= ? 354 GROUP BY link 355 ORDER BY cnt DESC, link" . 356 $this->limit; 357 return $this->db->queryAll($sql, [$this->from, $this->to]); 358 } 359 360 /** 361 * @return array 362 */ 363 public function pages(): array 364 { 365 $sql = "SELECT COUNT(*) as cnt, P.page 366 FROM pageviews as P 367 LEFT JOIN sessions as S ON P.session = S.session 368 WHERE P.dt >= ? AND P.dt <= ? 369 AND S.ua_type = ? 370 GROUP BY P.page 371 ORDER BY cnt DESC, P.page" . 372 $this->limit; 373 return $this->db->queryAll($sql, [$this->from, $this->to, 'browser']); 374 } 375 376 /** 377 * @return array 378 */ 379 public function edits(): array 380 { 381 $sql = "SELECT COUNT(*) as cnt, page 382 FROM edits as A 383 WHERE A.dt >= ? AND A.dt <= ? 384 GROUP BY page 385 ORDER BY cnt DESC, page" . 386 $this->limit; 387 return $this->db->queryAll($sql, [$this->from, $this->to]); 388 } 389 390 /** 391 * @return array 392 */ 393 public function images(): array 394 { 395 $sql = "SELECT COUNT(*) as cnt, media, SUM(size) as filesize 396 FROM media as A 397 WHERE A.dt >= ? AND A.dt <= ? 398 AND mime1 = ? 399 GROUP BY media 400 ORDER BY cnt DESC, media" . 401 $this->limit; 402 return $this->db->queryAll($sql, [$this->from, $this->to, 'image']); 403 } 404 405 /** 406 * @return array 407 */ 408 public function imagessum(): array 409 { 410 $sql = "SELECT COUNT(*) as cnt, SUM(size) as filesize 411 FROM media as A 412 WHERE A.dt >= ? AND A.dt <= ? 413 AND mime1 = ?"; 414 return $this->db->queryAll($sql, [$this->from, $this->to, 'image']); 415 } 416 417 /** 418 * @return array 419 */ 420 public function downloads(): array 421 { 422 $sql = "SELECT COUNT(*) as cnt, media, SUM(size) as filesize 423 FROM media as A 424 WHERE A.dt >= ? AND A.dt <= ? 425 AND mime1 != ? 426 GROUP BY media 427 ORDER BY cnt DESC, media" . 428 $this->limit; 429 return $this->db->queryAll($sql, [$this->from, $this->to, 'image']); 430 } 431 432 /** 433 * @return array 434 */ 435 public function downloadssum(): array 436 { 437 $sql = "SELECT COUNT(*) as cnt, SUM(size) as filesize 438 FROM media as A 439 WHERE A.dt >= ? AND A.dt <= ? 440 AND mime1 != ?"; 441 return $this->db->queryAll($sql, [$this->from, $this->to, 'image']); 442 } 443 444 /** 445 * @return array 446 */ 447 public function referer(): array 448 { 449 $sql = "SELECT COUNT(*) as cnt, R.url 450 FROM pageviews as P 451 LEFT JOIN sessions as S ON P.session = S.session 452 LEFT JOIN referers as R ON P.ref_id = R.id 453 WHERE P.dt >= ? AND P.dt <= ? 454 AND S.ua_type = ? 455 AND R.url IS NOT NULL 456 AND R.url != '' 457 AND R.engine IS NULL 458 GROUP BY R.url 459 ORDER BY cnt DESC, R.url" . 460 $this->limit; 461 return $this->db->queryAll($sql, [$this->from, $this->to, 'browser']); 462 } 463 464 /** 465 * @return array 466 */ 467 public function newreferer(): array 468 { 469 $sql = "SELECT COUNT(*) as cnt, R.url 470 FROM pageviews as P 471 LEFT JOIN sessions as S ON P.session = S.session 472 LEFT JOIN referers as R ON P.ref_id = R.id 473 WHERE P.dt >= ? AND P.dt <= ? 474 AND S.ua_type = ? 475 AND R.url IS NOT NULL 476 AND R.url != '' 477 AND R.engine IS NULL 478 AND R.dt >= ? 479 GROUP BY R.url 480 ORDER BY cnt DESC, R.url" . 481 $this->limit; 482 return $this->db->queryAll($sql, [$this->from, $this->to, 'browser', $this->from]); 483 } 484 485 /** 486 * @return array 487 */ 488 public function countries(): array 489 { 490 $sql = "SELECT COUNT(DISTINCT P.session) as cnt, I.country 491 FROM pageviews as P 492 LEFT JOIN iplocation as I ON P.ip = I.ip 493 WHERE P.dt >= ? AND P.dt <= ? 494 AND I.country IS NOT NULL 495 AND I.country != '' 496 GROUP BY I.code 497 ORDER BY cnt DESC, I.country" . 498 $this->limit; 499 return $this->db->queryAll($sql, [$this->from, $this->to]); 500 } 501 502 /** 503 * @param bool $ext return extended information 504 * @return array 505 */ 506 public function browsers(bool $ext = false): array 507 { 508 if ($ext) { 509 $sel = 'S.ua_info as browser, S.ua_ver'; 510 $grp = 'S.ua_info, S.ua_ver'; 511 } else { 512 $sel = 'S.ua_info as browser'; 513 $grp = 'S.ua_info'; 514 } 515 516 $sql = "SELECT COUNT(DISTINCT S.session) as cnt, $sel 517 FROM sessions as S 518 WHERE S.dt >= ? AND S.dt <= ? 519 AND S.ua_type = ? 520 GROUP BY $grp 521 ORDER BY cnt DESC, S.ua_info" . 522 $this->limit; 523 return $this->db->queryAll($sql, [$this->from, $this->to, 'browser']); 524 } 525 526 /** 527 * @return array 528 */ 529 public function os(): array 530 { 531 $sql = "SELECT COUNT(DISTINCT S.session) as cnt, S.os 532 FROM sessions as S 533 WHERE S.dt >= ? AND S.dt <= ? 534 AND S.ua_type = ? 535 GROUP BY S.os 536 ORDER BY cnt DESC, S.os" . 537 $this->limit; 538 return $this->db->queryAll($sql, [$this->from, $this->to, 'browser']); 539 } 540 541 /** 542 * @return array 543 */ 544 public function topuser(): array 545 { 546 $sql = "SELECT COUNT(*) as cnt, S.user 547 FROM pageviews as P 548 LEFT JOIN sessions as S ON P.session = S.session 549 WHERE P.dt >= ? AND P.dt <= ? 550 AND S.ua_type = ? 551 AND S.user IS NOT NULL 552 AND S.user != ? 553 GROUP BY S.user 554 ORDER BY cnt DESC, S.user" . 555 $this->limit; 556 return $this->db->queryAll($sql, [$this->from, $this->to, 'browser', '']); 557 } 558 559 /** 560 * @return array 561 */ 562 public function topeditor(): array 563 { 564 $sql = "SELECT COUNT(*) as cnt, user 565 FROM edits as E 566 LEFT JOIN sessions as S ON E.session = S.session 567 WHERE E.dt >= ? AND E.dt <= ? 568 AND S.user != ? 569 GROUP BY user 570 ORDER BY cnt DESC, user" . 571 $this->limit; 572 return $this->db->queryAll($sql, [$this->from, $this->to, '']); 573 } 574 575 /** 576 * @return array 577 */ 578 public function topgroup(): array 579 { 580 $sql = "SELECT COUNT(*) as cnt, G.`group` 581 FROM pageviews as P 582 LEFT JOIN sessions as S ON P.session = S.session 583 LEFT JOIN groups as G ON S.user = G.user 584 WHERE P.dt >= ? AND P.dt <= ? 585 AND S.ua_type = ? 586 AND G.`group` IS NOT NULL 587 GROUP BY G.`group` 588 ORDER BY cnt DESC, G.`group`" . 589 $this->limit; 590 return $this->db->queryAll($sql, [$this->from, $this->to, 'browser']); 591 } 592 593 /** 594 * @return array 595 */ 596 public function topgroupedit(): array 597 { 598 $sql = "SELECT COUNT(*) as cnt, G.`group` 599 FROM edits as E 600 LEFT JOIN sessions as S ON E.session = S.session 601 LEFT JOIN groups as G ON S.user = G.user 602 WHERE E.dt >= ? AND E.dt <= ? 603 AND G.`group` IS NOT NULL 604 GROUP BY G.`group` 605 ORDER BY cnt DESC, G.`group`" . 606 $this->limit; 607 return $this->db->queryAll($sql, [$this->from, $this->to]); 608 } 609 610 611 /** 612 * @return array 613 */ 614 public function resolution(): array 615 { 616 $sql = "SELECT COUNT(DISTINCT S.uid) as cnt, 617 ROUND(P.screen_x/100)*100 as res_x, 618 ROUND(P.screen_y/100)*100 as res_y, 619 CAST(ROUND(P.screen_x/100)*100 AS int) || 'x' || CAST(ROUND(P.screen_y/100)*100 AS int) as resolution 620 FROM pageviews as P 621 LEFT JOIN sessions as S ON P.session = S.session 622 WHERE P.dt >= ? AND P.dt <= ? 623 AND S.ua_type = ? 624 AND P.screen_x != ? 625 AND P.screen_y != ? 626 GROUP BY resolution 627 ORDER BY cnt DESC" . 628 $this->limit; 629 return $this->db->queryAll($sql, [$this->from, $this->to, 'browser', 0, 0]); 630 } 631 632 /** 633 * @return array 634 */ 635 public function viewport(): array 636 { 637 $sql = "SELECT COUNT(DISTINCT S.uid) as cnt, 638 ROUND(P.view_x/100)*100 as res_x, 639 ROUND(P.view_y/100)*100 as res_y, 640 CAST(ROUND(P.view_x/100)*100 AS int) || 'x' || CAST(ROUND(P.view_y/100)*100 AS int) as resolution 641 FROM pageviews as P 642 LEFT JOIN sessions as S ON P.session = S.session 643 WHERE P.dt >= ? AND P.dt <= ? 644 AND S.ua_type = ? 645 AND P.view_x != ? 646 AND P.view_y != ? 647 GROUP BY resolution 648 ORDER BY cnt DESC" . 649 $this->limit; 650 651 return $this->db->queryAll($sql, [$this->from, $this->to, 'browser', 0, 0]); 652 } 653 654 /** 655 * @return array 656 */ 657 public function seenusers(): array 658 { 659 $sql = "SELECT `user`, MAX(`dt`) as dt 660 FROM users 661 WHERE `user` IS NOT NULL 662 AND `user` != '' 663 GROUP BY `user` 664 ORDER BY `dt` DESC" . 665 $this->limit; 666 667 return $this->db->queryAll($sql); 668 } 669} 670