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 $data = []; 78 79 $sql = "SELECT ref_type, COUNT(*) as cnt 80 FROM access as A 81 WHERE A.dt >= ? AND A.dt <= ? 82 AND ua_type = ? 83 GROUP BY ref_type"; 84 $result = $this->db->queryAll($sql, [$this->from, $this->to, 'browser']); 85 86 foreach ($result as $row) { 87 if ($row['ref_type'] == 'search') $data['search'] = $row['cnt']; 88 if ($row['ref_type'] == 'external') $data['external'] = $row['cnt']; 89 if ($row['ref_type'] == 'internal') $data['internal'] = $row['cnt']; 90 if ($row['ref_type'] == '') $data['direct'] = $row['cnt']; 91 } 92 93 // general user and session info 94 $sql = "SELECT COUNT(DISTINCT session) as sessions, 95 COUNT(session) as views, 96 COUNT(DISTINCT user) as users, 97 COUNT(DISTINCT uid) as visitors 98 FROM access as A 99 WHERE A.dt >= ? AND A.dt <= ? 100 AND ua_type = ?"; 101 $result = $this->db->queryRecord($sql, [$this->from, $this->to, 'browser']); 102 103 $data['users'] = max($result['users'] - 1, 0); // subtract empty user 104 $data['sessions'] = $result['sessions']; 105 $data['pageviews'] = $result['views']; 106 $data['visitors'] = $result['visitors']; 107 108 // calculate bounce rate 109 if ($data['sessions']) { 110 $sql = "SELECT COUNT(*) as cnt 111 FROM session as A 112 WHERE A.dt >= ? AND A.dt <= ? 113 AND views = ?"; 114 $count = $this->db->queryValue($sql, [$this->from, $this->to, 1]); 115 $data['bouncerate'] = $count * 100 / $data['sessions']; 116 $data['newvisitors'] = $count * 100 / $data['sessions']; 117 } 118 119 // calculate avg. number of views per session 120 $sql = "SELECT AVG(views) as cnt 121 FROM session as A 122 WHERE A.dt >= ? AND A.dt <= ?"; 123 $data['avgpages'] = $this->db->queryValue($sql, [$this->from, $this->to]); 124 125 // average time spent on the site 126 $sql = "SELECT AVG(end - dt)/60 as time 127 FROM session as A 128 WHERE A.dt >= ? AND A.dt <= ? 129 AND dt != end 130 AND DATE(dt) = DATE(end)"; 131 $data['timespent'] = $this->db->queryValue($sql, [$this->from, $this->to]); 132 133 // logins 134 $sql = "SELECT COUNT(*) as logins 135 FROM logins as A 136 WHERE A.dt >= ? AND A.dt <= ? 137 AND (type = ? OR type = ?)"; 138 $data['logins'] = $this->db->queryValue($sql, [$this->from, $this->to, 'l', 'p']); 139 140 // registrations 141 $sql = "SELECT COUNT(*) as registrations 142 FROM logins as A 143 WHERE A.dt >= ? AND A.dt <= ? 144 AND type = ?"; 145 $data['registrations'] = $this->db->queryValue($sql, [$this->from, $this->to, 'C']); 146 147 // current users 148 $sql = "SELECT COUNT(*) as current 149 FROM lastseen 150 WHERE dt >= datetime('now', '-10 minutes')"; 151 $data['current'] = $this->db->queryValue($sql); 152 153 return $data; 154 } 155 156 157 /** 158 * Return some trend data about visits and edits in the wiki 159 * 160 * @param bool $hours Use hour resolution rather than days 161 * @return array 162 */ 163 public function dashboardviews(bool $hours = false): array 164 { 165 if ($hours) { 166 $TIME = 'strftime(\'%H\', dt)'; 167 } else { 168 $TIME = 'DATE(dt)'; 169 } 170 171 $data = []; 172 173 // access trends 174 $sql = "SELECT $TIME as time, 175 COUNT(DISTINCT session) as sessions, 176 COUNT(session) as pageviews, 177 COUNT(DISTINCT uid) as visitors 178 FROM access as A 179 WHERE A.dt >= ? AND A.dt <= ? 180 AND ua_type = ? 181 GROUP BY $TIME 182 ORDER BY time"; 183 $result = $this->db->queryAll($sql, [$this->from, $this->to, 'browser']); 184 foreach ($result as $row) { 185 $data[$row['time']]['sessions'] = $row['sessions']; 186 $data[$row['time']]['pageviews'] = $row['pageviews']; 187 $data[$row['time']]['visitors'] = $row['visitors']; 188 } 189 return $data; 190 } 191 192 /** 193 * @param bool $hours Use hour resolution rather than days 194 * @return array 195 */ 196 public function dashboardwiki(bool $hours = false): array 197 { 198 if ($hours) { 199 $TIME = 'strftime(\'%H\', dt)'; 200 } else { 201 $TIME = 'DATE(dt)'; 202 } 203 204 $data = []; 205 206 // edit trends 207 foreach (['E', 'C', 'D'] as $type) { 208 $sql = "SELECT $TIME as time, 209 COUNT(*) as cnt 210 FROM edits as A 211 WHERE A.dt >= ? AND A.dt <= ? 212 AND type = ? 213 GROUP BY $TIME 214 ORDER BY time"; 215 $result = $this->db->queryAll($sql, [$this->from, $this->to, $type]); 216 foreach ($result as $row) { 217 $data[$row['time']][$type] = $row['cnt']; 218 } 219 } 220 ksort($data); 221 return $data; 222 } 223 224 /** 225 * @param string $info Which type of history to select (FIXME which ones are there?) 226 * @param string $interval Group data by this interval (days, weeks, months) 227 * @return array 228 */ 229 public function history(string $info, string $interval = 'day'): array 230 { 231 if ($interval == 'weeks') { 232 $TIME = 'strftime(\'%Y\', dt), strftime(\'%W\', dt)'; 233 } elseif ($interval == 'months') { 234 $TIME = 'strftime(\'%Y-%m\', dt)'; 235 } else { 236 $TIME = 'strftime(\'%d-%m\', dt)'; 237 } 238 239 $mod = 1; 240 if ($info == 'media_size' || $info == 'page_size') { 241 $mod = 1024 * 1024; 242 } 243 244 $sql = "SELECT $TIME as time, 245 AVG(value)/$mod as cnt 246 FROM history as A 247 WHERE A.dt >= ? AND A.dt <= ? 248 AND info = ? 249 GROUP BY $TIME 250 ORDER BY $TIME"; 251 return $this->db->queryAll($sql, [$this->from, $this->to, $info]); 252 } 253 254 /** 255 * @return array 256 */ 257 public function searchengines(): array 258 { 259 $sql = "SELECT COUNT(*) as cnt, engine as eflag, engine 260 FROM search as A 261 WHERE A.dt >= ? AND A.dt <= ? 262 GROUP BY engine 263 ORDER BY cnt DESC, engine" . 264 $this->limit; 265 return $this->db->queryAll($sql, [$this->from, $this->to]); 266 } 267 268 /** 269 * @param bool $extern Limit results to external search engine (true) or dokuwiki (false) 270 * @return array 271 */ 272 public function searchphrases(bool $extern = false): array 273 { 274 if ($extern) { 275 $WHERE = "engine != ?"; 276 $engineParam = 'dokuwiki'; 277 $I = ''; 278 } else { 279 $WHERE = "engine = ?"; 280 $engineParam = 'dokuwiki'; 281 $I = 'i'; 282 } 283 $sql = "SELECT COUNT(*) as cnt, query, query as ${I}lookup 284 FROM search as A 285 WHERE A.dt >= ? AND A.dt <= ? 286 AND $WHERE 287 GROUP BY query 288 ORDER BY cnt DESC, query" . 289 $this->limit; 290 return $this->db->queryAll($sql, [$this->from, $this->to, $engineParam]); 291 } 292 293 /** 294 * @param bool $extern Limit results to external search engine (true) or dokuwiki (false) 295 * @return array 296 */ 297 public function searchwords(bool $extern = false): array 298 { 299 if ($extern) { 300 $WHERE = "engine != ?"; 301 $engineParam = 'dokuwiki'; 302 $I = ''; 303 } else { 304 $WHERE = "engine = ?"; 305 $engineParam = 'dokuwiki'; 306 $I = 'i'; 307 } 308 $sql = "SELECT COUNT(*) as cnt, word, word as ${I}lookup 309 FROM search as A, 310 searchwords as B 311 WHERE A.dt >= ? AND A.dt <= ? 312 AND A.id = B.sid 313 AND $WHERE 314 GROUP BY word 315 ORDER BY cnt DESC, word" . 316 $this->limit; 317 return $this->db->queryAll($sql, [$this->from, $this->to, $engineParam]); 318 } 319 320 /** 321 * @return array 322 */ 323 public function outlinks(): array 324 { 325 $sql = "SELECT COUNT(*) as cnt, link as url 326 FROM outlinks as A 327 WHERE A.dt >= ? AND A.dt <= ? 328 GROUP BY link 329 ORDER BY cnt DESC, link" . 330 $this->limit; 331 return $this->db->queryAll($sql, [$this->from, $this->to]); 332 } 333 334 /** 335 * @return array 336 */ 337 public function pages(): array 338 { 339 $sql = "SELECT COUNT(*) as cnt, page 340 FROM access as A 341 WHERE A.dt >= ? AND A.dt <= ? 342 AND ua_type = ? 343 GROUP BY page 344 ORDER BY cnt DESC, page" . 345 $this->limit; 346 return $this->db->queryAll($sql, [$this->from, $this->to, 'browser']); 347 } 348 349 /** 350 * @return array 351 */ 352 public function edits(): array 353 { 354 $sql = "SELECT COUNT(*) as cnt, page 355 FROM edits as A 356 WHERE A.dt >= ? AND A.dt <= ? 357 GROUP BY page 358 ORDER BY cnt DESC, page" . 359 $this->limit; 360 return $this->db->queryAll($sql, [$this->from, $this->to]); 361 } 362 363 /** 364 * @return array 365 */ 366 public function images(): array 367 { 368 $sql = "SELECT COUNT(*) as cnt, media, SUM(size) as filesize 369 FROM media as A 370 WHERE A.dt >= ? AND A.dt <= ? 371 AND mime1 = ? 372 GROUP BY media 373 ORDER BY cnt DESC, media" . 374 $this->limit; 375 return $this->db->queryAll($sql, [$this->from, $this->to, 'image']); 376 } 377 378 /** 379 * @return array 380 */ 381 public function imagessum(): array 382 { 383 $sql = "SELECT COUNT(*) as cnt, SUM(size) as filesize 384 FROM media as A 385 WHERE A.dt >= ? AND A.dt <= ? 386 AND mime1 = ?"; 387 return $this->db->queryAll($sql, [$this->from, $this->to, 'image']); 388 } 389 390 /** 391 * @return array 392 */ 393 public function downloads(): 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 downloadssum(): 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 referer(): array 421 { 422 $sql = "SELECT COUNT(*) as cnt, ref as url 423 FROM access as A 424 WHERE A.dt >= ? AND A.dt <= ? 425 AND ua_type = ? 426 AND ref_type = ? 427 GROUP BY ref_md5 428 ORDER BY cnt DESC, url" . 429 $this->limit; 430 return $this->db->queryAll($sql, [$this->from, $this->to, 'browser', 'external']); 431 } 432 433 /** 434 * @return array 435 */ 436 public function newreferer(): array 437 { 438 $sql = "SELECT COUNT(*) as cnt, ref as url 439 FROM access as B, 440 refseen as A 441 WHERE B.dt >= ? AND B.dt <= ? 442 AND ua_type = ? 443 AND ref_type = ? 444 AND A.ref_md5 = B.ref_md5 445 GROUP BY A.ref_md5 446 ORDER BY cnt DESC, url" . 447 $this->limit; 448 return $this->db->queryAll($sql, [$this->from, $this->to, 'browser', 'external']); 449 } 450 451 /** 452 * @return array 453 */ 454 public function countries(): array 455 { 456 $sql = "SELECT COUNT(DISTINCT session) as cnt, B.code AS cflag, B.country 457 FROM access as A, 458 iplocation as B 459 WHERE A.dt >= ? AND A.dt <= ? 460 AND A.ip = B.ip 461 GROUP BY B.code 462 ORDER BY cnt DESC, B.country" . 463 $this->limit; 464 return $this->db->queryAll($sql, [$this->from, $this->to]); 465 } 466 467 /** 468 * @param bool $ext return extended information 469 * @return array 470 */ 471 public function browsers(bool $ext = true): array 472 { 473 if ($ext) { 474 $sel = 'ua_info as bflag, ua_info as browser, ua_ver'; 475 $grp = 'ua_info, ua_ver'; 476 } else { 477 $grp = 'ua_info'; 478 $sel = 'ua_info'; 479 } 480 481 $sql = "SELECT COUNT(DISTINCT session) as cnt, $sel 482 FROM access as A 483 WHERE A.dt >= ? AND A.dt <= ? 484 AND ua_type = ? 485 GROUP BY $grp 486 ORDER BY cnt DESC, ua_info" . 487 $this->limit; 488 return $this->db->queryAll($sql, [$this->from, $this->to, 'browser']); 489 } 490 491 /** 492 * @return array 493 */ 494 public function os(): array 495 { 496 $sql = "SELECT COUNT(DISTINCT session) as cnt, os as osflag, os 497 FROM access as A 498 WHERE A.dt >= ? AND A.dt <= ? 499 AND ua_type = ? 500 GROUP BY os 501 ORDER BY cnt DESC, os" . 502 $this->limit; 503 return $this->db->queryAll($sql, [$this->from, $this->to, 'browser']); 504 } 505 506 /** 507 * @return array 508 */ 509 public function topuser(): array 510 { 511 $sql = "SELECT COUNT(*) as cnt, user 512 FROM access as A 513 WHERE A.dt >= ? AND A.dt <= ? 514 AND ua_type = ? 515 AND user != ? 516 GROUP BY user 517 ORDER BY cnt DESC, user" . 518 $this->limit; 519 return $this->db->queryAll($sql, [$this->from, $this->to, 'browser', '']); 520 } 521 522 /** 523 * @return array 524 */ 525 public function topeditor(): array 526 { 527 $sql = "SELECT COUNT(*) as cnt, user 528 FROM edits as A 529 WHERE A.dt >= ? AND A.dt <= ? 530 AND user != ? 531 GROUP BY user 532 ORDER BY cnt DESC, user" . 533 $this->limit; 534 return $this->db->queryAll($sql, [$this->from, $this->to, '']); 535 } 536 537 /** 538 * @return array 539 */ 540 public function topgroup(): array 541 { 542 $sql = "SELECT COUNT(*) as cnt, `group` 543 FROM groups as A 544 WHERE A.dt >= ? AND A.dt <= ? 545 AND `type` = ? 546 GROUP BY `group` 547 ORDER BY cnt DESC, `group`" . 548 $this->limit; 549 return $this->db->queryAll($sql, [$this->from, $this->to, 'view']); 550 } 551 552 /** 553 * @return array 554 */ 555 public function topgroupedit(): array 556 { 557 $sql = "SELECT COUNT(*) as cnt, `group` 558 FROM groups as A 559 WHERE A.dt >= ? AND A.dt <= ? 560 AND `type` = ? 561 GROUP BY `group` 562 ORDER BY cnt DESC, `group`" . 563 $this->limit; 564 return $this->db->queryAll($sql, [$this->from, $this->to, 'edit']); 565 } 566 567 568 /** 569 * @return array 570 */ 571 public function resolution(): array 572 { 573 $sql = "SELECT COUNT(DISTINCT uid) as cnt, 574 ROUND(screen_x/100)*100 as res_x, 575 ROUND(screen_y/100)*100 as res_y, 576 (ROUND(screen_x/100)*100 || 'x' || ROUND(screen_y/100)*100) as resolution 577 FROM access as A 578 WHERE A.dt >= ? AND A.dt <= ? 579 AND ua_type = ? 580 AND screen_x != ? 581 AND screen_y != ? 582 GROUP BY resolution 583 ORDER BY cnt DESC" . 584 $this->limit; 585 return $this->db->queryAll($sql, [$this->from, $this->to, 'browser', 0, 0]); 586 } 587 588 /** 589 * @return array 590 */ 591 public function viewport(): array 592 { 593 $sql = "SELECT COUNT(DISTINCT uid) as cnt, 594 ROUND(view_x/100)*100 as res_x, 595 ROUND(view_y/100)*100 as res_y, 596 (ROUND(view_x/100)*100 || 'x' || ROUND(view_y/100)*100) as resolution 597 FROM access as A 598 WHERE A.dt >= ? AND A.dt <= ? 599 AND ua_type = ? 600 AND view_x != ? 601 AND view_y != ? 602 GROUP BY resolution 603 ORDER BY cnt DESC" . 604 $this->limit; 605 606 return $this->db->queryAll($sql, [$this->from, $this->to, 'browser', 0, 0]); 607 } 608 609 /** 610 * @return array 611 */ 612 public function seenusers(): array 613 { 614 $sql = "SELECT `user`, `dt` 615 FROM lastseen as A 616 ORDER BY `dt` DESC" . 617 $this->limit; 618 619 return $this->db->queryAll($sql); 620 } 621} 622