849894755b3a27c409f80b905a3c308aa74d9df0
[lhc/web/wiklou.git] / tests / phpunit / includes / Storage / RevisionStoreTest.php
1 <?php
2
3 namespace MediaWiki\Tests\Storage;
4
5 use HashBagOStuff;
6 use Language;
7 use MediaWiki\MediaWikiServices;
8 use MediaWiki\Storage\RevisionAccessException;
9 use MediaWiki\Storage\RevisionStore;
10 use MediaWiki\Storage\SqlBlobStore;
11 use MediaWikiTestCase;
12 use Title;
13 use WANObjectCache;
14 use Wikimedia\Rdbms\Database;
15 use Wikimedia\Rdbms\LoadBalancer;
16
17 class RevisionStoreTest extends MediaWikiTestCase {
18
19 /**
20 * @param LoadBalancer $loadBalancer
21 * @param SqlBlobStore $blobStore
22 * @param WANObjectCache $WANObjectCache
23 *
24 * @return RevisionStore
25 */
26 private function getRevisionStore(
27 $loadBalancer = null,
28 $blobStore = null,
29 $WANObjectCache = null
30 ) {
31 return new RevisionStore(
32 $loadBalancer ? $loadBalancer : $this->getMockLoadBalancer(),
33 $blobStore ? $blobStore : $this->getMockSqlBlobStore(),
34 $WANObjectCache ? $WANObjectCache : $this->getHashWANObjectCache(),
35 MediaWikiServices::getInstance()->getCommentStore(),
36 MediaWikiServices::getInstance()->getActorMigration()
37 );
38 }
39
40 /**
41 * @return \PHPUnit_Framework_MockObject_MockObject|LoadBalancer
42 */
43 private function getMockLoadBalancer() {
44 return $this->getMockBuilder( LoadBalancer::class )
45 ->disableOriginalConstructor()->getMock();
46 }
47
48 /**
49 * @return \PHPUnit_Framework_MockObject_MockObject|Database
50 */
51 private function getMockDatabase() {
52 return $this->getMockBuilder( Database::class )
53 ->disableOriginalConstructor()->getMock();
54 }
55
56 /**
57 * @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore
58 */
59 private function getMockSqlBlobStore() {
60 return $this->getMockBuilder( SqlBlobStore::class )
61 ->disableOriginalConstructor()->getMock();
62 }
63
64 private function getHashWANObjectCache() {
65 return new WANObjectCache( [ 'cache' => new \HashBagOStuff() ] );
66 }
67
68 /**
69 * @covers \MediaWiki\Storage\RevisionStore::getContentHandlerUseDB
70 * @covers \MediaWiki\Storage\RevisionStore::setContentHandlerUseDB
71 */
72 public function testGetSetContentHandlerDb() {
73 $store = $this->getRevisionStore();
74 $this->assertTrue( $store->getContentHandlerUseDB() );
75 $store->setContentHandlerUseDB( false );
76 $this->assertFalse( $store->getContentHandlerUseDB() );
77 $store->setContentHandlerUseDB( true );
78 $this->assertTrue( $store->getContentHandlerUseDB() );
79 }
80
81 private function getDefaultQueryFields() {
82 return [
83 'rev_id',
84 'rev_page',
85 'rev_text_id',
86 'rev_timestamp',
87 'rev_minor_edit',
88 'rev_deleted',
89 'rev_len',
90 'rev_parent_id',
91 'rev_sha1',
92 ];
93 }
94
95 private function getCommentQueryFields() {
96 return [
97 'rev_comment_text' => 'rev_comment',
98 'rev_comment_data' => 'NULL',
99 'rev_comment_cid' => 'NULL',
100 ];
101 }
102
103 private function getActorQueryFields() {
104 return [
105 'rev_user' => 'rev_user',
106 'rev_user_text' => 'rev_user_text',
107 'rev_actor' => 'NULL',
108 ];
109 }
110
111 private function getContentHandlerQueryFields() {
112 return [
113 'rev_content_format',
114 'rev_content_model',
115 ];
116 }
117
118 public function provideGetQueryInfo() {
119 yield [
120 true,
121 [],
122 [
123 'tables' => [ 'revision' ],
124 'fields' => array_merge(
125 $this->getDefaultQueryFields(),
126 $this->getCommentQueryFields(),
127 $this->getActorQueryFields(),
128 $this->getContentHandlerQueryFields()
129 ),
130 'joins' => [],
131 ]
132 ];
133 yield [
134 false,
135 [],
136 [
137 'tables' => [ 'revision' ],
138 'fields' => array_merge(
139 $this->getDefaultQueryFields(),
140 $this->getCommentQueryFields(),
141 $this->getActorQueryFields()
142 ),
143 'joins' => [],
144 ]
145 ];
146 yield [
147 false,
148 [ 'page' ],
149 [
150 'tables' => [ 'revision', 'page' ],
151 'fields' => array_merge(
152 $this->getDefaultQueryFields(),
153 $this->getCommentQueryFields(),
154 $this->getActorQueryFields(),
155 [
156 'page_namespace',
157 'page_title',
158 'page_id',
159 'page_latest',
160 'page_is_redirect',
161 'page_len',
162 ]
163 ),
164 'joins' => [
165 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
166 ],
167 ]
168 ];
169 yield [
170 false,
171 [ 'user' ],
172 [
173 'tables' => [ 'revision', 'user' ],
174 'fields' => array_merge(
175 $this->getDefaultQueryFields(),
176 $this->getCommentQueryFields(),
177 $this->getActorQueryFields(),
178 [
179 'user_name',
180 ]
181 ),
182 'joins' => [
183 'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
184 ],
185 ]
186 ];
187 yield [
188 false,
189 [ 'text' ],
190 [
191 'tables' => [ 'revision', 'text' ],
192 'fields' => array_merge(
193 $this->getDefaultQueryFields(),
194 $this->getCommentQueryFields(),
195 $this->getActorQueryFields(),
196 [
197 'old_text',
198 'old_flags',
199 ]
200 ),
201 'joins' => [
202 'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ],
203 ],
204 ]
205 ];
206 yield [
207 true,
208 [ 'page', 'user', 'text' ],
209 [
210 'tables' => [ 'revision', 'page', 'user', 'text' ],
211 'fields' => array_merge(
212 $this->getDefaultQueryFields(),
213 $this->getCommentQueryFields(),
214 $this->getActorQueryFields(),
215 $this->getContentHandlerQueryFields(),
216 [
217 'page_namespace',
218 'page_title',
219 'page_id',
220 'page_latest',
221 'page_is_redirect',
222 'page_len',
223 'user_name',
224 'old_text',
225 'old_flags',
226 ]
227 ),
228 'joins' => [
229 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
230 'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
231 'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ],
232 ],
233 ]
234 ];
235 }
236
237 /**
238 * @dataProvider provideGetQueryInfo
239 * @covers \MediaWiki\Storage\RevisionStore::getQueryInfo
240 */
241 public function testGetQueryInfo( $contentHandlerUseDb, $options, $expected ) {
242 $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
243 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD );
244 $this->overrideMwServices();
245 $store = $this->getRevisionStore();
246 $store->setContentHandlerUseDB( $contentHandlerUseDb );
247 $this->assertEquals( $expected, $store->getQueryInfo( $options ) );
248 }
249
250 private function getDefaultArchiveFields() {
251 return [
252 'ar_id',
253 'ar_page_id',
254 'ar_namespace',
255 'ar_title',
256 'ar_rev_id',
257 'ar_text',
258 'ar_text_id',
259 'ar_timestamp',
260 'ar_minor_edit',
261 'ar_deleted',
262 'ar_len',
263 'ar_parent_id',
264 'ar_sha1',
265 ];
266 }
267
268 /**
269 * @covers \MediaWiki\Storage\RevisionStore::getArchiveQueryInfo
270 */
271 public function testGetArchiveQueryInfo_contentHandlerDb() {
272 $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
273 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD );
274 $this->overrideMwServices();
275 $store = $this->getRevisionStore();
276 $store->setContentHandlerUseDB( true );
277 $this->assertEquals(
278 [
279 'tables' => [
280 'archive'
281 ],
282 'fields' => array_merge(
283 $this->getDefaultArchiveFields(),
284 [
285 'ar_comment_text' => 'ar_comment',
286 'ar_comment_data' => 'NULL',
287 'ar_comment_cid' => 'NULL',
288 'ar_user_text' => 'ar_user_text',
289 'ar_user' => 'ar_user',
290 'ar_actor' => 'NULL',
291 'ar_content_format',
292 'ar_content_model',
293 ]
294 ),
295 'joins' => [],
296 ],
297 $store->getArchiveQueryInfo()
298 );
299 }
300
301 /**
302 * @covers \MediaWiki\Storage\RevisionStore::getArchiveQueryInfo
303 */
304 public function testGetArchiveQueryInfo_noContentHandlerDb() {
305 $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
306 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD );
307 $this->overrideMwServices();
308 $store = $this->getRevisionStore();
309 $store->setContentHandlerUseDB( false );
310 $this->assertEquals(
311 [
312 'tables' => [
313 'archive'
314 ],
315 'fields' => array_merge(
316 $this->getDefaultArchiveFields(),
317 [
318 'ar_comment_text' => 'ar_comment',
319 'ar_comment_data' => 'NULL',
320 'ar_comment_cid' => 'NULL',
321 'ar_user_text' => 'ar_user_text',
322 'ar_user' => 'ar_user',
323 'ar_actor' => 'NULL',
324 ]
325 ),
326 'joins' => [],
327 ],
328 $store->getArchiveQueryInfo()
329 );
330 }
331
332 public function testGetTitle_successFromPageId() {
333 $mockLoadBalancer = $this->getMockLoadBalancer();
334 // Title calls wfGetDB() so we have to set the main service
335 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
336
337 $db = $this->getMockDatabase();
338 // Title calls wfGetDB() which uses a regular Connection
339 $mockLoadBalancer->expects( $this->atLeastOnce() )
340 ->method( 'getConnection' )
341 ->willReturn( $db );
342
343 // First call to Title::newFromID, faking no result (db lag?)
344 $db->expects( $this->at( 0 ) )
345 ->method( 'selectRow' )
346 ->with(
347 'page',
348 $this->anything(),
349 [ 'page_id' => 1 ]
350 )
351 ->willReturn( (object)[
352 'page_namespace' => '1',
353 'page_title' => 'Food',
354 ] );
355
356 $store = $this->getRevisionStore( $mockLoadBalancer );
357 $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
358
359 $this->assertSame( 1, $title->getNamespace() );
360 $this->assertSame( 'Food', $title->getDBkey() );
361 }
362
363 public function testGetTitle_successFromPageIdOnFallback() {
364 $mockLoadBalancer = $this->getMockLoadBalancer();
365 // Title calls wfGetDB() so we have to set the main service
366 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
367
368 $db = $this->getMockDatabase();
369 // Title calls wfGetDB() which uses a regular Connection
370 // Assert that the first call uses a REPLICA and the second falls back to master
371 $mockLoadBalancer->expects( $this->exactly( 2 ) )
372 ->method( 'getConnection' )
373 ->willReturn( $db );
374 // RevisionStore getTitle uses a ConnectionRef
375 $mockLoadBalancer->expects( $this->atLeastOnce() )
376 ->method( 'getConnectionRef' )
377 ->willReturn( $db );
378
379 // First call to Title::newFromID, faking no result (db lag?)
380 $db->expects( $this->at( 0 ) )
381 ->method( 'selectRow' )
382 ->with(
383 'page',
384 $this->anything(),
385 [ 'page_id' => 1 ]
386 )
387 ->willReturn( false );
388
389 // First select using rev_id, faking no result (db lag?)
390 $db->expects( $this->at( 1 ) )
391 ->method( 'selectRow' )
392 ->with(
393 [ 'revision', 'page' ],
394 $this->anything(),
395 [ 'rev_id' => 2 ]
396 )
397 ->willReturn( false );
398
399 // Second call to Title::newFromID, no result
400 $db->expects( $this->at( 2 ) )
401 ->method( 'selectRow' )
402 ->with(
403 'page',
404 $this->anything(),
405 [ 'page_id' => 1 ]
406 )
407 ->willReturn( (object)[
408 'page_namespace' => '2',
409 'page_title' => 'Foodey',
410 ] );
411
412 $store = $this->getRevisionStore( $mockLoadBalancer );
413 $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
414
415 $this->assertSame( 2, $title->getNamespace() );
416 $this->assertSame( 'Foodey', $title->getDBkey() );
417 }
418
419 public function testGetTitle_successFromRevId() {
420 $mockLoadBalancer = $this->getMockLoadBalancer();
421 // Title calls wfGetDB() so we have to set the main service
422 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
423
424 $db = $this->getMockDatabase();
425 // Title calls wfGetDB() which uses a regular Connection
426 $mockLoadBalancer->expects( $this->atLeastOnce() )
427 ->method( 'getConnection' )
428 ->willReturn( $db );
429 // RevisionStore getTitle uses a ConnectionRef
430 $mockLoadBalancer->expects( $this->atLeastOnce() )
431 ->method( 'getConnectionRef' )
432 ->willReturn( $db );
433
434 // First call to Title::newFromID, faking no result (db lag?)
435 $db->expects( $this->at( 0 ) )
436 ->method( 'selectRow' )
437 ->with(
438 'page',
439 $this->anything(),
440 [ 'page_id' => 1 ]
441 )
442 ->willReturn( false );
443
444 // First select using rev_id, faking no result (db lag?)
445 $db->expects( $this->at( 1 ) )
446 ->method( 'selectRow' )
447 ->with(
448 [ 'revision', 'page' ],
449 $this->anything(),
450 [ 'rev_id' => 2 ]
451 )
452 ->willReturn( (object)[
453 'page_namespace' => '1',
454 'page_title' => 'Food2',
455 ] );
456
457 $store = $this->getRevisionStore( $mockLoadBalancer );
458 $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
459
460 $this->assertSame( 1, $title->getNamespace() );
461 $this->assertSame( 'Food2', $title->getDBkey() );
462 }
463
464 public function testGetTitle_successFromRevIdOnFallback() {
465 $mockLoadBalancer = $this->getMockLoadBalancer();
466 // Title calls wfGetDB() so we have to set the main service
467 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
468
469 $db = $this->getMockDatabase();
470 // Title calls wfGetDB() which uses a regular Connection
471 // Assert that the first call uses a REPLICA and the second falls back to master
472 $mockLoadBalancer->expects( $this->exactly( 2 ) )
473 ->method( 'getConnection' )
474 ->willReturn( $db );
475 // RevisionStore getTitle uses a ConnectionRef
476 $mockLoadBalancer->expects( $this->atLeastOnce() )
477 ->method( 'getConnectionRef' )
478 ->willReturn( $db );
479
480 // First call to Title::newFromID, faking no result (db lag?)
481 $db->expects( $this->at( 0 ) )
482 ->method( 'selectRow' )
483 ->with(
484 'page',
485 $this->anything(),
486 [ 'page_id' => 1 ]
487 )
488 ->willReturn( false );
489
490 // First select using rev_id, faking no result (db lag?)
491 $db->expects( $this->at( 1 ) )
492 ->method( 'selectRow' )
493 ->with(
494 [ 'revision', 'page' ],
495 $this->anything(),
496 [ 'rev_id' => 2 ]
497 )
498 ->willReturn( false );
499
500 // Second call to Title::newFromID, no result
501 $db->expects( $this->at( 2 ) )
502 ->method( 'selectRow' )
503 ->with(
504 'page',
505 $this->anything(),
506 [ 'page_id' => 1 ]
507 )
508 ->willReturn( false );
509
510 // Second select using rev_id, result
511 $db->expects( $this->at( 3 ) )
512 ->method( 'selectRow' )
513 ->with(
514 [ 'revision', 'page' ],
515 $this->anything(),
516 [ 'rev_id' => 2 ]
517 )
518 ->willReturn( (object)[
519 'page_namespace' => '2',
520 'page_title' => 'Foodey',
521 ] );
522
523 $store = $this->getRevisionStore( $mockLoadBalancer );
524 $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
525
526 $this->assertSame( 2, $title->getNamespace() );
527 $this->assertSame( 'Foodey', $title->getDBkey() );
528 }
529
530 /**
531 * @covers \MediaWiki\Storage\RevisionStore::getTitle
532 */
533 public function testGetTitle_correctFallbackAndthrowsExceptionAfterFallbacks() {
534 $mockLoadBalancer = $this->getMockLoadBalancer();
535 // Title calls wfGetDB() so we have to set the main service
536 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
537
538 $db = $this->getMockDatabase();
539 // Title calls wfGetDB() which uses a regular Connection
540 // Assert that the first call uses a REPLICA and the second falls back to master
541
542 // RevisionStore getTitle uses getConnectionRef
543 // Title::newFromID uses getConnection
544 foreach ( [ 'getConnection', 'getConnectionRef' ] as $method ) {
545 $mockLoadBalancer->expects( $this->exactly( 2 ) )
546 ->method( $method )
547 ->willReturnCallback( function ( $masterOrReplica ) use ( $db ) {
548 static $callCounter = 0;
549 $callCounter++;
550 // The first call should be to a REPLICA, and the second a MASTER.
551 if ( $callCounter === 1 ) {
552 $this->assertSame( DB_REPLICA, $masterOrReplica );
553 } elseif ( $callCounter === 2 ) {
554 $this->assertSame( DB_MASTER, $masterOrReplica );
555 }
556 return $db;
557 } );
558 }
559 // First and third call to Title::newFromID, faking no result
560 foreach ( [ 0, 2 ] as $counter ) {
561 $db->expects( $this->at( $counter ) )
562 ->method( 'selectRow' )
563 ->with(
564 'page',
565 $this->anything(),
566 [ 'page_id' => 1 ]
567 )
568 ->willReturn( false );
569 }
570
571 foreach ( [ 1, 3 ] as $counter ) {
572 $db->expects( $this->at( $counter ) )
573 ->method( 'selectRow' )
574 ->with(
575 [ 'revision', 'page' ],
576 $this->anything(),
577 [ 'rev_id' => 2 ]
578 )
579 ->willReturn( false );
580 }
581
582 $store = $this->getRevisionStore( $mockLoadBalancer );
583
584 $this->setExpectedException( RevisionAccessException::class );
585 $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
586 }
587
588 public function provideNewRevisionFromRow_legacyEncoding_applied() {
589 yield 'windows-1252, old_flags is empty' => [
590 'windows-1252',
591 'en',
592 [
593 'old_flags' => '',
594 'old_text' => "S\xF6me Content",
595 ],
596 'Söme Content'
597 ];
598
599 yield 'windows-1252, old_flags is null' => [
600 'windows-1252',
601 'en',
602 [
603 'old_flags' => null,
604 'old_text' => "S\xF6me Content",
605 ],
606 'Söme Content'
607 ];
608 }
609
610 /**
611 * @dataProvider provideNewRevisionFromRow_legacyEncoding_applied
612 *
613 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
614 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
615 */
616 public function testNewRevisionFromRow_legacyEncoding_applied( $encoding, $locale, $row, $text ) {
617 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
618
619 $blobStore = new SqlBlobStore( wfGetLB(), $cache );
620 $blobStore->setLegacyEncoding( $encoding, Language::factory( $locale ) );
621
622 $store = $this->getRevisionStore( wfGetLB(), $blobStore, $cache );
623
624 $record = $store->newRevisionFromRow(
625 $this->makeRow( $row ),
626 0,
627 Title::newFromText( __METHOD__ . '-UTPage' )
628 );
629
630 $this->assertSame( $text, $record->getContent( 'main' )->serialize() );
631 }
632
633 /**
634 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
635 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
636 */
637 public function testNewRevisionFromRow_legacyEncoding_ignored() {
638 $row = [
639 'old_flags' => 'utf-8',
640 'old_text' => 'Söme Content',
641 ];
642
643 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
644
645 $blobStore = new SqlBlobStore( wfGetLB(), $cache );
646 $blobStore->setLegacyEncoding( 'windows-1252', Language::factory( 'en' ) );
647
648 $store = $this->getRevisionStore( wfGetLB(), $blobStore, $cache );
649
650 $record = $store->newRevisionFromRow(
651 $this->makeRow( $row ),
652 0,
653 Title::newFromText( __METHOD__ . '-UTPage' )
654 );
655 $this->assertSame( 'Söme Content', $record->getContent( 'main' )->serialize() );
656 }
657
658 private function makeRow( array $array ) {
659 $row = $array + [
660 'rev_id' => 7,
661 'rev_page' => 5,
662 'rev_text_id' => 11,
663 'rev_timestamp' => '20110101000000',
664 'rev_user_text' => 'Tester',
665 'rev_user' => 17,
666 'rev_minor_edit' => 0,
667 'rev_deleted' => 0,
668 'rev_len' => 100,
669 'rev_parent_id' => 0,
670 'rev_sha1' => 'deadbeef',
671 'rev_comment_text' => 'Testing',
672 'rev_comment_data' => '{}',
673 'rev_comment_cid' => 111,
674 'rev_content_format' => CONTENT_FORMAT_TEXT,
675 'rev_content_model' => CONTENT_MODEL_TEXT,
676 'page_namespace' => 0,
677 'page_title' => 'TEST',
678 'page_id' => 5,
679 'page_latest' => 7,
680 'page_is_redirect' => 0,
681 'page_len' => 100,
682 'user_name' => 'Tester',
683 'old_is' => 13,
684 'old_text' => 'Hello World',
685 'old_flags' => 'utf-8',
686 ];
687
688 return (object)$row;
689 }
690
691 }