DifferenceEngine.php 62 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978
  1. <?php
  2. /**
  3. * User interface for the difference engine.
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. * @ingroup DifferenceEngine
  22. */
  23. use MediaWiki\MediaWikiServices;
  24. use MediaWiki\Revision\RevisionRecord;
  25. use MediaWiki\Revision\SlotRecord;
  26. use MediaWiki\Storage\NameTableAccessException;
  27. /**
  28. * DifferenceEngine is responsible for rendering the difference between two revisions as HTML.
  29. * This includes interpreting URL parameters, retrieving revision data, checking access permissions,
  30. * selecting and invoking the diff generator class for the individual slots, doing post-processing
  31. * on the generated diff, adding the rest of the HTML (such as headers) and writing the whole thing
  32. * to OutputPage.
  33. *
  34. * DifferenceEngine can be subclassed by extensions, by customizing
  35. * ContentHandler::createDifferenceEngine; the content handler will be selected based on the
  36. * content model of the main slot (of the new revision, when the two are different).
  37. * That might change after PageTypeHandler gets introduced.
  38. *
  39. * In the past, the class was also used for slot-level diff generation, and extensions might still
  40. * subclass it and add such functionality. When that is the case (sepcifically, when a
  41. * ContentHandler returns a standard SlotDiffRenderer but a nonstandard DifferenceEngine)
  42. * DifferenceEngineSlotDiffRenderer will be used to convert the old behavior into the new one.
  43. *
  44. * @ingroup DifferenceEngine
  45. *
  46. * @todo This class is huge and poorly defined. It should be split into a controller responsible
  47. * for interpreting query parameters, retrieving data and checking permissions; and a HTML renderer.
  48. */
  49. class DifferenceEngine extends ContextSource {
  50. use DeprecationHelper;
  51. /**
  52. * Constant to indicate diff cache compatibility.
  53. * Bump this when changing the diff formatting in a way that
  54. * fixes important bugs or such to force cached diff views to
  55. * clear.
  56. */
  57. const DIFF_VERSION = '1.12';
  58. /**
  59. * Revision ID for the old revision. 0 for the revision previous to $mNewid, false
  60. * if the diff does not have an old revision (e.g. 'oldid=<first revision of page>&diff=prev'),
  61. * or the revision does not exist, null if the revision is unsaved.
  62. * @var int|false|null
  63. */
  64. protected $mOldid;
  65. /**
  66. * Revision ID for the new revision. 0 for the last revision of the current page
  67. * (as defined by the request context), false if the revision does not exist, null
  68. * if it is unsaved, or an alias such as 'next'.
  69. * @var int|string|false|null
  70. */
  71. protected $mNewid;
  72. /**
  73. * Old revision (left pane).
  74. * Allowed to be an unsaved revision, unlikely that's ever needed though.
  75. * False when the old revision does not exist; this can happen when using
  76. * diff=prev on the first revision. Null when the revision should exist but
  77. * doesn't (e.g. load failure); loadRevisionData() will return false in that
  78. * case. Also null until lazy-loaded. Ignored completely when isContentOverridden
  79. * is set.
  80. * Since 1.32 public access is deprecated.
  81. * @var Revision|null|false
  82. */
  83. protected $mOldRev;
  84. /**
  85. * New revision (right pane).
  86. * Note that this might be an unsaved revision (e.g. for edit preview).
  87. * Null in case of load failure; diff methods will just return an error message in that case,
  88. * and loadRevisionData() will return false. Also null until lazy-loaded. Ignored completely
  89. * when isContentOverridden is set.
  90. * Since 1.32 public access is deprecated.
  91. * @var Revision|null
  92. */
  93. protected $mNewRev;
  94. /**
  95. * Title of $mOldRev or null if the old revision does not exist or does not belong to a page.
  96. * Since 1.32 public access is deprecated and the property can be null.
  97. * @var Title|null
  98. */
  99. protected $mOldPage;
  100. /**
  101. * Title of $mNewRev or null if the new revision does not exist or does not belong to a page.
  102. * Since 1.32 public access is deprecated and the property can be null.
  103. * @var Title|null
  104. */
  105. protected $mNewPage;
  106. /**
  107. * Change tags of $mOldRev or null if it does not exist / is not saved.
  108. * @var string[]|null
  109. */
  110. private $mOldTags;
  111. /**
  112. * Change tags of $mNewRev or null if it does not exist / is not saved.
  113. * @var string[]|null
  114. */
  115. private $mNewTags;
  116. /**
  117. * @var Content|null
  118. * @deprecated since 1.32, content slots are now handled by the corresponding SlotDiffRenderer.
  119. * This property is set to the content of the main slot, but not actually used for the main diff.
  120. */
  121. private $mOldContent;
  122. /**
  123. * @var Content|null
  124. * @deprecated since 1.32, content slots are now handled by the corresponding SlotDiffRenderer.
  125. * This property is set to the content of the main slot, but not actually used for the main diff.
  126. */
  127. private $mNewContent;
  128. /** @var Language */
  129. protected $mDiffLang;
  130. /** @var bool Have the revisions IDs been loaded */
  131. private $mRevisionsIdsLoaded = false;
  132. /** @var bool Have the revisions been loaded */
  133. protected $mRevisionsLoaded = false;
  134. /** @var int How many text blobs have been loaded, 0, 1 or 2? */
  135. protected $mTextLoaded = 0;
  136. /**
  137. * Was the content overridden via setContent()?
  138. * If the content was overridden, most internal state (e.g. mOldid or mOldRev) should be ignored
  139. * and only mOldContent and mNewContent is reliable.
  140. * (Note that setRevisions() does not set this flag as in that case all properties are
  141. * overriden and remain consistent with each other, so no special handling is needed.)
  142. * @var bool
  143. */
  144. protected $isContentOverridden = false;
  145. /** @var bool Was the diff fetched from cache? */
  146. protected $mCacheHit = false;
  147. /**
  148. * Set this to true to add debug info to the HTML output.
  149. * Warning: this may cause RSS readers to spuriously mark articles as "new"
  150. * (T22601)
  151. */
  152. public $enableDebugComment = false;
  153. /** @var bool If true, line X is not displayed when X is 1, for example
  154. * to increase readability and conserve space with many small diffs.
  155. */
  156. protected $mReducedLineNumbers = false;
  157. /** @var string Link to action=markpatrolled */
  158. protected $mMarkPatrolledLink = null;
  159. /** @var bool Show rev_deleted content if allowed */
  160. protected $unhide = false;
  161. /** @var bool Refresh the diff cache */
  162. protected $mRefreshCache = false;
  163. /** @var SlotDiffRenderer[] DifferenceEngine classes for the slots, keyed by role name. */
  164. protected $slotDiffRenderers = null;
  165. /**
  166. * Temporary hack for B/C while slot diff related methods of DifferenceEngine are being
  167. * deprecated. When true, we are inside a DifferenceEngineSlotDiffRenderer and
  168. * $slotDiffRenderers should not be used.
  169. * @var bool
  170. */
  171. protected $isSlotDiffRenderer = false;
  172. /** #@- */
  173. /**
  174. * @param IContextSource|null $context Context to use, anything else will be ignored
  175. * @param int $old Old ID we want to show and diff with.
  176. * @param string|int $new Either revision ID or 'prev' or 'next'. Default: 0.
  177. * @param int $rcid Deprecated, no longer used!
  178. * @param bool $refreshCache If set, refreshes the diff cache
  179. * @param bool $unhide If set, allow viewing deleted revs
  180. */
  181. public function __construct( $context = null, $old = 0, $new = 0, $rcid = 0,
  182. $refreshCache = false, $unhide = false
  183. ) {
  184. $this->deprecatePublicProperty( 'mOldid', '1.32', __CLASS__ );
  185. $this->deprecatePublicProperty( 'mNewid', '1.32', __CLASS__ );
  186. $this->deprecatePublicProperty( 'mOldRev', '1.32', __CLASS__ );
  187. $this->deprecatePublicProperty( 'mNewRev', '1.32', __CLASS__ );
  188. $this->deprecatePublicProperty( 'mOldPage', '1.32', __CLASS__ );
  189. $this->deprecatePublicProperty( 'mNewPage', '1.32', __CLASS__ );
  190. $this->deprecatePublicProperty( 'mOldContent', '1.32', __CLASS__ );
  191. $this->deprecatePublicProperty( 'mNewContent', '1.32', __CLASS__ );
  192. $this->deprecatePublicProperty( 'mRevisionsLoaded', '1.32', __CLASS__ );
  193. $this->deprecatePublicProperty( 'mTextLoaded', '1.32', __CLASS__ );
  194. $this->deprecatePublicProperty( 'mCacheHit', '1.32', __CLASS__ );
  195. if ( $context instanceof IContextSource ) {
  196. $this->setContext( $context );
  197. }
  198. wfDebug( "DifferenceEngine old '$old' new '$new' rcid '$rcid'\n" );
  199. $this->mOldid = $old;
  200. $this->mNewid = $new;
  201. $this->mRefreshCache = $refreshCache;
  202. $this->unhide = $unhide;
  203. }
  204. /**
  205. * @return SlotDiffRenderer[] Diff renderers for each slot, keyed by role name.
  206. * Includes slots only present in one of the revisions.
  207. */
  208. protected function getSlotDiffRenderers() {
  209. if ( $this->isSlotDiffRenderer ) {
  210. throw new LogicException( __METHOD__ . ' called in slot diff renderer mode' );
  211. }
  212. if ( $this->slotDiffRenderers === null ) {
  213. if ( !$this->loadRevisionData() ) {
  214. return [];
  215. }
  216. $slotContents = $this->getSlotContents();
  217. $this->slotDiffRenderers = array_map( function ( $contents ) {
  218. /** @var Content $content */
  219. $content = $contents['new'] ?: $contents['old'];
  220. return $content->getContentHandler()->getSlotDiffRenderer( $this->getContext() );
  221. }, $slotContents );
  222. }
  223. return $this->slotDiffRenderers;
  224. }
  225. /**
  226. * Mark this DifferenceEngine as a slot renderer (as opposed to a page renderer).
  227. * This is used in legacy mode when the DifferenceEngine is wrapped in a
  228. * DifferenceEngineSlotDiffRenderer.
  229. * @internal For use by DifferenceEngineSlotDiffRenderer only.
  230. */
  231. public function markAsSlotDiffRenderer() {
  232. $this->isSlotDiffRenderer = true;
  233. }
  234. /**
  235. * Get the old and new content objects for all slots.
  236. * This method does not do any permission checks.
  237. * @return array [ role => [ 'old' => SlotRecord|null, 'new' => SlotRecord|null ], ... ]
  238. */
  239. protected function getSlotContents() {
  240. if ( $this->isContentOverridden ) {
  241. return [
  242. SlotRecord::MAIN => [
  243. 'old' => $this->mOldContent,
  244. 'new' => $this->mNewContent,
  245. ]
  246. ];
  247. } elseif ( !$this->loadRevisionData() ) {
  248. return [];
  249. }
  250. $newSlots = $this->mNewRev->getRevisionRecord()->getSlots()->getSlots();
  251. if ( $this->mOldRev ) {
  252. $oldSlots = $this->mOldRev->getRevisionRecord()->getSlots()->getSlots();
  253. } else {
  254. $oldSlots = [];
  255. }
  256. // The order here will determine the visual order of the diff. The current logic is
  257. // slots of the new revision first in natural order, then deleted ones. This is ad hoc
  258. // and should not be relied on - in the future we may want the ordering to depend
  259. // on the page type.
  260. $roles = array_merge( array_keys( $newSlots ), array_keys( $oldSlots ) );
  261. $slots = [];
  262. foreach ( $roles as $role ) {
  263. $slots[$role] = [
  264. 'old' => isset( $oldSlots[$role] ) ? $oldSlots[$role]->getContent() : null,
  265. 'new' => isset( $newSlots[$role] ) ? $newSlots[$role]->getContent() : null,
  266. ];
  267. }
  268. // move main slot to front
  269. if ( isset( $slots[SlotRecord::MAIN] ) ) {
  270. $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
  271. }
  272. return $slots;
  273. }
  274. public function getTitle() {
  275. // T202454 avoid errors when there is no title
  276. return parent::getTitle() ?: Title::makeTitle( NS_SPECIAL, 'BadTitle/DifferenceEngine' );
  277. }
  278. /**
  279. * Set reduced line numbers mode.
  280. * When set, line X is not displayed when X is 1, for example to increase readability and
  281. * conserve space with many small diffs.
  282. * @param bool $value
  283. */
  284. public function setReducedLineNumbers( $value = true ) {
  285. $this->mReducedLineNumbers = $value;
  286. }
  287. /**
  288. * Get the language of the difference engine, defaults to page content language
  289. *
  290. * @return Language
  291. */
  292. public function getDiffLang() {
  293. if ( $this->mDiffLang === null ) {
  294. # Default language in which the diff text is written.
  295. $this->mDiffLang = $this->getTitle()->getPageLanguage();
  296. }
  297. return $this->mDiffLang;
  298. }
  299. /**
  300. * @return bool
  301. */
  302. public function wasCacheHit() {
  303. return $this->mCacheHit;
  304. }
  305. /**
  306. * Get the ID of old revision (left pane) of the diff. 0 for the revision
  307. * previous to getNewid(), false if the old revision does not exist, null
  308. * if it's unsaved.
  309. * To get a real revision ID instead of 0, call loadRevisionData() first.
  310. * @return int|false|null
  311. */
  312. public function getOldid() {
  313. $this->loadRevisionIds();
  314. return $this->mOldid;
  315. }
  316. /**
  317. * Get the ID of new revision (right pane) of the diff. 0 for the current revision,
  318. * false if the new revision does not exist, null if it's unsaved.
  319. * To get a real revision ID instead of 0, call loadRevisionData() first.
  320. * @return int|false|null
  321. */
  322. public function getNewid() {
  323. $this->loadRevisionIds();
  324. return $this->mNewid;
  325. }
  326. /**
  327. * Get the left side of the diff.
  328. * Could be null when the first revision of the page is diffed to 'prev' (or in the case of
  329. * load failure).
  330. * @return RevisionRecord|null
  331. */
  332. public function getOldRevision() {
  333. return $this->mOldRev ? $this->mOldRev->getRevisionRecord() : null;
  334. }
  335. /**
  336. * Get the right side of the diff.
  337. * Should not be null but can still happen in the case of load failure.
  338. * @return RevisionRecord|null
  339. */
  340. public function getNewRevision() {
  341. return $this->mNewRev ? $this->mNewRev->getRevisionRecord() : null;
  342. }
  343. /**
  344. * Look up a special:Undelete link to the given deleted revision id,
  345. * as a workaround for being unable to load deleted diffs in currently.
  346. *
  347. * @param int $id Revision ID
  348. *
  349. * @return string|bool Link HTML or false
  350. */
  351. public function deletedLink( $id ) {
  352. $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
  353. if ( $permissionManager->userHasRight( $this->getUser(), 'deletedhistory' ) ) {
  354. $dbr = wfGetDB( DB_REPLICA );
  355. $arQuery = Revision::getArchiveQueryInfo();
  356. $row = $dbr->selectRow(
  357. $arQuery['tables'],
  358. array_merge( $arQuery['fields'], [ 'ar_namespace', 'ar_title' ] ),
  359. [ 'ar_rev_id' => $id ],
  360. __METHOD__,
  361. [],
  362. $arQuery['joins']
  363. );
  364. if ( $row ) {
  365. $rev = Revision::newFromArchiveRow( $row );
  366. $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
  367. return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( [
  368. 'target' => $title->getPrefixedText(),
  369. 'timestamp' => $rev->getTimestamp()
  370. ] );
  371. }
  372. }
  373. return false;
  374. }
  375. /**
  376. * Build a wikitext link toward a deleted revision, if viewable.
  377. *
  378. * @param int $id Revision ID
  379. *
  380. * @return string Wikitext fragment
  381. */
  382. public function deletedIdMarker( $id ) {
  383. $link = $this->deletedLink( $id );
  384. if ( $link ) {
  385. return "[$link $id]";
  386. } else {
  387. return (string)$id;
  388. }
  389. }
  390. private function showMissingRevision() {
  391. $out = $this->getOutput();
  392. $missing = [];
  393. if ( $this->mOldRev === null ||
  394. ( $this->mOldRev && $this->mOldContent === null )
  395. ) {
  396. $missing[] = $this->deletedIdMarker( $this->mOldid );
  397. }
  398. if ( $this->mNewRev === null ||
  399. ( $this->mNewRev && $this->mNewContent === null )
  400. ) {
  401. $missing[] = $this->deletedIdMarker( $this->mNewid );
  402. }
  403. $out->setPageTitle( $this->msg( 'errorpagetitle' ) );
  404. $msg = $this->msg( 'difference-missing-revision' )
  405. ->params( $this->getLanguage()->listToText( $missing ) )
  406. ->numParams( count( $missing ) )
  407. ->parseAsBlock();
  408. $out->addHTML( $msg );
  409. }
  410. public function showDiffPage( $diffOnly = false ) {
  411. # Allow frames except in certain special cases
  412. $out = $this->getOutput();
  413. $out->allowClickjacking();
  414. $out->setRobotPolicy( 'noindex,nofollow' );
  415. // Allow extensions to add any extra output here
  416. Hooks::run( 'DifferenceEngineShowDiffPage', [ $out ] );
  417. if ( !$this->loadRevisionData() ) {
  418. if ( Hooks::run( 'DifferenceEngineShowDiffPageMaybeShowMissingRevision', [ $this ] ) ) {
  419. $this->showMissingRevision();
  420. }
  421. return;
  422. }
  423. $user = $this->getUser();
  424. $permErrors = [];
  425. if ( $this->mNewPage ) {
  426. $permErrors = $this->mNewPage->getUserPermissionsErrors( 'read', $user );
  427. }
  428. if ( $this->mOldPage ) {
  429. $permErrors = wfMergeErrorArrays( $permErrors,
  430. $this->mOldPage->getUserPermissionsErrors( 'read', $user ) );
  431. }
  432. if ( count( $permErrors ) ) {
  433. throw new PermissionsError( 'read', $permErrors );
  434. }
  435. $rollback = '';
  436. $query = [];
  437. # Carry over 'diffonly' param via navigation links
  438. if ( $diffOnly != $user->getBoolOption( 'diffonly' ) ) {
  439. $query['diffonly'] = $diffOnly;
  440. }
  441. # Cascade unhide param in links for easy deletion browsing
  442. if ( $this->unhide ) {
  443. $query['unhide'] = 1;
  444. }
  445. # Check if one of the revisions is deleted/suppressed
  446. $deleted = $suppressed = false;
  447. $allowed = $this->mNewRev->userCan( RevisionRecord::DELETED_TEXT, $user );
  448. $revisionTools = [];
  449. # mOldRev is false if the difference engine is called with a "vague" query for
  450. # a diff between a version V and its previous version V' AND the version V
  451. # is the first version of that article. In that case, V' does not exist.
  452. if ( $this->mOldRev === false ) {
  453. if ( $this->mNewPage ) {
  454. $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
  455. }
  456. $samePage = true;
  457. $oldHeader = '';
  458. // Allow extensions to change the $oldHeader variable
  459. Hooks::run( 'DifferenceEngineOldHeaderNoOldRev', [ &$oldHeader ] );
  460. } else {
  461. Hooks::run( 'DiffViewHeader', [ $this, $this->mOldRev, $this->mNewRev ] );
  462. if ( !$this->mOldPage || !$this->mNewPage ) {
  463. // XXX say something to the user?
  464. $samePage = false;
  465. } elseif ( $this->mNewPage->equals( $this->mOldPage ) ) {
  466. $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
  467. $samePage = true;
  468. } else {
  469. $out->setPageTitle( $this->msg( 'difference-title-multipage',
  470. $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) );
  471. $out->addSubtitle( $this->msg( 'difference-multipage' ) );
  472. $samePage = false;
  473. }
  474. $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
  475. if ( $samePage && $this->mNewPage && $permissionManager->quickUserCan(
  476. 'edit', $user, $this->mNewPage
  477. ) ) {
  478. if ( $this->mNewRev->isCurrent() && $permissionManager->quickUserCan(
  479. 'rollback', $user, $this->mNewPage
  480. ) ) {
  481. $rollbackLink = Linker::generateRollback( $this->mNewRev, $this->getContext(),
  482. [ 'noBrackets' ] );
  483. if ( $rollbackLink ) {
  484. $out->preventClickjacking();
  485. $rollback = "\u{00A0}\u{00A0}\u{00A0}" . $rollbackLink;
  486. }
  487. }
  488. if ( $this->userCanEdit( $this->mOldRev ) &&
  489. $this->userCanEdit( $this->mNewRev )
  490. ) {
  491. $undoLink = Html::element( 'a', [
  492. 'href' => $this->mNewPage->getLocalURL( [
  493. 'action' => 'edit',
  494. 'undoafter' => $this->mOldid,
  495. 'undo' => $this->mNewid
  496. ] ),
  497. 'title' => Linker::titleAttrib( 'undo' ),
  498. ],
  499. $this->msg( 'editundo' )->text()
  500. );
  501. $revisionTools['mw-diff-undo'] = $undoLink;
  502. }
  503. }
  504. # Make "previous revision link"
  505. if ( $samePage && $this->mOldPage && $this->mOldRev->getPrevious() ) {
  506. $prevlink = Linker::linkKnown(
  507. $this->mOldPage,
  508. $this->msg( 'previousdiff' )->escaped(),
  509. [ 'id' => 'differences-prevlink' ],
  510. [ 'diff' => 'prev', 'oldid' => $this->mOldid ] + $query
  511. );
  512. } else {
  513. $prevlink = "\u{00A0}";
  514. }
  515. if ( $this->mOldRev->isMinor() ) {
  516. $oldminor = ChangesList::flag( 'minor' );
  517. } else {
  518. $oldminor = '';
  519. }
  520. $ldel = $this->revisionDeleteLink( $this->mOldRev );
  521. $oldRevisionHeader = $this->getRevisionHeader( $this->mOldRev, 'complete' );
  522. $oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff', $this->getContext() );
  523. $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' .
  524. '<div id="mw-diff-otitle2">' .
  525. Linker::revUserTools( $this->mOldRev, !$this->unhide ) . '</div>' .
  526. '<div id="mw-diff-otitle3">' . $oldminor .
  527. Linker::revComment( $this->mOldRev, !$diffOnly, !$this->unhide ) . $ldel . '</div>' .
  528. '<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' .
  529. '<div id="mw-diff-otitle4">' . $prevlink . '</div>';
  530. // Allow extensions to change the $oldHeader variable
  531. Hooks::run( 'DifferenceEngineOldHeader', [ $this, &$oldHeader, $prevlink, $oldminor,
  532. $diffOnly, $ldel, $this->unhide ] );
  533. if ( $this->mOldRev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
  534. $deleted = true; // old revisions text is hidden
  535. if ( $this->mOldRev->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
  536. $suppressed = true; // also suppressed
  537. }
  538. }
  539. # Check if this user can see the revisions
  540. if ( !$this->mOldRev->userCan( RevisionRecord::DELETED_TEXT, $user ) ) {
  541. $allowed = false;
  542. }
  543. }
  544. $out->addJsConfigVars( [
  545. 'wgDiffOldId' => $this->mOldid,
  546. 'wgDiffNewId' => $this->mNewid,
  547. ] );
  548. # Make "next revision link"
  549. # Skip next link on the top revision
  550. if ( $samePage && $this->mNewPage && !$this->mNewRev->isCurrent() ) {
  551. $nextlink = Linker::linkKnown(
  552. $this->mNewPage,
  553. $this->msg( 'nextdiff' )->escaped(),
  554. [ 'id' => 'differences-nextlink' ],
  555. [ 'diff' => 'next', 'oldid' => $this->mNewid ] + $query
  556. );
  557. } else {
  558. $nextlink = "\u{00A0}";
  559. }
  560. if ( $this->mNewRev->isMinor() ) {
  561. $newminor = ChangesList::flag( 'minor' );
  562. } else {
  563. $newminor = '';
  564. }
  565. # Handle RevisionDelete links...
  566. $rdel = $this->revisionDeleteLink( $this->mNewRev );
  567. # Allow extensions to define their own revision tools
  568. Hooks::run( 'DiffRevisionTools',
  569. [ $this->mNewRev, &$revisionTools, $this->mOldRev, $user ] );
  570. $formattedRevisionTools = [];
  571. // Put each one in parentheses (poor man's button)
  572. foreach ( $revisionTools as $key => $tool ) {
  573. $toolClass = is_string( $key ) ? $key : 'mw-diff-tool';
  574. $element = Html::rawElement(
  575. 'span',
  576. [ 'class' => $toolClass ],
  577. $this->msg( 'parentheses' )->rawParams( $tool )->escaped()
  578. );
  579. $formattedRevisionTools[] = $element;
  580. }
  581. $newRevisionHeader = $this->getRevisionHeader( $this->mNewRev, 'complete' ) .
  582. ' ' . implode( ' ', $formattedRevisionTools );
  583. $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff', $this->getContext() );
  584. $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' .
  585. '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $this->mNewRev, !$this->unhide ) .
  586. " $rollback</div>" .
  587. '<div id="mw-diff-ntitle3">' . $newminor .
  588. Linker::revComment( $this->mNewRev, !$diffOnly, !$this->unhide ) . $rdel . '</div>' .
  589. '<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' .
  590. '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
  591. // Allow extensions to change the $newHeader variable
  592. Hooks::run( 'DifferenceEngineNewHeader', [ $this, &$newHeader, $formattedRevisionTools,
  593. $nextlink, $rollback, $newminor, $diffOnly, $rdel, $this->unhide ] );
  594. if ( $this->mNewRev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
  595. $deleted = true; // new revisions text is hidden
  596. if ( $this->mNewRev->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
  597. $suppressed = true; // also suppressed
  598. }
  599. }
  600. # If the diff cannot be shown due to a deleted revision, then output
  601. # the diff header and links to unhide (if available)...
  602. if ( $deleted && ( !$this->unhide || !$allowed ) ) {
  603. $this->showDiffStyle();
  604. $multi = $this->getMultiNotice();
  605. $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
  606. if ( !$allowed ) {
  607. $msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff';
  608. # Give explanation for why revision is not visible
  609. $out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
  610. [ $msg ] );
  611. } else {
  612. # Give explanation and add a link to view the diff...
  613. $query = $this->getRequest()->appendQueryValue( 'unhide', '1' );
  614. $link = $this->getTitle()->getFullURL( $query );
  615. $msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff';
  616. $out->wrapWikiMsg(
  617. "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
  618. [ $msg, $link ]
  619. );
  620. }
  621. # Otherwise, output a regular diff...
  622. } else {
  623. # Add deletion notice if the user is viewing deleted content
  624. $notice = '';
  625. if ( $deleted ) {
  626. $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
  627. $notice = "<div id='mw-$msg' class='mw-warning plainlinks'>\n" .
  628. $this->msg( $msg )->parse() .
  629. "</div>\n";
  630. }
  631. $this->showDiff( $oldHeader, $newHeader, $notice );
  632. if ( !$diffOnly ) {
  633. $this->renderNewRevision();
  634. }
  635. }
  636. }
  637. /**
  638. * Build a link to mark a change as patrolled.
  639. *
  640. * Returns empty string if there's either no revision to patrol or the user is not allowed to.
  641. * Side effect: When the patrol link is build, this method will call
  642. * OutputPage::preventClickjacking() and load mediawiki.page.patrol.ajax.
  643. *
  644. * @return string HTML or empty string
  645. */
  646. public function markPatrolledLink() {
  647. if ( $this->mMarkPatrolledLink === null ) {
  648. $linkInfo = $this->getMarkPatrolledLinkInfo();
  649. // If false, there is no patrol link needed/allowed
  650. if ( !$linkInfo || !$this->mNewPage ) {
  651. $this->mMarkPatrolledLink = '';
  652. } else {
  653. $this->mMarkPatrolledLink = ' <span class="patrollink" data-mw="interface">[' .
  654. Linker::linkKnown(
  655. $this->mNewPage,
  656. $this->msg( 'markaspatrolleddiff' )->escaped(),
  657. [],
  658. [
  659. 'action' => 'markpatrolled',
  660. 'rcid' => $linkInfo['rcid'],
  661. ]
  662. ) . ']</span>';
  663. // Allow extensions to change the markpatrolled link
  664. Hooks::run( 'DifferenceEngineMarkPatrolledLink', [ $this,
  665. &$this->mMarkPatrolledLink, $linkInfo['rcid'] ] );
  666. }
  667. }
  668. return $this->mMarkPatrolledLink;
  669. }
  670. /**
  671. * Returns an array of meta data needed to build a "mark as patrolled" link and
  672. * adds the mediawiki.page.patrol.ajax to the output.
  673. *
  674. * @return array|false An array of meta data for a patrol link (rcid only)
  675. * or false if no link is needed
  676. */
  677. protected function getMarkPatrolledLinkInfo() {
  678. $user = $this->getUser();
  679. $config = $this->getConfig();
  680. $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
  681. // Prepare a change patrol link, if applicable
  682. if (
  683. // Is patrolling enabled and the user allowed to?
  684. $config->get( 'UseRCPatrol' ) &&
  685. $this->mNewPage &&
  686. $permissionManager->quickUserCan( 'patrol', $user, $this->mNewPage ) &&
  687. // Only do this if the revision isn't more than 6 hours older
  688. // than the Max RC age (6h because the RC might not be cleaned out regularly)
  689. RecentChange::isInRCLifespan( $this->mNewRev->getTimestamp(), 21600 )
  690. ) {
  691. // Look for an unpatrolled change corresponding to this diff
  692. $db = wfGetDB( DB_REPLICA );
  693. $change = RecentChange::newFromConds(
  694. [
  695. 'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
  696. 'rc_this_oldid' => $this->mNewid,
  697. 'rc_patrolled' => RecentChange::PRC_UNPATROLLED
  698. ],
  699. __METHOD__
  700. );
  701. if ( $change && !$change->getPerformer()->equals( $user ) ) {
  702. $rcid = $change->getAttribute( 'rc_id' );
  703. } else {
  704. // None found or the page has been created by the current user.
  705. // If the user could patrol this it already would be patrolled
  706. $rcid = 0;
  707. }
  708. // Allow extensions to possibly change the rcid here
  709. // For example the rcid might be set to zero due to the user
  710. // being the same as the performer of the change but an extension
  711. // might still want to show it under certain conditions
  712. Hooks::run( 'DifferenceEngineMarkPatrolledRCID', [ &$rcid, $this, $change, $user ] );
  713. // Build the link
  714. if ( $rcid ) {
  715. $this->getOutput()->preventClickjacking();
  716. if ( $permissionManager->userHasRight( $user, 'writeapi' ) ) {
  717. $this->getOutput()->addModules( 'mediawiki.page.patrol.ajax' );
  718. }
  719. return [
  720. 'rcid' => $rcid,
  721. ];
  722. }
  723. }
  724. // No mark as patrolled link applicable
  725. return false;
  726. }
  727. /**
  728. * @param Revision $rev
  729. *
  730. * @return string
  731. */
  732. protected function revisionDeleteLink( $rev ) {
  733. $link = Linker::getRevDeleteLink( $this->getUser(), $rev, $rev->getTitle() );
  734. if ( $link !== '' ) {
  735. $link = "\u{00A0}\u{00A0}\u{00A0}" . $link . ' ';
  736. }
  737. return $link;
  738. }
  739. /**
  740. * Show the new revision of the page.
  741. *
  742. * @note Not supported after calling setContent().
  743. */
  744. public function renderNewRevision() {
  745. if ( $this->isContentOverridden ) {
  746. // The code below only works with a Revision object. We could construct a fake revision
  747. // (here or in setContent), but since this does not seem needed at the moment,
  748. // we'll just fail for now.
  749. throw new LogicException(
  750. __METHOD__
  751. . ' is not supported after calling setContent(). Use setRevisions() instead.'
  752. );
  753. }
  754. $out = $this->getOutput();
  755. $revHeader = $this->getRevisionHeader( $this->mNewRev );
  756. # Add "current version as of X" title
  757. $out->addHTML( "<hr class='diff-hr' id='mw-oldid' />
  758. <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
  759. # Page content may be handled by a hooked call instead...
  760. if ( Hooks::run( 'ArticleContentOnDiff', [ $this, $out ] ) ) {
  761. $this->loadNewText();
  762. if ( !$this->mNewPage ) {
  763. // New revision is unsaved; bail out.
  764. // TODO in theory rendering the new revision is a meaningful thing to do
  765. // even if it's unsaved, but a lot of untangling is required to do it safely.
  766. return;
  767. }
  768. $out->setRevisionId( $this->mNewid );
  769. $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() );
  770. $out->setArticleFlag( true );
  771. if ( !Hooks::run( 'ArticleRevisionViewCustom',
  772. [ $this->mNewRev->getRevisionRecord(), $this->mNewPage, $this->mOldid, $out ] )
  773. ) {
  774. // Handled by extension
  775. // NOTE: sync with hooks called in Article::view()
  776. } elseif ( !Hooks::run( 'ArticleContentViewCustom',
  777. [ $this->mNewContent, $this->mNewPage, $out ], '1.32' )
  778. ) {
  779. // Handled by extension
  780. // NOTE: sync with hooks called in Article::view()
  781. } else {
  782. // Normal page
  783. if ( $this->getTitle()->equals( $this->mNewPage ) ) {
  784. // If the Title stored in the context is the same as the one
  785. // of the new revision, we can use its associated WikiPage
  786. // object.
  787. $wikiPage = $this->getWikiPage();
  788. } else {
  789. // Otherwise we need to create our own WikiPage object
  790. $wikiPage = WikiPage::factory( $this->mNewPage );
  791. }
  792. $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRev );
  793. # WikiPage::getParserOutput() should not return false, but just in case
  794. if ( $parserOutput ) {
  795. // Allow extensions to change parser output here
  796. if ( Hooks::run( 'DifferenceEngineRenderRevisionAddParserOutput',
  797. [ $this, $out, $parserOutput, $wikiPage ] )
  798. ) {
  799. $out->addParserOutput( $parserOutput, [
  800. 'enableSectionEditLinks' => $this->mNewRev->isCurrent()
  801. && MediaWikiServices::getInstance()->getPermissionManager()->quickUserCan(
  802. 'edit',
  803. $this->getUser(),
  804. $this->mNewRev->getTitle()
  805. )
  806. ] );
  807. }
  808. }
  809. }
  810. }
  811. // Allow extensions to optionally not show the final patrolled link
  812. if ( Hooks::run( 'DifferenceEngineRenderRevisionShowFinalPatrolLink' ) ) {
  813. # Add redundant patrol link on bottom...
  814. $out->addHTML( $this->markPatrolledLink() );
  815. }
  816. }
  817. /**
  818. * @param WikiPage $page
  819. * @param Revision $rev
  820. *
  821. * @return ParserOutput|bool False if the revision was not found
  822. */
  823. protected function getParserOutput( WikiPage $page, Revision $rev ) {
  824. if ( !$rev->getId() ) {
  825. // WikiPage::getParserOutput wants a revision ID. Passing 0 will incorrectly show
  826. // the current revision, so fail instead. If need be, WikiPage::getParserOutput
  827. // could be made to accept a Revision or RevisionRecord instead of the id.
  828. return false;
  829. }
  830. $parserOptions = $page->makeParserOptions( $this->getContext() );
  831. $parserOutput = $page->getParserOutput( $parserOptions, $rev->getId() );
  832. return $parserOutput;
  833. }
  834. /**
  835. * Get the diff text, send it to the OutputPage object
  836. * Returns false if the diff could not be generated, otherwise returns true
  837. *
  838. * @param string|bool $otitle Header for old text or false
  839. * @param string|bool $ntitle Header for new text or false
  840. * @param string $notice HTML between diff header and body
  841. *
  842. * @return bool
  843. */
  844. public function showDiff( $otitle, $ntitle, $notice = '' ) {
  845. // Allow extensions to affect the output here
  846. Hooks::run( 'DifferenceEngineShowDiff', [ $this ] );
  847. $diff = $this->getDiff( $otitle, $ntitle, $notice );
  848. if ( $diff === false ) {
  849. $this->showMissingRevision();
  850. return false;
  851. } else {
  852. $this->showDiffStyle();
  853. $this->getOutput()->addHTML( $diff );
  854. return true;
  855. }
  856. }
  857. /**
  858. * Add style sheets for diff display.
  859. */
  860. public function showDiffStyle() {
  861. if ( !$this->isSlotDiffRenderer ) {
  862. $this->getOutput()->addModuleStyles( [
  863. 'mediawiki.interface.helpers.styles',
  864. 'mediawiki.diff.styles'
  865. ] );
  866. foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
  867. $slotDiffRenderer->addModules( $this->getOutput() );
  868. }
  869. }
  870. }
  871. /**
  872. * Get complete diff table, including header
  873. *
  874. * @param string|bool $otitle Header for old text or false
  875. * @param string|bool $ntitle Header for new text or false
  876. * @param string $notice HTML between diff header and body
  877. *
  878. * @return mixed
  879. */
  880. public function getDiff( $otitle, $ntitle, $notice = '' ) {
  881. $body = $this->getDiffBody();
  882. if ( $body === false ) {
  883. return false;
  884. }
  885. $multi = $this->getMultiNotice();
  886. // Display a message when the diff is empty
  887. if ( $body === '' ) {
  888. $notice .= '<div class="mw-diff-empty">' .
  889. $this->msg( 'diff-empty' )->parse() .
  890. "</div>\n";
  891. }
  892. return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
  893. }
  894. /**
  895. * Get the diff table body, without header
  896. *
  897. * @return mixed (string/false)
  898. */
  899. public function getDiffBody() {
  900. $this->mCacheHit = true;
  901. // Check if the diff should be hidden from this user
  902. if ( !$this->isContentOverridden ) {
  903. if ( !$this->loadRevisionData() ) {
  904. return false;
  905. } elseif ( $this->mOldRev &&
  906. !$this->mOldRev->userCan( RevisionRecord::DELETED_TEXT, $this->getUser() )
  907. ) {
  908. return false;
  909. } elseif ( $this->mNewRev &&
  910. !$this->mNewRev->userCan( RevisionRecord::DELETED_TEXT, $this->getUser() )
  911. ) {
  912. return false;
  913. }
  914. // Short-circuit
  915. if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev &&
  916. $this->mOldRev->getId() && $this->mOldRev->getId() == $this->mNewRev->getId() )
  917. ) {
  918. if ( Hooks::run( 'DifferenceEngineShowEmptyOldContent', [ $this ] ) ) {
  919. return '';
  920. }
  921. }
  922. }
  923. // Cacheable?
  924. $key = false;
  925. $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
  926. if ( $this->mOldid && $this->mNewid ) {
  927. // Check if subclass is still using the old way
  928. // for backwards-compatibility
  929. $key = $this->getDiffBodyCacheKey();
  930. if ( $key === null ) {
  931. $key = $cache->makeKey( ...$this->getDiffBodyCacheKeyParams() );
  932. }
  933. // Try cache
  934. if ( !$this->mRefreshCache ) {
  935. $difftext = $cache->get( $key );
  936. if ( is_string( $difftext ) ) {
  937. wfIncrStats( 'diff_cache.hit' );
  938. $difftext = $this->localiseDiff( $difftext );
  939. $difftext .= "\n<!-- diff cache key $key -->\n";
  940. return $difftext;
  941. }
  942. } // don't try to load but save the result
  943. }
  944. $this->mCacheHit = false;
  945. // Loadtext is permission safe, this just clears out the diff
  946. if ( !$this->loadText() ) {
  947. return false;
  948. }
  949. $difftext = '';
  950. // We've checked for revdelete at the beginning of this method; it's OK to ignore
  951. // read permissions here.
  952. $slotContents = $this->getSlotContents();
  953. foreach ( $this->getSlotDiffRenderers() as $role => $slotDiffRenderer ) {
  954. $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role]['old'],
  955. $slotContents[$role]['new'] );
  956. if ( $slotDiff && $role !== SlotRecord::MAIN ) {
  957. // FIXME: ask SlotRoleHandler::getSlotNameMessage
  958. $slotTitle = $role;
  959. $difftext .= $this->getSlotHeader( $slotTitle );
  960. }
  961. $difftext .= $slotDiff;
  962. }
  963. // Avoid PHP 7.1 warning from passing $this by reference
  964. $diffEngine = $this;
  965. // Save to cache for 7 days
  966. if ( !Hooks::run( 'AbortDiffCache', [ &$diffEngine ] ) ) {
  967. wfIncrStats( 'diff_cache.uncacheable' );
  968. } elseif ( $key !== false && $difftext !== false ) {
  969. wfIncrStats( 'diff_cache.miss' );
  970. $cache->set( $key, $difftext, 7 * 86400 );
  971. } else {
  972. wfIncrStats( 'diff_cache.uncacheable' );
  973. }
  974. // localise line numbers and title attribute text
  975. if ( $difftext !== false ) {
  976. $difftext = $this->localiseDiff( $difftext );
  977. }
  978. return $difftext;
  979. }
  980. /**
  981. * Get the diff table body for one slot, without header
  982. *
  983. * @param string $role
  984. * @return string|false
  985. */
  986. public function getDiffBodyForRole( $role ) {
  987. $diffRenderers = $this->getSlotDiffRenderers();
  988. if ( !isset( $diffRenderers[$role] ) ) {
  989. return false;
  990. }
  991. $slotContents = $this->getSlotContents();
  992. $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role]['old'],
  993. $slotContents[$role]['new'] );
  994. if ( !$slotDiff ) {
  995. return false;
  996. }
  997. if ( $role !== SlotRecord::MAIN ) {
  998. // TODO use human-readable role name at least
  999. $slotTitle = $role;
  1000. $slotDiff = $this->getSlotHeader( $slotTitle ) . $slotDiff;
  1001. }
  1002. return $this->localiseDiff( $slotDiff );
  1003. }
  1004. /**
  1005. * Get a slot header for inclusion in a diff body (as a table row).
  1006. *
  1007. * @param string $headerText The text of the header
  1008. * @return string
  1009. *
  1010. */
  1011. protected function getSlotHeader( $headerText ) {
  1012. // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
  1013. $columnCount = $this->mOldRev ? 4 : 2;
  1014. $userLang = $this->getLanguage()->getHtmlCode();
  1015. return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-header', 'lang' => $userLang ],
  1016. Html::element( 'th', [ 'colspan' => $columnCount ], $headerText ) );
  1017. }
  1018. /**
  1019. * Returns the cache key for diff body text or content.
  1020. *
  1021. * @deprecated since 1.31, use getDiffBodyCacheKeyParams() instead
  1022. * @since 1.23
  1023. *
  1024. * @throws MWException
  1025. * @return string|null
  1026. */
  1027. protected function getDiffBodyCacheKey() {
  1028. return null;
  1029. }
  1030. /**
  1031. * Get the cache key parameters
  1032. *
  1033. * Subclasses can replace the first element in the array to something
  1034. * more specific to the type of diff (e.g. "inline-diff"), or append
  1035. * if the cache should vary on more things. Overriding entirely should
  1036. * be avoided.
  1037. *
  1038. * @since 1.31
  1039. *
  1040. * @return array
  1041. * @throws MWException
  1042. */
  1043. protected function getDiffBodyCacheKeyParams() {
  1044. if ( !$this->mOldid || !$this->mNewid ) {
  1045. throw new MWException( 'mOldid and mNewid must be set to get diff cache key.' );
  1046. }
  1047. $engine = $this->getEngine();
  1048. $params = [
  1049. 'diff',
  1050. $engine === 'php' ? false : $engine, // Back compat
  1051. self::DIFF_VERSION,
  1052. "old-{$this->mOldid}",
  1053. "rev-{$this->mNewid}"
  1054. ];
  1055. if ( $engine === 'wikidiff2' ) {
  1056. $params[] = phpversion( 'wikidiff2' );
  1057. }
  1058. if ( !$this->isSlotDiffRenderer ) {
  1059. foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
  1060. $params = array_merge( $params, $slotDiffRenderer->getExtraCacheKeys() );
  1061. }
  1062. }
  1063. return $params;
  1064. }
  1065. /**
  1066. * Implements DifferenceEngineSlotDiffRenderer::getExtraCacheKeys(). Only used when
  1067. * DifferenceEngine is wrapped in DifferenceEngineSlotDiffRenderer.
  1068. * @return array
  1069. * @internal for use by DifferenceEngineSlotDiffRenderer only
  1070. * @deprecated
  1071. */
  1072. public function getExtraCacheKeys() {
  1073. // This method is called when the DifferenceEngine is used for a slot diff. We only care
  1074. // about special things, not the revision IDs, which are added to the cache key by the
  1075. // page-level DifferenceEngine, and which might not have a valid value for this object.
  1076. $this->mOldid = 123456789;
  1077. $this->mNewid = 987654321;
  1078. // This will repeat a bunch of unnecessary key fields for each slot. Not nice but harmless.
  1079. $cacheString = $this->getDiffBodyCacheKey();
  1080. if ( $cacheString ) {
  1081. return [ $cacheString ];
  1082. }
  1083. $params = $this->getDiffBodyCacheKeyParams();
  1084. // Try to get rid of the standard keys to keep the cache key human-readable:
  1085. // call the getDiffBodyCacheKeyParams implementation of the base class, and if
  1086. // the child class includes the same keys, drop them.
  1087. // Uses an obscure PHP feature where static calls to non-static methods are allowed
  1088. // as long as we are already in a non-static method of the same class, and the call context
  1089. // ($this) will be inherited.
  1090. // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
  1091. $standardParams = DifferenceEngine::getDiffBodyCacheKeyParams();
  1092. if ( array_slice( $params, 0, count( $standardParams ) ) === $standardParams ) {
  1093. $params = array_slice( $params, count( $standardParams ) );
  1094. }
  1095. return $params;
  1096. }
  1097. /**
  1098. * Generate a diff, no caching.
  1099. *
  1100. * @since 1.21
  1101. *
  1102. * @param Content $old Old content
  1103. * @param Content $new New content
  1104. *
  1105. * @throws Exception If old or new content is not an instance of TextContent.
  1106. * @return bool|string
  1107. *
  1108. * @deprecated since 1.32, use a SlotDiffRenderer instead.
  1109. */
  1110. public function generateContentDiffBody( Content $old, Content $new ) {
  1111. $slotDiffRenderer = $new->getContentHandler()->getSlotDiffRenderer( $this->getContext() );
  1112. if (
  1113. $slotDiffRenderer instanceof DifferenceEngineSlotDiffRenderer
  1114. && $this->isSlotDiffRenderer
  1115. ) {
  1116. // Oops, we are just about to enter an infinite loop (the slot-level DifferenceEngine
  1117. // called a DifferenceEngineSlotDiffRenderer that wraps the same DifferenceEngine class).
  1118. // This will happen when a content model has no custom slot diff renderer, it does have
  1119. // a custom difference engine, but that does not override this method.
  1120. throw new Exception( get_class( $this ) . ': could not maintain backwards compatibility. '
  1121. . 'Please use a SlotDiffRenderer.' );
  1122. }
  1123. return $slotDiffRenderer->getDiff( $old, $new ) . $this->getDebugString();
  1124. }
  1125. /**
  1126. * Generate a diff, no caching
  1127. *
  1128. * @param string $otext Old text, must be already segmented
  1129. * @param string $ntext New text, must be already segmented
  1130. *
  1131. * @throws Exception If content handling for text content is configured in a way
  1132. * that makes maintaining B/C hard.
  1133. * @return bool|string
  1134. *
  1135. * @deprecated since 1.32, use a TextSlotDiffRenderer instead.
  1136. */
  1137. public function generateTextDiffBody( $otext, $ntext ) {
  1138. $slotDiffRenderer = ContentHandler::getForModelID( CONTENT_MODEL_TEXT )
  1139. ->getSlotDiffRenderer( $this->getContext() );
  1140. if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
  1141. // Someone used the GetSlotDiffRenderer hook to replace the renderer.
  1142. // This is too unlikely to happen to bother handling properly.
  1143. throw new Exception( 'The slot diff renderer for text content should be a '
  1144. . 'TextSlotDiffRenderer subclass' );
  1145. }
  1146. return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
  1147. }
  1148. /**
  1149. * Process DiffEngine config and get a sane, usable engine
  1150. *
  1151. * @return string 'wikidiff2', 'php', or path to an executable
  1152. * @internal For use by this class and TextSlotDiffRenderer only.
  1153. */
  1154. public static function getEngine() {
  1155. $diffEngine = MediaWikiServices::getInstance()->getMainConfig()
  1156. ->get( 'DiffEngine' );
  1157. $externalDiffEngine = MediaWikiServices::getInstance()->getMainConfig()
  1158. ->get( 'ExternalDiffEngine' );
  1159. if ( $diffEngine === null ) {
  1160. $engines = [ 'external', 'wikidiff2', 'php' ];
  1161. } else {
  1162. $engines = [ $diffEngine ];
  1163. }
  1164. $failureReason = null;
  1165. foreach ( $engines as $engine ) {
  1166. switch ( $engine ) {
  1167. case 'external':
  1168. if ( is_string( $externalDiffEngine ) ) {
  1169. if ( is_executable( $externalDiffEngine ) ) {
  1170. return $externalDiffEngine;
  1171. }
  1172. $failureReason = 'ExternalDiffEngine config points to a non-executable';
  1173. if ( $diffEngine === null ) {
  1174. wfDebug( "$failureReason, ignoring" );
  1175. }
  1176. } else {
  1177. $failureReason = 'ExternalDiffEngine config is set to a non-string value';
  1178. if ( $diffEngine === null && $externalDiffEngine ) {
  1179. wfWarn( "$failureReason, ignoring" );
  1180. }
  1181. }
  1182. break;
  1183. case 'wikidiff2':
  1184. if ( function_exists( 'wikidiff2_do_diff' ) ) {
  1185. return 'wikidiff2';
  1186. }
  1187. $failureReason = 'wikidiff2 is not available';
  1188. break;
  1189. case 'php':
  1190. // Always available.
  1191. return 'php';
  1192. default:
  1193. throw new DomainException( 'Invalid value for $wgDiffEngine: ' . $engine );
  1194. }
  1195. }
  1196. throw new UnexpectedValueException( "Cannot use diff engine '$engine': $failureReason" );
  1197. }
  1198. /**
  1199. * Generates diff, to be wrapped internally in a logging/instrumentation
  1200. *
  1201. * @param string $otext Old text, must be already segmented
  1202. * @param string $ntext New text, must be already segmented
  1203. *
  1204. * @throws Exception If content handling for text content is configured in a way
  1205. * that makes maintaining B/C hard.
  1206. * @return bool|string
  1207. *
  1208. * @deprecated since 1.32, use a TextSlotDiffRenderer instead.
  1209. */
  1210. protected function textDiff( $otext, $ntext ) {
  1211. $slotDiffRenderer = ContentHandler::getForModelID( CONTENT_MODEL_TEXT )
  1212. ->getSlotDiffRenderer( $this->getContext() );
  1213. if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
  1214. // Someone used the GetSlotDiffRenderer hook to replace the renderer.
  1215. // This is too unlikely to happen to bother handling properly.
  1216. throw new Exception( 'The slot diff renderer for text content should be a '
  1217. . 'TextSlotDiffRenderer subclass' );
  1218. }
  1219. return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
  1220. }
  1221. /**
  1222. * Generate a debug comment indicating diff generating time,
  1223. * server node, and generator backend.
  1224. *
  1225. * @param string $generator : What diff engine was used
  1226. *
  1227. * @return string
  1228. */
  1229. protected function debug( $generator = "internal" ) {
  1230. if ( !$this->enableDebugComment ) {
  1231. return '';
  1232. }
  1233. $data = [ $generator ];
  1234. if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
  1235. $data[] = wfHostname();
  1236. }
  1237. $data[] = wfTimestamp( TS_DB );
  1238. return "<!-- diff generator: " .
  1239. implode( " ", array_map( "htmlspecialchars", $data ) ) .
  1240. " -->\n";
  1241. }
  1242. private function getDebugString() {
  1243. $engine = self::getEngine();
  1244. if ( $engine === 'wikidiff2' ) {
  1245. return $this->debug( 'wikidiff2' );
  1246. } elseif ( $engine === 'php' ) {
  1247. return $this->debug( 'native PHP' );
  1248. } else {
  1249. return $this->debug( "external $engine" );
  1250. }
  1251. }
  1252. /**
  1253. * Localise diff output
  1254. *
  1255. * @param string $text
  1256. * @return string
  1257. */
  1258. private function localiseDiff( $text ) {
  1259. $text = $this->localiseLineNumbers( $text );
  1260. if ( $this->getEngine() === 'wikidiff2' &&
  1261. version_compare( phpversion( 'wikidiff2' ), '1.5.1', '>=' )
  1262. ) {
  1263. $text = $this->addLocalisedTitleTooltips( $text );
  1264. }
  1265. return $text;
  1266. }
  1267. /**
  1268. * Replace line numbers with the text in the user's language
  1269. *
  1270. * @param string $text
  1271. *
  1272. * @return mixed
  1273. */
  1274. public function localiseLineNumbers( $text ) {
  1275. return preg_replace_callback(
  1276. '/<!--LINE (\d+)-->/',
  1277. [ $this, 'localiseLineNumbersCb' ],
  1278. $text
  1279. );
  1280. }
  1281. public function localiseLineNumbersCb( $matches ) {
  1282. if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
  1283. return '';
  1284. }
  1285. return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
  1286. }
  1287. /**
  1288. * Add title attributes for tooltips on moved paragraph indicators
  1289. *
  1290. * @param string $text
  1291. * @return string
  1292. */
  1293. private function addLocalisedTitleTooltips( $text ) {
  1294. return preg_replace_callback(
  1295. '/class="mw-diff-movedpara-(left|right)"/',
  1296. [ $this, 'addLocalisedTitleTooltipsCb' ],
  1297. $text
  1298. );
  1299. }
  1300. /**
  1301. * @param array $matches
  1302. * @return string
  1303. */
  1304. private function addLocalisedTitleTooltipsCb( array $matches ) {
  1305. $key = $matches[1] === 'right' ?
  1306. 'diff-paragraph-moved-toold' :
  1307. 'diff-paragraph-moved-tonew';
  1308. return $matches[0] . ' title="' . $this->msg( $key )->escaped() . '"';
  1309. }
  1310. /**
  1311. * If there are revisions between the ones being compared, return a note saying so.
  1312. *
  1313. * @return string
  1314. */
  1315. public function getMultiNotice() {
  1316. // The notice only make sense if we are diffing two saved revisions of the same page.
  1317. if (
  1318. !$this->mOldRev || !$this->mNewRev
  1319. || !$this->mOldPage || !$this->mNewPage
  1320. || !$this->mOldPage->equals( $this->mNewPage )
  1321. ) {
  1322. return '';
  1323. }
  1324. if ( $this->mOldRev->getTimestamp() > $this->mNewRev->getTimestamp() ) {
  1325. $oldRev = $this->mNewRev; // flip
  1326. $newRev = $this->mOldRev; // flip
  1327. } else { // normal case
  1328. $oldRev = $this->mOldRev;
  1329. $newRev = $this->mNewRev;
  1330. }
  1331. // Sanity: don't show the notice if too many rows must be scanned
  1332. // @todo show some special message for that case
  1333. $nEdits = $this->mNewPage->countRevisionsBetween( $oldRev, $newRev, 1000 );
  1334. if ( $nEdits > 0 && $nEdits <= 1000 ) {
  1335. $limit = 100; // use diff-multi-manyusers if too many users
  1336. $users = $this->mNewPage->getAuthorsBetween( $oldRev, $newRev, $limit );
  1337. $numUsers = count( $users );
  1338. if ( $numUsers == 1 && $users[0] == $newRev->getUserText( RevisionRecord::RAW ) ) {
  1339. $numUsers = 0; // special case to say "by the same user" instead of "by one other user"
  1340. }
  1341. return self::intermediateEditsMsg( $nEdits, $numUsers, $limit );
  1342. }
  1343. return '';
  1344. }
  1345. /**
  1346. * Get a notice about how many intermediate edits and users there are
  1347. *
  1348. * @param int $numEdits
  1349. * @param int $numUsers
  1350. * @param int $limit
  1351. *
  1352. * @return string
  1353. */
  1354. public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) {
  1355. if ( $numUsers === 0 ) {
  1356. $msg = 'diff-multi-sameuser';
  1357. } elseif ( $numUsers > $limit ) {
  1358. $msg = 'diff-multi-manyusers';
  1359. $numUsers = $limit;
  1360. } else {
  1361. $msg = 'diff-multi-otherusers';
  1362. }
  1363. return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
  1364. }
  1365. /**
  1366. * @param Revision $rev
  1367. * @return bool whether the user can see and edit the revision.
  1368. */
  1369. private function userCanEdit( Revision $rev ) {
  1370. $user = $this->getUser();
  1371. if ( !$rev->userCan( RevisionRecord::DELETED_TEXT, $user ) ) {
  1372. return false;
  1373. }
  1374. return true;
  1375. }
  1376. /**
  1377. * Get a header for a specified revision.
  1378. *
  1379. * @param Revision $rev
  1380. * @param string $complete 'complete' to get the header wrapped depending
  1381. * the visibility of the revision and a link to edit the page.
  1382. *
  1383. * @return string HTML fragment
  1384. */
  1385. public function getRevisionHeader( Revision $rev, $complete = '' ) {
  1386. $lang = $this->getLanguage();
  1387. $user = $this->getUser();
  1388. $revtimestamp = $rev->getTimestamp();
  1389. $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
  1390. $dateofrev = $lang->userDate( $revtimestamp, $user );
  1391. $timeofrev = $lang->userTime( $revtimestamp, $user );
  1392. $header = $this->msg(
  1393. $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
  1394. $timestamp,
  1395. $dateofrev,
  1396. $timeofrev
  1397. )->escaped();
  1398. if ( $complete !== 'complete' ) {
  1399. return $header;
  1400. }
  1401. $title = $rev->getTitle();
  1402. $header = Linker::linkKnown( $title, $header, [],
  1403. [ 'oldid' => $rev->getId() ] );
  1404. if ( $this->userCanEdit( $rev ) ) {
  1405. $editQuery = [ 'action' => 'edit' ];
  1406. if ( !$rev->isCurrent() ) {
  1407. $editQuery['oldid'] = $rev->getId();
  1408. }
  1409. $key = MediaWikiServices::getInstance()->getPermissionManager()
  1410. ->quickUserCan( 'edit', $user, $title ) ? 'editold' : 'viewsourceold';
  1411. $msg = $this->msg( $key )->escaped();
  1412. $editLink = $this->msg( 'parentheses' )->rawParams(
  1413. Linker::linkKnown( $title, $msg, [], $editQuery ) )->escaped();
  1414. $header .= ' ' . Html::rawElement(
  1415. 'span',
  1416. [ 'class' => 'mw-diff-edit' ],
  1417. $editLink
  1418. );
  1419. if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
  1420. $header = Html::rawElement(
  1421. 'span',
  1422. [ 'class' => 'history-deleted' ],
  1423. $header
  1424. );
  1425. }
  1426. } else {
  1427. $header = Html::rawElement( 'span', [ 'class' => 'history-deleted' ], $header );
  1428. }
  1429. return $header;
  1430. }
  1431. /**
  1432. * Add the header to a diff body
  1433. *
  1434. * @param string $diff Diff body
  1435. * @param string $otitle Old revision header
  1436. * @param string $ntitle New revision header
  1437. * @param string $multi Notice telling user that there are intermediate
  1438. * revisions between the ones being compared
  1439. * @param string $notice Other notices, e.g. that user is viewing deleted content
  1440. *
  1441. * @return string
  1442. */
  1443. public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
  1444. // shared.css sets diff in interface language/dir, but the actual content
  1445. // is often in a different language, mostly the page content language/dir
  1446. $header = Html::openElement( 'table', [
  1447. 'class' => [ 'diff', 'diff-contentalign-' . $this->getDiffLang()->alignStart() ],
  1448. 'data-mw' => 'interface',
  1449. ] );
  1450. $userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
  1451. if ( !$diff && !$otitle ) {
  1452. $header .= "
  1453. <tr class=\"diff-title\" lang=\"{$userLang}\">
  1454. <td class=\"diff-ntitle\">{$ntitle}</td>
  1455. </tr>";
  1456. $multiColspan = 1;
  1457. } else {
  1458. if ( $diff ) { // Safari/Chrome show broken output if cols not used
  1459. $header .= "
  1460. <col class=\"diff-marker\" />
  1461. <col class=\"diff-content\" />
  1462. <col class=\"diff-marker\" />
  1463. <col class=\"diff-content\" />";
  1464. $colspan = 2;
  1465. $multiColspan = 4;
  1466. } else {
  1467. $colspan = 1;
  1468. $multiColspan = 2;
  1469. }
  1470. if ( $otitle || $ntitle ) {
  1471. $header .= "
  1472. <tr class=\"diff-title\" lang=\"{$userLang}\">
  1473. <td colspan=\"$colspan\" class=\"diff-otitle\">{$otitle}</td>
  1474. <td colspan=\"$colspan\" class=\"diff-ntitle\">{$ntitle}</td>
  1475. </tr>";
  1476. }
  1477. }
  1478. if ( $multi != '' ) {
  1479. $header .= "<tr><td colspan=\"{$multiColspan}\" " .
  1480. "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
  1481. }
  1482. if ( $notice != '' ) {
  1483. $header .= "<tr><td colspan=\"{$multiColspan}\" " .
  1484. "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
  1485. }
  1486. return $header . $diff . "</table>";
  1487. }
  1488. /**
  1489. * Use specified text instead of loading from the database
  1490. * @param Content $oldContent
  1491. * @param Content $newContent
  1492. * @since 1.21
  1493. * @deprecated since 1.32, use setRevisions or ContentHandler::getSlotDiffRenderer.
  1494. */
  1495. public function setContent( Content $oldContent, Content $newContent ) {
  1496. $this->mOldContent = $oldContent;
  1497. $this->mNewContent = $newContent;
  1498. $this->mTextLoaded = 2;
  1499. $this->mRevisionsLoaded = true;
  1500. $this->isContentOverridden = true;
  1501. $this->slotDiffRenderers = null;
  1502. }
  1503. /**
  1504. * Use specified text instead of loading from the database.
  1505. * @param RevisionRecord|null $oldRevision
  1506. * @param RevisionRecord $newRevision
  1507. */
  1508. public function setRevisions(
  1509. RevisionRecord $oldRevision = null, RevisionRecord $newRevision
  1510. ) {
  1511. if ( $oldRevision ) {
  1512. $this->mOldRev = new Revision( $oldRevision );
  1513. $this->mOldid = $oldRevision->getId();
  1514. $this->mOldPage = Title::newFromLinkTarget( $oldRevision->getPageAsLinkTarget() );
  1515. // This method is meant for edit diffs and such so there is no reason to provide a
  1516. // revision that's not readable to the user, but check it just in case.
  1517. $this->mOldContent = $oldRevision->getContent( SlotRecord::MAIN,
  1518. RevisionRecord::FOR_THIS_USER, $this->getUser() );
  1519. } else {
  1520. $this->mOldPage = null;
  1521. $this->mOldRev = $this->mOldid = false;
  1522. }
  1523. $this->mNewRev = new Revision( $newRevision );
  1524. $this->mNewid = $newRevision->getId();
  1525. $this->mNewPage = Title::newFromLinkTarget( $newRevision->getPageAsLinkTarget() );
  1526. $this->mNewContent = $newRevision->getContent( SlotRecord::MAIN,
  1527. RevisionRecord::FOR_THIS_USER, $this->getUser() );
  1528. $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded = true;
  1529. $this->mTextLoaded = $oldRevision ? 2 : 1;
  1530. $this->isContentOverridden = false;
  1531. $this->slotDiffRenderers = null;
  1532. }
  1533. /**
  1534. * Set the language in which the diff text is written
  1535. *
  1536. * @param Language $lang
  1537. * @since 1.19
  1538. */
  1539. public function setTextLanguage( Language $lang ) {
  1540. $this->mDiffLang = $lang;
  1541. }
  1542. /**
  1543. * Maps a revision pair definition as accepted by DifferenceEngine constructor
  1544. * to a pair of actual integers representing revision ids.
  1545. *
  1546. * @param int $old Revision id, e.g. from URL parameter 'oldid'
  1547. * @param int|string $new Revision id or strings 'next' or 'prev', e.g. from URL parameter 'diff'
  1548. *
  1549. * @return array List of two revision ids, older first, later second.
  1550. * Zero signifies invalid argument passed.
  1551. * false signifies that there is no previous/next revision ($old is the oldest/newest one).
  1552. */
  1553. public function mapDiffPrevNext( $old, $new ) {
  1554. $rl = MediaWikiServices::getInstance()->getRevisionLookup();
  1555. if ( $new === 'prev' ) {
  1556. // Show diff between revision $old and the previous one. Get previous one from DB.
  1557. $newid = intval( $old );
  1558. $oldid = false;
  1559. $newRev = $rl->getRevisionById( $newid );
  1560. if ( $newRev ) {
  1561. $oldRev = $rl->getPreviousRevision( $newRev );
  1562. if ( $oldRev ) {
  1563. $oldid = $oldRev->getId();
  1564. }
  1565. }
  1566. } elseif ( $new === 'next' ) {
  1567. // Show diff between revision $old and the next one. Get next one from DB.
  1568. $oldid = intval( $old );
  1569. $newid = false;
  1570. $oldRev = $rl->getRevisionById( $oldid );
  1571. if ( $oldRev ) {
  1572. $newRev = $rl->getNextRevision( $oldRev );
  1573. if ( $newRev ) {
  1574. $newid = $newRev->getId();
  1575. }
  1576. }
  1577. } else {
  1578. $oldid = intval( $old );
  1579. $newid = intval( $new );
  1580. }
  1581. return [ $oldid, $newid ];
  1582. }
  1583. /**
  1584. * Load revision IDs
  1585. */
  1586. private function loadRevisionIds() {
  1587. if ( $this->mRevisionsIdsLoaded ) {
  1588. return;
  1589. }
  1590. $this->mRevisionsIdsLoaded = true;
  1591. $old = $this->mOldid;
  1592. $new = $this->mNewid;
  1593. list( $this->mOldid, $this->mNewid ) = self::mapDiffPrevNext( $old, $new );
  1594. if ( $new === 'next' && $this->mNewid === false ) {
  1595. # if no result, NewId points to the newest old revision. The only newer
  1596. # revision is cur, which is "0".
  1597. $this->mNewid = 0;
  1598. }
  1599. Hooks::run(
  1600. 'NewDifferenceEngine',
  1601. [ $this->getTitle(), &$this->mOldid, &$this->mNewid, $old, $new ]
  1602. );
  1603. }
  1604. /**
  1605. * Load revision metadata for the specified revisions. If newid is 0, then compare
  1606. * the old revision in oldid to the current revision of the current page (as defined
  1607. * by the request context); if oldid is 0, then compare the revision in newid to the
  1608. * immediately previous one.
  1609. *
  1610. * If oldid is false, leave the corresponding revision object set to false. This can
  1611. * happen with 'diff=prev' pointing to a non-existent revision, and is also used directly
  1612. * by the API.
  1613. *
  1614. * @return bool Whether both revisions were loaded successfully. Setting mOldRev
  1615. * to false counts as successful loading.
  1616. */
  1617. public function loadRevisionData() {
  1618. if ( $this->mRevisionsLoaded ) {
  1619. return $this->isContentOverridden || ( $this->mOldRev !== null && $this->mNewRev !== null );
  1620. }
  1621. // Whether it succeeds or fails, we don't want to try again
  1622. $this->mRevisionsLoaded = true;
  1623. $this->loadRevisionIds();
  1624. // Load the new revision object
  1625. if ( $this->mNewid ) {
  1626. $this->mNewRev = Revision::newFromId( $this->mNewid );
  1627. } else {
  1628. $this->mNewRev = Revision::newFromTitle(
  1629. $this->getTitle(),
  1630. false,
  1631. Revision::READ_NORMAL
  1632. );
  1633. }
  1634. if ( !$this->mNewRev instanceof Revision ) {
  1635. return false;
  1636. }
  1637. // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
  1638. $this->mNewid = $this->mNewRev->getId();
  1639. if ( $this->mNewid ) {
  1640. $this->mNewPage = $this->mNewRev->getTitle();
  1641. } else {
  1642. $this->mNewPage = null;
  1643. }
  1644. // Load the old revision object
  1645. $this->mOldRev = false;
  1646. if ( $this->mOldid ) {
  1647. $this->mOldRev = Revision::newFromId( $this->mOldid );
  1648. } elseif ( $this->mOldid === 0 ) {
  1649. $rev = $this->mNewRev->getPrevious();
  1650. if ( $rev ) {
  1651. $this->mOldid = $rev->getId();
  1652. $this->mOldRev = $rev;
  1653. } else {
  1654. // No previous revision; mark to show as first-version only.
  1655. $this->mOldid = false;
  1656. $this->mOldRev = false;
  1657. }
  1658. } /* elseif ( $this->mOldid === false ) leave mOldRev false; */
  1659. if ( is_null( $this->mOldRev ) ) {
  1660. return false;
  1661. }
  1662. if ( $this->mOldRev && $this->mOldRev->getId() ) {
  1663. $this->mOldPage = $this->mOldRev->getTitle();
  1664. } else {
  1665. $this->mOldPage = null;
  1666. }
  1667. // Load tags information for both revisions
  1668. $dbr = wfGetDB( DB_REPLICA );
  1669. $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
  1670. if ( $this->mOldid !== false ) {
  1671. $tagIds = $dbr->selectFieldValues(
  1672. 'change_tag',
  1673. 'ct_tag_id',
  1674. [ 'ct_rev_id' => $this->mOldid ],
  1675. __METHOD__
  1676. );
  1677. $tags = [];
  1678. foreach ( $tagIds as $tagId ) {
  1679. try {
  1680. $tags[] = $changeTagDefStore->getName( (int)$tagId );
  1681. } catch ( NameTableAccessException $exception ) {
  1682. continue;
  1683. }
  1684. }
  1685. $this->mOldTags = implode( ',', $tags );
  1686. } else {
  1687. $this->mOldTags = false;
  1688. }
  1689. $tagIds = $dbr->selectFieldValues(
  1690. 'change_tag',
  1691. 'ct_tag_id',
  1692. [ 'ct_rev_id' => $this->mNewid ],
  1693. __METHOD__
  1694. );
  1695. $tags = [];
  1696. foreach ( $tagIds as $tagId ) {
  1697. try {
  1698. $tags[] = $changeTagDefStore->getName( (int)$tagId );
  1699. } catch ( NameTableAccessException $exception ) {
  1700. continue;
  1701. }
  1702. }
  1703. $this->mNewTags = implode( ',', $tags );
  1704. return true;
  1705. }
  1706. /**
  1707. * Load the text of the revisions, as well as revision data.
  1708. * When the old revision is missing (mOldRev is false), loading mOldContent is not attempted.
  1709. *
  1710. * @return bool Whether the content of both revisions could be loaded successfully.
  1711. * (When mOldRev is false, that still counts as a success.)
  1712. *
  1713. */
  1714. public function loadText() {
  1715. if ( $this->mTextLoaded == 2 ) {
  1716. return $this->loadRevisionData() && ( $this->mOldRev === false || $this->mOldContent )
  1717. && $this->mNewContent;
  1718. }
  1719. // Whether it succeeds or fails, we don't want to try again
  1720. $this->mTextLoaded = 2;
  1721. if ( !$this->loadRevisionData() ) {
  1722. return false;
  1723. }
  1724. if ( $this->mOldRev ) {
  1725. $this->mOldContent = $this->mOldRev->getContent(
  1726. RevisionRecord::FOR_THIS_USER, $this->getUser()
  1727. );
  1728. if ( $this->mOldContent === null ) {
  1729. return false;
  1730. }
  1731. }
  1732. $this->mNewContent = $this->mNewRev->getContent(
  1733. RevisionRecord::FOR_THIS_USER, $this->getUser()
  1734. );
  1735. Hooks::run( 'DifferenceEngineLoadTextAfterNewContentIsLoaded', [ $this ] );
  1736. if ( $this->mNewContent === null ) {
  1737. return false;
  1738. }
  1739. return true;
  1740. }
  1741. /**
  1742. * Load the text of the new revision, not the old one
  1743. *
  1744. * @return bool Whether the content of the new revision could be loaded successfully.
  1745. */
  1746. public function loadNewText() {
  1747. if ( $this->mTextLoaded >= 1 ) {
  1748. return $this->loadRevisionData();
  1749. }
  1750. $this->mTextLoaded = 1;
  1751. if ( !$this->loadRevisionData() ) {
  1752. return false;
  1753. }
  1754. $this->mNewContent = $this->mNewRev->getContent(
  1755. RevisionRecord::FOR_THIS_USER, $this->getUser()
  1756. );
  1757. Hooks::run( 'DifferenceEngineAfterLoadNewText', [ $this ] );
  1758. return true;
  1759. }
  1760. }