build: Updating mediawiki/mediawiki-codesniffer to 24.0.0
[lhc/web/wiklou.git] / tests / phpunit / includes / Revision / RevisionRecordTests.php
1 <?php
2
3 // phpcs:disable MediaWiki.Commenting.PhpunitAnnotations.NotClass
4
5 namespace MediaWiki\Tests\Revision;
6
7 use CommentStoreComment;
8 use LogicException;
9 use MediaWiki\Revision\RevisionRecord;
10 use MediaWiki\Revision\RevisionSlots;
11 use MediaWiki\Revision\RevisionStoreRecord;
12 use MediaWiki\Revision\SlotRecord;
13 use MediaWiki\Revision\SuppressedDataException;
14 use MediaWiki\User\UserIdentityValue;
15 use TextContent;
16 use Title;
17
18 /**
19 * @covers \MediaWiki\Revision\RevisionRecord
20 *
21 * @note Expects to be used in classes that extend MediaWikiTestCase.
22 */
23 trait RevisionRecordTests {
24
25 /**
26 * @param array $rowOverrides
27 *
28 * @return RevisionRecord
29 */
30 abstract protected function newRevision( array $rowOverrides = [] );
31
32 private function provideAudienceCheckData( $field ) {
33 yield 'field accessible for oversighter (ALL)' => [
34 RevisionRecord::SUPPRESSED_ALL,
35 [ 'oversight' ],
36 true,
37 false
38 ];
39
40 yield 'field accessible for oversighter' => [
41 RevisionRecord::DELETED_RESTRICTED | $field,
42 [ 'oversight' ],
43 true,
44 false
45 ];
46
47 yield 'field not accessible for sysops (ALL)' => [
48 RevisionRecord::SUPPRESSED_ALL,
49 [ 'sysop' ],
50 false,
51 false
52 ];
53
54 yield 'field not accessible for sysops' => [
55 RevisionRecord::DELETED_RESTRICTED | $field,
56 [ 'sysop' ],
57 false,
58 false
59 ];
60
61 yield 'field accessible for sysops' => [
62 $field,
63 [ 'sysop' ],
64 true,
65 false
66 ];
67
68 yield 'field suppressed for logged in users' => [
69 $field,
70 [ 'user' ],
71 false,
72 false
73 ];
74
75 yield 'unrelated field suppressed' => [
76 $field === RevisionRecord::DELETED_COMMENT
77 ? RevisionRecord::DELETED_USER
78 : RevisionRecord::DELETED_COMMENT,
79 [ 'user' ],
80 true,
81 true
82 ];
83
84 yield 'nothing suppressed' => [
85 0,
86 [ 'user' ],
87 true,
88 true
89 ];
90 }
91
92 public function testSerialization_fails() {
93 $this->setExpectedException( LogicException::class );
94 $rev = $this->newRevision();
95 serialize( $rev );
96 }
97
98 public function provideGetComment_audience() {
99 return $this->provideAudienceCheckData( RevisionRecord::DELETED_COMMENT );
100 }
101
102 private function forceStandardPermissions() {
103 $this->setMwGlobals(
104 'wgGroupPermissions',
105 [
106 'user' => [
107 'viewsuppressed' => false,
108 'suppressrevision' => false,
109 'deletedtext' => false,
110 'deletedhistory' => false,
111 ],
112 'sysop' => [
113 'viewsuppressed' => false,
114 'suppressrevision' => false,
115 'deletedtext' => true,
116 'deletedhistory' => true,
117 ],
118 'oversight' => [
119 'deletedtext' => true,
120 'deletedhistory' => true,
121 'viewsuppressed' => true,
122 'suppressrevision' => true,
123 ],
124 ]
125 );
126 }
127
128 /**
129 * @dataProvider provideGetComment_audience
130 */
131 public function testGetComment_audience( $visibility, $groups, $userCan, $publicCan ) {
132 $this->forceStandardPermissions();
133
134 $user = $this->getTestUser( $groups )->getUser();
135 $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
136
137 $this->assertNotNull( $rev->getComment( RevisionRecord::RAW ), 'raw can' );
138
139 $this->assertSame(
140 $publicCan,
141 $rev->getComment( RevisionRecord::FOR_PUBLIC ) !== null,
142 'public can'
143 );
144 $this->assertSame(
145 $userCan,
146 $rev->getComment( RevisionRecord::FOR_THIS_USER, $user ) !== null,
147 'user can'
148 );
149 }
150
151 public function provideGetUser_audience() {
152 return $this->provideAudienceCheckData( RevisionRecord::DELETED_USER );
153 }
154
155 /**
156 * @dataProvider provideGetUser_audience
157 */
158 public function testGetUser_audience( $visibility, $groups, $userCan, $publicCan ) {
159 $this->forceStandardPermissions();
160
161 $user = $this->getTestUser( $groups )->getUser();
162 $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
163
164 $this->assertNotNull( $rev->getUser( RevisionRecord::RAW ), 'raw can' );
165
166 $this->assertSame(
167 $publicCan,
168 $rev->getUser( RevisionRecord::FOR_PUBLIC ) !== null,
169 'public can'
170 );
171 $this->assertSame(
172 $userCan,
173 $rev->getUser( RevisionRecord::FOR_THIS_USER, $user ) !== null,
174 'user can'
175 );
176 }
177
178 public function provideGetSlot_audience() {
179 return $this->provideAudienceCheckData( RevisionRecord::DELETED_TEXT );
180 }
181
182 /**
183 * @dataProvider provideGetSlot_audience
184 */
185 public function testGetSlot_audience( $visibility, $groups, $userCan, $publicCan ) {
186 $this->forceStandardPermissions();
187
188 $user = $this->getTestUser( $groups )->getUser();
189 $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
190
191 // NOTE: slot meta-data is never suppressed, just the content is!
192 $this->assertTrue( $rev->hasSlot( SlotRecord::MAIN ), 'hasSlot is never suppressed' );
193 $this->assertNotNull( $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW ), 'raw meta' );
194 $this->assertNotNull( $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_PUBLIC ),
195 'public meta' );
196
197 $this->assertNotNull(
198 $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $user ),
199 'user can'
200 );
201
202 try {
203 $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_PUBLIC )->getContent();
204 $exception = null;
205 } catch ( SuppressedDataException $ex ) {
206 $exception = $ex;
207 }
208
209 $this->assertSame(
210 $publicCan,
211 $exception === null,
212 'public can'
213 );
214
215 try {
216 $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $user )->getContent();
217 $exception = null;
218 } catch ( SuppressedDataException $ex ) {
219 $exception = $ex;
220 }
221
222 $this->assertSame(
223 $userCan,
224 $exception === null,
225 'user can'
226 );
227 }
228
229 /**
230 * @dataProvider provideGetSlot_audience
231 */
232 public function testGetContent_audience( $visibility, $groups, $userCan, $publicCan ) {
233 $this->forceStandardPermissions();
234
235 $user = $this->getTestUser( $groups )->getUser();
236 $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
237
238 $this->assertNotNull( $rev->getContent( SlotRecord::MAIN, RevisionRecord::RAW ), 'raw can' );
239
240 $this->assertSame(
241 $publicCan,
242 $rev->getContent( SlotRecord::MAIN, RevisionRecord::FOR_PUBLIC ) !== null,
243 'public can'
244 );
245 $this->assertSame(
246 $userCan,
247 $rev->getContent( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $user ) !== null,
248 'user can'
249 );
250 }
251
252 public function testGetSlot() {
253 $rev = $this->newRevision();
254
255 $slot = $rev->getSlot( SlotRecord::MAIN );
256 $this->assertNotNull( $slot, 'getSlot()' );
257 $this->assertSame( 'main', $slot->getRole(), 'getRole()' );
258 }
259
260 public function testHasSlot() {
261 $rev = $this->newRevision();
262
263 $this->assertTrue( $rev->hasSlot( SlotRecord::MAIN ) );
264 $this->assertFalse( $rev->hasSlot( 'xyz' ) );
265 }
266
267 public function testGetContent() {
268 $rev = $this->newRevision();
269
270 $content = $rev->getSlot( SlotRecord::MAIN );
271 $this->assertNotNull( $content, 'getContent()' );
272 $this->assertSame( CONTENT_MODEL_TEXT, $content->getModel(), 'getModel()' );
273 }
274
275 public function provideUserCanBitfield() {
276 yield [ 0, 0, [], null, true ];
277 // Bitfields match, user has no permissions
278 yield [
279 RevisionRecord::DELETED_TEXT,
280 RevisionRecord::DELETED_TEXT,
281 [],
282 null,
283 false
284 ];
285 yield [
286 RevisionRecord::DELETED_COMMENT,
287 RevisionRecord::DELETED_COMMENT,
288 [],
289 null,
290 false,
291 ];
292 yield [
293 RevisionRecord::DELETED_USER,
294 RevisionRecord::DELETED_USER,
295 [],
296 null,
297 false
298 ];
299 yield [
300 RevisionRecord::DELETED_RESTRICTED,
301 RevisionRecord::DELETED_RESTRICTED,
302 [],
303 null,
304 false,
305 ];
306 // Bitfields match, user (admin) does have permissions
307 yield [
308 RevisionRecord::DELETED_TEXT,
309 RevisionRecord::DELETED_TEXT,
310 [ 'sysop' ],
311 null,
312 true,
313 ];
314 yield [
315 RevisionRecord::DELETED_COMMENT,
316 RevisionRecord::DELETED_COMMENT,
317 [ 'sysop' ],
318 null,
319 true,
320 ];
321 yield [
322 RevisionRecord::DELETED_USER,
323 RevisionRecord::DELETED_USER,
324 [ 'sysop' ],
325 null,
326 true,
327 ];
328 // Bitfields match, user (admin) does not have permissions
329 yield [
330 RevisionRecord::DELETED_RESTRICTED,
331 RevisionRecord::DELETED_RESTRICTED,
332 [ 'sysop' ],
333 null,
334 false,
335 ];
336 // Bitfields match, user (oversight) does have permissions
337 yield [
338 RevisionRecord::DELETED_RESTRICTED,
339 RevisionRecord::DELETED_RESTRICTED,
340 [ 'oversight' ],
341 null,
342 true,
343 ];
344 // Check permissions using the title
345 yield [
346 RevisionRecord::DELETED_TEXT,
347 RevisionRecord::DELETED_TEXT,
348 [ 'sysop' ],
349 __METHOD__,
350 true,
351 ];
352 yield [
353 RevisionRecord::DELETED_TEXT,
354 RevisionRecord::DELETED_TEXT,
355 [],
356 __METHOD__,
357 false,
358 ];
359 }
360
361 /**
362 * @dataProvider provideUserCanBitfield
363 * @covers \MediaWiki\Revision\RevisionRecord::userCanBitfield
364 */
365 public function testUserCanBitfield( $bitField, $field, $userGroups, $title, $expected ) {
366 if ( is_string( $title ) ) {
367 // NOTE: Data providers cannot instantiate Title objects! See T202641.
368 $title = Title::newFromText( $title );
369 }
370
371 $this->forceStandardPermissions();
372
373 $user = $this->getTestUser( $userGroups )->getUser();
374
375 $this->assertSame(
376 $expected,
377 RevisionRecord::userCanBitfield( $bitField, $field, $user, $title )
378 );
379 }
380
381 public function provideHasSameContent() {
382 // Create some slots with content
383 $mainA = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'A' ) );
384 $mainB = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'B' ) );
385 $auxA = SlotRecord::newUnsaved( 'aux', new TextContent( 'A' ) );
386 $auxB = SlotRecord::newUnsaved( 'aux', new TextContent( 'A' ) );
387
388 $initialRecordSpec = [ [ $mainA ], 12 ];
389
390 return [
391 'same record object' => [
392 true,
393 $initialRecordSpec,
394 $initialRecordSpec,
395 ],
396 'same record content, different object' => [
397 true,
398 [ [ $mainA ], 12 ],
399 [ [ $mainA ], 13 ],
400 ],
401 'same record content, aux slot, different object' => [
402 true,
403 [ [ $auxA ], 12 ],
404 [ [ $auxB ], 13 ],
405 ],
406 'different content' => [
407 false,
408 [ [ $mainA ], 12 ],
409 [ [ $mainB ], 13 ],
410 ],
411 'different content and number of slots' => [
412 false,
413 [ [ $mainA ], 12 ],
414 [ [ $mainA, $mainB ], 13 ],
415 ],
416 ];
417 }
418
419 /**
420 * @note Do not call directly from a data provider! Data providers cannot instantiate
421 * Title objects! See T202641.
422 *
423 * @param SlotRecord[] $slots
424 * @param int $revId
425 * @return RevisionStoreRecord
426 */
427 private function makeHasSameContentTestRecord( array $slots, $revId ) {
428 $title = Title::newFromText( 'provideHasSameContent' );
429 $title->resetArticleID( 19 );
430 $slots = new RevisionSlots( $slots );
431
432 return new RevisionStoreRecord(
433 $title,
434 new UserIdentityValue( 11, __METHOD__, 0 ),
435 CommentStoreComment::newUnsavedComment( __METHOD__ ),
436 (object)[
437 'rev_id' => strval( $revId ),
438 'rev_page' => strval( $title->getArticleID() ),
439 'rev_timestamp' => '20200101000000',
440 'rev_deleted' => 0,
441 'rev_minor_edit' => 0,
442 'rev_parent_id' => '5',
443 'rev_len' => $slots->computeSize(),
444 'rev_sha1' => $slots->computeSha1(),
445 'page_latest' => '18',
446 ],
447 $slots
448 );
449 }
450
451 /**
452 * @dataProvider provideHasSameContent
453 * @covers \MediaWiki\Revision\RevisionRecord::hasSameContent
454 * @group Database
455 */
456 public function testHasSameContent(
457 $expected,
458 $recordSpec1,
459 $recordSpec2
460 ) {
461 $record1 = $this->makeHasSameContentTestRecord( ...$recordSpec1 );
462 $record2 = $this->makeHasSameContentTestRecord( ...$recordSpec2 );
463
464 $this->assertSame(
465 $expected,
466 $record1->hasSameContent( $record2 )
467 );
468 }
469
470 public function provideIsDeleted() {
471 yield 'no deletion' => [
472 0,
473 [
474 RevisionRecord::DELETED_TEXT => false,
475 RevisionRecord::DELETED_COMMENT => false,
476 RevisionRecord::DELETED_USER => false,
477 RevisionRecord::DELETED_RESTRICTED => false,
478 ]
479 ];
480 yield 'text deleted' => [
481 RevisionRecord::DELETED_TEXT,
482 [
483 RevisionRecord::DELETED_TEXT => true,
484 RevisionRecord::DELETED_COMMENT => false,
485 RevisionRecord::DELETED_USER => false,
486 RevisionRecord::DELETED_RESTRICTED => false,
487 ]
488 ];
489 yield 'text and comment deleted' => [
490 RevisionRecord::DELETED_TEXT + RevisionRecord::DELETED_COMMENT,
491 [
492 RevisionRecord::DELETED_TEXT => true,
493 RevisionRecord::DELETED_COMMENT => true,
494 RevisionRecord::DELETED_USER => false,
495 RevisionRecord::DELETED_RESTRICTED => false,
496 ]
497 ];
498 yield 'all 4 deleted' => [
499 RevisionRecord::DELETED_TEXT +
500 RevisionRecord::DELETED_COMMENT +
501 RevisionRecord::DELETED_RESTRICTED +
502 RevisionRecord::DELETED_USER,
503 [
504 RevisionRecord::DELETED_TEXT => true,
505 RevisionRecord::DELETED_COMMENT => true,
506 RevisionRecord::DELETED_USER => true,
507 RevisionRecord::DELETED_RESTRICTED => true,
508 ]
509 ];
510 }
511
512 /**
513 * @dataProvider provideIsDeleted
514 * @covers \MediaWiki\Revision\RevisionRecord::isDeleted
515 */
516 public function testIsDeleted( $revDeleted, $assertionMap ) {
517 $rev = $this->newRevision( [ 'rev_deleted' => $revDeleted ] );
518 foreach ( $assertionMap as $deletionLevel => $expected ) {
519 $this->assertSame( $expected, $rev->isDeleted( $deletionLevel ) );
520 }
521 }
522
523 public function testIsReadyForInsertion() {
524 $rev = $this->newRevision();
525 $this->assertTrue( $rev->isReadyForInsertion() );
526 }
527
528 }