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 
183 
185  protected $unhide = false;
186 
188  protected $mRefreshCache = false;
189 
192 
199  protected $isSlotDiffRenderer = false;
200 
211  public function __construct( $context = null, $old = 0, $new = 0, $rcid = 0,
212  $refreshCache = false, $unhide = false
213  ) {
214  $this->deprecatePublicProperty( 'mOldid', '1.32', __CLASS__ );
215  $this->deprecatePublicProperty( 'mNewid', '1.32', __CLASS__ );
216  $this->deprecatePublicProperty( 'mOldRev', '1.32', __CLASS__ );
217  $this->deprecatePublicProperty( 'mNewRev', '1.32', __CLASS__ );
218  $this->deprecatePublicProperty( 'mOldPage', '1.32', __CLASS__ );
219  $this->deprecatePublicProperty( 'mNewPage', '1.32', __CLASS__ );
220  $this->deprecatePublicProperty( 'mOldContent', '1.32', __CLASS__ );
221  $this->deprecatePublicProperty( 'mNewContent', '1.32', __CLASS__ );
222  $this->deprecatePublicProperty( 'mRevisionsLoaded', '1.32', __CLASS__ );
223  $this->deprecatePublicProperty( 'mTextLoaded', '1.32', __CLASS__ );
224  $this->deprecatePublicProperty( 'mCacheHit', '1.32', __CLASS__ );
225 
226  if ( $context instanceof IContextSource ) {
227  $this->setContext( $context );
228  }
229 
230  wfDebug( "DifferenceEngine old '$old' new '$new' rcid '$rcid'\n" );
231 
232  $this->mOldid = $old;
233  $this->mNewid = $new;
234  $this->mRefreshCache = $refreshCache;
235  $this->unhide = $unhide;
236  }
237 
242  protected function getSlotDiffRenderers() {
243  if ( $this->isSlotDiffRenderer ) {
244  throw new LogicException( __METHOD__ . ' called in slot diff renderer mode' );
245  }
246 
247  if ( $this->slotDiffRenderers === null ) {
248  if ( !$this->loadRevisionData() ) {
249  return [];
250  }
251 
252  $slotContents = $this->getSlotContents();
253  $this->slotDiffRenderers = array_map( function ( $contents ) {
255  $content = $contents['new'] ?: $contents['old'];
256  return $content->getContentHandler()->getSlotDiffRenderer( $this->getContext() );
257  }, $slotContents );
258  }
260  }
261 
268  public function markAsSlotDiffRenderer() {
269  $this->isSlotDiffRenderer = true;
270  }
271 
277  protected function getSlotContents() {
278  if ( $this->isContentOverridden ) {
279  return [
280  SlotRecord::MAIN => [
281  'old' => $this->mOldContent,
282  'new' => $this->mNewContent,
283  ]
284  ];
285  } elseif ( !$this->loadRevisionData() ) {
286  return [];
287  }
288 
289  $newSlots = $this->mNewRev->getRevisionRecord()->getSlots()->getSlots();
290  if ( $this->mOldRev ) {
291  $oldSlots = $this->mOldRev->getRevisionRecord()->getSlots()->getSlots();
292  } else {
293  $oldSlots = [];
294  }
295  // The order here will determine the visual order of the diff. The current logic is
296  // slots of the new revision first in natural order, then deleted ones. This is ad hoc
297  // and should not be relied on - in the future we may want the ordering to depend
298  // on the page type.
299  $roles = array_merge( array_keys( $newSlots ), array_keys( $oldSlots ) );
300 
301  $slots = [];
302  foreach ( $roles as $role ) {
303  $slots[$role] = [
304  'old' => isset( $oldSlots[$role] ) ? $oldSlots[$role]->getContent() : null,
305  'new' => isset( $newSlots[$role] ) ? $newSlots[$role]->getContent() : null,
306  ];
307  }
308  // move main slot to front
309  if ( isset( $slots[SlotRecord::MAIN] ) ) {
310  $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
311  }
312  return $slots;
313  }
314 
315  public function getTitle() {
316  // T202454 avoid errors when there is no title
317  return parent::getTitle() ?: Title::makeTitle( NS_SPECIAL, 'BadTitle/DifferenceEngine' );
318  }
319 
326  public function setReducedLineNumbers( $value = true ) {
327  $this->mReducedLineNumbers = $value;
328  }
329 
335  public function getDiffLang() {
336  if ( $this->mDiffLang === null ) {
337  # Default language in which the diff text is written.
338  $this->mDiffLang = $this->getTitle()->getPageLanguage();
339  }
340 
341  return $this->mDiffLang;
342  }
343 
347  public function wasCacheHit() {
348  return $this->mCacheHit;
349  }
350 
358  public function getOldid() {
359  $this->loadRevisionIds();
360 
361  return $this->mOldid;
362  }
363 
370  public function getNewid() {
371  $this->loadRevisionIds();
372 
373  return $this->mNewid;
374  }
375 
382  public function getOldRevision() {
383  return $this->mOldRev ? $this->mOldRev->getRevisionRecord() : null;
384  }
385 
391  public function getNewRevision() {
392  return $this->mNewRev ? $this->mNewRev->getRevisionRecord() : null;
393  }
394 
403  public function deletedLink( $id ) {
404  if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) {
405  $dbr = wfGetDB( DB_REPLICA );
406  $arQuery = Revision::getArchiveQueryInfo();
407  $row = $dbr->selectRow(
408  $arQuery['tables'],
409  array_merge( $arQuery['fields'], [ 'ar_namespace', 'ar_title' ] ),
410  [ 'ar_rev_id' => $id ],
411  __METHOD__,
412  [],
413  $arQuery['joins']
414  );
415  if ( $row ) {
417  $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
418 
419  return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( [
420  'target' => $title->getPrefixedText(),
421  'timestamp' => $rev->getTimestamp()
422  ] );
423  }
424  }
425 
426  return false;
427  }
428 
436  public function deletedIdMarker( $id ) {
437  $link = $this->deletedLink( $id );
438  if ( $link ) {
439  return "[$link $id]";
440  } else {
441  return (string)$id;
442  }
443  }
444 
445  private function showMissingRevision() {
446  $out = $this->getOutput();
447 
448  $missing = [];
449  if ( $this->mOldRev === null ||
450  ( $this->mOldRev && $this->mOldContent === null )
451  ) {
452  $missing[] = $this->deletedIdMarker( $this->mOldid );
453  }
454  if ( $this->mNewRev === null ||
455  ( $this->mNewRev && $this->mNewContent === null )
456  ) {
457  $missing[] = $this->deletedIdMarker( $this->mNewid );
458  }
459 
460  $out->setPageTitle( $this->msg( 'errorpagetitle' ) );
461  $msg = $this->msg( 'difference-missing-revision' )
462  ->params( $this->getLanguage()->listToText( $missing ) )
463  ->numParams( count( $missing ) )
464  ->parseAsBlock();
465  $out->addHTML( $msg );
466  }
467 
468  public function showDiffPage( $diffOnly = false ) {
469  # Allow frames except in certain special cases
470  $out = $this->getOutput();
471  $out->allowClickjacking();
472  $out->setRobotPolicy( 'noindex,nofollow' );
473 
474  // Allow extensions to add any extra output here
475  Hooks::run( 'DifferenceEngineShowDiffPage', [ $out ] );
476 
477  if ( !$this->loadRevisionData() ) {
478  if ( Hooks::run( 'DifferenceEngineShowDiffPageMaybeShowMissingRevision', [ $this ] ) ) {
479  $this->showMissingRevision();
480  }
481  return;
482  }
483 
484  $user = $this->getUser();
485  $permErrors = [];
486  if ( $this->mNewPage ) {
487  $permErrors = $this->mNewPage->getUserPermissionsErrors( 'read', $user );
488  }
489  if ( $this->mOldPage ) {
490  $permErrors = wfMergeErrorArrays( $permErrors,
491  $this->mOldPage->getUserPermissionsErrors( 'read', $user ) );
492  }
493  if ( count( $permErrors ) ) {
494  throw new PermissionsError( 'read', $permErrors );
495  }
496 
497  $rollback = '';
498 
499  $query = [];
500  # Carry over 'diffonly' param via navigation links
501  if ( $diffOnly != $user->getBoolOption( 'diffonly' ) ) {
502  $query['diffonly'] = $diffOnly;
503  }
504  # Cascade unhide param in links for easy deletion browsing
505  if ( $this->unhide ) {
506  $query['unhide'] = 1;
507  }
508 
509  # Check if one of the revisions is deleted/suppressed
510  $deleted = $suppressed = false;
511  $allowed = $this->mNewRev->userCan( RevisionRecord::DELETED_TEXT, $user );
512 
513  $revisionTools = [];
514 
515  # mOldRev is false if the difference engine is called with a "vague" query for
516  # a diff between a version V and its previous version V' AND the version V
517  # is the first version of that article. In that case, V' does not exist.
518  if ( $this->mOldRev === false ) {
519  if ( $this->mNewPage ) {
520  $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
521  }
522  $samePage = true;
523  $oldHeader = '';
524  // Allow extensions to change the $oldHeader variable
525  Hooks::run( 'DifferenceEngineOldHeaderNoOldRev', [ &$oldHeader ] );
526  } else {
527  Hooks::run( 'DiffViewHeader', [ $this, $this->mOldRev, $this->mNewRev ] );
528 
529  if ( !$this->mOldPage || !$this->mNewPage ) {
530  // XXX say something to the user?
531  $samePage = false;
532  } elseif ( $this->mNewPage->equals( $this->mOldPage ) ) {
533  $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
534  $samePage = true;
535  } else {
536  $out->setPageTitle( $this->msg( 'difference-title-multipage',
537  $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) );
538  $out->addSubtitle( $this->msg( 'difference-multipage' ) );
539  $samePage = false;
540  }
541 
542  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
543 
544  if ( $samePage && $this->mNewPage && $permissionManager->userCan(
545  'edit', $user, $this->mNewPage, PermissionManager::RIGOR_QUICK
546  ) ) {
547  if ( $this->mNewRev->isCurrent() && $permissionManager->userCan(
548  'rollback', $user, $this->mNewPage
549  ) ) {
550  $rollbackLink = Linker::generateRollback( $this->mNewRev, $this->getContext(),
551  [ 'noBrackets' ] );
552  if ( $rollbackLink ) {
553  $out->preventClickjacking();
554  $rollback = "\u{00A0}\u{00A0}\u{00A0}" . $rollbackLink;
555  }
556  }
557 
558  if ( !$this->mOldRev->isDeleted( RevisionRecord::DELETED_TEXT ) &&
559  !$this->mNewRev->isDeleted( RevisionRecord::DELETED_TEXT )
560  ) {
561  $undoLink = Html::element( 'a', [
562  'href' => $this->mNewPage->getLocalURL( [
563  'action' => 'edit',
564  'undoafter' => $this->mOldid,
565  'undo' => $this->mNewid
566  ] ),
567  'title' => Linker::titleAttrib( 'undo' ),
568  ],
569  $this->msg( 'editundo' )->text()
570  );
571  $revisionTools['mw-diff-undo'] = $undoLink;
572  }
573  }
574 
575  # Make "previous revision link"
576  if ( $samePage && $this->mOldPage && $this->mOldRev->getPrevious() ) {
577  $prevlink = Linker::linkKnown(
578  $this->mOldPage,
579  $this->msg( 'previousdiff' )->escaped(),
580  [ 'id' => 'differences-prevlink' ],
581  [ 'diff' => 'prev', 'oldid' => $this->mOldid ] + $query
582  );
583  } else {
584  $prevlink = "\u{00A0}";
585  }
586 
587  if ( $this->mOldRev->isMinor() ) {
588  $oldminor = ChangesList::flag( 'minor' );
589  } else {
590  $oldminor = '';
591  }
592 
593  $ldel = $this->revisionDeleteLink( $this->mOldRev );
594  $oldRevisionHeader = $this->getRevisionHeader( $this->mOldRev, 'complete' );
595  $oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff', $this->getContext() );
596 
597  $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' .
598  '<div id="mw-diff-otitle2">' .
599  Linker::revUserTools( $this->mOldRev, !$this->unhide ) . '</div>' .
600  '<div id="mw-diff-otitle3">' . $oldminor .
601  Linker::revComment( $this->mOldRev, !$diffOnly, !$this->unhide ) . $ldel . '</div>' .
602  '<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' .
603  '<div id="mw-diff-otitle4">' . $prevlink . '</div>';
604 
605  // Allow extensions to change the $oldHeader variable
606  Hooks::run( 'DifferenceEngineOldHeader', [ $this, &$oldHeader, $prevlink, $oldminor,
607  $diffOnly, $ldel, $this->unhide ] );
608 
609  if ( $this->mOldRev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
610  $deleted = true; // old revisions text is hidden
611  if ( $this->mOldRev->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
612  $suppressed = true; // also suppressed
613  }
614  }
615 
616  # Check if this user can see the revisions
617  if ( !$this->mOldRev->userCan( RevisionRecord::DELETED_TEXT, $user ) ) {
618  $allowed = false;
619  }
620  }
621 
622  $out->addJsConfigVars( [
623  'wgDiffOldId' => $this->mOldid,
624  'wgDiffNewId' => $this->mNewid,
625  ] );
626 
627  # Make "next revision link"
628  # Skip next link on the top revision
629  if ( $samePage && $this->mNewPage && !$this->mNewRev->isCurrent() ) {
630  $nextlink = Linker::linkKnown(
631  $this->mNewPage,
632  $this->msg( 'nextdiff' )->escaped(),
633  [ 'id' => 'differences-nextlink' ],
634  [ 'diff' => 'next', 'oldid' => $this->mNewid ] + $query
635  );
636  } else {
637  $nextlink = "\u{00A0}";
638  }
639 
640  if ( $this->mNewRev->isMinor() ) {
641  $newminor = ChangesList::flag( 'minor' );
642  } else {
643  $newminor = '';
644  }
645 
646  # Handle RevisionDelete links...
647  $rdel = $this->revisionDeleteLink( $this->mNewRev );
648 
649  # Allow extensions to define their own revision tools
650  Hooks::run( 'DiffRevisionTools',
651  [ $this->mNewRev, &$revisionTools, $this->mOldRev, $user ] );
652  $formattedRevisionTools = [];
653  // Put each one in parentheses (poor man's button)
654  foreach ( $revisionTools as $key => $tool ) {
655  $toolClass = is_string( $key ) ? $key : 'mw-diff-tool';
656  $element = Html::rawElement(
657  'span',
658  [ 'class' => $toolClass ],
659  $this->msg( 'parentheses' )->rawParams( $tool )->escaped()
660  );
661  $formattedRevisionTools[] = $element;
662  }
663  $newRevisionHeader = $this->getRevisionHeader( $this->mNewRev, 'complete' ) .
664  ' ' . implode( ' ', $formattedRevisionTools );
665  $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff', $this->getContext() );
666 
667  $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' .
668  '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $this->mNewRev, !$this->unhide ) .
669  " $rollback</div>" .
670  '<div id="mw-diff-ntitle3">' . $newminor .
671  Linker::revComment( $this->mNewRev, !$diffOnly, !$this->unhide ) . $rdel . '</div>' .
672  '<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' .
673  '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
674 
675  // Allow extensions to change the $newHeader variable
676  Hooks::run( 'DifferenceEngineNewHeader', [ $this, &$newHeader, $formattedRevisionTools,
677  $nextlink, $rollback, $newminor, $diffOnly, $rdel, $this->unhide ] );
678 
679  if ( $this->mNewRev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
680  $deleted = true; // new revisions text is hidden
681  if ( $this->mNewRev->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
682  $suppressed = true; // also suppressed
683  }
684  }
685 
686  # If the diff cannot be shown due to a deleted revision, then output
687  # the diff header and links to unhide (if available)...
688  if ( $deleted && ( !$this->unhide || !$allowed ) ) {
689  $this->showDiffStyle();
690  $multi = $this->getMultiNotice();
691  $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
692  if ( !$allowed ) {
693  $msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff';
694  # Give explanation for why revision is not visible
695  $out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
696  [ $msg ] );
697  } else {
698  # Give explanation and add a link to view the diff...
699  $query = $this->getRequest()->appendQueryValue( 'unhide', '1' );
700  $link = $this->getTitle()->getFullURL( $query );
701  $msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff';
702  $out->wrapWikiMsg(
703  "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
704  [ $msg, $link ]
705  );
706  }
707  # Otherwise, output a regular diff...
708  } else {
709  # Add deletion notice if the user is viewing deleted content
710  $notice = '';
711  if ( $deleted ) {
712  $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
713  $notice = "<div id='mw-$msg' class='mw-warning plainlinks'>\n" .
714  $this->msg( $msg )->parse() .
715  "</div>\n";
716  }
717  $this->showDiff( $oldHeader, $newHeader, $notice );
718  if ( !$diffOnly ) {
719  $this->renderNewRevision();
720  }
721  }
722  }
723 
733  public function markPatrolledLink() {
734  if ( $this->mMarkPatrolledLink === null ) {
735  $linkInfo = $this->getMarkPatrolledLinkInfo();
736  // If false, there is no patrol link needed/allowed
737  if ( !$linkInfo || !$this->mNewPage ) {
738  $this->mMarkPatrolledLink = '';
739  } else {
740  $this->mMarkPatrolledLink = ' <span class="patrollink" data-mw="interface">[' .
742  $this->mNewPage,
743  $this->msg( 'markaspatrolleddiff' )->escaped(),
744  [],
745  [
746  'action' => 'markpatrolled',
747  'rcid' => $linkInfo['rcid'],
748  ]
749  ) . ']</span>';
750  // Allow extensions to change the markpatrolled link
751  Hooks::run( 'DifferenceEngineMarkPatrolledLink', [ $this,
752  &$this->mMarkPatrolledLink, $linkInfo['rcid'] ] );
753  }
754  }
756  }
757 
765  protected function getMarkPatrolledLinkInfo() {
766  $user = $this->getUser();
767  $config = $this->getConfig();
768 
769  // Prepare a change patrol link, if applicable
770  if (
771  // Is patrolling enabled and the user allowed to?
772  $config->get( 'UseRCPatrol' ) &&
773  $this->mNewPage && $this->mNewPage->quickUserCan( 'patrol', $user ) &&
774  // Only do this if the revision isn't more than 6 hours older
775  // than the Max RC age (6h because the RC might not be cleaned out regularly)
776  RecentChange::isInRCLifespan( $this->mNewRev->getTimestamp(), 21600 )
777  ) {
778  // Look for an unpatrolled change corresponding to this diff
779  $db = wfGetDB( DB_REPLICA );
780  $change = RecentChange::newFromConds(
781  [
782  'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
783  'rc_this_oldid' => $this->mNewid,
784  'rc_patrolled' => RecentChange::PRC_UNPATROLLED
785  ],
786  __METHOD__
787  );
788 
789  if ( $change && !$change->getPerformer()->equals( $user ) ) {
790  $rcid = $change->getAttribute( 'rc_id' );
791  } else {
792  // None found or the page has been created by the current user.
793  // If the user could patrol this it already would be patrolled
794  $rcid = 0;
795  }
796 
797  // Allow extensions to possibly change the rcid here
798  // For example the rcid might be set to zero due to the user
799  // being the same as the performer of the change but an extension
800  // might still want to show it under certain conditions
801  Hooks::run( 'DifferenceEngineMarkPatrolledRCID', [ &$rcid, $this, $change, $user ] );
802 
803  // Build the link
804  if ( $rcid ) {
805  $this->getOutput()->preventClickjacking();
806  if ( $user->isAllowed( 'writeapi' ) ) {
807  $this->getOutput()->addModules( 'mediawiki.page.patrol.ajax' );
808  }
809 
810  return [
811  'rcid' => $rcid,
812  ];
813  }
814  }
815 
816  // No mark as patrolled link applicable
817  return false;
818  }
819 
825  protected function revisionDeleteLink( $rev ) {
826  $link = Linker::getRevDeleteLink( $this->getUser(), $rev, $rev->getTitle() );
827  if ( $link !== '' ) {
828  $link = "\u{00A0}\u{00A0}\u{00A0}" . $link . ' ';
829  }
830 
831  return $link;
832  }
833 
839  public function renderNewRevision() {
840  if ( $this->isContentOverridden ) {
841  // The code below only works with a Revision object. We could construct a fake revision
842  // (here or in setContent), but since this does not seem needed at the moment,
843  // we'll just fail for now.
844  throw new LogicException(
845  __METHOD__
846  . ' is not supported after calling setContent(). Use setRevisions() instead.'
847  );
848  }
849 
850  $out = $this->getOutput();
851  $revHeader = $this->getRevisionHeader( $this->mNewRev );
852  # Add "current version as of X" title
853  $out->addHTML( "<hr class='diff-hr' id='mw-oldid' />
854  <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
855  # Page content may be handled by a hooked call instead...
856  if ( Hooks::run( 'ArticleContentOnDiff', [ $this, $out ] ) ) {
857  $this->loadNewText();
858  if ( !$this->mNewPage ) {
859  // New revision is unsaved; bail out.
860  // TODO in theory rendering the new revision is a meaningful thing to do
861  // even if it's unsaved, but a lot of untangling is required to do it safely.
862  return;
863  }
864 
865  $out->setRevisionId( $this->mNewid );
866  $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() );
867  $out->setArticleFlag( true );
868 
869  if ( !Hooks::run( 'ArticleRevisionViewCustom',
870  [ $this->mNewRev->getRevisionRecord(), $this->mNewPage, $out ] )
871  ) {
872  // Handled by extension
873  // NOTE: sync with hooks called in Article::view()
874  } elseif ( !Hooks::run( 'ArticleContentViewCustom',
875  [ $this->mNewContent, $this->mNewPage, $out ], '1.32' )
876  ) {
877  // Handled by extension
878  // NOTE: sync with hooks called in Article::view()
879  } else {
880  // Normal page
881  if ( $this->getTitle()->equals( $this->mNewPage ) ) {
882  // If the Title stored in the context is the same as the one
883  // of the new revision, we can use its associated WikiPage
884  // object.
885  $wikiPage = $this->getWikiPage();
886  } else {
887  // Otherwise we need to create our own WikiPage object
888  $wikiPage = WikiPage::factory( $this->mNewPage );
889  }
890 
891  $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRev );
892 
893  # WikiPage::getParserOutput() should not return false, but just in case
894  if ( $parserOutput ) {
895  // Allow extensions to change parser output here
896  if ( Hooks::run( 'DifferenceEngineRenderRevisionAddParserOutput',
897  [ $this, $out, $parserOutput, $wikiPage ] )
898  ) {
899  $out->addParserOutput( $parserOutput, [
900  'enableSectionEditLinks' => $this->mNewRev->isCurrent()
901  && $this->mNewRev->getTitle()->quickUserCan( 'edit', $this->getUser() ),
902  ] );
903  }
904  }
905  }
906  }
907 
908  // Allow extensions to optionally not show the final patrolled link
909  if ( Hooks::run( 'DifferenceEngineRenderRevisionShowFinalPatrolLink' ) ) {
910  # Add redundant patrol link on bottom...
911  $out->addHTML( $this->markPatrolledLink() );
912  }
913  }
914 
921  protected function getParserOutput( WikiPage $page, Revision $rev ) {
922  if ( !$rev->getId() ) {
923  // WikiPage::getParserOutput wants a revision ID. Passing 0 will incorrectly show
924  // the current revision, so fail instead. If need be, WikiPage::getParserOutput
925  // could be made to accept a Revision or RevisionRecord instead of the id.
926  return false;
927  }
928 
929  $parserOptions = $page->makeParserOptions( $this->getContext() );
930  $parserOutput = $page->getParserOutput( $parserOptions, $rev->getId() );
931 
932  return $parserOutput;
933  }
934 
945  public function showDiff( $otitle, $ntitle, $notice = '' ) {
946  // Allow extensions to affect the output here
947  Hooks::run( 'DifferenceEngineShowDiff', [ $this ] );
948 
949  $diff = $this->getDiff( $otitle, $ntitle, $notice );
950  if ( $diff === false ) {
951  $this->showMissingRevision();
952 
953  return false;
954  } else {
955  $this->showDiffStyle();
956  $this->getOutput()->addHTML( $diff );
957 
958  return true;
959  }
960  }
961 
965  public function showDiffStyle() {
966  if ( !$this->isSlotDiffRenderer ) {
967  $this->getOutput()->addModuleStyles( [
968  'mediawiki.interface.helpers.styles',
969  'mediawiki.diff.styles'
970  ] );
971  foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
972  $slotDiffRenderer->addModules( $this->getOutput() );
973  }
974  }
975  }
976 
986  public function getDiff( $otitle, $ntitle, $notice = '' ) {
987  $body = $this->getDiffBody();
988  if ( $body === false ) {
989  return false;
990  }
991 
992  $multi = $this->getMultiNotice();
993  // Display a message when the diff is empty
994  if ( $body === '' ) {
995  $notice .= '<div class="mw-diff-empty">' .
996  $this->msg( 'diff-empty' )->parse() .
997  "</div>\n";
998  }
999 
1000  return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
1001  }
1002 
1008  public function getDiffBody() {
1009  $this->mCacheHit = true;
1010  // Check if the diff should be hidden from this user
1011  if ( !$this->isContentOverridden ) {
1012  if ( !$this->loadRevisionData() ) {
1013  return false;
1014  } elseif ( $this->mOldRev &&
1015  !$this->mOldRev->userCan( RevisionRecord::DELETED_TEXT, $this->getUser() )
1016  ) {
1017  return false;
1018  } elseif ( $this->mNewRev &&
1019  !$this->mNewRev->userCan( RevisionRecord::DELETED_TEXT, $this->getUser() )
1020  ) {
1021  return false;
1022  }
1023  // Short-circuit
1024  if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev &&
1025  $this->mOldRev->getId() && $this->mOldRev->getId() == $this->mNewRev->getId() )
1026  ) {
1027  if ( Hooks::run( 'DifferenceEngineShowEmptyOldContent', [ $this ] ) ) {
1028  return '';
1029  }
1030  }
1031  }
1032 
1033  // Cacheable?
1034  $key = false;
1035  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1036  if ( $this->mOldid && $this->mNewid ) {
1037  // Check if subclass is still using the old way
1038  // for backwards-compatibility
1039  $key = $this->getDiffBodyCacheKey();
1040  if ( $key === null ) {
1041  $key = $cache->makeKey( ...$this->getDiffBodyCacheKeyParams() );
1042  }
1043 
1044  // Try cache
1045  if ( !$this->mRefreshCache ) {
1046  $difftext = $cache->get( $key );
1047  if ( is_string( $difftext ) ) {
1048  wfIncrStats( 'diff_cache.hit' );
1049  $difftext = $this->localiseDiff( $difftext );
1050  $difftext .= "\n<!-- diff cache key $key -->\n";
1051 
1052  return $difftext;
1053  }
1054  } // don't try to load but save the result
1055  }
1056  $this->mCacheHit = false;
1057 
1058  // Loadtext is permission safe, this just clears out the diff
1059  if ( !$this->loadText() ) {
1060  return false;
1061  }
1062 
1063  $difftext = '';
1064  // We've checked for revdelete at the beginning of this method; it's OK to ignore
1065  // read permissions here.
1066  $slotContents = $this->getSlotContents();
1067  foreach ( $this->getSlotDiffRenderers() as $role => $slotDiffRenderer ) {
1068  $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role]['old'],
1069  $slotContents[$role]['new'] );
1070  if ( $slotDiff && $role !== SlotRecord::MAIN ) {
1071  // FIXME: ask SlotRoleHandler::getSlotNameMessage
1072  $slotTitle = $role;
1073  $difftext .= $this->getSlotHeader( $slotTitle );
1074  }
1075  $difftext .= $slotDiff;
1076  }
1077 
1078  // Avoid PHP 7.1 warning from passing $this by reference
1079  $diffEngine = $this;
1080 
1081  // Save to cache for 7 days
1082  if ( !Hooks::run( 'AbortDiffCache', [ &$diffEngine ] ) ) {
1083  wfIncrStats( 'diff_cache.uncacheable' );
1084  } elseif ( $key !== false && $difftext !== false ) {
1085  wfIncrStats( 'diff_cache.miss' );
1086  $cache->set( $key, $difftext, 7 * 86400 );
1087  } else {
1088  wfIncrStats( 'diff_cache.uncacheable' );
1089  }
1090  // localise line numbers and title attribute text
1091  if ( $difftext !== false ) {
1092  $difftext = $this->localiseDiff( $difftext );
1093  }
1094 
1095  return $difftext;
1096  }
1097 
1104  public function getDiffBodyForRole( $role ) {
1105  $diffRenderers = $this->getSlotDiffRenderers();
1106  if ( !isset( $diffRenderers[$role] ) ) {
1107  return false;
1108  }
1109 
1110  $slotContents = $this->getSlotContents();
1111  $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role]['old'],
1112  $slotContents[$role]['new'] );
1113  if ( !$slotDiff ) {
1114  return false;
1115  }
1116 
1117  if ( $role !== SlotRecord::MAIN ) {
1118  // TODO use human-readable role name at least
1119  $slotTitle = $role;
1120  $slotDiff = $this->getSlotHeader( $slotTitle ) . $slotDiff;
1121  }
1122 
1123  return $this->localiseDiff( $slotDiff );
1124  }
1125 
1133  protected function getSlotHeader( $headerText ) {
1134  // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
1135  $columnCount = $this->mOldRev ? 4 : 2;
1136  $userLang = $this->getLanguage()->getHtmlCode();
1137  return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-header', 'lang' => $userLang ],
1138  Html::element( 'th', [ 'colspan' => $columnCount ], $headerText ) );
1139  }
1140 
1150  protected function getDiffBodyCacheKey() {
1151  return null;
1152  }
1153 
1167  protected function getDiffBodyCacheKeyParams() {
1168  if ( !$this->mOldid || !$this->mNewid ) {
1169  throw new MWException( 'mOldid and mNewid must be set to get diff cache key.' );
1170  }
1171 
1172  $engine = $this->getEngine();
1173  $params = [
1174  'diff',
1175  $engine,
1176  self::DIFF_VERSION,
1177  "old-{$this->mOldid}",
1178  "rev-{$this->mNewid}"
1179  ];
1180 
1181  if ( $engine === 'wikidiff2' ) {
1182  $params[] = phpversion( 'wikidiff2' );
1183  }
1184 
1185  if ( !$this->isSlotDiffRenderer ) {
1186  foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1187  $params = array_merge( $params, $slotDiffRenderer->getExtraCacheKeys() );
1188  }
1189  }
1190 
1191  return $params;
1192  }
1193 
1201  public function getExtraCacheKeys() {
1202  // This method is called when the DifferenceEngine is used for a slot diff. We only care
1203  // about special things, not the revision IDs, which are added to the cache key by the
1204  // page-level DifferenceEngine, and which might not have a valid value for this object.
1205  $this->mOldid = 123456789;
1206  $this->mNewid = 987654321;
1207 
1208  // This will repeat a bunch of unnecessary key fields for each slot. Not nice but harmless.
1209  $cacheString = $this->getDiffBodyCacheKey();
1210  if ( $cacheString ) {
1211  return [ $cacheString ];
1212  }
1213 
1215 
1216  // Try to get rid of the standard keys to keep the cache key human-readable:
1217  // call the getDiffBodyCacheKeyParams implementation of the base class, and if
1218  // the child class includes the same keys, drop them.
1219  // Uses an obscure PHP feature where static calls to non-static methods are allowed
1220  // as long as we are already in a non-static method of the same class, and the call context
1221  // ($this) will be inherited.
1222  // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
1223  $standardParams = DifferenceEngine::getDiffBodyCacheKeyParams();
1224  if ( array_slice( $params, 0, count( $standardParams ) ) === $standardParams ) {
1225  $params = array_slice( $params, count( $standardParams ) );
1226  }
1227 
1228  return $params;
1229  }
1230 
1244  public function generateContentDiffBody( Content $old, Content $new ) {
1245  $slotDiffRenderer = $new->getContentHandler()->getSlotDiffRenderer( $this->getContext() );
1246  if (
1247  $slotDiffRenderer instanceof DifferenceEngineSlotDiffRenderer
1248  && $this->isSlotDiffRenderer
1249  ) {
1250  // Oops, we are just about to enter an infinite loop (the slot-level DifferenceEngine
1251  // called a DifferenceEngineSlotDiffRenderer that wraps the same DifferenceEngine class).
1252  // This will happen when a content model has no custom slot diff renderer, it does have
1253  // a custom difference engine, but that does not override this method.
1254  throw new Exception( get_class( $this ) . ': could not maintain backwards compatibility. '
1255  . 'Please use a SlotDiffRenderer.' );
1256  }
1257  return $slotDiffRenderer->getDiff( $old, $new ) . $this->getDebugString();
1258  }
1259 
1272  public function generateTextDiffBody( $otext, $ntext ) {
1273  $slotDiffRenderer = ContentHandler::getForModelID( CONTENT_MODEL_TEXT )
1274  ->getSlotDiffRenderer( $this->getContext() );
1275  if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1276  // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1277  // This is too unlikely to happen to bother handling properly.
1278  throw new Exception( 'The slot diff renderer for text content should be a '
1279  . 'TextSlotDiffRenderer subclass' );
1280  }
1281  return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1282  }
1283 
1290  public static function getEngine() {
1291  global $wgExternalDiffEngine;
1292  // We use the global here instead of Config because we write to the value,
1293  // and Config is not mutable.
1294  if ( $wgExternalDiffEngine == 'wikidiff' || $wgExternalDiffEngine == 'wikidiff3' ) {
1295  wfDeprecated( "\$wgExternalDiffEngine = '{$wgExternalDiffEngine}'", '1.27' );
1296  $wgExternalDiffEngine = false;
1297  } elseif ( $wgExternalDiffEngine == 'wikidiff2' ) {
1298  wfDeprecated( "\$wgExternalDiffEngine = '{$wgExternalDiffEngine}'", '1.32' );
1299  $wgExternalDiffEngine = false;
1300  } elseif ( !is_string( $wgExternalDiffEngine ) && $wgExternalDiffEngine !== false ) {
1301  // And prevent people from shooting themselves in the foot...
1302  wfWarn( '$wgExternalDiffEngine is set to a non-string value, forcing it to false' );
1303  $wgExternalDiffEngine = false;
1304  }
1305 
1306  if ( is_string( $wgExternalDiffEngine ) && is_executable( $wgExternalDiffEngine ) ) {
1307  return $wgExternalDiffEngine;
1308  } elseif ( $wgExternalDiffEngine === false && function_exists( 'wikidiff2_do_diff' ) ) {
1309  return 'wikidiff2';
1310  } else {
1311  // Native PHP
1312  return false;
1313  }
1314  }
1315 
1328  protected function textDiff( $otext, $ntext ) {
1329  $slotDiffRenderer = ContentHandler::getForModelID( CONTENT_MODEL_TEXT )
1330  ->getSlotDiffRenderer( $this->getContext() );
1331  if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1332  // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1333  // This is too unlikely to happen to bother handling properly.
1334  throw new Exception( 'The slot diff renderer for text content should be a '
1335  . 'TextSlotDiffRenderer subclass' );
1336  }
1337  return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1338  }
1339 
1348  protected function debug( $generator = "internal" ) {
1349  if ( !$this->enableDebugComment ) {
1350  return '';
1351  }
1352  $data = [ $generator ];
1353  if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
1354  $data[] = wfHostname();
1355  }
1356  $data[] = wfTimestamp( TS_DB );
1357 
1358  return "<!-- diff generator: " .
1359  implode( " ", array_map( "htmlspecialchars", $data ) ) .
1360  " -->\n";
1361  }
1362 
1363  private function getDebugString() {
1364  $engine = self::getEngine();
1365  if ( $engine === 'wikidiff2' ) {
1366  return $this->debug( 'wikidiff2' );
1367  } elseif ( $engine === false ) {
1368  return $this->debug( 'native PHP' );
1369  } else {
1370  return $this->debug( "external $engine" );
1371  }
1372  }
1373 
1380  private function localiseDiff( $text ) {
1381  $text = $this->localiseLineNumbers( $text );
1382  if ( $this->getEngine() === 'wikidiff2' &&
1383  version_compare( phpversion( 'wikidiff2' ), '1.5.1', '>=' )
1384  ) {
1385  $text = $this->addLocalisedTitleTooltips( $text );
1386  }
1387  return $text;
1388  }
1389 
1397  public function localiseLineNumbers( $text ) {
1398  return preg_replace_callback(
1399  '/<!--LINE (\d+)-->/',
1400  [ $this, 'localiseLineNumbersCb' ],
1401  $text
1402  );
1403  }
1404 
1405  public function localiseLineNumbersCb( $matches ) {
1406  if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
1407  return '';
1408  }
1409 
1410  return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
1411  }
1412 
1419  private function addLocalisedTitleTooltips( $text ) {
1420  return preg_replace_callback(
1421  '/class="mw-diff-movedpara-(left|right)"/',
1422  [ $this, 'addLocalisedTitleTooltipsCb' ],
1423  $text
1424  );
1425  }
1426 
1431  private function addLocalisedTitleTooltipsCb( array $matches ) {
1432  $key = $matches[1] === 'right' ?
1433  'diff-paragraph-moved-toold' :
1434  'diff-paragraph-moved-tonew';
1435  return $matches[0] . ' title="' . $this->msg( $key )->escaped() . '"';
1436  }
1437 
1443  public function getMultiNotice() {
1444  // The notice only make sense if we are diffing two saved revisions of the same page.
1445  if (
1446  !$this->mOldRev || !$this->mNewRev
1447  || !$this->mOldPage || !$this->mNewPage
1448  || !$this->mOldPage->equals( $this->mNewPage )
1449  ) {
1450  return '';
1451  }
1452 
1453  if ( $this->mOldRev->getTimestamp() > $this->mNewRev->getTimestamp() ) {
1454  $oldRev = $this->mNewRev; // flip
1455  $newRev = $this->mOldRev; // flip
1456  } else { // normal case
1457  $oldRev = $this->mOldRev;
1459  }
1460 
1461  // Sanity: don't show the notice if too many rows must be scanned
1462  // @todo show some special message for that case
1463  $nEdits = $this->mNewPage->countRevisionsBetween( $oldRev, $newRev, 1000 );
1464  if ( $nEdits > 0 && $nEdits <= 1000 ) {
1465  $limit = 100; // use diff-multi-manyusers if too many users
1466  $users = $this->mNewPage->getAuthorsBetween( $oldRev, $newRev, $limit );
1467  $numUsers = count( $users );
1468 
1469  if ( $numUsers == 1 && $users[0] == $newRev->getUserText( RevisionRecord::RAW ) ) {
1470  $numUsers = 0; // special case to say "by the same user" instead of "by one other user"
1471  }
1472 
1473  return self::intermediateEditsMsg( $nEdits, $numUsers, $limit );
1474  }
1475 
1476  return '';
1477  }
1478 
1488  public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) {
1489  if ( $numUsers === 0 ) {
1490  $msg = 'diff-multi-sameuser';
1491  } elseif ( $numUsers > $limit ) {
1492  $msg = 'diff-multi-manyusers';
1493  $numUsers = $limit;
1494  } else {
1495  $msg = 'diff-multi-otherusers';
1496  }
1497 
1498  return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1499  }
1500 
1510  public function getRevisionHeader( Revision $rev, $complete = '' ) {
1511  $lang = $this->getLanguage();
1512  $user = $this->getUser();
1513  $revtimestamp = $rev->getTimestamp();
1514  $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
1515  $dateofrev = $lang->userDate( $revtimestamp, $user );
1516  $timeofrev = $lang->userTime( $revtimestamp, $user );
1517 
1518  $header = $this->msg(
1519  $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
1520  $timestamp,
1521  $dateofrev,
1522  $timeofrev
1523  )->escaped();
1524 
1525  if ( $complete !== 'complete' ) {
1526  return $header;
1527  }
1528 
1529  $title = $rev->getTitle();
1530 
1532  [ 'oldid' => $rev->getId() ] );
1533 
1534  if ( $rev->userCan( RevisionRecord::DELETED_TEXT, $user ) ) {
1535  $editQuery = [ 'action' => 'edit' ];
1536  if ( !$rev->isCurrent() ) {
1537  $editQuery['oldid'] = $rev->getId();
1538  }
1539 
1540  $key = $title->quickUserCan( 'edit', $user ) ? 'editold' : 'viewsourceold';
1541  $msg = $this->msg( $key )->escaped();
1542  $editLink = $this->msg( 'parentheses' )->rawParams(
1543  Linker::linkKnown( $title, $msg, [], $editQuery ) )->escaped();
1544  $header .= ' ' . Html::rawElement(
1545  'span',
1546  [ 'class' => 'mw-diff-edit' ],
1547  $editLink
1548  );
1549  if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1551  'span',
1552  [ 'class' => 'history-deleted' ],
1553  $header
1554  );
1555  }
1556  } else {
1557  $header = Html::rawElement( 'span', [ 'class' => 'history-deleted' ], $header );
1558  }
1559 
1560  return $header;
1561  }
1562 
1575  public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
1576  // shared.css sets diff in interface language/dir, but the actual content
1577  // is often in a different language, mostly the page content language/dir
1578  $header = Html::openElement( 'table', [
1579  'class' => [ 'diff', 'diff-contentalign-' . $this->getDiffLang()->alignStart() ],
1580  'data-mw' => 'interface',
1581  ] );
1582  $userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
1583 
1584  if ( !$diff && !$otitle ) {
1585  $header .= "
1586  <tr class=\"diff-title\" lang=\"{$userLang}\">
1587  <td class=\"diff-ntitle\">{$ntitle}</td>
1588  </tr>";
1589  $multiColspan = 1;
1590  } else {
1591  if ( $diff ) { // Safari/Chrome show broken output if cols not used
1592  $header .= "
1593  <col class=\"diff-marker\" />
1594  <col class=\"diff-content\" />
1595  <col class=\"diff-marker\" />
1596  <col class=\"diff-content\" />";
1597  $colspan = 2;
1598  $multiColspan = 4;
1599  } else {
1600  $colspan = 1;
1601  $multiColspan = 2;
1602  }
1603  if ( $otitle || $ntitle ) {
1604  $header .= "
1605  <tr class=\"diff-title\" lang=\"{$userLang}\">
1606  <td colspan=\"$colspan\" class=\"diff-otitle\">{$otitle}</td>
1607  <td colspan=\"$colspan\" class=\"diff-ntitle\">{$ntitle}</td>
1608  </tr>";
1609  }
1610  }
1611 
1612  if ( $multi != '' ) {
1613  $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1614  "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
1615  }
1616  if ( $notice != '' ) {
1617  $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1618  "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
1619  }
1620 
1621  return $header . $diff . "</table>";
1622  }
1623 
1631  public function setContent( Content $oldContent, Content $newContent ) {
1632  $this->mOldContent = $oldContent;
1633  $this->mNewContent = $newContent;
1634 
1635  $this->mTextLoaded = 2;
1636  $this->mRevisionsLoaded = true;
1637  $this->isContentOverridden = true;
1638  $this->slotDiffRenderers = null;
1639  }
1640 
1646  public function setRevisions(
1647  RevisionRecord $oldRevision = null, RevisionRecord $newRevision
1648  ) {
1649  if ( $oldRevision ) {
1650  $this->mOldRev = new Revision( $oldRevision );
1651  $this->mOldid = $oldRevision->getId();
1652  $this->mOldPage = Title::newFromLinkTarget( $oldRevision->getPageAsLinkTarget() );
1653  // This method is meant for edit diffs and such so there is no reason to provide a
1654  // revision that's not readable to the user, but check it just in case.
1655  $this->mOldContent = $oldRevision->getContent( SlotRecord::MAIN,
1656  RevisionRecord::FOR_THIS_USER, $this->getUser() );
1657  } else {
1658  $this->mOldPage = null;
1659  $this->mOldRev = $this->mOldid = false;
1660  }
1661  $this->mNewRev = new Revision( $newRevision );
1662  $this->mNewid = $newRevision->getId();
1663  $this->mNewPage = Title::newFromLinkTarget( $newRevision->getPageAsLinkTarget() );
1664  $this->mNewContent = $newRevision->getContent( SlotRecord::MAIN,
1665  RevisionRecord::FOR_THIS_USER, $this->getUser() );
1666 
1667  $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded = true;
1668  $this->mTextLoaded = $oldRevision ? 2 : 1;
1669  $this->isContentOverridden = false;
1670  $this->slotDiffRenderers = null;
1671  }
1672 
1679  public function setTextLanguage( Language $lang ) {
1680  $this->mDiffLang = $lang;
1681  }
1682 
1694  public function mapDiffPrevNext( $old, $new ) {
1695  if ( $new === 'prev' ) {
1696  // Show diff between revision $old and the previous one. Get previous one from DB.
1697  $newid = intval( $old );
1698  $oldid = $this->getTitle()->getPreviousRevisionID( $newid );
1699  } elseif ( $new === 'next' ) {
1700  // Show diff between revision $old and the next one. Get next one from DB.
1701  $oldid = intval( $old );
1702  $newid = $this->getTitle()->getNextRevisionID( $oldid );
1703  } else {
1704  $oldid = intval( $old );
1705  $newid = intval( $new );
1706  }
1707 
1708  return [ $oldid, $newid ];
1709  }
1710 
1714  private function loadRevisionIds() {
1715  if ( $this->mRevisionsIdsLoaded ) {
1716  return;
1717  }
1718 
1719  $this->mRevisionsIdsLoaded = true;
1720 
1721  $old = $this->mOldid;
1722  $new = $this->mNewid;
1723 
1724  list( $this->mOldid, $this->mNewid ) = self::mapDiffPrevNext( $old, $new );
1725  if ( $new === 'next' && $this->mNewid === false ) {
1726  # if no result, NewId points to the newest old revision. The only newer
1727  # revision is cur, which is "0".
1728  $this->mNewid = 0;
1729  }
1730 
1731  Hooks::run(
1732  'NewDifferenceEngine',
1733  [ $this->getTitle(), &$this->mOldid, &$this->mNewid, $old, $new ]
1734  );
1735  }
1736 
1750  public function loadRevisionData() {
1751  if ( $this->mRevisionsLoaded ) {
1752  return $this->isContentOverridden || ( $this->mOldRev !== null && $this->mNewRev !== null );
1753  }
1754 
1755  // Whether it succeeds or fails, we don't want to try again
1756  $this->mRevisionsLoaded = true;
1757 
1758  $this->loadRevisionIds();
1759 
1760  // Load the new revision object
1761  if ( $this->mNewid ) {
1762  $this->mNewRev = Revision::newFromId( $this->mNewid );
1763  } else {
1764  $this->mNewRev = Revision::newFromTitle(
1765  $this->getTitle(),
1766  false,
1767  Revision::READ_NORMAL
1768  );
1769  }
1770 
1771  if ( !$this->mNewRev instanceof Revision ) {
1772  return false;
1773  }
1774 
1775  // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
1776  $this->mNewid = $this->mNewRev->getId();
1777  if ( $this->mNewid ) {
1778  $this->mNewPage = $this->mNewRev->getTitle();
1779  } else {
1780  $this->mNewPage = null;
1781  }
1782 
1783  // Load the old revision object
1784  $this->mOldRev = false;
1785  if ( $this->mOldid ) {
1786  $this->mOldRev = Revision::newFromId( $this->mOldid );
1787  } elseif ( $this->mOldid === 0 ) {
1788  $rev = $this->mNewRev->getPrevious();
1789  if ( $rev ) {
1790  $this->mOldid = $rev->getId();
1791  $this->mOldRev = $rev;
1792  } else {
1793  // No previous revision; mark to show as first-version only.
1794  $this->mOldid = false;
1795  $this->mOldRev = false;
1796  }
1797  } /* elseif ( $this->mOldid === false ) leave mOldRev false; */
1798 
1799  if ( is_null( $this->mOldRev ) ) {
1800  return false;
1801  }
1802 
1803  if ( $this->mOldRev && $this->mOldRev->getId() ) {
1804  $this->mOldPage = $this->mOldRev->getTitle();
1805  } else {
1806  $this->mOldPage = null;
1807  }
1808 
1809  // Load tags information for both revisions
1810  $dbr = wfGetDB( DB_REPLICA );
1811  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
1812  if ( $this->mOldid !== false ) {
1813  $tagIds = $dbr->selectFieldValues(
1814  'change_tag',
1815  'ct_tag_id',
1816  [ 'ct_rev_id' => $this->mOldid ],
1817  __METHOD__
1818  );
1819  $tags = [];
1820  foreach ( $tagIds as $tagId ) {
1821  try {
1822  $tags[] = $changeTagDefStore->getName( (int)$tagId );
1823  } catch ( NameTableAccessException $exception ) {
1824  continue;
1825  }
1826  }
1827  $this->mOldTags = implode( ',', $tags );
1828  } else {
1829  $this->mOldTags = false;
1830  }
1831 
1832  $tagIds = $dbr->selectFieldValues(
1833  'change_tag',
1834  'ct_tag_id',
1835  [ 'ct_rev_id' => $this->mNewid ],
1836  __METHOD__
1837  );
1838  $tags = [];
1839  foreach ( $tagIds as $tagId ) {
1840  try {
1841  $tags[] = $changeTagDefStore->getName( (int)$tagId );
1842  } catch ( NameTableAccessException $exception ) {
1843  continue;
1844  }
1845  }
1846  $this->mNewTags = implode( ',', $tags );
1847 
1848  return true;
1849  }
1850 
1859  public function loadText() {
1860  if ( $this->mTextLoaded == 2 ) {
1861  return $this->loadRevisionData() && ( $this->mOldRev === false || $this->mOldContent )
1862  && $this->mNewContent;
1863  }
1864 
1865  // Whether it succeeds or fails, we don't want to try again
1866  $this->mTextLoaded = 2;
1867 
1868  if ( !$this->loadRevisionData() ) {
1869  return false;
1870  }
1871 
1872  if ( $this->mOldRev ) {
1873  $this->mOldContent = $this->mOldRev->getContent(
1874  RevisionRecord::FOR_THIS_USER, $this->getUser()
1875  );
1876  if ( $this->mOldContent === null ) {
1877  return false;
1878  }
1879  }
1880 
1881  $this->mNewContent = $this->mNewRev->getContent(
1882  RevisionRecord::FOR_THIS_USER, $this->getUser()
1883  );
1884  Hooks::run( 'DifferenceEngineLoadTextAfterNewContentIsLoaded', [ $this ] );
1885  if ( $this->mNewContent === null ) {
1886  return false;
1887  }
1888 
1889  return true;
1890  }
1891 
1897  public function loadNewText() {
1898  if ( $this->mTextLoaded >= 1 ) {
1899  return $this->loadRevisionData();
1900  }
1901 
1902  $this->mTextLoaded = 1;
1903 
1904  if ( !$this->loadRevisionData() ) {
1905  return false;
1906  }
1907 
1908  $this->mNewContent = $this->mNewRev->getContent(
1909  RevisionRecord::FOR_THIS_USER, $this->getUser()
1910  );
1911 
1912  Hooks::run( 'DifferenceEngineAfterLoadNewText', [ $this ] );
1913 
1914  return true;
1915  }
1916 
1917 }
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:1224
bool $mCacheHit
Was the diff fetched from cache?
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.
return true to allow those checks to and false if checking is done remove or add to the links of a group of changes in EnhancedChangesList Hook subscribers can return false to omit this line from recentchanges use this to change the tables headers change it to an object instance and return false override the list derivative used $groups Array of ChangesListFilterGroup objects(added in 1.34) 'FileDeleteComplete' 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:1529
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:231
getContentHandler()
Convenience method that returns the ContentHandler singleton for handling the content model that this...
return true to allow those checks to and false if checking is done remove or add to the links of a group of changes in EnhancedChangesList Hook subscribers can return false to omit this line from recentchanges use this to change the tables headers change it to an object instance and return false override the list derivative used $groups Array of ChangesListFilterGroup objects(added in 1.34) 'FileDeleteComplete' 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:1529
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:251
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:209
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:49
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:218
also included in $newHeader if any $newminor
Definition: hooks.txt:1247
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:1806
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:3039
$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:767
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:2021
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).
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:1251
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:767
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:912
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:83
$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:1748
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.
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:2886
getOldRevision()
Get the left side of the diff.
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:620
const PRC_UNPATROLLED
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
Definition: Title.php:274
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:1957
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:592
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:2105
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:141
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:94
also included in $newHeader $rollback
Definition: hooks.txt:1247
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:1572
getParserOutput(ParserOptions $parserOptions, $oldid=null, $forceParse=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1229
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:2147
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:1454
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:1119