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