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( Revision::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  if ( $rollbackLink ) {
552  $out->preventClickjacking();
553  $rollback = "\u{00A0}\u{00A0}\u{00A0}" . $rollbackLink;
554  }
555  }
556 
557  if ( !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) &&
558  !$this->mNewRev->isDeleted( Revision::DELETED_TEXT )
559  ) {
560  $undoLink = Html::element( 'a', [
561  'href' => $this->mNewPage->getLocalURL( [
562  'action' => 'edit',
563  'undoafter' => $this->mOldid,
564  'undo' => $this->mNewid
565  ] ),
566  'title' => Linker::titleAttrib( 'undo' ),
567  ],
568  $this->msg( 'editundo' )->text()
569  );
570  $revisionTools['mw-diff-undo'] = $undoLink;
571  }
572  }
573 
574  # Make "previous revision link"
575  if ( $samePage && $this->mOldPage && $this->mOldRev->getPrevious() ) {
576  $prevlink = Linker::linkKnown(
577  $this->mOldPage,
578  $this->msg( 'previousdiff' )->escaped(),
579  [ 'id' => 'differences-prevlink' ],
580  [ 'diff' => 'prev', 'oldid' => $this->mOldid ] + $query
581  );
582  } else {
583  $prevlink = "\u{00A0}";
584  }
585 
586  if ( $this->mOldRev->isMinor() ) {
587  $oldminor = ChangesList::flag( 'minor' );
588  } else {
589  $oldminor = '';
590  }
591 
592  $ldel = $this->revisionDeleteLink( $this->mOldRev );
593  $oldRevisionHeader = $this->getRevisionHeader( $this->mOldRev, 'complete' );
594  $oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff', $this->getContext() );
595 
596  $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' .
597  '<div id="mw-diff-otitle2">' .
598  Linker::revUserTools( $this->mOldRev, !$this->unhide ) . '</div>' .
599  '<div id="mw-diff-otitle3">' . $oldminor .
600  Linker::revComment( $this->mOldRev, !$diffOnly, !$this->unhide ) . $ldel . '</div>' .
601  '<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' .
602  '<div id="mw-diff-otitle4">' . $prevlink . '</div>';
603 
604  // Allow extensions to change the $oldHeader variable
605  Hooks::run( 'DifferenceEngineOldHeader', [ $this, &$oldHeader, $prevlink, $oldminor,
606  $diffOnly, $ldel, $this->unhide ] );
607 
608  if ( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) {
609  $deleted = true; // old revisions text is hidden
610  if ( $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
611  $suppressed = true; // also suppressed
612  }
613  }
614 
615  # Check if this user can see the revisions
616  if ( !$this->mOldRev->userCan( Revision::DELETED_TEXT, $user ) ) {
617  $allowed = false;
618  }
619  }
620 
621  $out->addJsConfigVars( [
622  'wgDiffOldId' => $this->mOldid,
623  'wgDiffNewId' => $this->mNewid,
624  ] );
625 
626  # Make "next revision link"
627  # Skip next link on the top revision
628  if ( $samePage && $this->mNewPage && !$this->mNewRev->isCurrent() ) {
629  $nextlink = Linker::linkKnown(
630  $this->mNewPage,
631  $this->msg( 'nextdiff' )->escaped(),
632  [ 'id' => 'differences-nextlink' ],
633  [ 'diff' => 'next', 'oldid' => $this->mNewid ] + $query
634  );
635  } else {
636  $nextlink = "\u{00A0}";
637  }
638 
639  if ( $this->mNewRev->isMinor() ) {
640  $newminor = ChangesList::flag( 'minor' );
641  } else {
642  $newminor = '';
643  }
644 
645  # Handle RevisionDelete links...
646  $rdel = $this->revisionDeleteLink( $this->mNewRev );
647 
648  # Allow extensions to define their own revision tools
649  Hooks::run( 'DiffRevisionTools',
650  [ $this->mNewRev, &$revisionTools, $this->mOldRev, $user ] );
651  $formattedRevisionTools = [];
652  // Put each one in parentheses (poor man's button)
653  foreach ( $revisionTools as $key => $tool ) {
654  $toolClass = is_string( $key ) ? $key : 'mw-diff-tool';
655  $element = Html::rawElement(
656  'span',
657  [ 'class' => $toolClass ],
658  $this->msg( 'parentheses' )->rawParams( $tool )->escaped()
659  );
660  $formattedRevisionTools[] = $element;
661  }
662  $newRevisionHeader = $this->getRevisionHeader( $this->mNewRev, 'complete' ) .
663  ' ' . implode( ' ', $formattedRevisionTools );
664  $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff', $this->getContext() );
665 
666  $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' .
667  '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $this->mNewRev, !$this->unhide ) .
668  " $rollback</div>" .
669  '<div id="mw-diff-ntitle3">' . $newminor .
670  Linker::revComment( $this->mNewRev, !$diffOnly, !$this->unhide ) . $rdel . '</div>' .
671  '<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' .
672  '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
673 
674  // Allow extensions to change the $newHeader variable
675  Hooks::run( 'DifferenceEngineNewHeader', [ $this, &$newHeader, $formattedRevisionTools,
676  $nextlink, $rollback, $newminor, $diffOnly, $rdel, $this->unhide ] );
677 
678  if ( $this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
679  $deleted = true; // new revisions text is hidden
680  if ( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
681  $suppressed = true; // also suppressed
682  }
683  }
684 
685  # If the diff cannot be shown due to a deleted revision, then output
686  # the diff header and links to unhide (if available)...
687  if ( $deleted && ( !$this->unhide || !$allowed ) ) {
688  $this->showDiffStyle();
689  $multi = $this->getMultiNotice();
690  $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
691  if ( !$allowed ) {
692  $msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff';
693  # Give explanation for why revision is not visible
694  $out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
695  [ $msg ] );
696  } else {
697  # Give explanation and add a link to view the diff...
698  $query = $this->getRequest()->appendQueryValue( 'unhide', '1' );
699  $link = $this->getTitle()->getFullURL( $query );
700  $msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff';
701  $out->wrapWikiMsg(
702  "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
703  [ $msg, $link ]
704  );
705  }
706  # Otherwise, output a regular diff...
707  } else {
708  # Add deletion notice if the user is viewing deleted content
709  $notice = '';
710  if ( $deleted ) {
711  $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
712  $notice = "<div id='mw-$msg' class='mw-warning plainlinks'>\n" .
713  $this->msg( $msg )->parse() .
714  "</div>\n";
715  }
716  $this->showDiff( $oldHeader, $newHeader, $notice );
717  if ( !$diffOnly ) {
718  $this->renderNewRevision();
719  }
720  }
721  }
722 
732  public function markPatrolledLink() {
733  if ( $this->mMarkPatrolledLink === null ) {
734  $linkInfo = $this->getMarkPatrolledLinkInfo();
735  // If false, there is no patrol link needed/allowed
736  if ( !$linkInfo || !$this->mNewPage ) {
737  $this->mMarkPatrolledLink = '';
738  } else {
739  $this->mMarkPatrolledLink = ' <span class="patrollink" data-mw="interface">[' .
741  $this->mNewPage,
742  $this->msg( 'markaspatrolleddiff' )->escaped(),
743  [],
744  [
745  'action' => 'markpatrolled',
746  'rcid' => $linkInfo['rcid'],
747  ]
748  ) . ']</span>';
749  // Allow extensions to change the markpatrolled link
750  Hooks::run( 'DifferenceEngineMarkPatrolledLink', [ $this,
751  &$this->mMarkPatrolledLink, $linkInfo['rcid'] ] );
752  }
753  }
755  }
756 
764  protected function getMarkPatrolledLinkInfo() {
765  $user = $this->getUser();
766  $config = $this->getConfig();
767 
768  // Prepare a change patrol link, if applicable
769  if (
770  // Is patrolling enabled and the user allowed to?
771  $config->get( 'UseRCPatrol' ) &&
772  $this->mNewPage && $this->mNewPage->quickUserCan( 'patrol', $user ) &&
773  // Only do this if the revision isn't more than 6 hours older
774  // than the Max RC age (6h because the RC might not be cleaned out regularly)
775  RecentChange::isInRCLifespan( $this->mNewRev->getTimestamp(), 21600 )
776  ) {
777  // Look for an unpatrolled change corresponding to this diff
778  $db = wfGetDB( DB_REPLICA );
779  $change = RecentChange::newFromConds(
780  [
781  'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
782  'rc_this_oldid' => $this->mNewid,
783  'rc_patrolled' => RecentChange::PRC_UNPATROLLED
784  ],
785  __METHOD__
786  );
787 
788  if ( $change && !$change->getPerformer()->equals( $user ) ) {
789  $rcid = $change->getAttribute( 'rc_id' );
790  } else {
791  // None found or the page has been created by the current user.
792  // If the user could patrol this it already would be patrolled
793  $rcid = 0;
794  }
795 
796  // Allow extensions to possibly change the rcid here
797  // For example the rcid might be set to zero due to the user
798  // being the same as the performer of the change but an extension
799  // might still want to show it under certain conditions
800  Hooks::run( 'DifferenceEngineMarkPatrolledRCID', [ &$rcid, $this, $change, $user ] );
801 
802  // Build the link
803  if ( $rcid ) {
804  $this->getOutput()->preventClickjacking();
805  if ( $user->isAllowed( 'writeapi' ) ) {
806  $this->getOutput()->addModules( 'mediawiki.page.patrol.ajax' );
807  }
808 
809  return [
810  'rcid' => $rcid,
811  ];
812  }
813  }
814 
815  // No mark as patrolled link applicable
816  return false;
817  }
818 
824  protected function revisionDeleteLink( $rev ) {
825  $link = Linker::getRevDeleteLink( $this->getUser(), $rev, $rev->getTitle() );
826  if ( $link !== '' ) {
827  $link = "\u{00A0}\u{00A0}\u{00A0}" . $link . ' ';
828  }
829 
830  return $link;
831  }
832 
838  public function renderNewRevision() {
839  if ( $this->isContentOverridden ) {
840  // The code below only works with a Revision object. We could construct a fake revision
841  // (here or in setContent), but since this does not seem needed at the moment,
842  // we'll just fail for now.
843  throw new LogicException(
844  __METHOD__
845  . ' is not supported after calling setContent(). Use setRevisions() instead.'
846  );
847  }
848 
849  $out = $this->getOutput();
850  $revHeader = $this->getRevisionHeader( $this->mNewRev );
851  # Add "current version as of X" title
852  $out->addHTML( "<hr class='diff-hr' id='mw-oldid' />
853  <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
854  # Page content may be handled by a hooked call instead...
855  if ( Hooks::run( 'ArticleContentOnDiff', [ $this, $out ] ) ) {
856  $this->loadNewText();
857  if ( !$this->mNewPage ) {
858  // New revision is unsaved; bail out.
859  // TODO in theory rendering the new revision is a meaningful thing to do
860  // even if it's unsaved, but a lot of untangling is required to do it safely.
861  return;
862  }
863 
864  $out->setRevisionId( $this->mNewid );
865  $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() );
866  $out->setArticleFlag( true );
867 
868  if ( !Hooks::run( 'ArticleRevisionViewCustom',
869  [ $this->mNewRev->getRevisionRecord(), $this->mNewPage, $out ] )
870  ) {
871  // Handled by extension
872  // NOTE: sync with hooks called in Article::view()
873  } elseif ( !Hooks::run( 'ArticleContentViewCustom',
874  [ $this->mNewContent, $this->mNewPage, $out ], '1.32' )
875  ) {
876  // Handled by extension
877  // NOTE: sync with hooks called in Article::view()
878  } else {
879  // Normal page
880  if ( $this->getTitle()->equals( $this->mNewPage ) ) {
881  // If the Title stored in the context is the same as the one
882  // of the new revision, we can use its associated WikiPage
883  // object.
884  $wikiPage = $this->getWikiPage();
885  } else {
886  // Otherwise we need to create our own WikiPage object
887  $wikiPage = WikiPage::factory( $this->mNewPage );
888  }
889 
890  $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRev );
891 
892  # WikiPage::getParserOutput() should not return false, but just in case
893  if ( $parserOutput ) {
894  // Allow extensions to change parser output here
895  if ( Hooks::run( 'DifferenceEngineRenderRevisionAddParserOutput',
896  [ $this, $out, $parserOutput, $wikiPage ] )
897  ) {
898  $out->addParserOutput( $parserOutput, [
899  'enableSectionEditLinks' => $this->mNewRev->isCurrent()
900  && $this->mNewRev->getTitle()->quickUserCan( 'edit', $this->getUser() ),
901  ] );
902  }
903  }
904  }
905  }
906 
907  // Allow extensions to optionally not show the final patrolled link
908  if ( Hooks::run( 'DifferenceEngineRenderRevisionShowFinalPatrolLink' ) ) {
909  # Add redundant patrol link on bottom...
910  $out->addHTML( $this->markPatrolledLink() );
911  }
912  }
913 
920  protected function getParserOutput( WikiPage $page, Revision $rev ) {
921  if ( !$rev->getId() ) {
922  // WikiPage::getParserOutput wants a revision ID. Passing 0 will incorrectly show
923  // the current revision, so fail instead. If need be, WikiPage::getParserOutput
924  // could be made to accept a Revision or RevisionRecord instead of the id.
925  return false;
926  }
927 
928  $parserOptions = $page->makeParserOptions( $this->getContext() );
929  $parserOutput = $page->getParserOutput( $parserOptions, $rev->getId() );
930 
931  return $parserOutput;
932  }
933 
944  public function showDiff( $otitle, $ntitle, $notice = '' ) {
945  // Allow extensions to affect the output here
946  Hooks::run( 'DifferenceEngineShowDiff', [ $this ] );
947 
948  $diff = $this->getDiff( $otitle, $ntitle, $notice );
949  if ( $diff === false ) {
950  $this->showMissingRevision();
951 
952  return false;
953  } else {
954  $this->showDiffStyle();
955  $this->getOutput()->addHTML( $diff );
956 
957  return true;
958  }
959  }
960 
964  public function showDiffStyle() {
965  if ( !$this->isSlotDiffRenderer ) {
966  $this->getOutput()->addModuleStyles( [
967  'mediawiki.interface.helpers.styles',
968  'mediawiki.diff.styles'
969  ] );
970  foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
971  $slotDiffRenderer->addModules( $this->getOutput() );
972  }
973  }
974  }
975 
985  public function getDiff( $otitle, $ntitle, $notice = '' ) {
986  $body = $this->getDiffBody();
987  if ( $body === false ) {
988  return false;
989  }
990 
991  $multi = $this->getMultiNotice();
992  // Display a message when the diff is empty
993  if ( $body === '' ) {
994  $notice .= '<div class="mw-diff-empty">' .
995  $this->msg( 'diff-empty' )->parse() .
996  "</div>\n";
997  }
998 
999  return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
1000  }
1001 
1007  public function getDiffBody() {
1008  $this->mCacheHit = true;
1009  // Check if the diff should be hidden from this user
1010  if ( !$this->isContentOverridden ) {
1011  if ( !$this->loadRevisionData() ) {
1012  return false;
1013  } elseif ( $this->mOldRev &&
1014  !$this->mOldRev->userCan( Revision::DELETED_TEXT, $this->getUser() )
1015  ) {
1016  return false;
1017  } elseif ( $this->mNewRev &&
1018  !$this->mNewRev->userCan( Revision::DELETED_TEXT, $this->getUser() )
1019  ) {
1020  return false;
1021  }
1022  // Short-circuit
1023  if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev &&
1024  $this->mOldRev->getId() && $this->mOldRev->getId() == $this->mNewRev->getId() )
1025  ) {
1026  if ( Hooks::run( 'DifferenceEngineShowEmptyOldContent', [ $this ] ) ) {
1027  return '';
1028  }
1029  }
1030  }
1031 
1032  // Cacheable?
1033  $key = false;
1034  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1035  if ( $this->mOldid && $this->mNewid ) {
1036  // Check if subclass is still using the old way
1037  // for backwards-compatibility
1038  $key = $this->getDiffBodyCacheKey();
1039  if ( $key === null ) {
1040  $key = $cache->makeKey( ...$this->getDiffBodyCacheKeyParams() );
1041  }
1042 
1043  // Try cache
1044  if ( !$this->mRefreshCache ) {
1045  $difftext = $cache->get( $key );
1046  if ( is_string( $difftext ) ) {
1047  wfIncrStats( 'diff_cache.hit' );
1048  $difftext = $this->localiseDiff( $difftext );
1049  $difftext .= "\n<!-- diff cache key $key -->\n";
1050 
1051  return $difftext;
1052  }
1053  } // don't try to load but save the result
1054  }
1055  $this->mCacheHit = false;
1056 
1057  // Loadtext is permission safe, this just clears out the diff
1058  if ( !$this->loadText() ) {
1059  return false;
1060  }
1061 
1062  $difftext = '';
1063  // We've checked for revdelete at the beginning of this method; it's OK to ignore
1064  // read permissions here.
1065  $slotContents = $this->getSlotContents();
1066  foreach ( $this->getSlotDiffRenderers() as $role => $slotDiffRenderer ) {
1067  $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role]['old'],
1068  $slotContents[$role]['new'] );
1069  if ( $slotDiff && $role !== SlotRecord::MAIN ) {
1070  // FIXME: ask SlotRoleHandler::getSlotNameMessage
1071  $slotTitle = $role;
1072  $difftext .= $this->getSlotHeader( $slotTitle );
1073  }
1074  $difftext .= $slotDiff;
1075  }
1076 
1077  // Avoid PHP 7.1 warning from passing $this by reference
1078  $diffEngine = $this;
1079 
1080  // Save to cache for 7 days
1081  if ( !Hooks::run( 'AbortDiffCache', [ &$diffEngine ] ) ) {
1082  wfIncrStats( 'diff_cache.uncacheable' );
1083  } elseif ( $key !== false && $difftext !== false ) {
1084  wfIncrStats( 'diff_cache.miss' );
1085  $cache->set( $key, $difftext, 7 * 86400 );
1086  } else {
1087  wfIncrStats( 'diff_cache.uncacheable' );
1088  }
1089  // localise line numbers and title attribute text
1090  if ( $difftext !== false ) {
1091  $difftext = $this->localiseDiff( $difftext );
1092  }
1093 
1094  return $difftext;
1095  }
1096 
1103  public function getDiffBodyForRole( $role ) {
1104  $diffRenderers = $this->getSlotDiffRenderers();
1105  if ( !isset( $diffRenderers[$role] ) ) {
1106  return false;
1107  }
1108 
1109  $slotContents = $this->getSlotContents();
1110  $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role]['old'],
1111  $slotContents[$role]['new'] );
1112  if ( !$slotDiff ) {
1113  return false;
1114  }
1115 
1116  if ( $role !== SlotRecord::MAIN ) {
1117  // TODO use human-readable role name at least
1118  $slotTitle = $role;
1119  $slotDiff = $this->getSlotHeader( $slotTitle ) . $slotDiff;
1120  }
1121 
1122  return $this->localiseDiff( $slotDiff );
1123  }
1124 
1132  protected function getSlotHeader( $headerText ) {
1133  // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
1134  $columnCount = $this->mOldRev ? 4 : 2;
1135  $userLang = $this->getLanguage()->getHtmlCode();
1136  return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-header', 'lang' => $userLang ],
1137  Html::element( 'th', [ 'colspan' => $columnCount ], $headerText ) );
1138  }
1139 
1149  protected function getDiffBodyCacheKey() {
1150  return null;
1151  }
1152 
1166  protected function getDiffBodyCacheKeyParams() {
1167  if ( !$this->mOldid || !$this->mNewid ) {
1168  throw new MWException( 'mOldid and mNewid must be set to get diff cache key.' );
1169  }
1170 
1171  $engine = $this->getEngine();
1172  $params = [
1173  'diff',
1174  $engine,
1175  self::DIFF_VERSION,
1176  "old-{$this->mOldid}",
1177  "rev-{$this->mNewid}"
1178  ];
1179 
1180  if ( $engine === 'wikidiff2' ) {
1181  $params[] = phpversion( 'wikidiff2' );
1182  }
1183 
1184  if ( !$this->isSlotDiffRenderer ) {
1185  foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1186  $params = array_merge( $params, $slotDiffRenderer->getExtraCacheKeys() );
1187  }
1188  }
1189 
1190  return $params;
1191  }
1192 
1200  public function getExtraCacheKeys() {
1201  // This method is called when the DifferenceEngine is used for a slot diff. We only care
1202  // about special things, not the revision IDs, which are added to the cache key by the
1203  // page-level DifferenceEngine, and which might not have a valid value for this object.
1204  $this->mOldid = 123456789;
1205  $this->mNewid = 987654321;
1206 
1207  // This will repeat a bunch of unnecessary key fields for each slot. Not nice but harmless.
1208  $cacheString = $this->getDiffBodyCacheKey();
1209  if ( $cacheString ) {
1210  return [ $cacheString ];
1211  }
1212 
1214 
1215  // Try to get rid of the standard keys to keep the cache key human-readable:
1216  // call the getDiffBodyCacheKeyParams implementation of the base class, and if
1217  // the child class includes the same keys, drop them.
1218  // Uses an obscure PHP feature where static calls to non-static methods are allowed
1219  // as long as we are already in a non-static method of the same class, and the call context
1220  // ($this) will be inherited.
1221  // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
1222  $standardParams = DifferenceEngine::getDiffBodyCacheKeyParams();
1223  if ( array_slice( $params, 0, count( $standardParams ) ) === $standardParams ) {
1224  $params = array_slice( $params, count( $standardParams ) );
1225  }
1226 
1227  return $params;
1228  }
1229 
1243  public function generateContentDiffBody( Content $old, Content $new ) {
1244  $slotDiffRenderer = $new->getContentHandler()->getSlotDiffRenderer( $this->getContext() );
1245  if (
1246  $slotDiffRenderer instanceof DifferenceEngineSlotDiffRenderer
1247  && $this->isSlotDiffRenderer
1248  ) {
1249  // Oops, we are just about to enter an infinite loop (the slot-level DifferenceEngine
1250  // called a DifferenceEngineSlotDiffRenderer that wraps the same DifferenceEngine class).
1251  // This will happen when a content model has no custom slot diff renderer, it does have
1252  // a custom difference engine, but that does not override this method.
1253  throw new Exception( get_class( $this ) . ': could not maintain backwards compatibility. '
1254  . 'Please use a SlotDiffRenderer.' );
1255  }
1256  return $slotDiffRenderer->getDiff( $old, $new ) . $this->getDebugString();
1257  }
1258 
1271  public function generateTextDiffBody( $otext, $ntext ) {
1272  $slotDiffRenderer = ContentHandler::getForModelID( CONTENT_MODEL_TEXT )
1273  ->getSlotDiffRenderer( $this->getContext() );
1274  if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1275  // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1276  // This is too unlikely to happen to bother handling properly.
1277  throw new Exception( 'The slot diff renderer for text content should be a '
1278  . 'TextSlotDiffRenderer subclass' );
1279  }
1280  return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1281  }
1282 
1289  public static function getEngine() {
1290  global $wgExternalDiffEngine;
1291  // We use the global here instead of Config because we write to the value,
1292  // and Config is not mutable.
1293  if ( $wgExternalDiffEngine == 'wikidiff' || $wgExternalDiffEngine == 'wikidiff3' ) {
1294  wfDeprecated( "\$wgExternalDiffEngine = '{$wgExternalDiffEngine}'", '1.27' );
1295  $wgExternalDiffEngine = false;
1296  } elseif ( $wgExternalDiffEngine == 'wikidiff2' ) {
1297  wfDeprecated( "\$wgExternalDiffEngine = '{$wgExternalDiffEngine}'", '1.32' );
1298  $wgExternalDiffEngine = false;
1299  } elseif ( !is_string( $wgExternalDiffEngine ) && $wgExternalDiffEngine !== false ) {
1300  // And prevent people from shooting themselves in the foot...
1301  wfWarn( '$wgExternalDiffEngine is set to a non-string value, forcing it to false' );
1302  $wgExternalDiffEngine = false;
1303  }
1304 
1305  if ( is_string( $wgExternalDiffEngine ) && is_executable( $wgExternalDiffEngine ) ) {
1306  return $wgExternalDiffEngine;
1307  } elseif ( $wgExternalDiffEngine === false && function_exists( 'wikidiff2_do_diff' ) ) {
1308  return 'wikidiff2';
1309  } else {
1310  // Native PHP
1311  return false;
1312  }
1313  }
1314 
1327  protected function textDiff( $otext, $ntext ) {
1328  $slotDiffRenderer = ContentHandler::getForModelID( CONTENT_MODEL_TEXT )
1329  ->getSlotDiffRenderer( $this->getContext() );
1330  if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1331  // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1332  // This is too unlikely to happen to bother handling properly.
1333  throw new Exception( 'The slot diff renderer for text content should be a '
1334  . 'TextSlotDiffRenderer subclass' );
1335  }
1336  return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1337  }
1338 
1347  protected function debug( $generator = "internal" ) {
1348  if ( !$this->enableDebugComment ) {
1349  return '';
1350  }
1351  $data = [ $generator ];
1352  if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
1353  $data[] = wfHostname();
1354  }
1355  $data[] = wfTimestamp( TS_DB );
1356 
1357  return "<!-- diff generator: " .
1358  implode( " ", array_map( "htmlspecialchars", $data ) ) .
1359  " -->\n";
1360  }
1361 
1362  private function getDebugString() {
1363  $engine = self::getEngine();
1364  if ( $engine === 'wikidiff2' ) {
1365  return $this->debug( 'wikidiff2' );
1366  } elseif ( $engine === false ) {
1367  return $this->debug( 'native PHP' );
1368  } else {
1369  return $this->debug( "external $engine" );
1370  }
1371  }
1372 
1379  private function localiseDiff( $text ) {
1380  $text = $this->localiseLineNumbers( $text );
1381  if ( $this->getEngine() === 'wikidiff2' &&
1382  version_compare( phpversion( 'wikidiff2' ), '1.5.1', '>=' )
1383  ) {
1384  $text = $this->addLocalisedTitleTooltips( $text );
1385  }
1386  return $text;
1387  }
1388 
1396  public function localiseLineNumbers( $text ) {
1397  return preg_replace_callback(
1398  '/<!--LINE (\d+)-->/',
1399  [ $this, 'localiseLineNumbersCb' ],
1400  $text
1401  );
1402  }
1403 
1404  public function localiseLineNumbersCb( $matches ) {
1405  if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
1406  return '';
1407  }
1408 
1409  return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
1410  }
1411 
1418  private function addLocalisedTitleTooltips( $text ) {
1419  return preg_replace_callback(
1420  '/class="mw-diff-movedpara-(left|right)"/',
1421  [ $this, 'addLocalisedTitleTooltipsCb' ],
1422  $text
1423  );
1424  }
1425 
1431  $key = $matches[1] === 'right' ?
1432  'diff-paragraph-moved-toold' :
1433  'diff-paragraph-moved-tonew';
1434  return $matches[0] . ' title="' . $this->msg( $key )->escaped() . '"';
1435  }
1436 
1442  public function getMultiNotice() {
1443  // The notice only make sense if we are diffing two saved revisions of the same page.
1444  if (
1445  !$this->mOldRev || !$this->mNewRev
1446  || !$this->mOldPage || !$this->mNewPage
1447  || !$this->mOldPage->equals( $this->mNewPage )
1448  ) {
1449  return '';
1450  }
1451 
1452  if ( $this->mOldRev->getTimestamp() > $this->mNewRev->getTimestamp() ) {
1453  $oldRev = $this->mNewRev; // flip
1454  $newRev = $this->mOldRev; // flip
1455  } else { // normal case
1456  $oldRev = $this->mOldRev;
1458  }
1459 
1460  // Sanity: don't show the notice if too many rows must be scanned
1461  // @todo show some special message for that case
1462  $nEdits = $this->mNewPage->countRevisionsBetween( $oldRev, $newRev, 1000 );
1463  if ( $nEdits > 0 && $nEdits <= 1000 ) {
1464  $limit = 100; // use diff-multi-manyusers if too many users
1465  $users = $this->mNewPage->getAuthorsBetween( $oldRev, $newRev, $limit );
1466  $numUsers = count( $users );
1467 
1468  if ( $numUsers == 1 && $users[0] == $newRev->getUserText( Revision::RAW ) ) {
1469  $numUsers = 0; // special case to say "by the same user" instead of "by one other user"
1470  }
1471 
1472  return self::intermediateEditsMsg( $nEdits, $numUsers, $limit );
1473  }
1474 
1475  return '';
1476  }
1477 
1487  public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) {
1488  if ( $numUsers === 0 ) {
1489  $msg = 'diff-multi-sameuser';
1490  } elseif ( $numUsers > $limit ) {
1491  $msg = 'diff-multi-manyusers';
1492  $numUsers = $limit;
1493  } else {
1494  $msg = 'diff-multi-otherusers';
1495  }
1496 
1497  return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1498  }
1499 
1509  public function getRevisionHeader( Revision $rev, $complete = '' ) {
1510  $lang = $this->getLanguage();
1511  $user = $this->getUser();
1512  $revtimestamp = $rev->getTimestamp();
1513  $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
1514  $dateofrev = $lang->userDate( $revtimestamp, $user );
1515  $timeofrev = $lang->userTime( $revtimestamp, $user );
1516 
1517  $header = $this->msg(
1518  $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
1519  $timestamp,
1520  $dateofrev,
1521  $timeofrev
1522  )->escaped();
1523 
1524  if ( $complete !== 'complete' ) {
1525  return $header;
1526  }
1527 
1528  $title = $rev->getTitle();
1529 
1531  [ 'oldid' => $rev->getId() ] );
1532 
1533  if ( $rev->userCan( Revision::DELETED_TEXT, $user ) ) {
1534  $editQuery = [ 'action' => 'edit' ];
1535  if ( !$rev->isCurrent() ) {
1536  $editQuery['oldid'] = $rev->getId();
1537  }
1538 
1539  $key = $title->quickUserCan( 'edit', $user ) ? 'editold' : 'viewsourceold';
1540  $msg = $this->msg( $key )->escaped();
1541  $editLink = $this->msg( 'parentheses' )->rawParams(
1542  Linker::linkKnown( $title, $msg, [], $editQuery ) )->escaped();
1543  $header .= ' ' . Html::rawElement(
1544  'span',
1545  [ 'class' => 'mw-diff-edit' ],
1546  $editLink
1547  );
1548  if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
1550  'span',
1551  [ 'class' => 'history-deleted' ],
1552  $header
1553  );
1554  }
1555  } else {
1556  $header = Html::rawElement( 'span', [ 'class' => 'history-deleted' ], $header );
1557  }
1558 
1559  return $header;
1560  }
1561 
1574  public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
1575  // shared.css sets diff in interface language/dir, but the actual content
1576  // is often in a different language, mostly the page content language/dir
1577  $header = Html::openElement( 'table', [
1578  'class' => [ 'diff', 'diff-contentalign-' . $this->getDiffLang()->alignStart() ],
1579  'data-mw' => 'interface',
1580  ] );
1581  $userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
1582 
1583  if ( !$diff && !$otitle ) {
1584  $header .= "
1585  <tr class=\"diff-title\" lang=\"{$userLang}\">
1586  <td class=\"diff-ntitle\">{$ntitle}</td>
1587  </tr>";
1588  $multiColspan = 1;
1589  } else {
1590  if ( $diff ) { // Safari/Chrome show broken output if cols not used
1591  $header .= "
1592  <col class=\"diff-marker\" />
1593  <col class=\"diff-content\" />
1594  <col class=\"diff-marker\" />
1595  <col class=\"diff-content\" />";
1596  $colspan = 2;
1597  $multiColspan = 4;
1598  } else {
1599  $colspan = 1;
1600  $multiColspan = 2;
1601  }
1602  if ( $otitle || $ntitle ) {
1603  $header .= "
1604  <tr class=\"diff-title\" lang=\"{$userLang}\">
1605  <td colspan=\"$colspan\" class=\"diff-otitle\">{$otitle}</td>
1606  <td colspan=\"$colspan\" class=\"diff-ntitle\">{$ntitle}</td>
1607  </tr>";
1608  }
1609  }
1610 
1611  if ( $multi != '' ) {
1612  $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1613  "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
1614  }
1615  if ( $notice != '' ) {
1616  $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1617  "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
1618  }
1619 
1620  return $header . $diff . "</table>";
1621  }
1622 
1630  public function setContent( Content $oldContent, Content $newContent ) {
1631  $this->mOldContent = $oldContent;
1632  $this->mNewContent = $newContent;
1633 
1634  $this->mTextLoaded = 2;
1635  $this->mRevisionsLoaded = true;
1636  $this->isContentOverridden = true;
1637  $this->slotDiffRenderers = null;
1638  }
1639 
1645  public function setRevisions(
1646  RevisionRecord $oldRevision = null, RevisionRecord $newRevision
1647  ) {
1648  if ( $oldRevision ) {
1649  $this->mOldRev = new Revision( $oldRevision );
1650  $this->mOldid = $oldRevision->getId();
1651  $this->mOldPage = Title::newFromLinkTarget( $oldRevision->getPageAsLinkTarget() );
1652  // This method is meant for edit diffs and such so there is no reason to provide a
1653  // revision that's not readable to the user, but check it just in case.
1654  $this->mOldContent = $oldRevision->getContent( SlotRecord::MAIN,
1655  RevisionRecord::FOR_THIS_USER, $this->getUser() );
1656  } else {
1657  $this->mOldPage = null;
1658  $this->mOldRev = $this->mOldid = false;
1659  }
1660  $this->mNewRev = new Revision( $newRevision );
1661  $this->mNewid = $newRevision->getId();
1662  $this->mNewPage = Title::newFromLinkTarget( $newRevision->getPageAsLinkTarget() );
1663  $this->mNewContent = $newRevision->getContent( SlotRecord::MAIN,
1664  RevisionRecord::FOR_THIS_USER, $this->getUser() );
1665 
1666  $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded = true;
1667  $this->mTextLoaded = $oldRevision ? 2 : 1;
1668  $this->isContentOverridden = false;
1669  $this->slotDiffRenderers = null;
1670  }
1671 
1678  public function setTextLanguage( Language $lang ) {
1679  $this->mDiffLang = $lang;
1680  }
1681 
1693  public function mapDiffPrevNext( $old, $new ) {
1694  if ( $new === 'prev' ) {
1695  // Show diff between revision $old and the previous one. Get previous one from DB.
1696  $newid = intval( $old );
1697  $oldid = $this->getTitle()->getPreviousRevisionID( $newid );
1698  } elseif ( $new === 'next' ) {
1699  // Show diff between revision $old and the next one. Get next one from DB.
1700  $oldid = intval( $old );
1701  $newid = $this->getTitle()->getNextRevisionID( $oldid );
1702  } else {
1703  $oldid = intval( $old );
1704  $newid = intval( $new );
1705  }
1706 
1707  return [ $oldid, $newid ];
1708  }
1709 
1713  private function loadRevisionIds() {
1714  if ( $this->mRevisionsIdsLoaded ) {
1715  return;
1716  }
1717 
1718  $this->mRevisionsIdsLoaded = true;
1719 
1720  $old = $this->mOldid;
1721  $new = $this->mNewid;
1722 
1723  list( $this->mOldid, $this->mNewid ) = self::mapDiffPrevNext( $old, $new );
1724  if ( $new === 'next' && $this->mNewid === false ) {
1725  # if no result, NewId points to the newest old revision. The only newer
1726  # revision is cur, which is "0".
1727  $this->mNewid = 0;
1728  }
1729 
1730  Hooks::run(
1731  'NewDifferenceEngine',
1732  [ $this->getTitle(), &$this->mOldid, &$this->mNewid, $old, $new ]
1733  );
1734  }
1735 
1749  public function loadRevisionData() {
1750  if ( $this->mRevisionsLoaded ) {
1751  return $this->isContentOverridden || ( $this->mOldRev !== null && $this->mNewRev !== null );
1752  }
1753 
1754  // Whether it succeeds or fails, we don't want to try again
1755  $this->mRevisionsLoaded = true;
1756 
1757  $this->loadRevisionIds();
1758 
1759  // Load the new revision object
1760  if ( $this->mNewid ) {
1761  $this->mNewRev = Revision::newFromId( $this->mNewid );
1762  } else {
1763  $this->mNewRev = Revision::newFromTitle(
1764  $this->getTitle(),
1765  false,
1766  Revision::READ_NORMAL
1767  );
1768  }
1769 
1770  if ( !$this->mNewRev instanceof Revision ) {
1771  return false;
1772  }
1773 
1774  // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
1775  $this->mNewid = $this->mNewRev->getId();
1776  if ( $this->mNewid ) {
1777  $this->mNewPage = $this->mNewRev->getTitle();
1778  } else {
1779  $this->mNewPage = null;
1780  }
1781 
1782  // Load the old revision object
1783  $this->mOldRev = false;
1784  if ( $this->mOldid ) {
1785  $this->mOldRev = Revision::newFromId( $this->mOldid );
1786  } elseif ( $this->mOldid === 0 ) {
1787  $rev = $this->mNewRev->getPrevious();
1788  if ( $rev ) {
1789  $this->mOldid = $rev->getId();
1790  $this->mOldRev = $rev;
1791  } else {
1792  // No previous revision; mark to show as first-version only.
1793  $this->mOldid = false;
1794  $this->mOldRev = false;
1795  }
1796  } /* elseif ( $this->mOldid === false ) leave mOldRev false; */
1797 
1798  if ( is_null( $this->mOldRev ) ) {
1799  return false;
1800  }
1801 
1802  if ( $this->mOldRev && $this->mOldRev->getId() ) {
1803  $this->mOldPage = $this->mOldRev->getTitle();
1804  } else {
1805  $this->mOldPage = null;
1806  }
1807 
1808  // Load tags information for both revisions
1809  $dbr = wfGetDB( DB_REPLICA );
1810  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
1811  if ( $this->mOldid !== false ) {
1812  $tagIds = $dbr->selectFieldValues(
1813  'change_tag',
1814  'ct_tag_id',
1815  [ 'ct_rev_id' => $this->mOldid ],
1816  __METHOD__
1817  );
1818  $tags = [];
1819  foreach ( $tagIds as $tagId ) {
1820  try {
1821  $tags[] = $changeTagDefStore->getName( (int)$tagId );
1822  } catch ( NameTableAccessException $exception ) {
1823  continue;
1824  }
1825  }
1826  $this->mOldTags = implode( ',', $tags );
1827  } else {
1828  $this->mOldTags = false;
1829  }
1830 
1831  $tagIds = $dbr->selectFieldValues(
1832  'change_tag',
1833  'ct_tag_id',
1834  [ 'ct_rev_id' => $this->mNewid ],
1835  __METHOD__
1836  );
1837  $tags = [];
1838  foreach ( $tagIds as $tagId ) {
1839  try {
1840  $tags[] = $changeTagDefStore->getName( (int)$tagId );
1841  } catch ( NameTableAccessException $exception ) {
1842  continue;
1843  }
1844  }
1845  $this->mNewTags = implode( ',', $tags );
1846 
1847  return true;
1848  }
1849 
1858  public function loadText() {
1859  if ( $this->mTextLoaded == 2 ) {
1860  return $this->loadRevisionData() && ( $this->mOldRev === false || $this->mOldContent )
1861  && $this->mNewContent;
1862  }
1863 
1864  // Whether it succeeds or fails, we don't want to try again
1865  $this->mTextLoaded = 2;
1866 
1867  if ( !$this->loadRevisionData() ) {
1868  return false;
1869  }
1870 
1871  if ( $this->mOldRev ) {
1872  $this->mOldContent = $this->mOldRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
1873  if ( $this->mOldContent === null ) {
1874  return false;
1875  }
1876  }
1877 
1878  $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
1879  Hooks::run( 'DifferenceEngineLoadTextAfterNewContentIsLoaded', [ $this ] );
1880  if ( $this->mNewContent === null ) {
1881  return false;
1882  }
1883 
1884  return true;
1885  }
1886 
1892  public function loadNewText() {
1893  if ( $this->mTextLoaded >= 1 ) {
1894  return $this->loadRevisionData();
1895  }
1896 
1897  $this->mTextLoaded = 1;
1898 
1899  if ( !$this->loadRevisionData() ) {
1900  return false;
1901  }
1902 
1903  $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
1904 
1905  Hooks::run( 'DifferenceEngineAfterLoadNewText', [ $this ] );
1906 
1907  return true;
1908  }
1909 
1910 }
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:138
Revision null false $mOldRev
Old revision (left pane).
setContext(IContextSource $context)
userCan( $field, User $user=null)
Determine if the current user is allowed to view a particular field of this revision, if it&#39;s marked as deleted.
Definition: Revision.php:1224
bool $mCacheHit
Was the diff fetched from cache?
const FOR_THIS_USER
Definition: Revision.php:55
DifferenceEngine is responsible for rendering the difference between two revisions as HTML...
__construct( $context=null, $old=0, $new=0, $rcid=0, $refreshCache=false, $unhide=false)
#-
$enableDebugComment
Set this to true to add debug info to the HTML output.
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition: deferred.txt:11
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
static newFromArchiveRow( $row, $overrides=[])
Make a fake revision object from an archive table row.
Definition: Revision.php:171
getDiffBody()
Get the diff table body, without header.
setTextLanguage(Language $lang)
Set the language in which the diff text is written.
null for the local wiki Added should default to null in handler for backwards compatibility add a value to it if you want to add a cookie that have to vary cache options can modify $query
Definition: hooks.txt:1585
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:232
getContentHandler()
Convenience method that returns the ContentHandler singleton for handling the content model that this...
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
markAsSlotDiffRenderer()
Mark this DifferenceEngine as a slot renderer (as opposed to a page renderer).
getTimestamp()
Definition: Revision.php:994
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
Exception representing a failure to look up a row from a name table.
int false null $mOldid
Revision ID for the old revision.
wfHostname()
Fetch server name for use in error reporting etc.
if(!isset( $args[0])) $lang
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing &#39;/&#39;...
Definition: Html.php:252
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:210
string [] null $mOldTags
Change tags of $mOldRev or null if it does not exist / is not saved.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
static intermediateEditsMsg( $numEdits, $numUsers, $limit)
Get a notice about how many intermediate edits and users there are.
getParserOutput(WikiPage $page, Revision $rev)
$value
const NS_SPECIAL
Definition: Defines.php: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:234
also included in $newHeader if any $newminor
Definition: hooks.txt:1266
getExtraCacheKeys()
Implements DifferenceEngineSlotDiffRenderer::getExtraCacheKeys().
B/C adapter for turning a DifferenceEngine into a SlotDiffRenderer.
const DIFF_VERSION
Constant to indicate diff cache compatibility.
bool $isContentOverridden
Was the content overridden via setContent()? If the content was overridden, most internal state (e...
static generateRollback( $rev, IContextSource $context=null, $options=[ 'verify'])
Generate a rollback link for a given revision.
Definition: Linker.php:1812
loadRevisionIds()
Load revision IDs.
static newFromTitle(LinkTarget $linkTarget, $id=0, $flags=0)
Load either the current, or a specified, revision that&#39;s attached to a given link target...
Definition: Revision.php:137
getNewRevision()
Get the right side of the diff.
IContextSource $context
debug( $generator="internal")
Generate a debug comment indicating diff generating time, server node, and generator backend...
getDiff( $otitle, $ntitle, $notice='')
Get complete diff table, including header.
usually copyright or history_copyright This message must be in HTML not wikitext & $link
Definition: hooks.txt:3050
$newRev
Definition: pageupdater.txt:66
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output $out
Definition: hooks.txt:780
Title null $mOldPage
Title of $mOldRev or null if the old revision does not exist or does not belong to a page...
generateContentDiffBody(Content $old, Content $new)
Generate a diff, no caching.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
string $mMarkPatrolledLink
Link to action=markpatrolled.
getTitle()
Returns the title of the page associated with this entry.
Definition: Revision.php:755
wfMergeErrorArrays(... $args)
Merge arrays in the style of getUserPermissionsErrors, with duplicate removal e.g.
wfIncrStats( $key, $count=1)
Increment a statistics counter.
localiseLineNumbers( $text)
Replace line numbers with the text in the user&#39;s language.
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation use $formDescriptor instead default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt
getPageAsLinkTarget()
Returns the title of the page this revision is associated with as a LinkTarget object.
static getForModelID( $modelId)
Returns the ContentHandler singleton for the given model ID.
Content null $mOldContent
renderNewRevision()
Show the new revision of the page.
static titleAttrib( $name, $options=null, array $msgParams=[])
Given the id of an interface element, constructs the appropriate title attribute from the system mess...
Definition: Linker.php:2027
static flag( $flag, IContextSource $context=null)
Make an "<abbr>" element for a given change flag.
bool $mRefreshCache
Refresh the diff cache.
Renders a slot diff by doing a text diff on the native representation.
Revision null $mNewRev
New revision (right pane).
null for the local wiki Added should default to null in handler for backwards compatibility add a value to it if you want to add a cookie that have to vary cache options can modify as strings Extensions should add to this list prev or next $refreshCache
Definition: hooks.txt:1585
isDeleted( $field)
Definition: Revision.php:888
mapDiffPrevNext( $old, $new)
Maps a revision pair definition as accepted by DifferenceEngine constructor to a pair of actual integ...
getId()
Get revision ID.
Definition: Revision.php:638
bool $mRevisionsIdsLoaded
Have the revisions IDs been loaded.
$wgExternalDiffEngine
Name of the external diff engine to use.
getSlotContents()
Get the old and new content objects for all slots.
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
markPatrolledLink()
Build a link to mark a change as patrolled.
getContent( $role, $audience=self::FOR_PUBLIC, User $user=null)
Returns the Content of the given slot of this revision.
addHeader( $diff, $otitle, $ntitle, $multi='', $notice='')
Add the header to a diff body.
static getEngine()
Process $wgExternalDiffEngine and get a sane, usable engine.
addLocalisedTitleTooltipsCb(array $matches)
getContext()
Get the base IContextSource object.
textDiff( $otext, $ntext)
Generates diff, to be wrapped internally in a logging/instrumentation.
$cache
Definition: mcc.php:33
$params
Title null $mNewPage
Title of $mNewRev or null if the new revision does not exist or does not belong to a page...
getMultiNotice()
If there are revisions between the ones being compared, return a note saying so.
passed in as a query string parameter to the various URLs constructed here(i.e. $nextlink) $rdel also included in $oldHeader $oldminor
Definition: hooks.txt:1270
const DELETED_RESTRICTED
Definition: Revision.php:49
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:780
bool $unhide
Show rev_deleted content if allowed.
static getArchiveQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new archived revision objec...
Definition: Revision.php:525
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:925
setReducedLineNumbers( $value=true)
Set reduced line numbers mode.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don&#39;t need a full Title object...
Definition: SpecialPage.php: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:1766
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
setContent(Content $oldContent, Content $newContent)
Use specified text instead of loading from the database.
const RAW
Definition: Revision.php:56
int $mTextLoaded
How many text blobs have been loaded, 0, 1 or 2?
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
getDiffBodyCacheKeyParams()
Get the cache key parameters.
getNewid()
Get the ID of new revision (right pane) of the diff.
the value to return A Title object or null for latest all implement SearchIndexField $engine
Definition: hooks.txt:2898
getOldRevision()
Get the left side of the diff.
const DELETED_TEXT
Definition: Revision.php:46
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:617
const PRC_UNPATROLLED
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
Definition: Title.php:271
deprecatePublicProperty( $property, $version, $class=null, $component=null)
Mark a property as deprecated.
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
Definition: WikiPage.php:1949
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:589
static isInRCLifespan( $timestamp, $tolerance=0)
Check whether the given timestamp is new enough to have a RC row with a given tolerance as the recent...
getId()
Get revision ID.
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
addLocalisedTitleTooltips( $text)
Add title attributes for tooltips on moved paragraph indicators.
static getRevDeleteLink(User $user, Revision $rev, LinkTarget $title)
Get a revision-deletion link, or disabled link, or nothing, depending on user permissions & the setti...
Definition: Linker.php:2111
getOldid()
Get the ID of old revision (left pane) of the diff.
trait DeprecationHelper
Use this trait in classes which have properties for which public access is deprecated.
static linkKnown( $target, $html=null, $customAttribs=[], $query=[], $options=[ 'known'])
Identical to link(), except $options defaults to &#39;known&#39;.
Definition: Linker.php:146
localiseDiff( $text)
Localise diff output.
int string false null $mNewid
Revision ID for the new revision.
bool $mRevisionsLoaded
Have the revisions been loaded.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
Show an error when a user tries to do something they do not have the necessary permissions for...
deletedIdMarker( $id)
Build a wikitext link toward a deleted revision, if viewable.
bool $isSlotDiffRenderer
Temporary hack for B/C while slot diff related methods of DifferenceEngine are being deprecated...
showDiffStyle()
Add style sheets for diff display.
bool $mReducedLineNumbers
If true, line X is not displayed when X is 1, for example to increase readability and conserve space ...
loadNewText()
Load the text of the new revision, not the old one.
static formatSummaryRow( $tags, $page, IContextSource $context=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:93
also included in $newHeader $rollback
Definition: hooks.txt:1266
static revComment(Revision $rev, $local=false, $isPublic=false, $useParentheses=true)
Wrap and format the given revision&#39;s comment block, if the current user is allowed to view it...
Definition: Linker.php:1572
getParserOutput(ParserOptions $parserOptions, $oldid=null, $forceParse=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1223
showDiff( $otitle, $ntitle, $notice='')
Get the diff text, send it to the OutputPage object Returns false if the diff could not be generated...
Page revision base class.
getDiffBodyCacheKey()
Returns the cache key for diff body text or content.
const DB_REPLICA
Definition: defines.php:25
string [] null $mNewTags
Change tags of $mNewRev or null if it does not exist / is not saved.
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException' returning false will NOT prevent logging a wrapping ErrorException $suppressed
Definition: hooks.txt:2159
generateTextDiffBody( $otext, $ntext)
Generate a diff, no caching.
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition: Revision.php:118
getSlotHeader( $headerText)
Get a slot header for inclusion in a diff body (as a table row).
$content
Definition: pageupdater.txt:72
getWikiPage()
Get the WikiPage object.
Content null $mNewContent
loadRevisionData()
Load revision metadata for the specified revisions.
deletedLink( $id)
Look up a special:Undelete link to the given deleted revision id, as a workaround for being unable to...
localiseLineNumbersCb( $matches)
getRevisionHeader(Revision $rev, $complete='')
Get a header for a specified revision.
SlotDiffRenderer [] $slotDiffRenderers
DifferenceEngine classes for the slots, keyed by role name.
getDiffBodyForRole( $role)
Get the diff table body for one slot, without header.
getDiffLang()
Get the language of the difference engine, defaults to page content language.
return true to allow those checks to and false if checking is done & $user
Definition: hooks.txt:1473
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
loadText()
Load the text of the revisions, as well as revision data.
$matches
static revUserTools( $rev, $isPublic=false, $useParentheses=true)
Generate a user tool link cluster if the current user is allowed to view it.
Definition: Linker.php:1124