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