MediaWiki  master
DifferenceEngine.php
Go to the documentation of this file.
1 <?php
29 
53 
55 
62  const DIFF_VERSION = '1.12';
63 
70  protected $mOldid;
71 
78  protected $mNewid;
79 
91  protected $mOldRev;
92 
102  protected $mNewRev;
103 
109  protected $mOldPage;
110 
116  protected $mNewPage;
117 
122  private $mOldTags;
123 
128  private $mNewTags;
129 
135  private $mOldContent;
136 
142  private $mNewContent;
143 
145  protected $mDiffLang;
146 
148  private $mRevisionsIdsLoaded = false;
149 
151  protected $mRevisionsLoaded = false;
152 
154  protected $mTextLoaded = 0;
155 
164  protected $isContentOverridden = false;
165 
167  protected $mCacheHit = false;
168 
174  public $enableDebugComment = false;
175 
179  protected $mReducedLineNumbers = false;
180 
182  protected $mMarkPatrolledLink = null;
183 
185  protected $unhide = false;
186 
188  protected $mRefreshCache = false;
189 
191  protected $slotDiffRenderers = null;
192 
199  protected $isSlotDiffRenderer = false;
200 
201  /* A set of options that will be passed to the SlotDiffRenderer upon creation
202  * @var array
203  */
204  private $slotDiffOptions = [];
205 
209  protected $linkRenderer;
210 
221  public function __construct( $context = null, $old = 0, $new = 0, $rcid = 0,
222  $refreshCache = false, $unhide = false
223  ) {
224  $this->deprecatePublicProperty( 'mOldid', '1.32', __CLASS__ );
225  $this->deprecatePublicProperty( 'mNewid', '1.32', __CLASS__ );
226  $this->deprecatePublicProperty( 'mOldRev', '1.32', __CLASS__ );
227  $this->deprecatePublicProperty( 'mNewRev', '1.32', __CLASS__ );
228  $this->deprecatePublicProperty( 'mOldPage', '1.32', __CLASS__ );
229  $this->deprecatePublicProperty( 'mNewPage', '1.32', __CLASS__ );
230  $this->deprecatePublicProperty( 'mOldContent', '1.32', __CLASS__ );
231  $this->deprecatePublicProperty( 'mNewContent', '1.32', __CLASS__ );
232  $this->deprecatePublicProperty( 'mRevisionsLoaded', '1.32', __CLASS__ );
233  $this->deprecatePublicProperty( 'mTextLoaded', '1.32', __CLASS__ );
234  $this->deprecatePublicProperty( 'mCacheHit', '1.32', __CLASS__ );
235 
236  if ( $context instanceof IContextSource ) {
237  $this->setContext( $context );
238  }
239 
240  wfDebug( "DifferenceEngine old '$old' new '$new' rcid '$rcid'\n" );
241 
242  $this->mOldid = $old;
243  $this->mNewid = $new;
244  $this->mRefreshCache = $refreshCache;
245  $this->unhide = $unhide;
246  $this->linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
247  }
248 
253  protected function getSlotDiffRenderers() {
254  if ( $this->isSlotDiffRenderer ) {
255  throw new LogicException( __METHOD__ . ' called in slot diff renderer mode' );
256  }
257 
258  if ( $this->slotDiffRenderers === null ) {
259  if ( !$this->loadRevisionData() ) {
260  return [];
261  }
262 
263  $slotContents = $this->getSlotContents();
264  $this->slotDiffRenderers = array_map( function ( $contents ) {
266  $content = $contents['new'] ?: $contents['old'];
267  $context = $this->getContext();
268 
269  return $content->getContentHandler()->getSlotDiffRenderer(
270  $context,
271  $this->slotDiffOptions
272  );
273  }, $slotContents );
274  }
276  }
277 
284  public function markAsSlotDiffRenderer() {
285  $this->isSlotDiffRenderer = true;
286  }
287 
293  protected function getSlotContents() {
294  if ( $this->isContentOverridden ) {
295  return [
296  SlotRecord::MAIN => [
297  'old' => $this->mOldContent,
298  'new' => $this->mNewContent,
299  ]
300  ];
301  } elseif ( !$this->loadRevisionData() ) {
302  return [];
303  }
304 
305  $newSlots = $this->mNewRev->getRevisionRecord()->getSlots()->getSlots();
306  if ( $this->mOldRev ) {
307  $oldSlots = $this->mOldRev->getRevisionRecord()->getSlots()->getSlots();
308  } else {
309  $oldSlots = [];
310  }
311  // The order here will determine the visual order of the diff. The current logic is
312  // slots of the new revision first in natural order, then deleted ones. This is ad hoc
313  // and should not be relied on - in the future we may want the ordering to depend
314  // on the page type.
315  $roles = array_merge( array_keys( $newSlots ), array_keys( $oldSlots ) );
316 
317  $slots = [];
318  foreach ( $roles as $role ) {
319  $slots[$role] = [
320  'old' => isset( $oldSlots[$role] ) ? $oldSlots[$role]->getContent() : null,
321  'new' => isset( $newSlots[$role] ) ? $newSlots[$role]->getContent() : null,
322  ];
323  }
324  // move main slot to front
325  if ( isset( $slots[SlotRecord::MAIN] ) ) {
326  $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
327  }
328  return $slots;
329  }
330 
331  public function getTitle() {
332  // T202454 avoid errors when there is no title
333  return parent::getTitle() ?: Title::makeTitle( NS_SPECIAL, 'BadTitle/DifferenceEngine' );
334  }
335 
342  public function setReducedLineNumbers( $value = true ) {
343  $this->mReducedLineNumbers = $value;
344  }
345 
351  public function getDiffLang() {
352  if ( $this->mDiffLang === null ) {
353  # Default language in which the diff text is written.
354  $this->mDiffLang = $this->getTitle()->getPageLanguage();
355  }
356 
357  return $this->mDiffLang;
358  }
359 
363  public function wasCacheHit() {
364  return $this->mCacheHit;
365  }
366 
374  public function getOldid() {
375  $this->loadRevisionIds();
376 
377  return $this->mOldid;
378  }
379 
386  public function getNewid() {
387  $this->loadRevisionIds();
388 
389  return $this->mNewid;
390  }
391 
398  public function getOldRevision() {
399  return $this->mOldRev ? $this->mOldRev->getRevisionRecord() : null;
400  }
401 
407  public function getNewRevision() {
408  return $this->mNewRev ? $this->mNewRev->getRevisionRecord() : null;
409  }
410 
419  public function deletedLink( $id ) {
420  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
421  if ( $permissionManager->userHasRight( $this->getUser(), 'deletedhistory' ) ) {
422  $dbr = wfGetDB( DB_REPLICA );
423  $arQuery = Revision::getArchiveQueryInfo();
424  $row = $dbr->selectRow(
425  $arQuery['tables'],
426  array_merge( $arQuery['fields'], [ 'ar_namespace', 'ar_title' ] ),
427  [ 'ar_rev_id' => $id ],
428  __METHOD__,
429  [],
430  $arQuery['joins']
431  );
432  if ( $row ) {
433  $rev = Revision::newFromArchiveRow( $row );
434  $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
435 
436  return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( [
437  'target' => $title->getPrefixedText(),
438  'timestamp' => $rev->getTimestamp()
439  ] );
440  }
441  }
442 
443  return false;
444  }
445 
453  public function deletedIdMarker( $id ) {
454  $link = $this->deletedLink( $id );
455  if ( $link ) {
456  return "[$link $id]";
457  } else {
458  return (string)$id;
459  }
460  }
461 
462  private function showMissingRevision() {
463  $out = $this->getOutput();
464 
465  $missing = [];
466  if ( $this->mOldRev === null ||
467  ( $this->mOldRev && $this->mOldContent === null )
468  ) {
469  $missing[] = $this->deletedIdMarker( $this->mOldid );
470  }
471  if ( $this->mNewRev === null ||
472  ( $this->mNewRev && $this->mNewContent === null )
473  ) {
474  $missing[] = $this->deletedIdMarker( $this->mNewid );
475  }
476 
477  $out->setPageTitle( $this->msg( 'errorpagetitle' ) );
478  $msg = $this->msg( 'difference-missing-revision' )
479  ->params( $this->getLanguage()->listToText( $missing ) )
480  ->numParams( count( $missing ) )
481  ->parseAsBlock();
482  $out->addHTML( $msg );
483  }
484 
490  public function hasDeletedRevision() {
491  $this->loadRevisionData();
492  return ( $this->mNewRev && $this->mNewRev->isDeleted( RevisionRecord::DELETED_TEXT ) ) ||
493  ( $this->mOldRev && $this->mOldRev->isDeleted( RevisionRecord::DELETED_TEXT ) );
494  }
495 
502  public function getPermissionErrors( User $user ) {
503  $this->loadRevisionData();
504  $permErrors = [];
505  if ( $this->mNewPage ) {
506  $permErrors = $this->mNewPage->getUserPermissionsErrors( 'read', $user );
507  }
508  if ( $this->mOldPage ) {
509  $permErrors = wfMergeErrorArrays( $permErrors,
510  $this->mOldPage->getUserPermissionsErrors( 'read', $user ) );
511  }
512  return $permErrors;
513  }
514 
520  public function hasSuppressedRevision() {
521  return $this->hasDeletedRevision() &&
522  (
523  ( $this->mOldRev && $this->mOldRev->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) ||
524  ( $this->mNewRev && $this->mNewRev->isDeleted( RevisionRecord::DELETED_RESTRICTED ) )
525  );
526  }
527 
539  public function isUserAllowedToSeeRevisions( $user ) {
540  $this->loadRevisionData();
541  // $this->mNewRev will only be falsy if a loading error occurred
542  // (in which case the user is allowed to see).
543  $allowed = !$this->mNewRev || $this->mNewRev->userCan( RevisionRecord::DELETED_TEXT, $user );
544  if ( $this->mOldRev &&
545  !$this->mOldRev->userCan( RevisionRecord::DELETED_TEXT, $user )
546  ) {
547  $allowed = false;
548  }
549  return $allowed;
550  }
551 
559  public function shouldBeHiddenFromUser( $user ) {
560  return $this->hasDeletedRevision() && ( !$this->unhide ||
561  !$this->isUserAllowedToSeeRevisions( $user ) );
562  }
563 
564  public function showDiffPage( $diffOnly = false ) {
565  # Allow frames except in certain special cases
566  $out = $this->getOutput();
567  $out->allowClickjacking();
568  $out->setRobotPolicy( 'noindex,nofollow' );
569 
570  // Allow extensions to add any extra output here
571  Hooks::run( 'DifferenceEngineShowDiffPage', [ $out ] );
572 
573  if ( !$this->loadRevisionData() ) {
574  if ( Hooks::run( 'DifferenceEngineShowDiffPageMaybeShowMissingRevision', [ $this ] ) ) {
575  $this->showMissingRevision();
576  }
577  return;
578  }
579 
580  $user = $this->getUser();
581  $permErrors = $this->getPermissionErrors( $user );
582  if ( count( $permErrors ) ) {
583  throw new PermissionsError( 'read', $permErrors );
584  }
585 
586  $rollback = '';
587 
588  $query = $this->slotDiffOptions;
589  # Carry over 'diffonly' param via navigation links
590  if ( $diffOnly != $user->getBoolOption( 'diffonly' ) ) {
591  $query['diffonly'] = $diffOnly;
592  }
593  # Cascade unhide param in links for easy deletion browsing
594  if ( $this->unhide ) {
595  $query['unhide'] = 1;
596  }
597 
598  # Check if one of the revisions is deleted/suppressed
599  $deleted = $this->hasDeletedRevision();
600  $suppressed = $this->hasSuppressedRevision();
601  $allowed = $this->isUserAllowedToSeeRevisions( $user );
602 
603  $revisionTools = [];
604 
605  # mOldRev is false if the difference engine is called with a "vague" query for
606  # a diff between a version V and its previous version V' AND the version V
607  # is the first version of that article. In that case, V' does not exist.
608  if ( $this->mOldRev === false ) {
609  if ( $this->mNewPage ) {
610  $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
611  }
612  $samePage = true;
613  $oldHeader = '';
614  // Allow extensions to change the $oldHeader variable
615  Hooks::run( 'DifferenceEngineOldHeaderNoOldRev', [ &$oldHeader ] );
616  } else {
617  Hooks::run( 'DiffViewHeader', [ $this, $this->mOldRev, $this->mNewRev ] );
618 
619  if ( !$this->mOldPage || !$this->mNewPage ) {
620  // XXX say something to the user?
621  $samePage = false;
622  } elseif ( $this->mNewPage->equals( $this->mOldPage ) ) {
623  $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
624  $samePage = true;
625  } else {
626  $out->setPageTitle( $this->msg( 'difference-title-multipage',
627  $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) );
628  $out->addSubtitle( $this->msg( 'difference-multipage' ) );
629  $samePage = false;
630  }
631 
632  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
633 
634  if ( $samePage && $this->mNewPage && $permissionManager->quickUserCan(
635  'edit', $user, $this->mNewPage
636  ) ) {
637  if ( $this->mNewRev->isCurrent() && $permissionManager->quickUserCan(
638  'rollback', $user, $this->mNewPage
639  ) ) {
640  $rollbackLink = Linker::generateRollback( $this->mNewRev, $this->getContext(),
641  [ 'noBrackets' ] );
642  if ( $rollbackLink ) {
643  $out->preventClickjacking();
644  $rollback = "\u{00A0}\u{00A0}\u{00A0}" . $rollbackLink;
645  }
646  }
647 
648  if ( $this->userCanEdit( $this->mOldRev ) &&
649  $this->userCanEdit( $this->mNewRev )
650  ) {
651  $undoLink = Html::element( 'a', [
652  'href' => $this->mNewPage->getLocalURL( [
653  'action' => 'edit',
654  'undoafter' => $this->mOldid,
655  'undo' => $this->mNewid
656  ] ),
657  'title' => Linker::titleAttrib( 'undo' ),
658  ],
659  $this->msg( 'editundo' )->text()
660  );
661  $revisionTools['mw-diff-undo'] = $undoLink;
662  }
663  }
664 
665  # Make "previous revision link"
666  if ( $samePage && $this->mOldPage && $this->mOldRev->getPrevious() ) {
667  $prevlink = $this->linkRenderer->makeKnownLink(
668  $this->mOldPage,
669  $this->msg( 'previousdiff' )->text(),
670  [ 'id' => 'differences-prevlink' ],
671  [ 'diff' => 'prev', 'oldid' => $this->mOldid ] + $query
672  );
673  } else {
674  $prevlink = "\u{00A0}";
675  }
676 
677  if ( $this->mOldRev->isMinor() ) {
678  $oldminor = ChangesList::flag( 'minor' );
679  } else {
680  $oldminor = '';
681  }
682 
683  $ldel = $this->revisionDeleteLink( $this->mOldRev );
684  $oldRevisionHeader = $this->getRevisionHeader( $this->mOldRev, 'complete' );
685  $oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff', $this->getContext() );
686 
687  $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' .
688  '<div id="mw-diff-otitle2">' .
689  Linker::revUserTools( $this->mOldRev, !$this->unhide ) . '</div>' .
690  '<div id="mw-diff-otitle3">' . $oldminor .
691  Linker::revComment( $this->mOldRev, !$diffOnly, !$this->unhide ) . $ldel . '</div>' .
692  '<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' .
693  '<div id="mw-diff-otitle4">' . $prevlink . '</div>';
694 
695  // Allow extensions to change the $oldHeader variable
696  Hooks::run( 'DifferenceEngineOldHeader', [ $this, &$oldHeader, $prevlink, $oldminor,
697  $diffOnly, $ldel, $this->unhide ] );
698  }
699 
700  $out->addJsConfigVars( [
701  'wgDiffOldId' => $this->mOldid,
702  'wgDiffNewId' => $this->mNewid,
703  ] );
704 
705  # Make "next revision link"
706  # Skip next link on the top revision
707  if ( $samePage && $this->mNewPage && !$this->mNewRev->isCurrent() ) {
708  $nextlink = $this->linkRenderer->makeKnownLink(
709  $this->mNewPage,
710  $this->msg( 'nextdiff' )->text(),
711  [ 'id' => 'differences-nextlink' ],
712  [ 'diff' => 'next', 'oldid' => $this->mNewid ] + $query
713  );
714  } else {
715  $nextlink = "\u{00A0}";
716  }
717 
718  if ( $this->mNewRev->isMinor() ) {
719  $newminor = ChangesList::flag( 'minor' );
720  } else {
721  $newminor = '';
722  }
723 
724  # Handle RevisionDelete links...
725  $rdel = $this->revisionDeleteLink( $this->mNewRev );
726 
727  # Allow extensions to define their own revision tools
728  Hooks::run( 'DiffRevisionTools',
729  [ $this->mNewRev, &$revisionTools, $this->mOldRev, $user ] );
730  $formattedRevisionTools = [];
731  // Put each one in parentheses (poor man's button)
732  foreach ( $revisionTools as $key => $tool ) {
733  $toolClass = is_string( $key ) ? $key : 'mw-diff-tool';
734  $element = Html::rawElement(
735  'span',
736  [ 'class' => $toolClass ],
737  $this->msg( 'parentheses' )->rawParams( $tool )->escaped()
738  );
739  $formattedRevisionTools[] = $element;
740  }
741  $newRevisionHeader = $this->getRevisionHeader( $this->mNewRev, 'complete' ) .
742  ' ' . implode( ' ', $formattedRevisionTools );
743  $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff', $this->getContext() );
744 
745  $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' .
746  '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $this->mNewRev, !$this->unhide ) .
747  " $rollback</div>" .
748  '<div id="mw-diff-ntitle3">' . $newminor .
749  Linker::revComment( $this->mNewRev, !$diffOnly, !$this->unhide ) . $rdel . '</div>' .
750  '<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' .
751  '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
752 
753  // Allow extensions to change the $newHeader variable
754  Hooks::run( 'DifferenceEngineNewHeader', [ $this, &$newHeader, $formattedRevisionTools,
755  $nextlink, $rollback, $newminor, $diffOnly, $rdel, $this->unhide ] );
756 
757  # If the diff cannot be shown due to a deleted revision, then output
758  # the diff header and links to unhide (if available)...
759  if ( $this->shouldBeHiddenFromUser( $user ) ) {
760  $this->showDiffStyle();
761  $multi = $this->getMultiNotice();
762  $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
763  if ( !$allowed ) {
764  $msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff';
765  # Give explanation for why revision is not visible
766  $out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
767  [ $msg ] );
768  } else {
769  # Give explanation and add a link to view the diff...
770  $query = $this->getRequest()->appendQueryValue( 'unhide', '1' );
771  $link = $this->getTitle()->getFullURL( $query );
772  $msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff';
773  $out->wrapWikiMsg(
774  "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
775  [ $msg, $link ]
776  );
777  }
778  # Otherwise, output a regular diff...
779  } else {
780  # Add deletion notice if the user is viewing deleted content
781  $notice = '';
782  if ( $deleted ) {
783  $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
784  $notice = "<div id='mw-$msg' class='mw-warning plainlinks'>\n" .
785  $this->msg( $msg )->parse() .
786  "</div>\n";
787  }
788  $this->showDiff( $oldHeader, $newHeader, $notice );
789  if ( !$diffOnly ) {
790  $this->renderNewRevision();
791  }
792  }
793  }
794 
804  public function markPatrolledLink() {
805  if ( $this->mMarkPatrolledLink === null ) {
806  $linkInfo = $this->getMarkPatrolledLinkInfo();
807  // If false, there is no patrol link needed/allowed
808  if ( !$linkInfo || !$this->mNewPage ) {
809  $this->mMarkPatrolledLink = '';
810  } else {
811  $this->mMarkPatrolledLink = ' <span class="patrollink" data-mw="interface">[' .
812  $this->linkRenderer->makeKnownLink(
813  $this->mNewPage,
814  $this->msg( 'markaspatrolleddiff' )->text(),
815  [],
816  [
817  'action' => 'markpatrolled',
818  'rcid' => $linkInfo['rcid'],
819  ]
820  ) . ']</span>';
821  // Allow extensions to change the markpatrolled link
822  Hooks::run( 'DifferenceEngineMarkPatrolledLink', [ $this,
823  &$this->mMarkPatrolledLink, $linkInfo['rcid'] ] );
824  }
825  }
827  }
828 
836  protected function getMarkPatrolledLinkInfo() {
837  $user = $this->getUser();
838  $config = $this->getConfig();
839  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
840 
841  // Prepare a change patrol link, if applicable
842  if (
843  // Is patrolling enabled and the user allowed to?
844  $config->get( 'UseRCPatrol' ) &&
845  $this->mNewPage &&
846  $permissionManager->quickUserCan( 'patrol', $user, $this->mNewPage ) &&
847  // Only do this if the revision isn't more than 6 hours older
848  // than the Max RC age (6h because the RC might not be cleaned out regularly)
849  RecentChange::isInRCLifespan( $this->mNewRev->getTimestamp(), 21600 )
850  ) {
851  // Look for an unpatrolled change corresponding to this diff
852  $change = RecentChange::newFromConds(
853  [
854  'rc_this_oldid' => $this->mNewid,
855  'rc_patrolled' => RecentChange::PRC_UNPATROLLED
856  ],
857  __METHOD__
858  );
859 
860  if ( $change && !$change->getPerformer()->equals( $user ) ) {
861  $rcid = $change->getAttribute( 'rc_id' );
862  } else {
863  // None found or the page has been created by the current user.
864  // If the user could patrol this it already would be patrolled
865  $rcid = 0;
866  }
867 
868  // Allow extensions to possibly change the rcid here
869  // For example the rcid might be set to zero due to the user
870  // being the same as the performer of the change but an extension
871  // might still want to show it under certain conditions
872  Hooks::run( 'DifferenceEngineMarkPatrolledRCID', [ &$rcid, $this, $change, $user ] );
873 
874  // Build the link
875  if ( $rcid ) {
876  $this->getOutput()->preventClickjacking();
877  if ( $permissionManager->userHasRight( $user, 'writeapi' ) ) {
878  $this->getOutput()->addModules( 'mediawiki.page.patrol.ajax' );
879  }
880 
881  return [
882  'rcid' => $rcid,
883  ];
884  }
885  }
886 
887  // No mark as patrolled link applicable
888  return false;
889  }
890 
896  protected function revisionDeleteLink( $rev ) {
897  $link = Linker::getRevDeleteLink( $this->getUser(), $rev, $rev->getTitle() );
898  if ( $link !== '' ) {
899  $link = "\u{00A0}\u{00A0}\u{00A0}" . $link . ' ';
900  }
901 
902  return $link;
903  }
904 
910  public function renderNewRevision() {
911  if ( $this->isContentOverridden ) {
912  // The code below only works with a Revision object. We could construct a fake revision
913  // (here or in setContent), but since this does not seem needed at the moment,
914  // we'll just fail for now.
915  throw new LogicException(
916  __METHOD__
917  . ' is not supported after calling setContent(). Use setRevisions() instead.'
918  );
919  }
920 
921  $out = $this->getOutput();
922  $revHeader = $this->getRevisionHeader( $this->mNewRev );
923  # Add "current version as of X" title
924  $out->addHTML( "<hr class='diff-hr' id='mw-oldid' />
925  <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
926  # Page content may be handled by a hooked call instead...
927  if ( Hooks::run( 'ArticleContentOnDiff', [ $this, $out ] ) ) {
928  $this->loadNewText();
929  if ( !$this->mNewPage ) {
930  // New revision is unsaved; bail out.
931  // TODO in theory rendering the new revision is a meaningful thing to do
932  // even if it's unsaved, but a lot of untangling is required to do it safely.
933  return;
934  }
935 
936  $out->setRevisionId( $this->mNewid );
937  $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() );
938  $out->setArticleFlag( true );
939 
940  if ( !Hooks::run( 'ArticleRevisionViewCustom',
941  [ $this->mNewRev->getRevisionRecord(), $this->mNewPage, $this->mOldid, $out ] )
942  ) {
943  // Handled by extension
944  // NOTE: sync with hooks called in Article::view()
945  } elseif ( !Hooks::run( 'ArticleContentViewCustom',
946  [ $this->mNewContent, $this->mNewPage, $out ], '1.32' )
947  ) {
948  // Handled by extension
949  // NOTE: sync with hooks called in Article::view()
950  } else {
951  // Normal page
952  if ( $this->getTitle()->equals( $this->mNewPage ) ) {
953  // If the Title stored in the context is the same as the one
954  // of the new revision, we can use its associated WikiPage
955  // object.
956  $wikiPage = $this->getWikiPage();
957  } else {
958  // Otherwise we need to create our own WikiPage object
959  $wikiPage = WikiPage::factory( $this->mNewPage );
960  }
961 
962  $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRev );
963 
964  # WikiPage::getParserOutput() should not return false, but just in case
965  if ( $parserOutput ) {
966  // Allow extensions to change parser output here
967  if ( Hooks::run( 'DifferenceEngineRenderRevisionAddParserOutput',
968  [ $this, $out, $parserOutput, $wikiPage ] )
969  ) {
970  $out->addParserOutput( $parserOutput, [
971  'enableSectionEditLinks' => $this->mNewRev->isCurrent()
972  && MediaWikiServices::getInstance()->getPermissionManager()->quickUserCan(
973  'edit',
974  $this->getUser(),
975  $this->mNewRev->getTitle()
976  )
977  ] );
978  }
979  }
980  }
981  }
982 
983  // Allow extensions to optionally not show the final patrolled link
984  if ( Hooks::run( 'DifferenceEngineRenderRevisionShowFinalPatrolLink' ) ) {
985  # Add redundant patrol link on bottom...
986  $out->addHTML( $this->markPatrolledLink() );
987  }
988  }
989 
996  protected function getParserOutput( WikiPage $page, Revision $rev ) {
997  if ( !$rev->getId() ) {
998  // WikiPage::getParserOutput wants a revision ID. Passing 0 will incorrectly show
999  // the current revision, so fail instead. If need be, WikiPage::getParserOutput
1000  // could be made to accept a Revision or RevisionRecord instead of the id.
1001  return false;
1002  }
1003 
1004  $parserOptions = $page->makeParserOptions( $this->getContext() );
1005  $parserOutput = $page->getParserOutput( $parserOptions, $rev->getId() );
1006 
1007  return $parserOutput;
1008  }
1009 
1020  public function showDiff( $otitle, $ntitle, $notice = '' ) {
1021  // Allow extensions to affect the output here
1022  Hooks::run( 'DifferenceEngineShowDiff', [ $this ] );
1023 
1024  $diff = $this->getDiff( $otitle, $ntitle, $notice );
1025  if ( $diff === false ) {
1026  $this->showMissingRevision();
1027 
1028  return false;
1029  } else {
1030  $this->showDiffStyle();
1031  $this->getOutput()->addHTML( $diff );
1032 
1033  return true;
1034  }
1035  }
1036 
1040  public function showDiffStyle() {
1041  if ( !$this->isSlotDiffRenderer ) {
1042  $this->getOutput()->addModuleStyles( [
1043  'mediawiki.interface.helpers.styles',
1044  'mediawiki.diff.styles'
1045  ] );
1046  foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1047  $slotDiffRenderer->addModules( $this->getOutput() );
1048  }
1049  }
1050  }
1051 
1061  public function getDiff( $otitle, $ntitle, $notice = '' ) {
1062  $body = $this->getDiffBody();
1063  if ( $body === false ) {
1064  return false;
1065  }
1066 
1067  $multi = $this->getMultiNotice();
1068  // Display a message when the diff is empty
1069  if ( $body === '' ) {
1070  $notice .= '<div class="mw-diff-empty">' .
1071  $this->msg( 'diff-empty' )->parse() .
1072  "</div>\n";
1073  }
1074 
1075  return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
1076  }
1077 
1083  public function getDiffBody() {
1084  $this->mCacheHit = true;
1085  // Check if the diff should be hidden from this user
1086  if ( !$this->isContentOverridden ) {
1087  if ( !$this->loadRevisionData() ) {
1088  return false;
1089  } elseif ( $this->mOldRev &&
1090  !$this->mOldRev->userCan( RevisionRecord::DELETED_TEXT, $this->getUser() )
1091  ) {
1092  return false;
1093  } elseif ( $this->mNewRev &&
1094  !$this->mNewRev->userCan( RevisionRecord::DELETED_TEXT, $this->getUser() )
1095  ) {
1096  return false;
1097  }
1098  // Short-circuit
1099  if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev &&
1100  $this->mOldRev->getId() && $this->mOldRev->getId() == $this->mNewRev->getId() )
1101  ) {
1102  if ( Hooks::run( 'DifferenceEngineShowEmptyOldContent', [ $this ] ) ) {
1103  return '';
1104  }
1105  }
1106  }
1107 
1108  // Cacheable?
1109  $key = false;
1110  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1111  if ( $this->mOldid && $this->mNewid ) {
1112  // Check if subclass is still using the old way
1113  // for backwards-compatibility
1114  $key = $this->getDiffBodyCacheKey();
1115  if ( $key === null ) {
1116  $key = $cache->makeKey( ...$this->getDiffBodyCacheKeyParams() );
1117  }
1118 
1119  // Try cache
1120  if ( !$this->mRefreshCache ) {
1121  $difftext = $cache->get( $key );
1122  if ( is_string( $difftext ) ) {
1123  wfIncrStats( 'diff_cache.hit' );
1124  $difftext = $this->localiseDiff( $difftext );
1125  $difftext .= "\n<!-- diff cache key $key -->\n";
1126 
1127  return $difftext;
1128  }
1129  } // don't try to load but save the result
1130  }
1131  $this->mCacheHit = false;
1132 
1133  // Loadtext is permission safe, this just clears out the diff
1134  if ( !$this->loadText() ) {
1135  return false;
1136  }
1137 
1138  $difftext = '';
1139  // We've checked for revdelete at the beginning of this method; it's OK to ignore
1140  // read permissions here.
1141  $slotContents = $this->getSlotContents();
1142  foreach ( $this->getSlotDiffRenderers() as $role => $slotDiffRenderer ) {
1143  $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role]['old'],
1144  $slotContents[$role]['new'] );
1145  if ( $slotDiff && $role !== SlotRecord::MAIN ) {
1146  // FIXME: ask SlotRoleHandler::getSlotNameMessage
1147  $slotTitle = $role;
1148  $difftext .= $this->getSlotHeader( $slotTitle );
1149  }
1150  $difftext .= $slotDiff;
1151  }
1152 
1153  // Avoid PHP 7.1 warning from passing $this by reference
1154  $diffEngine = $this;
1155 
1156  // Save to cache for 7 days
1157  if ( !Hooks::run( 'AbortDiffCache', [ &$diffEngine ] ) ) {
1158  wfIncrStats( 'diff_cache.uncacheable' );
1159  } elseif ( $key !== false && $difftext !== false ) {
1160  wfIncrStats( 'diff_cache.miss' );
1161  $cache->set( $key, $difftext, 7 * 86400 );
1162  } else {
1163  wfIncrStats( 'diff_cache.uncacheable' );
1164  }
1165  // localise line numbers and title attribute text
1166  if ( $difftext !== false ) {
1167  $difftext = $this->localiseDiff( $difftext );
1168  }
1169 
1170  return $difftext;
1171  }
1172 
1179  public function getDiffBodyForRole( $role ) {
1180  $diffRenderers = $this->getSlotDiffRenderers();
1181  if ( !isset( $diffRenderers[$role] ) ) {
1182  return false;
1183  }
1184 
1185  $slotContents = $this->getSlotContents();
1186  $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role]['old'],
1187  $slotContents[$role]['new'] );
1188  if ( !$slotDiff ) {
1189  return false;
1190  }
1191 
1192  if ( $role !== SlotRecord::MAIN ) {
1193  // TODO use human-readable role name at least
1194  $slotTitle = $role;
1195  $slotDiff = $this->getSlotHeader( $slotTitle ) . $slotDiff;
1196  }
1197 
1198  return $this->localiseDiff( $slotDiff );
1199  }
1200 
1208  protected function getSlotHeader( $headerText ) {
1209  // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
1210  $columnCount = $this->mOldRev ? 4 : 2;
1211  $userLang = $this->getLanguage()->getHtmlCode();
1212  return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-header', 'lang' => $userLang ],
1213  Html::element( 'th', [ 'colspan' => $columnCount ], $headerText ) );
1214  }
1215 
1225  protected function getDiffBodyCacheKey() {
1226  return null;
1227  }
1228 
1242  protected function getDiffBodyCacheKeyParams() {
1243  if ( !$this->mOldid || !$this->mNewid ) {
1244  throw new MWException( 'mOldid and mNewid must be set to get diff cache key.' );
1245  }
1246 
1247  $engine = $this->getEngine();
1248  $params = [
1249  'diff',
1250  $engine === 'php' ? false : $engine, // Back compat
1252  "old-{$this->mOldid}",
1253  "rev-{$this->mNewid}"
1254  ];
1255 
1256  if ( $engine === 'wikidiff2' ) {
1257  $params[] = phpversion( 'wikidiff2' );
1258  }
1259 
1260  if ( !$this->isSlotDiffRenderer ) {
1261  foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1262  $params = array_merge( $params, $slotDiffRenderer->getExtraCacheKeys() );
1263  }
1264  }
1265 
1266  return $params;
1267  }
1268 
1276  public function getExtraCacheKeys() {
1277  // This method is called when the DifferenceEngine is used for a slot diff. We only care
1278  // about special things, not the revision IDs, which are added to the cache key by the
1279  // page-level DifferenceEngine, and which might not have a valid value for this object.
1280  $this->mOldid = 123456789;
1281  $this->mNewid = 987654321;
1282 
1283  // This will repeat a bunch of unnecessary key fields for each slot. Not nice but harmless.
1284  $cacheString = $this->getDiffBodyCacheKey();
1285  if ( $cacheString ) {
1286  return [ $cacheString ];
1287  }
1288 
1289  $params = $this->getDiffBodyCacheKeyParams();
1290 
1291  // Try to get rid of the standard keys to keep the cache key human-readable:
1292  // call the getDiffBodyCacheKeyParams implementation of the base class, and if
1293  // the child class includes the same keys, drop them.
1294  // Uses an obscure PHP feature where static calls to non-static methods are allowed
1295  // as long as we are already in a non-static method of the same class, and the call context
1296  // ($this) will be inherited.
1297  // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
1298  $standardParams = DifferenceEngine::getDiffBodyCacheKeyParams();
1299  if ( array_slice( $params, 0, count( $standardParams ) ) === $standardParams ) {
1300  $params = array_slice( $params, count( $standardParams ) );
1301  }
1302 
1303  return $params;
1304  }
1305 
1309  public function setSlotDiffOptions( $options ) {
1310  $this->slotDiffOptions = $options;
1311  }
1312 
1326  public function generateContentDiffBody( Content $old, Content $new ) {
1327  $slotDiffRenderer = $new->getContentHandler()->getSlotDiffRenderer( $this->getContext() );
1328  if (
1329  $slotDiffRenderer instanceof DifferenceEngineSlotDiffRenderer
1330  && $this->isSlotDiffRenderer
1331  ) {
1332  // Oops, we are just about to enter an infinite loop (the slot-level DifferenceEngine
1333  // called a DifferenceEngineSlotDiffRenderer that wraps the same DifferenceEngine class).
1334  // This will happen when a content model has no custom slot diff renderer, it does have
1335  // a custom difference engine, but that does not override this method.
1336  throw new Exception( get_class( $this ) . ': could not maintain backwards compatibility. '
1337  . 'Please use a SlotDiffRenderer.' );
1338  }
1339  return $slotDiffRenderer->getDiff( $old, $new ) . $this->getDebugString();
1340  }
1341 
1354  public function generateTextDiffBody( $otext, $ntext ) {
1355  $slotDiffRenderer = ContentHandler::getForModelID( CONTENT_MODEL_TEXT )
1356  ->getSlotDiffRenderer( $this->getContext() );
1357  if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1358  // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1359  // This is too unlikely to happen to bother handling properly.
1360  throw new Exception( 'The slot diff renderer for text content should be a '
1361  . 'TextSlotDiffRenderer subclass' );
1362  }
1363  return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1364  }
1365 
1372  public static function getEngine() {
1373  $diffEngine = MediaWikiServices::getInstance()->getMainConfig()
1374  ->get( 'DiffEngine' );
1375  $externalDiffEngine = MediaWikiServices::getInstance()->getMainConfig()
1376  ->get( 'ExternalDiffEngine' );
1377 
1378  if ( $diffEngine === null ) {
1379  $engines = [ 'external', 'wikidiff2', 'php' ];
1380  } else {
1381  $engines = [ $diffEngine ];
1382  }
1383 
1384  $failureReason = null;
1385  foreach ( $engines as $engine ) {
1386  switch ( $engine ) {
1387  case 'external':
1388  if ( is_string( $externalDiffEngine ) ) {
1389  if ( is_executable( $externalDiffEngine ) ) {
1390  return $externalDiffEngine;
1391  }
1392  $failureReason = 'ExternalDiffEngine config points to a non-executable';
1393  if ( $diffEngine === null ) {
1394  wfDebug( "$failureReason, ignoring" );
1395  }
1396  } else {
1397  $failureReason = 'ExternalDiffEngine config is set to a non-string value';
1398  if ( $diffEngine === null && $externalDiffEngine ) {
1399  wfWarn( "$failureReason, ignoring" );
1400  }
1401  }
1402  break;
1403 
1404  case 'wikidiff2':
1405  if ( function_exists( 'wikidiff2_do_diff' ) ) {
1406  return 'wikidiff2';
1407  }
1408  $failureReason = 'wikidiff2 is not available';
1409  break;
1410 
1411  case 'php':
1412  // Always available.
1413  return 'php';
1414 
1415  default:
1416  throw new DomainException( 'Invalid value for $wgDiffEngine: ' . $engine );
1417  }
1418  }
1419  throw new UnexpectedValueException( "Cannot use diff engine '$engine': $failureReason" );
1420  }
1421 
1434  protected function textDiff( $otext, $ntext ) {
1435  $slotDiffRenderer = ContentHandler::getForModelID( CONTENT_MODEL_TEXT )
1436  ->getSlotDiffRenderer( $this->getContext() );
1437  if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1438  // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1439  // This is too unlikely to happen to bother handling properly.
1440  throw new Exception( 'The slot diff renderer for text content should be a '
1441  . 'TextSlotDiffRenderer subclass' );
1442  }
1443  return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1444  }
1445 
1454  protected function debug( $generator = "internal" ) {
1455  if ( !$this->enableDebugComment ) {
1456  return '';
1457  }
1458  $data = [ $generator ];
1459  if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
1460  $data[] = wfHostname();
1461  }
1462  $data[] = wfTimestamp( TS_DB );
1463 
1464  return "<!-- diff generator: " .
1465  implode( " ", array_map( "htmlspecialchars", $data ) ) .
1466  " -->\n";
1467  }
1468 
1469  private function getDebugString() {
1470  $engine = self::getEngine();
1471  if ( $engine === 'wikidiff2' ) {
1472  return $this->debug( 'wikidiff2' );
1473  } elseif ( $engine === 'php' ) {
1474  return $this->debug( 'native PHP' );
1475  } else {
1476  return $this->debug( "external $engine" );
1477  }
1478  }
1479 
1486  private function localiseDiff( $text ) {
1487  $text = $this->localiseLineNumbers( $text );
1488  if ( $this->getEngine() === 'wikidiff2' &&
1489  version_compare( phpversion( 'wikidiff2' ), '1.5.1', '>=' )
1490  ) {
1491  $text = $this->addLocalisedTitleTooltips( $text );
1492  }
1493  return $text;
1494  }
1495 
1503  public function localiseLineNumbers( $text ) {
1504  return preg_replace_callback(
1505  '/<!--LINE (\d+)-->/',
1506  [ $this, 'localiseLineNumbersCb' ],
1507  $text
1508  );
1509  }
1510 
1511  public function localiseLineNumbersCb( $matches ) {
1512  if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
1513  return '';
1514  }
1515 
1516  return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
1517  }
1518 
1525  private function addLocalisedTitleTooltips( $text ) {
1526  return preg_replace_callback(
1527  '/class="mw-diff-movedpara-(left|right)"/',
1528  [ $this, 'addLocalisedTitleTooltipsCb' ],
1529  $text
1530  );
1531  }
1532 
1537  private function addLocalisedTitleTooltipsCb( array $matches ) {
1538  $key = $matches[1] === 'right' ?
1539  'diff-paragraph-moved-toold' :
1540  'diff-paragraph-moved-tonew';
1541  return $matches[0] . ' title="' . $this->msg( $key )->escaped() . '"';
1542  }
1543 
1549  public function getMultiNotice() {
1550  // The notice only make sense if we are diffing two saved revisions of the same page.
1551  if (
1552  !$this->mOldRev || !$this->mNewRev
1553  || !$this->mOldPage || !$this->mNewPage
1554  || !$this->mOldPage->equals( $this->mNewPage )
1555  || $this->mOldRev->getId() === null || $this->mNewRev->getId() === null
1556  // (T237709) Deleted revs might have different page IDs
1557  || $this->mNewPage->getArticleId() !== $this->mOldRev->getPage()
1558  || $this->mNewPage->getArticleId() !== $this->mNewRev->getPage()
1559  ) {
1560  return '';
1561  }
1562 
1563  if ( $this->mOldRev->getTimestamp() > $this->mNewRev->getTimestamp() ) {
1564  $oldRev = $this->mNewRev; // flip
1565  $newRev = $this->mOldRev; // flip
1566  } else { // normal case
1567  $oldRev = $this->mOldRev;
1568  $newRev = $this->mNewRev;
1569  }
1570 
1571  // Sanity: don't show the notice if too many rows must be scanned
1572  // @todo show some special message for that case
1573  $nEdits = MediaWikiServices::getInstance()->getRevisionStore()
1574  ->countRevisionsBetween(
1575  $this->mNewPage->getArticleID(),
1576  $oldRev->getRevisionRecord(),
1577  $newRev->getRevisionRecord(),
1578  1000
1579  );
1580  if ( $nEdits > 0 && $nEdits <= 1000 ) {
1581  $limit = 100; // use diff-multi-manyusers if too many users
1582  $users = $this->mNewPage->getAuthorsBetween( $oldRev, $newRev, $limit );
1583  $numUsers = count( $users );
1584 
1585  // @phan-suppress-next-line PhanTypeArraySuspiciousNullable False positive
1586  if ( $numUsers == 1 && $users[0] == $newRev->getUserText( RevisionRecord::RAW ) ) {
1587  $numUsers = 0; // special case to say "by the same user" instead of "by one other user"
1588  }
1589 
1590  return self::intermediateEditsMsg( $nEdits, $numUsers, $limit );
1591  }
1592 
1593  return '';
1594  }
1595 
1605  public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) {
1606  if ( $numUsers === 0 ) {
1607  $msg = 'diff-multi-sameuser';
1608  } elseif ( $numUsers > $limit ) {
1609  $msg = 'diff-multi-manyusers';
1610  $numUsers = $limit;
1611  } else {
1612  $msg = 'diff-multi-otherusers';
1613  }
1614 
1615  return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1616  }
1617 
1622  private function userCanEdit( Revision $rev ) {
1623  $user = $this->getUser();
1624 
1625  if ( !$rev->userCan( RevisionRecord::DELETED_TEXT, $user ) ) {
1626  return false;
1627  }
1628 
1629  return true;
1630  }
1631 
1641  public function getRevisionHeader( Revision $rev, $complete = '' ) {
1642  $lang = $this->getLanguage();
1643  $user = $this->getUser();
1644  $revtimestamp = $rev->getTimestamp();
1645  $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
1646  $dateofrev = $lang->userDate( $revtimestamp, $user );
1647  $timeofrev = $lang->userTime( $revtimestamp, $user );
1648 
1649  $header = $this->msg(
1650  $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
1651  $timestamp,
1652  $dateofrev,
1653  $timeofrev
1654  );
1655 
1656  if ( $complete !== 'complete' ) {
1657  return $header->escaped();
1658  }
1659 
1660  $title = $rev->getTitle();
1661 
1662  $header = $this->linkRenderer->makeKnownLink( $title, $header->text(), [],
1663  [ 'oldid' => $rev->getId() ] );
1664 
1665  if ( $this->userCanEdit( $rev ) ) {
1666  $editQuery = [ 'action' => 'edit' ];
1667  if ( !$rev->isCurrent() ) {
1668  $editQuery['oldid'] = $rev->getId();
1669  }
1670 
1671  $key = MediaWikiServices::getInstance()->getPermissionManager()
1672  ->quickUserCan( 'edit', $user, $title ) ? 'editold' : 'viewsourceold';
1673  $msg = $this->msg( $key )->text();
1674  $editLink = $this->msg( 'parentheses' )->rawParams(
1675  $this->linkRenderer->makeKnownLink( $title, $msg, [], $editQuery ) )->escaped();
1676  $header .= ' ' . Html::rawElement(
1677  'span',
1678  [ 'class' => 'mw-diff-edit' ],
1679  $editLink
1680  );
1681  if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1682  $header = Html::rawElement(
1683  'span',
1684  [ 'class' => 'history-deleted' ],
1685  $header
1686  );
1687  }
1688  } else {
1689  $header = Html::rawElement( 'span', [ 'class' => 'history-deleted' ], $header );
1690  }
1691 
1692  return $header;
1693  }
1694 
1707  public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
1708  // shared.css sets diff in interface language/dir, but the actual content
1709  // is often in a different language, mostly the page content language/dir
1710  $header = Html::openElement( 'table', [
1711  'class' => [ 'diff', 'diff-contentalign-' . $this->getDiffLang()->alignStart() ],
1712  'data-mw' => 'interface',
1713  ] );
1714  $userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
1715 
1716  if ( !$diff && !$otitle ) {
1717  $header .= "
1718  <tr class=\"diff-title\" lang=\"{$userLang}\">
1719  <td class=\"diff-ntitle\">{$ntitle}</td>
1720  </tr>";
1721  $multiColspan = 1;
1722  } else {
1723  if ( $diff ) { // Safari/Chrome show broken output if cols not used
1724  $header .= "
1725  <col class=\"diff-marker\" />
1726  <col class=\"diff-content\" />
1727  <col class=\"diff-marker\" />
1728  <col class=\"diff-content\" />";
1729  $colspan = 2;
1730  $multiColspan = 4;
1731  } else {
1732  $colspan = 1;
1733  $multiColspan = 2;
1734  }
1735  if ( $otitle || $ntitle ) {
1736  $header .= "
1737  <tr class=\"diff-title\" lang=\"{$userLang}\">
1738  <td colspan=\"$colspan\" class=\"diff-otitle\">{$otitle}</td>
1739  <td colspan=\"$colspan\" class=\"diff-ntitle\">{$ntitle}</td>
1740  </tr>";
1741  }
1742  }
1743 
1744  if ( $multi != '' ) {
1745  $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1746  "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
1747  }
1748  if ( $notice != '' ) {
1749  $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1750  "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
1751  }
1752 
1753  return $header . $diff . "</table>";
1754  }
1755 
1763  public function setContent( Content $oldContent, Content $newContent ) {
1764  $this->mOldContent = $oldContent;
1765  $this->mNewContent = $newContent;
1766 
1767  $this->mTextLoaded = 2;
1768  $this->mRevisionsLoaded = true;
1769  $this->isContentOverridden = true;
1770  $this->slotDiffRenderers = null;
1771  }
1772 
1778  public function setRevisions(
1779  ?RevisionRecord $oldRevision, RevisionRecord $newRevision
1780  ) {
1781  if ( $oldRevision ) {
1782  $this->mOldRev = new Revision( $oldRevision );
1783  $this->mOldid = $oldRevision->getId();
1784  $this->mOldPage = Title::newFromLinkTarget( $oldRevision->getPageAsLinkTarget() );
1785  // This method is meant for edit diffs and such so there is no reason to provide a
1786  // revision that's not readable to the user, but check it just in case.
1787  $this->mOldContent = $oldRevision->getContent( SlotRecord::MAIN,
1788  RevisionRecord::FOR_THIS_USER, $this->getUser() );
1789  } else {
1790  $this->mOldPage = null;
1791  $this->mOldRev = $this->mOldid = false;
1792  }
1793  $this->mNewRev = new Revision( $newRevision );
1794  $this->mNewid = $newRevision->getId();
1795  $this->mNewPage = Title::newFromLinkTarget( $newRevision->getPageAsLinkTarget() );
1796  $this->mNewContent = $newRevision->getContent( SlotRecord::MAIN,
1797  RevisionRecord::FOR_THIS_USER, $this->getUser() );
1798 
1799  $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded = true;
1800  $this->mTextLoaded = $oldRevision ? 2 : 1;
1801  $this->isContentOverridden = false;
1802  $this->slotDiffRenderers = null;
1803  }
1804 
1811  public function setTextLanguage( Language $lang ) {
1812  $this->mDiffLang = $lang;
1813  }
1814 
1826  public function mapDiffPrevNext( $old, $new ) {
1827  $rl = MediaWikiServices::getInstance()->getRevisionLookup();
1828  if ( $new === 'prev' ) {
1829  // Show diff between revision $old and the previous one. Get previous one from DB.
1830  $newid = intval( $old );
1831  $oldid = false;
1832  $newRev = $rl->getRevisionById( $newid );
1833  if ( $newRev ) {
1834  $oldRev = $rl->getPreviousRevision( $newRev );
1835  if ( $oldRev ) {
1836  $oldid = $oldRev->getId();
1837  }
1838  }
1839  } elseif ( $new === 'next' ) {
1840  // Show diff between revision $old and the next one. Get next one from DB.
1841  $oldid = intval( $old );
1842  $newid = false;
1843  $oldRev = $rl->getRevisionById( $oldid );
1844  if ( $oldRev ) {
1845  $newRev = $rl->getNextRevision( $oldRev );
1846  if ( $newRev ) {
1847  $newid = $newRev->getId();
1848  }
1849  }
1850  } else {
1851  $oldid = intval( $old );
1852  $newid = intval( $new );
1853  }
1854 
1855  return [ $oldid, $newid ];
1856  }
1857 
1861  private function loadRevisionIds() {
1862  if ( $this->mRevisionsIdsLoaded ) {
1863  return;
1864  }
1865 
1866  $this->mRevisionsIdsLoaded = true;
1867 
1868  $old = $this->mOldid;
1869  $new = $this->mNewid;
1870 
1871  list( $this->mOldid, $this->mNewid ) = self::mapDiffPrevNext( $old, $new );
1872  if ( $new === 'next' && $this->mNewid === false ) {
1873  # if no result, NewId points to the newest old revision. The only newer
1874  # revision is cur, which is "0".
1875  $this->mNewid = 0;
1876  }
1877 
1878  Hooks::run(
1879  'NewDifferenceEngine',
1880  [ $this->getTitle(), &$this->mOldid, &$this->mNewid, $old, $new ]
1881  );
1882  }
1883 
1897  public function loadRevisionData() {
1898  if ( $this->mRevisionsLoaded ) {
1899  return $this->isContentOverridden || ( $this->mOldRev !== null && $this->mNewRev !== null );
1900  }
1901 
1902  // Whether it succeeds or fails, we don't want to try again
1903  $this->mRevisionsLoaded = true;
1904 
1905  $this->loadRevisionIds();
1906 
1907  // Load the new revision object
1908  if ( $this->mNewid ) {
1909  $this->mNewRev = Revision::newFromId( $this->mNewid );
1910  } else {
1911  $this->mNewRev = Revision::newFromTitle(
1912  $this->getTitle(),
1913  false,
1914  Revision::READ_NORMAL
1915  );
1916  }
1917 
1918  if ( !$this->mNewRev instanceof Revision ) {
1919  return false;
1920  }
1921 
1922  // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
1923  $this->mNewid = $this->mNewRev->getId();
1924  if ( $this->mNewid ) {
1925  $this->mNewPage = $this->mNewRev->getTitle();
1926  } else {
1927  $this->mNewPage = null;
1928  }
1929 
1930  // Load the old revision object
1931  $this->mOldRev = false;
1932  if ( $this->mOldid ) {
1933  $this->mOldRev = Revision::newFromId( $this->mOldid );
1934  } elseif ( $this->mOldid === 0 ) {
1935  $rev = $this->mNewRev->getPrevious();
1936  if ( $rev ) {
1937  $this->mOldid = $rev->getId();
1938  $this->mOldRev = $rev;
1939  } else {
1940  // No previous revision; mark to show as first-version only.
1941  $this->mOldid = false;
1942  $this->mOldRev = false;
1943  }
1944  } /* elseif ( $this->mOldid === false ) leave mOldRev false; */
1945 
1946  if ( $this->mOldRev === null ) {
1947  return false;
1948  }
1949 
1950  if ( $this->mOldRev && $this->mOldRev->getId() ) {
1951  $this->mOldPage = $this->mOldRev->getTitle();
1952  } else {
1953  $this->mOldPage = null;
1954  }
1955 
1956  // Load tags information for both revisions
1957  $dbr = wfGetDB( DB_REPLICA );
1958  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
1959  if ( $this->mOldid !== false ) {
1960  $tagIds = $dbr->selectFieldValues(
1961  'change_tag',
1962  'ct_tag_id',
1963  [ 'ct_rev_id' => $this->mOldid ],
1964  __METHOD__
1965  );
1966  $tags = [];
1967  foreach ( $tagIds as $tagId ) {
1968  try {
1969  $tags[] = $changeTagDefStore->getName( (int)$tagId );
1970  } catch ( NameTableAccessException $exception ) {
1971  continue;
1972  }
1973  }
1974  $this->mOldTags = implode( ',', $tags );
1975  } else {
1976  $this->mOldTags = false;
1977  }
1978 
1979  $tagIds = $dbr->selectFieldValues(
1980  'change_tag',
1981  'ct_tag_id',
1982  [ 'ct_rev_id' => $this->mNewid ],
1983  __METHOD__
1984  );
1985  $tags = [];
1986  foreach ( $tagIds as $tagId ) {
1987  try {
1988  $tags[] = $changeTagDefStore->getName( (int)$tagId );
1989  } catch ( NameTableAccessException $exception ) {
1990  continue;
1991  }
1992  }
1993  $this->mNewTags = implode( ',', $tags );
1994 
1995  return true;
1996  }
1997 
2006  public function loadText() {
2007  if ( $this->mTextLoaded == 2 ) {
2008  return $this->loadRevisionData() && ( $this->mOldRev === false || $this->mOldContent )
2009  && $this->mNewContent;
2010  }
2011 
2012  // Whether it succeeds or fails, we don't want to try again
2013  $this->mTextLoaded = 2;
2014 
2015  if ( !$this->loadRevisionData() ) {
2016  return false;
2017  }
2018 
2019  if ( $this->mOldRev ) {
2020  $this->mOldContent = $this->mOldRev->getContent(
2021  RevisionRecord::FOR_THIS_USER, $this->getUser()
2022  );
2023  if ( $this->mOldContent === null ) {
2024  return false;
2025  }
2026  }
2027 
2028  $this->mNewContent = $this->mNewRev->getContent(
2029  RevisionRecord::FOR_THIS_USER, $this->getUser()
2030  );
2031  Hooks::run( 'DifferenceEngineLoadTextAfterNewContentIsLoaded', [ $this ] );
2032  if ( $this->mNewContent === null ) {
2033  return false;
2034  }
2035 
2036  return true;
2037  }
2038 
2044  public function loadNewText() {
2045  if ( $this->mTextLoaded >= 1 ) {
2046  return $this->loadRevisionData();
2047  }
2048 
2049  $this->mTextLoaded = 1;
2050 
2051  if ( !$this->loadRevisionData() ) {
2052  return false;
2053  }
2054 
2055  $this->mNewContent = $this->mNewRev->getContent(
2056  RevisionRecord::FOR_THIS_USER, $this->getUser()
2057  );
2058 
2059  Hooks::run( 'DifferenceEngineAfterLoadNewText', [ $this ] );
2060 
2061  return true;
2062  }
2063 
2064 }
Content\getContentHandler
getContentHandler()
Convenience method that returns the ContentHandler singleton for handling the content model that this...
Revision\newFromArchiveRow
static newFromArchiveRow( $row, $overrides=[])
Make a fake revision object from an archive table row.
Definition: Revision.php:172
DifferenceEngine\$mRevisionsIdsLoaded
bool $mRevisionsIdsLoaded
Have the revisions IDs been loaded.
Definition: DifferenceEngine.php:148
DifferenceEngine\revisionDeleteLink
revisionDeleteLink( $rev)
Definition: DifferenceEngine.php:896
ContextSource\$context
IContextSource $context
Definition: ContextSource.php:33
ContextSource\getConfig
getConfig()
Definition: ContextSource.php:63
Revision\getTimestamp
getTimestamp()
Definition: Revision.php:798
ContentHandler\getForModelID
static getForModelID( $modelId)
Returns the ContentHandler singleton for the given model ID.
Definition: ContentHandler.php:254
DifferenceEngine\getSlotContents
getSlotContents()
Get the old and new content objects for all slots.
Definition: DifferenceEngine.php:293
wfMergeErrorArrays
wfMergeErrorArrays(... $args)
Merge arrays in the style of getUserPermissionsErrors, with duplicate removal e.g.
Definition: GlobalFunctions.php:181
DifferenceEngine\markPatrolledLink
markPatrolledLink()
Build a link to mark a change as patrolled.
Definition: DifferenceEngine.php:804
ContextSource\getContext
getContext()
Get the base IContextSource object.
Definition: ContextSource.php:40
Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:46
DifferenceEngine\getEngine
static getEngine()
Process DiffEngine config and get a sane, usable engine.
Definition: DifferenceEngine.php:1372
DifferenceEngine\$mTextLoaded
int $mTextLoaded
How many text blobs have been loaded, 0, 1 or 2?
Definition: DifferenceEngine.php:154
DifferenceEngine\addLocalisedTitleTooltipsCb
addLocalisedTitleTooltipsCb(array $matches)
Definition: DifferenceEngine.php:1537
DifferenceEngine\getDiffBodyCacheKeyParams
getDiffBodyCacheKeyParams()
Get the cache key parameters.
Definition: DifferenceEngine.php:1242
Revision\newFromId
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition: Revision.php:119
DifferenceEngine\$unhide
bool $unhide
Show rev_deleted content if allowed.
Definition: DifferenceEngine.php:185
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:130
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:35
RecentChange\newFromConds
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
Definition: RecentChange.php:208
DifferenceEngine\setReducedLineNumbers
setReducedLineNumbers( $value=true)
Set reduced line numbers mode.
Definition: DifferenceEngine.php:342
DifferenceEngine\shouldBeHiddenFromUser
shouldBeHiddenFromUser( $user)
Checks whether the diff should be hidden from the current user This is based on whether the user is a...
Definition: DifferenceEngine.php:559
DifferenceEngine\setContent
setContent(Content $oldContent, Content $newContent)
Use specified text instead of loading from the database.
Definition: DifferenceEngine.php:1763
MediaWiki\Linker\LinkRenderer
Class that generates HTML links for pages.
Definition: LinkRenderer.php:41
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1871
DifferenceEngine\getNewid
getNewid()
Get the ID of new revision (right pane) of the diff.
Definition: DifferenceEngine.php:386
DifferenceEngine\getOldRevision
getOldRevision()
Get the left side of the diff.
Definition: DifferenceEngine.php:398
DifferenceEngine\getOldid
getOldid()
Get the ID of old revision (left pane) of the diff.
Definition: DifferenceEngine.php:374
WikiPage
Class representing a MediaWiki article and history.
Definition: WikiPage.php:46
DifferenceEngine\$mRevisionsLoaded
bool $mRevisionsLoaded
Have the revisions been loaded.
Definition: DifferenceEngine.php:151
DifferenceEngine\deletedIdMarker
deletedIdMarker( $id)
Build a wikitext link toward a deleted revision, if viewable.
Definition: DifferenceEngine.php:453
Linker\revComment
static revComment(Revision $rev, $local=false, $isPublic=false, $useParentheses=true)
Wrap and format the given revision's comment block, if the current user is allowed to view it.
Definition: Linker.php:1574
WikiPage\makeParserOptions
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
Definition: WikiPage.php:1960
wfHostname
wfHostname()
Fetch server name for use in error reporting etc.
Definition: GlobalFunctions.php:1325
DifferenceEngine\$isSlotDiffRenderer
bool $isSlotDiffRenderer
Temporary hack for B/C while slot diff related methods of DifferenceEngine are being deprecated.
Definition: DifferenceEngine.php:199
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1263
Revision\getArchiveQueryInfo
static getArchiveQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new archived revision objec...
Definition: Revision.php:329
SpecialPage\getTitleFor
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
Definition: SpecialPage.php:83
DifferenceEngine\addLocalisedTitleTooltips
addLocalisedTitleTooltips( $text)
Add title attributes for tooltips on moved paragraph indicators.
Definition: DifferenceEngine.php:1525
Revision\isCurrent
isCurrent()
Definition: Revision.php:805
ContextSource\getRequest
getRequest()
Definition: ContextSource.php:71
PermissionsError
Show an error when a user tries to do something they do not have the necessary permissions for.
Definition: PermissionsError.php:30
DifferenceEngine\showDiffStyle
showDiffStyle()
Add style sheets for diff display.
Definition: DifferenceEngine.php:1040
DifferenceEngine\loadNewText
loadNewText()
Load the text of the new revision, not the old one.
Definition: DifferenceEngine.php:2044
Revision\getId
getId()
Get revision ID.
Definition: Revision.php:442
ContextSource\getUser
getUser()
Definition: ContextSource.php:120
DifferenceEngine\showDiff
showDiff( $otitle, $ntitle, $notice='')
Get the diff text, send it to the OutputPage object Returns false if the diff could not be generated,...
Definition: DifferenceEngine.php:1020
DifferenceEngine\getDiffBodyCacheKey
getDiffBodyCacheKey()
Returns the cache key for diff body text or content.
Definition: DifferenceEngine.php:1225
DifferenceEngine\localiseDiff
localiseDiff( $text)
Localise diff output.
Definition: DifferenceEngine.php:1486
DifferenceEngine\$mNewid
int string false null $mNewid
Revision ID for the new revision.
Definition: DifferenceEngine.php:78
DifferenceEngine\hasDeletedRevision
hasDeletedRevision()
Checks whether one of the given Revisions was deleted.
Definition: DifferenceEngine.php:490
DifferenceEngine\generateTextDiffBody
generateTextDiffBody( $otext, $ntext)
Generate a diff, no caching.
Definition: DifferenceEngine.php:1354
$dbr
$dbr
Definition: testCompression.php:52
ContextSource\getLanguage
getLanguage()
Definition: ContextSource.php:128
Revision
Definition: Revision.php:40
DifferenceEngine\$mReducedLineNumbers
bool $mReducedLineNumbers
If true, line X is not displayed when X is 1, for example to increase readability and conserve space ...
Definition: DifferenceEngine.php:179
Revision\newFromTitle
static newFromTitle(LinkTarget $linkTarget, $id=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given link target.
Definition: Revision.php:138
NS_SPECIAL
const NS_SPECIAL
Definition: Defines.php:49
DifferenceEngine\loadRevisionData
loadRevisionData()
Load revision metadata for the specified revisions.
Definition: DifferenceEngine.php:1897
DifferenceEngine\localiseLineNumbersCb
localiseLineNumbersCb( $matches)
Definition: DifferenceEngine.php:1511
MWException
MediaWiki exception.
Definition: MWException.php:26
WikiPage\factory
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:141
DifferenceEngine\getDiffBodyForRole
getDiffBodyForRole( $role)
Get the diff table body for one slot, without header.
Definition: DifferenceEngine.php:1179
DifferenceEngine\$slotDiffRenderers
SlotDiffRenderer[] $slotDiffRenderers
DifferenceEngine classes for the slots, keyed by role name.
Definition: DifferenceEngine.php:191
Linker\generateRollback
static generateRollback( $rev, IContextSource $context=null, $options=[ 'verify'])
Generate a rollback link for a given revision.
Definition: Linker.php:1808
DifferenceEngine\wasCacheHit
wasCacheHit()
Definition: DifferenceEngine.php:363
DifferenceEngine\$mNewTags
string[] null $mNewTags
Change tags of $mNewRev or null if it does not exist / is not saved.
Definition: DifferenceEngine.php:128
wfIncrStats
wfIncrStats( $key, $count=1)
Increment a statistics counter.
Definition: GlobalFunctions.php:1160
DifferenceEngine\getDiffLang
getDiffLang()
Get the language of the difference engine, defaults to page content language.
Definition: DifferenceEngine.php:351
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2562
WikiPage\getParserOutput
getParserOutput(ParserOptions $parserOptions, $oldid=null, $forceParse=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1232
ContextSource\getOutput
getOutput()
Definition: ContextSource.php:112
$matches
$matches
Definition: NoLocalSettings.php:24
DifferenceEngine\getSlotHeader
getSlotHeader( $headerText)
Get a slot header for inclusion in a diff body (as a table row).
Definition: DifferenceEngine.php:1208
ContextSource
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
Definition: ContextSource.php:29
DifferenceEngine\$mOldRev
Revision null false $mOldRev
Old revision (left pane).
Definition: DifferenceEngine.php:91
ContextSource\getWikiPage
getWikiPage()
Get the WikiPage object.
Definition: ContextSource.php:104
DifferenceEngine\$mNewContent
Content null $mNewContent
Definition: DifferenceEngine.php:142
DifferenceEngine\deletedLink
deletedLink( $id)
Look up a special:Undelete link to the given deleted revision id, as a workaround for being unable to...
Definition: DifferenceEngine.php:419
ChangesList\flag
static flag( $flag, IContextSource $context=null)
Make an "<abbr>" element for a given change flag.
Definition: ChangesList.php:262
DifferenceEngine\getDiffBody
getDiffBody()
Get the diff table body, without header.
Definition: DifferenceEngine.php:1083
DifferenceEngine\getRevisionHeader
getRevisionHeader(Revision $rev, $complete='')
Get a header for a specified revision.
Definition: DifferenceEngine.php:1641
DifferenceEngine\getSlotDiffRenderers
getSlotDiffRenderers()
Definition: DifferenceEngine.php:253
$generator
$generator
Definition: generateLocalAutoload.php:13
$title
$title
Definition: testCompression.php:36
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:584
DifferenceEngine\setRevisions
setRevisions(?RevisionRecord $oldRevision, RevisionRecord $newRevision)
Use specified text instead of loading from the database.
Definition: DifferenceEngine.php:1778
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
DifferenceEngine\loadText
loadText()
Load the text of the revisions, as well as revision data.
Definition: DifferenceEngine.php:2006
DifferenceEngine\userCanEdit
userCanEdit(Revision $rev)
Definition: DifferenceEngine.php:1622
Linker\revUserTools
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:1125
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:913
ContextSource\setContext
setContext(IContextSource $context)
Definition: ContextSource.php:55
DifferenceEngine\$mCacheHit
bool $mCacheHit
Was the diff fetched from cache?
Definition: DifferenceEngine.php:167
DifferenceEngine\$slotDiffOptions
$slotDiffOptions
Definition: DifferenceEngine.php:204
deprecatePublicProperty
deprecatePublicProperty( $property, $version, $class=null, $component=null)
Mark a property as deprecated.
Definition: DeprecationHelper.php:68
Revision\getRevisionStore
static getRevisionStore( $wiki=false)
Definition: Revision.php:64
DifferenceEngine\markAsSlotDiffRenderer
markAsSlotDiffRenderer()
Mark this DifferenceEngine as a slot renderer (as opposed to a page renderer).
Definition: DifferenceEngine.php:284
DifferenceEngine\$enableDebugComment
$enableDebugComment
Set this to true to add debug info to the HTML output.
Definition: DifferenceEngine.php:174
Revision\RevisionRecord\getId
getId()
Get revision ID.
Definition: RevisionRecord.php:279
Revision\getTitle
getTitle()
Returns the title of the page associated with this entry.
Definition: Revision.php:559
DifferenceEngine\__construct
__construct( $context=null, $old=0, $new=0, $rcid=0, $refreshCache=false, $unhide=false)
#-
Definition: DifferenceEngine.php:221
ContextSource\msg
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition: ContextSource.php:168
DifferenceEngine\getDebugString
getDebugString()
Definition: DifferenceEngine.php:1469
Title\makeTitleSafe
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:610
DifferenceEngine\setTextLanguage
setTextLanguage(Language $lang)
Set the language in which the diff text is written.
Definition: DifferenceEngine.php:1811
$content
$content
Definition: router.php:78
DifferenceEngine\$mOldTags
string[] null $mOldTags
Change tags of $mOldRev or null if it does not exist / is not saved.
Definition: DifferenceEngine.php:122
$header
$header
Definition: updateCredits.php:41
Linker\getRevDeleteLink
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:2107
DifferenceEngine\$linkRenderer
LinkRenderer $linkRenderer
Definition: DifferenceEngine.php:209
Revision\isDeleted
isDeleted( $field)
Definition: Revision.php:692
DifferenceEngine\getTitle
getTitle()
Definition: DifferenceEngine.php:331
DifferenceEngine\showDiffPage
showDiffPage( $diffOnly=false)
Definition: DifferenceEngine.php:564
Title\newFromLinkTarget
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
Definition: Title.php:269
DifferenceEngine\$mOldid
int false null $mOldid
Revision ID for the old revision.
Definition: DifferenceEngine.php:70
Linker\titleAttrib
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:2023
DifferenceEngine\$isContentOverridden
bool $isContentOverridden
Was the content overridden via setContent()? If the content was overridden, most internal state (e....
Definition: DifferenceEngine.php:164
DifferenceEngine\DIFF_VERSION
const DIFF_VERSION
Constant to indicate diff cache compatibility.
Definition: DifferenceEngine.php:62
IContextSource
Interface for objects which can provide a MediaWiki context on request.
Definition: IContextSource.php:53
DifferenceEngine\intermediateEditsMsg
static intermediateEditsMsg( $numEdits, $numUsers, $limit)
Get a notice about how many intermediate edits and users there are.
Definition: DifferenceEngine.php:1605
DifferenceEngine\showMissingRevision
showMissingRevision()
Definition: DifferenceEngine.php:462
Content
Base interface for content objects.
Definition: Content.php:34
DifferenceEngine\loadRevisionIds
loadRevisionIds()
Load revision IDs.
Definition: DifferenceEngine.php:1861
DifferenceEngine\getParserOutput
getParserOutput(WikiPage $page, Revision $rev)
Definition: DifferenceEngine.php:996
DifferenceEngine\getNewRevision
getNewRevision()
Get the right side of the diff.
Definition: DifferenceEngine.php:407
Revision\RevisionRecord\getPageAsLinkTarget
getPageAsLinkTarget()
Returns the title of the page this revision is associated with as a LinkTarget object.
Definition: RevisionRecord.php:351
Title
Represents a title within MediaWiki.
Definition: Title.php:42
SlotDiffRenderer
Renders a diff for a single slot (that is, a diff between two content objects).
Definition: SlotDiffRenderer.php:39
$cache
$cache
Definition: mcc.php:33
DifferenceEngine\$mDiffLang
Language $mDiffLang
Definition: DifferenceEngine.php:145
DifferenceEngine\getMarkPatrolledLinkInfo
getMarkPatrolledLinkInfo()
Returns an array of meta data needed to build a "mark as patrolled" link and adds the mediawiki....
Definition: DifferenceEngine.php:836
DifferenceEngine\$mOldPage
Title null $mOldPage
Title of $mOldRev or null if the old revision does not exist or does not belong to a page.
Definition: DifferenceEngine.php:109
RecentChange\PRC_UNPATROLLED
const PRC_UNPATROLLED
Definition: RecentChange.php:80
DifferenceEngine\getPermissionErrors
getPermissionErrors(User $user)
Get the permission errors associated with the revisions for the current diff.
Definition: DifferenceEngine.php:502
DifferenceEngine\getExtraCacheKeys
getExtraCacheKeys()
Implements DifferenceEngineSlotDiffRenderer::getExtraCacheKeys().
Definition: DifferenceEngine.php:1276
DifferenceEngine\isUserAllowedToSeeRevisions
isUserAllowedToSeeRevisions( $user)
Checks whether the current user has permission for accessing the revisions of the diff.
Definition: DifferenceEngine.php:539
getTitle
getTitle()
Definition: RevisionSearchResultTrait.php:74
DifferenceEngine
DifferenceEngine is responsible for rendering the difference between two revisions as HTML.
Definition: DifferenceEngine.php:52
MediaWiki\Storage\NameTableAccessException
Exception representing a failure to look up a row from a name table.
Definition: NameTableAccessException.php:32
DifferenceEngineSlotDiffRenderer
B/C adapter for turning a DifferenceEngine into a SlotDiffRenderer.
Definition: DifferenceEngineSlotDiffRenderer.php:31
RecentChange\isInRCLifespan
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...
Definition: RecentChange.php:1131
Revision\RevisionRecord\getContent
getContent( $role, $audience=self::FOR_PUBLIC, User $user=null)
Returns the Content of the given slot of this revision.
Definition: RevisionRecord.php:167
DifferenceEngine\$mMarkPatrolledLink
string $mMarkPatrolledLink
Link to action=markpatrolled.
Definition: DifferenceEngine.php:182
DifferenceEngine\localiseLineNumbers
localiseLineNumbers( $text)
Replace line numbers with the text in the user's language.
Definition: DifferenceEngine.php:1503
DifferenceEngine\hasSuppressedRevision
hasSuppressedRevision()
Checks whether one of the given Revisions was suppressed.
Definition: DifferenceEngine.php:520
DifferenceEngine\$mOldContent
Content null $mOldContent
Definition: DifferenceEngine.php:135
wfWarn
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
Definition: GlobalFunctions.php:1065
DifferenceEngine\debug
debug( $generator="internal")
Generate a debug comment indicating diff generating time, server node, and generator backend.
Definition: DifferenceEngine.php:1454
DifferenceEngine\renderNewRevision
renderNewRevision()
Show the new revision of the page.
Definition: DifferenceEngine.php:910
CONTENT_MODEL_TEXT
const CONTENT_MODEL_TEXT
Definition: Defines.php:218
DifferenceEngine\getDiff
getDiff( $otitle, $ntitle, $notice='')
Get complete diff table, including header.
Definition: DifferenceEngine.php:1061
DifferenceEngine\setSlotDiffOptions
setSlotDiffOptions( $options)
Definition: DifferenceEngine.php:1309
DifferenceEngine\$mNewRev
Revision null $mNewRev
New revision (right pane).
Definition: DifferenceEngine.php:102
DeprecationHelper
trait DeprecationHelper
Use this trait in classes which have properties for which public access is deprecated.
Definition: DeprecationHelper.php:45
DifferenceEngine\generateContentDiffBody
generateContentDiffBody(Content $old, Content $new)
Generate a diff, no caching.
Definition: DifferenceEngine.php:1326
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:52
DifferenceEngine\mapDiffPrevNext
mapDiffPrevNext( $old, $new)
Maps a revision pair definition as accepted by DifferenceEngine constructor to a pair of actual integ...
Definition: DifferenceEngine.php:1826
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
DifferenceEngine\addHeader
addHeader( $diff, $otitle, $ntitle, $multi='', $notice='')
Add the header to a diff body.
Definition: DifferenceEngine.php:1707
ChangeTags\formatSummaryRow
static formatSummaryRow( $tags, $page, IContextSource $context=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:99
Language
Internationalisation code.
Definition: Language.php:39
DifferenceEngine\textDiff
textDiff( $otext, $ntext)
Generates diff, to be wrapped internally in a logging/instrumentation.
Definition: DifferenceEngine.php:1434
Revision\userCan
userCan( $field, User $user=null)
Determine if the current user is allowed to view a particular field of this revision,...
Definition: Revision.php:1022
Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:39
DifferenceEngine\$mNewPage
Title null $mNewPage
Title of $mNewRev or null if the new revision does not exist or does not belong to a page.
Definition: DifferenceEngine.php:116
TextSlotDiffRenderer
Renders a slot diff by doing a text diff on the native representation.
Definition: TextSlotDiffRenderer.php:37
DifferenceEngine\$mRefreshCache
bool $mRefreshCache
Refresh the diff cache.
Definition: DifferenceEngine.php:188
DifferenceEngine\getMultiNotice
getMultiNotice()
If there are revisions between the ones being compared, return a note saying so.
Definition: DifferenceEngine.php:1549