xref: /plugin/statistics/Query.php (revision e6aa7fd9092df52f4fafef1dee183b584364e2fc)
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 A.query != ''
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 = false): 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 lastseen as A
617              ORDER BY `dt` DESC" .
618            $this->limit;
619
620        return $this->db->queryAll($sql);
621    }
622}
623