RevisionTest.php 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956
  1. <?php
  2. use MediaWiki\MediaWikiServices;
  3. use MediaWiki\Storage\BlobStoreFactory;
  4. use MediaWiki\Storage\MutableRevisionRecord;
  5. use MediaWiki\Storage\RevisionAccessException;
  6. use MediaWiki\Storage\RevisionRecord;
  7. use MediaWiki\Storage\RevisionStore;
  8. use MediaWiki\Storage\SlotRecord;
  9. use MediaWiki\Storage\SqlBlobStore;
  10. use Wikimedia\Rdbms\IDatabase;
  11. use Wikimedia\Rdbms\LoadBalancer;
  12. /**
  13. * Test cases in RevisionTest should not interact with the Database.
  14. * For test cases that need Database interaction see RevisionDbTestBase.
  15. */
  16. class RevisionTest extends MediaWikiTestCase {
  17. public function setUp() {
  18. parent::setUp();
  19. $this->setMwGlobals(
  20. 'wgMultiContentRevisionSchemaMigrationStage',
  21. SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
  22. );
  23. }
  24. public function provideConstructFromArray() {
  25. yield 'with text' => [
  26. [
  27. 'text' => 'hello world.',
  28. 'content_model' => CONTENT_MODEL_JAVASCRIPT
  29. ],
  30. ];
  31. yield 'with content' => [
  32. [
  33. 'content' => new JavaScriptContent( 'hellow world.' )
  34. ],
  35. ];
  36. // FIXME: test with and without user ID, and with a user object.
  37. // We can't prepare that here though, since we don't yet have a dummy DB
  38. }
  39. /**
  40. * @param string $model
  41. * @return Title
  42. */
  43. public function getMockTitle( $model = CONTENT_MODEL_WIKITEXT ) {
  44. $mock = $this->getMockBuilder( Title::class )
  45. ->disableOriginalConstructor()
  46. ->getMock();
  47. $mock->expects( $this->any() )
  48. ->method( 'getNamespace' )
  49. ->will( $this->returnValue( $this->getDefaultWikitextNS() ) );
  50. $mock->expects( $this->any() )
  51. ->method( 'getPrefixedText' )
  52. ->will( $this->returnValue( 'RevisionTest' ) );
  53. $mock->expects( $this->any() )
  54. ->method( 'getDBkey' )
  55. ->will( $this->returnValue( 'RevisionTest' ) );
  56. $mock->expects( $this->any() )
  57. ->method( 'getArticleID' )
  58. ->will( $this->returnValue( 23 ) );
  59. $mock->expects( $this->any() )
  60. ->method( 'getContentModel' )
  61. ->will( $this->returnValue( $model ) );
  62. return $mock;
  63. }
  64. /**
  65. * @dataProvider provideConstructFromArray
  66. * @covers Revision::__construct
  67. * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
  68. */
  69. public function testConstructFromArray( $rowArray ) {
  70. $rev = new Revision( $rowArray, 0, $this->getMockTitle() );
  71. $this->assertNotNull( $rev->getContent(), 'no content object available' );
  72. $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContent()->getModel() );
  73. $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() );
  74. }
  75. /**
  76. * @covers Revision::__construct
  77. * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
  78. */
  79. public function testConstructFromEmptyArray() {
  80. $rev = new Revision( [], 0, $this->getMockTitle() );
  81. $this->assertNull( $rev->getContent(), 'no content object should be available' );
  82. }
  83. /**
  84. * @covers Revision::__construct
  85. * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
  86. */
  87. public function testConstructFromArrayWithBadPageId() {
  88. Wikimedia\suppressWarnings();
  89. $rev = new Revision( [ 'page' => 77777777 ] );
  90. $this->assertSame( 77777777, $rev->getPage() );
  91. Wikimedia\restoreWarnings();
  92. }
  93. public function provideConstructFromArray_userSetAsExpected() {
  94. yield 'no user defaults to wgUser' => [
  95. [
  96. 'content' => new JavaScriptContent( 'hello world.' ),
  97. ],
  98. null,
  99. null,
  100. ];
  101. yield 'user text and id' => [
  102. [
  103. 'content' => new JavaScriptContent( 'hello world.' ),
  104. 'user_text' => 'SomeTextUserName',
  105. 'user' => 99,
  106. ],
  107. 99,
  108. 'SomeTextUserName',
  109. ];
  110. yield 'user text only' => [
  111. [
  112. 'content' => new JavaScriptContent( 'hello world.' ),
  113. 'user_text' => '111.111.111.111',
  114. ],
  115. 0,
  116. '111.111.111.111',
  117. ];
  118. }
  119. /**
  120. * @dataProvider provideConstructFromArray_userSetAsExpected
  121. * @covers Revision::__construct
  122. * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
  123. *
  124. * @param array $rowArray
  125. * @param mixed $expectedUserId null to expect the current wgUser ID
  126. * @param mixed $expectedUserName null to expect the current wgUser name
  127. */
  128. public function testConstructFromArray_userSetAsExpected(
  129. array $rowArray,
  130. $expectedUserId,
  131. $expectedUserName
  132. ) {
  133. $testUser = $this->getTestUser()->getUser();
  134. $this->setMwGlobals( 'wgUser', $testUser );
  135. if ( $expectedUserId === null ) {
  136. $expectedUserId = $testUser->getId();
  137. }
  138. if ( $expectedUserName === null ) {
  139. $expectedUserName = $testUser->getName();
  140. }
  141. $rev = new Revision( $rowArray, 0, $this->getMockTitle() );
  142. $this->assertEquals( $expectedUserId, $rev->getUser() );
  143. $this->assertEquals( $expectedUserName, $rev->getUserText() );
  144. }
  145. public function provideConstructFromArrayThrowsExceptions() {
  146. yield 'content and text_id both not empty' => [
  147. [
  148. 'content' => new WikitextContent( 'GOAT' ),
  149. 'text_id' => 'someid',
  150. ],
  151. new MWException( 'The text_id field is only available in the pre-MCR schema' )
  152. ];
  153. yield 'with bad content object (class)' => [
  154. [ 'content' => new stdClass() ],
  155. new MWException( 'content field must contain a Content object' )
  156. ];
  157. yield 'with bad content object (string)' => [
  158. [ 'content' => 'ImAGoat' ],
  159. new MWException( 'content field must contain a Content object' )
  160. ];
  161. yield 'bad row format' => [
  162. 'imastring, not a row',
  163. new InvalidArgumentException(
  164. '$row must be a row object, an associative array, or a RevisionRecord'
  165. )
  166. ];
  167. }
  168. /**
  169. * @dataProvider provideConstructFromArrayThrowsExceptions
  170. * @covers Revision::__construct
  171. * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
  172. */
  173. public function testConstructFromArrayThrowsExceptions( $rowArray, Exception $expectedException ) {
  174. $this->setExpectedException(
  175. get_class( $expectedException ),
  176. $expectedException->getMessage(),
  177. $expectedException->getCode()
  178. );
  179. new Revision( $rowArray, 0, $this->getMockTitle() );
  180. }
  181. /**
  182. * @covers Revision::__construct
  183. * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
  184. */
  185. public function testConstructFromNothing() {
  186. $this->setExpectedException(
  187. InvalidArgumentException::class
  188. );
  189. new Revision( [] );
  190. }
  191. public function provideConstructFromRow() {
  192. yield 'Full construction' => [
  193. [
  194. 'rev_id' => '42',
  195. 'rev_page' => '23',
  196. 'rev_timestamp' => '20171017114835',
  197. 'rev_user_text' => '127.0.0.1',
  198. 'rev_user' => '0',
  199. 'rev_minor_edit' => '0',
  200. 'rev_deleted' => '0',
  201. 'rev_len' => '46',
  202. 'rev_parent_id' => '1',
  203. 'rev_sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
  204. 'rev_comment_text' => 'Goat Comment!',
  205. 'rev_comment_data' => null,
  206. 'rev_comment_cid' => null,
  207. ],
  208. function ( RevisionTest $testCase, Revision $rev ) {
  209. $testCase->assertSame( 42, $rev->getId() );
  210. $testCase->assertSame( 23, $rev->getPage() );
  211. $testCase->assertSame( '20171017114835', $rev->getTimestamp() );
  212. $testCase->assertSame( '127.0.0.1', $rev->getUserText() );
  213. $testCase->assertSame( 0, $rev->getUser() );
  214. $testCase->assertSame( false, $rev->isMinor() );
  215. $testCase->assertSame( false, $rev->isDeleted( Revision::DELETED_TEXT ) );
  216. $testCase->assertSame( 46, $rev->getSize() );
  217. $testCase->assertSame( 1, $rev->getParentId() );
  218. $testCase->assertSame( 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', $rev->getSha1() );
  219. $testCase->assertSame( 'Goat Comment!', $rev->getComment() );
  220. }
  221. ];
  222. yield 'default field values' => [
  223. [
  224. 'rev_id' => '42',
  225. 'rev_page' => '23',
  226. 'rev_timestamp' => '20171017114835',
  227. 'rev_user_text' => '127.0.0.1',
  228. 'rev_user' => '0',
  229. 'rev_minor_edit' => '0',
  230. 'rev_deleted' => '0',
  231. 'rev_comment_text' => 'Goat Comment!',
  232. 'rev_comment_data' => null,
  233. 'rev_comment_cid' => null,
  234. ],
  235. function ( RevisionTest $testCase, Revision $rev ) {
  236. // parent ID may be null
  237. $testCase->assertSame( null, $rev->getParentId(), 'revision id' );
  238. // given fields
  239. $testCase->assertSame( $rev->getTimestamp(), '20171017114835', 'timestamp' );
  240. $testCase->assertSame( $rev->getUserText(), '127.0.0.1', 'user name' );
  241. $testCase->assertSame( $rev->getUser(), 0, 'user id' );
  242. $testCase->assertSame( $rev->getComment(), 'Goat Comment!' );
  243. $testCase->assertSame( false, $rev->isMinor(), 'minor edit' );
  244. $testCase->assertSame( 0, $rev->getVisibility(), 'visibility flags' );
  245. }
  246. ];
  247. }
  248. /**
  249. * @dataProvider provideConstructFromRow
  250. * @covers Revision::__construct
  251. * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
  252. */
  253. public function testConstructFromRow( array $arrayData, callable $assertions ) {
  254. $row = (object)$arrayData;
  255. $rev = new Revision( $row, 0, $this->getMockTitle() );
  256. $assertions( $this, $rev );
  257. }
  258. /**
  259. * @covers Revision::__construct
  260. * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
  261. */
  262. public function testConstructFromRowWithBadPageId() {
  263. $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
  264. $this->overrideMwServices();
  265. Wikimedia\suppressWarnings();
  266. $rev = new Revision( (object)[ 'rev_page' => 77777777 ] );
  267. $this->assertSame( 77777777, $rev->getPage() );
  268. Wikimedia\restoreWarnings();
  269. }
  270. public function provideGetRevisionText() {
  271. yield 'Generic test' => [
  272. 'This is a goat of revision text.',
  273. [
  274. 'old_flags' => '',
  275. 'old_text' => 'This is a goat of revision text.',
  276. ],
  277. ];
  278. }
  279. public function provideGetId() {
  280. yield [
  281. [],
  282. null
  283. ];
  284. yield [
  285. [ 'id' => 998 ],
  286. 998
  287. ];
  288. }
  289. /**
  290. * @dataProvider provideGetId
  291. * @covers Revision::getId
  292. */
  293. public function testGetId( $rowArray, $expectedId ) {
  294. $rev = new Revision( $rowArray, 0, $this->getMockTitle() );
  295. $this->assertEquals( $expectedId, $rev->getId() );
  296. }
  297. public function provideSetId() {
  298. yield [ '123', 123 ];
  299. yield [ 456, 456 ];
  300. }
  301. /**
  302. * @dataProvider provideSetId
  303. * @covers Revision::setId
  304. */
  305. public function testSetId( $input, $expected ) {
  306. $rev = new Revision( [], 0, $this->getMockTitle() );
  307. $rev->setId( $input );
  308. $this->assertSame( $expected, $rev->getId() );
  309. }
  310. public function provideSetUserIdAndName() {
  311. yield [ '123', 123, 'GOaT' ];
  312. yield [ 456, 456, 'GOaT' ];
  313. }
  314. /**
  315. * @dataProvider provideSetUserIdAndName
  316. * @covers Revision::setUserIdAndName
  317. */
  318. public function testSetUserIdAndName( $inputId, $expectedId, $name ) {
  319. $rev = new Revision( [], 0, $this->getMockTitle() );
  320. $rev->setUserIdAndName( $inputId, $name );
  321. $this->assertSame( $expectedId, $rev->getUser( Revision::RAW ) );
  322. $this->assertEquals( $name, $rev->getUserText( Revision::RAW ) );
  323. }
  324. public function provideGetParentId() {
  325. yield [ [], null ];
  326. yield [ [ 'parent_id' => '123' ], 123 ];
  327. yield [ [ 'parent_id' => 456 ], 456 ];
  328. }
  329. /**
  330. * @dataProvider provideGetParentId
  331. * @covers Revision::getParentId()
  332. */
  333. public function testGetParentId( $rowArray, $expected ) {
  334. $rev = new Revision( $rowArray, 0, $this->getMockTitle() );
  335. $this->assertSame( $expected, $rev->getParentId() );
  336. }
  337. /**
  338. * @covers Revision::getRevisionText
  339. * @dataProvider provideGetRevisionText
  340. */
  341. public function testGetRevisionText( $expected, $rowData, $prefix = 'old_', $wiki = false ) {
  342. $this->assertEquals(
  343. $expected,
  344. Revision::getRevisionText( (object)$rowData, $prefix, $wiki ) );
  345. }
  346. public function provideGetRevisionTextWithZlibExtension() {
  347. yield 'Generic gzip test' => [
  348. 'This is a small goat of revision text.',
  349. [
  350. 'old_flags' => 'gzip',
  351. 'old_text' => gzdeflate( 'This is a small goat of revision text.' ),
  352. ],
  353. ];
  354. }
  355. /**
  356. * @covers Revision::getRevisionText
  357. * @dataProvider provideGetRevisionTextWithZlibExtension
  358. */
  359. public function testGetRevisionWithZlibExtension( $expected, $rowData ) {
  360. $this->checkPHPExtension( 'zlib' );
  361. $this->testGetRevisionText( $expected, $rowData );
  362. }
  363. public function provideGetRevisionTextWithZlibExtension_badData() {
  364. yield 'Generic gzip test' => [
  365. 'This is a small goat of revision text.',
  366. [
  367. 'old_flags' => 'gzip',
  368. 'old_text' => 'DEAD BEEF',
  369. ],
  370. ];
  371. }
  372. /**
  373. * @covers Revision::getRevisionText
  374. * @dataProvider provideGetRevisionTextWithZlibExtension_badData
  375. */
  376. public function testGetRevisionWithZlibExtension_badData( $expected, $rowData ) {
  377. $this->checkPHPExtension( 'zlib' );
  378. Wikimedia\suppressWarnings();
  379. $this->assertFalse(
  380. Revision::getRevisionText(
  381. (object)$rowData
  382. )
  383. );
  384. Wikimedia\suppressWarnings( true );
  385. }
  386. private function getWANObjectCache() {
  387. return new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
  388. }
  389. /**
  390. * @return SqlBlobStore
  391. */
  392. private function getBlobStore() {
  393. /** @var LoadBalancer $lb */
  394. $lb = $this->getMockBuilder( LoadBalancer::class )
  395. ->disableOriginalConstructor()
  396. ->getMock();
  397. $cache = $this->getWANObjectCache();
  398. $blobStore = new SqlBlobStore( $lb, $cache );
  399. return $blobStore;
  400. }
  401. private function mockBlobStoreFactory( $blobStore ) {
  402. /** @var LoadBalancer $lb */
  403. $factory = $this->getMockBuilder( BlobStoreFactory::class )
  404. ->disableOriginalConstructor()
  405. ->getMock();
  406. $factory->expects( $this->any() )
  407. ->method( 'newBlobStore' )
  408. ->willReturn( $blobStore );
  409. $factory->expects( $this->any() )
  410. ->method( 'newSqlBlobStore' )
  411. ->willReturn( $blobStore );
  412. return $factory;
  413. }
  414. /**
  415. * @return RevisionStore
  416. */
  417. private function getRevisionStore() {
  418. /** @var LoadBalancer $lb */
  419. $lb = $this->getMockBuilder( LoadBalancer::class )
  420. ->disableOriginalConstructor()
  421. ->getMock();
  422. $cache = $this->getWANObjectCache();
  423. $blobStore = new RevisionStore(
  424. $lb,
  425. $this->getBlobStore(),
  426. $cache,
  427. MediaWikiServices::getInstance()->getCommentStore(),
  428. MediaWikiServices::getInstance()->getContentModelStore(),
  429. MediaWikiServices::getInstance()->getSlotRoleStore(),
  430. MIGRATION_OLD,
  431. MediaWikiServices::getInstance()->getActorMigration()
  432. );
  433. return $blobStore;
  434. }
  435. public function provideGetRevisionTextWithLegacyEncoding() {
  436. yield 'Utf8Native' => [
  437. "Wiki est l'\xc3\xa9cole superieur !",
  438. 'fr',
  439. 'iso-8859-1',
  440. [
  441. 'old_flags' => 'utf-8',
  442. 'old_text' => "Wiki est l'\xc3\xa9cole superieur !",
  443. ]
  444. ];
  445. yield 'Utf8Legacy' => [
  446. "Wiki est l'\xc3\xa9cole superieur !",
  447. 'fr',
  448. 'iso-8859-1',
  449. [
  450. 'old_flags' => '',
  451. 'old_text' => "Wiki est l'\xe9cole superieur !",
  452. ]
  453. ];
  454. }
  455. /**
  456. * @covers Revision::getRevisionText
  457. * @dataProvider provideGetRevisionTextWithLegacyEncoding
  458. */
  459. public function testGetRevisionWithLegacyEncoding( $expected, $lang, $encoding, $rowData ) {
  460. $blobStore = $this->getBlobStore();
  461. $blobStore->setLegacyEncoding( $encoding, Language::factory( $lang ) );
  462. $this->setService( 'BlobStoreFactory', $this->mockBlobStoreFactory( $blobStore ) );
  463. $this->testGetRevisionText( $expected, $rowData );
  464. }
  465. public function provideGetRevisionTextWithGzipAndLegacyEncoding() {
  466. /**
  467. * WARNING!
  468. * Do not set the external flag!
  469. * Otherwise, getRevisionText will hit the live database (if ExternalStore is enabled)!
  470. */
  471. yield 'Utf8NativeGzip' => [
  472. "Wiki est l'\xc3\xa9cole superieur !",
  473. 'fr',
  474. 'iso-8859-1',
  475. [
  476. 'old_flags' => 'gzip,utf-8',
  477. 'old_text' => gzdeflate( "Wiki est l'\xc3\xa9cole superieur !" ),
  478. ]
  479. ];
  480. yield 'Utf8LegacyGzip' => [
  481. "Wiki est l'\xc3\xa9cole superieur !",
  482. 'fr',
  483. 'iso-8859-1',
  484. [
  485. 'old_flags' => 'gzip',
  486. 'old_text' => gzdeflate( "Wiki est l'\xe9cole superieur !" ),
  487. ]
  488. ];
  489. }
  490. /**
  491. * @covers Revision::getRevisionText
  492. * @dataProvider provideGetRevisionTextWithGzipAndLegacyEncoding
  493. */
  494. public function testGetRevisionWithGzipAndLegacyEncoding( $expected, $lang, $encoding, $rowData ) {
  495. $this->checkPHPExtension( 'zlib' );
  496. $blobStore = $this->getBlobStore();
  497. $blobStore->setLegacyEncoding( $encoding, Language::factory( $lang ) );
  498. $this->setService( 'BlobStoreFactory', $this->mockBlobStoreFactory( $blobStore ) );
  499. $this->testGetRevisionText( $expected, $rowData );
  500. }
  501. /**
  502. * @covers Revision::compressRevisionText
  503. */
  504. public function testCompressRevisionTextUtf8() {
  505. $row = new stdClass;
  506. $row->old_text = "Wiki est l'\xc3\xa9cole superieur !";
  507. $row->old_flags = Revision::compressRevisionText( $row->old_text );
  508. $this->assertTrue( false !== strpos( $row->old_flags, 'utf-8' ),
  509. "Flags should contain 'utf-8'" );
  510. $this->assertFalse( false !== strpos( $row->old_flags, 'gzip' ),
  511. "Flags should not contain 'gzip'" );
  512. $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !",
  513. $row->old_text, "Direct check" );
  514. $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !",
  515. Revision::getRevisionText( $row ), "getRevisionText" );
  516. }
  517. /**
  518. * @covers Revision::compressRevisionText
  519. */
  520. public function testCompressRevisionTextUtf8Gzip() {
  521. $this->checkPHPExtension( 'zlib' );
  522. $blobStore = $this->getBlobStore();
  523. $blobStore->setCompressBlobs( true );
  524. $this->setService( 'BlobStoreFactory', $this->mockBlobStoreFactory( $blobStore ) );
  525. $row = new stdClass;
  526. $row->old_text = "Wiki est l'\xc3\xa9cole superieur !";
  527. $row->old_flags = Revision::compressRevisionText( $row->old_text );
  528. $this->assertTrue( false !== strpos( $row->old_flags, 'utf-8' ),
  529. "Flags should contain 'utf-8'" );
  530. $this->assertTrue( false !== strpos( $row->old_flags, 'gzip' ),
  531. "Flags should contain 'gzip'" );
  532. $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !",
  533. gzinflate( $row->old_text ), "Direct check" );
  534. $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !",
  535. Revision::getRevisionText( $row ), "getRevisionText" );
  536. }
  537. /**
  538. * @covers Revision::loadFromTitle
  539. */
  540. public function testLoadFromTitle() {
  541. $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
  542. $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD );
  543. $this->overrideMwServices();
  544. $title = $this->getMockTitle();
  545. $conditions = [
  546. 'rev_id=page_latest',
  547. 'page_namespace' => $title->getNamespace(),
  548. 'page_title' => $title->getDBkey()
  549. ];
  550. $row = (object)[
  551. 'rev_id' => '42',
  552. 'rev_page' => $title->getArticleID(),
  553. 'rev_timestamp' => '20171017114835',
  554. 'rev_user_text' => '127.0.0.1',
  555. 'rev_user' => '0',
  556. 'rev_minor_edit' => '0',
  557. 'rev_deleted' => '0',
  558. 'rev_len' => '46',
  559. 'rev_parent_id' => '1',
  560. 'rev_sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
  561. 'rev_comment_text' => 'Goat Comment!',
  562. 'rev_comment_data' => null,
  563. 'rev_comment_cid' => null,
  564. 'rev_content_format' => 'GOATFORMAT',
  565. 'rev_content_model' => 'GOATMODEL',
  566. ];
  567. $db = $this->getMock( IDatabase::class );
  568. $db->expects( $this->any() )
  569. ->method( 'getDomainId' )
  570. ->will( $this->returnValue( wfWikiID() ) );
  571. $db->expects( $this->once() )
  572. ->method( 'selectRow' )
  573. ->with(
  574. $this->equalTo( [ 'revision', 'page', 'user' ] ),
  575. // We don't really care about the fields are they come from the selectField methods
  576. $this->isType( 'array' ),
  577. $this->equalTo( $conditions ),
  578. // Method name
  579. $this->stringContains( 'fetchRevisionRowFromConds' ),
  580. // We don't really care about the options here
  581. $this->isType( 'array' ),
  582. // We don't really care about the join conds are they come from the joinCond methods
  583. $this->isType( 'array' )
  584. )
  585. ->willReturn( $row );
  586. $revision = Revision::loadFromTitle( $db, $title );
  587. $this->assertEquals( $title->getArticleID(), $revision->getTitle()->getArticleID() );
  588. $this->assertEquals( $row->rev_id, $revision->getId() );
  589. $this->assertEquals( $row->rev_len, $revision->getSize() );
  590. $this->assertEquals( $row->rev_sha1, $revision->getSha1() );
  591. $this->assertEquals( $row->rev_parent_id, $revision->getParentId() );
  592. $this->assertEquals( $row->rev_timestamp, $revision->getTimestamp() );
  593. $this->assertEquals( $row->rev_comment_text, $revision->getComment() );
  594. $this->assertEquals( $row->rev_user_text, $revision->getUserText() );
  595. }
  596. public function provideDecompressRevisionText() {
  597. yield '(no legacy encoding), false in false out' => [ false, false, [], false ];
  598. yield '(no legacy encoding), empty in empty out' => [ false, '', [], '' ];
  599. yield '(no legacy encoding), empty in empty out' => [ false, 'A', [], 'A' ];
  600. yield '(no legacy encoding), string in with gzip flag returns string' => [
  601. // gzip string below generated with gzdeflate( 'AAAABBAAA' )
  602. false, "sttttr\002\022\000", [ 'gzip' ], 'AAAABBAAA',
  603. ];
  604. yield '(no legacy encoding), string in with object flag returns false' => [
  605. // gzip string below generated with serialize( 'JOJO' )
  606. false, "s:4:\"JOJO\";", [ 'object' ], false,
  607. ];
  608. yield '(no legacy encoding), serialized object in with object flag returns string' => [
  609. false,
  610. // Using a TitleValue object as it has a getText method (which is needed)
  611. serialize( new TitleValue( 0, 'HHJJDDFF' ) ),
  612. [ 'object' ],
  613. 'HHJJDDFF',
  614. ];
  615. yield '(no legacy encoding), serialized object in with object & gzip flag returns string' => [
  616. false,
  617. // Using a TitleValue object as it has a getText method (which is needed)
  618. gzdeflate( serialize( new TitleValue( 0, '8219JJJ840' ) ) ),
  619. [ 'object', 'gzip' ],
  620. '8219JJJ840',
  621. ];
  622. yield '(ISO-8859-1 encoding), string in string out' => [
  623. 'ISO-8859-1',
  624. iconv( 'utf-8', 'ISO-8859-1', "1®Àþ1" ),
  625. [],
  626. '1®Àþ1',
  627. ];
  628. yield '(ISO-8859-1 encoding), serialized object in with gzip flags returns string' => [
  629. 'ISO-8859-1',
  630. gzdeflate( iconv( 'utf-8', 'ISO-8859-1', "4®Àþ4" ) ),
  631. [ 'gzip' ],
  632. '4®Àþ4',
  633. ];
  634. yield '(ISO-8859-1 encoding), serialized object in with object flags returns string' => [
  635. 'ISO-8859-1',
  636. serialize( new TitleValue( 0, iconv( 'utf-8', 'ISO-8859-1', "3®Àþ3" ) ) ),
  637. [ 'object' ],
  638. '3®Àþ3',
  639. ];
  640. yield '(ISO-8859-1 encoding), serialized object in with object & gzip flags returns string' => [
  641. 'ISO-8859-1',
  642. gzdeflate( serialize( new TitleValue( 0, iconv( 'utf-8', 'ISO-8859-1', "2®Àþ2" ) ) ) ),
  643. [ 'gzip', 'object' ],
  644. '2®Àþ2',
  645. ];
  646. }
  647. /**
  648. * @dataProvider provideDecompressRevisionText
  649. * @covers Revision::decompressRevisionText
  650. *
  651. * @param bool $legacyEncoding
  652. * @param mixed $text
  653. * @param array $flags
  654. * @param mixed $expected
  655. */
  656. public function testDecompressRevisionText( $legacyEncoding, $text, $flags, $expected ) {
  657. $blobStore = $this->getBlobStore();
  658. if ( $legacyEncoding ) {
  659. $blobStore->setLegacyEncoding( $legacyEncoding, Language::factory( 'en' ) );
  660. }
  661. $this->setService( 'BlobStoreFactory', $this->mockBlobStoreFactory( $blobStore ) );
  662. $this->assertSame(
  663. $expected,
  664. Revision::decompressRevisionText( $text, $flags )
  665. );
  666. }
  667. /**
  668. * @covers Revision::getRevisionText
  669. */
  670. public function testGetRevisionText_returnsFalseWhenNoTextField() {
  671. $this->assertFalse( Revision::getRevisionText( new stdClass() ) );
  672. }
  673. public function provideTestGetRevisionText_returnsDecompressedTextFieldWhenNotExternal() {
  674. yield 'Just text' => [
  675. (object)[ 'old_text' => 'SomeText' ],
  676. 'old_',
  677. 'SomeText'
  678. ];
  679. // gzip string below generated with gzdeflate( 'AAAABBAAA' )
  680. yield 'gzip text' => [
  681. (object)[
  682. 'old_text' => "sttttr\002\022\000",
  683. 'old_flags' => 'gzip'
  684. ],
  685. 'old_',
  686. 'AAAABBAAA'
  687. ];
  688. yield 'gzip text and different prefix' => [
  689. (object)[
  690. 'jojo_text' => "sttttr\002\022\000",
  691. 'jojo_flags' => 'gzip'
  692. ],
  693. 'jojo_',
  694. 'AAAABBAAA'
  695. ];
  696. }
  697. /**
  698. * @dataProvider provideTestGetRevisionText_returnsDecompressedTextFieldWhenNotExternal
  699. * @covers Revision::getRevisionText
  700. */
  701. public function testGetRevisionText_returnsDecompressedTextFieldWhenNotExternal(
  702. $row,
  703. $prefix,
  704. $expected
  705. ) {
  706. $this->assertSame( $expected, Revision::getRevisionText( $row, $prefix ) );
  707. }
  708. public function provideTestGetRevisionText_external_returnsFalseWhenNotEnoughUrlParts() {
  709. yield 'Just some text' => [ 'someNonUrlText' ];
  710. yield 'No second URL part' => [ 'someProtocol://' ];
  711. }
  712. /**
  713. * @dataProvider provideTestGetRevisionText_external_returnsFalseWhenNotEnoughUrlParts
  714. * @covers Revision::getRevisionText
  715. */
  716. public function testGetRevisionText_external_returnsFalseWhenNotEnoughUrlParts(
  717. $text
  718. ) {
  719. Wikimedia\suppressWarnings();
  720. $this->assertFalse(
  721. Revision::getRevisionText(
  722. (object)[
  723. 'old_text' => $text,
  724. 'old_flags' => 'external',
  725. ]
  726. )
  727. );
  728. Wikimedia\suppressWarnings( true );
  729. }
  730. /**
  731. * @covers Revision::getRevisionText
  732. */
  733. public function testGetRevisionText_external_noOldId() {
  734. $this->setService(
  735. 'ExternalStoreFactory',
  736. new ExternalStoreFactory( [ 'ForTesting' ] )
  737. );
  738. $this->assertSame(
  739. 'AAAABBAAA',
  740. Revision::getRevisionText(
  741. (object)[
  742. 'old_text' => 'ForTesting://cluster1/12345',
  743. 'old_flags' => 'external,gzip',
  744. ]
  745. )
  746. );
  747. }
  748. /**
  749. * @covers Revision::getRevisionText
  750. */
  751. public function testGetRevisionText_external_oldId() {
  752. $cache = $this->getWANObjectCache();
  753. $this->setService( 'MainWANObjectCache', $cache );
  754. $this->setService(
  755. 'ExternalStoreFactory',
  756. new ExternalStoreFactory( [ 'ForTesting' ] )
  757. );
  758. $lb = $this->getMockBuilder( LoadBalancer::class )
  759. ->disableOriginalConstructor()
  760. ->getMock();
  761. $blobStore = new SqlBlobStore( $lb, $cache );
  762. $this->setService( 'BlobStoreFactory', $this->mockBlobStoreFactory( $blobStore ) );
  763. $this->assertSame(
  764. 'AAAABBAAA',
  765. Revision::getRevisionText(
  766. (object)[
  767. 'old_text' => 'ForTesting://cluster1/12345',
  768. 'old_flags' => 'external,gzip',
  769. 'old_id' => '7777',
  770. ]
  771. )
  772. );
  773. $cacheKey = $cache->makeGlobalKey(
  774. 'BlobStore',
  775. 'address',
  776. $lb->getLocalDomainID(),
  777. 'tt:7777'
  778. );
  779. $this->assertSame( 'AAAABBAAA', $cache->get( $cacheKey ) );
  780. }
  781. /**
  782. * @covers Revision::getSize
  783. */
  784. public function testGetSize() {
  785. $title = $this->getMockTitle();
  786. $rec = new MutableRevisionRecord( $title );
  787. $rev = new Revision( $rec, 0, $title );
  788. $this->assertSame( 0, $rev->getSize(), 'Size of no slots is 0' );
  789. $rec->setSize( 13 );
  790. $this->assertSame( 13, $rev->getSize() );
  791. }
  792. /**
  793. * @covers Revision::getSize
  794. */
  795. public function testGetSize_failure() {
  796. $title = $this->getMockTitle();
  797. $rec = $this->getMockBuilder( RevisionRecord::class )
  798. ->disableOriginalConstructor()
  799. ->getMock();
  800. $rec->method( 'getSize' )
  801. ->willThrowException( new RevisionAccessException( 'Oops!' ) );
  802. $rev = new Revision( $rec, 0, $title );
  803. $this->assertNull( $rev->getSize() );
  804. }
  805. /**
  806. * @covers Revision::getSha1
  807. */
  808. public function testGetSha1() {
  809. $title = $this->getMockTitle();
  810. $rec = new MutableRevisionRecord( $title );
  811. $rev = new Revision( $rec, 0, $title );
  812. $emptyHash = SlotRecord::base36Sha1( '' );
  813. $this->assertSame( $emptyHash, $rev->getSha1(), 'Sha1 of no slots is hash of empty string' );
  814. $rec->setSha1( 'deadbeef' );
  815. $this->assertSame( 'deadbeef', $rev->getSha1() );
  816. }
  817. /**
  818. * @covers Revision::getSha1
  819. */
  820. public function testGetSha1_failure() {
  821. $title = $this->getMockTitle();
  822. $rec = $this->getMockBuilder( RevisionRecord::class )
  823. ->disableOriginalConstructor()
  824. ->getMock();
  825. $rec->method( 'getSha1' )
  826. ->willThrowException( new RevisionAccessException( 'Oops!' ) );
  827. $rev = new Revision( $rec, 0, $title );
  828. $this->assertNull( $rev->getSha1() );
  829. }
  830. /**
  831. * @covers Revision::getContent
  832. */
  833. public function testGetContent() {
  834. $title = $this->getMockTitle();
  835. $rec = new MutableRevisionRecord( $title );
  836. $rev = new Revision( $rec, 0, $title );
  837. $this->assertNull( $rev->getContent(), 'Content of no slots is null' );
  838. $content = new TextContent( 'Hello Kittens!' );
  839. $rec->setContent( 'main', $content );
  840. $this->assertSame( $content, $rev->getContent() );
  841. }
  842. /**
  843. * @covers Revision::getContent
  844. */
  845. public function testGetContent_failure() {
  846. $title = $this->getMockTitle();
  847. $rec = $this->getMockBuilder( RevisionRecord::class )
  848. ->disableOriginalConstructor()
  849. ->getMock();
  850. $rec->method( 'getContent' )
  851. ->willThrowException( new RevisionAccessException( 'Oops!' ) );
  852. $rev = new Revision( $rec, 0, $title );
  853. $this->assertNull( $rev->getContent() );
  854. }
  855. }