ApiComparePages.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739
  1. <?php
  2. /**
  3. *
  4. * This program is free software; you can redistribute it and/or modify
  5. * it under the terms of the GNU General Public License as published by
  6. * the Free Software Foundation; either version 2 of the License, or
  7. * (at your option) any later version.
  8. *
  9. * This program is distributed in the hope that it will be useful,
  10. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. * GNU General Public License for more details.
  13. *
  14. * You should have received a copy of the GNU General Public License along
  15. * with this program; if not, write to the Free Software Foundation, Inc.,
  16. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  17. * http://www.gnu.org/copyleft/gpl.html
  18. *
  19. * @file
  20. */
  21. use MediaWiki\MediaWikiServices;
  22. use MediaWiki\Revision\MutableRevisionRecord;
  23. use MediaWiki\Revision\RevisionRecord;
  24. use MediaWiki\Revision\RevisionArchiveRecord;
  25. use MediaWiki\Revision\RevisionStore;
  26. use MediaWiki\Revision\SlotRecord;
  27. /**
  28. * @ingroup API
  29. */
  30. class ApiComparePages extends ApiBase {
  31. /** @var RevisionStore */
  32. private $revisionStore;
  33. /** @var \MediaWiki\Revision\SlotRoleRegistry */
  34. private $slotRoleRegistry;
  35. private $guessedTitle = false, $props;
  36. public function __construct( ApiMain $mainModule, $moduleName, $modulePrefix = '' ) {
  37. parent::__construct( $mainModule, $moduleName, $modulePrefix );
  38. $this->revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
  39. $this->slotRoleRegistry = MediaWikiServices::getInstance()->getSlotRoleRegistry();
  40. }
  41. public function execute() {
  42. $params = $this->extractRequestParams();
  43. // Parameter validation
  44. $this->requireAtLeastOneParameter(
  45. $params, 'fromtitle', 'fromid', 'fromrev', 'fromtext', 'fromslots'
  46. );
  47. $this->requireAtLeastOneParameter(
  48. $params, 'totitle', 'toid', 'torev', 'totext', 'torelative', 'toslots'
  49. );
  50. $this->props = array_flip( $params['prop'] );
  51. // Cache responses publicly by default. This may be overridden later.
  52. $this->getMain()->setCacheMode( 'public' );
  53. // Get the 'from' RevisionRecord
  54. list( $fromRev, $fromRelRev, $fromValsRev ) = $this->getDiffRevision( 'from', $params );
  55. // Get the 'to' RevisionRecord
  56. if ( $params['torelative'] !== null ) {
  57. if ( !$fromRelRev ) {
  58. $this->dieWithError( 'apierror-compare-relative-to-nothing' );
  59. }
  60. if ( $params['torelative'] !== 'cur' && $fromRelRev instanceof RevisionArchiveRecord ) {
  61. // RevisionStore's getPreviousRevision/getNextRevision blow up
  62. // when passed an RevisionArchiveRecord for a deleted page
  63. $this->dieWithError( [ 'apierror-compare-relative-to-deleted', $params['torelative'] ] );
  64. }
  65. switch ( $params['torelative'] ) {
  66. case 'prev':
  67. // Swap 'from' and 'to'
  68. list( $toRev, $toRelRev, $toValsRev ) = [ $fromRev, $fromRelRev, $fromValsRev ];
  69. $fromRev = $this->revisionStore->getPreviousRevision( $toRelRev );
  70. $fromRelRev = $fromRev;
  71. $fromValsRev = $fromRev;
  72. if ( !$fromRev ) {
  73. $title = Title::newFromLinkTarget( $toRelRev->getPageAsLinkTarget() );
  74. $this->addWarning( [
  75. 'apiwarn-compare-no-prev',
  76. wfEscapeWikiText( $title->getPrefixedText() ),
  77. $toRelRev->getId()
  78. ] );
  79. // (T203433) Create an empty dummy revision as the "previous".
  80. // The main slot has to exist, the rest will be handled by DifferenceEngine.
  81. $fromRev = $this->revisionStore->newMutableRevisionFromArray( [
  82. 'title' => $title ?: Title::makeTitle( NS_SPECIAL, 'Badtitle/' . __METHOD__ )
  83. ] );
  84. $fromRev->setContent(
  85. SlotRecord::MAIN,
  86. $toRelRev->getContent( SlotRecord::MAIN, RevisionRecord::RAW )
  87. ->getContentHandler()
  88. ->makeEmptyContent()
  89. );
  90. }
  91. break;
  92. case 'next':
  93. $toRev = $this->revisionStore->getNextRevision( $fromRelRev );
  94. $toRelRev = $toRev;
  95. $toValsRev = $toRev;
  96. if ( !$toRev ) {
  97. $title = Title::newFromLinkTarget( $fromRelRev->getPageAsLinkTarget() );
  98. $this->addWarning( [
  99. 'apiwarn-compare-no-next',
  100. wfEscapeWikiText( $title->getPrefixedText() ),
  101. $fromRelRev->getId()
  102. ] );
  103. // (T203433) The web UI treats "next" as "cur" in this case.
  104. // Avoid repeating metadata by making a MutableRevisionRecord with no changes.
  105. $toRev = MutableRevisionRecord::newFromParentRevision( $fromRelRev );
  106. }
  107. break;
  108. case 'cur':
  109. $title = $fromRelRev->getPageAsLinkTarget();
  110. $toRev = $this->revisionStore->getRevisionByTitle( $title );
  111. if ( !$toRev ) {
  112. $title = Title::newFromLinkTarget( $title );
  113. $this->dieWithError(
  114. [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ], 'nosuchrevid'
  115. );
  116. }
  117. $toRelRev = $toRev;
  118. $toValsRev = $toRev;
  119. break;
  120. }
  121. } else {
  122. list( $toRev, $toRelRev, $toValsRev ) = $this->getDiffRevision( 'to', $params );
  123. }
  124. // Handle missing from or to revisions (should never happen)
  125. // @codeCoverageIgnoreStart
  126. if ( !$fromRev || !$toRev ) {
  127. $this->dieWithError( 'apierror-baddiff' );
  128. }
  129. // @codeCoverageIgnoreEnd
  130. // Handle revdel
  131. if ( !$fromRev->audienceCan(
  132. RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_THIS_USER, $this->getUser()
  133. ) ) {
  134. $this->dieWithError( [ 'apierror-missingcontent-revid', $fromRev->getId() ], 'missingcontent' );
  135. }
  136. if ( !$toRev->audienceCan(
  137. RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_THIS_USER, $this->getUser()
  138. ) ) {
  139. $this->dieWithError( [ 'apierror-missingcontent-revid', $toRev->getId() ], 'missingcontent' );
  140. }
  141. // Get the diff
  142. $context = new DerivativeContext( $this->getContext() );
  143. if ( $fromRelRev && $fromRelRev->getPageAsLinkTarget() ) {
  144. $context->setTitle( Title::newFromLinkTarget( $fromRelRev->getPageAsLinkTarget() ) );
  145. } elseif ( $toRelRev && $toRelRev->getPageAsLinkTarget() ) {
  146. $context->setTitle( Title::newFromLinkTarget( $toRelRev->getPageAsLinkTarget() ) );
  147. } else {
  148. $guessedTitle = $this->guessTitle();
  149. if ( $guessedTitle ) {
  150. $context->setTitle( $guessedTitle );
  151. }
  152. }
  153. $de = new DifferenceEngine( $context );
  154. $de->setRevisions( $fromRev, $toRev );
  155. if ( $params['slots'] === null ) {
  156. $difftext = $de->getDiffBody();
  157. if ( $difftext === false ) {
  158. $this->dieWithError( 'apierror-baddiff' );
  159. }
  160. } else {
  161. $difftext = [];
  162. foreach ( $params['slots'] as $role ) {
  163. $difftext[$role] = $de->getDiffBodyForRole( $role );
  164. }
  165. }
  166. // Fill in the response
  167. $vals = [];
  168. $this->setVals( $vals, 'from', $fromValsRev );
  169. $this->setVals( $vals, 'to', $toValsRev );
  170. if ( isset( $this->props['rel'] ) ) {
  171. if ( !$fromRev instanceof MutableRevisionRecord ) {
  172. $rev = $this->revisionStore->getPreviousRevision( $fromRev );
  173. if ( $rev ) {
  174. $vals['prev'] = $rev->getId();
  175. }
  176. }
  177. if ( !$toRev instanceof MutableRevisionRecord ) {
  178. $rev = $this->revisionStore->getNextRevision( $toRev );
  179. if ( $rev ) {
  180. $vals['next'] = $rev->getId();
  181. }
  182. }
  183. }
  184. if ( isset( $this->props['diffsize'] ) ) {
  185. $vals['diffsize'] = 0;
  186. foreach ( (array)$difftext as $text ) {
  187. $vals['diffsize'] += strlen( $text );
  188. }
  189. }
  190. if ( isset( $this->props['diff'] ) ) {
  191. if ( is_array( $difftext ) ) {
  192. ApiResult::setArrayType( $difftext, 'kvp', 'diff' );
  193. $vals['bodies'] = $difftext;
  194. } else {
  195. ApiResult::setContentValue( $vals, 'body', $difftext );
  196. }
  197. }
  198. // Diffs can be really big and there's little point in having
  199. // ApiResult truncate it to an empty response since the diff is the
  200. // whole reason this module exists. So pass NO_SIZE_CHECK here.
  201. $this->getResult()->addValue( null, $this->getModuleName(), $vals, ApiResult::NO_SIZE_CHECK );
  202. }
  203. /**
  204. * Load a revision by ID
  205. *
  206. * Falls back to checking the archive table if appropriate.
  207. *
  208. * @param int $id
  209. * @return RevisionRecord|null
  210. */
  211. private function getRevisionById( $id ) {
  212. $rev = $this->revisionStore->getRevisionById( $id );
  213. if ( !$rev && $this->getPermissionManager()
  214. ->userHasAnyRight( $this->getUser(), 'deletedtext', 'undelete' )
  215. ) {
  216. // Try the 'archive' table
  217. $arQuery = $this->revisionStore->getArchiveQueryInfo();
  218. $row = $this->getDB()->selectRow(
  219. $arQuery['tables'],
  220. array_merge(
  221. $arQuery['fields'],
  222. [ 'ar_namespace', 'ar_title' ]
  223. ),
  224. [ 'ar_rev_id' => $id ],
  225. __METHOD__,
  226. [],
  227. $arQuery['joins']
  228. );
  229. if ( $row ) {
  230. $rev = $this->revisionStore->newRevisionFromArchiveRow( $row );
  231. // @phan-suppress-next-line PhanUndeclaredProperty
  232. $rev->isArchive = true;
  233. }
  234. }
  235. return $rev;
  236. }
  237. /**
  238. * Guess an appropriate default Title for this request
  239. *
  240. * @return Title|null
  241. */
  242. private function guessTitle() {
  243. if ( $this->guessedTitle !== false ) {
  244. return $this->guessedTitle;
  245. }
  246. $this->guessedTitle = null;
  247. $params = $this->extractRequestParams();
  248. foreach ( [ 'from', 'to' ] as $prefix ) {
  249. if ( $params["{$prefix}rev"] !== null ) {
  250. $rev = $this->getRevisionById( $params["{$prefix}rev"] );
  251. if ( $rev ) {
  252. $this->guessedTitle = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
  253. break;
  254. }
  255. }
  256. if ( $params["{$prefix}title"] !== null ) {
  257. $title = Title::newFromText( $params["{$prefix}title"] );
  258. if ( $title && !$title->isExternal() ) {
  259. $this->guessedTitle = $title;
  260. break;
  261. }
  262. }
  263. if ( $params["{$prefix}id"] !== null ) {
  264. $title = Title::newFromID( $params["{$prefix}id"] );
  265. if ( $title ) {
  266. $this->guessedTitle = $title;
  267. break;
  268. }
  269. }
  270. }
  271. return $this->guessedTitle;
  272. }
  273. /**
  274. * Guess an appropriate default content model for this request
  275. * @param string $role Slot for which to guess the model
  276. * @return string|null Guessed content model
  277. */
  278. private function guessModel( $role ) {
  279. $params = $this->extractRequestParams();
  280. $title = null;
  281. foreach ( [ 'from', 'to' ] as $prefix ) {
  282. if ( $params["{$prefix}rev"] !== null ) {
  283. $rev = $this->getRevisionById( $params["{$prefix}rev"] );
  284. if ( $rev && $rev->hasSlot( $role ) ) {
  285. return $rev->getSlot( $role, RevisionRecord::RAW )->getModel();
  286. }
  287. }
  288. }
  289. $guessedTitle = $this->guessTitle();
  290. if ( $guessedTitle ) {
  291. return $this->slotRoleRegistry->getRoleHandler( $role )->getDefaultModel( $guessedTitle );
  292. }
  293. if ( isset( $params["fromcontentmodel-$role"] ) ) {
  294. return $params["fromcontentmodel-$role"];
  295. }
  296. if ( isset( $params["tocontentmodel-$role"] ) ) {
  297. return $params["tocontentmodel-$role"];
  298. }
  299. if ( $role === SlotRecord::MAIN ) {
  300. if ( isset( $params['fromcontentmodel'] ) ) {
  301. return $params['fromcontentmodel'];
  302. }
  303. if ( isset( $params['tocontentmodel'] ) ) {
  304. return $params['tocontentmodel'];
  305. }
  306. }
  307. return null;
  308. }
  309. /**
  310. * Get the RevisionRecord for one side of the diff
  311. *
  312. * This uses the appropriate set of parameters to determine what content
  313. * should be diffed.
  314. *
  315. * Returns three values:
  316. * - A RevisionRecord holding the content
  317. * - The revision specified, if any, even if content was supplied
  318. * - The revision to pass to setVals(), if any
  319. *
  320. * @param string $prefix 'from' or 'to'
  321. * @param array $params
  322. * @return array [ RevisionRecord|null, RevisionRecord|null, RevisionRecord|null ]
  323. */
  324. private function getDiffRevision( $prefix, array $params ) {
  325. // Back compat params
  326. $this->requireMaxOneParameter( $params, "{$prefix}text", "{$prefix}slots" );
  327. $this->requireMaxOneParameter( $params, "{$prefix}section", "{$prefix}slots" );
  328. if ( $params["{$prefix}text"] !== null ) {
  329. $params["{$prefix}slots"] = [ SlotRecord::MAIN ];
  330. $params["{$prefix}text-main"] = $params["{$prefix}text"];
  331. $params["{$prefix}section-main"] = null;
  332. $params["{$prefix}contentmodel-main"] = $params["{$prefix}contentmodel"];
  333. $params["{$prefix}contentformat-main"] = $params["{$prefix}contentformat"];
  334. }
  335. $title = null;
  336. $rev = null;
  337. $suppliedContent = $params["{$prefix}slots"] !== null;
  338. // Get the revision and title, if applicable
  339. $revId = null;
  340. if ( $params["{$prefix}rev"] !== null ) {
  341. $revId = $params["{$prefix}rev"];
  342. } elseif ( $params["{$prefix}title"] !== null || $params["{$prefix}id"] !== null ) {
  343. if ( $params["{$prefix}title"] !== null ) {
  344. $title = Title::newFromText( $params["{$prefix}title"] );
  345. if ( !$title || $title->isExternal() ) {
  346. $this->dieWithError(
  347. [ 'apierror-invalidtitle', wfEscapeWikiText( $params["{$prefix}title"] ) ]
  348. );
  349. }
  350. } else {
  351. $title = Title::newFromID( $params["{$prefix}id"] );
  352. if ( !$title ) {
  353. $this->dieWithError( [ 'apierror-nosuchpageid', $params["{$prefix}id"] ] );
  354. }
  355. }
  356. $revId = $title->getLatestRevID();
  357. if ( !$revId ) {
  358. $revId = null;
  359. // Only die here if we're not using supplied text
  360. if ( !$suppliedContent ) {
  361. if ( $title->exists() ) {
  362. $this->dieWithError(
  363. [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ], 'nosuchrevid'
  364. );
  365. } else {
  366. $this->dieWithError(
  367. [ 'apierror-missingtitle-byname', wfEscapeWikiText( $title->getPrefixedText() ) ],
  368. 'missingtitle'
  369. );
  370. }
  371. }
  372. }
  373. }
  374. if ( $revId !== null ) {
  375. $rev = $this->getRevisionById( $revId );
  376. if ( !$rev ) {
  377. $this->dieWithError( [ 'apierror-nosuchrevid', $revId ] );
  378. }
  379. $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
  380. // If we don't have supplied content, return here. Otherwise,
  381. // continue on below with the supplied content.
  382. if ( !$suppliedContent ) {
  383. $newRev = $rev;
  384. // Deprecated 'fromsection'/'tosection'
  385. if ( isset( $params["{$prefix}section"] ) ) {
  386. $section = $params["{$prefix}section"];
  387. $newRev = MutableRevisionRecord::newFromParentRevision( $rev );
  388. $content = $rev->getContent( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER,
  389. $this->getUser() );
  390. if ( !$content ) {
  391. $this->dieWithError(
  392. [ 'apierror-missingcontent-revid-role', $rev->getId(), SlotRecord::MAIN ], 'missingcontent'
  393. );
  394. }
  395. $content = $content ? $content->getSection( $section ) : null;
  396. if ( !$content ) {
  397. $this->dieWithError(
  398. [ "apierror-compare-nosuch{$prefix}section", wfEscapeWikiText( $section ) ],
  399. "nosuch{$prefix}section"
  400. );
  401. }
  402. $newRev->setContent( SlotRecord::MAIN, $content );
  403. }
  404. return [ $newRev, $rev, $rev ];
  405. }
  406. }
  407. // Override $content based on supplied text
  408. if ( !$title ) {
  409. $title = $this->guessTitle();
  410. }
  411. if ( $rev ) {
  412. $newRev = MutableRevisionRecord::newFromParentRevision( $rev );
  413. } else {
  414. $newRev = $this->revisionStore->newMutableRevisionFromArray( [
  415. 'title' => $title ?: Title::makeTitle( NS_SPECIAL, 'Badtitle/' . __METHOD__ )
  416. ] );
  417. }
  418. foreach ( $params["{$prefix}slots"] as $role ) {
  419. $text = $params["{$prefix}text-{$role}"];
  420. if ( $text === null ) {
  421. // The SlotRecord::MAIN role can't be deleted
  422. if ( $role === SlotRecord::MAIN ) {
  423. $this->dieWithError( [ 'apierror-compare-maintextrequired', $prefix ] );
  424. }
  425. // These parameters make no sense without text. Reject them to avoid
  426. // confusion.
  427. foreach ( [ 'section', 'contentmodel', 'contentformat' ] as $param ) {
  428. if ( isset( $params["{$prefix}{$param}-{$role}"] ) ) {
  429. $this->dieWithError( [
  430. 'apierror-compare-notext',
  431. wfEscapeWikiText( "{$prefix}{$param}-{$role}" ),
  432. wfEscapeWikiText( "{$prefix}text-{$role}" ),
  433. ] );
  434. }
  435. }
  436. $newRev->removeSlot( $role );
  437. continue;
  438. }
  439. $model = $params["{$prefix}contentmodel-{$role}"];
  440. $format = $params["{$prefix}contentformat-{$role}"];
  441. if ( !$model && $rev && $rev->hasSlot( $role ) ) {
  442. $model = $rev->getSlot( $role, RevisionRecord::RAW )->getModel();
  443. }
  444. if ( !$model && $title && $role === SlotRecord::MAIN ) {
  445. // @todo: Use SlotRoleRegistry and do this for all slots
  446. $model = $title->getContentModel();
  447. }
  448. if ( !$model ) {
  449. $model = $this->guessModel( $role );
  450. }
  451. if ( !$model ) {
  452. $model = CONTENT_MODEL_WIKITEXT;
  453. $this->addWarning( [ 'apiwarn-compare-nocontentmodel', $model ] );
  454. }
  455. try {
  456. $content = ContentHandler::makeContent( $text, $title, $model, $format );
  457. } catch ( MWContentSerializationException $ex ) {
  458. $this->dieWithException( $ex, [
  459. 'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' )
  460. ] );
  461. }
  462. if ( $params["{$prefix}pst"] ) {
  463. if ( !$title ) {
  464. $this->dieWithError( 'apierror-compare-no-title' );
  465. }
  466. $popts = ParserOptions::newFromContext( $this->getContext() );
  467. $content = $content->preSaveTransform( $title, $this->getUser(), $popts );
  468. }
  469. $section = $params["{$prefix}section-{$role}"];
  470. if ( $section !== null && $section !== '' ) {
  471. if ( !$rev ) {
  472. $this->dieWithError( "apierror-compare-no{$prefix}revision" );
  473. }
  474. $oldContent = $rev->getContent( $role, RevisionRecord::FOR_THIS_USER, $this->getUser() );
  475. if ( !$oldContent ) {
  476. $this->dieWithError(
  477. [ 'apierror-missingcontent-revid-role', $rev->getId(), wfEscapeWikiText( $role ) ],
  478. 'missingcontent'
  479. );
  480. }
  481. if ( !$oldContent->getContentHandler()->supportsSections() ) {
  482. $this->dieWithError( [ 'apierror-sectionsnotsupported', $content->getModel() ] );
  483. }
  484. try {
  485. $content = $oldContent->replaceSection( $section, $content, '' );
  486. } catch ( Exception $ex ) {
  487. // Probably a content model mismatch.
  488. $content = null;
  489. }
  490. if ( !$content ) {
  491. $this->dieWithError( [ 'apierror-sectionreplacefailed' ] );
  492. }
  493. }
  494. // Deprecated 'fromsection'/'tosection'
  495. if ( $role === SlotRecord::MAIN && isset( $params["{$prefix}section"] ) ) {
  496. $section = $params["{$prefix}section"];
  497. $content = $content->getSection( $section );
  498. if ( !$content ) {
  499. $this->dieWithError(
  500. [ "apierror-compare-nosuch{$prefix}section", wfEscapeWikiText( $section ) ],
  501. "nosuch{$prefix}section"
  502. );
  503. }
  504. }
  505. $newRev->setContent( $role, $content );
  506. }
  507. return [ $newRev, $rev, null ];
  508. }
  509. /**
  510. * Set value fields from a RevisionRecord object
  511. *
  512. * @param array &$vals Result array to set data into
  513. * @param string $prefix 'from' or 'to'
  514. * @param RevisionRecord|null $rev
  515. */
  516. private function setVals( &$vals, $prefix, $rev ) {
  517. if ( $rev ) {
  518. $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
  519. if ( isset( $this->props['ids'] ) ) {
  520. $vals["{$prefix}id"] = $title->getArticleID();
  521. $vals["{$prefix}revid"] = $rev->getId();
  522. }
  523. if ( isset( $this->props['title'] ) ) {
  524. ApiQueryBase::addTitleInfo( $vals, $title, $prefix );
  525. }
  526. if ( isset( $this->props['size'] ) ) {
  527. $vals["{$prefix}size"] = $rev->getSize();
  528. }
  529. $anyHidden = false;
  530. if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
  531. $vals["{$prefix}texthidden"] = true;
  532. $anyHidden = true;
  533. }
  534. if ( $rev->isDeleted( RevisionRecord::DELETED_USER ) ) {
  535. $vals["{$prefix}userhidden"] = true;
  536. $anyHidden = true;
  537. }
  538. if ( isset( $this->props['user'] ) ) {
  539. $user = $rev->getUser( RevisionRecord::FOR_THIS_USER, $this->getUser() );
  540. if ( $user ) {
  541. $vals["{$prefix}user"] = $user->getName();
  542. $vals["{$prefix}userid"] = $user->getId();
  543. }
  544. }
  545. if ( $rev->isDeleted( RevisionRecord::DELETED_COMMENT ) ) {
  546. $vals["{$prefix}commenthidden"] = true;
  547. $anyHidden = true;
  548. }
  549. if ( isset( $this->props['comment'] ) || isset( $this->props['parsedcomment'] ) ) {
  550. $comment = $rev->getComment( RevisionRecord::FOR_THIS_USER, $this->getUser() );
  551. if ( $comment !== null ) {
  552. if ( isset( $this->props['comment'] ) ) {
  553. $vals["{$prefix}comment"] = $comment->text;
  554. }
  555. $vals["{$prefix}parsedcomment"] = Linker::formatComment(
  556. $comment->text, $title
  557. );
  558. }
  559. }
  560. if ( $anyHidden ) {
  561. $this->getMain()->setCacheMode( 'private' );
  562. if ( $rev->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
  563. $vals["{$prefix}suppressed"] = true;
  564. }
  565. }
  566. // @phan-suppress-next-line PhanUndeclaredProperty
  567. if ( !empty( $rev->isArchive ) ) {
  568. $this->getMain()->setCacheMode( 'private' );
  569. $vals["{$prefix}archive"] = true;
  570. }
  571. }
  572. }
  573. public function getAllowedParams() {
  574. $slotRoles = $this->slotRoleRegistry->getKnownRoles();
  575. sort( $slotRoles, SORT_STRING );
  576. // Parameters for the 'from' and 'to' content
  577. $fromToParams = [
  578. 'title' => null,
  579. 'id' => [
  580. ApiBase::PARAM_TYPE => 'integer'
  581. ],
  582. 'rev' => [
  583. ApiBase::PARAM_TYPE => 'integer'
  584. ],
  585. 'slots' => [
  586. ApiBase::PARAM_TYPE => $slotRoles,
  587. ApiBase::PARAM_ISMULTI => true,
  588. ],
  589. 'text-{slot}' => [
  590. ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
  591. ApiBase::PARAM_TYPE => 'text',
  592. ],
  593. 'section-{slot}' => [
  594. ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
  595. ApiBase::PARAM_TYPE => 'string',
  596. ],
  597. 'contentformat-{slot}' => [
  598. ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
  599. ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
  600. ],
  601. 'contentmodel-{slot}' => [
  602. ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
  603. ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
  604. ],
  605. 'pst' => false,
  606. 'text' => [
  607. ApiBase::PARAM_TYPE => 'text',
  608. ApiBase::PARAM_DEPRECATED => true,
  609. ],
  610. 'contentformat' => [
  611. ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
  612. ApiBase::PARAM_DEPRECATED => true,
  613. ],
  614. 'contentmodel' => [
  615. ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
  616. ApiBase::PARAM_DEPRECATED => true,
  617. ],
  618. 'section' => [
  619. ApiBase::PARAM_DFLT => null,
  620. ApiBase::PARAM_DEPRECATED => true,
  621. ],
  622. ];
  623. $ret = [];
  624. foreach ( $fromToParams as $k => $v ) {
  625. if ( isset( $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] ) ) {
  626. $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] = 'fromslots';
  627. }
  628. $ret["from$k"] = $v;
  629. }
  630. foreach ( $fromToParams as $k => $v ) {
  631. if ( isset( $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] ) ) {
  632. $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] = 'toslots';
  633. }
  634. $ret["to$k"] = $v;
  635. }
  636. $ret = wfArrayInsertAfter(
  637. $ret,
  638. [ 'torelative' => [ ApiBase::PARAM_TYPE => [ 'prev', 'next', 'cur' ], ] ],
  639. 'torev'
  640. );
  641. $ret['prop'] = [
  642. ApiBase::PARAM_DFLT => 'diff|ids|title',
  643. ApiBase::PARAM_TYPE => [
  644. 'diff',
  645. 'diffsize',
  646. 'rel',
  647. 'ids',
  648. 'title',
  649. 'user',
  650. 'comment',
  651. 'parsedcomment',
  652. 'size',
  653. ],
  654. ApiBase::PARAM_ISMULTI => true,
  655. ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
  656. ];
  657. $ret['slots'] = [
  658. ApiBase::PARAM_TYPE => $slotRoles,
  659. ApiBase::PARAM_ISMULTI => true,
  660. ApiBase::PARAM_ALL => true,
  661. ];
  662. return $ret;
  663. }
  664. protected function getExamplesMessages() {
  665. return [
  666. 'action=compare&fromrev=1&torev=2'
  667. => 'apihelp-compare-example-1',
  668. ];
  669. }
  670. }