parser: Validate $length in padleft/padright parser functions
[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 public function testGetTitle_successFromPageId() {
251 $mockLoadBalancer = $this->getMockLoadBalancer();
252 // Title calls wfGetDB() so we have to set the main service
253 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
254
255 $db = $this->getMockDatabase();
256 // Title calls wfGetDB() which uses a regular Connection
257 $mockLoadBalancer->expects( $this->atLeastOnce() )
258 ->method( 'getConnection' )
259 ->willReturn( $db );
260
261 // First call to Title::newFromID, faking no result (db lag?)
262 $db->expects( $this->at( 0 ) )
263 ->method( 'selectRow' )
264 ->with(
265 'page',
266 $this->anything(),
267 [ 'page_id' => 1 ]
268 )
269 ->willReturn( (object)[
270 'page_namespace' => '1',
271 'page_title' => 'Food',
272 ] );
273
274 $store = $this->getRevisionStore( $mockLoadBalancer );
275 $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
276
277 $this->assertSame( 1, $title->getNamespace() );
278 $this->assertSame( 'Food', $title->getDBkey() );
279 }
280
281 public function testGetTitle_successFromPageIdOnFallback() {
282 $mockLoadBalancer = $this->getMockLoadBalancer();
283 // Title calls wfGetDB() so we have to set the main service
284 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
285
286 $db = $this->getMockDatabase();
287 // Title calls wfGetDB() which uses a regular Connection
288 // Assert that the first call uses a REPLICA and the second falls back to master
289 $mockLoadBalancer->expects( $this->exactly( 2 ) )
290 ->method( 'getConnection' )
291 ->willReturn( $db );
292 // RevisionStore getTitle uses a ConnectionRef
293 $mockLoadBalancer->expects( $this->atLeastOnce() )
294 ->method( 'getConnectionRef' )
295 ->willReturn( $db );
296
297 // First call to Title::newFromID, faking no result (db lag?)
298 $db->expects( $this->at( 0 ) )
299 ->method( 'selectRow' )
300 ->with(
301 'page',
302 $this->anything(),
303 [ 'page_id' => 1 ]
304 )
305 ->willReturn( false );
306
307 // First select using rev_id, faking no result (db lag?)
308 $db->expects( $this->at( 1 ) )
309 ->method( 'selectRow' )
310 ->with(
311 [ 'revision', 'page' ],
312 $this->anything(),
313 [ 'rev_id' => 2 ]
314 )
315 ->willReturn( false );
316
317 // Second call to Title::newFromID, no result
318 $db->expects( $this->at( 2 ) )
319 ->method( 'selectRow' )
320 ->with(
321 'page',
322 $this->anything(),
323 [ 'page_id' => 1 ]
324 )
325 ->willReturn( (object)[
326 'page_namespace' => '2',
327 'page_title' => 'Foodey',
328 ] );
329
330 $store = $this->getRevisionStore( $mockLoadBalancer );
331 $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
332
333 $this->assertSame( 2, $title->getNamespace() );
334 $this->assertSame( 'Foodey', $title->getDBkey() );
335 }
336
337 public function testGetTitle_successFromRevId() {
338 $mockLoadBalancer = $this->getMockLoadBalancer();
339 // Title calls wfGetDB() so we have to set the main service
340 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
341
342 $db = $this->getMockDatabase();
343 // Title calls wfGetDB() which uses a regular Connection
344 $mockLoadBalancer->expects( $this->atLeastOnce() )
345 ->method( 'getConnection' )
346 ->willReturn( $db );
347 // RevisionStore getTitle uses a ConnectionRef
348 $mockLoadBalancer->expects( $this->atLeastOnce() )
349 ->method( 'getConnectionRef' )
350 ->willReturn( $db );
351
352 // First call to Title::newFromID, faking no result (db lag?)
353 $db->expects( $this->at( 0 ) )
354 ->method( 'selectRow' )
355 ->with(
356 'page',
357 $this->anything(),
358 [ 'page_id' => 1 ]
359 )
360 ->willReturn( false );
361
362 // First select using rev_id, faking no result (db lag?)
363 $db->expects( $this->at( 1 ) )
364 ->method( 'selectRow' )
365 ->with(
366 [ 'revision', 'page' ],
367 $this->anything(),
368 [ 'rev_id' => 2 ]
369 )
370 ->willReturn( (object)[
371 'page_namespace' => '1',
372 'page_title' => 'Food2',
373 ] );
374
375 $store = $this->getRevisionStore( $mockLoadBalancer );
376 $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
377
378 $this->assertSame( 1, $title->getNamespace() );
379 $this->assertSame( 'Food2', $title->getDBkey() );
380 }
381
382 public function testGetTitle_successFromRevIdOnFallback() {
383 $mockLoadBalancer = $this->getMockLoadBalancer();
384 // Title calls wfGetDB() so we have to set the main service
385 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
386
387 $db = $this->getMockDatabase();
388 // Title calls wfGetDB() which uses a regular Connection
389 // Assert that the first call uses a REPLICA and the second falls back to master
390 $mockLoadBalancer->expects( $this->exactly( 2 ) )
391 ->method( 'getConnection' )
392 ->willReturn( $db );
393 // RevisionStore getTitle uses a ConnectionRef
394 $mockLoadBalancer->expects( $this->atLeastOnce() )
395 ->method( 'getConnectionRef' )
396 ->willReturn( $db );
397
398 // First call to Title::newFromID, faking no result (db lag?)
399 $db->expects( $this->at( 0 ) )
400 ->method( 'selectRow' )
401 ->with(
402 'page',
403 $this->anything(),
404 [ 'page_id' => 1 ]
405 )
406 ->willReturn( false );
407
408 // First select using rev_id, faking no result (db lag?)
409 $db->expects( $this->at( 1 ) )
410 ->method( 'selectRow' )
411 ->with(
412 [ 'revision', 'page' ],
413 $this->anything(),
414 [ 'rev_id' => 2 ]
415 )
416 ->willReturn( false );
417
418 // Second call to Title::newFromID, no result
419 $db->expects( $this->at( 2 ) )
420 ->method( 'selectRow' )
421 ->with(
422 'page',
423 $this->anything(),
424 [ 'page_id' => 1 ]
425 )
426 ->willReturn( false );
427
428 // Second select using rev_id, result
429 $db->expects( $this->at( 3 ) )
430 ->method( 'selectRow' )
431 ->with(
432 [ 'revision', 'page' ],
433 $this->anything(),
434 [ 'rev_id' => 2 ]
435 )
436 ->willReturn( (object)[
437 'page_namespace' => '2',
438 'page_title' => 'Foodey',
439 ] );
440
441 $store = $this->getRevisionStore( $mockLoadBalancer );
442 $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
443
444 $this->assertSame( 2, $title->getNamespace() );
445 $this->assertSame( 'Foodey', $title->getDBkey() );
446 }
447
448 /**
449 * @covers \MediaWiki\Storage\RevisionStore::getTitle
450 */
451 public function testGetTitle_correctFallbackAndthrowsExceptionAfterFallbacks() {
452 $mockLoadBalancer = $this->getMockLoadBalancer();
453 // Title calls wfGetDB() so we have to set the main service
454 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
455
456 $db = $this->getMockDatabase();
457 // Title calls wfGetDB() which uses a regular Connection
458 // Assert that the first call uses a REPLICA and the second falls back to master
459
460 // RevisionStore getTitle uses getConnectionRef
461 // Title::newFromID uses getConnection
462 foreach ( [ 'getConnection', 'getConnectionRef' ] as $method ) {
463 $mockLoadBalancer->expects( $this->exactly( 2 ) )
464 ->method( $method )
465 ->willReturnCallback( function ( $masterOrReplica ) use ( $db ) {
466 static $callCounter = 0;
467 $callCounter++;
468 // The first call should be to a REPLICA, and the second a MASTER.
469 if ( $callCounter === 1 ) {
470 $this->assertSame( DB_REPLICA, $masterOrReplica );
471 } elseif ( $callCounter === 2 ) {
472 $this->assertSame( DB_MASTER, $masterOrReplica );
473 }
474 return $db;
475 } );
476 }
477 // First and third call to Title::newFromID, faking no result
478 foreach ( [ 0, 2 ] as $counter ) {
479 $db->expects( $this->at( $counter ) )
480 ->method( 'selectRow' )
481 ->with(
482 'page',
483 $this->anything(),
484 [ 'page_id' => 1 ]
485 )
486 ->willReturn( false );
487 }
488
489 foreach ( [ 1, 3 ] as $counter ) {
490 $db->expects( $this->at( $counter ) )
491 ->method( 'selectRow' )
492 ->with(
493 [ 'revision', 'page' ],
494 $this->anything(),
495 [ 'rev_id' => 2 ]
496 )
497 ->willReturn( false );
498 }
499
500 $store = $this->getRevisionStore( $mockLoadBalancer );
501
502 $this->setExpectedException( RevisionAccessException::class );
503 $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
504 }
505
506 public function provideNewRevisionFromRow_legacyEncoding_applied() {
507 yield 'windows-1252, old_flags is empty' => [
508 'windows-1252',
509 'en',
510 [
511 'old_flags' => '',
512 'old_text' => "S\xF6me Content",
513 ],
514 'Söme Content'
515 ];
516
517 yield 'windows-1252, old_flags is null' => [
518 'windows-1252',
519 'en',
520 [
521 'old_flags' => null,
522 'old_text' => "S\xF6me Content",
523 ],
524 'Söme Content'
525 ];
526 }
527
528 /**
529 * @dataProvider provideNewRevisionFromRow_legacyEncoding_applied
530 *
531 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
532 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
533 */
534 public function testNewRevisionFromRow_legacyEncoding_applied( $encoding, $locale, $row, $text ) {
535 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
536 $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
537
538 $blobStore = new SqlBlobStore( $lb, $cache );
539 $blobStore->setLegacyEncoding( $encoding, Language::factory( $locale ) );
540
541 $store = $this->getRevisionStore( $lb, $blobStore, $cache );
542
543 $record = $store->newRevisionFromRow(
544 $this->makeRow( $row ),
545 0,
546 Title::newFromText( __METHOD__ . '-UTPage' )
547 );
548
549 $this->assertSame( $text, $record->getContent( 'main' )->serialize() );
550 }
551
552 /**
553 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
554 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
555 */
556 public function testNewRevisionFromRow_legacyEncoding_ignored() {
557 $row = [
558 'old_flags' => 'utf-8',
559 'old_text' => 'Söme Content',
560 ];
561
562 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
563 $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
564
565 $blobStore = new SqlBlobStore( $lb, $cache );
566 $blobStore->setLegacyEncoding( 'windows-1252', Language::factory( 'en' ) );
567
568 $store = $this->getRevisionStore( $lb, $blobStore, $cache );
569
570 $record = $store->newRevisionFromRow(
571 $this->makeRow( $row ),
572 0,
573 Title::newFromText( __METHOD__ . '-UTPage' )
574 );
575 $this->assertSame( 'Söme Content', $record->getContent( 'main' )->serialize() );
576 }
577
578 private function makeRow( array $array ) {
579 $row = $array + [
580 'rev_id' => 7,
581 'rev_page' => 5,
582 'rev_text_id' => 11,
583 'rev_timestamp' => '20110101000000',
584 'rev_user_text' => 'Tester',
585 'rev_user' => 17,
586 'rev_minor_edit' => 0,
587 'rev_deleted' => 0,
588 'rev_len' => 100,
589 'rev_parent_id' => 0,
590 'rev_sha1' => 'deadbeef',
591 'rev_comment_text' => 'Testing',
592 'rev_comment_data' => '{}',
593 'rev_comment_cid' => 111,
594 'rev_content_format' => CONTENT_FORMAT_TEXT,
595 'rev_content_model' => CONTENT_MODEL_TEXT,
596 'page_namespace' => 0,
597 'page_title' => 'TEST',
598 'page_id' => 5,
599 'page_latest' => 7,
600 'page_is_redirect' => 0,
601 'page_len' => 100,
602 'user_name' => 'Tester',
603 'old_is' => 13,
604 'old_text' => 'Hello World',
605 'old_flags' => 'utf-8',
606 ];
607
608 return (object)$row;
609 }
610
611 }