Fix handling of ar_length and ar_sha1 in RevisionArchiveRecord.
[lhc/web/wiklou.git] / tests / phpunit / includes / Storage / RevisionRecordTests.php
1 <?php
2
3 namespace MediaWiki\Tests\Storage;
4
5 use CommentStoreComment;
6 use LogicException;
7 use MediaWiki\Storage\RevisionRecord;
8 use MediaWiki\Storage\RevisionSlots;
9 use MediaWiki\Storage\RevisionStoreRecord;
10 use MediaWiki\Storage\SlotRecord;
11 use MediaWiki\Storage\SuppressedDataException;
12 use MediaWiki\User\UserIdentityValue;
13 use TextContent;
14 use Title;
15
16 // PHPCS should not complain about @covers and @dataProvider being used in traits, see T192384
17 // phpcs:disable MediaWiki.Commenting.PhpunitAnnotations.NotTestClass
18
19 /**
20 * @covers \MediaWiki\Storage\RevisionRecord
21 *
22 * @note Expects to be used in classes that extend MediaWikiTestCase.
23 */
24 trait RevisionRecordTests {
25
26 /**
27 * @param array $rowOverrides
28 *
29 * @return RevisionRecord
30 */
31 protected abstract function newRevision( array $rowOverrides = [] );
32
33 private function provideAudienceCheckData( $field ) {
34 yield 'field accessible for oversighter (ALL)' => [
35 RevisionRecord::SUPPRESSED_ALL,
36 [ 'oversight' ],
37 true,
38 false
39 ];
40
41 yield 'field accessible for oversighter' => [
42 RevisionRecord::DELETED_RESTRICTED | $field,
43 [ 'oversight' ],
44 true,
45 false
46 ];
47
48 yield 'field not accessible for sysops (ALL)' => [
49 RevisionRecord::SUPPRESSED_ALL,
50 [ 'sysop' ],
51 false,
52 false
53 ];
54
55 yield 'field not accessible for sysops' => [
56 RevisionRecord::DELETED_RESTRICTED | $field,
57 [ 'sysop' ],
58 false,
59 false
60 ];
61
62 yield 'field accessible for sysops' => [
63 $field,
64 [ 'sysop' ],
65 true,
66 false
67 ];
68
69 yield 'field suppressed for logged in users' => [
70 $field,
71 [ 'user' ],
72 false,
73 false
74 ];
75
76 yield 'unrelated field suppressed' => [
77 $field === RevisionRecord::DELETED_COMMENT
78 ? RevisionRecord::DELETED_USER
79 : RevisionRecord::DELETED_COMMENT,
80 [ 'user' ],
81 true,
82 true
83 ];
84
85 yield 'nothing suppressed' => [
86 0,
87 [ 'user' ],
88 true,
89 true
90 ];
91 }
92
93 public function testSerialization_fails() {
94 $this->setExpectedException( LogicException::class );
95 $rev = $this->newRevision();
96 serialize( $rev );
97 }
98
99 public function provideGetComment_audience() {
100 return $this->provideAudienceCheckData( RevisionRecord::DELETED_COMMENT );
101 }
102
103 private function forceStandardPermissions() {
104 $this->setMwGlobals(
105 'wgGroupPermissions',
106 [
107 'user' => [
108 'viewsuppressed' => false,
109 'suppressrevision' => false,
110 'deletedtext' => false,
111 'deletedhistory' => false,
112 ],
113 'sysop' => [
114 'viewsuppressed' => false,
115 'suppressrevision' => false,
116 'deletedtext' => true,
117 'deletedhistory' => true,
118 ],
119 'oversight' => [
120 'deletedtext' => true,
121 'deletedhistory' => true,
122 'viewsuppressed' => true,
123 'suppressrevision' => true,
124 ],
125 ]
126 );
127 }
128
129 /**
130 * @dataProvider provideGetComment_audience
131 */
132 public function testGetComment_audience( $visibility, $groups, $userCan, $publicCan ) {
133 $this->forceStandardPermissions();
134
135 $user = $this->getTestUser( $groups )->getUser();
136 $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
137
138 $this->assertNotNull( $rev->getComment( RevisionRecord::RAW ), 'raw can' );
139
140 $this->assertSame(
141 $publicCan,
142 $rev->getComment( RevisionRecord::FOR_PUBLIC ) !== null,
143 'public can'
144 );
145 $this->assertSame(
146 $userCan,
147 $rev->getComment( RevisionRecord::FOR_THIS_USER, $user ) !== null,
148 'user can'
149 );
150 }
151
152 public function provideGetUser_audience() {
153 return $this->provideAudienceCheckData( RevisionRecord::DELETED_USER );
154 }
155
156 /**
157 * @dataProvider provideGetUser_audience
158 */
159 public function testGetUser_audience( $visibility, $groups, $userCan, $publicCan ) {
160 $this->forceStandardPermissions();
161
162 $user = $this->getTestUser( $groups )->getUser();
163 $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
164
165 $this->assertNotNull( $rev->getUser( RevisionRecord::RAW ), 'raw can' );
166
167 $this->assertSame(
168 $publicCan,
169 $rev->getUser( RevisionRecord::FOR_PUBLIC ) !== null,
170 'public can'
171 );
172 $this->assertSame(
173 $userCan,
174 $rev->getUser( RevisionRecord::FOR_THIS_USER, $user ) !== null,
175 'user can'
176 );
177 }
178
179 public function provideGetSlot_audience() {
180 return $this->provideAudienceCheckData( RevisionRecord::DELETED_TEXT );
181 }
182
183 /**
184 * @dataProvider provideGetSlot_audience
185 */
186 public function testGetSlot_audience( $visibility, $groups, $userCan, $publicCan ) {
187 $this->forceStandardPermissions();
188
189 $user = $this->getTestUser( $groups )->getUser();
190 $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
191
192 // NOTE: slot meta-data is never suppressed, just the content is!
193 $this->assertTrue( $rev->hasSlot( 'main' ), 'hasSlot is never suppressed' );
194 $this->assertNotNull( $rev->getSlot( 'main', RevisionRecord::RAW ), 'raw meta' );
195 $this->assertNotNull( $rev->getSlot( 'main', RevisionRecord::FOR_PUBLIC ), 'public meta' );
196
197 $this->assertNotNull(
198 $rev->getSlot( 'main', RevisionRecord::FOR_THIS_USER, $user ),
199 'user can'
200 );
201
202 try {
203 $rev->getSlot( '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( '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( 'main', RevisionRecord::RAW ), 'raw can' );
239
240 $this->assertSame(
241 $publicCan,
242 $rev->getContent( 'main', RevisionRecord::FOR_PUBLIC ) !== null,
243 'public can'
244 );
245 $this->assertSame(
246 $userCan,
247 $rev->getContent( '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( '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( 'main' ) );
264 $this->assertFalse( $rev->hasSlot( 'xyz' ) );
265 }
266
267 public function testGetContent() {
268 $rev = $this->newRevision();
269
270 $content = $rev->getSlot( '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 Title::newFromText( __METHOD__ ),
350 true,
351 ];
352 yield [
353 RevisionRecord::DELETED_TEXT,
354 RevisionRecord::DELETED_TEXT,
355 [],
356 Title::newFromText( __METHOD__ ),
357 false,
358 ];
359 }
360
361 /**
362 * @dataProvider provideUserCanBitfield
363 * @covers \MediaWiki\Storage\RevisionRecord::userCanBitfield
364 */
365 public function testUserCanBitfield( $bitField, $field, $userGroups, $title, $expected ) {
366 $this->forceStandardPermissions();
367
368 $user = $this->getTestUser( $userGroups )->getUser();
369
370 $this->assertSame(
371 $expected,
372 RevisionRecord::userCanBitfield( $bitField, $field, $user, $title )
373 );
374 }
375
376 public function provideHasSameContent() {
377 /**
378 * @param SlotRecord[] $slots
379 * @param int $revId
380 * @return RevisionStoreRecord
381 */
382 $recordCreator = function ( array $slots, $revId ) {
383 $title = Title::newFromText( 'provideHasSameContent' );
384 $title->resetArticleID( 19 );
385 $slots = new RevisionSlots( $slots );
386
387 return new RevisionStoreRecord(
388 $title,
389 new UserIdentityValue( 11, __METHOD__, 0 ),
390 CommentStoreComment::newUnsavedComment( __METHOD__ ),
391 (object)[
392 'rev_id' => strval( $revId ),
393 'rev_page' => strval( $title->getArticleID() ),
394 'rev_timestamp' => '20200101000000',
395 'rev_deleted' => 0,
396 'rev_minor_edit' => 0,
397 'rev_parent_id' => '5',
398 'rev_len' => $slots->computeSize(),
399 'rev_sha1' => $slots->computeSha1(),
400 'page_latest' => '18',
401 ],
402 $slots
403 );
404 };
405
406 // Create some slots with content
407 $mainA = SlotRecord::newUnsaved( 'main', new TextContent( 'A' ) );
408 $mainB = SlotRecord::newUnsaved( 'main', new TextContent( 'B' ) );
409 $auxA = SlotRecord::newUnsaved( 'aux', new TextContent( 'A' ) );
410 $auxB = SlotRecord::newUnsaved( 'aux', new TextContent( 'A' ) );
411
412 $initialRecord = $recordCreator( [ $mainA ], 12 );
413
414 return [
415 'same record object' => [
416 true,
417 $initialRecord,
418 $initialRecord,
419 ],
420 'same record content, different object' => [
421 true,
422 $recordCreator( [ $mainA ], 12 ),
423 $recordCreator( [ $mainA ], 13 ),
424 ],
425 'same record content, aux slot, different object' => [
426 true,
427 $recordCreator( [ $auxA ], 12 ),
428 $recordCreator( [ $auxB ], 13 ),
429 ],
430 'different content' => [
431 false,
432 $recordCreator( [ $mainA ], 12 ),
433 $recordCreator( [ $mainB ], 13 ),
434 ],
435 'different content and number of slots' => [
436 false,
437 $recordCreator( [ $mainA ], 12 ),
438 $recordCreator( [ $mainA, $mainB ], 13 ),
439 ],
440 ];
441 }
442
443 /**
444 * @dataProvider provideHasSameContent
445 * @covers \MediaWiki\Storage\RevisionRecord::hasSameContent
446 * @group Database
447 */
448 public function testHasSameContent(
449 $expected,
450 RevisionRecord $record1,
451 RevisionRecord $record2
452 ) {
453 $this->assertSame(
454 $expected,
455 $record1->hasSameContent( $record2 )
456 );
457 }
458
459 public function provideIsDeleted() {
460 yield 'no deletion' => [
461 0,
462 [
463 RevisionRecord::DELETED_TEXT => false,
464 RevisionRecord::DELETED_COMMENT => false,
465 RevisionRecord::DELETED_USER => false,
466 RevisionRecord::DELETED_RESTRICTED => false,
467 ]
468 ];
469 yield 'text deleted' => [
470 RevisionRecord::DELETED_TEXT,
471 [
472 RevisionRecord::DELETED_TEXT => true,
473 RevisionRecord::DELETED_COMMENT => false,
474 RevisionRecord::DELETED_USER => false,
475 RevisionRecord::DELETED_RESTRICTED => false,
476 ]
477 ];
478 yield 'text and comment deleted' => [
479 RevisionRecord::DELETED_TEXT + RevisionRecord::DELETED_COMMENT,
480 [
481 RevisionRecord::DELETED_TEXT => true,
482 RevisionRecord::DELETED_COMMENT => true,
483 RevisionRecord::DELETED_USER => false,
484 RevisionRecord::DELETED_RESTRICTED => false,
485 ]
486 ];
487 yield 'all 4 deleted' => [
488 RevisionRecord::DELETED_TEXT +
489 RevisionRecord::DELETED_COMMENT +
490 RevisionRecord::DELETED_RESTRICTED +
491 RevisionRecord::DELETED_USER,
492 [
493 RevisionRecord::DELETED_TEXT => true,
494 RevisionRecord::DELETED_COMMENT => true,
495 RevisionRecord::DELETED_USER => true,
496 RevisionRecord::DELETED_RESTRICTED => true,
497 ]
498 ];
499 }
500
501 /**
502 * @dataProvider provideIsDeleted
503 * @covers \MediaWiki\Storage\RevisionRecord::isDeleted
504 */
505 public function testIsDeleted( $revDeleted, $assertionMap ) {
506 $rev = $this->newRevision( [ 'rev_deleted' => $revDeleted ] );
507 foreach ( $assertionMap as $deletionLevel => $expected ) {
508 $this->assertSame( $expected, $rev->isDeleted( $deletionLevel ) );
509 }
510 }
511
512 }