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