xref: /plugin/statistics/Query.php (revision 2adee4c68974f633621c42f9308b229422ef36a9)
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): 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): 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 " . $this->hlp->prefix . "lastseen as A
616              ORDER BY `dt` DESC" .
617            $this->limit;
618
619        return $this->db->queryAll($sql);
620    }
621}
622