MediaWiki  master
DifferenceEngine.php
Go to the documentation of this file.
1 <?php
28 
52 
54 
61  const DIFF_VERSION = '1.12';
62 
69  protected $mOldid;
70 
77  protected $mNewid;
78 
90  protected $mOldRev;
91 
101  protected $mNewRev;
102 
108  protected $mOldPage;
109 
115  protected $mNewPage;
116 
121  private $mOldTags;
122 
127  private $mNewTags;
128 
134  private $mOldContent;
135 
141  private $mNewContent;
142 
144  protected $mDiffLang;
145 
147  private $mRevisionsIdsLoaded = false;
148 
150  protected $mRevisionsLoaded = false;
151 
153  protected $mTextLoaded = 0;
154 
163  protected $isContentOverridden = false;
164 
166  protected $mCacheHit = false;
167 
173  public $enableDebugComment = false;
174 
178  protected $mReducedLineNumbers = false;
179 
182 
184  protected $unhide = false;
185 
187  protected $mRefreshCache = false;
188 
191 
198  protected $isSlotDiffRenderer = false;
199 
210  public function __construct( $context = null, $old = 0, $new = 0, $rcid = 0,
211  $refreshCache = false, $unhide = false
212  ) {
213  $this->deprecatePublicProperty( 'mOldid', '1.32', __CLASS__ );
214  $this->deprecatePublicProperty( 'mNewid', '1.32', __CLASS__ );
215  $this->deprecatePublicProperty( 'mOldRev', '1.32', __CLASS__ );
216  $this->deprecatePublicProperty( 'mNewRev', '1.32', __CLASS__ );
217  $this->deprecatePublicProperty( 'mOldPage', '1.32', __CLASS__ );
218  $this->deprecatePublicProperty( 'mNewPage', '1.32', __CLASS__ );
219  $this->deprecatePublicProperty( 'mOldContent', '1.32', __CLASS__ );
220  $this->deprecatePublicProperty( 'mNewContent', '1.32', __CLASS__ );
221  $this->deprecatePublicProperty( 'mRevisionsLoaded', '1.32', __CLASS__ );
222  $this->deprecatePublicProperty( 'mTextLoaded', '1.32', __CLASS__ );
223  $this->deprecatePublicProperty( 'mCacheHit', '1.32', __CLASS__ );
224 
225  if ( $context instanceof IContextSource ) {
226  $this->setContext( $context );
227  }
228 
229  wfDebug( "DifferenceEngine old '$old' new '$new' rcid '$rcid'\n" );
230 
231  $this->mOldid = $old;
232  $this->mNewid = $new;
233  $this->mRefreshCache = $refreshCache;
234  $this->unhide = $unhide;
235  }
236 
241  protected function getSlotDiffRenderers() {
242  if ( $this->isSlotDiffRenderer ) {
243  throw new LogicException( __METHOD__ . ' called in slot diff renderer mode' );
244  }
245 
246  if ( $this->slotDiffRenderers === null ) {
247  if ( !$this->loadRevisionData() ) {
248  return [];
249  }
250 
251  $slotContents = $this->getSlotContents();
252  $this->slotDiffRenderers = array_map( function ( $contents ) {
254  $content = $contents['new'] ?: $contents['old'];
255  return $content->getContentHandler()->getSlotDiffRenderer( $this->getContext() );
256  }, $slotContents );
257  }
259  }
260 
267  public function markAsSlotDiffRenderer() {
268  $this->isSlotDiffRenderer = true;
269  }
270 
276  protected function getSlotContents() {
277  if ( $this->isContentOverridden ) {
278  return [
279  SlotRecord::MAIN => [
280  'old' => $this->mOldContent,
281  'new' => $this->mNewContent,
282  ]
283  ];
284  } elseif ( !$this->loadRevisionData() ) {
285  return [];
286  }
287 
288  $newSlots = $this->mNewRev->getRevisionRecord()->getSlots()->getSlots();
289  if ( $this->mOldRev ) {
290  $oldSlots = $this->mOldRev->getRevisionRecord()->getSlots()->getSlots();
291  } else {
292  $oldSlots = [];
293  }
294  // The order here will determine the visual order of the diff. The current logic is
295  // slots of the new revision first in natural order, then deleted ones. This is ad hoc
296  // and should not be relied on - in the future we may want the ordering to depend
297  // on the page type.
298  $roles = array_merge( array_keys( $newSlots ), array_keys( $oldSlots ) );
299 
300  $slots = [];
301  foreach ( $roles as $role ) {
302  $slots[$role] = [
303  'old' => isset( $oldSlots[$role] ) ? $oldSlots[$role]->getContent() : null,
304  'new' => isset( $newSlots[$role] ) ? $newSlots[$role]->getContent() : null,
305  ];
306  }
307  // move main slot to front
308  if ( isset( $slots[SlotRecord::MAIN] ) ) {
309  $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
310  }
311  return $slots;
312  }
313 
314  public function getTitle() {
315  // T202454 avoid errors when there is no title
316  return parent::getTitle() ?: Title::makeTitle( NS_SPECIAL, 'BadTitle/DifferenceEngine' );
317  }
318 
325  public function setReducedLineNumbers( $value = true ) {
326  $this->mReducedLineNumbers = $value;
327  }
328 
334  public function getDiffLang() {
335  if ( $this->mDiffLang === null ) {
336  # Default language in which the diff text is written.
337  $this->mDiffLang = $this->getTitle()->getPageLanguage();
338  }
339 
340  return $this->mDiffLang;
341  }
342 
346  public function wasCacheHit() {
347  return $this->mCacheHit;
348  }
349 
357  public function getOldid() {
358  $this->loadRevisionIds();
359 
360  return $this->mOldid;
361  }
362 
369  public function getNewid() {
370  $this->loadRevisionIds();
371 
372  return $this->mNewid;
373  }
374 
381  public function getOldRevision() {
382  return $this->mOldRev ? $this->mOldRev->getRevisionRecord() : null;
383  }
384 
390  public function getNewRevision() {
391  return $this->mNewRev ? $this->mNewRev->getRevisionRecord() : null;
392  }
393 
402  public function deletedLink( $id ) {
403  if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) {
404  $dbr = wfGetDB( DB_REPLICA );
405  $arQuery = Revision::getArchiveQueryInfo();
406  $row = $dbr->selectRow(
407  $arQuery['tables'],
408  array_merge( $arQuery['fields'], [ 'ar_namespace', 'ar_title' ] ),
409  [ 'ar_rev_id' => $id ],
410  __METHOD__,
411  [],
412  $arQuery['joins']
413  );
414  if ( $row ) {
416  $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
417 
418  return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( [
419  'target' => $title->getPrefixedText(),
420  'timestamp' => $rev->getTimestamp()
421  ] );
422  }
423  }
424 
425  return false;
426  }
427 
435  public function deletedIdMarker( $id ) {
436  $link = $this->deletedLink( $id );
437  if ( $link ) {
438  return "[$link $id]";
439  } else {
440  return (string)$id;
441  }
442  }
443 
444  private function showMissingRevision() {
445  $out = $this->getOutput();
446 
447  $missing = [];
448  if ( $this->mOldRev === null ||
449  ( $this->mOldRev && $this->mOldContent === null )
450  ) {
451  $missing[] = $this->deletedIdMarker( $this->mOldid );
452  }
453  if ( $this->mNewRev === null ||
454  ( $this->mNewRev && $this->mNewContent === null )
455  ) {
456  $missing[] = $this->deletedIdMarker( $this->mNewid );
457  }
458 
459  $out->setPageTitle( $this->msg( 'errorpagetitle' ) );
460  $msg = $this->msg( 'difference-missing-revision' )
461  ->params( $this->getLanguage()->listToText( $missing ) )
462  ->numParams( count( $missing ) )
463  ->parseAsBlock();
464  $out->addHTML( $msg );
465  }
466 
467  public function showDiffPage( $diffOnly = false ) {
468  # Allow frames except in certain special cases
469  $out = $this->getOutput();
470  $out->allowClickjacking();
471  $out->setRobotPolicy( 'noindex,nofollow' );
472 
473  // Allow extensions to add any extra output here
474  Hooks::run( 'DifferenceEngineShowDiffPage', [ $out ] );
475 
476  if ( !$this->loadRevisionData() ) {
477  if ( Hooks::run( 'DifferenceEngineShowDiffPageMaybeShowMissingRevision', [ $this ] ) ) {
478  $this->showMissingRevision();
479  }
480  return;
481  }
482 
483  $user = $this->getUser();
484  $permErrors = [];
485  if ( $this->mNewPage ) {
486  $permErrors = $this->mNewPage->getUserPermissionsErrors( 'read', $user );
487  }
488  if ( $this->mOldPage ) {
489  $permErrors = wfMergeErrorArrays( $permErrors,
490  $this->mOldPage->getUserPermissionsErrors( 'read', $user ) );
491  }
492  if ( count( $permErrors ) ) {
493  throw new PermissionsError( 'read', $permErrors );
494  }
495 
496  $rollback = '';
497 
498  $query = [];
499  # Carry over 'diffonly' param via navigation links
500  if ( $diffOnly != $user->getBoolOption( 'diffonly' ) ) {
501  $query['diffonly'] = $diffOnly;
502  }
503  # Cascade unhide param in links for easy deletion browsing
504  if ( $this->unhide ) {
505  $query['unhide'] = 1;
506  }
507 
508  # Check if one of the revisions is deleted/suppressed
509  $deleted = $suppressed = false;
510  $allowed = $this->mNewRev->userCan( Revision::DELETED_TEXT, $user );
511 
512  $revisionTools = [];
513 
514  # mOldRev is false if the difference engine is called with a "vague" query for
515  # a diff between a version V and its previous version V' AND the version V
516  # is the first version of that article. In that case, V' does not exist.
517  if ( $this->mOldRev === false ) {
518  if ( $this->mNewPage ) {
519  $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
520  }
521  $samePage = true;
522  $oldHeader = '';
523  // Allow extensions to change the $oldHeader variable
524  Hooks::run( 'DifferenceEngineOldHeaderNoOldRev', [ &$oldHeader ] );
525  } else {
526  Hooks::run( 'DiffViewHeader', [ $this, $this->mOldRev, $this->mNewRev ] );
527 
528  if ( !$this->mOldPage || !$this->mNewPage ) {
529  // XXX say something to the user?
530  $samePage = false;
531  } elseif ( $this->mNewPage->equals( $this->mOldPage ) ) {
532  $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
533  $samePage = true;
534  } else {
535  $out->setPageTitle( $this->msg( 'difference-title-multipage',
536  $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) );
537  $out->addSubtitle( $this->msg( 'difference-multipage' ) );
538  $samePage = false;
539  }
540 
541  if ( $samePage && $this->mNewPage && $this->mNewPage->quickUserCan( 'edit', $user ) ) {
542  if ( $this->mNewRev->isCurrent() && $this->mNewPage->userCan( 'rollback', $user ) ) {
543  $rollbackLink = Linker::generateRollback( $this->mNewRev, $this->getContext() );
544  if ( $rollbackLink ) {
545  $out->preventClickjacking();
546  $rollback = "\u{00A0}\u{00A0}\u{00A0}" . $rollbackLink;
547  }
548  }
549 
550  if ( !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) &&
551  !$this->mNewRev->isDeleted( Revision::DELETED_TEXT )
552  ) {
553  $undoLink = Html::element( 'a', [
554  'href' => $this->mNewPage->getLocalURL( [
555  'action' => 'edit',
556  'undoafter' => $this->mOldid,
557  'undo' => $this->mNewid
558  ] ),
559  'title' => Linker::titleAttrib( 'undo' ),
560  ],
561  $this->msg( 'editundo' )->text()
562  );
563  $revisionTools['mw-diff-undo'] = $undoLink;
564  }
565  }
566 
567  # Make "previous revision link"
568  if ( $samePage && $this->mOldPage && $this->mOldRev->getPrevious() ) {
569  $prevlink = Linker::linkKnown(
570  $this->mOldPage,
571  $this->msg( 'previousdiff' )->escaped(),
572  [ 'id' => 'differences-prevlink' ],
573  [ 'diff' => 'prev', 'oldid' => $this->mOldid ] + $query
574  );
575  } else {
576  $prevlink = "\u{00A0}";
577  }
578 
579  if ( $this->mOldRev->isMinor() ) {
580  $oldminor = ChangesList::flag( 'minor' );
581  } else {
582  $oldminor = '';
583  }
584 
585  $ldel = $this->revisionDeleteLink( $this->mOldRev );
586  $oldRevisionHeader = $this->getRevisionHeader( $this->mOldRev, 'complete' );
587  $oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff', $this->getContext() );
588 
589  $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' .
590  '<div id="mw-diff-otitle2">' .
591  Linker::revUserTools( $this->mOldRev, !$this->unhide ) . '</div>' .
592  '<div id="mw-diff-otitle3">' . $oldminor .
593  Linker::revComment( $this->mOldRev, !$diffOnly, !$this->unhide ) . $ldel . '</div>' .
594  '<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' .
595  '<div id="mw-diff-otitle4">' . $prevlink . '</div>';
596 
597  // Allow extensions to change the $oldHeader variable
598  Hooks::run( 'DifferenceEngineOldHeader', [ $this, &$oldHeader, $prevlink, $oldminor,
599  $diffOnly, $ldel, $this->unhide ] );
600 
601  if ( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) {
602  $deleted = true; // old revisions text is hidden
603  if ( $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
604  $suppressed = true; // also suppressed
605  }
606  }
607 
608  # Check if this user can see the revisions
609  if ( !$this->mOldRev->userCan( Revision::DELETED_TEXT, $user ) ) {
610  $allowed = false;
611  }
612  }
613 
614  $out->addJsConfigVars( [
615  'wgDiffOldId' => $this->mOldid,
616  'wgDiffNewId' => $this->mNewid,
617  ] );
618 
619  # Make "next revision link"
620  # Skip next link on the top revision
621  if ( $samePage && $this->mNewPage && !$this->mNewRev->isCurrent() ) {
622  $nextlink = Linker::linkKnown(
623  $this->mNewPage,
624  $this->msg( 'nextdiff' )->escaped(),
625  [ 'id' => 'differences-nextlink' ],
626  [ 'diff' => 'next', 'oldid' => $this->mNewid ] + $query
627  );
628  } else {
629  $nextlink = "\u{00A0}";
630  }
631 
632  if ( $this->mNewRev->isMinor() ) {
633  $newminor = ChangesList::flag( 'minor' );
634  } else {
635  $newminor = '';
636  }
637 
638  # Handle RevisionDelete links...
639  $rdel = $this->revisionDeleteLink( $this->mNewRev );
640 
641  # Allow extensions to define their own revision tools
642  Hooks::run( 'DiffRevisionTools',
643  [ $this->mNewRev, &$revisionTools, $this->mOldRev, $user ] );
644  $formattedRevisionTools = [];
645  // Put each one in parentheses (poor man's button)
646  foreach ( $revisionTools as $key => $tool ) {
647  $toolClass = is_string( $key ) ? $key : 'mw-diff-tool';
648  $element = Html::rawElement(
649  'span',
650  [ 'class' => $toolClass ],
651  $this->msg( 'parentheses' )->rawParams( $tool )->escaped()
652  );
653  $formattedRevisionTools[] = $element;
654  }
655  $newRevisionHeader = $this->getRevisionHeader( $this->mNewRev, 'complete' ) .
656  ' ' . implode( ' ', $formattedRevisionTools );
657  $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff', $this->getContext() );
658 
659  $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' .
660  '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $this->mNewRev, !$this->unhide ) .
661  " $rollback</div>" .
662  '<div id="mw-diff-ntitle3">' . $newminor .
663  Linker::revComment( $this->mNewRev, !$diffOnly, !$this->unhide ) . $rdel . '</div>' .
664  '<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' .
665  '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
666 
667  // Allow extensions to change the $newHeader variable
668  Hooks::run( 'DifferenceEngineNewHeader', [ $this, &$newHeader, $formattedRevisionTools,
669  $nextlink, $rollback, $newminor, $diffOnly, $rdel, $this->unhide ] );
670 
671  if ( $this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
672  $deleted = true; // new revisions text is hidden
673  if ( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
674  $suppressed = true; // also suppressed
675  }
676  }
677 
678  # If the diff cannot be shown due to a deleted revision, then output
679  # the diff header and links to unhide (if available)...
680  if ( $deleted && ( !$this->unhide || !$allowed ) ) {
681  $this->showDiffStyle();
682  $multi = $this->getMultiNotice();
683  $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
684  if ( !$allowed ) {
685  $msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff';
686  # Give explanation for why revision is not visible
687  $out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
688  [ $msg ] );
689  } else {
690  # Give explanation and add a link to view the diff...
691  $query = $this->getRequest()->appendQueryValue( 'unhide', '1' );
692  $link = $this->getTitle()->getFullURL( $query );
693  $msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff';
694  $out->wrapWikiMsg(
695  "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
696  [ $msg, $link ]
697  );
698  }
699  # Otherwise, output a regular diff...
700  } else {
701  # Add deletion notice if the user is viewing deleted content
702  $notice = '';
703  if ( $deleted ) {
704  $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
705  $notice = "<div id='mw-$msg' class='mw-warning plainlinks'>\n" .
706  $this->msg( $msg )->parse() .
707  "</div>\n";
708  }
709  $this->showDiff( $oldHeader, $newHeader, $notice );
710  if ( !$diffOnly ) {
711  $this->renderNewRevision();
712  }
713  }
714  }
715 
725  public function markPatrolledLink() {
726  if ( $this->mMarkPatrolledLink === null ) {
727  $linkInfo = $this->getMarkPatrolledLinkInfo();
728  // If false, there is no patrol link needed/allowed
729  if ( !$linkInfo || !$this->mNewPage ) {
730  $this->mMarkPatrolledLink = '';
731  } else {
732  $this->mMarkPatrolledLink = ' <span class="patrollink" data-mw="interface">[' .
734  $this->mNewPage,
735  $this->msg( 'markaspatrolleddiff' )->escaped(),
736  [],
737  [
738  'action' => 'markpatrolled',
739  'rcid' => $linkInfo['rcid'],
740  ]
741  ) . ']</span>';
742  // Allow extensions to change the markpatrolled link
743  Hooks::run( 'DifferenceEngineMarkPatrolledLink', [ $this,
744  &$this->mMarkPatrolledLink, $linkInfo['rcid'] ] );
745  }
746  }
748  }
749 
757  protected function getMarkPatrolledLinkInfo() {
758  $user = $this->getUser();
759  $config = $this->getConfig();
760 
761  // Prepare a change patrol link, if applicable
762  if (
763  // Is patrolling enabled and the user allowed to?
764  $config->get( 'UseRCPatrol' ) &&
765  $this->mNewPage && $this->mNewPage->quickUserCan( 'patrol', $user ) &&
766  // Only do this if the revision isn't more than 6 hours older
767  // than the Max RC age (6h because the RC might not be cleaned out regularly)
768  RecentChange::isInRCLifespan( $this->mNewRev->getTimestamp(), 21600 )
769  ) {
770  // Look for an unpatrolled change corresponding to this diff
771  $db = wfGetDB( DB_REPLICA );
772  $change = RecentChange::newFromConds(
773  [
774  'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
775  'rc_this_oldid' => $this->mNewid,
776  'rc_patrolled' => RecentChange::PRC_UNPATROLLED
777  ],
778  __METHOD__
779  );
780 
781  if ( $change && !$change->getPerformer()->equals( $user ) ) {
782  $rcid = $change->getAttribute( 'rc_id' );
783  } else {
784  // None found or the page has been created by the current user.
785  // If the user could patrol this it already would be patrolled
786  $rcid = 0;
787  }
788 
789  // Allow extensions to possibly change the rcid here
790  // For example the rcid might be set to zero due to the user
791  // being the same as the performer of the change but an extension
792  // might still want to show it under certain conditions
793  Hooks::run( 'DifferenceEngineMarkPatrolledRCID', [ &$rcid, $this, $change, $user ] );
794 
795  // Build the link
796  if ( $rcid ) {
797  $this->getOutput()->preventClickjacking();
798  if ( $user->isAllowed( 'writeapi' ) ) {
799  $this->getOutput()->addModules( 'mediawiki.page.patrol.ajax' );
800  }
801 
802  return [
803  'rcid' => $rcid,
804  ];
805  }
806  }
807 
808  // No mark as patrolled link applicable
809  return false;
810  }
811 
817  protected function revisionDeleteLink( $rev ) {
818  $link = Linker::getRevDeleteLink( $this->getUser(), $rev, $rev->getTitle() );
819  if ( $link !== '' ) {
820  $link = "\u{00A0}\u{00A0}\u{00A0}" . $link . ' ';
821  }
822 
823  return $link;
824  }
825 
831  public function renderNewRevision() {
832  if ( $this->isContentOverridden ) {
833  // The code below only works with a Revision object. We could construct a fake revision
834  // (here or in setContent), but since this does not seem needed at the moment,
835  // we'll just fail for now.
836  throw new LogicException(
837  __METHOD__
838  . ' is not supported after calling setContent(). Use setRevisions() instead.'
839  );
840  }
841 
842  $out = $this->getOutput();
843  $revHeader = $this->getRevisionHeader( $this->mNewRev );
844  # Add "current version as of X" title
845  $out->addHTML( "<hr class='diff-hr' id='mw-oldid' />
846  <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
847  # Page content may be handled by a hooked call instead...
848  if ( Hooks::run( 'ArticleContentOnDiff', [ $this, $out ] ) ) {
849  $this->loadNewText();
850  if ( !$this->mNewPage ) {
851  // New revision is unsaved; bail out.
852  // TODO in theory rendering the new revision is a meaningful thing to do
853  // even if it's unsaved, but a lot of untangling is required to do it safely.
854  return;
855  }
856 
857  $out->setRevisionId( $this->mNewid );
858  $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() );
859  $out->setArticleFlag( true );
860 
861  if ( !Hooks::run( 'ArticleRevisionViewCustom',
862  [ $this->mNewRev->getRevisionRecord(), $this->mNewPage, $out ] )
863  ) {
864  // Handled by extension
865  // NOTE: sync with hooks called in Article::view()
866  } elseif ( !Hooks::run( 'ArticleContentViewCustom',
867  [ $this->mNewContent, $this->mNewPage, $out ], '1.32' )
868  ) {
869  // Handled by extension
870  // NOTE: sync with hooks called in Article::view()
871  } else {
872  // Normal page
873  if ( $this->getTitle()->equals( $this->mNewPage ) ) {
874  // If the Title stored in the context is the same as the one
875  // of the new revision, we can use its associated WikiPage
876  // object.
877  $wikiPage = $this->getWikiPage();
878  } else {
879  // Otherwise we need to create our own WikiPage object
880  $wikiPage = WikiPage::factory( $this->mNewPage );
881  }
882 
883  $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRev );
884 
885  # WikiPage::getParserOutput() should not return false, but just in case
886  if ( $parserOutput ) {
887  // Allow extensions to change parser output here
888  if ( Hooks::run( 'DifferenceEngineRenderRevisionAddParserOutput',
889  [ $this, $out, $parserOutput, $wikiPage ] )
890  ) {
891  $out->addParserOutput( $parserOutput, [
892  'enableSectionEditLinks' => $this->mNewRev->isCurrent()
893  && $this->mNewRev->getTitle()->quickUserCan( 'edit', $this->getUser() ),
894  ] );
895  }
896  }
897  }
898  }
899 
900  // Allow extensions to optionally not show the final patrolled link
901  if ( Hooks::run( 'DifferenceEngineRenderRevisionShowFinalPatrolLink' ) ) {
902  # Add redundant patrol link on bottom...
903  $out->addHTML( $this->markPatrolledLink() );
904  }
905  }
906 
913  protected function getParserOutput( WikiPage $page, Revision $rev ) {
914  if ( !$rev->getId() ) {
915  // WikiPage::getParserOutput wants a revision ID. Passing 0 will incorrectly show
916  // the current revision, so fail instead. If need be, WikiPage::getParserOutput
917  // could be made to accept a Revision or RevisionRecord instead of the id.
918  return false;
919  }
920 
921  $parserOptions = $page->makeParserOptions( $this->getContext() );
922  $parserOutput = $page->getParserOutput( $parserOptions, $rev->getId() );
923 
924  return $parserOutput;
925  }
926 
937  public function showDiff( $otitle, $ntitle, $notice = '' ) {
938  // Allow extensions to affect the output here
939  Hooks::run( 'DifferenceEngineShowDiff', [ $this ] );
940 
941  $diff = $this->getDiff( $otitle, $ntitle, $notice );
942  if ( $diff === false ) {
943  $this->showMissingRevision();
944 
945  return false;
946  } else {
947  $this->showDiffStyle();
948  $this->getOutput()->addHTML( $diff );
949 
950  return true;
951  }
952  }
953 
957  public function showDiffStyle() {
958  if ( !$this->isSlotDiffRenderer ) {
959  $this->getOutput()->addModuleStyles( [
960  'mediawiki.interface.helpers.styles',
961  'mediawiki.diff.styles'
962  ] );
963  foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
964  $slotDiffRenderer->addModules( $this->getOutput() );
965  }
966  }
967  }
968 
978  public function getDiff( $otitle, $ntitle, $notice = '' ) {
979  $body = $this->getDiffBody();
980  if ( $body === false ) {
981  return false;
982  }
983 
984  $multi = $this->getMultiNotice();
985  // Display a message when the diff is empty
986  if ( $body === '' ) {
987  $notice .= '<div class="mw-diff-empty">' .
988  $this->msg( 'diff-empty' )->parse() .
989  "</div>\n";
990  }
991 
992  return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
993  }
994 
1000  public function getDiffBody() {
1001  $this->mCacheHit = true;
1002  // Check if the diff should be hidden from this user
1003  if ( !$this->isContentOverridden ) {
1004  if ( !$this->loadRevisionData() ) {
1005  return false;
1006  } elseif ( $this->mOldRev &&
1007  !$this->mOldRev->userCan( Revision::DELETED_TEXT, $this->getUser() )
1008  ) {
1009  return false;
1010  } elseif ( $this->mNewRev &&
1011  !$this->mNewRev->userCan( Revision::DELETED_TEXT, $this->getUser() )
1012  ) {
1013  return false;
1014  }
1015  // Short-circuit
1016  if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev &&
1017  $this->mOldRev->getId() && $this->mOldRev->getId() == $this->mNewRev->getId() )
1018  ) {
1019  if ( Hooks::run( 'DifferenceEngineShowEmptyOldContent', [ $this ] ) ) {
1020  return '';
1021  }
1022  }
1023  }
1024 
1025  // Cacheable?
1026  $key = false;
1027  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1028  if ( $this->mOldid && $this->mNewid ) {
1029  // Check if subclass is still using the old way
1030  // for backwards-compatibility
1031  $key = $this->getDiffBodyCacheKey();
1032  if ( $key === null ) {
1033  $key = $cache->makeKey( ...$this->getDiffBodyCacheKeyParams() );
1034  }
1035 
1036  // Try cache
1037  if ( !$this->mRefreshCache ) {
1038  $difftext = $cache->get( $key );
1039  if ( is_string( $difftext ) ) {
1040  wfIncrStats( 'diff_cache.hit' );
1041  $difftext = $this->localiseDiff( $difftext );
1042  $difftext .= "\n<!-- diff cache key $key -->\n";
1043 
1044  return $difftext;
1045  }
1046  } // don't try to load but save the result
1047  }
1048  $this->mCacheHit = false;
1049 
1050  // Loadtext is permission safe, this just clears out the diff
1051  if ( !$this->loadText() ) {
1052  return false;
1053  }
1054 
1055  $difftext = '';
1056  // We've checked for revdelete at the beginning of this method; it's OK to ignore
1057  // read permissions here.
1058  $slotContents = $this->getSlotContents();
1059  foreach ( $this->getSlotDiffRenderers() as $role => $slotDiffRenderer ) {
1060  $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role]['old'],
1061  $slotContents[$role]['new'] );
1062  if ( $slotDiff && $role !== SlotRecord::MAIN ) {
1063  // FIXME: ask SlotRoleHandler::getSlotNameMessage
1064  $slotTitle = $role;
1065  $difftext .= $this->getSlotHeader( $slotTitle );
1066  }
1067  $difftext .= $slotDiff;
1068  }
1069 
1070  // Avoid PHP 7.1 warning from passing $this by reference
1071  $diffEngine = $this;
1072 
1073  // Save to cache for 7 days
1074  if ( !Hooks::run( 'AbortDiffCache', [ &$diffEngine ] ) ) {
1075  wfIncrStats( 'diff_cache.uncacheable' );
1076  } elseif ( $key !== false && $difftext !== false ) {
1077  wfIncrStats( 'diff_cache.miss' );
1078  $cache->set( $key, $difftext, 7 * 86400 );
1079  } else {
1080  wfIncrStats( 'diff_cache.uncacheable' );
1081  }
1082  // localise line numbers and title attribute text
1083  if ( $difftext !== false ) {
1084  $difftext = $this->localiseDiff( $difftext );
1085  }
1086 
1087  return $difftext;
1088  }
1089 
1096  public function getDiffBodyForRole( $role ) {
1097  $diffRenderers = $this->getSlotDiffRenderers();
1098  if ( !isset( $diffRenderers[$role] ) ) {
1099  return false;
1100  }
1101 
1102  $slotContents = $this->getSlotContents();
1103  $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role]['old'],
1104  $slotContents[$role]['new'] );
1105  if ( !$slotDiff ) {
1106  return false;
1107  }
1108 
1109  if ( $role !== SlotRecord::MAIN ) {
1110  // TODO use human-readable role name at least
1111  $slotTitle = $role;
1112  $slotDiff = $this->getSlotHeader( $slotTitle ) . $slotDiff;
1113  }
1114 
1115  return $this->localiseDiff( $slotDiff );
1116  }
1117 
1125  protected function getSlotHeader( $headerText ) {
1126  // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
1127  $columnCount = $this->mOldRev ? 4 : 2;
1128  $userLang = $this->getLanguage()->getHtmlCode();
1129  return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-header', 'lang' => $userLang ],
1130  Html::element( 'th', [ 'colspan' => $columnCount ], $headerText ) );
1131  }
1132 
1142  protected function getDiffBodyCacheKey() {
1143  return null;
1144  }
1145 
1159  protected function getDiffBodyCacheKeyParams() {
1160  if ( !$this->mOldid || !$this->mNewid ) {
1161  throw new MWException( 'mOldid and mNewid must be set to get diff cache key.' );
1162  }
1163 
1164  $engine = $this->getEngine();
1165  $params = [
1166  'diff',
1167  $engine,
1168  self::DIFF_VERSION,
1169  "old-{$this->mOldid}",
1170  "rev-{$this->mNewid}"
1171  ];
1172 
1173  if ( $engine === 'wikidiff2' ) {
1174  $params[] = phpversion( 'wikidiff2' );
1175  $params[] = $this->getConfig()->get( 'WikiDiff2MovedParagraphDetectionCutoff' );
1176  }
1177 
1178  if ( !$this->isSlotDiffRenderer ) {
1179  foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1180  $params = array_merge( $params, $slotDiffRenderer->getExtraCacheKeys() );
1181  }
1182  }
1183 
1184  return $params;
1185  }
1186 
1194  public function getExtraCacheKeys() {
1195  // This method is called when the DifferenceEngine is used for a slot diff. We only care
1196  // about special things, not the revision IDs, which are added to the cache key by the
1197  // page-level DifferenceEngine, and which might not have a valid value for this object.
1198  $this->mOldid = 123456789;
1199  $this->mNewid = 987654321;
1200 
1201  // This will repeat a bunch of unnecessary key fields for each slot. Not nice but harmless.
1202  $cacheString = $this->getDiffBodyCacheKey();
1203  if ( $cacheString ) {
1204  return [ $cacheString ];
1205  }
1206 
1208 
1209  // Try to get rid of the standard keys to keep the cache key human-readable:
1210  // call the getDiffBodyCacheKeyParams implementation of the base class, and if
1211  // the child class includes the same keys, drop them.
1212  // Uses an obscure PHP feature where static calls to non-static methods are allowed
1213  // as long as we are already in a non-static method of the same class, and the call context
1214  // ($this) will be inherited.
1215  // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
1216  $standardParams = DifferenceEngine::getDiffBodyCacheKeyParams();
1217  if ( array_slice( $params, 0, count( $standardParams ) ) === $standardParams ) {
1218  $params = array_slice( $params, count( $standardParams ) );
1219  }
1220 
1221  return $params;
1222  }
1223 
1237  public function generateContentDiffBody( Content $old, Content $new ) {
1238  $slotDiffRenderer = $new->getContentHandler()->getSlotDiffRenderer( $this->getContext() );
1239  if (
1240  $slotDiffRenderer instanceof DifferenceEngineSlotDiffRenderer
1241  && $this->isSlotDiffRenderer
1242  ) {
1243  // Oops, we are just about to enter an infinite loop (the slot-level DifferenceEngine
1244  // called a DifferenceEngineSlotDiffRenderer that wraps the same DifferenceEngine class).
1245  // This will happen when a content model has no custom slot diff renderer, it does have
1246  // a custom difference engine, but that does not override this method.
1247  throw new Exception( get_class( $this ) . ': could not maintain backwards compatibility. '
1248  . 'Please use a SlotDiffRenderer.' );
1249  }
1250  return $slotDiffRenderer->getDiff( $old, $new ) . $this->getDebugString();
1251  }
1252 
1265  public function generateTextDiffBody( $otext, $ntext ) {
1266  $slotDiffRenderer = ContentHandler::getForModelID( CONTENT_MODEL_TEXT )
1267  ->getSlotDiffRenderer( $this->getContext() );
1268  if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1269  // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1270  // This is too unlikely to happen to bother handling properly.
1271  throw new Exception( 'The slot diff renderer for text content should be a '
1272  . 'TextSlotDiffRenderer subclass' );
1273  }
1274  return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1275  }
1276 
1283  public static function getEngine() {
1284  global $wgExternalDiffEngine;
1285  // We use the global here instead of Config because we write to the value,
1286  // and Config is not mutable.
1287  if ( $wgExternalDiffEngine == 'wikidiff' || $wgExternalDiffEngine == 'wikidiff3' ) {
1288  wfDeprecated( "\$wgExternalDiffEngine = '{$wgExternalDiffEngine}'", '1.27' );
1289  $wgExternalDiffEngine = false;
1290  } elseif ( $wgExternalDiffEngine == 'wikidiff2' ) {
1291  wfDeprecated( "\$wgExternalDiffEngine = '{$wgExternalDiffEngine}'", '1.32' );
1292  $wgExternalDiffEngine = false;
1293  } elseif ( !is_string( $wgExternalDiffEngine ) && $wgExternalDiffEngine !== false ) {
1294  // And prevent people from shooting themselves in the foot...
1295  wfWarn( '$wgExternalDiffEngine is set to a non-string value, forcing it to false' );
1296  $wgExternalDiffEngine = false;
1297  }
1298 
1299  if ( is_string( $wgExternalDiffEngine ) && is_executable( $wgExternalDiffEngine ) ) {
1300  return $wgExternalDiffEngine;
1301  } elseif ( $wgExternalDiffEngine === false && function_exists( 'wikidiff2_do_diff' ) ) {
1302  return 'wikidiff2';
1303  } else {
1304  // Native PHP
1305  return false;
1306  }
1307  }
1308 
1321  protected function textDiff( $otext, $ntext ) {
1322  $slotDiffRenderer = ContentHandler::getForModelID( CONTENT_MODEL_TEXT )
1323  ->getSlotDiffRenderer( $this->getContext() );
1324  if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1325  // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1326  // This is too unlikely to happen to bother handling properly.
1327  throw new Exception( 'The slot diff renderer for text content should be a '
1328  . 'TextSlotDiffRenderer subclass' );
1329  }
1330  return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1331  }
1332 
1341  protected function debug( $generator = "internal" ) {
1342  if ( !$this->enableDebugComment ) {
1343  return '';
1344  }
1345  $data = [ $generator ];
1346  if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
1347  $data[] = wfHostname();
1348  }
1349  $data[] = wfTimestamp( TS_DB );
1350 
1351  return "<!-- diff generator: " .
1352  implode( " ", array_map( "htmlspecialchars", $data ) ) .
1353  " -->\n";
1354  }
1355 
1356  private function getDebugString() {
1357  $engine = self::getEngine();
1358  if ( $engine === 'wikidiff2' ) {
1359  return $this->debug( 'wikidiff2' );
1360  } elseif ( $engine === false ) {
1361  return $this->debug( 'native PHP' );
1362  } else {
1363  return $this->debug( "external $engine" );
1364  }
1365  }
1366 
1373  private function localiseDiff( $text ) {
1374  $text = $this->localiseLineNumbers( $text );
1375  if ( $this->getEngine() === 'wikidiff2' &&
1376  version_compare( phpversion( 'wikidiff2' ), '1.5.1', '>=' )
1377  ) {
1378  $text = $this->addLocalisedTitleTooltips( $text );
1379  }
1380  return $text;
1381  }
1382 
1390  public function localiseLineNumbers( $text ) {
1391  return preg_replace_callback(
1392  '/<!--LINE (\d+)-->/',
1393  [ $this, 'localiseLineNumbersCb' ],
1394  $text
1395  );
1396  }
1397 
1398  public function localiseLineNumbersCb( $matches ) {
1399  if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
1400  return '';
1401  }
1402 
1403  return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
1404  }
1405 
1412  private function addLocalisedTitleTooltips( $text ) {
1413  return preg_replace_callback(
1414  '/class="mw-diff-movedpara-(left|right)"/',
1415  [ $this, 'addLocalisedTitleTooltipsCb' ],
1416  $text
1417  );
1418  }
1419 
1425  $key = $matches[1] === 'right' ?
1426  'diff-paragraph-moved-toold' :
1427  'diff-paragraph-moved-tonew';
1428  return $matches[0] . ' title="' . $this->msg( $key )->escaped() . '"';
1429  }
1430 
1436  public function getMultiNotice() {
1437  // The notice only make sense if we are diffing two saved revisions of the same page.
1438  if (
1439  !$this->mOldRev || !$this->mNewRev
1440  || !$this->mOldPage || !$this->mNewPage
1441  || !$this->mOldPage->equals( $this->mNewPage )
1442  ) {
1443  return '';
1444  }
1445 
1446  if ( $this->mOldRev->getTimestamp() > $this->mNewRev->getTimestamp() ) {
1447  $oldRev = $this->mNewRev; // flip
1448  $newRev = $this->mOldRev; // flip
1449  } else { // normal case
1450  $oldRev = $this->mOldRev;
1452  }
1453 
1454  // Sanity: don't show the notice if too many rows must be scanned
1455  // @todo show some special message for that case
1456  $nEdits = $this->mNewPage->countRevisionsBetween( $oldRev, $newRev, 1000 );
1457  if ( $nEdits > 0 && $nEdits <= 1000 ) {
1458  $limit = 100; // use diff-multi-manyusers if too many users
1459  $users = $this->mNewPage->getAuthorsBetween( $oldRev, $newRev, $limit );
1460  $numUsers = count( $users );
1461 
1462  if ( $numUsers == 1 && $users[0] == $newRev->getUserText( Revision::RAW ) ) {
1463  $numUsers = 0; // special case to say "by the same user" instead of "by one other user"
1464  }
1465 
1466  return self::intermediateEditsMsg( $nEdits, $numUsers, $limit );
1467  }
1468 
1469  return '';
1470  }
1471 
1481  public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) {
1482  if ( $numUsers === 0 ) {
1483  $msg = 'diff-multi-sameuser';
1484  } elseif ( $numUsers > $limit ) {
1485  $msg = 'diff-multi-manyusers';
1486  $numUsers = $limit;
1487  } else {
1488  $msg = 'diff-multi-otherusers';
1489  }
1490 
1491  return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1492  }
1493 
1503  public function getRevisionHeader( Revision $rev, $complete = '' ) {
1504  $lang = $this->getLanguage();
1505  $user = $this->getUser();
1506  $revtimestamp = $rev->getTimestamp();
1507  $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
1508  $dateofrev = $lang->userDate( $revtimestamp, $user );
1509  $timeofrev = $lang->userTime( $revtimestamp, $user );
1510 
1511  $header = $this->msg(
1512  $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
1513  $timestamp,
1514  $dateofrev,
1515  $timeofrev
1516  )->escaped();
1517 
1518  if ( $complete !== 'complete' ) {
1519  return $header;
1520  }
1521 
1522  $title = $rev->getTitle();
1523 
1525  [ 'oldid' => $rev->getId() ] );
1526 
1527  if ( $rev->userCan( Revision::DELETED_TEXT, $user ) ) {
1528  $editQuery = [ 'action' => 'edit' ];
1529  if ( !$rev->isCurrent() ) {
1530  $editQuery['oldid'] = $rev->getId();
1531  }
1532 
1533  $key = $title->quickUserCan( 'edit', $user ) ? 'editold' : 'viewsourceold';
1534  $msg = $this->msg( $key )->escaped();
1535  $editLink = $this->msg( 'parentheses' )->rawParams(
1536  Linker::linkKnown( $title, $msg, [], $editQuery ) )->escaped();
1537  $header .= ' ' . Html::rawElement(
1538  'span',
1539  [ 'class' => 'mw-diff-edit' ],
1540  $editLink
1541  );
1542  if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
1544  'span',
1545  [ 'class' => 'history-deleted' ],
1546  $header
1547  );
1548  }
1549  } else {
1550  $header = Html::rawElement( 'span', [ 'class' => 'history-deleted' ], $header );
1551  }
1552 
1553  return $header;
1554  }
1555 
1568  public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
1569  // shared.css sets diff in interface language/dir, but the actual content
1570  // is often in a different language, mostly the page content language/dir
1571  $header = Html::openElement( 'table', [
1572  'class' => [ 'diff', 'diff-contentalign-' . $this->getDiffLang()->alignStart() ],
1573  'data-mw' => 'interface',
1574  ] );
1575  $userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
1576 
1577  if ( !$diff && !$otitle ) {
1578  $header .= "
1579  <tr class=\"diff-title\" lang=\"{$userLang}\">
1580  <td class=\"diff-ntitle\">{$ntitle}</td>
1581  </tr>";
1582  $multiColspan = 1;
1583  } else {
1584  if ( $diff ) { // Safari/Chrome show broken output if cols not used
1585  $header .= "
1586  <col class=\"diff-marker\" />
1587  <col class=\"diff-content\" />
1588  <col class=\"diff-marker\" />
1589  <col class=\"diff-content\" />";
1590  $colspan = 2;
1591  $multiColspan = 4;
1592  } else {
1593  $colspan = 1;
1594  $multiColspan = 2;
1595  }
1596  if ( $otitle || $ntitle ) {
1597  $header .= "
1598  <tr class=\"diff-title\" lang=\"{$userLang}\">
1599  <td colspan=\"$colspan\" class=\"diff-otitle\">{$otitle}</td>
1600  <td colspan=\"$colspan\" class=\"diff-ntitle\">{$ntitle}</td>
1601  </tr>";
1602  }
1603  }
1604 
1605  if ( $multi != '' ) {
1606  $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1607  "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
1608  }
1609  if ( $notice != '' ) {
1610  $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1611  "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
1612  }
1613 
1614  return $header . $diff . "</table>";
1615  }
1616 
1624  public function setContent( Content $oldContent, Content $newContent ) {
1625  $this->mOldContent = $oldContent;
1626  $this->mNewContent = $newContent;
1627 
1628  $this->mTextLoaded = 2;
1629  $this->mRevisionsLoaded = true;
1630  $this->isContentOverridden = true;
1631  $this->slotDiffRenderers = null;
1632  }
1633 
1639  public function setRevisions(
1640  RevisionRecord $oldRevision = null, RevisionRecord $newRevision
1641  ) {
1642  if ( $oldRevision ) {
1643  $this->mOldRev = new Revision( $oldRevision );
1644  $this->mOldid = $oldRevision->getId();
1645  $this->mOldPage = Title::newFromLinkTarget( $oldRevision->getPageAsLinkTarget() );
1646  // This method is meant for edit diffs and such so there is no reason to provide a
1647  // revision that's not readable to the user, but check it just in case.
1648  $this->mOldContent = $oldRevision->getContent( SlotRecord::MAIN,
1649  RevisionRecord::FOR_THIS_USER, $this->getUser() );
1650  } else {
1651  $this->mOldPage = null;
1652  $this->mOldRev = $this->mOldid = false;
1653  }
1654  $this->mNewRev = new Revision( $newRevision );
1655  $this->mNewid = $newRevision->getId();
1656  $this->mNewPage = Title::newFromLinkTarget( $newRevision->getPageAsLinkTarget() );
1657  $this->mNewContent = $newRevision->getContent( SlotRecord::MAIN,
1658  RevisionRecord::FOR_THIS_USER, $this->getUser() );
1659 
1660  $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded = true;
1661  $this->mTextLoaded = $oldRevision ? 2 : 1;
1662  $this->isContentOverridden = false;
1663  $this->slotDiffRenderers = null;
1664  }
1665 
1672  public function setTextLanguage( Language $lang ) {
1673  $this->mDiffLang = $lang;
1674  }
1675 
1687  public function mapDiffPrevNext( $old, $new ) {
1688  if ( $new === 'prev' ) {
1689  // Show diff between revision $old and the previous one. Get previous one from DB.
1690  $newid = intval( $old );
1691  $oldid = $this->getTitle()->getPreviousRevisionID( $newid );
1692  } elseif ( $new === 'next' ) {
1693  // Show diff between revision $old and the next one. Get next one from DB.
1694  $oldid = intval( $old );
1695  $newid = $this->getTitle()->getNextRevisionID( $oldid );
1696  } else {
1697  $oldid = intval( $old );
1698  $newid = intval( $new );
1699  }
1700 
1701  return [ $oldid, $newid ];
1702  }
1703 
1707  private function loadRevisionIds() {
1708  if ( $this->mRevisionsIdsLoaded ) {
1709  return;
1710  }
1711 
1712  $this->mRevisionsIdsLoaded = true;
1713 
1714  $old = $this->mOldid;
1715  $new = $this->mNewid;
1716 
1717  list( $this->mOldid, $this->mNewid ) = self::mapDiffPrevNext( $old, $new );
1718  if ( $new === 'next' && $this->mNewid === false ) {
1719  # if no result, NewId points to the newest old revision. The only newer
1720  # revision is cur, which is "0".
1721  $this->mNewid = 0;
1722  }
1723 
1724  Hooks::run(
1725  'NewDifferenceEngine',
1726  [ $this->getTitle(), &$this->mOldid, &$this->mNewid, $old, $new ]
1727  );
1728  }
1729 
1743  public function loadRevisionData() {
1744  if ( $this->mRevisionsLoaded ) {
1745  return $this->isContentOverridden || ( $this->mOldRev !== null && $this->mNewRev !== null );
1746  }
1747 
1748  // Whether it succeeds or fails, we don't want to try again
1749  $this->mRevisionsLoaded = true;
1750 
1751  $this->loadRevisionIds();
1752 
1753  // Load the new revision object
1754  if ( $this->mNewid ) {
1755  $this->mNewRev = Revision::newFromId( $this->mNewid );
1756  } else {
1757  $this->mNewRev = Revision::newFromTitle(
1758  $this->getTitle(),
1759  false,
1760  Revision::READ_NORMAL
1761  );
1762  }
1763 
1764  if ( !$this->mNewRev instanceof Revision ) {
1765  return false;
1766  }
1767 
1768  // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
1769  $this->mNewid = $this->mNewRev->getId();
1770  if ( $this->mNewid ) {
1771  $this->mNewPage = $this->mNewRev->getTitle();
1772  } else {
1773  $this->mNewPage = null;
1774  }
1775 
1776  // Load the old revision object
1777  $this->mOldRev = false;
1778  if ( $this->mOldid ) {
1779  $this->mOldRev = Revision::newFromId( $this->mOldid );
1780  } elseif ( $this->mOldid === 0 ) {
1781  $rev = $this->mNewRev->getPrevious();
1782  if ( $rev ) {
1783  $this->mOldid = $rev->getId();
1784  $this->mOldRev = $rev;
1785  } else {
1786  // No previous revision; mark to show as first-version only.
1787  $this->mOldid = false;
1788  $this->mOldRev = false;
1789  }
1790  } /* elseif ( $this->mOldid === false ) leave mOldRev false; */
1791 
1792  if ( is_null( $this->mOldRev ) ) {
1793  return false;
1794  }
1795 
1796  if ( $this->mOldRev && $this->mOldRev->getId() ) {
1797  $this->mOldPage = $this->mOldRev->getTitle();
1798  } else {
1799  $this->mOldPage = null;
1800  }
1801 
1802  // Load tags information for both revisions
1803  $dbr = wfGetDB( DB_REPLICA );
1804  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
1805  if ( $this->mOldid !== false ) {
1806  $tagIds = $dbr->selectFieldValues(
1807  'change_tag',
1808  'ct_tag_id',
1809  [ 'ct_rev_id' => $this->mOldid ],
1810  __METHOD__
1811  );
1812  $tags = [];
1813  foreach ( $tagIds as $tagId ) {
1814  try {
1815  $tags[] = $changeTagDefStore->getName( (int)$tagId );
1816  } catch ( NameTableAccessException $exception ) {
1817  continue;
1818  }
1819  }
1820  $this->mOldTags = implode( ',', $tags );
1821  } else {
1822  $this->mOldTags = false;
1823  }
1824 
1825  $tagIds = $dbr->selectFieldValues(
1826  'change_tag',
1827  'ct_tag_id',
1828  [ 'ct_rev_id' => $this->mNewid ],
1829  __METHOD__
1830  );
1831  $tags = [];
1832  foreach ( $tagIds as $tagId ) {
1833  try {
1834  $tags[] = $changeTagDefStore->getName( (int)$tagId );
1835  } catch ( NameTableAccessException $exception ) {
1836  continue;
1837  }
1838  }
1839  $this->mNewTags = implode( ',', $tags );
1840 
1841  return true;
1842  }
1843 
1852  public function loadText() {
1853  if ( $this->mTextLoaded == 2 ) {
1854  return $this->loadRevisionData() && ( $this->mOldRev === false || $this->mOldContent )
1855  && $this->mNewContent;
1856  }
1857 
1858  // Whether it succeeds or fails, we don't want to try again
1859  $this->mTextLoaded = 2;
1860 
1861  if ( !$this->loadRevisionData() ) {
1862  return false;
1863  }
1864 
1865  if ( $this->mOldRev ) {
1866  $this->mOldContent = $this->mOldRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
1867  if ( $this->mOldContent === null ) {
1868  return false;
1869  }
1870  }
1871 
1872  $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
1873  Hooks::run( 'DifferenceEngineLoadTextAfterNewContentIsLoaded', [ $this ] );
1874  if ( $this->mNewContent === null ) {
1875  return false;
1876  }
1877 
1878  return true;
1879  }
1880 
1886  public function loadNewText() {
1887  if ( $this->mTextLoaded >= 1 ) {
1888  return $this->loadRevisionData();
1889  }
1890 
1891  $this->mTextLoaded = 1;
1892 
1893  if ( !$this->loadRevisionData() ) {
1894  return false;
1895  }
1896 
1897  $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
1898 
1899  Hooks::run( 'DifferenceEngineAfterLoadNewText', [ $this ] );
1900 
1901  return true;
1902  }
1903 
1904 }
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:138
Revision null false $mOldRev
Old revision (left pane).
setContext(IContextSource $context)
userCan( $field, User $user=null)
Determine if the current user is allowed to view a particular field of this revision, if it&#39;s marked as deleted.
Definition: Revision.php:1226
bool $mCacheHit
Was the diff fetched from cache?
const FOR_THIS_USER
Definition: Revision.php:55
DifferenceEngine is responsible for rendering the difference between two revisions as HTML...
__construct( $context=null, $old=0, $new=0, $rcid=0, $refreshCache=false, $unhide=false)
#-
$enableDebugComment
Set this to true to add debug info to the HTML output.
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition: deferred.txt:11
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
static newFromArchiveRow( $row, $overrides=[])
Make a fake revision object from an archive table row.
Definition: Revision.php:171
getDiffBody()
Get the diff table body, without header.
setTextLanguage(Language $lang)
Set the language in which the diff text is written.
null for the local wiki Added should default to null in handler for backwards compatibility add a value to it if you want to add a cookie that have to vary cache options can modify $query
Definition: hooks.txt:1585
$data
Utility to generate mapping file used in mw.Title (phpCharToUpper.json)
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:232
getContentHandler()
Convenience method that returns the ContentHandler singleton for handling the content model that this...
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
markAsSlotDiffRenderer()
Mark this DifferenceEngine as a slot renderer (as opposed to a page renderer).
getTimestamp()
Definition: Revision.php:994
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
Exception representing a failure to look up a row from a name table.
int false null $mOldid
Revision ID for the old revision.
wfHostname()
Fetch server name for use in error reporting etc.
if(!isset( $args[0])) $lang
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing &#39;/&#39;...
Definition: Html.php:252
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:210
string [] null $mOldTags
Change tags of $mOldRev or null if it does not exist / is not saved.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
static intermediateEditsMsg( $numEdits, $numUsers, $limit)
Get a notice about how many intermediate edits and users there are.
getParserOutput(WikiPage $page, Revision $rev)
$value
const NS_SPECIAL
Definition: Defines.php:53
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency MediaWikiServices
Definition: injection.txt:23
msg( $key)
Get a Message object with context set Parameters are the same as wfMessage()
setRevisions(RevisionRecord $oldRevision=null, RevisionRecord $newRevision)
Use specified text instead of loading from the database.
getMarkPatrolledLinkInfo()
Returns an array of meta data needed to build a "mark as patrolled" link and adds the mediawiki...
showDiffPage( $diffOnly=false)
const CONTENT_MODEL_TEXT
Definition: Defines.php:238
also included in $newHeader if any $newminor
Definition: hooks.txt:1266
getExtraCacheKeys()
Implements DifferenceEngineSlotDiffRenderer::getExtraCacheKeys().
B/C adapter for turning a DifferenceEngine into a SlotDiffRenderer.
const DIFF_VERSION
Constant to indicate diff cache compatibility.
bool $isContentOverridden
Was the content overridden via setContent()? If the content was overridden, most internal state (e...
static generateRollback( $rev, IContextSource $context=null, $options=[ 'verify'])
Generate a rollback link for a given revision.
Definition: Linker.php:1762
loadRevisionIds()
Load revision IDs.
static newFromTitle(LinkTarget $linkTarget, $id=0, $flags=0)
Load either the current, or a specified, revision that&#39;s attached to a given link target...
Definition: Revision.php:137
getNewRevision()
Get the right side of the diff.
IContextSource $context
debug( $generator="internal")
Generate a debug comment indicating diff generating time, server node, and generator backend...
getDiff( $otitle, $ntitle, $notice='')
Get complete diff table, including header.
usually copyright or history_copyright This message must be in HTML not wikitext & $link
Definition: hooks.txt:3050
$newRev
Definition: pageupdater.txt:66
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output $out
Definition: hooks.txt:780
Title null $mOldPage
Title of $mOldRev or null if the old revision does not exist or does not belong to a page...
generateContentDiffBody(Content $old, Content $new)
Generate a diff, no caching.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
string $mMarkPatrolledLink
Link to action=markpatrolled.
getTitle()
Returns the title of the page associated with this entry.
Definition: Revision.php:755
wfMergeErrorArrays(... $args)
Merge arrays in the style of getUserPermissionsErrors, with duplicate removal e.g.
wfIncrStats( $key, $count=1)
Increment a statistics counter.
localiseLineNumbers( $text)
Replace line numbers with the text in the user&#39;s language.
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation use $formDescriptor instead default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt
getPageAsLinkTarget()
Returns the title of the page this revision is associated with as a LinkTarget object.
static getForModelID( $modelId)
Returns the ContentHandler singleton for the given model ID.
Content null $mOldContent
renderNewRevision()
Show the new revision of the page.
static titleAttrib( $name, $options=null, array $msgParams=[])
Given the id of an interface element, constructs the appropriate title attribute from the system mess...
Definition: Linker.php:1977
static flag( $flag, IContextSource $context=null)
Make an "<abbr>" element for a given change flag.
bool $mRefreshCache
Refresh the diff cache.
Renders a slot diff by doing a text diff on the native representation.
Revision null $mNewRev
New revision (right pane).
null for the local wiki Added should default to null in handler for backwards compatibility add a value to it if you want to add a cookie that have to vary cache options can modify as strings Extensions should add to this list prev or next $refreshCache
Definition: hooks.txt:1585
isDeleted( $field)
Definition: Revision.php:888
mapDiffPrevNext( $old, $new)
Maps a revision pair definition as accepted by DifferenceEngine constructor to a pair of actual integ...
getId()
Get revision ID.
Definition: Revision.php:638
bool $mRevisionsIdsLoaded
Have the revisions IDs been loaded.
$wgExternalDiffEngine
Name of the external diff engine to use.
getSlotContents()
Get the old and new content objects for all slots.
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
markPatrolledLink()
Build a link to mark a change as patrolled.
getContent( $role, $audience=self::FOR_PUBLIC, User $user=null)
Returns the Content of the given slot of this revision.
addHeader( $diff, $otitle, $ntitle, $multi='', $notice='')
Add the header to a diff body.
static getEngine()
Process $wgExternalDiffEngine and get a sane, usable engine.
addLocalisedTitleTooltipsCb(array $matches)
getContext()
Get the base IContextSource object.
textDiff( $otext, $ntext)
Generates diff, to be wrapped internally in a logging/instrumentation.
$cache
Definition: mcc.php:33
$params
Title null $mNewPage
Title of $mNewRev or null if the new revision does not exist or does not belong to a page...
getMultiNotice()
If there are revisions between the ones being compared, return a note saying so.
passed in as a query string parameter to the various URLs constructed here(i.e. $nextlink) $rdel also included in $oldHeader $oldminor
Definition: hooks.txt:1270
const DELETED_RESTRICTED
Definition: Revision.php:49
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:780
bool $unhide
Show rev_deleted content if allowed.
static getArchiveQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new archived revision objec...
Definition: Revision.php:525
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:925
setReducedLineNumbers( $value=true)
Set reduced line numbers mode.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don&#39;t need a full Title object...
Definition: SpecialPage.php:82
$header
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition: hooks.txt:1766
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
setContent(Content $oldContent, Content $newContent)
Use specified text instead of loading from the database.
const RAW
Definition: Revision.php:56
int $mTextLoaded
How many text blobs have been loaded, 0, 1 or 2?
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
getDiffBodyCacheKeyParams()
Get the cache key parameters.
getNewid()
Get the ID of new revision (right pane) of the diff.
the value to return A Title object or null for latest all implement SearchIndexField $engine
Definition: hooks.txt:2898
getOldRevision()
Get the left side of the diff.
const DELETED_TEXT
Definition: Revision.php:46
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:617
const PRC_UNPATROLLED
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
Definition: Title.php:271
deprecatePublicProperty( $property, $version, $class=null, $component=null)
Mark a property as deprecated.
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
Definition: WikiPage.php:1945
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:589
static isInRCLifespan( $timestamp, $tolerance=0)
Check whether the given timestamp is new enough to have a RC row with a given tolerance as the recent...
getId()
Get revision ID.
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
addLocalisedTitleTooltips( $text)
Add title attributes for tooltips on moved paragraph indicators.
static getRevDeleteLink(User $user, Revision $rev, LinkTarget $title)
Get a revision-deletion link, or disabled link, or nothing, depending on user permissions & the setti...
Definition: Linker.php:2061
getOldid()
Get the ID of old revision (left pane) of the diff.
trait DeprecationHelper
Use this trait in classes which have properties for which public access is deprecated.
static linkKnown( $target, $html=null, $customAttribs=[], $query=[], $options=[ 'known'])
Identical to link(), except $options defaults to &#39;known&#39;.
Definition: Linker.php:146
localiseDiff( $text)
Localise diff output.
int string false null $mNewid
Revision ID for the new revision.
bool $mRevisionsLoaded
Have the revisions been loaded.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
Show an error when a user tries to do something they do not have the necessary permissions for...
deletedIdMarker( $id)
Build a wikitext link toward a deleted revision, if viewable.
bool $isSlotDiffRenderer
Temporary hack for B/C while slot diff related methods of DifferenceEngine are being deprecated...
showDiffStyle()
Add style sheets for diff display.
bool $mReducedLineNumbers
If true, line X is not displayed when X is 1, for example to increase readability and conserve space ...
loadNewText()
Load the text of the new revision, not the old one.
static formatSummaryRow( $tags, $page, IContextSource $context=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:93
also included in $newHeader $rollback
Definition: hooks.txt:1266
static revComment(Revision $rev, $local=false, $isPublic=false, $useParentheses=true)
Wrap and format the given revision&#39;s comment block, if the current user is allowed to view it...
Definition: Linker.php:1522
getParserOutput(ParserOptions $parserOptions, $oldid=null, $forceParse=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1223
showDiff( $otitle, $ntitle, $notice='')
Get the diff text, send it to the OutputPage object Returns false if the diff could not be generated...
Page revision base class.
getDiffBodyCacheKey()
Returns the cache key for diff body text or content.
const DB_REPLICA
Definition: defines.php:25
string [] null $mNewTags
Change tags of $mNewRev or null if it does not exist / is not saved.
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException' returning false will NOT prevent logging a wrapping ErrorException $suppressed
Definition: hooks.txt:2159
generateTextDiffBody( $otext, $ntext)
Generate a diff, no caching.
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition: Revision.php:118
getSlotHeader( $headerText)
Get a slot header for inclusion in a diff body (as a table row).
$content
Definition: pageupdater.txt:72
getWikiPage()
Get the WikiPage object.
Content null $mNewContent
loadRevisionData()
Load revision metadata for the specified revisions.
deletedLink( $id)
Look up a special:Undelete link to the given deleted revision id, as a workaround for being unable to...
localiseLineNumbersCb( $matches)
getRevisionHeader(Revision $rev, $complete='')
Get a header for a specified revision.
SlotDiffRenderer [] $slotDiffRenderers
DifferenceEngine classes for the slots, keyed by role name.
getDiffBodyForRole( $role)
Get the diff table body for one slot, without header.
getDiffLang()
Get the language of the difference engine, defaults to page content language.
return true to allow those checks to and false if checking is done & $user
Definition: hooks.txt:1473
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
loadText()
Load the text of the revisions, as well as revision data.
$matches
static revUserTools( $rev, $isPublic=false, $useParentheses=true)
Generate a user tool link cluster if the current user is allowed to view it.
Definition: Linker.php:1090