DifferenceEngine.php 61 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174
  1. <?php
  2. /**
  3. * @defgroup DifferenceEngine DifferenceEngine
  4. */
  5. /**
  6. * Constant to indicate diff cache compatibility.
  7. * Bump this when changing the diff formatting in a way that
  8. * fixes important bugs or such to force cached diff views to
  9. * clear.
  10. */
  11. define( 'MW_DIFF_VERSION', '1.11a' );
  12. /**
  13. * @todo document
  14. * @ingroup DifferenceEngine
  15. */
  16. class DifferenceEngine {
  17. /**#@+
  18. * @private
  19. */
  20. var $mOldid, $mNewid, $mTitle;
  21. var $mOldtitle, $mNewtitle, $mPagetitle;
  22. var $mOldtext, $mNewtext;
  23. var $mOldPage, $mNewPage;
  24. var $mRcidMarkPatrolled;
  25. var $mOldRev, $mNewRev;
  26. var $mRevisionsLoaded = false; // Have the revisions been loaded
  27. var $mTextLoaded = 0; // How many text blobs have been loaded, 0, 1 or 2?
  28. var $mCacheHit = false; // Was the diff fetched from cache?
  29. var $htmldiff;
  30. protected $unhide = false;
  31. /**#@-*/
  32. /**
  33. * Constructor
  34. * @param $titleObj Title object that the diff is associated with
  35. * @param $old Integer: old ID we want to show and diff with.
  36. * @param $new String: either 'prev' or 'next'.
  37. * @param $rcid Integer: ??? FIXME (default 0)
  38. * @param $refreshCache boolean If set, refreshes the diff cache
  39. * @param $htmldiff boolean If set, output using HTMLDiff instead of raw wikicode diff
  40. * @param $unhide boolean If set, allow viewing deleted revs
  41. */
  42. function __construct( $titleObj = null, $old = 0, $new = 0, $rcid = 0, $refreshCache = false , $htmldiff = false, $unhide = false ) {
  43. $this->mTitle = $titleObj;
  44. wfDebug("DifferenceEngine old '$old' new '$new' rcid '$rcid'\n");
  45. if ( 'prev' === $new ) {
  46. # Show diff between revision $old and the previous one.
  47. # Get previous one from DB.
  48. $this->mNewid = intval($old);
  49. $this->mOldid = $this->mTitle->getPreviousRevisionID( $this->mNewid );
  50. } elseif ( 'next' === $new ) {
  51. # Show diff between revision $old and the next one.
  52. # Get next one from DB.
  53. $this->mOldid = intval($old);
  54. $this->mNewid = $this->mTitle->getNextRevisionID( $this->mOldid );
  55. if ( false === $this->mNewid ) {
  56. # if no result, NewId points to the newest old revision. The only newer
  57. # revision is cur, which is "0".
  58. $this->mNewid = 0;
  59. }
  60. } else {
  61. $this->mOldid = intval($old);
  62. $this->mNewid = intval($new);
  63. wfRunHooks( 'NewDifferenceEngine', array(&$titleObj, &$this->mOldid, &$this->mNewid, $old, $new) );
  64. }
  65. $this->mRcidMarkPatrolled = intval($rcid); # force it to be an integer
  66. $this->mRefreshCache = $refreshCache;
  67. $this->htmldiff = $htmldiff;
  68. $this->unhide = $unhide;
  69. }
  70. function getTitle() {
  71. return $this->mTitle;
  72. }
  73. function wasCacheHit() {
  74. return $this->mCacheHit;
  75. }
  76. function getOldid() {
  77. return $this->mOldid;
  78. }
  79. function getNewid() {
  80. return $this->mNewid;
  81. }
  82. function showDiffPage( $diffOnly = false ) {
  83. global $wgUser, $wgOut, $wgUseExternalEditor, $wgUseRCPatrol, $wgEnableHtmlDiff;
  84. wfProfileIn( __METHOD__ );
  85. # If external diffs are enabled both globally and for the user,
  86. # we'll use the application/x-external-editor interface to call
  87. # an external diff tool like kompare, kdiff3, etc.
  88. if($wgUseExternalEditor && $wgUser->getOption('externaldiff')) {
  89. global $wgInputEncoding,$wgServer,$wgScript,$wgLang;
  90. $wgOut->disable();
  91. header ( "Content-type: application/x-external-editor; charset=".$wgInputEncoding );
  92. $url1=$this->mTitle->getFullURL("action=raw&oldid=".$this->mOldid);
  93. $url2=$this->mTitle->getFullURL("action=raw&oldid=".$this->mNewid);
  94. $special=$wgLang->getNsText(NS_SPECIAL);
  95. $control=<<<CONTROL
  96. [Process]
  97. Type=Diff text
  98. Engine=MediaWiki
  99. Script={$wgServer}{$wgScript}
  100. Special namespace={$special}
  101. [File]
  102. Extension=wiki
  103. URL=$url1
  104. [File 2]
  105. Extension=wiki
  106. URL=$url2
  107. CONTROL;
  108. echo($control);
  109. return;
  110. }
  111. $wgOut->setArticleFlag( false );
  112. if ( !$this->loadRevisionData() ) {
  113. $t = $this->mTitle->getPrefixedText();
  114. $d = wfMsgExt( 'missingarticle-diff', array( 'escape' ), $this->mOldid, $this->mNewid );
  115. $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
  116. $wgOut->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", $d );
  117. wfProfileOut( __METHOD__ );
  118. return;
  119. }
  120. wfRunHooks( 'DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ) );
  121. if ( $this->mNewRev->isCurrent() ) {
  122. $wgOut->setArticleFlag( true );
  123. }
  124. # mOldid is false if the difference engine is called with a "vague" query for
  125. # a diff between a version V and its previous version V' AND the version V
  126. # is the first version of that article. In that case, V' does not exist.
  127. if ( $this->mOldid === false ) {
  128. $this->showFirstRevision();
  129. $this->renderNewRevision(); // should we respect $diffOnly here or not?
  130. wfProfileOut( __METHOD__ );
  131. return;
  132. }
  133. $wgOut->suppressQuickbar();
  134. $oldTitle = $this->mOldPage->getPrefixedText();
  135. $newTitle = $this->mNewPage->getPrefixedText();
  136. if( $oldTitle == $newTitle ) {
  137. $wgOut->setPageTitle( $newTitle );
  138. } else {
  139. $wgOut->setPageTitle( $oldTitle . ', ' . $newTitle );
  140. }
  141. $wgOut->setSubtitle( wfMsgExt( 'difference', array( 'parseinline' ) ) );
  142. $wgOut->setRobotPolicy( 'noindex,nofollow' );
  143. if ( !$this->mOldPage->userCanRead() || !$this->mNewPage->userCanRead() ) {
  144. $wgOut->loginToUse();
  145. $wgOut->output();
  146. $wgOut->disable();
  147. wfProfileOut( __METHOD__ );
  148. return;
  149. }
  150. $sk = $wgUser->getSkin();
  151. // Check if page is editable
  152. $editable = $this->mNewRev->getTitle()->userCan( 'edit' );
  153. if ( $editable && $this->mNewRev->isCurrent() && $wgUser->isAllowed( 'rollback' ) ) {
  154. $rollback = '&nbsp;&nbsp;&nbsp;' . $sk->generateRollback( $this->mNewRev );
  155. } else {
  156. $rollback = '';
  157. }
  158. // Prepare a change patrol link, if applicable
  159. if( $wgUseRCPatrol && $this->mTitle->userCan('patrol') ) {
  160. // If we've been given an explicit change identifier, use it; saves time
  161. if( $this->mRcidMarkPatrolled ) {
  162. $rcid = $this->mRcidMarkPatrolled;
  163. $rc = RecentChange::newFromId( $rcid );
  164. // Already patrolled?
  165. $rcid = is_object($rc) && !$rc->getAttribute('rc_patrolled') ? $rcid : 0;
  166. } else {
  167. // Look for an unpatrolled change corresponding to this diff
  168. $db = wfGetDB( DB_SLAVE );
  169. $change = RecentChange::newFromConds(
  170. array(
  171. // Add redundant user,timestamp condition so we can use the existing index
  172. 'rc_user_text' => $this->mNewRev->getRawUserText(),
  173. 'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
  174. 'rc_this_oldid' => $this->mNewid,
  175. 'rc_last_oldid' => $this->mOldid,
  176. 'rc_patrolled' => 0
  177. ),
  178. __METHOD__
  179. );
  180. if( $change instanceof RecentChange ) {
  181. $rcid = $change->mAttribs['rc_id'];
  182. $this->mRcidMarkPatrolled = $rcid;
  183. } else {
  184. // None found
  185. $rcid = 0;
  186. }
  187. }
  188. // Build the link
  189. if( $rcid ) {
  190. $patrol = ' <span class="patrollink">[' . $sk->makeKnownLinkObj( $this->mTitle,
  191. wfMsgHtml( 'markaspatrolleddiff' ), "action=markpatrolled&rcid={$rcid}" ) . ']</span>';
  192. } else {
  193. $patrol = '';
  194. }
  195. } else {
  196. $patrol = '';
  197. }
  198. $diffOnlyArg = '';
  199. # Carry over 'diffonly' param via navigation links
  200. if( $diffOnly != $wgUser->getBoolOption('diffonly') ) {
  201. $diffOnlyArg = '&diffonly='.$diffOnly;
  202. }
  203. $htmldiffarg = $this->htmlDiffArgument();
  204. # Make "previous revision link"
  205. $prevlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'previousdiff' ),
  206. "diff=prev&oldid={$this->mOldid}{$htmldiffarg}{$diffOnlyArg}", '', '', 'id="differences-prevlink"' );
  207. # Make "next revision link"
  208. if( $this->mNewRev->isCurrent() ) {
  209. $nextlink = '&nbsp;';
  210. } else {
  211. $nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ),
  212. "diff=next&oldid={$this->mNewid}{$htmldiffarg}{$diffOnlyArg}", '', '', 'id="differences-nextlink"' );
  213. }
  214. $oldminor = '';
  215. $newminor = '';
  216. if( $this->mOldRev->isMinor() ) {
  217. $oldminor = Xml::span( wfMsg( 'minoreditletter' ), 'minor' ) . ' ';
  218. }
  219. if( $this->mNewRev->isMinor() ) {
  220. $newminor = Xml::span( wfMsg( 'minoreditletter' ), 'minor' ) . ' ';
  221. }
  222. $rdel = ''; $ldel = '';
  223. if( $wgUser->isAllowed( 'deleterevision' ) ) {
  224. if( !$this->mOldRev->userCan( Revision::DELETED_RESTRICTED ) ) {
  225. // If revision was hidden from sysops
  226. $ldel = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.wfMsgHtml( 'rev-delundel' ).')' );
  227. } else {
  228. $query = array( 'target' => $this->mOldRev->mTitle->getPrefixedDbkey(),
  229. 'oldid' => $this->mOldRev->getId()
  230. );
  231. $ldel = $sk->revDeleteLink( $query, $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) );
  232. }
  233. $ldel = "&nbsp;&nbsp;&nbsp;$ldel ";
  234. // We don't currently handle well changing the top revision's settings
  235. if( $this->mNewRev->isCurrent() ) {
  236. $rdel = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.wfMsgHtml( 'rev-delundel' ).')' );
  237. } else if( !$this->mNewRev->userCan( Revision::DELETED_RESTRICTED ) ) {
  238. // If revision was hidden from sysops
  239. $rdel = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.wfMsgHtml( 'rev-delundel' ).')' );
  240. } else {
  241. $query = array( 'target' => $this->mNewRev->mTitle->getPrefixedDbkey(),
  242. 'oldid' => $this->mNewRev->getId()
  243. );
  244. $rdel = $sk->revDeleteLink( $query, $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) );
  245. }
  246. $rdel = "&nbsp;&nbsp;&nbsp;$rdel ";
  247. }
  248. $oldHeader = '<div id="mw-diff-otitle1"><strong>'.$this->mOldtitle.'</strong></div>' .
  249. '<div id="mw-diff-otitle2">' . $sk->revUserTools( $this->mOldRev, !$this->unhide ) . "</div>" .
  250. '<div id="mw-diff-otitle3">' . $oldminor . $sk->revComment( $this->mOldRev, !$diffOnly, !$this->unhide ).$ldel."</div>" .
  251. '<div id="mw-diff-otitle4">' . $prevlink .'</div>';
  252. $newHeader = '<div id="mw-diff-ntitle1"><strong>'.$this->mNewtitle.'</strong></div>' .
  253. '<div id="mw-diff-ntitle2">' . $sk->revUserTools( $this->mNewRev, !$this->unhide ) . " $rollback</div>" .
  254. '<div id="mw-diff-ntitle3">' . $newminor . $sk->revComment( $this->mNewRev, !$diffOnly, !$this->unhide ).$rdel."</div>" .
  255. '<div id="mw-diff-ntitle4">' . $nextlink . $patrol . '</div>';
  256. # Check if this user can see the revisions
  257. $allowed = $this->mOldRev->userCan(Revision::DELETED_TEXT)
  258. && $this->mNewRev->userCan(Revision::DELETED_TEXT);
  259. $deleted = $this->mOldRev->isDeleted(Revision::DELETED_TEXT)
  260. || $this->mNewRev->isDeleted(Revision::DELETED_TEXT);
  261. # Output the diff if allowed...
  262. if( $deleted && (!$this->unhide || !$allowed) ) {
  263. $this->showDiffStyle();
  264. $multi = $this->getMultiNotice();
  265. $wgOut->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
  266. if( !$allowed ) {
  267. # Give explanation for why revision is not visible
  268. $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n",
  269. array( 'rev-deleted-no-diff' ) );
  270. } else {
  271. # Give explanation and add a link to view the diff...
  272. $link = $this->mTitle->getFullUrl( "diff={$this->mNewid}&oldid={$this->mOldid}".
  273. '&unhide=1&token='.urlencode( $wgUser->editToken($this->mNewid) ) );
  274. $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n",
  275. array( 'rev-deleted-unhide-diff', $link ) );
  276. }
  277. } else if( $wgEnableHtmlDiff && $this->htmldiff ) {
  278. $multi = $this->getMultiNotice();
  279. $wgOut->addHTML('<div class="diff-switchtype">'.$sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'wikicodecomparison' ),
  280. 'diff='.$this->mNewid.'&oldid='.$this->mOldid.'&htmldiff=0', '', '', 'id="differences-switchtype"' ).'</div>');
  281. $wgOut->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
  282. $this->renderHtmlDiff();
  283. } else {
  284. if( $wgEnableHtmlDiff ) {
  285. $wgOut->addHTML('<div class="diff-switchtype">'.$sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'visualcomparison' ),
  286. 'diff='.$this->mNewid.'&oldid='.$this->mOldid.'&htmldiff=1', '', '', 'id="differences-switchtype"' ).'</div>');
  287. }
  288. $this->showDiff( $oldHeader, $newHeader );
  289. if( !$diffOnly ) {
  290. $this->renderNewRevision();
  291. }
  292. }
  293. wfProfileOut( __METHOD__ );
  294. }
  295. /**
  296. * Show the new revision of the page.
  297. */
  298. function renderNewRevision() {
  299. global $wgOut, $wgUser;
  300. wfProfileIn( __METHOD__ );
  301. $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" );
  302. # Add deleted rev tag if needed
  303. if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
  304. $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-permission' );
  305. } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
  306. $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-view' );
  307. }
  308. if( !$this->mNewRev->isCurrent() ) {
  309. $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false );
  310. }
  311. $this->loadNewText();
  312. if( is_object( $this->mNewRev ) ) {
  313. $wgOut->setRevisionId( $this->mNewRev->getId() );
  314. }
  315. if( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
  316. // Stolen from Article::view --AG 2007-10-11
  317. // Give hooks a chance to customise the output
  318. if( wfRunHooks( 'ShowRawCssJs', array( $this->mNewtext, $this->mTitle, $wgOut ) ) ) {
  319. // Wrap the whole lot in a <pre> and don't parse
  320. $m = array();
  321. preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m );
  322. $wgOut->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
  323. $wgOut->addHTML( htmlspecialchars( $this->mNewtext ) );
  324. $wgOut->addHTML( "\n</pre>\n" );
  325. }
  326. } else {
  327. $wgOut->addWikiTextTidy( $this->mNewtext );
  328. }
  329. if( is_object( $this->mNewRev ) && !$this->mNewRev->isCurrent() ) {
  330. $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting );
  331. }
  332. # Add redundant patrol link on bottom...
  333. if( $this->mRcidMarkPatrolled && $this->mTitle->quickUserCan('patrol') ) {
  334. $sk = $wgUser->getSkin();
  335. $wgOut->addHTML(
  336. "<div class='patrollink'>[" . $sk->makeKnownLinkObj( $this->mTitle,
  337. wfMsgHtml( 'markaspatrolleddiff' ), "action=markpatrolled&rcid={$this->mRcidMarkPatrolled}" ) .
  338. ']</div>'
  339. );
  340. }
  341. wfProfileOut( __METHOD__ );
  342. }
  343. function renderHtmlDiff() {
  344. global $wgOut, $wgTitle, $wgParser, $wgDebugComments;
  345. wfProfileIn( __METHOD__ );
  346. $this->showDiffStyle();
  347. $wgOut->addHTML( '<h2>'.wfMsgHtml( 'visual-comparison' )."</h2>\n" );
  348. #add deleted rev tag if needed
  349. if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
  350. $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-permission' );
  351. } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
  352. $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-view' );
  353. }
  354. if( !$this->mNewRev->isCurrent() ) {
  355. $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false );
  356. }
  357. $this->loadText();
  358. // Old revision
  359. if( is_object( $this->mOldRev ) ) {
  360. $wgOut->setRevisionId( $this->mOldRev->getId() );
  361. }
  362. $popts = $wgOut->parserOptions();
  363. $oldTidy = $popts->setTidy( true );
  364. $popts->setEditSection( false );
  365. $parserOutput = $wgParser->parse( $this->mOldtext, $wgTitle, $popts, true, true, $wgOut->getRevisionId() );
  366. $popts->setTidy( $oldTidy );
  367. //only for new?
  368. //$wgOut->addParserOutputNoText( $parserOutput );
  369. $oldHtml = $parserOutput->getText();
  370. wfRunHooks( 'OutputPageBeforeHTML', array( &$wgOut, &$oldHtml ) );
  371. // New revision
  372. if( is_object( $this->mNewRev ) ) {
  373. $wgOut->setRevisionId( $this->mNewRev->getId() );
  374. }
  375. $popts = $wgOut->parserOptions();
  376. $oldTidy = $popts->setTidy( true );
  377. $parserOutput = $wgParser->parse( $this->mNewtext, $wgTitle, $popts, true, true, $wgOut->getRevisionId() );
  378. $popts->setTidy( $oldTidy );
  379. $wgOut->addParserOutputNoText( $parserOutput );
  380. $newHtml = $parserOutput->getText();
  381. wfRunHooks( 'OutputPageBeforeHTML', array( &$wgOut, &$newHtml ) );
  382. unset($parserOutput, $popts);
  383. $differ = new HTMLDiffer(new DelegatingContentHandler($wgOut));
  384. $differ->htmlDiff($oldHtml, $newHtml);
  385. if ( $wgDebugComments ) {
  386. $wgOut->addHTML( "\n<!-- HtmlDiff Debug Output:\n" . HTMLDiffer::getDebugOutput() . " End Debug -->" );
  387. }
  388. wfProfileOut( __METHOD__ );
  389. }
  390. /**
  391. * Show the first revision of an article. Uses normal diff headers in
  392. * contrast to normal "old revision" display style.
  393. */
  394. function showFirstRevision() {
  395. global $wgOut, $wgUser;
  396. wfProfileIn( __METHOD__ );
  397. # Get article text from the DB
  398. #
  399. if ( ! $this->loadNewText() ) {
  400. $t = $this->mTitle->getPrefixedText();
  401. $d = wfMsgExt( 'missingarticle-diff', array( 'escape' ), $this->mOldid, $this->mNewid );
  402. $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
  403. $wgOut->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", $d );
  404. wfProfileOut( __METHOD__ );
  405. return;
  406. }
  407. if ( $this->mNewRev->isCurrent() ) {
  408. $wgOut->setArticleFlag( true );
  409. }
  410. # Check if user is allowed to look at this page. If not, bail out.
  411. #
  412. if ( !$this->mTitle->userCanRead() ) {
  413. $wgOut->loginToUse();
  414. $wgOut->output();
  415. wfProfileOut( __METHOD__ );
  416. throw new MWException("Permission Error: you do not have access to view this page");
  417. }
  418. # Prepare the header box
  419. #
  420. $sk = $wgUser->getSkin();
  421. $next = $this->mTitle->getNextRevisionID( $this->mNewid );
  422. if( !$next ) {
  423. $nextlink = '';
  424. } else {
  425. $nextlink = '<br/>' . $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ),
  426. 'diff=next&oldid=' . $this->mNewid.$this->htmlDiffArgument(), '', '', 'id="differences-nextlink"' );
  427. }
  428. $header = "<div class=\"firstrevisionheader\" style=\"text-align: center\">" .
  429. $sk->revUserTools( $this->mNewRev ) . "<br/>" . $sk->revComment( $this->mNewRev ) . $nextlink . "</div>\n";
  430. $wgOut->addHTML( $header );
  431. $wgOut->setSubtitle( wfMsgExt( 'difference', array( 'parseinline' ) ) );
  432. $wgOut->setRobotPolicy( 'noindex,nofollow' );
  433. wfProfileOut( __METHOD__ );
  434. }
  435. function htmlDiffArgument(){
  436. global $wgEnableHtmlDiff;
  437. if($wgEnableHtmlDiff){
  438. if($this->htmldiff){
  439. return '&htmldiff=1';
  440. }else{
  441. return '&htmldiff=0';
  442. }
  443. }else{
  444. return '';
  445. }
  446. }
  447. /**
  448. * Get the diff text, send it to $wgOut
  449. * Returns false if the diff could not be generated, otherwise returns true
  450. */
  451. function showDiff( $otitle, $ntitle ) {
  452. global $wgOut;
  453. $diff = $this->getDiff( $otitle, $ntitle );
  454. if ( $diff === false ) {
  455. $wgOut->addWikiMsg( 'missing-article', "<nowiki>(fixme, bug)</nowiki>", '' );
  456. return false;
  457. } else {
  458. $this->showDiffStyle();
  459. $wgOut->addHTML( $diff );
  460. return true;
  461. }
  462. }
  463. /**
  464. * Add style sheets and supporting JS for diff display.
  465. */
  466. function showDiffStyle() {
  467. global $wgStylePath, $wgStyleVersion, $wgOut;
  468. $wgOut->addStyle( 'common/diff.css' );
  469. // JS is needed to detect old versions of Mozilla to work around an annoyance bug.
  470. $wgOut->addScript( "<script type=\"text/javascript\" src=\"$wgStylePath/common/diff.js?$wgStyleVersion\"></script>" );
  471. }
  472. /**
  473. * Get complete diff table, including header
  474. *
  475. * @param Title $otitle Old title
  476. * @param Title $ntitle New title
  477. * @return mixed
  478. */
  479. function getDiff( $otitle, $ntitle ) {
  480. $body = $this->getDiffBody();
  481. if ( $body === false ) {
  482. return false;
  483. } else {
  484. $multi = $this->getMultiNotice();
  485. return $this->addHeader( $body, $otitle, $ntitle, $multi );
  486. }
  487. }
  488. /**
  489. * Get the diff table body, without header
  490. *
  491. * @return mixed
  492. */
  493. function getDiffBody() {
  494. global $wgMemc;
  495. wfProfileIn( __METHOD__ );
  496. $this->mCacheHit = true;
  497. // Check if the diff should be hidden from this user
  498. if ( !$this->loadRevisionData() )
  499. return '';
  500. if ( $this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) {
  501. return '';
  502. } else if ( $this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
  503. return '';
  504. } else if ( $this->mOldRev && $this->mNewRev && $this->mOldRev->getID() == $this->mNewRev->getID() ) {
  505. return '';
  506. }
  507. // Cacheable?
  508. $key = false;
  509. if ( $this->mOldid && $this->mNewid ) {
  510. $key = wfMemcKey( 'diff', 'version', MW_DIFF_VERSION, 'oldid', $this->mOldid, 'newid', $this->mNewid );
  511. // Try cache
  512. if ( !$this->mRefreshCache ) {
  513. $difftext = $wgMemc->get( $key );
  514. if ( $difftext ) {
  515. wfIncrStats( 'diff_cache_hit' );
  516. $difftext = $this->localiseLineNumbers( $difftext );
  517. $difftext .= "\n<!-- diff cache key $key -->\n";
  518. wfProfileOut( __METHOD__ );
  519. return $difftext;
  520. }
  521. } // don't try to load but save the result
  522. }
  523. $this->mCacheHit = false;
  524. // Loadtext is permission safe, this just clears out the diff
  525. if ( !$this->loadText() ) {
  526. wfProfileOut( __METHOD__ );
  527. return false;
  528. }
  529. $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext );
  530. // Save to cache for 7 days
  531. if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) {
  532. wfIncrStats( 'diff_uncacheable' );
  533. } else if ( $key !== false && $difftext !== false ) {
  534. wfIncrStats( 'diff_cache_miss' );
  535. $wgMemc->set( $key, $difftext, 7*86400 );
  536. } else {
  537. wfIncrStats( 'diff_uncacheable' );
  538. }
  539. // Replace line numbers with the text in the user's language
  540. if ( $difftext !== false ) {
  541. $difftext = $this->localiseLineNumbers( $difftext );
  542. }
  543. wfProfileOut( __METHOD__ );
  544. return $difftext;
  545. }
  546. /**
  547. * Generate a diff, no caching
  548. * $otext and $ntext must be already segmented
  549. */
  550. function generateDiffBody( $otext, $ntext ) {
  551. global $wgExternalDiffEngine, $wgContLang;
  552. $otext = str_replace( "\r\n", "\n", $otext );
  553. $ntext = str_replace( "\r\n", "\n", $ntext );
  554. if ( $wgExternalDiffEngine == 'wikidiff' ) {
  555. # For historical reasons, external diff engine expects
  556. # input text to be HTML-escaped already
  557. $otext = htmlspecialchars ( $wgContLang->segmentForDiff( $otext ) );
  558. $ntext = htmlspecialchars ( $wgContLang->segmentForDiff( $ntext ) );
  559. if( !function_exists( 'wikidiff_do_diff' ) ) {
  560. dl('php_wikidiff.so');
  561. }
  562. return $wgContLang->unsegementForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ) .
  563. $this->debug( 'wikidiff1' );
  564. }
  565. if ( $wgExternalDiffEngine == 'wikidiff2' ) {
  566. # Better external diff engine, the 2 may some day be dropped
  567. # This one does the escaping and segmenting itself
  568. if ( !function_exists( 'wikidiff2_do_diff' ) ) {
  569. wfProfileIn( __METHOD__ . "-dl" );
  570. @dl('php_wikidiff2.so');
  571. wfProfileOut( __METHOD__ . "-dl" );
  572. }
  573. if ( function_exists( 'wikidiff2_do_diff' ) ) {
  574. wfProfileIn( 'wikidiff2_do_diff' );
  575. $text = wikidiff2_do_diff( $otext, $ntext, 2 );
  576. $text .= $this->debug( 'wikidiff2' );
  577. wfProfileOut( 'wikidiff2_do_diff' );
  578. return $text;
  579. }
  580. }
  581. if ( $wgExternalDiffEngine != 'wikidiff3' && $wgExternalDiffEngine !== false ) {
  582. # Diff via the shell
  583. global $wgTmpDirectory;
  584. $tempName1 = tempnam( $wgTmpDirectory, 'diff_' );
  585. $tempName2 = tempnam( $wgTmpDirectory, 'diff_' );
  586. $tempFile1 = fopen( $tempName1, "w" );
  587. if ( !$tempFile1 ) {
  588. wfProfileOut( __METHOD__ );
  589. return false;
  590. }
  591. $tempFile2 = fopen( $tempName2, "w" );
  592. if ( !$tempFile2 ) {
  593. wfProfileOut( __METHOD__ );
  594. return false;
  595. }
  596. fwrite( $tempFile1, $otext );
  597. fwrite( $tempFile2, $ntext );
  598. fclose( $tempFile1 );
  599. fclose( $tempFile2 );
  600. $cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 );
  601. wfProfileIn( __METHOD__ . "-shellexec" );
  602. $difftext = wfShellExec( $cmd );
  603. $difftext .= $this->debug( "external $wgExternalDiffEngine" );
  604. wfProfileOut( __METHOD__ . "-shellexec" );
  605. unlink( $tempName1 );
  606. unlink( $tempName2 );
  607. return $difftext;
  608. }
  609. # Native PHP diff
  610. $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) );
  611. $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) );
  612. $diffs = new Diff( $ota, $nta );
  613. $formatter = new TableDiffFormatter();
  614. return $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ) .
  615. $this->debug();
  616. }
  617. /**
  618. * Generate a debug comment indicating diff generating time,
  619. * server node, and generator backend.
  620. */
  621. protected function debug( $generator="internal" ) {
  622. global $wgShowHostnames;
  623. $data = array( $generator );
  624. if( $wgShowHostnames ) {
  625. $data[] = wfHostname();
  626. }
  627. $data[] = wfTimestamp( TS_DB );
  628. return "<!-- diff generator: " .
  629. implode( " ",
  630. array_map(
  631. "htmlspecialchars",
  632. $data ) ) .
  633. " -->\n";
  634. }
  635. /**
  636. * Replace line numbers with the text in the user's language
  637. */
  638. function localiseLineNumbers( $text ) {
  639. return preg_replace_callback( '/<!--LINE (\d+)-->/',
  640. array( &$this, 'localiseLineNumbersCb' ), $text );
  641. }
  642. function localiseLineNumbersCb( $matches ) {
  643. global $wgLang;
  644. return wfMsgExt( 'lineno', array (), $wgLang->formatNum( $matches[1] ) );
  645. }
  646. /**
  647. * If there are revisions between the ones being compared, return a note saying so.
  648. */
  649. function getMultiNotice() {
  650. if ( !is_object($this->mOldRev) || !is_object($this->mNewRev) )
  651. return '';
  652. if( !$this->mOldPage->equals( $this->mNewPage ) ) {
  653. // Comparing two different pages? Count would be meaningless.
  654. return '';
  655. }
  656. $oldid = $this->mOldRev->getId();
  657. $newid = $this->mNewRev->getId();
  658. if ( $oldid > $newid ) {
  659. $tmp = $oldid; $oldid = $newid; $newid = $tmp;
  660. }
  661. $n = $this->mTitle->countRevisionsBetween( $oldid, $newid );
  662. if ( !$n )
  663. return '';
  664. return wfMsgExt( 'diff-multi', array( 'parseinline' ), $n );
  665. }
  666. /**
  667. * Add the header to a diff body
  668. */
  669. static function addHeader( $diff, $otitle, $ntitle, $multi = '' ) {
  670. $header = "
  671. <table class='diff'>
  672. <col class='diff-marker' />
  673. <col class='diff-content' />
  674. <col class='diff-marker' />
  675. <col class='diff-content' />
  676. <tr valign='top'>
  677. <td colspan='2' class='diff-otitle'>{$otitle}</td>
  678. <td colspan='2' class='diff-ntitle'>{$ntitle}</td>
  679. </tr>
  680. ";
  681. if ( $multi != '' )
  682. $header .= "<tr><td colspan='4' align='center' class='diff-multi'>{$multi}</td></tr>";
  683. return $header . $diff . "</table>";
  684. }
  685. /**
  686. * Use specified text instead of loading from the database
  687. */
  688. function setText( $oldText, $newText ) {
  689. $this->mOldtext = $oldText;
  690. $this->mNewtext = $newText;
  691. $this->mTextLoaded = 2;
  692. $this->mRevisionsLoaded = true;
  693. }
  694. /**
  695. * Load revision metadata for the specified articles. If newid is 0, then compare
  696. * the old article in oldid to the current article; if oldid is 0, then
  697. * compare the current article to the immediately previous one (ignoring the
  698. * value of newid).
  699. *
  700. * If oldid is false, leave the corresponding revision object set
  701. * to false. This is impossible via ordinary user input, and is provided for
  702. * API convenience.
  703. */
  704. function loadRevisionData() {
  705. global $wgLang, $wgUser;
  706. if ( $this->mRevisionsLoaded ) {
  707. return true;
  708. } else {
  709. // Whether it succeeds or fails, we don't want to try again
  710. $this->mRevisionsLoaded = true;
  711. }
  712. // Load the new revision object
  713. $this->mNewRev = $this->mNewid
  714. ? Revision::newFromId( $this->mNewid )
  715. : Revision::newFromTitle( $this->mTitle );
  716. if( !$this->mNewRev instanceof Revision )
  717. return false;
  718. // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
  719. $this->mNewid = $this->mNewRev->getId();
  720. // Check if page is editable
  721. $editable = $this->mNewRev->getTitle()->userCan( 'edit' );
  722. // Set assorted variables
  723. $timestamp = $wgLang->timeanddate( $this->mNewRev->getTimestamp(), true );
  724. $this->mNewPage = $this->mNewRev->getTitle();
  725. if( $this->mNewRev->isCurrent() ) {
  726. $newLink = $this->mNewPage->escapeLocalUrl( 'oldid=' . $this->mNewid );
  727. $this->mPagetitle = wfMsgHTML( 'currentrev-asof', $timestamp );
  728. $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit' );
  729. $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>";
  730. $this->mNewtitle .= " (<a href='$newEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
  731. } else {
  732. $newLink = $this->mNewPage->escapeLocalUrl( 'oldid=' . $this->mNewid );
  733. $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mNewid );
  734. $this->mPagetitle = wfMsgHTML( 'revisionasof', $timestamp );
  735. $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>";
  736. $this->mNewtitle .= " (<a href='$newEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
  737. }
  738. if ( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
  739. $this->mNewtitle = "<span class='history-deleted'>{$this->mPagetitle}</span>";
  740. } else if ( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
  741. $this->mNewtitle = '<span class="history-deleted">'.$this->mNewtitle.'</span>';
  742. }
  743. // Load the old revision object
  744. $this->mOldRev = false;
  745. if( $this->mOldid ) {
  746. $this->mOldRev = Revision::newFromId( $this->mOldid );
  747. } elseif ( $this->mOldid === 0 ) {
  748. $rev = $this->mNewRev->getPrevious();
  749. if( $rev ) {
  750. $this->mOldid = $rev->getId();
  751. $this->mOldRev = $rev;
  752. } else {
  753. // No previous revision; mark to show as first-version only.
  754. $this->mOldid = false;
  755. $this->mOldRev = false;
  756. }
  757. }/* elseif ( $this->mOldid === false ) leave mOldRev false; */
  758. if( is_null( $this->mOldRev ) ) {
  759. return false;
  760. }
  761. if ( $this->mOldRev ) {
  762. $this->mOldPage = $this->mOldRev->getTitle();
  763. $t = $wgLang->timeanddate( $this->mOldRev->getTimestamp(), true );
  764. $oldLink = $this->mOldPage->escapeLocalUrl( 'oldid=' . $this->mOldid );
  765. $oldEdit = $this->mOldPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mOldid );
  766. $this->mOldPagetitle = htmlspecialchars( wfMsg( 'revisionasof', $t ) );
  767. $this->mOldtitle = "<a href='$oldLink'>{$this->mOldPagetitle}</a>"
  768. . " (<a href='$oldEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
  769. // Add an "undo" link
  770. $newUndo = $this->mNewPage->escapeLocalUrl( 'action=edit&undoafter=' . $this->mOldid . '&undo=' . $this->mNewid);
  771. $htmlLink = htmlspecialchars( wfMsg( 'editundo' ) );
  772. $htmlTitle = $wgUser->getSkin()->tooltip( 'undo' );
  773. if( $editable && !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
  774. $this->mNewtitle .= " (<a href='$newUndo' $htmlTitle>" . $htmlLink . "</a>)";
  775. }
  776. if( !$this->mOldRev->userCan( Revision::DELETED_TEXT ) ) {
  777. $this->mOldtitle = '<span class="history-deleted">' . $this->mOldPagetitle . '</span>';
  778. } else if( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) {
  779. $this->mOldtitle = '<span class="history-deleted">' . $this->mOldtitle . '</span>';
  780. }
  781. }
  782. return true;
  783. }
  784. /**
  785. * Load the text of the revisions, as well as revision data.
  786. */
  787. function loadText() {
  788. if ( $this->mTextLoaded == 2 ) {
  789. return true;
  790. } else {
  791. // Whether it succeeds or fails, we don't want to try again
  792. $this->mTextLoaded = 2;
  793. }
  794. if ( !$this->loadRevisionData() ) {
  795. return false;
  796. }
  797. if ( $this->mOldRev ) {
  798. $this->mOldtext = $this->mOldRev->getText( Revision::FOR_THIS_USER );
  799. if ( $this->mOldtext === false ) {
  800. return false;
  801. }
  802. }
  803. if ( $this->mNewRev ) {
  804. $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
  805. if ( $this->mNewtext === false ) {
  806. return false;
  807. }
  808. }
  809. return true;
  810. }
  811. /**
  812. * Load the text of the new revision, not the old one
  813. */
  814. function loadNewText() {
  815. if ( $this->mTextLoaded >= 1 ) {
  816. return true;
  817. } else {
  818. $this->mTextLoaded = 1;
  819. }
  820. if ( !$this->loadRevisionData() ) {
  821. return false;
  822. }
  823. $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
  824. return true;
  825. }
  826. }
  827. // A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3)
  828. //
  829. // Copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
  830. // You may copy this code freely under the conditions of the GPL.
  831. //
  832. define('USE_ASSERTS', function_exists('assert'));
  833. /**
  834. * @todo document
  835. * @private
  836. * @ingroup DifferenceEngine
  837. */
  838. class _DiffOp {
  839. var $type;
  840. var $orig;
  841. var $closing;
  842. function reverse() {
  843. trigger_error('pure virtual', E_USER_ERROR);
  844. }
  845. function norig() {
  846. return $this->orig ? sizeof($this->orig) : 0;
  847. }
  848. function nclosing() {
  849. return $this->closing ? sizeof($this->closing) : 0;
  850. }
  851. }
  852. /**
  853. * @todo document
  854. * @private
  855. * @ingroup DifferenceEngine
  856. */
  857. class _DiffOp_Copy extends _DiffOp {
  858. var $type = 'copy';
  859. function _DiffOp_Copy ($orig, $closing = false) {
  860. if (!is_array($closing))
  861. $closing = $orig;
  862. $this->orig = $orig;
  863. $this->closing = $closing;
  864. }
  865. function reverse() {
  866. return new _DiffOp_Copy($this->closing, $this->orig);
  867. }
  868. }
  869. /**
  870. * @todo document
  871. * @private
  872. * @ingroup DifferenceEngine
  873. */
  874. class _DiffOp_Delete extends _DiffOp {
  875. var $type = 'delete';
  876. function _DiffOp_Delete ($lines) {
  877. $this->orig = $lines;
  878. $this->closing = false;
  879. }
  880. function reverse() {
  881. return new _DiffOp_Add($this->orig);
  882. }
  883. }
  884. /**
  885. * @todo document
  886. * @private
  887. * @ingroup DifferenceEngine
  888. */
  889. class _DiffOp_Add extends _DiffOp {
  890. var $type = 'add';
  891. function _DiffOp_Add ($lines) {
  892. $this->closing = $lines;
  893. $this->orig = false;
  894. }
  895. function reverse() {
  896. return new _DiffOp_Delete($this->closing);
  897. }
  898. }
  899. /**
  900. * @todo document
  901. * @private
  902. * @ingroup DifferenceEngine
  903. */
  904. class _DiffOp_Change extends _DiffOp {
  905. var $type = 'change';
  906. function _DiffOp_Change ($orig, $closing) {
  907. $this->orig = $orig;
  908. $this->closing = $closing;
  909. }
  910. function reverse() {
  911. return new _DiffOp_Change($this->closing, $this->orig);
  912. }
  913. }
  914. /**
  915. * Class used internally by Diff to actually compute the diffs.
  916. *
  917. * The algorithm used here is mostly lifted from the perl module
  918. * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
  919. * http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
  920. *
  921. * More ideas are taken from:
  922. * http://www.ics.uci.edu/~eppstein/161/960229.html
  923. *
  924. * Some ideas are (and a bit of code) are from from analyze.c, from GNU
  925. * diffutils-2.7, which can be found at:
  926. * ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
  927. *
  928. * closingly, some ideas (subdivision by NCHUNKS > 2, and some optimizations)
  929. * are my own.
  930. *
  931. * Line length limits for robustness added by Tim Starling, 2005-08-31
  932. * Alternative implementation added by Guy Van den Broeck, 2008-07-30
  933. *
  934. * @author Geoffrey T. Dairiki, Tim Starling, Guy Van den Broeck
  935. * @private
  936. * @ingroup DifferenceEngine
  937. */
  938. class _DiffEngine {
  939. const MAX_XREF_LENGTH = 10000;
  940. function diff ($from_lines, $to_lines){
  941. wfProfileIn( __METHOD__ );
  942. // Diff and store locally
  943. $this->diff_local($from_lines, $to_lines);
  944. // Merge edits when possible
  945. $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
  946. $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
  947. // Compute the edit operations.
  948. $n_from = sizeof($from_lines);
  949. $n_to = sizeof($to_lines);
  950. $edits = array();
  951. $xi = $yi = 0;
  952. while ($xi < $n_from || $yi < $n_to) {
  953. USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]);
  954. USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]);
  955. // Skip matching "snake".
  956. $copy = array();
  957. while ( $xi < $n_from && $yi < $n_to
  958. && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
  959. $copy[] = $from_lines[$xi++];
  960. ++$yi;
  961. }
  962. if ($copy)
  963. $edits[] = new _DiffOp_Copy($copy);
  964. // Find deletes & adds.
  965. $delete = array();
  966. while ($xi < $n_from && $this->xchanged[$xi])
  967. $delete[] = $from_lines[$xi++];
  968. $add = array();
  969. while ($yi < $n_to && $this->ychanged[$yi])
  970. $add[] = $to_lines[$yi++];
  971. if ($delete && $add)
  972. $edits[] = new _DiffOp_Change($delete, $add);
  973. elseif ($delete)
  974. $edits[] = new _DiffOp_Delete($delete);
  975. elseif ($add)
  976. $edits[] = new _DiffOp_Add($add);
  977. }
  978. wfProfileOut( __METHOD__ );
  979. return $edits;
  980. }
  981. function diff_local ($from_lines, $to_lines) {
  982. global $wgExternalDiffEngine;
  983. wfProfileIn( __METHOD__);
  984. if($wgExternalDiffEngine == 'wikidiff3'){
  985. // wikidiff3
  986. $wikidiff3 = new WikiDiff3();
  987. $wikidiff3->diff($from_lines, $to_lines);
  988. $this->xchanged = $wikidiff3->removed;
  989. $this->ychanged = $wikidiff3->added;
  990. unset($wikidiff3);
  991. }else{
  992. // old diff
  993. $n_from = sizeof($from_lines);
  994. $n_to = sizeof($to_lines);
  995. $this->xchanged = $this->ychanged = array();
  996. $this->xv = $this->yv = array();
  997. $this->xind = $this->yind = array();
  998. unset($this->seq);
  999. unset($this->in_seq);
  1000. unset($this->lcs);
  1001. // Skip leading common lines.
  1002. for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
  1003. if ($from_lines[$skip] !== $to_lines[$skip])
  1004. break;
  1005. $this->xchanged[$skip] = $this->ychanged[$skip] = false;
  1006. }
  1007. // Skip trailing common lines.
  1008. $xi = $n_from; $yi = $n_to;
  1009. for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
  1010. if ($from_lines[$xi] !== $to_lines[$yi])
  1011. break;
  1012. $this->xchanged[$xi] = $this->ychanged[$yi] = false;
  1013. }
  1014. // Ignore lines which do not exist in both files.
  1015. for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
  1016. $xhash[$this->_line_hash($from_lines[$xi])] = 1;
  1017. }
  1018. for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
  1019. $line = $to_lines[$yi];
  1020. if ( ($this->ychanged[$yi] = empty($xhash[$this->_line_hash($line)])) )
  1021. continue;
  1022. $yhash[$this->_line_hash($line)] = 1;
  1023. $this->yv[] = $line;
  1024. $this->yind[] = $yi;
  1025. }
  1026. for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
  1027. $line = $from_lines[$xi];
  1028. if ( ($this->xchanged[$xi] = empty($yhash[$this->_line_hash($line)])) )
  1029. continue;
  1030. $this->xv[] = $line;
  1031. $this->xind[] = $xi;
  1032. }
  1033. // Find the LCS.
  1034. $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
  1035. }
  1036. wfProfileOut( __METHOD__ );
  1037. }
  1038. /**
  1039. * Returns the whole line if it's small enough, or the MD5 hash otherwise
  1040. */
  1041. function _line_hash( $line ) {
  1042. if ( strlen( $line ) > self::MAX_XREF_LENGTH ) {
  1043. return md5( $line );
  1044. } else {
  1045. return $line;
  1046. }
  1047. }
  1048. /* Divide the Largest Common Subsequence (LCS) of the sequences
  1049. * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
  1050. * sized segments.
  1051. *
  1052. * Returns (LCS, PTS). LCS is the length of the LCS. PTS is an
  1053. * array of NCHUNKS+1 (X, Y) indexes giving the diving points between
  1054. * sub sequences. The first sub-sequence is contained in [X0, X1),
  1055. * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on. Note
  1056. * that (X0, Y0) == (XOFF, YOFF) and
  1057. * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
  1058. *
  1059. * This function assumes that the first lines of the specified portions
  1060. * of the two files do not match, and likewise that the last lines do not
  1061. * match. The caller must trim matching lines from the beginning and end
  1062. * of the portions it is going to specify.
  1063. */
  1064. function _diag ($xoff, $xlim, $yoff, $ylim, $nchunks) {
  1065. $flip = false;
  1066. if ($xlim - $xoff > $ylim - $yoff) {
  1067. // Things seems faster (I'm not sure I understand why)
  1068. // when the shortest sequence in X.
  1069. $flip = true;
  1070. list ($xoff, $xlim, $yoff, $ylim)
  1071. = array( $yoff, $ylim, $xoff, $xlim);
  1072. }
  1073. if ($flip)
  1074. for ($i = $ylim - 1; $i >= $yoff; $i--)
  1075. $ymatches[$this->xv[$i]][] = $i;
  1076. else
  1077. for ($i = $ylim - 1; $i >= $yoff; $i--)
  1078. $ymatches[$this->yv[$i]][] = $i;
  1079. $this->lcs = 0;
  1080. $this->seq[0]= $yoff - 1;
  1081. $this->in_seq = array();
  1082. $ymids[0] = array();
  1083. $numer = $xlim - $xoff + $nchunks - 1;
  1084. $x = $xoff;
  1085. for ($chunk = 0; $chunk < $nchunks; $chunk++) {
  1086. if ($chunk > 0)
  1087. for ($i = 0; $i <= $this->lcs; $i++)
  1088. $ymids[$i][$chunk-1] = $this->seq[$i];
  1089. $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
  1090. for ( ; $x < $x1; $x++) {
  1091. $line = $flip ? $this->yv[$x] : $this->xv[$x];
  1092. if (empty($ymatches[$line]))
  1093. continue;
  1094. $matches = $ymatches[$line];
  1095. reset($matches);
  1096. while (list ($junk, $y) = each($matches))
  1097. if (empty($this->in_seq[$y])) {
  1098. $k = $this->_lcs_pos($y);
  1099. USE_ASSERTS && assert($k > 0);
  1100. $ymids[$k] = $ymids[$k-1];
  1101. break;
  1102. }
  1103. while (list ( /* $junk */, $y) = each($matches)) {
  1104. if ($y > $this->seq[$k-1]) {
  1105. USE_ASSERTS && assert($y < $this->seq[$k]);
  1106. // Optimization: this is a common case:
  1107. // next match is just replacing previous match.
  1108. $this->in_seq[$this->seq[$k]] = false;
  1109. $this->seq[$k] = $y;
  1110. $this->in_seq[$y] = 1;
  1111. } else if (empty($this->in_seq[$y])) {
  1112. $k = $this->_lcs_pos($y);
  1113. USE_ASSERTS && assert($k > 0);
  1114. $ymids[$k] = $ymids[$k-1];
  1115. }
  1116. }
  1117. }
  1118. }
  1119. $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
  1120. $ymid = $ymids[$this->lcs];
  1121. for ($n = 0; $n < $nchunks - 1; $n++) {
  1122. $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
  1123. $y1 = $ymid[$n] + 1;
  1124. $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
  1125. }
  1126. $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
  1127. return array($this->lcs, $seps);
  1128. }
  1129. function _lcs_pos ($ypos) {
  1130. $end = $this->lcs;
  1131. if ($end == 0 || $ypos > $this->seq[$end]) {
  1132. $this->seq[++$this->lcs] = $ypos;
  1133. $this->in_seq[$ypos] = 1;
  1134. return $this->lcs;
  1135. }
  1136. $beg = 1;
  1137. while ($beg < $end) {
  1138. $mid = (int)(($beg + $end) / 2);
  1139. if ( $ypos > $this->seq[$mid] )
  1140. $beg = $mid + 1;
  1141. else
  1142. $end = $mid;
  1143. }
  1144. USE_ASSERTS && assert($ypos != $this->seq[$end]);
  1145. $this->in_seq[$this->seq[$end]] = false;
  1146. $this->seq[$end] = $ypos;
  1147. $this->in_seq[$ypos] = 1;
  1148. return $end;
  1149. }
  1150. /* Find LCS of two sequences.
  1151. *
  1152. * The results are recorded in the vectors $this->{x,y}changed[], by
  1153. * storing a 1 in the element for each line that is an insertion
  1154. * or deletion (ie. is not in the LCS).
  1155. *
  1156. * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
  1157. *
  1158. * Note that XLIM, YLIM are exclusive bounds.
  1159. * All line numbers are origin-0 and discarded lines are not counted.
  1160. */
  1161. function _compareseq ($xoff, $xlim, $yoff, $ylim) {
  1162. // Slide down the bottom initial diagonal.
  1163. while ($xoff < $xlim && $yoff < $ylim
  1164. && $this->xv[$xoff] == $this->yv[$yoff]) {
  1165. ++$xoff;
  1166. ++$yoff;
  1167. }
  1168. // Slide up the top initial diagonal.
  1169. while ($xlim > $xoff && $ylim > $yoff
  1170. && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
  1171. --$xlim;
  1172. --$ylim;
  1173. }
  1174. if ($xoff == $xlim || $yoff == $ylim)
  1175. $lcs = 0;
  1176. else {
  1177. // This is ad hoc but seems to work well.
  1178. //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
  1179. //$nchunks = max(2,min(8,(int)$nchunks));
  1180. $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
  1181. list ($lcs, $seps)
  1182. = $this->_diag($xoff,$xlim,$yoff, $ylim,$nchunks);
  1183. }
  1184. if ($lcs == 0) {
  1185. // X and Y sequences have no common subsequence:
  1186. // mark all changed.
  1187. while ($yoff < $ylim)
  1188. $this->ychanged[$this->yind[$yoff++]] = 1;
  1189. while ($xoff < $xlim)
  1190. $this->xchanged[$this->xind[$xoff++]] = 1;
  1191. } else {
  1192. // Use the partitions to split this problem into subproblems.
  1193. reset($seps);
  1194. $pt1 = $seps[0];
  1195. while ($pt2 = next($seps)) {
  1196. $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
  1197. $pt1 = $pt2;
  1198. }
  1199. }
  1200. }
  1201. /* Adjust inserts/deletes of identical lines to join changes
  1202. * as much as possible.
  1203. *
  1204. * We do something when a run of changed lines include a
  1205. * line at one end and has an excluded, identical line at the other.
  1206. * We are free to choose which identical line is included.
  1207. * `compareseq' usually chooses the one at the beginning,
  1208. * but usually it is cleaner to consider the following identical line
  1209. * to be the "change".
  1210. *
  1211. * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
  1212. */
  1213. function _shift_boundaries ($lines, &$changed, $other_changed) {
  1214. wfProfileIn( __METHOD__ );
  1215. $i = 0;
  1216. $j = 0;
  1217. USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
  1218. $len = sizeof($lines);
  1219. $other_len = sizeof($other_changed);
  1220. while (1) {
  1221. /*
  1222. * Scan forwards to find beginning of another run of changes.
  1223. * Also keep track of the corresponding point in the other file.
  1224. *
  1225. * Throughout this code, $i and $j are adjusted together so that
  1226. * the first $i elements of $changed and the first $j elements
  1227. * of $other_changed both contain the same number of zeros
  1228. * (unchanged lines).
  1229. * Furthermore, $j is always kept so that $j == $other_len or
  1230. * $other_changed[$j] == false.
  1231. */
  1232. while ($j < $other_len && $other_changed[$j])
  1233. $j++;
  1234. while ($i < $len && ! $changed[$i]) {
  1235. USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
  1236. $i++; $j++;
  1237. while ($j < $other_len && $other_changed[$j])
  1238. $j++;
  1239. }
  1240. if ($i == $len)
  1241. break;
  1242. $start = $i;
  1243. // Find the end of this run of changes.
  1244. while (++$i < $len && $changed[$i])
  1245. continue;
  1246. do {
  1247. /*
  1248. * Record the length of this run of changes, so that
  1249. * we can later determine whether the run has grown.
  1250. */
  1251. $runlength = $i - $start;
  1252. /*
  1253. * Move the changed region back, so long as the
  1254. * previous unchanged line matches the last changed one.
  1255. * This merges with previous changed regions.
  1256. */
  1257. while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
  1258. $changed[--$start] = 1;
  1259. $changed[--$i] = false;
  1260. while ($start > 0 && $changed[$start - 1])
  1261. $start--;
  1262. USE_ASSERTS && assert('$j > 0');
  1263. while ($other_changed[--$j])
  1264. continue;
  1265. USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
  1266. }
  1267. /*
  1268. * Set CORRESPONDING to the end of the changed run, at the last
  1269. * point where it corresponds to a changed run in the other file.
  1270. * CORRESPONDING == LEN means no such point has been found.
  1271. */
  1272. $corresponding = $j < $other_len ? $i : $len;
  1273. /*
  1274. * Move the changed region forward, so long as the
  1275. * first changed line matches the following unchanged one.
  1276. * This merges with following changed regions.
  1277. * Do this second, so that if there are no merges,
  1278. * the changed region is moved forward as far as possible.
  1279. */
  1280. while ($i < $len && $lines[$start] == $lines[$i]) {
  1281. $changed[$start++] = false;
  1282. $changed[$i++] = 1;
  1283. while ($i < $len && $changed[$i])
  1284. $i++;
  1285. USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
  1286. $j++;
  1287. if ($j < $other_len && $other_changed[$j]) {
  1288. $corresponding = $i;
  1289. while ($j < $other_len && $other_changed[$j])
  1290. $j++;
  1291. }
  1292. }
  1293. } while ($runlength != $i - $start);
  1294. /*
  1295. * If possible, move the fully-merged run of changes
  1296. * back to a corresponding run in the other file.
  1297. */
  1298. while ($corresponding < $i) {
  1299. $changed[--$start] = 1;
  1300. $changed[--$i] = 0;
  1301. USE_ASSERTS && assert('$j > 0');
  1302. while ($other_changed[--$j])
  1303. continue;
  1304. USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
  1305. }
  1306. }
  1307. wfProfileOut( __METHOD__ );
  1308. }
  1309. }
  1310. /**
  1311. * Class representing a 'diff' between two sequences of strings.
  1312. * @todo document
  1313. * @private
  1314. * @ingroup DifferenceEngine
  1315. */
  1316. class Diff
  1317. {
  1318. var $edits;
  1319. /**
  1320. * Constructor.
  1321. * Computes diff between sequences of strings.
  1322. *
  1323. * @param $from_lines array An array of strings.
  1324. * (Typically these are lines from a file.)
  1325. * @param $to_lines array An array of strings.
  1326. */
  1327. function Diff($from_lines, $to_lines) {
  1328. $eng = new _DiffEngine;
  1329. $this->edits = $eng->diff($from_lines, $to_lines);
  1330. //$this->_check($from_lines, $to_lines);
  1331. }
  1332. /**
  1333. * Compute reversed Diff.
  1334. *
  1335. * SYNOPSIS:
  1336. *
  1337. * $diff = new Diff($lines1, $lines2);
  1338. * $rev = $diff->reverse();
  1339. * @return object A Diff object representing the inverse of the
  1340. * original diff.
  1341. */
  1342. function reverse () {
  1343. $rev = $this;
  1344. $rev->edits = array();
  1345. foreach ($this->edits as $edit) {
  1346. $rev->edits[] = $edit->reverse();
  1347. }
  1348. return $rev;
  1349. }
  1350. /**
  1351. * Check for empty diff.
  1352. *
  1353. * @return bool True iff two sequences were identical.
  1354. */
  1355. function isEmpty () {
  1356. foreach ($this->edits as $edit) {
  1357. if ($edit->type != 'copy')
  1358. return false;
  1359. }
  1360. return true;
  1361. }
  1362. /**
  1363. * Compute the length of the Longest Common Subsequence (LCS).
  1364. *
  1365. * This is mostly for diagnostic purposed.
  1366. *
  1367. * @return int The length of the LCS.
  1368. */
  1369. function lcs () {
  1370. $lcs = 0;
  1371. foreach ($this->edits as $edit) {
  1372. if ($edit->type == 'copy')
  1373. $lcs += sizeof($edit->orig);
  1374. }
  1375. return $lcs;
  1376. }
  1377. /**
  1378. * Get the original set of lines.
  1379. *
  1380. * This reconstructs the $from_lines parameter passed to the
  1381. * constructor.
  1382. *
  1383. * @return array The original sequence of strings.
  1384. */
  1385. function orig() {
  1386. $lines = array();
  1387. foreach ($this->edits as $edit) {
  1388. if ($edit->orig)
  1389. array_splice($lines, sizeof($lines), 0, $edit->orig);
  1390. }
  1391. return $lines;
  1392. }
  1393. /**
  1394. * Get the closing set of lines.
  1395. *
  1396. * This reconstructs the $to_lines parameter passed to the
  1397. * constructor.
  1398. *
  1399. * @return array The sequence of strings.
  1400. */
  1401. function closing() {
  1402. $lines = array();
  1403. foreach ($this->edits as $edit) {
  1404. if ($edit->closing)
  1405. array_splice($lines, sizeof($lines), 0, $edit->closing);
  1406. }
  1407. return $lines;
  1408. }
  1409. /**
  1410. * Check a Diff for validity.
  1411. *
  1412. * This is here only for debugging purposes.
  1413. */
  1414. function _check ($from_lines, $to_lines) {
  1415. wfProfileIn( __METHOD__ );
  1416. if (serialize($from_lines) != serialize($this->orig()))
  1417. trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
  1418. if (serialize($to_lines) != serialize($this->closing()))
  1419. trigger_error("Reconstructed closing doesn't match", E_USER_ERROR);
  1420. $rev = $this->reverse();
  1421. if (serialize($to_lines) != serialize($rev->orig()))
  1422. trigger_error("Reversed original doesn't match", E_USER_ERROR);
  1423. if (serialize($from_lines) != serialize($rev->closing()))
  1424. trigger_error("Reversed closing doesn't match", E_USER_ERROR);
  1425. $prevtype = 'none';
  1426. foreach ($this->edits as $edit) {
  1427. if ( $prevtype == $edit->type )
  1428. trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
  1429. $prevtype = $edit->type;
  1430. }
  1431. $lcs = $this->lcs();
  1432. trigger_error('Diff okay: LCS = '.$lcs, E_USER_NOTICE);
  1433. wfProfileOut( __METHOD__ );
  1434. }
  1435. }
  1436. /**
  1437. * @todo document, bad name.
  1438. * @private
  1439. * @ingroup DifferenceEngine
  1440. */
  1441. class MappedDiff extends Diff
  1442. {
  1443. /**
  1444. * Constructor.
  1445. *
  1446. * Computes diff between sequences of strings.
  1447. *
  1448. * This can be used to compute things like
  1449. * case-insensitve diffs, or diffs which ignore
  1450. * changes in white-space.
  1451. *
  1452. * @param $from_lines array An array of strings.
  1453. * (Typically these are lines from a file.)
  1454. *
  1455. * @param $to_lines array An array of strings.
  1456. *
  1457. * @param $mapped_from_lines array This array should
  1458. * have the same size number of elements as $from_lines.
  1459. * The elements in $mapped_from_lines and
  1460. * $mapped_to_lines are what is actually compared
  1461. * when computing the diff.
  1462. *
  1463. * @param $mapped_to_lines array This array should
  1464. * have the same number of elements as $to_lines.
  1465. */
  1466. function MappedDiff($from_lines, $to_lines,
  1467. $mapped_from_lines, $mapped_to_lines) {
  1468. wfProfileIn( __METHOD__ );
  1469. assert(sizeof($from_lines) == sizeof($mapped_from_lines));
  1470. assert(sizeof($to_lines) == sizeof($mapped_to_lines));
  1471. $this->Diff($mapped_from_lines, $mapped_to_lines);
  1472. $xi = $yi = 0;
  1473. for ($i = 0; $i < sizeof($this->edits); $i++) {
  1474. $orig = &$this->edits[$i]->orig;
  1475. if (is_array($orig)) {
  1476. $orig = array_slice($from_lines, $xi, sizeof($orig));
  1477. $xi += sizeof($orig);
  1478. }
  1479. $closing = &$this->edits[$i]->closing;
  1480. if (is_array($closing)) {
  1481. $closing = array_slice($to_lines, $yi, sizeof($closing));
  1482. $yi += sizeof($closing);
  1483. }
  1484. }
  1485. wfProfileOut( __METHOD__ );
  1486. }
  1487. }
  1488. /**
  1489. * A class to format Diffs
  1490. *
  1491. * This class formats the diff in classic diff format.
  1492. * It is intended that this class be customized via inheritance,
  1493. * to obtain fancier outputs.
  1494. * @todo document
  1495. * @private
  1496. * @ingroup DifferenceEngine
  1497. */
  1498. class DiffFormatter {
  1499. /**
  1500. * Number of leading context "lines" to preserve.
  1501. *
  1502. * This should be left at zero for this class, but subclasses
  1503. * may want to set this to other values.
  1504. */
  1505. var $leading_context_lines = 0;
  1506. /**
  1507. * Number of trailing context "lines" to preserve.
  1508. *
  1509. * This should be left at zero for this class, but subclasses
  1510. * may want to set this to other values.
  1511. */
  1512. var $trailing_context_lines = 0;
  1513. /**
  1514. * Format a diff.
  1515. *
  1516. * @param $diff object A Diff object.
  1517. * @return string The formatted output.
  1518. */
  1519. function format($diff) {
  1520. wfProfileIn( __METHOD__ );
  1521. $xi = $yi = 1;
  1522. $block = false;
  1523. $context = array();
  1524. $nlead = $this->leading_context_lines;
  1525. $ntrail = $this->trailing_context_lines;
  1526. $this->_start_diff();
  1527. foreach ($diff->edits as $edit) {
  1528. if ($edit->type == 'copy') {
  1529. if (is_array($block)) {
  1530. if (sizeof($edit->orig) <= $nlead + $ntrail) {
  1531. $block[] = $edit;
  1532. }
  1533. else{
  1534. if ($ntrail) {
  1535. $context = array_slice($edit->orig, 0, $ntrail);
  1536. $block[] = new _DiffOp_Copy($context);
  1537. }
  1538. $this->_block($x0, $ntrail + $xi - $x0,
  1539. $y0, $ntrail + $yi - $y0,
  1540. $block);
  1541. $block = false;
  1542. }
  1543. }
  1544. $context = $edit->orig;
  1545. }
  1546. else {
  1547. if (! is_array($block)) {
  1548. $context = array_slice($context, sizeof($context) - $nlead);
  1549. $x0 = $xi - sizeof($context);
  1550. $y0 = $yi - sizeof($context);
  1551. $block = array();
  1552. if ($context)
  1553. $block[] = new _DiffOp_Copy($context);
  1554. }
  1555. $block[] = $edit;
  1556. }
  1557. if ($edit->orig)
  1558. $xi += sizeof($edit->orig);
  1559. if ($edit->closing)
  1560. $yi += sizeof($edit->closing);
  1561. }
  1562. if (is_array($block))
  1563. $this->_block($x0, $xi - $x0,
  1564. $y0, $yi - $y0,
  1565. $block);
  1566. $end = $this->_end_diff();
  1567. wfProfileOut( __METHOD__ );
  1568. return $end;
  1569. }
  1570. function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) {
  1571. wfProfileIn( __METHOD__ );
  1572. $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
  1573. foreach ($edits as $edit) {
  1574. if ($edit->type == 'copy')
  1575. $this->_context($edit->orig);
  1576. elseif ($edit->type == 'add')
  1577. $this->_added($edit->closing);
  1578. elseif ($edit->type == 'delete')
  1579. $this->_deleted($edit->orig);
  1580. elseif ($edit->type == 'change')
  1581. $this->_changed($edit->orig, $edit->closing);
  1582. else
  1583. trigger_error('Unknown edit type', E_USER_ERROR);
  1584. }
  1585. $this->_end_block();
  1586. wfProfileOut( __METHOD__ );
  1587. }
  1588. function _start_diff() {
  1589. ob_start();
  1590. }
  1591. function _end_diff() {
  1592. $val = ob_get_contents();
  1593. ob_end_clean();
  1594. return $val;
  1595. }
  1596. function _block_header($xbeg, $xlen, $ybeg, $ylen) {
  1597. if ($xlen > 1)
  1598. $xbeg .= "," . ($xbeg + $xlen - 1);
  1599. if ($ylen > 1)
  1600. $ybeg .= "," . ($ybeg + $ylen - 1);
  1601. return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
  1602. }
  1603. function _start_block($header) {
  1604. echo $header . "\n";
  1605. }
  1606. function _end_block() {
  1607. }
  1608. function _lines($lines, $prefix = ' ') {
  1609. foreach ($lines as $line)
  1610. echo "$prefix $line\n";
  1611. }
  1612. function _context($lines) {
  1613. $this->_lines($lines);
  1614. }
  1615. function _added($lines) {
  1616. $this->_lines($lines, '>');
  1617. }
  1618. function _deleted($lines) {
  1619. $this->_lines($lines, '<');
  1620. }
  1621. function _changed($orig, $closing) {
  1622. $this->_deleted($orig);
  1623. echo "---\n";
  1624. $this->_added($closing);
  1625. }
  1626. }
  1627. /**
  1628. * A formatter that outputs unified diffs
  1629. * @ingroup DifferenceEngine
  1630. */
  1631. class UnifiedDiffFormatter extends DiffFormatter {
  1632. var $leading_context_lines = 2;
  1633. var $trailing_context_lines = 2;
  1634. function _added($lines) {
  1635. $this->_lines($lines, '+');
  1636. }
  1637. function _deleted($lines) {
  1638. $this->_lines($lines, '-');
  1639. }
  1640. function _changed($orig, $closing) {
  1641. $this->_deleted($orig);
  1642. $this->_added($closing);
  1643. }
  1644. function _block_header($xbeg, $xlen, $ybeg, $ylen) {
  1645. return "@@ -$xbeg,$xlen +$ybeg,$ylen @@";
  1646. }
  1647. }
  1648. /**
  1649. * A pseudo-formatter that just passes along the Diff::$edits array
  1650. * @ingroup DifferenceEngine
  1651. */
  1652. class ArrayDiffFormatter extends DiffFormatter {
  1653. function format($diff) {
  1654. $oldline = 1;
  1655. $newline = 1;
  1656. $retval = array();
  1657. foreach($diff->edits as $edit)
  1658. switch($edit->type) {
  1659. case 'add':
  1660. foreach($edit->closing as $l) {
  1661. $retval[] = array(
  1662. 'action' => 'add',
  1663. 'new'=> $l,
  1664. 'newline' => $newline++
  1665. );
  1666. }
  1667. break;
  1668. case 'delete':
  1669. foreach($edit->orig as $l) {
  1670. $retval[] = array(
  1671. 'action' => 'delete',
  1672. 'old' => $l,
  1673. 'oldline' => $oldline++,
  1674. );
  1675. }
  1676. break;
  1677. case 'change':
  1678. foreach($edit->orig as $i => $l) {
  1679. $retval[] = array(
  1680. 'action' => 'change',
  1681. 'old' => $l,
  1682. 'new' => @$edit->closing[$i],
  1683. 'oldline' => $oldline++,
  1684. 'newline' => $newline++,
  1685. );
  1686. }
  1687. break;
  1688. case 'copy':
  1689. $oldline += count($edit->orig);
  1690. $newline += count($edit->orig);
  1691. }
  1692. return $retval;
  1693. }
  1694. }
  1695. /**
  1696. * Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3
  1697. *
  1698. */
  1699. define('NBSP', '&#160;'); // iso-8859-x non-breaking space.
  1700. /**
  1701. * @todo document
  1702. * @private
  1703. * @ingroup DifferenceEngine
  1704. */
  1705. class _HWLDF_WordAccumulator {
  1706. function _HWLDF_WordAccumulator () {
  1707. $this->_lines = array();
  1708. $this->_line = '';
  1709. $this->_group = '';
  1710. $this->_tag = '';
  1711. }
  1712. function _flushGroup ($new_tag) {
  1713. if ($this->_group !== '') {
  1714. if ($this->_tag == 'ins')
  1715. $this->_line .= '<ins class="diffchange diffchange-inline">' .
  1716. htmlspecialchars ( $this->_group ) . '</ins>';
  1717. elseif ($this->_tag == 'del')
  1718. $this->_line .= '<del class="diffchange diffchange-inline">' .
  1719. htmlspecialchars ( $this->_group ) . '</del>';
  1720. else
  1721. $this->_line .= htmlspecialchars ( $this->_group );
  1722. }
  1723. $this->_group = '';
  1724. $this->_tag = $new_tag;
  1725. }
  1726. function _flushLine ($new_tag) {
  1727. $this->_flushGroup($new_tag);
  1728. if ($this->_line != '')
  1729. array_push ( $this->_lines, $this->_line );
  1730. else
  1731. # make empty lines visible by inserting an NBSP
  1732. array_push ( $this->_lines, NBSP );
  1733. $this->_line = '';
  1734. }
  1735. function addWords ($words, $tag = '') {
  1736. if ($tag != $this->_tag)
  1737. $this->_flushGroup($tag);
  1738. foreach ($words as $word) {
  1739. // new-line should only come as first char of word.
  1740. if ($word == '')
  1741. continue;
  1742. if ($word[0] == "\n") {
  1743. $this->_flushLine($tag);
  1744. $word = substr($word, 1);
  1745. }
  1746. assert(!strstr($word, "\n"));
  1747. $this->_group .= $word;
  1748. }
  1749. }
  1750. function getLines() {
  1751. $this->_flushLine('~done');
  1752. return $this->_lines;
  1753. }
  1754. }
  1755. /**
  1756. * @todo document
  1757. * @private
  1758. * @ingroup DifferenceEngine
  1759. */
  1760. class WordLevelDiff extends MappedDiff {
  1761. const MAX_LINE_LENGTH = 10000;
  1762. function WordLevelDiff ($orig_lines, $closing_lines) {
  1763. wfProfileIn( __METHOD__ );
  1764. list ($orig_words, $orig_stripped) = $this->_split($orig_lines);
  1765. list ($closing_words, $closing_stripped) = $this->_split($closing_lines);
  1766. $this->MappedDiff($orig_words, $closing_words,
  1767. $orig_stripped, $closing_stripped);
  1768. wfProfileOut( __METHOD__ );
  1769. }
  1770. function _split($lines) {
  1771. wfProfileIn( __METHOD__ );
  1772. $words = array();
  1773. $stripped = array();
  1774. $first = true;
  1775. foreach ( $lines as $line ) {
  1776. # If the line is too long, just pretend the entire line is one big word
  1777. # This prevents resource exhaustion problems
  1778. if ( $first ) {
  1779. $first = false;
  1780. } else {
  1781. $words[] = "\n";
  1782. $stripped[] = "\n";
  1783. }
  1784. if ( strlen( $line ) > self::MAX_LINE_LENGTH ) {
  1785. $words[] = $line;
  1786. $stripped[] = $line;
  1787. } else {
  1788. $m = array();
  1789. if (preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs',
  1790. $line, $m))
  1791. {
  1792. $words = array_merge( $words, $m[0] );
  1793. $stripped = array_merge( $stripped, $m[1] );
  1794. }
  1795. }
  1796. }
  1797. wfProfileOut( __METHOD__ );
  1798. return array($words, $stripped);
  1799. }
  1800. function orig () {
  1801. wfProfileIn( __METHOD__ );
  1802. $orig = new _HWLDF_WordAccumulator;
  1803. foreach ($this->edits as $edit) {
  1804. if ($edit->type == 'copy')
  1805. $orig->addWords($edit->orig);
  1806. elseif ($edit->orig)
  1807. $orig->addWords($edit->orig, 'del');
  1808. }
  1809. $lines = $orig->getLines();
  1810. wfProfileOut( __METHOD__ );
  1811. return $lines;
  1812. }
  1813. function closing () {
  1814. wfProfileIn( __METHOD__ );
  1815. $closing = new _HWLDF_WordAccumulator;
  1816. foreach ($this->edits as $edit) {
  1817. if ($edit->type == 'copy')
  1818. $closing->addWords($edit->closing);
  1819. elseif ($edit->closing)
  1820. $closing->addWords($edit->closing, 'ins');
  1821. }
  1822. $lines = $closing->getLines();
  1823. wfProfileOut( __METHOD__ );
  1824. return $lines;
  1825. }
  1826. }
  1827. /**
  1828. * Wikipedia Table style diff formatter.
  1829. * @todo document
  1830. * @private
  1831. * @ingroup DifferenceEngine
  1832. */
  1833. class TableDiffFormatter extends DiffFormatter {
  1834. function TableDiffFormatter() {
  1835. $this->leading_context_lines = 2;
  1836. $this->trailing_context_lines = 2;
  1837. }
  1838. public static function escapeWhiteSpace( $msg ) {
  1839. $msg = preg_replace( '/^ /m', '&nbsp; ', $msg );
  1840. $msg = preg_replace( '/ $/m', ' &nbsp;', $msg );
  1841. $msg = preg_replace( '/ /', '&nbsp; ', $msg );
  1842. return $msg;
  1843. }
  1844. function _block_header( $xbeg, $xlen, $ybeg, $ylen ) {
  1845. $r = '<tr><td colspan="2" class="diff-lineno"><!--LINE '.$xbeg."--></td>\n" .
  1846. '<td colspan="2" class="diff-lineno"><!--LINE '.$ybeg."--></td></tr>\n";
  1847. return $r;
  1848. }
  1849. function _start_block( $header ) {
  1850. echo $header;
  1851. }
  1852. function _end_block() {
  1853. }
  1854. function _lines( $lines, $prefix=' ', $color='white' ) {
  1855. }
  1856. # HTML-escape parameter before calling this
  1857. function addedLine( $line ) {
  1858. return $this->wrapLine( '+', 'diff-addedline', $line );
  1859. }
  1860. # HTML-escape parameter before calling this
  1861. function deletedLine( $line ) {
  1862. return $this->wrapLine( '-', 'diff-deletedline', $line );
  1863. }
  1864. # HTML-escape parameter before calling this
  1865. function contextLine( $line ) {
  1866. return $this->wrapLine( ' ', 'diff-context', $line );
  1867. }
  1868. private function wrapLine( $marker, $class, $line ) {
  1869. if( $line !== '' ) {
  1870. // The <div> wrapper is needed for 'overflow: auto' style to scroll properly
  1871. $line = Xml::tags( 'div', null, $this->escapeWhiteSpace( $line ) );
  1872. }
  1873. return "<td class='diff-marker'>$marker</td><td class='$class'>$line</td>";
  1874. }
  1875. function emptyLine() {
  1876. return '<td colspan="2">&nbsp;</td>';
  1877. }
  1878. function _added( $lines ) {
  1879. foreach ($lines as $line) {
  1880. echo '<tr>' . $this->emptyLine() .
  1881. $this->addedLine( '<ins class="diffchange">' .
  1882. htmlspecialchars ( $line ) . '</ins>' ) . "</tr>\n";
  1883. }
  1884. }
  1885. function _deleted($lines) {
  1886. foreach ($lines as $line) {
  1887. echo '<tr>' . $this->deletedLine( '<del class="diffchange">' .
  1888. htmlspecialchars ( $line ) . '</del>' ) .
  1889. $this->emptyLine() . "</tr>\n";
  1890. }
  1891. }
  1892. function _context( $lines ) {
  1893. foreach ($lines as $line) {
  1894. echo '<tr>' .
  1895. $this->contextLine( htmlspecialchars ( $line ) ) .
  1896. $this->contextLine( htmlspecialchars ( $line ) ) . "</tr>\n";
  1897. }
  1898. }
  1899. function _changed( $orig, $closing ) {
  1900. wfProfileIn( __METHOD__ );
  1901. $diff = new WordLevelDiff( $orig, $closing );
  1902. $del = $diff->orig();
  1903. $add = $diff->closing();
  1904. # Notice that WordLevelDiff returns HTML-escaped output.
  1905. # Hence, we will be calling addedLine/deletedLine without HTML-escaping.
  1906. while ( $line = array_shift( $del ) ) {
  1907. $aline = array_shift( $add );
  1908. echo '<tr>' . $this->deletedLine( $line ) .
  1909. $this->addedLine( $aline ) . "</tr>\n";
  1910. }
  1911. foreach ($add as $line) { # If any leftovers
  1912. echo '<tr>' . $this->emptyLine() .
  1913. $this->addedLine( $line ) . "</tr>\n";
  1914. }
  1915. wfProfileOut( __METHOD__ );
  1916. }
  1917. }