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