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