1<?php 2 3// phpcs:disable MediaWiki.Commenting.PhpunitAnnotations.NotClass 4 5namespace MediaWiki\Tests\Revision; 6 7use CommentStoreComment; 8use LogicException; 9use MediaWiki\Revision\RevisionRecord; 10use MediaWiki\Revision\RevisionSlots; 11use MediaWiki\Revision\RevisionStoreRecord; 12use MediaWiki\Revision\SlotRecord; 13use MediaWiki\Revision\SuppressedDataException; 14use MediaWiki\User\UserIdentityValue; 15use TextContent; 16use Title; 17 18/** 19 * @covers \MediaWiki\Revision\RevisionRecord 20 * 21 * @note Expects to be used in classes that extend MediaWikiIntegrationTestCase. 22 */ 23trait 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->expectException( 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} 529