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