ApiComparePages.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  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. class ApiComparePages extends ApiBase {
  22. private $guessed = false, $guessedTitle, $guessedModel, $props;
  23. public function execute() {
  24. $params = $this->extractRequestParams();
  25. // Parameter validation
  26. $this->requireAtLeastOneParameter( $params, 'fromtitle', 'fromid', 'fromrev', 'fromtext' );
  27. $this->requireAtLeastOneParameter( $params, 'totitle', 'toid', 'torev', 'totext', 'torelative' );
  28. $this->props = array_flip( $params['prop'] );
  29. // Cache responses publicly by default. This may be overridden later.
  30. $this->getMain()->setCacheMode( 'public' );
  31. // Get the 'from' Revision and Content
  32. list( $fromRev, $fromContent, $relRev ) = $this->getDiffContent( 'from', $params );
  33. // Get the 'to' Revision and Content
  34. if ( $params['torelative'] !== null ) {
  35. if ( !$relRev ) {
  36. $this->dieWithError( 'apierror-compare-relative-to-nothing' );
  37. }
  38. switch ( $params['torelative'] ) {
  39. case 'prev':
  40. // Swap 'from' and 'to'
  41. $toRev = $fromRev;
  42. $toContent = $fromContent;
  43. $fromRev = $relRev->getPrevious();
  44. $fromContent = $fromRev
  45. ? $fromRev->getContent( Revision::FOR_THIS_USER, $this->getUser() )
  46. : $toContent->getContentHandler()->makeEmptyContent();
  47. if ( !$fromContent ) {
  48. $this->dieWithError(
  49. [ 'apierror-missingcontent-revid', $fromRev->getId() ], 'missingcontent'
  50. );
  51. }
  52. break;
  53. case 'next':
  54. $toRev = $relRev->getNext();
  55. $toContent = $toRev
  56. ? $toRev->getContent( Revision::FOR_THIS_USER, $this->getUser() )
  57. : $fromContent;
  58. if ( !$toContent ) {
  59. $this->dieWithError( [ 'apierror-missingcontent-revid', $toRev->getId() ], 'missingcontent' );
  60. }
  61. break;
  62. case 'cur':
  63. $title = $relRev->getTitle();
  64. $id = $title->getLatestRevID();
  65. $toRev = $id ? Revision::newFromId( $id ) : null;
  66. if ( !$toRev ) {
  67. $this->dieWithError(
  68. [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ], 'nosuchrevid'
  69. );
  70. }
  71. $toContent = $toRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
  72. if ( !$toContent ) {
  73. $this->dieWithError( [ 'apierror-missingcontent-revid', $toRev->getId() ], 'missingcontent' );
  74. }
  75. break;
  76. }
  77. $relRev2 = null;
  78. } else {
  79. list( $toRev, $toContent, $relRev2 ) = $this->getDiffContent( 'to', $params );
  80. }
  81. // Should never happen, but just in case...
  82. if ( !$fromContent || !$toContent ) {
  83. $this->dieWithError( 'apierror-baddiff' );
  84. }
  85. // Get the diff
  86. $context = new DerivativeContext( $this->getContext() );
  87. if ( $relRev && $relRev->getTitle() ) {
  88. $context->setTitle( $relRev->getTitle() );
  89. } elseif ( $relRev2 && $relRev2->getTitle() ) {
  90. $context->setTitle( $relRev2->getTitle() );
  91. } else {
  92. $this->guessTitleAndModel();
  93. if ( $this->guessedTitle ) {
  94. $context->setTitle( $this->guessedTitle );
  95. }
  96. }
  97. $de = $fromContent->getContentHandler()->createDifferenceEngine(
  98. $context,
  99. $fromRev ? $fromRev->getId() : 0,
  100. $toRev ? $toRev->getId() : 0,
  101. /* $rcid = */ null,
  102. /* $refreshCache = */ false,
  103. /* $unhide = */ true
  104. );
  105. $de->setContent( $fromContent, $toContent );
  106. $difftext = $de->getDiffBody();
  107. if ( $difftext === false ) {
  108. $this->dieWithError( 'apierror-baddiff' );
  109. }
  110. // Fill in the response
  111. $vals = [];
  112. $this->setVals( $vals, 'from', $fromRev );
  113. $this->setVals( $vals, 'to', $toRev );
  114. if ( isset( $this->props['rel'] ) ) {
  115. if ( $fromRev ) {
  116. $rev = $fromRev->getPrevious();
  117. if ( $rev ) {
  118. $vals['prev'] = $rev->getId();
  119. }
  120. }
  121. if ( $toRev ) {
  122. $rev = $toRev->getNext();
  123. if ( $rev ) {
  124. $vals['next'] = $rev->getId();
  125. }
  126. }
  127. }
  128. if ( isset( $this->props['diffsize'] ) ) {
  129. $vals['diffsize'] = strlen( $difftext );
  130. }
  131. if ( isset( $this->props['diff'] ) ) {
  132. ApiResult::setContentValue( $vals, 'body', $difftext );
  133. }
  134. $this->getResult()->addValue( null, $this->getModuleName(), $vals );
  135. }
  136. /**
  137. * Guess an appropriate default Title and content model for this request
  138. *
  139. * Fills in $this->guessedTitle based on the first of 'fromrev',
  140. * 'fromtitle', 'fromid', 'torev', 'totitle', and 'toid' that's present and
  141. * valid.
  142. *
  143. * Fills in $this->guessedModel based on the Revision or Title used to
  144. * determine $this->guessedTitle, or the 'fromcontentmodel' or
  145. * 'tocontentmodel' parameters if no title was guessed.
  146. */
  147. private function guessTitleAndModel() {
  148. if ( $this->guessed ) {
  149. return;
  150. }
  151. $this->guessed = true;
  152. $params = $this->extractRequestParams();
  153. foreach ( [ 'from', 'to' ] as $prefix ) {
  154. if ( $params["{$prefix}rev"] !== null ) {
  155. $revId = $params["{$prefix}rev"];
  156. $rev = Revision::newFromId( $revId );
  157. if ( !$rev ) {
  158. // Titles of deleted revisions aren't secret, per T51088
  159. $row = $this->getDB()->selectRow(
  160. 'archive',
  161. array_merge(
  162. Revision::selectArchiveFields(),
  163. [ 'ar_namespace', 'ar_title' ]
  164. ),
  165. [ 'ar_rev_id' => $revId ],
  166. __METHOD__
  167. );
  168. if ( $row ) {
  169. $rev = Revision::newFromArchiveRow( $row );
  170. }
  171. }
  172. if ( $rev ) {
  173. $this->guessedTitle = $rev->getTitle();
  174. $this->guessedModel = $rev->getContentModel();
  175. break;
  176. }
  177. }
  178. if ( $params["{$prefix}title"] !== null ) {
  179. $title = Title::newFromText( $params["{$prefix}title"] );
  180. if ( $title && !$title->isExternal() ) {
  181. $this->guessedTitle = $title;
  182. break;
  183. }
  184. }
  185. if ( $params["{$prefix}id"] !== null ) {
  186. $title = Title::newFromID( $params["{$prefix}id"] );
  187. if ( $title ) {
  188. $this->guessedTitle = $title;
  189. break;
  190. }
  191. }
  192. }
  193. if ( !$this->guessedModel ) {
  194. if ( $this->guessedTitle ) {
  195. $this->guessedModel = $this->guessedTitle->getContentModel();
  196. } elseif ( $params['fromcontentmodel'] !== null ) {
  197. $this->guessedModel = $params['fromcontentmodel'];
  198. } elseif ( $params['tocontentmodel'] !== null ) {
  199. $this->guessedModel = $params['tocontentmodel'];
  200. }
  201. }
  202. }
  203. /**
  204. * Get the Revision and Content for one side of the diff
  205. *
  206. * This uses the appropriate set of 'rev', 'id', 'title', 'text', 'pst',
  207. * 'contentmodel', and 'contentformat' parameters to determine what content
  208. * should be diffed.
  209. *
  210. * Returns three values:
  211. * - The revision used to retrieve the content, if any
  212. * - The content to be diffed
  213. * - The revision specified, if any, even if not used to retrieve the
  214. * Content
  215. *
  216. * @param string $prefix 'from' or 'to'
  217. * @param array $params
  218. * @return array [ Revision|null, Content, Revision|null ]
  219. */
  220. private function getDiffContent( $prefix, array $params ) {
  221. $title = null;
  222. $rev = null;
  223. $suppliedContent = $params["{$prefix}text"] !== null;
  224. // Get the revision and title, if applicable
  225. $revId = null;
  226. if ( $params["{$prefix}rev"] !== null ) {
  227. $revId = $params["{$prefix}rev"];
  228. } elseif ( $params["{$prefix}title"] !== null || $params["{$prefix}id"] !== null ) {
  229. if ( $params["{$prefix}title"] !== null ) {
  230. $title = Title::newFromText( $params["{$prefix}title"] );
  231. if ( !$title || $title->isExternal() ) {
  232. $this->dieWithError(
  233. [ 'apierror-invalidtitle', wfEscapeWikiText( $params["{$prefix}title"] ) ]
  234. );
  235. }
  236. } else {
  237. $title = Title::newFromID( $params["{$prefix}id"] );
  238. if ( !$title ) {
  239. $this->dieWithError( [ 'apierror-nosuchpageid', $params["{$prefix}id"] ] );
  240. }
  241. }
  242. $revId = $title->getLatestRevID();
  243. if ( !$revId ) {
  244. $revId = null;
  245. // Only die here if we're not using supplied text
  246. if ( !$suppliedContent ) {
  247. if ( $title->exists() ) {
  248. $this->dieWithError(
  249. [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ], 'nosuchrevid'
  250. );
  251. } else {
  252. $this->dieWithError(
  253. [ 'apierror-missingtitle-byname', wfEscapeWikiText( $title->getPrefixedText() ) ],
  254. 'missingtitle'
  255. );
  256. }
  257. }
  258. }
  259. }
  260. if ( $revId !== null ) {
  261. $rev = Revision::newFromId( $revId );
  262. if ( !$rev && $this->getUser()->isAllowedAny( 'deletedtext', 'undelete' ) ) {
  263. // Try the 'archive' table
  264. $row = $this->getDB()->selectRow(
  265. 'archive',
  266. array_merge(
  267. Revision::selectArchiveFields(),
  268. [ 'ar_namespace', 'ar_title' ]
  269. ),
  270. [ 'ar_rev_id' => $revId ],
  271. __METHOD__
  272. );
  273. if ( $row ) {
  274. $rev = Revision::newFromArchiveRow( $row );
  275. $rev->isArchive = true;
  276. }
  277. }
  278. if ( !$rev ) {
  279. $this->dieWithError( [ 'apierror-nosuchrevid', $revId ] );
  280. }
  281. $title = $rev->getTitle();
  282. // If we don't have supplied content, return here. Otherwise,
  283. // continue on below with the supplied content.
  284. if ( !$suppliedContent ) {
  285. $content = $rev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
  286. if ( !$content ) {
  287. $this->dieWithError( [ 'apierror-missingcontent-revid', $revId ], 'missingcontent' );
  288. }
  289. return [ $rev, $content, $rev ];
  290. }
  291. }
  292. // Override $content based on supplied text
  293. $model = $params["{$prefix}contentmodel"];
  294. $format = $params["{$prefix}contentformat"];
  295. if ( !$model && $rev ) {
  296. $model = $rev->getContentModel();
  297. }
  298. if ( !$model && $title ) {
  299. $model = $title->getContentModel();
  300. }
  301. if ( !$model ) {
  302. $this->guessTitleAndModel();
  303. $model = $this->guessedModel;
  304. }
  305. if ( !$model ) {
  306. $model = CONTENT_MODEL_WIKITEXT;
  307. $this->addWarning( [ 'apiwarn-compare-nocontentmodel', $model ] );
  308. }
  309. if ( !$title ) {
  310. $this->guessTitleAndModel();
  311. $title = $this->guessedTitle;
  312. }
  313. try {
  314. $content = ContentHandler::makeContent( $params["{$prefix}text"], $title, $model, $format );
  315. } catch ( MWContentSerializationException $ex ) {
  316. $this->dieWithException( $ex, [
  317. 'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' )
  318. ] );
  319. }
  320. if ( $params["{$prefix}pst"] ) {
  321. if ( !$title ) {
  322. $this->dieWithError( 'apierror-compare-no-title' );
  323. }
  324. $popts = ParserOptions::newFromContext( $this->getContext() );
  325. $content = $content->preSaveTransform( $title, $this->getUser(), $popts );
  326. }
  327. return [ null, $content, $rev ];
  328. }
  329. /**
  330. * Set value fields from a Revision object
  331. * @param array &$vals Result array to set data into
  332. * @param string $prefix 'from' or 'to'
  333. * @param Revision|null $rev
  334. */
  335. private function setVals( &$vals, $prefix, $rev ) {
  336. if ( $rev ) {
  337. $title = $rev->getTitle();
  338. if ( isset( $this->props['ids'] ) ) {
  339. $vals["{$prefix}id"] = $title->getArticleId();
  340. $vals["{$prefix}revid"] = $rev->getId();
  341. }
  342. if ( isset( $this->props['title'] ) ) {
  343. ApiQueryBase::addTitleInfo( $vals, $title, $prefix );
  344. }
  345. if ( isset( $this->props['size'] ) ) {
  346. $vals["{$prefix}size"] = $rev->getSize();
  347. }
  348. $anyHidden = false;
  349. if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
  350. $vals["{$prefix}texthidden"] = true;
  351. $anyHidden = true;
  352. }
  353. if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
  354. $vals["{$prefix}userhidden"] = true;
  355. $anyHidden = true;
  356. }
  357. if ( isset( $this->props['user'] ) &&
  358. $rev->userCan( Revision::DELETED_USER, $this->getUser() )
  359. ) {
  360. $vals["{$prefix}user"] = $rev->getUserText( Revision::RAW );
  361. $vals["{$prefix}userid"] = $rev->getUser( Revision::RAW );
  362. }
  363. if ( $rev->isDeleted( Revision::DELETED_COMMENT ) ) {
  364. $vals["{$prefix}commenthidden"] = true;
  365. $anyHidden = true;
  366. }
  367. if ( $rev->userCan( Revision::DELETED_COMMENT, $this->getUser() ) ) {
  368. if ( isset( $this->props['comment'] ) ) {
  369. $vals["{$prefix}comment"] = $rev->getComment( Revision::RAW );
  370. }
  371. if ( isset( $this->props['parsedcomment'] ) ) {
  372. $vals["{$prefix}parsedcomment"] = Linker::formatComment(
  373. $rev->getComment( Revision::RAW ),
  374. $rev->getTitle()
  375. );
  376. }
  377. }
  378. if ( $anyHidden ) {
  379. $this->getMain()->setCacheMode( 'private' );
  380. if ( $rev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
  381. $vals["{$prefix}suppressed"] = true;
  382. }
  383. }
  384. if ( !empty( $rev->isArchive ) ) {
  385. $this->getMain()->setCacheMode( 'private' );
  386. $vals["{$prefix}archive"] = true;
  387. }
  388. }
  389. }
  390. public function getAllowedParams() {
  391. // Parameters for the 'from' and 'to' content
  392. $fromToParams = [
  393. 'title' => null,
  394. 'id' => [
  395. ApiBase::PARAM_TYPE => 'integer'
  396. ],
  397. 'rev' => [
  398. ApiBase::PARAM_TYPE => 'integer'
  399. ],
  400. 'text' => [
  401. ApiBase::PARAM_TYPE => 'text'
  402. ],
  403. 'pst' => false,
  404. 'contentformat' => [
  405. ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
  406. ],
  407. 'contentmodel' => [
  408. ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
  409. ]
  410. ];
  411. $ret = [];
  412. foreach ( $fromToParams as $k => $v ) {
  413. $ret["from$k"] = $v;
  414. }
  415. foreach ( $fromToParams as $k => $v ) {
  416. $ret["to$k"] = $v;
  417. }
  418. $ret = wfArrayInsertAfter(
  419. $ret,
  420. [ 'torelative' => [ ApiBase::PARAM_TYPE => [ 'prev', 'next', 'cur' ], ] ],
  421. 'torev'
  422. );
  423. $ret['prop'] = [
  424. ApiBase::PARAM_DFLT => 'diff|ids|title',
  425. ApiBase::PARAM_TYPE => [
  426. 'diff',
  427. 'diffsize',
  428. 'rel',
  429. 'ids',
  430. 'title',
  431. 'user',
  432. 'comment',
  433. 'parsedcomment',
  434. 'size',
  435. ],
  436. ApiBase::PARAM_ISMULTI => true,
  437. ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
  438. ];
  439. return $ret;
  440. }
  441. protected function getExamplesMessages() {
  442. return [
  443. 'action=compare&fromrev=1&torev=2'
  444. => 'apihelp-compare-example-1',
  445. ];
  446. }
  447. }