xref: /plugin/statistics/Query.php (revision f0a4ccee9985c9b288636fd60e7968a2d5af6789)
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 = 'dt'; // FIXME
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