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