MediaWiki  master
DifferenceEngine.php
Go to the documentation of this file.
1 <?php
33 
57 
59 
66  private const DIFF_VERSION = '1.12';
67 
74  protected $mOldid;
75 
82  protected $mNewid;
83 
95 
105 
111  protected $mOldPage;
112 
118  protected $mNewPage;
119 
124  private $mOldTags;
125 
130  private $mNewTags;
131 
137  private $mOldContent;
138 
144  private $mNewContent;
145 
147  protected $mDiffLang;
148 
150  private $mRevisionsIdsLoaded = false;
151 
153  protected $mRevisionsLoaded = false;
154 
156  protected $mTextLoaded = 0;
157 
166  protected $isContentOverridden = false;
167 
169  protected $mCacheHit = false;
170 
176  public $enableDebugComment = false;
177 
181  protected $mReducedLineNumbers = false;
182 
184  protected $mMarkPatrolledLink = null;
185 
187  protected $unhide = false;
188 
190  protected $mRefreshCache = false;
191 
193  protected $slotDiffRenderers = null;
194 
201  protected $isSlotDiffRenderer = false;
202 
207  private $slotDiffOptions = [];
208 
212  protected $linkRenderer;
213 
218 
222  private $revisionStore;
223 
225  private $hookRunner;
226 
228  private $hookContainer;
229 
240  public function __construct( $context = null, $old = 0, $new = 0, $rcid = 0,
241  $refreshCache = false, $unhide = false
242  ) {
243  $this->deprecatePublicProperty( 'mOldid', '1.32', __CLASS__ );
244  $this->deprecatePublicProperty( 'mNewid', '1.32', __CLASS__ );
245  $this->deprecatePublicProperty( 'mOldPage', '1.32', __CLASS__ );
246  $this->deprecatePublicProperty( 'mNewPage', '1.32', __CLASS__ );
247  $this->deprecatePublicProperty( 'mOldContent', '1.32', __CLASS__ );
248  $this->deprecatePublicProperty( 'mNewContent', '1.32', __CLASS__ );
249  $this->deprecatePublicProperty( 'mRevisionsLoaded', '1.32', __CLASS__ );
250  $this->deprecatePublicProperty( 'mTextLoaded', '1.32', __CLASS__ );
251  $this->deprecatePublicProperty( 'mCacheHit', '1.32', __CLASS__ );
252 
253  if ( $context instanceof IContextSource ) {
254  $this->setContext( $context );
255  }
256 
257  wfDebug( "DifferenceEngine old '$old' new '$new' rcid '$rcid'" );
258 
259  $this->mOldid = $old;
260  $this->mNewid = $new;
261  $this->mRefreshCache = $refreshCache;
262  $this->unhide = $unhide;
263 
264  $services = MediaWikiServices::getInstance();
265  $this->linkRenderer = $services->getLinkRenderer();
266  $this->contentHandlerFactory = $services->getContentHandlerFactory();
267  $this->revisionStore = $services->getRevisionStore();
268  $this->hookContainer = $services->getHookContainer();
269  $this->hookRunner = new HookRunner( $this->hookContainer );
270  }
271 
276  protected function getSlotDiffRenderers() {
277  if ( $this->isSlotDiffRenderer ) {
278  throw new LogicException( __METHOD__ . ' called in slot diff renderer mode' );
279  }
280 
281  if ( $this->slotDiffRenderers === null ) {
282  if ( !$this->loadRevisionData() ) {
283  return [];
284  }
285 
286  $slotContents = $this->getSlotContents();
287  $this->slotDiffRenderers = array_map( function ( $contents ) {
289  $content = $contents['new'] ?: $contents['old'];
290  $context = $this->getContext();
291 
292  return $content->getContentHandler()->getSlotDiffRenderer(
293  $context,
294  $this->slotDiffOptions
295  );
296  }, $slotContents );
297  }
299  }
300 
307  public function markAsSlotDiffRenderer() {
308  $this->isSlotDiffRenderer = true;
309  }
310 
316  protected function getSlotContents() {
317  if ( $this->isContentOverridden ) {
318  return [
319  SlotRecord::MAIN => [
320  'old' => $this->mOldContent,
321  'new' => $this->mNewContent,
322  ]
323  ];
324  } elseif ( !$this->loadRevisionData() ) {
325  return [];
326  }
327 
328  $newSlots = $this->mNewRevisionRecord->getSlots()->getSlots();
329  if ( $this->mOldRevisionRecord ) {
330  $oldSlots = $this->mOldRevisionRecord->getSlots()->getSlots();
331  } else {
332  $oldSlots = [];
333  }
334  // The order here will determine the visual order of the diff. The current logic is
335  // slots of the new revision first in natural order, then deleted ones. This is ad hoc
336  // and should not be relied on - in the future we may want the ordering to depend
337  // on the page type.
338  $roles = array_merge( array_keys( $newSlots ), array_keys( $oldSlots ) );
339 
340  $slots = [];
341  foreach ( $roles as $role ) {
342  $slots[$role] = [
343  'old' => isset( $oldSlots[$role] ) ? $oldSlots[$role]->getContent() : null,
344  'new' => isset( $newSlots[$role] ) ? $newSlots[$role]->getContent() : null,
345  ];
346  }
347  // move main slot to front
348  if ( isset( $slots[SlotRecord::MAIN] ) ) {
349  $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
350  }
351  return $slots;
352  }
353 
354  public function getTitle() {
355  // T202454 avoid errors when there is no title
356  return parent::getTitle() ?: Title::makeTitle( NS_SPECIAL, 'BadTitle/DifferenceEngine' );
357  }
358 
365  public function setReducedLineNumbers( $value = true ) {
366  $this->mReducedLineNumbers = $value;
367  }
368 
374  public function getDiffLang() {
375  if ( $this->mDiffLang === null ) {
376  # Default language in which the diff text is written.
377  $this->mDiffLang = $this->getTitle()->getPageLanguage();
378  }
379 
380  return $this->mDiffLang;
381  }
382 
386  public function wasCacheHit() {
387  return $this->mCacheHit;
388  }
389 
397  public function getOldid() {
398  $this->loadRevisionIds();
399 
400  return $this->mOldid;
401  }
402 
409  public function getNewid() {
410  $this->loadRevisionIds();
411 
412  return $this->mNewid;
413  }
414 
421  public function getOldRevision() {
422  return $this->mOldRevisionRecord ?: null;
423  }
424 
430  public function getNewRevision() {
432  }
433 
442  public function deletedLink( $id ) {
443  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
444  if ( $permissionManager->userHasRight( $this->getUser(), 'deletedhistory' ) ) {
445  $dbr = wfGetDB( DB_REPLICA );
447  $arQuery = $revStore->getArchiveQueryInfo();
448  $row = $dbr->selectRow(
449  $arQuery['tables'],
450  array_merge( $arQuery['fields'], [ 'ar_namespace', 'ar_title' ] ),
451  [ 'ar_rev_id' => $id ],
452  __METHOD__,
453  [],
454  $arQuery['joins']
455  );
456  if ( $row ) {
457  $revRecord = $revStore->newRevisionFromArchiveRow( $row );
458  $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
459 
460  return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( [
461  'target' => $title->getPrefixedText(),
462  'timestamp' => $revRecord->getTimestamp()
463  ] );
464  }
465  }
466 
467  return false;
468  }
469 
477  public function deletedIdMarker( $id ) {
478  $link = $this->deletedLink( $id );
479  if ( $link ) {
480  return "[$link $id]";
481  } else {
482  return (string)$id;
483  }
484  }
485 
486  private function showMissingRevision() {
487  $out = $this->getOutput();
488 
489  $missing = [];
490  if ( $this->mOldRevisionRecord === null ||
491  ( $this->mOldRevisionRecord && $this->mOldContent === null )
492  ) {
493  $missing[] = $this->deletedIdMarker( $this->mOldid );
494  }
495  if ( $this->mNewRevisionRecord === null ||
496  ( $this->mNewRevisionRecord && $this->mNewContent === null )
497  ) {
498  $missing[] = $this->deletedIdMarker( $this->mNewid );
499  }
500 
501  $out->setPageTitle( $this->msg( 'errorpagetitle' ) );
502  $msg = $this->msg( 'difference-missing-revision' )
503  ->params( $this->getLanguage()->listToText( $missing ) )
504  ->numParams( count( $missing ) )
505  ->parseAsBlock();
506  $out->addHTML( $msg );
507  }
508 
514  public function hasDeletedRevision() {
515  $this->loadRevisionData();
516  return (
517  $this->mNewRevisionRecord &&
518  $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
519  ) ||
520  (
521  $this->mOldRevisionRecord &&
522  $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
523  );
524  }
525 
532  public function getPermissionErrors( User $user ) {
533  $this->loadRevisionData();
534  $permErrors = [];
535  $permManager = MediaWikiServices::getInstance()->getPermissionManager();
536  if ( $this->mNewPage ) {
537  $permErrors = $permManager->getPermissionErrors( 'read', $user, $this->mNewPage );
538  }
539  if ( $this->mOldPage ) {
540  $permErrors = wfMergeErrorArrays( $permErrors,
541  $permManager->getPermissionErrors( 'read', $user, $this->mOldPage ) );
542  }
543  return $permErrors;
544  }
545 
551  public function hasSuppressedRevision() {
552  return $this->hasDeletedRevision() && (
553  ( $this->mOldRevisionRecord &&
554  $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) ||
555  ( $this->mNewRevisionRecord &&
556  $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) )
557  );
558  }
559 
571  public function isUserAllowedToSeeRevisions( $user ) {
572  $this->loadRevisionData();
573  // $this->mNewRev will only be falsy if a loading error occurred
574  // (in which case the user is allowed to see).
575  $allowed = !$this->mNewRevisionRecord || RevisionRecord::userCanBitfield(
576  $this->mNewRevisionRecord->getVisibility(),
577  RevisionRecord::DELETED_TEXT,
578  $user
579  );
580  if ( $this->mOldRevisionRecord &&
581  !RevisionRecord::userCanBitfield(
582  $this->mOldRevisionRecord->getVisibility(),
583  RevisionRecord::DELETED_TEXT,
584  $user
585  )
586  ) {
587  $allowed = false;
588  }
589  return $allowed;
590  }
591 
599  public function shouldBeHiddenFromUser( $user ) {
600  return $this->hasDeletedRevision() && ( !$this->unhide ||
601  !$this->isUserAllowedToSeeRevisions( $user ) );
602  }
603 
604  public function showDiffPage( $diffOnly = false ) {
605  # Allow frames except in certain special cases
606  $out = $this->getOutput();
607  $out->allowClickjacking();
608  $out->setRobotPolicy( 'noindex,nofollow' );
609 
610  // Allow extensions to add any extra output here
611  $this->hookRunner->onDifferenceEngineShowDiffPage( $out );
612 
613  if ( !$this->loadRevisionData() ) {
614  if ( $this->hookRunner->onDifferenceEngineShowDiffPageMaybeShowMissingRevision( $this ) ) {
615  $this->showMissingRevision();
616  }
617  return;
618  }
619 
620  $user = $this->getUser();
621  $permErrors = $this->getPermissionErrors( $user );
622  if ( count( $permErrors ) ) {
623  throw new PermissionsError( 'read', $permErrors );
624  }
625 
626  $rollback = '';
627 
628  $query = $this->slotDiffOptions;
629  # Carry over 'diffonly' param via navigation links
630  if ( $diffOnly != $user->getBoolOption( 'diffonly' ) ) {
631  $query['diffonly'] = $diffOnly;
632  }
633  # Cascade unhide param in links for easy deletion browsing
634  if ( $this->unhide ) {
635  $query['unhide'] = 1;
636  }
637 
638  # Check if one of the revisions is deleted/suppressed
639  $deleted = $this->hasDeletedRevision();
640  $suppressed = $this->hasSuppressedRevision();
641  $allowed = $this->isUserAllowedToSeeRevisions( $user );
642 
643  $revisionTools = [];
644 
645  # mOldRevisionRecord is false if the difference engine is called with a "vague" query for
646  # a diff between a version V and its previous version V' AND the version V
647  # is the first version of that article. In that case, V' does not exist.
648  if ( $this->mOldRevisionRecord === false ) {
649  if ( $this->mNewPage ) {
650  $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
651  }
652  $samePage = true;
653  $oldHeader = '';
654  // Allow extensions to change the $oldHeader variable
655  $this->hookRunner->onDifferenceEngineOldHeaderNoOldRev( $oldHeader );
656  } else {
657  $this->hookRunner->onDifferenceEngineViewHeader( $this );
658 
659  // DiffViewHeader hook is hard deprecated since 1.35
660  if ( $this->hookContainer->isRegistered( 'DiffViewHeader' ) ) {
661  // Only create the Revision object if needed
662  // If old or new are falsey, use null
663  $legacyOldRev = $this->mOldRevisionRecord ?
664  new Revision( $this->mOldRevisionRecord ) :
665  null;
666  $legacyNewRev = $this->mNewRevisionRecord ?
667  new Revision( $this->mNewRevisionRecord ) :
668  null;
669  $this->hookRunner->onDiffViewHeader(
670  $this,
671  $legacyOldRev,
672  $legacyNewRev
673  );
674  }
675 
676  if ( !$this->mOldPage || !$this->mNewPage ) {
677  // XXX say something to the user?
678  $samePage = false;
679  } elseif ( $this->mNewPage->equals( $this->mOldPage ) ) {
680  $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
681  $samePage = true;
682  } else {
683  $out->setPageTitle( $this->msg( 'difference-title-multipage',
684  $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) );
685  $out->addSubtitle( $this->msg( 'difference-multipage' ) );
686  $samePage = false;
687  }
688 
689  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
690 
691  if ( $samePage && $this->mNewPage && $permissionManager->quickUserCan(
692  'edit', $user, $this->mNewPage
693  ) ) {
694  if ( $this->mNewRevisionRecord->isCurrent() && $permissionManager->quickUserCan(
695  'rollback', $user, $this->mNewPage
696  ) ) {
697  $rollbackLink = Linker::generateRollback(
698  $this->mNewRevisionRecord,
699  $this->getContext(),
700  [ 'noBrackets' ]
701  );
702  if ( $rollbackLink ) {
703  $out->preventClickjacking();
704  $rollback = "\u{00A0}\u{00A0}\u{00A0}" . $rollbackLink;
705  }
706  }
707 
708  if ( $this->userCanEdit( $this->mOldRevisionRecord ) &&
709  $this->userCanEdit( $this->mNewRevisionRecord )
710  ) {
711  $undoLink = Html::element( 'a', [
712  'href' => $this->mNewPage->getLocalURL( [
713  'action' => 'edit',
714  'undoafter' => $this->mOldid,
715  'undo' => $this->mNewid
716  ] ),
717  'title' => Linker::titleAttrib( 'undo' ),
718  ],
719  $this->msg( 'editundo' )->text()
720  );
721  $revisionTools['mw-diff-undo'] = $undoLink;
722  }
723  }
724  # Make "previous revision link"
725  $hasPrevious = $samePage && $this->mOldPage &&
726  $this->revisionStore->getPreviousRevision( $this->mOldRevisionRecord );
727  if ( $hasPrevious ) {
728  $prevlink = $this->linkRenderer->makeKnownLink(
729  $this->mOldPage,
730  $this->msg( 'previousdiff' )->text(),
731  [ 'id' => 'differences-prevlink' ],
732  [ 'diff' => 'prev', 'oldid' => $this->mOldid ] + $query
733  );
734  } else {
735  $prevlink = "\u{00A0}";
736  }
737 
738  if ( $this->mOldRevisionRecord->isMinor() ) {
739  $oldminor = ChangesList::flag( 'minor' );
740  } else {
741  $oldminor = '';
742  }
743 
744  $oldRevRecord = $this->mOldRevisionRecord;
745 
746  $ldel = $this->revisionDeleteLink( $oldRevRecord );
747  $oldRevisionHeader = $this->getRevisionHeader( $oldRevRecord, 'complete' );
748  $oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff', $this->getContext() );
749 
750  $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' .
751  '<div id="mw-diff-otitle2">' .
752  Linker::revUserTools( $oldRevRecord, !$this->unhide ) . '</div>' .
753  '<div id="mw-diff-otitle3">' . $oldminor .
754  Linker::revComment( $oldRevRecord, !$diffOnly, !$this->unhide ) . $ldel . '</div>' .
755  '<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' .
756  '<div id="mw-diff-otitle4">' . $prevlink . '</div>';
757 
758  // Allow extensions to change the $oldHeader variable
759  $this->hookRunner->onDifferenceEngineOldHeader(
760  $this, $oldHeader, $prevlink, $oldminor, $diffOnly, $ldel, $this->unhide );
761  }
762 
763  $out->addJsConfigVars( [
764  'wgDiffOldId' => $this->mOldid,
765  'wgDiffNewId' => $this->mNewid,
766  ] );
767 
768  # Make "next revision link"
769  # Skip next link on the top revision
770  if ( $samePage && $this->mNewPage && !$this->mNewRevisionRecord->isCurrent() ) {
771  $nextlink = $this->linkRenderer->makeKnownLink(
772  $this->mNewPage,
773  $this->msg( 'nextdiff' )->text(),
774  [ 'id' => 'differences-nextlink' ],
775  [ 'diff' => 'next', 'oldid' => $this->mNewid ] + $query
776  );
777  } else {
778  $nextlink = "\u{00A0}";
779  }
780 
781  if ( $this->mNewRevisionRecord->isMinor() ) {
782  $newminor = ChangesList::flag( 'minor' );
783  } else {
784  $newminor = '';
785  }
786 
787  # Handle RevisionDelete links...
788  $rdel = $this->revisionDeleteLink( $this->mNewRevisionRecord );
789 
790  # Allow extensions to define their own revision tools
791  $this->hookRunner->onDiffTools(
792  $this->mNewRevisionRecord,
793  $revisionTools,
794  $this->mOldRevisionRecord ?: null,
795  $user
796  );
797 
798  # Hook deprecated since 1.35
799  if ( $this->hookContainer->isRegistered( 'DiffRevisionTools' ) ) {
800  # Only create the Revision objects if they are needed
801  $legacyOldRev = $this->mOldRevisionRecord ?
802  new Revision( $this->mOldRevisionRecord ) :
803  null;
804  $legacyNewRev = $this->mNewRevisionRecord ?
805  new Revision( $this->mNewRevisionRecord ) :
806  null;
807  $this->hookRunner->onDiffRevisionTools(
808  $legacyNewRev,
809  $revisionTools,
810  $legacyOldRev,
811  $user
812  );
813  }
814 
815  $formattedRevisionTools = [];
816  // Put each one in parentheses (poor man's button)
817  foreach ( $revisionTools as $key => $tool ) {
818  $toolClass = is_string( $key ) ? $key : 'mw-diff-tool';
819  $element = Html::rawElement(
820  'span',
821  [ 'class' => $toolClass ],
822  $this->msg( 'parentheses' )->rawParams( $tool )->escaped()
823  );
824  $formattedRevisionTools[] = $element;
825  }
826 
827  $newRevRecord = $this->mNewRevisionRecord;
828 
829  $newRevisionHeader = $this->getRevisionHeader( $newRevRecord, 'complete' ) .
830  ' ' . implode( ' ', $formattedRevisionTools );
831  $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff', $this->getContext() );
832 
833  $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' .
834  '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $newRevRecord, !$this->unhide ) .
835  " $rollback</div>" .
836  '<div id="mw-diff-ntitle3">' . $newminor .
837  Linker::revComment( $newRevRecord, !$diffOnly, !$this->unhide ) . $rdel . '</div>' .
838  '<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' .
839  '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
840 
841  // Allow extensions to change the $newHeader variable
842  $this->hookRunner->onDifferenceEngineNewHeader( $this, $newHeader,
843  $formattedRevisionTools, $nextlink, $rollback, $newminor, $diffOnly,
844  $rdel, $this->unhide );
845 
846  # If the diff cannot be shown due to a deleted revision, then output
847  # the diff header and links to unhide (if available)...
848  if ( $this->shouldBeHiddenFromUser( $user ) ) {
849  $this->showDiffStyle();
850  $multi = $this->getMultiNotice();
851  $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
852  if ( !$allowed ) {
853  $msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff';
854  # Give explanation for why revision is not visible
855  $out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
856  [ $msg ] );
857  } else {
858  # Give explanation and add a link to view the diff...
859  $query = $this->getRequest()->appendQueryValue( 'unhide', '1' );
860  $link = $this->getTitle()->getFullURL( $query );
861  $msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff';
862  $out->wrapWikiMsg(
863  "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
864  [ $msg, $link ]
865  );
866  }
867  # Otherwise, output a regular diff...
868  } else {
869  # Add deletion notice if the user is viewing deleted content
870  $notice = '';
871  if ( $deleted ) {
872  $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
873  $notice = "<div id='mw-$msg' class='mw-warning plainlinks'>\n" .
874  $this->msg( $msg )->parse() .
875  "</div>\n";
876  }
877  $this->showDiff( $oldHeader, $newHeader, $notice );
878  if ( !$diffOnly ) {
879  $this->renderNewRevision();
880  }
881  }
882  }
883 
894  public function markPatrolledLink() {
895  if ( $this->mMarkPatrolledLink === null ) {
896  $linkInfo = $this->getMarkPatrolledLinkInfo();
897  // If false, there is no patrol link needed/allowed
898  if ( !$linkInfo || !$this->mNewPage ) {
899  $this->mMarkPatrolledLink = '';
900  } else {
901  $this->mMarkPatrolledLink = ' <span class="patrollink" data-mw="interface">[' .
902  $this->linkRenderer->makeKnownLink(
903  $this->mNewPage,
904  $this->msg( 'markaspatrolleddiff' )->text(),
905  [],
906  [
907  'action' => 'markpatrolled',
908  'rcid' => $linkInfo['rcid'],
909  ]
910  ) . ']</span>';
911  // Allow extensions to change the markpatrolled link
912  $this->hookRunner->onDifferenceEngineMarkPatrolledLink( $this,
913  $this->mMarkPatrolledLink, $linkInfo['rcid'] );
914  }
915  }
917  }
918 
926  protected function getMarkPatrolledLinkInfo() {
927  $user = $this->getUser();
928  $config = $this->getConfig();
929  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
930 
931  // Prepare a change patrol link, if applicable
932  if (
933  // Is patrolling enabled and the user allowed to?
934  $config->get( 'UseRCPatrol' ) &&
935  $this->mNewPage &&
936  $permissionManager->quickUserCan( 'patrol', $user, $this->mNewPage ) &&
937  // Only do this if the revision isn't more than 6 hours older
938  // than the Max RC age (6h because the RC might not be cleaned out regularly)
939  RecentChange::isInRCLifespan( $this->mNewRevisionRecord->getTimestamp(), 21600 )
940  ) {
941  // Look for an unpatrolled change corresponding to this diff
942  $change = RecentChange::newFromConds(
943  [
944  'rc_this_oldid' => $this->mNewid,
945  'rc_patrolled' => RecentChange::PRC_UNPATROLLED
946  ],
947  __METHOD__
948  );
949 
950  if ( $change && !$change->getPerformer()->equals( $user ) ) {
951  $rcid = $change->getAttribute( 'rc_id' );
952  } else {
953  // None found or the page has been created by the current user.
954  // If the user could patrol this it already would be patrolled
955  $rcid = 0;
956  }
957 
958  // Allow extensions to possibly change the rcid here
959  // For example the rcid might be set to zero due to the user
960  // being the same as the performer of the change but an extension
961  // might still want to show it under certain conditions
962  $this->hookRunner->onDifferenceEngineMarkPatrolledRCID( $rcid, $this, $change, $user );
963 
964  // Build the link
965  if ( $rcid ) {
966  $this->getOutput()->preventClickjacking();
967  if ( $permissionManager->userHasRight( $user, 'writeapi' ) ) {
968  $this->getOutput()->addModules( 'mediawiki.misc-authed-curate' );
969  }
970 
971  return [
972  'rcid' => $rcid,
973  ];
974  }
975  }
976 
977  // No mark as patrolled link applicable
978  return false;
979  }
980 
986  private function revisionDeleteLink( RevisionRecord $revRecord ) {
987  $link = Linker::getRevDeleteLink(
988  $this->getUser(),
989  $revRecord,
990  $revRecord->getPageAsLinkTarget()
991  );
992  if ( $link !== '' ) {
993  $link = "\u{00A0}\u{00A0}\u{00A0}" . $link . ' ';
994  }
995 
996  return $link;
997  }
998 
1004  public function renderNewRevision() {
1005  if ( $this->isContentOverridden ) {
1006  // The code below only works with a Revision object. We could construct a fake revision
1007  // (here or in setContent), but since this does not seem needed at the moment,
1008  // we'll just fail for now.
1009  throw new LogicException(
1010  __METHOD__
1011  . ' is not supported after calling setContent(). Use setRevisions() instead.'
1012  );
1013  }
1014 
1015  $out = $this->getOutput();
1016  $revHeader = $this->getRevisionHeader( $this->mNewRevisionRecord );
1017  # Add "current version as of X" title
1018  $out->addHTML( "<hr class='diff-hr' id='mw-oldid' />
1019  <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
1020  # Page content may be handled by a hooked call instead...
1021  if ( $this->hookRunner->onArticleContentOnDiff( $this, $out ) ) {
1022  $this->loadNewText();
1023  if ( !$this->mNewPage ) {
1024  // New revision is unsaved; bail out.
1025  // TODO in theory rendering the new revision is a meaningful thing to do
1026  // even if it's unsaved, but a lot of untangling is required to do it safely.
1027  return;
1028  }
1029 
1030  $out->setRevisionId( $this->mNewid );
1031  $out->setRevisionTimestamp( $this->mNewRevisionRecord->getTimestamp() );
1032  $out->setArticleFlag( true );
1033 
1034  if ( !$this->hookRunner->onArticleRevisionViewCustom(
1035  $this->mNewRevisionRecord, $this->mNewPage, $this->mOldid, $out )
1036  ) {
1037  // Handled by extension
1038  // NOTE: sync with hooks called in Article::view()
1039  } else {
1040  // Normal page
1041  if ( $this->getTitle()->equals( $this->mNewPage ) ) {
1042  // If the Title stored in the context is the same as the one
1043  // of the new revision, we can use its associated WikiPage
1044  // object.
1045  $wikiPage = $this->getWikiPage();
1046  } else {
1047  // Otherwise we need to create our own WikiPage object
1048  $wikiPage = WikiPage::factory( $this->mNewPage );
1049  }
1050 
1051  $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRevisionRecord );
1052 
1053  # WikiPage::getParserOutput() should not return false, but just in case
1054  if ( $parserOutput ) {
1055  // Allow extensions to change parser output here
1056  if ( $this->hookRunner->onDifferenceEngineRenderRevisionAddParserOutput(
1057  $this, $out, $parserOutput, $wikiPage )
1058  ) {
1059  $out->addParserOutput( $parserOutput, [
1060  'enableSectionEditLinks' => $this->mNewRevisionRecord->isCurrent()
1061  && MediaWikiServices::getInstance()->getPermissionManager()->quickUserCan(
1062  'edit',
1063  $this->getUser(),
1064  $this->mNewRevisionRecord->getPageAsLinkTarget()
1065  )
1066  ] );
1067  }
1068  }
1069  }
1070  }
1071 
1072  // Allow extensions to optionally not show the final patrolled link
1073  if ( $this->hookRunner->onDifferenceEngineRenderRevisionShowFinalPatrolLink() ) {
1074  # Add redundant patrol link on bottom...
1075  $out->addHTML( $this->markPatrolledLink() );
1076  }
1077  }
1078 
1085  protected function getParserOutput( WikiPage $page, RevisionRecord $revRecord ) {
1086  if ( !$revRecord->getId() ) {
1087  // WikiPage::getParserOutput wants a revision ID. Passing 0 will incorrectly show
1088  // the current revision, so fail instead. If need be, WikiPage::getParserOutput
1089  // could be made to accept a Revision or RevisionRecord instead of the id.
1090  return false;
1091  }
1092 
1093  $parserOptions = $page->makeParserOptions( $this->getContext() );
1094  $parserOutput = $page->getParserOutput( $parserOptions, $revRecord->getId() );
1095 
1096  return $parserOutput;
1097  }
1098 
1109  public function showDiff( $otitle, $ntitle, $notice = '' ) {
1110  // Allow extensions to affect the output here
1111  $this->hookRunner->onDifferenceEngineShowDiff( $this );
1112 
1113  $diff = $this->getDiff( $otitle, $ntitle, $notice );
1114  if ( $diff === false ) {
1115  $this->showMissingRevision();
1116 
1117  return false;
1118  } else {
1119  $this->showDiffStyle();
1120  $this->getOutput()->addHTML( $diff );
1121 
1122  return true;
1123  }
1124  }
1125 
1129  public function showDiffStyle() {
1130  if ( !$this->isSlotDiffRenderer ) {
1131  $this->getOutput()->addModuleStyles( [
1132  'mediawiki.interface.helpers.styles',
1133  'mediawiki.diff.styles'
1134  ] );
1135  foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1136  $slotDiffRenderer->addModules( $this->getOutput() );
1137  }
1138  }
1139  }
1140 
1150  public function getDiff( $otitle, $ntitle, $notice = '' ) {
1151  $body = $this->getDiffBody();
1152  if ( $body === false ) {
1153  return false;
1154  }
1155 
1156  $multi = $this->getMultiNotice();
1157  // Display a message when the diff is empty
1158  if ( $body === '' ) {
1159  $notice .= '<div class="mw-diff-empty">' .
1160  $this->msg( 'diff-empty' )->parse() .
1161  "</div>\n";
1162  }
1163 
1164  return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
1165  }
1166 
1172  public function getDiffBody() {
1173  $this->mCacheHit = true;
1174  // Check if the diff should be hidden from this user
1175  if ( !$this->isContentOverridden ) {
1176  if ( !$this->loadRevisionData() ) {
1177  return false;
1178  } elseif ( $this->mOldRevisionRecord &&
1179  !RevisionRecord::userCanBitfield(
1180  $this->mOldRevisionRecord->getVisibility(),
1181  RevisionRecord::DELETED_TEXT,
1182  $this->getUser()
1183  )
1184  ) {
1185  return false;
1186  } elseif ( $this->mNewRevisionRecord &&
1187  !RevisionRecord::userCanBitfield(
1188  $this->mNewRevisionRecord->getVisibility(),
1189  RevisionRecord::DELETED_TEXT,
1190  $this->getUser()
1191  )
1192  ) {
1193  return false;
1194  }
1195  // Short-circuit
1196  if ( $this->mOldRevisionRecord === false || (
1197  $this->mOldRevisionRecord &&
1198  $this->mNewRevisionRecord &&
1199  $this->mOldRevisionRecord->getId() &&
1200  $this->mOldRevisionRecord->getId() == $this->mNewRevisionRecord->getId()
1201  ) ) {
1202  if ( $this->hookRunner->onDifferenceEngineShowEmptyOldContent( $this ) ) {
1203  return '';
1204  }
1205  }
1206  }
1207 
1208  // Cacheable?
1209  $key = false;
1210  $services = MediaWikiServices::getInstance();
1211  $cache = $services->getMainWANObjectCache();
1212  $stats = $services->getStatsdDataFactory();
1213  if ( $this->mOldid && $this->mNewid ) {
1214  // Check if subclass is still using the old way
1215  // for backwards-compatibility
1216  $key = $this->getDiffBodyCacheKey();
1217  if ( $key === null ) {
1218  $key = $cache->makeKey( ...$this->getDiffBodyCacheKeyParams() );
1219  }
1220 
1221  // Try cache
1222  if ( !$this->mRefreshCache ) {
1223  $difftext = $cache->get( $key );
1224  if ( is_string( $difftext ) ) {
1225  $stats->updateCount( 'diff_cache.hit', 1 );
1226  $difftext = $this->localiseDiff( $difftext );
1227  $difftext .= "\n<!-- diff cache key $key -->\n";
1228 
1229  return $difftext;
1230  }
1231  } // don't try to load but save the result
1232  }
1233  $this->mCacheHit = false;
1234 
1235  // Loadtext is permission safe, this just clears out the diff
1236  if ( !$this->loadText() ) {
1237  return false;
1238  }
1239 
1240  $difftext = '';
1241  // We've checked for revdelete at the beginning of this method; it's OK to ignore
1242  // read permissions here.
1243  $slotContents = $this->getSlotContents();
1244  foreach ( $this->getSlotDiffRenderers() as $role => $slotDiffRenderer ) {
1245  $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role]['old'],
1246  $slotContents[$role]['new'] );
1247  if ( $slotDiff && $role !== SlotRecord::MAIN ) {
1248  // FIXME: ask SlotRoleHandler::getSlotNameMessage
1249  $slotTitle = $role;
1250  $difftext .= $this->getSlotHeader( $slotTitle );
1251  }
1252  $difftext .= $slotDiff;
1253  }
1254 
1255  // Save to cache for 7 days
1256  if ( !$this->hookRunner->onAbortDiffCache( $this ) ) {
1257  $stats->updateCount( 'diff_cache.uncacheable', 1 );
1258  } elseif ( $key !== false ) {
1259  $stats->updateCount( 'diff_cache.miss', 1 );
1260  $cache->set( $key, $difftext, 7 * 86400 );
1261  } else {
1262  $stats->updateCount( 'diff_cache.uncacheable', 1 );
1263  }
1264  // localise line numbers and title attribute text
1265  $difftext = $this->localiseDiff( $difftext );
1266 
1267  return $difftext;
1268  }
1269 
1276  public function getDiffBodyForRole( $role ) {
1277  $diffRenderers = $this->getSlotDiffRenderers();
1278  if ( !isset( $diffRenderers[$role] ) ) {
1279  return false;
1280  }
1281 
1282  $slotContents = $this->getSlotContents();
1283  $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role]['old'],
1284  $slotContents[$role]['new'] );
1285  if ( !$slotDiff ) {
1286  return false;
1287  }
1288 
1289  if ( $role !== SlotRecord::MAIN ) {
1290  // TODO use human-readable role name at least
1291  $slotTitle = $role;
1292  $slotDiff = $this->getSlotHeader( $slotTitle ) . $slotDiff;
1293  }
1294 
1295  return $this->localiseDiff( $slotDiff );
1296  }
1297 
1305  protected function getSlotHeader( $headerText ) {
1306  // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
1307  $columnCount = $this->mOldRevisionRecord ? 4 : 2;
1308  $userLang = $this->getLanguage()->getHtmlCode();
1309  return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-header', 'lang' => $userLang ],
1310  Html::element( 'th', [ 'colspan' => $columnCount ], $headerText ) );
1311  }
1312 
1322  protected function getDiffBodyCacheKey() {
1323  return null;
1324  }
1325 
1339  protected function getDiffBodyCacheKeyParams() {
1340  if ( !$this->mOldid || !$this->mNewid ) {
1341  throw new MWException( 'mOldid and mNewid must be set to get diff cache key.' );
1342  }
1343 
1344  $engine = $this->getEngine();
1345  $params = [
1346  'diff',
1347  $engine === 'php' ? false : $engine, // Back compat
1349  "old-{$this->mOldid}",
1350  "rev-{$this->mNewid}"
1351  ];
1352 
1353  if ( $engine === 'wikidiff2' ) {
1354  $params[] = phpversion( 'wikidiff2' );
1355  }
1356 
1357  if ( !$this->isSlotDiffRenderer ) {
1358  foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1359  $params = array_merge( $params, $slotDiffRenderer->getExtraCacheKeys() );
1360  }
1361  }
1362 
1363  return $params;
1364  }
1365 
1373  public function getExtraCacheKeys() {
1374  // This method is called when the DifferenceEngine is used for a slot diff. We only care
1375  // about special things, not the revision IDs, which are added to the cache key by the
1376  // page-level DifferenceEngine, and which might not have a valid value for this object.
1377  $this->mOldid = 123456789;
1378  $this->mNewid = 987654321;
1379 
1380  // This will repeat a bunch of unnecessary key fields for each slot. Not nice but harmless.
1381  $cacheString = $this->getDiffBodyCacheKey();
1382  if ( $cacheString ) {
1383  return [ $cacheString ];
1384  }
1385 
1386  $params = $this->getDiffBodyCacheKeyParams();
1387 
1388  // Try to get rid of the standard keys to keep the cache key human-readable:
1389  // call the getDiffBodyCacheKeyParams implementation of the base class, and if
1390  // the child class includes the same keys, drop them.
1391  // Uses an obscure PHP feature where static calls to non-static methods are allowed
1392  // as long as we are already in a non-static method of the same class, and the call context
1393  // ($this) will be inherited.
1394  // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
1395  $standardParams = DifferenceEngine::getDiffBodyCacheKeyParams();
1396  if ( array_slice( $params, 0, count( $standardParams ) ) === $standardParams ) {
1397  $params = array_slice( $params, count( $standardParams ) );
1398  }
1399 
1400  return $params;
1401  }
1402 
1406  public function setSlotDiffOptions( $options ) {
1407  $this->slotDiffOptions = $options;
1408  }
1409 
1423  public function generateContentDiffBody( Content $old, Content $new ) {
1424  $slotDiffRenderer = $new->getContentHandler()->getSlotDiffRenderer( $this->getContext() );
1425  if (
1426  $slotDiffRenderer instanceof DifferenceEngineSlotDiffRenderer
1427  && $this->isSlotDiffRenderer
1428  ) {
1429  // Oops, we are just about to enter an infinite loop (the slot-level DifferenceEngine
1430  // called a DifferenceEngineSlotDiffRenderer that wraps the same DifferenceEngine class).
1431  // This will happen when a content model has no custom slot diff renderer, it does have
1432  // a custom difference engine, but that does not override this method.
1433  throw new Exception( get_class( $this ) . ': could not maintain backwards compatibility. '
1434  . 'Please use a SlotDiffRenderer.' );
1435  }
1436  return $slotDiffRenderer->getDiff( $old, $new ) . $this->getDebugString();
1437  }
1438 
1451  public function generateTextDiffBody( $otext, $ntext ) {
1452  $slotDiffRenderer = $this->contentHandlerFactory
1453  ->getContentHandler( CONTENT_MODEL_TEXT )
1454  ->getSlotDiffRenderer( $this->getContext() );
1455  if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1456  // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1457  // This is too unlikely to happen to bother handling properly.
1458  throw new Exception( 'The slot diff renderer for text content should be a '
1459  . 'TextSlotDiffRenderer subclass' );
1460  }
1461  return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1462  }
1463 
1470  public static function getEngine() {
1471  $diffEngine = MediaWikiServices::getInstance()->getMainConfig()
1472  ->get( 'DiffEngine' );
1473  $externalDiffEngine = MediaWikiServices::getInstance()->getMainConfig()
1474  ->get( 'ExternalDiffEngine' );
1475 
1476  if ( $diffEngine === null ) {
1477  $engines = [ 'external', 'wikidiff2', 'php' ];
1478  } else {
1479  $engines = [ $diffEngine ];
1480  }
1481 
1482  $failureReason = null;
1483  foreach ( $engines as $engine ) {
1484  switch ( $engine ) {
1485  case 'external':
1486  if ( is_string( $externalDiffEngine ) ) {
1487  if ( is_executable( $externalDiffEngine ) ) {
1488  return $externalDiffEngine;
1489  }
1490  $failureReason = 'ExternalDiffEngine config points to a non-executable';
1491  if ( $diffEngine === null ) {
1492  wfDebug( "$failureReason, ignoring" );
1493  }
1494  } else {
1495  $failureReason = 'ExternalDiffEngine config is set to a non-string value';
1496  if ( $diffEngine === null && $externalDiffEngine ) {
1497  wfWarn( "$failureReason, ignoring" );
1498  }
1499  }
1500  break;
1501 
1502  case 'wikidiff2':
1503  if ( function_exists( 'wikidiff2_do_diff' ) ) {
1504  return 'wikidiff2';
1505  }
1506  $failureReason = 'wikidiff2 is not available';
1507  break;
1508 
1509  case 'php':
1510  // Always available.
1511  return 'php';
1512 
1513  default:
1514  throw new DomainException( 'Invalid value for $wgDiffEngine: ' . $engine );
1515  }
1516  }
1517  throw new UnexpectedValueException( "Cannot use diff engine '$engine': $failureReason" );
1518  }
1519 
1532  protected function textDiff( $otext, $ntext ) {
1533  $slotDiffRenderer = $this->contentHandlerFactory
1534  ->getContentHandler( CONTENT_MODEL_TEXT )
1535  ->getSlotDiffRenderer( $this->getContext() );
1536  if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1537  // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1538  // This is too unlikely to happen to bother handling properly.
1539  throw new Exception( 'The slot diff renderer for text content should be a '
1540  . 'TextSlotDiffRenderer subclass' );
1541  }
1542  return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1543  }
1544 
1553  protected function debug( $generator = "internal" ) {
1554  if ( !$this->enableDebugComment ) {
1555  return '';
1556  }
1557  $data = [ $generator ];
1558  if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
1559  $data[] = wfHostname();
1560  }
1561  $data[] = wfTimestamp( TS_DB );
1562 
1563  return "<!-- diff generator: " .
1564  implode( " ", array_map( "htmlspecialchars", $data ) ) .
1565  " -->\n";
1566  }
1567 
1568  private function getDebugString() {
1569  $engine = self::getEngine();
1570  if ( $engine === 'wikidiff2' ) {
1571  return $this->debug( 'wikidiff2' );
1572  } elseif ( $engine === 'php' ) {
1573  return $this->debug( 'native PHP' );
1574  } else {
1575  return $this->debug( "external $engine" );
1576  }
1577  }
1578 
1585  private function localiseDiff( $text ) {
1586  $text = $this->localiseLineNumbers( $text );
1587  if ( $this->getEngine() === 'wikidiff2' &&
1588  version_compare( phpversion( 'wikidiff2' ), '1.5.1', '>=' )
1589  ) {
1590  $text = $this->addLocalisedTitleTooltips( $text );
1591  }
1592  return $text;
1593  }
1594 
1602  public function localiseLineNumbers( $text ) {
1603  return preg_replace_callback(
1604  '/<!--LINE (\d+)-->/',
1605  [ $this, 'localiseLineNumbersCb' ],
1606  $text
1607  );
1608  }
1609 
1610  public function localiseLineNumbersCb( $matches ) {
1611  if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
1612  return '';
1613  }
1614 
1615  return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
1616  }
1617 
1624  private function addLocalisedTitleTooltips( $text ) {
1625  return preg_replace_callback(
1626  '/class="mw-diff-movedpara-(left|right)"/',
1627  [ $this, 'addLocalisedTitleTooltipsCb' ],
1628  $text
1629  );
1630  }
1631 
1636  private function addLocalisedTitleTooltipsCb( array $matches ) {
1637  $key = $matches[1] === 'right' ?
1638  'diff-paragraph-moved-toold' :
1639  'diff-paragraph-moved-tonew';
1640  return $matches[0] . ' title="' . $this->msg( $key )->escaped() . '"';
1641  }
1642 
1648  public function getMultiNotice() {
1649  // The notice only make sense if we are diffing two saved revisions of the same page.
1650  if (
1651  !$this->mOldRevisionRecord || !$this->mNewRevisionRecord
1652  || !$this->mOldPage || !$this->mNewPage
1653  || !$this->mOldPage->equals( $this->mNewPage )
1654  || $this->mOldRevisionRecord->getId() === null
1655  || $this->mNewRevisionRecord->getId() === null
1656  // (T237709) Deleted revs might have different page IDs
1657  || $this->mNewPage->getArticleID() !== $this->mOldRevisionRecord->getPageId()
1658  || $this->mNewPage->getArticleID() !== $this->mNewRevisionRecord->getPageId()
1659  ) {
1660  return '';
1661  }
1662 
1663  if ( $this->mOldRevisionRecord->getTimestamp() > $this->mNewRevisionRecord->getTimestamp() ) {
1664  $oldRevRecord = $this->mNewRevisionRecord; // flip
1665  $newRevRecord = $this->mOldRevisionRecord; // flip
1666  } else { // normal case
1667  $oldRevRecord = $this->mOldRevisionRecord;
1668  $newRevRecord = $this->mNewRevisionRecord;
1669  }
1670 
1671  // Sanity: don't show the notice if too many rows must be scanned
1672  // @todo show some special message for that case
1673  $nEdits = $this->revisionStore->countRevisionsBetween(
1674  $this->mNewPage->getArticleID(),
1675  $oldRevRecord,
1676  $newRevRecord,
1677  1000
1678  );
1679  if ( $nEdits > 0 && $nEdits <= 1000 ) {
1680  $limit = 100; // use diff-multi-manyusers if too many users
1681  try {
1682  $users = $this->revisionStore->getAuthorsBetween(
1683  $this->mNewPage->getArticleID(),
1684  $oldRevRecord,
1685  $newRevRecord,
1686  null,
1687  $limit
1688  );
1689  $numUsers = count( $users );
1690 
1691  $newRevUser = $newRevRecord->getUser( RevisionRecord::RAW );
1692  $newRevUserText = $newRevUser ? $newRevUser->getName() : '';
1693  if ( $numUsers == 1 && $users[0]->getName() == $newRevUserText ) {
1694  $numUsers = 0; // special case to say "by the same user" instead of "by one other user"
1695  }
1696  } catch ( InvalidArgumentException $e ) {
1697  $numUsers = 0;
1698  }
1699 
1700  return self::intermediateEditsMsg( $nEdits, $numUsers, $limit );
1701  }
1702 
1703  return '';
1704  }
1705 
1715  public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) {
1716  if ( $numUsers === 0 ) {
1717  $msg = 'diff-multi-sameuser';
1718  } elseif ( $numUsers > $limit ) {
1719  $msg = 'diff-multi-manyusers';
1720  $numUsers = $limit;
1721  } else {
1722  $msg = 'diff-multi-otherusers';
1723  }
1724 
1725  return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1726  }
1727 
1732  private function userCanEdit( RevisionRecord $revRecord ) {
1733  $user = $this->getUser();
1734 
1735  if ( !RevisionRecord::userCanBitfield(
1736  $revRecord->getVisibility(),
1737  RevisionRecord::DELETED_TEXT,
1738  $user
1739  ) ) {
1740  return false;
1741  }
1742 
1743  return true;
1744  }
1745 
1755  public function getRevisionHeader( $rev, $complete = '' ) {
1756  if ( $rev instanceof Revision ) {
1757  wfDeprecated( __METHOD__ . ' with a Revision object', '1.35' );
1758  $rev = $rev->getRevisionRecord();
1759  }
1760 
1761  $lang = $this->getLanguage();
1762  $user = $this->getUser();
1763  $revtimestamp = $rev->getTimestamp();
1764  $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
1765  $dateofrev = $lang->userDate( $revtimestamp, $user );
1766  $timeofrev = $lang->userTime( $revtimestamp, $user );
1767 
1768  $header = $this->msg(
1769  $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
1770  $timestamp,
1771  $dateofrev,
1772  $timeofrev
1773  );
1774 
1775  if ( $complete !== 'complete' ) {
1776  return $header->escaped();
1777  }
1778 
1779  $title = $rev->getPageAsLinkTarget();
1780 
1781  $header = $this->linkRenderer->makeKnownLink( $title, $header->text(), [],
1782  [ 'oldid' => $rev->getId() ] );
1783 
1784  if ( $this->userCanEdit( $rev ) ) {
1785  $editQuery = [ 'action' => 'edit' ];
1786  if ( !$rev->isCurrent() ) {
1787  $editQuery['oldid'] = $rev->getId();
1788  }
1789 
1790  $key = MediaWikiServices::getInstance()->getPermissionManager()
1791  ->quickUserCan( 'edit', $user, $title ) ? 'editold' : 'viewsourceold';
1792  $msg = $this->msg( $key )->text();
1793  $editLink = $this->msg( 'parentheses' )->rawParams(
1794  $this->linkRenderer->makeKnownLink( $title, $msg, [], $editQuery ) )->escaped();
1795  $header .= ' ' . Html::rawElement(
1796  'span',
1797  [ 'class' => 'mw-diff-edit' ],
1798  $editLink
1799  );
1800  if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1802  'span',
1803  [ 'class' => 'history-deleted' ],
1804  $header
1805  );
1806  }
1807  } else {
1808  $header = Html::rawElement( 'span', [ 'class' => 'history-deleted' ], $header );
1809  }
1810 
1811  return $header;
1812  }
1813 
1826  public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
1827  // shared.css sets diff in interface language/dir, but the actual content
1828  // is often in a different language, mostly the page content language/dir
1829  $header = Html::openElement( 'table', [
1830  'class' => [
1831  'diff',
1832  'diff-contentalign-' . $this->getDiffLang()->alignStart(),
1833  'diff-editfont-' . $this->getUser()->getOption( 'editfont' )
1834  ],
1835  'data-mw' => 'interface',
1836  ] );
1837  $userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
1838 
1839  if ( !$diff && !$otitle ) {
1840  $header .= "
1841  <tr class=\"diff-title\" lang=\"{$userLang}\">
1842  <td class=\"diff-ntitle\">{$ntitle}</td>
1843  </tr>";
1844  $multiColspan = 1;
1845  } else {
1846  if ( $diff ) { // Safari/Chrome show broken output if cols not used
1847  $header .= "
1848  <col class=\"diff-marker\" />
1849  <col class=\"diff-content\" />
1850  <col class=\"diff-marker\" />
1851  <col class=\"diff-content\" />";
1852  $colspan = 2;
1853  $multiColspan = 4;
1854  } else {
1855  $colspan = 1;
1856  $multiColspan = 2;
1857  }
1858  if ( $otitle || $ntitle ) {
1859  $header .= "
1860  <tr class=\"diff-title\" lang=\"{$userLang}\">
1861  <td colspan=\"$colspan\" class=\"diff-otitle\">{$otitle}</td>
1862  <td colspan=\"$colspan\" class=\"diff-ntitle\">{$ntitle}</td>
1863  </tr>";
1864  }
1865  }
1866 
1867  if ( $multi != '' ) {
1868  $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1869  "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
1870  }
1871  if ( $notice != '' ) {
1872  $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1873  "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
1874  }
1875 
1876  return $header . $diff . "</table>";
1877  }
1878 
1886  public function setContent( Content $oldContent, Content $newContent ) {
1887  $this->mOldContent = $oldContent;
1888  $this->mNewContent = $newContent;
1889 
1890  $this->mTextLoaded = 2;
1891  $this->mRevisionsLoaded = true;
1892  $this->isContentOverridden = true;
1893  $this->slotDiffRenderers = null;
1894  }
1895 
1901  public function setRevisions(
1902  ?RevisionRecord $oldRevision, RevisionRecord $newRevision
1903  ) {
1904  if ( $oldRevision ) {
1905  $this->mOldRevisionRecord = $oldRevision;
1906  $this->mOldid = $oldRevision->getId();
1907  $this->mOldPage = Title::newFromLinkTarget( $oldRevision->getPageAsLinkTarget() );
1908  // This method is meant for edit diffs and such so there is no reason to provide a
1909  // revision that's not readable to the user, but check it just in case.
1910  $this->mOldContent = $oldRevision->getContent( SlotRecord::MAIN,
1911  RevisionRecord::FOR_THIS_USER, $this->getUser() );
1912  } else {
1913  $this->mOldPage = null;
1914  $this->mOldRevisionRecord = $this->mOldid = false;
1915  }
1916  $this->mNewRevisionRecord = $newRevision;
1917  $this->mNewid = $newRevision->getId();
1918  $this->mNewPage = Title::newFromLinkTarget( $newRevision->getPageAsLinkTarget() );
1919  $this->mNewContent = $newRevision->getContent( SlotRecord::MAIN,
1920  RevisionRecord::FOR_THIS_USER, $this->getUser() );
1921 
1922  $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded = true;
1923  $this->mTextLoaded = $oldRevision ? 2 : 1;
1924  $this->isContentOverridden = false;
1925  $this->slotDiffRenderers = null;
1926  }
1927 
1934  public function setTextLanguage( Language $lang ) {
1935  $this->mDiffLang = $lang;
1936  }
1937 
1950  public function mapDiffPrevNext( $old, $new ) {
1951  $rl = MediaWikiServices::getInstance()->getRevisionLookup();
1952  if ( $new === 'prev' ) {
1953  // Show diff between revision $old and the previous one. Get previous one from DB.
1954  $newid = intval( $old );
1955  $oldid = false;
1956  $newRev = $rl->getRevisionById( $newid );
1957  if ( $newRev ) {
1958  $oldRev = $rl->getPreviousRevision( $newRev );
1959  if ( $oldRev ) {
1960  $oldid = $oldRev->getId();
1961  }
1962  }
1963  } elseif ( $new === 'next' ) {
1964  // Show diff between revision $old and the next one. Get next one from DB.
1965  $oldid = intval( $old );
1966  $newid = false;
1967  $oldRev = $rl->getRevisionById( $oldid );
1968  if ( $oldRev ) {
1969  $newRev = $rl->getNextRevision( $oldRev );
1970  if ( $newRev ) {
1971  $newid = $newRev->getId();
1972  }
1973  }
1974  } else {
1975  $oldid = intval( $old );
1976  $newid = intval( $new );
1977  }
1978 
1979  return [ $oldid, $newid ];
1980  }
1981 
1985  private function loadRevisionIds() {
1986  if ( $this->mRevisionsIdsLoaded ) {
1987  return;
1988  }
1989 
1990  $this->mRevisionsIdsLoaded = true;
1991 
1992  $old = $this->mOldid;
1993  $new = $this->mNewid;
1994 
1995  list( $this->mOldid, $this->mNewid ) = self::mapDiffPrevNext( $old, $new );
1996  if ( $new === 'next' && $this->mNewid === false ) {
1997  # if no result, NewId points to the newest old revision. The only newer
1998  # revision is cur, which is "0".
1999  $this->mNewid = 0;
2000  }
2001 
2002  $this->hookRunner->onNewDifferenceEngine(
2003  $this->getTitle(), $this->mOldid, $this->mNewid, $old, $new );
2004  }
2005 
2019  public function loadRevisionData() {
2020  if ( $this->mRevisionsLoaded ) {
2021  return $this->isContentOverridden ||
2022  ( $this->mOldRevisionRecord !== null && $this->mNewRevisionRecord !== null );
2023  }
2024 
2025  // Whether it succeeds or fails, we don't want to try again
2026  $this->mRevisionsLoaded = true;
2027 
2028  $this->loadRevisionIds();
2029 
2030  // Load the new revision object
2031  if ( $this->mNewid ) {
2032  $this->mNewRevisionRecord = $this->revisionStore->getRevisionById( $this->mNewid );
2033  } else {
2034  $this->mNewRevisionRecord = $this->revisionStore->getRevisionByTitle( $this->getTitle() );
2035  }
2036 
2037  if ( !$this->mNewRevisionRecord instanceof RevisionRecord ) {
2038  return false;
2039  }
2040 
2041  // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
2042  $this->mNewid = $this->mNewRevisionRecord->getId();
2043  if ( $this->mNewid ) {
2044  $this->mNewPage = Title::newFromLinkTarget(
2045  $this->mNewRevisionRecord->getPageAsLinkTarget()
2046  );
2047  } else {
2048  $this->mNewPage = null;
2049  }
2050 
2051  // Load the old revision object
2052  $this->mOldRevisionRecord = false;
2053  if ( $this->mOldid ) {
2054  $this->mOldRevisionRecord = $this->revisionStore->getRevisionById( $this->mOldid );
2055  } elseif ( $this->mOldid === 0 ) {
2056  $revRecord = $this->revisionStore->getPreviousRevision( $this->mNewRevisionRecord );
2057  if ( $revRecord ) {
2058  $this->mOldid = $revRecord->getId();
2059  $this->mOldRevisionRecord = $revRecord;
2060  } else {
2061  // No previous revision; mark to show as first-version only.
2062  $this->mOldid = false;
2063  $this->mOldRevisionRecord = false;
2064  }
2065  } /* elseif ( $this->mOldid === false ) leave mOldRevisionRecord false; */
2066 
2067  if ( $this->mOldRevisionRecord === null ) {
2068  return false;
2069  }
2070 
2071  if ( $this->mOldRevisionRecord && $this->mOldRevisionRecord->getId() ) {
2072  $this->mOldPage = Title::newFromLinkTarget(
2073  $this->mOldRevisionRecord->getPageAsLinkTarget()
2074  );
2075  } else {
2076  $this->mOldPage = null;
2077  }
2078 
2079  // Load tags information for both revisions
2080  $dbr = wfGetDB( DB_REPLICA );
2081  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
2082  if ( $this->mOldid !== false ) {
2083  $tagIds = $dbr->selectFieldValues(
2084  'change_tag',
2085  'ct_tag_id',
2086  [ 'ct_rev_id' => $this->mOldid ],
2087  __METHOD__
2088  );
2089  $tags = [];
2090  foreach ( $tagIds as $tagId ) {
2091  try {
2092  $tags[] = $changeTagDefStore->getName( (int)$tagId );
2093  } catch ( NameTableAccessException $exception ) {
2094  continue;
2095  }
2096  }
2097  $this->mOldTags = implode( ',', $tags );
2098  } else {
2099  $this->mOldTags = false;
2100  }
2101 
2102  $tagIds = $dbr->selectFieldValues(
2103  'change_tag',
2104  'ct_tag_id',
2105  [ 'ct_rev_id' => $this->mNewid ],
2106  __METHOD__
2107  );
2108  $tags = [];
2109  foreach ( $tagIds as $tagId ) {
2110  try {
2111  $tags[] = $changeTagDefStore->getName( (int)$tagId );
2112  } catch ( NameTableAccessException $exception ) {
2113  continue;
2114  }
2115  }
2116  $this->mNewTags = implode( ',', $tags );
2117 
2118  return true;
2119  }
2120 
2129  public function loadText() {
2130  if ( $this->mTextLoaded == 2 ) {
2131  return $this->loadRevisionData() &&
2132  ( $this->mOldRevisionRecord === false || $this->mOldContent )
2133  && $this->mNewContent;
2134  }
2135 
2136  // Whether it succeeds or fails, we don't want to try again
2137  $this->mTextLoaded = 2;
2138 
2139  if ( !$this->loadRevisionData() ) {
2140  return false;
2141  }
2142 
2143  if ( $this->mOldRevisionRecord ) {
2144  $this->mOldContent = $this->mOldRevisionRecord->getContent(
2145  SlotRecord::MAIN,
2146  RevisionRecord::FOR_THIS_USER,
2147  $this->getUser()
2148  );
2149  if ( $this->mOldContent === null ) {
2150  return false;
2151  }
2152  }
2153 
2154  $this->mNewContent = $this->mNewRevisionRecord->getContent(
2155  SlotRecord::MAIN,
2156  RevisionRecord::FOR_THIS_USER,
2157  $this->getUser()
2158  );
2159  $this->hookRunner->onDifferenceEngineLoadTextAfterNewContentIsLoaded( $this );
2160  if ( $this->mNewContent === null ) {
2161  return false;
2162  }
2163 
2164  return true;
2165  }
2166 
2172  public function loadNewText() {
2173  if ( $this->mTextLoaded >= 1 ) {
2174  return $this->loadRevisionData();
2175  }
2176 
2177  $this->mTextLoaded = 1;
2178 
2179  if ( !$this->loadRevisionData() ) {
2180  return false;
2181  }
2182 
2183  $this->mNewContent = $this->mNewRevisionRecord->getContent(
2184  SlotRecord::MAIN,
2185  RevisionRecord::FOR_THIS_USER,
2186  $this->getUser()
2187  );
2188 
2189  $this->hookRunner->onDifferenceEngineAfterLoadNewText( $this );
2190 
2191  return true;
2192  }
2193 
2194 }
Content\getContentHandler
getContentHandler()
Convenience method that returns the ContentHandler singleton for handling the content model that this...
DifferenceEngine\$mRevisionsIdsLoaded
bool $mRevisionsIdsLoaded
Have the revisions IDs been loaded.
Definition: DifferenceEngine.php:150
DifferenceEngine\$mNewRevisionRecord
RevisionRecord null $mNewRevisionRecord
New revision (right pane).
Definition: DifferenceEngine.php:104
ContextSource\$context
IContextSource $context
Definition: ContextSource.php:34
ContextSource\getConfig
getConfig()
Definition: ContextSource.php:67
DifferenceEngine\getSlotContents
getSlotContents()
Get the old and new content objects for all slots.
Definition: DifferenceEngine.php:316
wfMergeErrorArrays
wfMergeErrorArrays(... $args)
Merge arrays in the style of PermissionManager::getPermissionErrors, with duplicate removal e....
Definition: GlobalFunctions.php:180
DifferenceEngine\markPatrolledLink
markPatrolledLink()
Build a link to mark a change as patrolled.
Definition: DifferenceEngine.php:894
ContextSource\getContext
getContext()
Get the base IContextSource object.
Definition: ContextSource.php:42
Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:46
DifferenceEngine\getEngine
static getEngine()
Process DiffEngine config and get a sane, usable engine.
Definition: DifferenceEngine.php:1470
DifferenceEngine\$mTextLoaded
int $mTextLoaded
How many text blobs have been loaded, 0, 1 or 2?
Definition: DifferenceEngine.php:156
DifferenceEngine\addLocalisedTitleTooltipsCb
addLocalisedTitleTooltipsCb(array $matches)
Definition: DifferenceEngine.php:1636
DifferenceEngine\getDiffBodyCacheKeyParams
getDiffBodyCacheKeyParams()
Get the cache key parameters.
Definition: DifferenceEngine.php:1339
DifferenceEngine\$unhide
bool $unhide
Show rev_deleted content if allowed.
Definition: DifferenceEngine.php:187
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:157
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:37
RecentChange\newFromConds
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
Definition: RecentChange.php:220
DifferenceEngine\setReducedLineNumbers
setReducedLineNumbers( $value=true)
Set reduced line numbers mode.
Definition: DifferenceEngine.php:365
Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:81
DifferenceEngine\shouldBeHiddenFromUser
shouldBeHiddenFromUser( $user)
Checks whether the diff should be hidden from the current user This is based on whether the user is a...
Definition: DifferenceEngine.php:599
DifferenceEngine\setContent
setContent(Content $oldContent, Content $newContent)
Use specified text instead of loading from the database.
Definition: DifferenceEngine.php:1886
MediaWiki\Linker\LinkRenderer
Class that generates HTML links for pages.
Definition: LinkRenderer.php:41
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1809
DifferenceEngine\getNewid
getNewid()
Get the ID of new revision (right pane) of the diff.
Definition: DifferenceEngine.php:409
DifferenceEngine\getOldRevision
getOldRevision()
Get the left side of the diff.
Definition: DifferenceEngine.php:421
DifferenceEngine\getOldid
getOldid()
Get the ID of old revision (left pane) of the diff.
Definition: DifferenceEngine.php:397
WikiPage
Class representing a MediaWiki article and history.
Definition: WikiPage.php:53
Linker\revComment
static revComment( $rev, $local=false, $isPublic=false, $useParentheses=true)
Wrap and format the given revision's comment block, if the current user is allowed to view it.
Definition: Linker.php:1605
Linker\getRevDeleteLink
static getRevDeleteLink(User $user, $rev, LinkTarget $title)
Get a revision-deletion link, or disabled link, or nothing, depending on user permissions & the setti...
Definition: Linker.php:2195
DifferenceEngine\$mRevisionsLoaded
bool $mRevisionsLoaded
Have the revisions been loaded.
Definition: DifferenceEngine.php:153
DifferenceEngine\deletedIdMarker
deletedIdMarker( $id)
Build a wikitext link toward a deleted revision, if viewable.
Definition: DifferenceEngine.php:477
WikiPage\makeParserOptions
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
Definition: WikiPage.php:2001
wfHostname
wfHostname()
Get host name of the current machine, for use in error reporting.
Definition: GlobalFunctions.php:1283
DifferenceEngine\$isSlotDiffRenderer
bool $isSlotDiffRenderer
Temporary hack for B/C while slot diff related methods of DifferenceEngine are being deprecated.
Definition: DifferenceEngine.php:201
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1220
SpecialPage\getTitleFor
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
Definition: SpecialPage.php:92
DifferenceEngine\addLocalisedTitleTooltips
addLocalisedTitleTooltips( $text)
Add title attributes for tooltips on moved paragraph indicators.
Definition: DifferenceEngine.php:1624
DifferenceEngine\$mOldRevisionRecord
RevisionRecord null false $mOldRevisionRecord
Old revision (left pane).
Definition: DifferenceEngine.php:94
ContextSource\getRequest
getRequest()
Definition: ContextSource.php:76
PermissionsError
Show an error when a user tries to do something they do not have the necessary permissions for.
Definition: PermissionsError.php:31
DifferenceEngine\showDiffStyle
showDiffStyle()
Add style sheets for diff display.
Definition: DifferenceEngine.php:1129
DifferenceEngine\loadNewText
loadNewText()
Load the text of the new revision, not the old one.
Definition: DifferenceEngine.php:2172
ContextSource\getUser
getUser()
Stable to override.
Definition: ContextSource.php:131
DifferenceEngine\showDiff
showDiff( $otitle, $ntitle, $notice='')
Get the diff text, send it to the OutputPage object Returns false if the diff could not be generated,...
Definition: DifferenceEngine.php:1109
DifferenceEngine\getDiffBodyCacheKey
getDiffBodyCacheKey()
Returns the cache key for diff body text or content.
Definition: DifferenceEngine.php:1322
DifferenceEngine\localiseDiff
localiseDiff( $text)
Localise diff output.
Definition: DifferenceEngine.php:1585
DifferenceEngine\$mNewid
int string false null $mNewid
Revision ID for the new revision.
Definition: DifferenceEngine.php:82
DifferenceEngine\$contentHandlerFactory
IContentHandlerFactory $contentHandlerFactory
Definition: DifferenceEngine.php:217
DifferenceEngine\hasDeletedRevision
hasDeletedRevision()
Checks whether one of the given Revisions was deleted.
Definition: DifferenceEngine.php:514
DifferenceEngine\generateTextDiffBody
generateTextDiffBody( $otext, $ntext)
Generate a diff, no caching.
Definition: DifferenceEngine.php:1451
$dbr
$dbr
Definition: testCompression.php:54
ContextSource\getLanguage
getLanguage()
Definition: ContextSource.php:140
Revision
Definition: Revision.php:40
DifferenceEngine\$mReducedLineNumbers
bool $mReducedLineNumbers
If true, line X is not displayed when X is 1, for example to increase readability and conserve space ...
Definition: DifferenceEngine.php:181
NS_SPECIAL
const NS_SPECIAL
Definition: Defines.php:58
DifferenceEngine\revisionDeleteLink
revisionDeleteLink(RevisionRecord $revRecord)
Definition: DifferenceEngine.php:986
DifferenceEngine\loadRevisionData
loadRevisionData()
Load revision metadata for the specified revisions.
Definition: DifferenceEngine.php:2019
DifferenceEngine\localiseLineNumbersCb
localiseLineNumbersCb( $matches)
Definition: DifferenceEngine.php:1610
MWException
MediaWiki exception.
Definition: MWException.php:29
WikiPage\factory
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:157
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1027
DifferenceEngine\getDiffBodyForRole
getDiffBodyForRole( $role)
Get the diff table body for one slot, without header.
Definition: DifferenceEngine.php:1276
DifferenceEngine\$slotDiffRenderers
SlotDiffRenderer[] $slotDiffRenderers
DifferenceEngine classes for the slots, keyed by role name.
Definition: DifferenceEngine.php:193
Linker\generateRollback
static generateRollback( $rev, IContextSource $context=null, $options=[ 'verify'])
Generate a rollback link for a given revision.
Definition: Linker.php:1861
DifferenceEngine\wasCacheHit
wasCacheHit()
Definition: DifferenceEngine.php:386
DifferenceEngine\$mNewTags
string[] null $mNewTags
Change tags of new revision or null if it does not exist / is not saved.
Definition: DifferenceEngine.php:130
DifferenceEngine\getDiffLang
getDiffLang()
Get the language of the difference engine, defaults to page content language.
Definition: DifferenceEngine.php:374
DifferenceEngine\getRevisionHeader
getRevisionHeader( $rev, $complete='')
Get a header for a specified revision.
Definition: DifferenceEngine.php:1755
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2469
WikiPage\getParserOutput
getParserOutput(ParserOptions $parserOptions, $oldid=null, $forceParse=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1187
ContextSource\getOutput
getOutput()
Definition: ContextSource.php:121
$matches
$matches
Definition: NoLocalSettings.php:24
DifferenceEngine\getSlotHeader
getSlotHeader( $headerText)
Get a slot header for inclusion in a diff body (as a table row).
Definition: DifferenceEngine.php:1305
ContextSource
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
Definition: ContextSource.php:30
ContextSource\getWikiPage
getWikiPage()
Get the WikiPage object.
Definition: ContextSource.php:112
DifferenceEngine\getParserOutput
getParserOutput(WikiPage $page, RevisionRecord $revRecord)
Definition: DifferenceEngine.php:1085
DifferenceEngine\$mNewContent
Content null $mNewContent
Definition: DifferenceEngine.php:144
DifferenceEngine\deletedLink
deletedLink( $id)
Look up a special:Undelete link to the given deleted revision id, as a workaround for being unable to...
Definition: DifferenceEngine.php:442
ChangesList\flag
static flag( $flag, IContextSource $context=null)
Make an "<abbr>" element for a given change flag.
Definition: ChangesList.php:269
DifferenceEngine\getDiffBody
getDiffBody()
Get the diff table body, without header.
Definition: DifferenceEngine.php:1172
DifferenceEngine\$slotDiffOptions
array $slotDiffOptions
A set of options that will be passed to the SlotDiffRenderer upon creation.
Definition: DifferenceEngine.php:207
DifferenceEngine\getSlotDiffRenderers
getSlotDiffRenderers()
Definition: DifferenceEngine.php:276
$generator
$generator
Definition: generateLocalAutoload.php:13
$title
$title
Definition: testCompression.php:38
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:591
DifferenceEngine\setRevisions
setRevisions(?RevisionRecord $oldRevision, RevisionRecord $newRevision)
Use specified text instead of loading from the database.
Definition: DifferenceEngine.php:1901
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
DifferenceEngine\loadText
loadText()
Load the text of the revisions, as well as revision data.
Definition: DifferenceEngine.php:2129
Linker\revUserTools
static revUserTools( $rev, $isPublic=false, $useParentheses=true)
Generate a user tool link cluster if the current user is allowed to view it.
Definition: Linker.php:1142
$revStore
$revStore
Definition: testCompression.php:55
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:910
ContextSource\setContext
setContext(IContextSource $context)
Definition: ContextSource.php:58
DifferenceEngine\$mCacheHit
bool $mCacheHit
Was the diff fetched from cache?
Definition: DifferenceEngine.php:169
deprecatePublicProperty
deprecatePublicProperty( $property, $version, $class=null, $component=null)
Mark a property as deprecated.
Definition: DeprecationHelper.php:68
DifferenceEngine\markAsSlotDiffRenderer
markAsSlotDiffRenderer()
Mark this DifferenceEngine as a slot renderer (as opposed to a page renderer).
Definition: DifferenceEngine.php:307
DifferenceEngine\$enableDebugComment
$enableDebugComment
Set this to true to add debug info to the HTML output.
Definition: DifferenceEngine.php:176
Revision\RevisionRecord\getId
getId()
Get revision ID.
Definition: RevisionRecord.php:279
DifferenceEngine\$hookContainer
HookContainer $hookContainer
Definition: DifferenceEngine.php:228
DifferenceEngine\__construct
__construct( $context=null, $old=0, $new=0, $rcid=0, $refreshCache=false, $unhide=false)
#-
Definition: DifferenceEngine.php:240
ContextSource\msg
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition: ContextSource.php:184
DifferenceEngine\getDebugString
getDebugString()
Definition: DifferenceEngine.php:1568
Title\makeTitleSafe
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:617
DifferenceEngine\setTextLanguage
setTextLanguage(Language $lang)
Set the language in which the diff text is written.
Definition: DifferenceEngine.php:1934
$content
$content
Definition: router.php:76
DifferenceEngine\$mOldTags
string[] null $mOldTags
Change tags of old revision or null if it does not exist / is not saved.
Definition: DifferenceEngine.php:124
MediaWiki\Content\IContentHandlerFactory
Definition: IContentHandlerFactory.php:10
$header
$header
Definition: updateCredits.php:41
DifferenceEngine\$linkRenderer
LinkRenderer $linkRenderer
Definition: DifferenceEngine.php:212
DifferenceEngine\$hookRunner
HookRunner $hookRunner
Definition: DifferenceEngine.php:225
DifferenceEngine\getTitle
getTitle()
Definition: DifferenceEngine.php:354
DifferenceEngine\showDiffPage
showDiffPage( $diffOnly=false)
Definition: DifferenceEngine.php:604
Title\newFromLinkTarget
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
Definition: Title.php:280
DifferenceEngine\$mOldid
int false null $mOldid
Revision ID for the old revision.
Definition: DifferenceEngine.php:74
Linker\titleAttrib
static titleAttrib( $name, $options=null, array $msgParams=[])
Given the id of an interface element, constructs the appropriate title attribute from the system mess...
Definition: Linker.php:2110
DifferenceEngine\$isContentOverridden
bool $isContentOverridden
Was the content overridden via setContent()? If the content was overridden, most internal state (e....
Definition: DifferenceEngine.php:166
DifferenceEngine\DIFF_VERSION
const DIFF_VERSION
Constant to indicate diff cache compatibility.
Definition: DifferenceEngine.php:66
IContextSource
Interface for objects which can provide a MediaWiki context on request.
Definition: IContextSource.php:55
DifferenceEngine\intermediateEditsMsg
static intermediateEditsMsg( $numEdits, $numUsers, $limit)
Get a notice about how many intermediate edits and users there are.
Definition: DifferenceEngine.php:1715
DifferenceEngine\showMissingRevision
showMissingRevision()
Definition: DifferenceEngine.php:486
Content
Base interface for content objects.
Definition: Content.php:35
DifferenceEngine\loadRevisionIds
loadRevisionIds()
Load revision IDs.
Definition: DifferenceEngine.php:1985
DifferenceEngine\getNewRevision
getNewRevision()
Get the right side of the diff.
Definition: DifferenceEngine.php:430
Revision\RevisionRecord\getVisibility
getVisibility()
Get the deletion bitfield of the revision.
Definition: RevisionRecord.php:431
Revision\RevisionRecord\getPageAsLinkTarget
getPageAsLinkTarget()
Returns the title of the page this revision is associated with as a LinkTarget object.
Definition: RevisionRecord.php:351
Title
Represents a title within MediaWiki.
Definition: Title.php:41
SlotDiffRenderer
Renders a diff for a single slot (that is, a diff between two content objects).
Definition: SlotDiffRenderer.php:40
$cache
$cache
Definition: mcc.php:33
DifferenceEngine\$mDiffLang
Language $mDiffLang
Definition: DifferenceEngine.php:147
DifferenceEngine\getMarkPatrolledLinkInfo
getMarkPatrolledLinkInfo()
Returns an array of meta data needed to build a "mark as patrolled" link and adds a JS module to the ...
Definition: DifferenceEngine.php:926
DifferenceEngine\$mOldPage
Title null $mOldPage
Title of old revision or null if the old revision does not exist or does not belong to a page.
Definition: DifferenceEngine.php:111
RecentChange\PRC_UNPATROLLED
const PRC_UNPATROLLED
Definition: RecentChange.php:82
DifferenceEngine\getPermissionErrors
getPermissionErrors(User $user)
Get the permission errors associated with the revisions for the current diff.
Definition: DifferenceEngine.php:532
Html\openElement
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing '/'...
Definition: Html.php:251
DifferenceEngine\getExtraCacheKeys
getExtraCacheKeys()
Implements DifferenceEngineSlotDiffRenderer::getExtraCacheKeys().
Definition: DifferenceEngine.php:1373
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:209
DifferenceEngine\isUserAllowedToSeeRevisions
isUserAllowedToSeeRevisions( $user)
Checks whether the current user has permission for accessing the revisions of the diff.
Definition: DifferenceEngine.php:571
getTitle
getTitle()
Definition: RevisionSearchResultTrait.php:81
DifferenceEngine
DifferenceEngine is responsible for rendering the difference between two revisions as HTML.
Definition: DifferenceEngine.php:56
MediaWiki\Storage\NameTableAccessException
Exception representing a failure to look up a row from a name table.
Definition: NameTableAccessException.php:33
DifferenceEngineSlotDiffRenderer
B/C adapter for turning a DifferenceEngine into a SlotDiffRenderer.
Definition: DifferenceEngineSlotDiffRenderer.php:32
RecentChange\isInRCLifespan
static isInRCLifespan( $timestamp, $tolerance=0)
Check whether the given timestamp is new enough to have a RC row with a given tolerance as the recent...
Definition: RecentChange.php:1160
Revision\RevisionRecord\getContent
getContent( $role, $audience=self::FOR_PUBLIC, User $user=null)
Returns the Content of the given slot of this revision.
Definition: RevisionRecord.php:167
DifferenceEngine\$mMarkPatrolledLink
string $mMarkPatrolledLink
Link to action=markpatrolled.
Definition: DifferenceEngine.php:184
DifferenceEngine\localiseLineNumbers
localiseLineNumbers( $text)
Replace line numbers with the text in the user's language.
Definition: DifferenceEngine.php:1602
DifferenceEngine\hasSuppressedRevision
hasSuppressedRevision()
Checks whether one of the given Revisions was suppressed.
Definition: DifferenceEngine.php:551
DifferenceEngine\$mOldContent
Content null $mOldContent
Definition: DifferenceEngine.php:137
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:44
wfWarn
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
Definition: GlobalFunctions.php:1074
DifferenceEngine\userCanEdit
userCanEdit(RevisionRecord $revRecord)
Definition: DifferenceEngine.php:1732
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:570
DifferenceEngine\debug
debug( $generator="internal")
Generate a debug comment indicating diff generating time, server node, and generator backend.
Definition: DifferenceEngine.php:1553
DifferenceEngine\renderNewRevision
renderNewRevision()
Show the new revision of the page.
Definition: DifferenceEngine.php:1004
Html\element
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:231
CONTENT_MODEL_TEXT
const CONTENT_MODEL_TEXT
Definition: Defines.php:227
DifferenceEngine\getDiff
getDiff( $otitle, $ntitle, $notice='')
Get complete diff table, including header.
Definition: DifferenceEngine.php:1150
DifferenceEngine\setSlotDiffOptions
setSlotDiffOptions( $options)
Definition: DifferenceEngine.php:1406
DeprecationHelper
trait DeprecationHelper
Use this trait in classes which have properties for which public access is deprecated.
Definition: DeprecationHelper.php:45
DifferenceEngine\generateContentDiffBody
generateContentDiffBody(Content $old, Content $new)
Generate a diff, no caching.
Definition: DifferenceEngine.php:1423
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:55
DifferenceEngine\mapDiffPrevNext
mapDiffPrevNext( $old, $new)
Maps a revision pair definition as accepted by DifferenceEngine constructor to a pair of actual integ...
Definition: DifferenceEngine.php:1950
DifferenceEngine\addHeader
addHeader( $diff, $otitle, $ntitle, $multi='', $notice='')
Add the header to a diff body.
Definition: DifferenceEngine.php:1826
ChangeTags\formatSummaryRow
static formatSummaryRow( $tags, $page, IContextSource $context=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:114
Language
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
Definition: Language.php:41
DifferenceEngine\textDiff
textDiff( $otext, $ntext)
Generates diff, to be wrapped internally in a logging/instrumentation.
Definition: DifferenceEngine.php:1532
DifferenceEngine\$revisionStore
RevisionStore $revisionStore
Definition: DifferenceEngine.php:222
Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:39
DifferenceEngine\$mNewPage
Title null $mNewPage
Title of new revision or null if the new revision does not exist or does not belong to a page.
Definition: DifferenceEngine.php:118
TextSlotDiffRenderer
Renders a slot diff by doing a text diff on the native representation.
Definition: TextSlotDiffRenderer.php:38
DifferenceEngine\$mRefreshCache
bool $mRefreshCache
Refresh the diff cache.
Definition: DifferenceEngine.php:190
DifferenceEngine\getMultiNotice
getMultiNotice()
If there are revisions between the ones being compared, return a note saying so.
Definition: DifferenceEngine.php:1648