DifferenceEngine.php 60 KB

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