xref: /plugin/statistics/_test/LoggerTest.php (revision 569a50664ac8b883b8bdf6f42f326d88d099ad6e)
1<?php
2
3namespace dokuwiki\plugin\statistics\test;
4
5use dokuwiki\plugin\statistics\Logger;
6use DokuWikiTest;
7use helper_plugin_statistics;
8
9/**
10 * Tests for the statistics plugin Logger class
11 *
12 * @group plugin_statistics
13 * @group plugins
14 */
15class LoggerTest extends DokuWikiTest
16{
17    protected $pluginsEnabled = ['statistics', 'sqlite'];
18
19    /** @var helper_plugin_statistics */
20    protected $helper;
21
22    const SESSION_ID = 'test-session-12345';
23    const USER_ID = 'test-uid-12345';
24    const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36';
25
26    public function setUp(): void
27    {
28        parent::setUp();
29
30        // Load the helper plugin
31        $this->helper = plugin_load('helper', 'statistics');
32
33        // set default user agent
34        $_SERVER['HTTP_USER_AGENT'] = self::USER_AGENT;
35
36        // Set up session data that Logger expects
37        $_SESSION[DOKU_COOKIE]['statistics']['uid'] = self::USER_ID;
38        $_SESSION[DOKU_COOKIE]['statistics']['id'] = self::SESSION_ID;
39    }
40
41    public function tearDown(): void
42    {
43        unset($_SERVER['HTTP_USER_AGENT']);
44        unset($_SESSION[DOKU_COOKIE]['statistics']);
45        parent::tearDown();
46    }
47
48    /**
49     * Test constructor initializes properties correctly
50     */
51    public function testConstructor()
52    {
53        $this->assertInstanceOf(Logger::class, $this->helper->getLogger());
54
55        // Test that bot user agents throw exception
56        $_SERVER['HTTP_USER_AGENT'] = 'Googlebot/2.1 (+http://www.google.com/bot.html)';
57
58        $this->expectException(\dokuwiki\plugin\statistics\IgnoreException::class);
59        $this->expectExceptionMessage('Bot detected, not logging');
60        new Logger($this->helper);
61    }
62
63    /**
64     * Test begin and end transaction methods
65     */
66    public function testBeginEnd()
67    {
68        $this->helper->getLogger()->begin();
69
70        // Verify transaction is active by checking PDO
71        $pdo = $this->helper->getDB()->getPdo();
72        $this->assertTrue($pdo->inTransaction());
73
74        $this->helper->getLogger()->end();
75
76        // Verify transaction is committed
77        $this->assertFalse($pdo->inTransaction());
78    }
79
80    /**
81     * Test user logging
82     */
83    public function testLogUser()
84    {
85        // Test with no user (should not log)
86        $_SERVER['REMOTE_USER'] = '';
87        $this->helper->getLogger()->begin();
88        $this->helper->getLogger()->end();
89
90        $count = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM users');
91        $this->assertEquals(0, $count);
92
93        // Test with user
94        $_SERVER['REMOTE_USER'] = 'testuser';
95        $this->helper->getLogger()->begin();
96        $this->helper->getLogger()->end();
97
98        $count = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM users');
99        $this->assertEquals(1, $count);
100
101        $user = $this->helper->getDB()->queryValue('SELECT user FROM users WHERE user = ?', ['testuser']);
102        $this->assertEquals('testuser', $user);
103    }
104
105    /**
106     * Data provider for logGroups test
107     */
108    public function logGroupsProvider()
109    {
110        return [
111            'empty groups' => [[], 0],
112            'single group' => [['admin'], 1],
113            'multiple groups' => [['admin', 'user'], 2],
114            'filtered groups' => [['admin', 'nonexistent'], 2], // all groups are logged
115        ];
116    }
117
118    /**
119     * Test logGroups method
120     * @dataProvider logGroupsProvider
121     */
122    public function testLogGroups($groups, $expectedCount)
123    {
124        global $USERINFO;
125
126        // Set up a test user and groups
127        $_SERVER['REMOTE_USER'] = 'testuser';
128        $USERINFO = ['grps' => $groups];
129
130
131        $this->helper->getLogger()->begin();
132        $this->helper->getLogger()->end();
133
134        $count = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM groups WHERE user = ?', ['testuser']);
135        $this->assertEquals($expectedCount, $count);
136
137        if ($expectedCount > 0) {
138            $loggedGroups = $this->helper->getDB()->queryAll('SELECT `group` FROM groups WHERE user = ?', ['testuser']);
139            $this->assertCount($expectedCount, $loggedGroups);
140        }
141    }
142
143    /**
144     * Data provider for testLogReferer test
145     */
146    public function logRefererProvider()
147    {
148        return [
149            'google search' => [
150                'https://www.google.com/search?q=dokuwiki+test',
151                'google',
152                true // should be logged
153            ],
154            'bing search' => [
155                'https://www.bing.com/search?q=test+query',
156                'bing',
157                true // should be logged
158            ],
159            'external referer' => [
160                'https://example.com/page',
161                null,
162                true // should be logged
163            ],
164            'direct access (empty referer)' => [
165                '',
166                null,
167                true // should be logged
168            ],
169            'ws' => [
170                '   ',
171                null,
172                true // should be logged (trimmed to empty)
173            ],
174        ];
175    }
176
177    /**
178     * Test logReferer method
179     * @dataProvider logRefererProvider
180     */
181    public function testLogReferer($referer, $expectedEngine, $shouldBeLogged)
182    {
183        $logger = $this->helper->getLogger();
184        $logger->begin();
185        $refId = $logger->logReferer($referer);
186        $logger->end();
187
188        if ($shouldBeLogged) {
189            $this->assertNotNull($refId);
190            $refererRecord = $this->helper->getDB()->queryRecord('SELECT * FROM referers WHERE id = ?', [$refId]);
191            $this->assertNotNull($refererRecord);
192            $this->assertEquals($expectedEngine, $refererRecord['engine']);
193            $this->assertEquals(trim($referer), $refererRecord['url']);
194        } else {
195            $this->assertNull($refId);
196        }
197    }
198
199    /**
200     * Test that internal referers (our own pages) are not logged
201     */
202    public function testLogRefererInternal()
203    {
204        // Test internal referer (should return null and not be logged)
205        $internalReferer = DOKU_URL;
206        $refId = $this->helper->getLogger()->logReferer($internalReferer);
207        $this->assertNull($refId, 'Internal referers should not be logged');
208
209        // Verify no referer was actually stored
210        $count = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM referers WHERE url = ?', [$internalReferer]);
211        $this->assertEquals(0, $count, 'Internal referer should not be stored in database');
212
213        // Test another internal referer pattern
214        $internalReferer2 = rtrim(DOKU_URL, '/') . '/doku.php?id=start';
215        $refId2 = $this->helper->getLogger()->logReferer($internalReferer2);
216        $this->assertNull($refId2, 'Internal wiki pages should not be logged as referers');
217    }
218
219    /**
220     * Test logSearch method
221     */
222    public function testLogSearch()
223    {
224        $query = 'test search query';
225        $words = ['test', 'search', 'query'];
226
227        $this->helper->getLogger()->logSearch($query, $words);
228
229        // Check search table
230        $search = $this->helper->getDB()->queryRecord('SELECT * FROM search ORDER BY dt DESC LIMIT 1');
231        $this->assertEquals($query, $search['query']);
232
233        // Check searchwords table
234        $wordCount = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM searchwords WHERE sid = ?', [$search['id']]);
235        $this->assertEquals(3, $wordCount);
236
237        $loggedWords = $this->helper->getDB()->queryAll('SELECT word FROM searchwords WHERE sid = ? ORDER BY word', [$search['id']]);
238        $this->assertEquals(['query', 'search', 'test'], array_column($loggedWords, 'word'));
239    }
240
241    /**
242     * Test logSession method
243     */
244    public function testLogSession()
245    {
246        $_SERVER['REMOTE_USER'] = 'testuser';
247
248        // Test session creation
249        $logger = $this->helper->getLogger();
250
251        $logger->begin();
252        $logger->end();
253
254        $sessionCount = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM sessions');
255        $this->assertEquals(1, $sessionCount);
256
257        $session = $this->helper->getDB()->queryRecord('SELECT * FROM sessions LIMIT 1');
258        $this->assertIsArray($session);
259        $this->assertEquals('testuser', $session['user']);
260        $this->assertEquals(self::SESSION_ID, $session['session']);
261        $this->assertEquals(self::USER_ID, $session['uid']);
262        $this->assertEquals(self::USER_AGENT, $session['ua']);
263        $this->assertEquals('Chrome', $session['ua_info']);
264        $this->assertEquals('browser', $session['ua_type']);
265        $this->assertEquals('91', $session['ua_ver']);
266        $this->assertEquals('Windows', $session['os']);
267
268    }
269
270    /**
271     * Test logIp method
272     */
273    public function testLogIp()
274    {
275        $ip = '8.8.8.8';
276        $_SERVER['REMOTE_ADDR'] = $ip;
277
278        // Create a mock HTTP client
279        $mockHttpClient = $this->createMock(\dokuwiki\HTTP\DokuHTTPClient::class);
280
281        // Mock the API response
282        $mockResponse = json_encode([
283            'status' => 'success',
284            'country' => 'United States',
285            'countryCode' => 'US',
286            'city' => 'Ashburn',
287            'query' => $ip
288        ]);
289
290        $mockHttpClient->expects($this->once())
291            ->method('get')
292            ->with('http://ip-api.com/json/' . $ip)
293            ->willReturn($mockResponse);
294
295        // Set timeout property
296        $mockHttpClient->timeout = 10;
297
298        // Create logger with mock HTTP client
299        $logger = new Logger($this->helper, $mockHttpClient);
300
301        // Test with IP that doesn't exist in database
302        $logger->logIp();
303
304        // Verify the IP was logged
305        $ipRecord = $this->helper->getDB()->queryRecord('SELECT * FROM iplocation WHERE ip = ?', [$ip]);
306        $this->assertNotNull($ipRecord);
307        $this->assertEquals($ip, $ipRecord['ip']);
308        $this->assertEquals('United States', $ipRecord['country']);
309        $this->assertEquals('US', $ipRecord['code']);
310        $this->assertEquals('Ashburn', $ipRecord['city']);
311        $this->assertNotEmpty($ipRecord['host']); // gethostbyaddr result
312
313        // Test with IP that already exists and is recent (should not make HTTP call)
314        $mockHttpClient2 = $this->createMock(\dokuwiki\HTTP\DokuHTTPClient::class);
315        $mockHttpClient2->expects($this->never())->method('get');
316
317        $logger2 = new Logger($this->helper, $mockHttpClient2);
318        $logger2->logIp(); // Should not trigger HTTP call
319    }
320
321    /**
322     * Test logOutgoing method
323     */
324    public function testLogOutgoing()
325    {
326        global $INPUT;
327
328        // Test without outgoing link
329        $INPUT->set('ol', '');
330        $this->helper->getLogger()->logOutgoing();
331
332        $count = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM outlinks');
333        $this->assertEquals(0, $count);
334
335        // Test with outgoing link
336        $link = 'https://example.com';
337        $page = 'test:page';
338        $INPUT->set('ol', $link);
339        $INPUT->set('p', $page);
340
341        $this->helper->getLogger()->logOutgoing();
342
343        $count = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM outlinks');
344        $this->assertEquals(1, $count);
345
346        $outlink = $this->helper->getDB()->queryRecord('SELECT * FROM outlinks ORDER BY dt DESC LIMIT 1');
347        $this->assertEquals($link, $outlink['link']);
348        $this->assertEquals($page, $outlink['page']);
349    }
350
351    /**
352     * Test logPageView method
353     */
354    public function testLogPageView()
355    {
356        global $INPUT, $USERINFO, $conf;
357
358        $conf['plugin']['statistics']['loggroups'] = ['admin', 'user'];
359
360        $page = 'test:page';
361        $referer = 'https://example.com';
362        $user = 'testuser';
363
364        $INPUT->set('p', $page);
365        $INPUT->set('r', $referer);
366        $INPUT->set('sx', 1920);
367        $INPUT->set('sy', 1080);
368        $INPUT->set('vx', 1200);
369        $INPUT->set('vy', 800);
370        $INPUT->server->set('REMOTE_USER', $user);
371
372        $USERINFO = ['grps' => ['admin', 'user']];
373
374        $logger = $this->helper->getLogger();
375        $logger->begin();
376        $logger->logPageView();
377        $logger->end();
378
379        // Check pageviews table
380        $pageview = $this->helper->getDB()->queryRecord('SELECT * FROM pageviews ORDER BY dt DESC LIMIT 1');
381        $this->assertEquals($page, $pageview['page']);
382        $this->assertEquals(1920, $pageview['screen_x']);
383        $this->assertEquals(1080, $pageview['screen_y']);
384        $this->assertEquals(1200, $pageview['view_x']);
385        $this->assertEquals(800, $pageview['view_y']);
386        $this->assertEquals(self::SESSION_ID, $pageview['session']);
387    }
388
389    /**
390     * Data provider for logMedia test
391     */
392    public function logMediaProvider()
393    {
394        return [
395            'image inline' => ['test.jpg', 'image/jpeg', true, 1024],
396            'video not inline' => ['test.mp4', 'video/mp4', false, 2048],
397            'document' => ['test.pdf', 'application/pdf', false, 512],
398        ];
399    }
400
401    /**
402     * Test logMedia method
403     * @dataProvider logMediaProvider
404     */
405    public function testLogMedia($media, $mime, $inline, $size)
406    {
407        global $INPUT;
408
409        $user = 'testuser';
410        $INPUT->server->set('REMOTE_USER', $user);
411
412        $this->helper->getLogger()->logMedia($media, $mime, $inline, $size);
413
414        $mediaLog = $this->helper->getDB()->queryRecord('SELECT * FROM media ORDER BY dt DESC LIMIT 1');
415        $this->assertEquals($media, $mediaLog['media']);
416        $this->assertEquals($size, $mediaLog['size']);
417        $this->assertEquals($inline ? 1 : 0, $mediaLog['inline']);
418
419        [$mime1, $mime2] = explode('/', strtolower($mime));
420        $this->assertEquals($mime1, $mediaLog['mime1']);
421        $this->assertEquals($mime2, $mediaLog['mime2']);
422    }
423
424    /**
425     * Data provider for logEdit test
426     */
427    public function logEditProvider()
428    {
429        return [
430            'create page' => ['new:page', 'create'],
431            'edit page' => ['existing:page', 'edit'],
432            'delete page' => ['old:page', 'delete'],
433        ];
434    }
435
436    /**
437     * Test logEdit method
438     * @dataProvider logEditProvider
439     */
440    public function testLogEdit($page, $type)
441    {
442        global $INPUT, $USERINFO;
443
444
445        $user = 'testuser';
446        $INPUT->server->set('REMOTE_USER', $user);
447        $USERINFO = ['grps' => ['admin']];
448
449        $this->helper->getLogger()->logEdit($page, $type);
450
451        // Check edits table
452        $edit = $this->helper->getDB()->queryRecord('SELECT * FROM edits ORDER BY dt DESC LIMIT 1');
453        $this->assertEquals($page, $edit['page']);
454        $this->assertEquals($type, $edit['type']);
455    }
456
457    /**
458     * Data provider for logLogin test
459     */
460    public function logLoginProvider()
461    {
462        return [
463            'login' => ['login', 'testuser'],
464            'logout' => ['logout', 'testuser'],
465            'create' => ['create', 'newuser'],
466        ];
467    }
468
469    /**
470     * Test logLogin method
471     * @dataProvider logLoginProvider
472     */
473    public function testLogLogin($type, $user)
474    {
475        $this->helper->getLogger()->logLogin($type, $user);
476        $login = $this->helper->getDB()->queryRecord('SELECT * FROM logins ORDER BY dt DESC LIMIT 1');
477        $this->assertEquals($type, $login['type']);
478        $this->assertEquals($user, $login['user']);
479    }
480
481    /**
482     * Test logHistoryPages method
483     */
484    public function testLogHistoryPages()
485    {
486        $this->helper->getLogger()->logHistoryPages();
487
488        // Check that both page_count and page_size entries were created
489        $pageCount = $this->helper->getDB()->queryValue('SELECT value FROM history WHERE info = ?', ['page_count']);
490        $pageSize = $this->helper->getDB()->queryValue('SELECT value FROM history WHERE info = ?', ['page_size']);
491
492        $this->assertIsNumeric($pageCount);
493        $this->assertIsNumeric($pageSize);
494        $this->assertGreaterThanOrEqual(0, $pageCount);
495        $this->assertGreaterThanOrEqual(0, $pageSize);
496    }
497
498    /**
499     * Test logHistoryMedia method
500     */
501    public function testLogHistoryMedia()
502    {
503        $this->helper->getLogger()->logHistoryMedia();
504
505        // Check that both media_count and media_size entries were created
506        $mediaCount = $this->helper->getDB()->queryValue('SELECT value FROM history WHERE info = ?', ['media_count']);
507        $mediaSize = $this->helper->getDB()->queryValue('SELECT value FROM history WHERE info = ?', ['media_size']);
508
509        $this->assertIsNumeric($mediaCount);
510        $this->assertIsNumeric($mediaSize);
511        $this->assertGreaterThanOrEqual(0, $mediaCount);
512        $this->assertGreaterThanOrEqual(0, $mediaSize);
513    }
514
515    /**
516     * Test that feedreader user agents are handled correctly
517     */
518    public function testFeedReaderUserAgent()
519    {
520        // Use a user agent that DeviceDetector recognizes as a feedreader
521        $_SERVER['HTTP_USER_AGENT'] = 'BashPodder/1.0 (http://bashpodder.sourceforge.net/)';
522
523        $logger = new Logger($this->helper);
524
525        // Use reflection to access protected property
526        $reflection = new \ReflectionClass($logger);
527        $uaTypeProperty = $reflection->getProperty('uaType');
528        $uaTypeProperty->setAccessible(true);
529
530        $this->assertEquals('feedreader', $uaTypeProperty->getValue($logger));
531    }
532}
533