MediaWiki master
DifferenceEngine.php
Go to the documentation of this file.
1<?php
29use MediaWiki\Debug\DeprecationHelper;
58
82
83 use DeprecationHelper;
84
91 private const DIFF_VERSION = '1.41';
92
99 protected $mOldid;
100
107 protected $mNewid;
108
119 private $mOldRevisionRecord;
120
129 private $mNewRevisionRecord;
130
135 protected $mOldPage;
136
141 protected $mNewPage;
142
147 private $mOldTags;
148
153 private $mNewTags;
154
160 private $mOldContent;
161
167 private $mNewContent;
168
170 protected $mDiffLang;
171
173 private $mRevisionsIdsLoaded = false;
174
176 protected $mRevisionsLoaded = false;
177
179 protected $mTextLoaded = 0;
180
189 protected $isContentOverridden = false;
190
192 protected $mCacheHit = false;
193
195 private $cacheHitKey = null;
196
203 public $enableDebugComment = false;
204
208 protected $mReducedLineNumbers = false;
209
211 protected $mMarkPatrolledLink = null;
212
214 protected $unhide = false;
215
217 protected $mRefreshCache = false;
218
220 protected $slotDiffRenderers = null;
221
228 protected $isSlotDiffRenderer = false;
229
234 private $slotDiffOptions = [];
235
240 private $extraQueryParams = [];
241
243 private $textDiffer;
244
246 private IContentHandlerFactory $contentHandlerFactory;
247 private RevisionStore $revisionStore;
248 private ArchivedRevisionLookup $archivedRevisionLookup;
249 private HookRunner $hookRunner;
250 private WikiPageFactory $wikiPageFactory;
251 private UserOptionsLookup $userOptionsLookup;
252 private CommentFormatter $commentFormatter;
253 private IConnectionProvider $dbProvider;
254 private UserGroupManager $userGroupManager;
255 private UserEditTracker $userEditTracker;
256 private UserIdentityUtils $userIdentityUtils;
257
259 private $revisionLoadErrors = [];
260
269 public function __construct( $context = null, $old = 0, $new = 0, $rcid = 0,
270 $refreshCache = false, $unhide = false
271 ) {
272 if ( $context instanceof IContextSource ) {
273 $this->setContext( $context );
274 }
275
276 wfDebug( "DifferenceEngine old '$old' new '$new' rcid '$rcid'" );
277
278 $this->mOldid = $old;
279 $this->mNewid = $new;
280 $this->mRefreshCache = $refreshCache;
281 $this->unhide = $unhide;
282
283 $services = MediaWikiServices::getInstance();
284 $this->linkRenderer = $services->getLinkRenderer();
285 $this->contentHandlerFactory = $services->getContentHandlerFactory();
286 $this->revisionStore = $services->getRevisionStore();
287 $this->archivedRevisionLookup = $services->getArchivedRevisionLookup();
288 $this->hookRunner = new HookRunner( $services->getHookContainer() );
289 $this->wikiPageFactory = $services->getWikiPageFactory();
290 $this->userOptionsLookup = $services->getUserOptionsLookup();
291 $this->commentFormatter = $services->getCommentFormatter();
292 $this->dbProvider = $services->getConnectionProvider();
293 $this->userGroupManager = $services->getUserGroupManager();
294 $this->userEditTracker = $services->getUserEditTracker();
295 $this->userIdentityUtils = $services->getUserIdentityUtils();
296 }
297
303 protected function getSlotDiffRenderers() {
304 if ( $this->isSlotDiffRenderer ) {
305 throw new LogicException( __METHOD__ . ' called in slot diff renderer mode' );
306 }
307
308 if ( $this->slotDiffRenderers === null ) {
309 if ( !$this->loadRevisionData() ) {
310 return [];
311 }
312
313 $slotContents = $this->getSlotContents();
314 $this->slotDiffRenderers = [];
315 foreach ( $slotContents as $role => $contents ) {
316 if ( $contents['new'] && $contents['old']
317 && $contents['new']->equals( $contents['old'] )
318 ) {
319 // Do not produce a diff of identical content
320 continue;
321 }
322 $handler = ( $contents['new'] ?: $contents['old'] )->getContentHandler();
323 $this->slotDiffRenderers[$role] = $handler->getSlotDiffRenderer(
324 $this->getContext(),
325 $this->slotDiffOptions + [
326 'contentLanguage' => $this->getDiffLang()->getCode(),
327 'textDiffer' => $this->getTextDiffer()
328 ]
329 );
330 }
331 }
332
333 return $this->slotDiffRenderers;
334 }
335
342 public function markAsSlotDiffRenderer() {
343 $this->isSlotDiffRenderer = true;
344 }
345
351 protected function getSlotContents() {
352 if ( $this->isContentOverridden ) {
353 return [
354 SlotRecord::MAIN => [ 'old' => $this->mOldContent, 'new' => $this->mNewContent ]
355 ];
356 } elseif ( !$this->loadRevisionData() ) {
357 return [];
358 }
359
360 $newSlots = $this->mNewRevisionRecord->getPrimarySlots()->getSlots();
361 $oldSlots = $this->mOldRevisionRecord ?
362 $this->mOldRevisionRecord->getPrimarySlots()->getSlots() :
363 [];
364 // The order here will determine the visual order of the diff. The current logic is
365 // slots of the new revision first in natural order, then deleted ones. This is ad hoc
366 // and should not be relied on - in the future we may want the ordering to depend
367 // on the page type.
368 $roles = array_keys( array_merge( $newSlots, $oldSlots ) );
369
370 $slots = [];
371 foreach ( $roles as $role ) {
372 $slots[$role] = [
373 'old' => $this->loadSingleSlot(
374 $oldSlots[$role] ?? null,
375 'old'
376 ),
377 'new' => $this->loadSingleSlot(
378 $newSlots[$role] ?? null,
379 'new'
380 )
381 ];
382 }
383 // move main slot to front
384 if ( isset( $slots[SlotRecord::MAIN] ) ) {
385 $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
386 }
387 return $slots;
388 }
389
397 private function loadSingleSlot( ?SlotRecord $slot, string $which ) {
398 if ( !$slot ) {
399 return null;
400 }
401 try {
402 return $slot->getContent();
403 } catch ( BadRevisionException $e ) {
404 $this->addRevisionLoadError( $which );
405 return null;
406 }
407 }
408
414 private function addRevisionLoadError( $which ) {
415 $this->revisionLoadErrors[] = $this->msg( $which === 'new'
416 ? 'difference-bad-new-revision' : 'difference-bad-old-revision'
417 );
418 }
419
426 public function getRevisionLoadErrors() {
427 return $this->revisionLoadErrors;
428 }
429
434 private function hasNewRevisionLoadError() {
435 foreach ( $this->revisionLoadErrors as $error ) {
436 if ( $error->getKey() === 'difference-bad-new-revision' ) {
437 return true;
438 }
439 }
440 return false;
441 }
442
444 public function getTitle() {
445 // T202454 avoid errors when there is no title
446 return parent::getTitle() ?: Title::makeTitle( NS_SPECIAL, 'BadTitle/DifferenceEngine' );
447 }
448
455 public function setReducedLineNumbers( $value = true ) {
456 $this->mReducedLineNumbers = $value;
457 }
458
464 public function getDiffLang() {
465 # Default language in which the diff text is written.
466 $this->mDiffLang ??= $this->getDefaultLanguage();
467 return $this->mDiffLang;
468 }
469
476 protected function getDefaultLanguage() {
477 return $this->getTitle()->getPageLanguage();
478 }
479
483 public function wasCacheHit() {
484 return $this->mCacheHit;
485 }
486
494 public function getOldid() {
495 $this->loadRevisionIds();
496
497 return $this->mOldid;
498 }
499
506 public function getNewid() {
507 $this->loadRevisionIds();
508
509 return $this->mNewid;
510 }
511
518 public function getOldRevision() {
519 return $this->mOldRevisionRecord ?: null;
520 }
521
527 public function getNewRevision() {
528 return $this->mNewRevisionRecord;
529 }
530
539 public function deletedLink( $id ) {
540 if ( $this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
541 $revRecord = $this->archivedRevisionLookup->getArchivedRevisionRecord( null, $id );
542 if ( $revRecord ) {
543 $title = Title::newFromPageIdentity( $revRecord->getPage() );
544
545 return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( [
546 'target' => $title->getPrefixedText(),
547 'timestamp' => $revRecord->getTimestamp()
548 ] );
549 }
550 }
551
552 return false;
553 }
554
562 public function deletedIdMarker( $id ) {
563 $link = $this->deletedLink( $id );
564 if ( $link ) {
565 return "[$link $id]";
566 } else {
567 return (string)$id;
568 }
569 }
570
571 private function showMissingRevision() {
572 $out = $this->getOutput();
573
574 $missing = [];
575 if ( $this->mOldid && ( !$this->mOldRevisionRecord || !$this->mOldContent ) ) {
576 $missing[] = $this->deletedIdMarker( $this->mOldid );
577 }
578 if ( !$this->mNewRevisionRecord || !$this->mNewContent ) {
579 $missing[] = $this->deletedIdMarker( $this->mNewid );
580 }
581
582 $out->setPageTitleMsg( $this->msg( 'errorpagetitle' ) );
583 $msg = $this->msg( 'difference-missing-revision' )
584 ->params( $this->getLanguage()->listToText( $missing ) )
585 ->numParams( count( $missing ) )
586 ->parseAsBlock();
587 $out->addHTML( $msg );
588 }
589
595 public function hasDeletedRevision() {
596 $this->loadRevisionData();
597 return (
598 $this->mNewRevisionRecord &&
599 $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
600 ) ||
601 (
602 $this->mOldRevisionRecord &&
603 $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
604 );
605 }
606
613 public function getPermissionErrors( Authority $performer ) {
614 $this->loadRevisionData();
615 $permStatus = PermissionStatus::newEmpty();
616 if ( $this->mNewPage ) {
617 $performer->authorizeRead( 'read', $this->mNewPage, $permStatus );
618 }
619 if ( $this->mOldPage ) {
620 $performer->authorizeRead( 'read', $this->mOldPage, $permStatus );
621 }
622 return $permStatus->toLegacyErrorArray();
623 }
624
630 public function hasSuppressedRevision() {
631 return $this->hasDeletedRevision() && (
632 ( $this->mOldRevisionRecord &&
633 $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) ||
634 ( $this->mNewRevisionRecord &&
635 $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) )
636 );
637 }
638
645 private function getUserEditCount( $user ): string {
646 $editCount = $this->userEditTracker->getUserEditCount( $user );
647 if ( $editCount === null ) {
648 return '';
649 }
650
651 return Html::rawElement( 'div', [
652 'class' => 'mw-diff-usereditcount',
653 ],
654 $this->msg(
655 'diff-user-edits',
656 $this->getLanguage()->formatNum( $editCount )
657 )->parse()
658 );
659 }
660
667 private function getUserRoles( UserIdentity $user ) {
668 if ( !$this->userIdentityUtils->isNamed( $user ) ) {
669 return '';
670 }
671 $userGroups = $this->userGroupManager->getUserGroups( $user );
672 $userGroupLinks = [];
673 foreach ( $userGroups as $group ) {
674 $userGroupLinks[] = UserGroupMembership::getLinkHTML( $group, $this->getContext() );
675 }
676 return Html::rawElement( 'div', [
677 'class' => 'mw-diff-userroles',
678 ], $this->getLanguage()->commaList( $userGroupLinks ) );
679 }
680
687 private function getUserMetaData( ?UserIdentity $user ) {
688 if ( !$user ) {
689 return '';
690 }
691 return Html::rawElement( 'div', [
692 'class' => 'mw-diff-usermetadata',
693 ], $this->getUserRoles( $user ) . $this->getUserEditCount( $user ) );
694 }
695
707 public function isUserAllowedToSeeRevisions( Authority $performer ) {
708 $this->loadRevisionData();
709
710 if ( $this->mOldRevisionRecord && !$this->mOldRevisionRecord->userCan(
711 RevisionRecord::DELETED_TEXT,
712 $performer
713 ) ) {
714 return false;
715 }
716
717 // $this->mNewRev will only be falsy if a loading error occurred
718 // (in which case the user is allowed to see).
719 return !$this->mNewRevisionRecord || $this->mNewRevisionRecord->userCan(
720 RevisionRecord::DELETED_TEXT,
721 $performer
722 );
723 }
724
732 public function shouldBeHiddenFromUser( Authority $performer ) {
733 return $this->hasDeletedRevision() && ( !$this->unhide ||
734 !$this->isUserAllowedToSeeRevisions( $performer ) );
735 }
736
740 public function showDiffPage( $diffOnly = false ) {
741 # Allow frames except in certain special cases
742 $out = $this->getOutput();
743 $out->getMetadata()->setPreventClickjacking( false );
744 $out->setRobotPolicy( 'noindex,nofollow' );
745
746 // Allow extensions to add any extra output here
747 $this->hookRunner->onDifferenceEngineShowDiffPage( $out );
748
749 if ( !$this->loadRevisionData() ) {
750 if ( $this->hookRunner->onDifferenceEngineShowDiffPageMaybeShowMissingRevision( $this ) ) {
751 $this->showMissingRevision();
752 }
753 return;
754 }
755
756 $user = $this->getUser();
757 $permErrors = $this->getPermissionErrors( $this->getAuthority() );
758 if ( $permErrors ) {
759 throw new PermissionsError( 'read', $permErrors );
760 }
761
762 $rollback = '';
763
764 $query = $this->extraQueryParams;
765 # Carry over 'diffonly' param via navigation links
766 if ( $diffOnly != MediaWikiServices::getInstance()
767 ->getUserOptionsLookup()->getBoolOption( $user, 'diffonly' )
768 ) {
769 $query['diffonly'] = $diffOnly;
770 }
771 # Cascade unhide param in links for easy deletion browsing
772 if ( $this->unhide ) {
773 $query['unhide'] = 1;
774 }
775
776 # Check if one of the revisions is deleted/suppressed
777 $deleted = $this->hasDeletedRevision();
778 $suppressed = $this->hasSuppressedRevision();
779 $allowed = $this->isUserAllowedToSeeRevisions( $this->getAuthority() );
780
781 $revisionTools = [];
782 $breadCrumbs = '';
783
784 # mOldRevisionRecord is false if the difference engine is called with a "vague" query for
785 # a diff between a version V and its previous version V' AND the version V
786 # is the first version of that article. In that case, V' does not exist.
787 if ( $this->mOldRevisionRecord === false ) {
788 if ( $this->mNewPage ) {
789 $out->setPageTitleMsg(
790 $this->msg( 'difference-title' )->plaintextParams( $this->mNewPage->getPrefixedText() )
791 );
792 }
793 $samePage = true;
794 $oldHeader = '';
795 // Allow extensions to change the $oldHeader variable
796 $this->hookRunner->onDifferenceEngineOldHeaderNoOldRev( $oldHeader );
797 } else {
798 $this->hookRunner->onDifferenceEngineViewHeader( $this );
799
800 if ( !$this->mOldPage || !$this->mNewPage ) {
801 // XXX say something to the user?
802 $samePage = false;
803 } elseif ( $this->mNewPage->equals( $this->mOldPage ) ) {
804 $out->setPageTitleMsg(
805 $this->msg( 'difference-title' )->plaintextParams( $this->mNewPage->getPrefixedText() )
806 );
807 $samePage = true;
808 } else {
809 $out->setPageTitleMsg( $this->msg( 'difference-title-multipage' )->plaintextParams(
810 $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) );
811 $out->addSubtitle( $this->msg( 'difference-multipage' ) );
812 $samePage = false;
813 }
814
815 if ( $samePage && $this->mNewPage &&
816 $this->getAuthority()->probablyCan( 'edit', $this->mNewPage )
817 ) {
818 if ( $this->mNewRevisionRecord->isCurrent() &&
819 $this->getAuthority()->probablyCan( 'rollback', $this->mNewPage )
820 ) {
821 $rollbackLink = Linker::generateRollback(
822 $this->mNewRevisionRecord,
823 $this->getContext(),
824 [ 'noBrackets' ]
825 );
826 if ( $rollbackLink ) {
827 $out->getMetadata()->setPreventClickjacking( true );
828 $rollback = "\u{00A0}\u{00A0}\u{00A0}" . $rollbackLink;
829 }
830 }
831
832 if ( $this->userCanEdit( $this->mOldRevisionRecord ) &&
833 $this->userCanEdit( $this->mNewRevisionRecord )
834 ) {
835 $undoLink = $this->linkRenderer->makeKnownLink(
836 $this->mNewPage,
837 $this->msg( 'editundo' )->text(),
838 [ 'title' => Linker::titleAttrib( 'undo' ) ],
839 [
840 'action' => 'edit',
841 'undoafter' => $this->mOldid,
842 'undo' => $this->mNewid
843 ]
844 );
845 $revisionTools['mw-diff-undo'] = $undoLink;
846 }
847 }
848 # Make "previous revision link"
849 $hasPrevious = $samePage && $this->mOldPage &&
850 $this->revisionStore->getPreviousRevision( $this->mOldRevisionRecord );
851 if ( $hasPrevious ) {
852 $prevlinkQuery = [ 'diff' => 'prev', 'oldid' => $this->mOldid ] + $query;
853 $prevlink = $this->linkRenderer->makeKnownLink(
854 $this->mOldPage,
855 $this->msg( 'previousdiff' )->text(),
856 [ 'id' => 'differences-prevlink' ],
857 $prevlinkQuery
858 );
859 $breadCrumbs .= $this->linkRenderer->makeKnownLink(
860 $this->mOldPage,
861 $this->msg( 'previousdiff' )->text(),
862 [
863 'class' => 'mw-diff-revision-history-link-previous'
864 ],
865 $prevlinkQuery
866 );
867 } else {
868 $prevlink = "\u{00A0}";
869 }
870
871 if ( $this->mOldRevisionRecord->isMinor() ) {
872 $oldminor = ChangesList::flag( 'minor' );
873 } else {
874 $oldminor = '';
875 }
876
877 $oldRevRecord = $this->mOldRevisionRecord;
878
879 $ldel = $this->revisionDeleteLink( $oldRevRecord );
880 $oldRevisionHeader = $this->getRevisionHeader( $oldRevRecord, 'complete' );
881 $oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff', $this->getContext() );
882 $oldRevComment = $this->commentFormatter
883 ->formatRevision(
884 $oldRevRecord, $user, !$diffOnly, !$this->unhide, false
885 );
886
887 if ( $oldRevComment === '' ) {
888 $defaultComment = $this->msg( 'changeslist-nocomment' )->escaped();
889 $oldRevComment = "<span class=\"comment mw-comment-none\">$defaultComment</span>";
890 }
891
892 $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' .
893 '<div id="mw-diff-otitle2">' .
894 Linker::revUserTools( $oldRevRecord, !$this->unhide ) .
895 $this->getUserMetaData( $oldRevRecord->getUser() ) .
896 '</div>' .
897 '<div id="mw-diff-otitle3">' . $oldminor . $oldRevComment . $ldel . '</div>' .
898 '<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' .
899 '<div id="mw-diff-otitle4">' . $prevlink . '</div>';
900
901 // Allow extensions to change the $oldHeader variable
902 $this->hookRunner->onDifferenceEngineOldHeader(
903 $this, $oldHeader, $prevlink, $oldminor, $diffOnly, $ldel, $this->unhide );
904 }
905
906 $out->addJsConfigVars( [
907 'wgDiffOldId' => $this->mOldid,
908 'wgDiffNewId' => $this->mNewid,
909 ] );
910
911 # Make "next revision link"
912 # Skip next link on the top revision
913 if ( $samePage && $this->mNewPage && !$this->mNewRevisionRecord->isCurrent() ) {
914 $nextlinkQuery = [ 'diff' => 'next', 'oldid' => $this->mNewid ] + $query;
915 $nextlink = $this->linkRenderer->makeKnownLink(
916 $this->mNewPage,
917 $this->msg( 'nextdiff' )->text(),
918 [ 'id' => 'differences-nextlink' ],
919 $nextlinkQuery
920 );
921 $breadCrumbs .= $this->linkRenderer->makeKnownLink(
922 $this->mNewPage,
923 $this->msg( 'nextdiff' )->text(),
924 [
925 'class' => 'mw-diff-revision-history-link-next'
926 ],
927 $nextlinkQuery
928 );
929 } else {
930 $nextlink = "\u{00A0}";
931 }
932
933 if ( $this->mNewRevisionRecord->isMinor() ) {
934 $newminor = ChangesList::flag( 'minor' );
935 } else {
936 $newminor = '';
937 }
938
939 # Handle RevisionDelete links...
940 $rdel = $this->revisionDeleteLink( $this->mNewRevisionRecord );
941
942 # Allow extensions to define their own revision tools
943 $this->hookRunner->onDiffTools(
944 $this->mNewRevisionRecord,
945 $revisionTools,
946 $this->mOldRevisionRecord ?: null,
947 $user
948 );
949
950 $formattedRevisionTools = [];
951 // Put each one in parentheses (poor man's button)
952 foreach ( $revisionTools as $key => $tool ) {
953 $toolClass = is_string( $key ) ? $key : 'mw-diff-tool';
954 $element = Html::rawElement(
955 'span',
956 [ 'class' => $toolClass ],
957 $tool
958 );
959 $formattedRevisionTools[] = $element;
960 }
961
962 $newRevRecord = $this->mNewRevisionRecord;
963
964 $newRevisionHeader = $this->getRevisionHeader( $newRevRecord, 'complete' ) .
965 ' ' . implode( ' ', $formattedRevisionTools );
966 $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff', $this->getContext() );
967 $newRevComment = $this->commentFormatter->formatRevision(
968 $newRevRecord, $user, !$diffOnly, !$this->unhide, false
969 );
970
971 if ( $newRevComment === '' ) {
972 $defaultComment = $this->msg( 'changeslist-nocomment' )->escaped();
973 $newRevComment = "<span class=\"comment mw-comment-none\">$defaultComment</span>";
974 }
975
976 $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' .
977 '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $newRevRecord, !$this->unhide ) .
978 $rollback .
979 $this->getUserMetaData( $newRevRecord->getUser() ) .
980 '</div>' .
981 '<div id="mw-diff-ntitle3">' . $newminor . $newRevComment . $rdel . '</div>' .
982 '<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' .
983 '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
984
985 // Allow extensions to change the $newHeader variable
986 $this->hookRunner->onDifferenceEngineNewHeader( $this, $newHeader,
987 $formattedRevisionTools, $nextlink, $rollback, $newminor, $diffOnly,
988 $rdel, $this->unhide );
989
990 $out->addHTML(
991 Html::rawElement( 'div', [
992 'class' => 'mw-diff-revision-history-links'
993 ], $breadCrumbs )
994 );
995 $addMessageBoxStyles = false;
996 # If the diff cannot be shown due to a deleted revision, then output
997 # the diff header and links to unhide (if available)...
998 if ( $this->shouldBeHiddenFromUser( $this->getAuthority() ) ) {
999 $this->showDiffStyle();
1000 $multi = $this->getMultiNotice();
1001 $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
1002 if ( !$allowed ) {
1003 # Give explanation for why revision is not visible
1004 $msg = [ $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff' ];
1005 } else {
1006 # Give explanation and add a link to view the diff...
1007 $query = $this->getRequest()->appendQueryValue( 'unhide', '1' );
1008 $msg = [
1009 $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff',
1010 $this->getTitle()->getFullURL( $query )
1011 ];
1012 }
1013 $out->addHTML( Html::warningBox( $this->msg( ...$msg )->parse(), 'plainlinks' ) );
1014 $addMessageBoxStyles = true;
1015 # Otherwise, output a regular diff...
1016 } else {
1017 # Add deletion notice if the user is viewing deleted content
1018 $notice = '';
1019 if ( $deleted ) {
1020 $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
1021 $notice = Html::warningBox( $this->msg( $msg )->parse(), 'plainlinks' );
1022 $addMessageBoxStyles = true;
1023 }
1024
1025 # Add an error if the content can't be loaded
1026 $this->getSlotContents();
1027 foreach ( $this->getRevisionLoadErrors() as $msg ) {
1028 $notice .= Html::warningBox( $msg->parse() );
1029 $addMessageBoxStyles = true;
1030 }
1031
1032 // Check if inline switcher will be needed
1033 if ( $this->getTextDiffer()->hasFormat( 'inline' ) ) {
1034 $out->enableOOUI();
1035 }
1036
1037 $this->showTablePrefixes();
1038 $this->showDiff( $oldHeader, $newHeader, $notice );
1039 if ( !$diffOnly ) {
1040 $this->renderNewRevision();
1041 }
1042
1043 // Allow extensions to optionally not show the final patrolled link
1044 if ( $this->hookRunner->onDifferenceEngineRenderRevisionShowFinalPatrolLink() ) {
1045 # Add redundant patrol link on bottom...
1046 $out->addHTML( $this->markPatrolledLink() );
1047 }
1048 }
1049 if ( $addMessageBoxStyles ) {
1050 $out->addModuleStyles( 'mediawiki.codex.messagebox.styles' );
1051 }
1052 }
1053
1057 private function showTablePrefixes() {
1058 $parts = [];
1059 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1060 $parts += $slotDiffRenderer->getTablePrefix( $this->getContext(), $this->mNewPage );
1061 }
1062 ksort( $parts );
1063 if ( count( array_filter( $parts ) ) > 0 ) {
1064 $language = $this->getLanguage();
1065 $attrs = [
1066 'class' => 'mw-diff-table-prefix',
1067 'dir' => $language->getDir(),
1068 'lang' => $language->getCode(),
1069 ];
1070 $this->getOutput()->addHTML(
1071 Html::rawElement( 'div', $attrs, implode( '', $parts ) ) );
1072 }
1073 }
1074
1086 public function markPatrolledLink() {
1087 if ( $this->mMarkPatrolledLink === null ) {
1088 $linkInfo = $this->getMarkPatrolledLinkInfo();
1089 // If false, there is no patrol link needed/allowed
1090 if ( !$linkInfo || !$this->mNewPage ) {
1091 $this->mMarkPatrolledLink = '';
1092 } else {
1093 $patrolLinkClass = 'patrollink';
1094 $this->mMarkPatrolledLink = ' <span class="' . $patrolLinkClass . '" data-mw="interface">[' .
1095 $this->linkRenderer->makeKnownLink(
1096 $this->mNewPage,
1097 $this->msg( 'markaspatrolleddiff' )->text(),
1098 [],
1099 [
1100 'action' => 'markpatrolled',
1101 'rcid' => $linkInfo['rcid'],
1102 ]
1103 ) . ']</span>';
1104 // Allow extensions to change the markpatrolled link
1105 $this->hookRunner->onDifferenceEngineMarkPatrolledLink( $this,
1106 $this->mMarkPatrolledLink, $linkInfo['rcid'] );
1107 }
1108 }
1109 return $this->mMarkPatrolledLink;
1110 }
1111
1119 protected function getMarkPatrolledLinkInfo() {
1120 $user = $this->getUser();
1121 $config = $this->getConfig();
1122
1123 // Prepare a change patrol link, if applicable
1124 if (
1125 // Is patrolling enabled and the user allowed to?
1126 $config->get( MainConfigNames::UseRCPatrol ) &&
1127 $this->mNewPage &&
1128 $this->getAuthority()->probablyCan( 'patrol', $this->mNewPage ) &&
1129 // Only do this if the revision isn't more than 6 hours older
1130 // than the Max RC age (6h because the RC might not be cleaned out regularly)
1131 RecentChange::isInRCLifespan( $this->mNewRevisionRecord->getTimestamp(), 21600 )
1132 ) {
1133 // Look for an unpatrolled change corresponding to this diff
1134 $change = RecentChange::newFromConds(
1135 [
1136 'rc_this_oldid' => $this->mNewid,
1137 'rc_patrolled' => RecentChange::PRC_UNPATROLLED
1138 ],
1139 __METHOD__
1140 );
1141
1142 if ( $change && !$change->getPerformerIdentity()->equals( $user ) ) {
1143 $rcid = $change->getAttribute( 'rc_id' );
1144 } else {
1145 // None found or the page has been created by the current user.
1146 // If the user could patrol this it already would be patrolled
1147 $rcid = 0;
1148 }
1149
1150 // Allow extensions to possibly change the rcid here
1151 // For example the rcid might be set to zero due to the user
1152 // being the same as the performer of the change but an extension
1153 // might still want to show it under certain conditions
1154 $this->hookRunner->onDifferenceEngineMarkPatrolledRCID( $rcid, $this, $change, $user );
1155
1156 // Build the link
1157 if ( $rcid ) {
1158 $this->getOutput()->getMetadata()->setPreventClickjacking( true );
1159 $this->getOutput()->addModules( 'mediawiki.misc-authed-curate' );
1160
1161 return [ 'rcid' => $rcid ];
1162 }
1163 }
1164
1165 // No mark as patrolled link applicable
1166 return false;
1167 }
1168
1174 private function revisionDeleteLink( RevisionRecord $revRecord ) {
1175 $link = Linker::getRevDeleteLink(
1176 $this->getAuthority(),
1177 $revRecord,
1178 $revRecord->getPageAsLinkTarget()
1179 );
1180 if ( $link !== '' ) {
1181 $link = "\u{00A0}\u{00A0}\u{00A0}" . $link . ' ';
1182 }
1183
1184 return $link;
1185 }
1186
1192 public function renderNewRevision() {
1193 if ( $this->isContentOverridden ) {
1194 // The code below only works with a RevisionRecord object. We could construct a
1195 // fake RevisionRecord (here or in setContent), but since this does not seem
1196 // needed at the moment, we'll just fail for now.
1197 throw new LogicException(
1198 __METHOD__
1199 . ' is not supported after calling setContent(). Use setRevisions() instead.'
1200 );
1201 }
1202
1203 $out = $this->getOutput();
1204 $revHeader = $this->getRevisionHeader( $this->mNewRevisionRecord );
1205 # Add "current version as of X" title
1206 $out->addHTML( "<hr class='diff-hr' id='mw-oldid' />
1207 <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
1208 # Page content may be handled by a hooked call instead...
1209 if ( $this->hookRunner->onArticleContentOnDiff( $this, $out ) ) {
1210 $this->loadNewText();
1211 if ( !$this->mNewPage ) {
1212 // New revision is unsaved; bail out.
1213 // TODO in theory rendering the new revision is a meaningful thing to do
1214 // even if it's unsaved, but a lot of untangling is required to do it safely.
1215 return;
1216 }
1217 if ( $this->hasNewRevisionLoadError() ) {
1218 // There was an error loading the new revision
1219 return;
1220 }
1221
1222 $out->setRevisionId( $this->mNewid );
1223 $out->setRevisionIsCurrent( $this->mNewRevisionRecord->isCurrent() );
1224 $out->setRevisionTimestamp( $this->mNewRevisionRecord->getTimestamp() );
1225 $out->setArticleFlag( true );
1226
1227 if ( !$this->hookRunner->onArticleRevisionViewCustom(
1228 $this->mNewRevisionRecord, $this->mNewPage, $this->mOldid, $out )
1229 ) {
1230 // Handled by extension
1231 // NOTE: sync with hooks called in Article::view()
1232 } else {
1233 // Normal page
1234 if ( $this->getTitle()->equals( $this->mNewPage ) ) {
1235 // If the Title stored in the context is the same as the one
1236 // of the new revision, we can use its associated WikiPage
1237 // object.
1238 $wikiPage = $this->getWikiPage();
1239 } else {
1240 // Otherwise we need to create our own WikiPage object
1241 $wikiPage = $this->wikiPageFactory->newFromTitle( $this->mNewPage );
1242 }
1243
1244 $parserOptions = $wikiPage->makeParserOptions( $this->getContext() );
1245 $parserOptions->setRenderReason( 'diff-page' );
1246
1247 $parserOutputAccess = MediaWikiServices::getInstance()->getParserOutputAccess();
1248 $status = $parserOutputAccess->getParserOutput(
1249 $wikiPage,
1250 $parserOptions,
1251 $this->mNewRevisionRecord,
1252 // we already checked
1253 ParserOutputAccess::OPT_NO_AUDIENCE_CHECK |
1254 // Update cascading protection
1255 ParserOutputAccess::OPT_LINKS_UPDATE
1256 );
1257 if ( $status->isOK() ) {
1258 $parserOutput = $status->getValue();
1259 // Allow extensions to change parser output here
1260 if ( $this->hookRunner->onDifferenceEngineRenderRevisionAddParserOutput(
1261 $this, $out, $parserOutput, $wikiPage )
1262 ) {
1263 $out->addParserOutput( $parserOutput, [
1264 'enableSectionEditLinks' => $this->mNewRevisionRecord->isCurrent()
1265 && $this->getAuthority()->probablyCan(
1266 'edit',
1267 $this->mNewRevisionRecord->getPage()
1268 ),
1269 'absoluteURLs' => $this->slotDiffOptions['expand-url'] ?? false
1270 ] );
1271 }
1272 } else {
1273 $out->addModuleStyles( 'mediawiki.codex.messagebox.styles' );
1274 foreach ( $status->getMessages() as $msg ) {
1275 $out->addHTML( Html::errorBox(
1276 $this->msg( $msg )->parse()
1277 ) );
1278 }
1279 }
1280 }
1281 }
1282 }
1283
1294 public function showDiff( $otitle, $ntitle, $notice = '' ) {
1295 // Allow extensions to affect the output here
1296 $this->hookRunner->onDifferenceEngineShowDiff( $this );
1297
1298 $diff = $this->getDiff( $otitle, $ntitle, $notice );
1299 if ( $diff === false ) {
1300 $this->showMissingRevision();
1301 return false;
1302 }
1303
1304 $this->showDiffStyle();
1305 if ( $this->slotDiffOptions['expand-url'] ?? false ) {
1306 $diff = Linker::expandLocalLinks( $diff );
1307 }
1308 $this->getOutput()->addHTML( $diff );
1309 return true;
1310 }
1311
1315 public function showDiffStyle() {
1316 if ( !$this->isSlotDiffRenderer ) {
1317 $this->getOutput()->addModules( 'mediawiki.diff' );
1318 $this->getOutput()->addModuleStyles( [
1319 'mediawiki.interface.helpers.styles',
1320 'mediawiki.diff.styles'
1321 ] );
1322 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1323 $slotDiffRenderer->addModules( $this->getOutput() );
1324 }
1325 }
1326 }
1327
1337 public function getDiff( $otitle, $ntitle, $notice = '' ) {
1338 $body = $this->getDiffBody();
1339 if ( $body === false ) {
1340 return false;
1341 }
1342
1343 $multi = $this->getMultiNotice();
1344 // Display a message when the diff is empty
1345 if ( $body === '' ) {
1346 $notice .= '<div class="mw-diff-empty">' .
1347 $this->msg( 'diff-empty' )->parse() .
1348 "</div>\n";
1349 }
1350
1351 if ( $this->cacheHitKey !== null ) {
1352 $body .= "\n<!-- diff cache key " . htmlspecialchars( $this->cacheHitKey ) . " -->\n";
1353 }
1354
1355 return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
1356 }
1357
1358 private function incrementStats( string $cacheStatus ): void {
1359 $stats = MediaWikiServices::getInstance()->getStatsFactory();
1360 $stats->getCounter( 'diff_cache_total' )
1361 ->setLabel( 'status', $cacheStatus )
1362 ->copyToStatsdAt( 'diff_cache.' . $cacheStatus )
1363 ->increment();
1364 }
1365
1371 public function getDiffBody() {
1372 $this->mCacheHit = true;
1373 // Check if the diff should be hidden from this user
1374 if ( !$this->isContentOverridden ) {
1375 if ( !$this->loadRevisionData() ) {
1376 return false;
1377 } elseif ( $this->mOldRevisionRecord &&
1378 !$this->mOldRevisionRecord->userCan(
1379 RevisionRecord::DELETED_TEXT,
1380 $this->getAuthority()
1381 )
1382 ) {
1383 return false;
1384 } elseif ( $this->mNewRevisionRecord &&
1385 !$this->mNewRevisionRecord->userCan(
1386 RevisionRecord::DELETED_TEXT,
1387 $this->getAuthority()
1388 ) ) {
1389 return false;
1390 }
1391 // Short-circuit
1392 if ( $this->mOldRevisionRecord === false || (
1393 $this->mOldRevisionRecord &&
1394 $this->mNewRevisionRecord &&
1395 $this->mOldRevisionRecord->getId() &&
1396 $this->mOldRevisionRecord->getId() == $this->mNewRevisionRecord->getId()
1397 ) ) {
1398 if ( $this->hookRunner->onDifferenceEngineShowEmptyOldContent( $this ) ) {
1399 return '';
1400 }
1401 }
1402 }
1403
1404 // Cacheable?
1405 $key = false;
1406 $services = MediaWikiServices::getInstance();
1407 $cache = $services->getMainWANObjectCache();
1408 $stats = $services->getStatsdDataFactory();
1409 if ( $this->mOldid && $this->mNewid ) {
1410 $key = $cache->makeKey( ...$this->getDiffBodyCacheKeyParams() );
1411
1412 // Try cache
1413 if ( !$this->mRefreshCache ) {
1414 $difftext = $cache->get( $key );
1415 if ( is_string( $difftext ) ) {
1416 $this->incrementStats( 'hit' );
1417 $difftext = $this->localiseDiff( $difftext );
1418 $this->cacheHitKey = $key;
1419 return $difftext;
1420 }
1421 } // don't try to load but save the result
1422 }
1423 $this->mCacheHit = false;
1424 $this->cacheHitKey = null;
1425
1426 // Loadtext is permission safe, this just clears out the diff
1427 if ( !$this->loadText() ) {
1428 return false;
1429 }
1430
1431 $difftext = '';
1432 // We've checked for revdelete at the beginning of this method; it's OK to ignore
1433 // read permissions here.
1434 $slotContents = $this->getSlotContents();
1435 foreach ( $this->getSlotDiffRenderers() as $role => $slotDiffRenderer ) {
1436 try {
1437 $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role]['old'],
1438 $slotContents[$role]['new'] );
1439 } catch ( IncompatibleDiffTypesException $e ) {
1440 $slotDiff = $this->getSlotError( $e->getMessageObject()->parse() );
1441 }
1442 if ( $slotDiff && $role !== SlotRecord::MAIN ) {
1443 // FIXME: ask SlotRoleHandler::getSlotNameMessage
1444 $slotTitle = $role;
1445 $difftext .= $this->getSlotHeader( $slotTitle );
1446 }
1447 $difftext .= $slotDiff;
1448 }
1449
1450 // Save to cache for 7 days
1451 if ( !$this->hookRunner->onAbortDiffCache( $this ) ) {
1452 $this->incrementStats( 'uncacheable' );
1453 } elseif ( $key !== false ) {
1454 $this->incrementStats( 'miss' );
1455 $cache->set( $key, $difftext, 7 * 86400 );
1456 } else {
1457 $this->incrementStats( 'uncacheable' );
1458 }
1459 // localise line numbers and title attribute text
1460 $difftext = $this->localiseDiff( $difftext );
1461
1462 return $difftext;
1463 }
1464
1471 public function getDiffBodyForRole( $role ) {
1472 $diffRenderers = $this->getSlotDiffRenderers();
1473 if ( !isset( $diffRenderers[$role] ) ) {
1474 return false;
1475 }
1476
1477 $slotContents = $this->getSlotContents();
1478 try {
1479 $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role]['old'],
1480 $slotContents[$role]['new'] );
1481 } catch ( IncompatibleDiffTypesException $e ) {
1482 $slotDiff = $this->getSlotError( $e->getMessageObject()->parse() );
1483 }
1484 if ( $slotDiff === '' ) {
1485 return false;
1486 }
1487
1488 if ( $role !== SlotRecord::MAIN ) {
1489 // TODO use human-readable role name at least
1490 $slotTitle = $role;
1491 $slotDiff = $this->getSlotHeader( $slotTitle ) . $slotDiff;
1492 }
1493
1494 return $this->localiseDiff( $slotDiff );
1495 }
1496
1504 protected function getSlotHeader( $headerText ) {
1505 // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
1506 $columnCount = $this->mOldRevisionRecord ? 4 : 2;
1507 $userLang = $this->getLanguage()->getHtmlCode();
1508 return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-header', 'lang' => $userLang ],
1509 Html::element( 'th', [ 'colspan' => $columnCount ], $headerText ) );
1510 }
1511
1518 protected function getSlotError( $errorText ) {
1519 // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
1520 $columnCount = $this->mOldRevisionRecord ? 4 : 2;
1521 $userLang = $this->getLanguage()->getHtmlCode();
1522 return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-error', 'lang' => $userLang ],
1523 Html::rawElement( 'td', [ 'colspan' => $columnCount ], $errorText ) );
1524 }
1525
1539 protected function getDiffBodyCacheKeyParams() {
1540 if ( !$this->mOldid || !$this->mNewid ) {
1541 throw new BadMethodCallException( 'mOldid and mNewid must be set to get diff cache key.' );
1542 }
1543
1544 $params = [
1545 'diff',
1546 self::DIFF_VERSION,
1547 "old-{$this->mOldid}",
1548 "rev-{$this->mNewid}"
1549 ];
1550
1551 $extraKeys = [];
1552 if ( !$this->isSlotDiffRenderer ) {
1553 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1554 $extraKeys = array_merge( $extraKeys, $slotDiffRenderer->getExtraCacheKeys() );
1555 }
1556 }
1557 ksort( $extraKeys );
1558 return array_merge( $params, array_values( $extraKeys ) );
1559 }
1560
1568 public function getExtraCacheKeys() {
1569 // This method is called when the DifferenceEngine is used for a slot diff. We only care
1570 // about special things, not the revision IDs, which are added to the cache key by the
1571 // page-level DifferenceEngine, and which might not have a valid value for this object.
1572 $this->mOldid = 123456789;
1573 $this->mNewid = 987654321;
1574
1575 // This will repeat a bunch of unnecessary key fields for each slot. Not nice but harmless.
1576 $params = $this->getDiffBodyCacheKeyParams();
1577
1578 // Try to get rid of the standard keys to keep the cache key human-readable:
1579 // call the getDiffBodyCacheKeyParams implementation of the base class, and if
1580 // the child class includes the same keys, drop them.
1581 // Uses an obscure PHP feature where static calls to non-static methods are allowed
1582 // as long as we are already in a non-static method of the same class, and the call context
1583 // ($this) will be inherited.
1584 // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
1586 if ( array_slice( $params, 0, count( $standardParams ) ) === $standardParams ) {
1587 $params = array_slice( $params, count( $standardParams ) );
1588 }
1589
1590 return $params;
1591 }
1592
1603 public function setSlotDiffOptions( $options ) {
1604 $validatedOptions = [];
1605 if ( isset( $options['diff-type'] )
1606 && $this->getTextDiffer()->hasFormat( $options['diff-type'] )
1607 ) {
1608 $validatedOptions['diff-type'] = $options['diff-type'];
1609 }
1610 if ( !empty( $options['expand-url'] ) ) {
1611 $validatedOptions['expand-url'] = true;
1612 }
1613 if ( !empty( $options['inline-toggle'] ) ) {
1614 $validatedOptions['inline-toggle'] = true;
1615 }
1616 $this->slotDiffOptions = $validatedOptions;
1617 }
1618
1626 public function setExtraQueryParams( $params ) {
1627 $this->extraQueryParams = $params;
1628 }
1629
1643 public function generateContentDiffBody( Content $old, Content $new ) {
1644 $slotDiffRenderer = $new->getContentHandler()->getSlotDiffRenderer( $this->getContext() );
1645 if (
1646 $slotDiffRenderer instanceof DifferenceEngineSlotDiffRenderer
1647 && $this->isSlotDiffRenderer
1648 ) {
1649 // Oops, we are just about to enter an infinite loop (the slot-level DifferenceEngine
1650 // called a DifferenceEngineSlotDiffRenderer that wraps the same DifferenceEngine class).
1651 // This will happen when a content model has no custom slot diff renderer, it does have
1652 // a custom difference engine, but that does not override this method.
1653 throw new LogicException( get_class( $this ) . ': could not maintain backwards compatibility. '
1654 . 'Please use a SlotDiffRenderer.' );
1655 }
1656 return $slotDiffRenderer->getDiff( $old, $new ) . $this->getDebugString();
1657 }
1658
1671 public function generateTextDiffBody( $otext, $ntext ) {
1672 $slotDiffRenderer = $this->contentHandlerFactory
1673 ->getContentHandler( CONTENT_MODEL_TEXT )
1674 ->getSlotDiffRenderer( $this->getContext() );
1675 if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1676 // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1677 // This is too unlikely to happen to bother handling properly.
1678 throw new LogicException( 'The slot diff renderer for text content should be a '
1679 . 'TextSlotDiffRenderer subclass' );
1680 }
1681 return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1682 }
1683
1690 public static function getEngine() {
1691 $differenceEngine = new self;
1692 $engine = $differenceEngine->getTextDiffer()->getEngineForFormat( 'table' );
1693 if ( $engine === 'external' ) {
1694 return MediaWikiServices::getInstance()->getMainConfig()
1695 ->get( MainConfigNames::ExternalDiffEngine );
1696 } else {
1697 return $engine;
1698 }
1699 }
1700
1709 protected function debug( $generator = "internal" ) {
1710 if ( !$this->enableDebugComment ) {
1711 return '';
1712 }
1713 $data = [ $generator ];
1714 if ( $this->getConfig()->get( MainConfigNames::ShowHostnames ) ) {
1715 $data[] = wfHostname();
1716 }
1717 $data[] = wfTimestamp( TS_DB );
1718
1719 return "<!-- diff generator: " .
1720 implode( " ", array_map( "htmlspecialchars", $data ) ) .
1721 " -->\n";
1722 }
1723
1727 private function getDebugString() {
1728 $engine = self::getEngine();
1729 if ( $engine === 'wikidiff2' ) {
1730 return $this->debug( 'wikidiff2' );
1731 } elseif ( $engine === 'php' ) {
1732 return $this->debug( 'native PHP' );
1733 } else {
1734 return $this->debug( "external $engine" );
1735 }
1736 }
1737
1744 private function localiseDiff( $text ) {
1745 return $this->getTextDiffer()->localize( $this->getTextDiffFormat(), $text );
1746 }
1747
1756 public function localiseLineNumbers( $text ) {
1757 return preg_replace_callback( '/<!--LINE (\d+)-->/',
1758 function ( array $matches ) {
1759 if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
1760 return '';
1761 }
1762 return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
1763 }, $text );
1764 }
1765
1771 public function getMultiNotice() {
1772 // The notice only make sense if we are diffing two saved revisions of the same page.
1773 if (
1774 !$this->mOldRevisionRecord || !$this->mNewRevisionRecord
1775 || !$this->mOldPage || !$this->mNewPage
1776 || !$this->mOldPage->equals( $this->mNewPage )
1777 || $this->mOldRevisionRecord->getId() === null
1778 || $this->mNewRevisionRecord->getId() === null
1779 // (T237709) Deleted revs might have different page IDs
1780 || $this->mNewPage->getArticleID() !== $this->mOldRevisionRecord->getPageId()
1781 || $this->mNewPage->getArticleID() !== $this->mNewRevisionRecord->getPageId()
1782 ) {
1783 return '';
1784 }
1785
1786 if ( $this->mOldRevisionRecord->getTimestamp() > $this->mNewRevisionRecord->getTimestamp() ) {
1787 $oldRevRecord = $this->mNewRevisionRecord; // flip
1788 $newRevRecord = $this->mOldRevisionRecord; // flip
1789 } else { // normal case
1790 $oldRevRecord = $this->mOldRevisionRecord;
1791 $newRevRecord = $this->mNewRevisionRecord;
1792 }
1793
1794 // Don't show the notice if too many rows must be scanned
1795 // @todo show some special message for that case
1796 $nEdits = 0;
1797 $revisionIdList = $this->revisionStore->getRevisionIdsBetween(
1798 $this->mNewPage->getArticleID(),
1799 $oldRevRecord,
1800 $newRevRecord,
1801 1000
1802 );
1803 // only count revisions that are visible
1804 if ( count( $revisionIdList ) > 0 ) {
1805 foreach ( $revisionIdList as $revisionId ) {
1806 $revision = $this->revisionStore->getRevisionById( $revisionId );
1807 if ( $revision->getUser( RevisionRecord::FOR_THIS_USER, $this->getAuthority() ) ) {
1808 $nEdits++;
1809 }
1810 }
1811 }
1812 if ( $nEdits > 0 && $nEdits <= 1000 ) {
1813 // Use an invalid username to get the wiki's default gender (as fallback)
1814 $newRevUserForGender = '[HIDDEN]';
1815 $limit = 100; // use diff-multi-manyusers if too many users
1816 try {
1817 $users = $this->revisionStore->getAuthorsBetween(
1818 $this->mNewPage->getArticleID(),
1819 $oldRevRecord,
1820 $newRevRecord,
1821 null,
1822 $limit
1823 );
1824 $numUsers = count( $users );
1825
1826 $newRevUser = $newRevRecord->getUser( RevisionRecord::RAW );
1827 $newRevUserText = $newRevUser ? $newRevUser->getName() : '';
1828 $newRevUserSafe = $newRevRecord->getUser(
1829 RevisionRecord::FOR_THIS_USER,
1830 $this->getAuthority()
1831 );
1832 $newRevUserForGender = $newRevUserSafe ? $newRevUserSafe->getName() : '[HIDDEN]';
1833 if ( $numUsers == 1 && $users[0]->getName() == $newRevUserText ) {
1834 $numUsers = 0; // special case to say "by the same user" instead of "by one other user"
1835 }
1836 } catch ( InvalidArgumentException $e ) {
1837 $numUsers = 0;
1838 }
1839
1840 return self::intermediateEditsMsg( $nEdits, $numUsers, $limit, $newRevUserForGender );
1841 }
1842
1843 return '';
1844 }
1845
1856 public static function intermediateEditsMsg( $numEdits, $numUsers, $limit, $lastUser = '[HIDDEN]' ) {
1857 if ( $numUsers === 0 ) {
1858 $msg = 'diff-multi-sameuser';
1859 return wfMessage( $msg )
1860 ->numParams( $numEdits, $numUsers )
1861 ->params( $lastUser )
1862 ->parse();
1863 } elseif ( $numUsers > $limit ) {
1864 $msg = 'diff-multi-manyusers';
1865 $numUsers = $limit;
1866 } else {
1867 $msg = 'diff-multi-otherusers';
1868 }
1869
1870 return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1871 }
1872
1877 private function userCanEdit( RevisionRecord $revRecord ) {
1878 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
1879 return false;
1880 }
1881
1882 return true;
1883 }
1884
1894 public function getRevisionHeader( RevisionRecord $rev, $complete = '' ) {
1895 $lang = $this->getLanguage();
1896 $user = $this->getUser();
1897 $revtimestamp = $rev->getTimestamp();
1898 $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
1899 $dateofrev = $lang->userDate( $revtimestamp, $user );
1900 $timeofrev = $lang->userTime( $revtimestamp, $user );
1901
1902 $header = $this->msg(
1903 $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
1904 $timestamp,
1905 $dateofrev,
1906 $timeofrev
1907 );
1908
1909 if ( $complete !== 'complete' ) {
1910 return $header->escaped();
1911 }
1912
1913 $title = $rev->getPageAsLinkTarget();
1914
1915 if ( $this->userCanEdit( $rev ) ) {
1916 $header = $this->linkRenderer->makeKnownLink(
1917 $title,
1918 $header->text(),
1919 [],
1920 [ 'oldid' => $rev->getId() ]
1921 );
1922 $editQuery = [ 'action' => 'edit' ];
1923 if ( !$rev->isCurrent() ) {
1924 $editQuery['oldid'] = $rev->getId();
1925 }
1926
1927 $key = $this->getAuthority()->probablyCan( 'edit', $rev->getPage() ) ? 'editold' : 'viewsourceold';
1928 $msg = $this->msg( $key )->text();
1929 $editLink = $this->linkRenderer->makeKnownLink( $title, $msg, [], $editQuery );
1930 $header .= ' ' . Html::rawElement(
1931 'span',
1932 [ 'class' => 'mw-diff-edit' ],
1933 $editLink
1934 );
1935 } else {
1936 $header = $header->escaped();
1937 }
1938
1939 // Machine readable information
1940 $header .= Html::element( 'span',
1941 [
1942 'class' => 'mw-diff-timestamp',
1943 'data-timestamp' => wfTimestamp( TS_ISO_8601, $revtimestamp ),
1944 ], ''
1945 );
1946
1947 if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1948 return Html::rawElement(
1949 'span',
1950 [ 'class' => Linker::getRevisionDeletedClass( $rev ) ],
1951 $header
1952 );
1953 }
1954
1955 return $header;
1956 }
1957
1970 public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
1971 // shared.css sets diff in interface language/dir, but the actual content
1972 // is often in a different language, mostly the page content language/dir
1973 $header = Html::openElement( 'table', [
1974 'class' => [
1975 'diff',
1976 // The following classes are used here:
1977 // * diff-type-table
1978 // * diff-type-inline
1979 'diff-type-' . $this->getTextDiffFormat(),
1980 // The following classes are used here:
1981 // * diff-contentalign-left
1982 // * diff-contentalign-right
1983 'diff-contentalign-' . $this->getDiffLang()->alignStart(),
1984 // The following classes are used here:
1985 // * diff-editfont-monospace
1986 // * diff-editfont-sans-serif
1987 // * diff-editfont-serif
1988 'diff-editfont-' . $this->userOptionsLookup->getOption(
1989 $this->getUser(),
1990 'editfont'
1991 )
1992 ],
1993 'data-mw' => 'interface',
1994 ] );
1995 $userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
1996
1997 if ( !$diff && !$otitle ) {
1998 $header .= "
1999 <tr class=\"diff-title\" lang=\"{$userLang}\">
2000 <td class=\"diff-ntitle\">{$ntitle}</td>
2001 </tr>";
2002 $multiColspan = 1;
2003 } else {
2004 if ( $diff ) { // Safari/Chrome show broken output if cols not used
2005 $header .= "
2006 <col class=\"diff-marker\" />
2007 <col class=\"diff-content\" />
2008 <col class=\"diff-marker\" />
2009 <col class=\"diff-content\" />";
2010 $colspan = 2;
2011 $multiColspan = 4;
2012 } else {
2013 $colspan = 1;
2014 $multiColspan = 2;
2015 }
2016 if ( $otitle || $ntitle ) {
2017 // FIXME Hardcoding values from TableDiffFormatter.
2018 $deletedClass = 'diff-side-deleted';
2019 $addedClass = 'diff-side-added';
2020 $header .= "
2021 <tr class=\"diff-title\" lang=\"{$userLang}\">
2022 <td colspan=\"$colspan\" class=\"diff-otitle {$deletedClass}\">{$otitle}</td>
2023 <td colspan=\"$colspan\" class=\"diff-ntitle {$addedClass}\">{$ntitle}</td>
2024 </tr>";
2025 }
2026 }
2027
2028 if ( $multi != '' ) {
2029 $header .= "<tr><td colspan=\"{$multiColspan}\" " .
2030 "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
2031 }
2032 if ( $notice != '' ) {
2033 $header .= "<tr><td colspan=\"{$multiColspan}\" " .
2034 "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
2035 }
2036
2037 return $header . $diff . "</table>";
2038 }
2039
2047 public function setContent( Content $oldContent, Content $newContent ) {
2048 $this->mOldContent = $oldContent;
2049 $this->mNewContent = $newContent;
2050
2051 $this->mTextLoaded = 2;
2052 $this->mRevisionsLoaded = true;
2053 $this->isContentOverridden = true;
2054 $this->slotDiffRenderers = null;
2055 }
2056
2062 public function setRevisions(
2063 ?RevisionRecord $oldRevision, RevisionRecord $newRevision
2064 ) {
2065 if ( $oldRevision ) {
2066 $this->mOldRevisionRecord = $oldRevision;
2067 $this->mOldid = $oldRevision->getId();
2068 $this->mOldPage = Title::newFromLinkTarget( $oldRevision->getPageAsLinkTarget() );
2069 // This method is meant for edit diffs and such so there is no reason to provide a
2070 // revision that's not readable to the user, but check it just in case.
2071 $this->mOldContent = $oldRevision->getContent( SlotRecord::MAIN,
2072 RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
2073 if ( !$this->mOldContent ) {
2074 $this->addRevisionLoadError( 'old' );
2075 }
2076 } else {
2077 $this->mOldPage = null;
2078 $this->mOldRevisionRecord = $this->mOldid = false;
2079 }
2080 $this->mNewRevisionRecord = $newRevision;
2081 $this->mNewid = $newRevision->getId();
2082 $this->mNewPage = Title::newFromLinkTarget( $newRevision->getPageAsLinkTarget() );
2083 $this->mNewContent = $newRevision->getContent( SlotRecord::MAIN,
2084 RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
2085 if ( !$this->mNewContent ) {
2086 $this->addRevisionLoadError( 'new' );
2087 }
2088
2089 $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded = true;
2090 $this->mTextLoaded = $oldRevision ? 2 : 1;
2091 $this->isContentOverridden = false;
2092 $this->slotDiffRenderers = null;
2093 }
2094
2101 public function setTextLanguage( Language $lang ) {
2102 $this->mDiffLang = $lang;
2103 }
2104
2117 public function mapDiffPrevNext( $old, $new ) {
2118 if ( $new === 'prev' ) {
2119 // Show diff between revision $old and the previous one. Get previous one from DB.
2120 $newid = intval( $old );
2121 $oldid = false;
2122 $newRev = $this->revisionStore->getRevisionById( $newid );
2123 if ( $newRev ) {
2124 $oldRev = $this->revisionStore->getPreviousRevision( $newRev );
2125 if ( $oldRev ) {
2126 $oldid = $oldRev->getId();
2127 }
2128 }
2129 } elseif ( $new === 'next' ) {
2130 // Show diff between revision $old and the next one. Get next one from DB.
2131 $oldid = intval( $old );
2132 $newid = false;
2133 $oldRev = $this->revisionStore->getRevisionById( $oldid );
2134 if ( $oldRev ) {
2135 $newRev = $this->revisionStore->getNextRevision( $oldRev );
2136 if ( $newRev ) {
2137 $newid = $newRev->getId();
2138 }
2139 }
2140 } else {
2141 $oldid = intval( $old );
2142 $newid = intval( $new );
2143 }
2144
2145 // @phan-suppress-next-line PhanTypeMismatchReturn getId does not return null here
2146 return [ $oldid, $newid ];
2147 }
2148
2149 private function loadRevisionIds() {
2150 if ( $this->mRevisionsIdsLoaded ) {
2151 return;
2152 }
2153
2154 $this->mRevisionsIdsLoaded = true;
2155
2156 $old = $this->mOldid;
2157 $new = $this->mNewid;
2158
2159 [ $this->mOldid, $this->mNewid ] = self::mapDiffPrevNext( $old, $new );
2160 if ( $new === 'next' && $this->mNewid === false ) {
2161 # if no result, NewId points to the newest old revision. The only newer
2162 # revision is cur, which is "0".
2163 $this->mNewid = 0;
2164 }
2165
2166 $this->hookRunner->onNewDifferenceEngine(
2167 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
2168 $this->getTitle(), $this->mOldid, $this->mNewid, $old, $new );
2169 }
2170
2184 public function loadRevisionData() {
2185 if ( $this->mRevisionsLoaded ) {
2186 return $this->isContentOverridden ||
2187 ( $this->mOldRevisionRecord !== null && $this->mNewRevisionRecord !== null );
2188 }
2189
2190 // Whether it succeeds or fails, we don't want to try again
2191 $this->mRevisionsLoaded = true;
2192
2193 $this->loadRevisionIds();
2194
2195 // Load the new RevisionRecord object
2196 if ( $this->mNewid ) {
2197 $this->mNewRevisionRecord = $this->revisionStore->getRevisionById( $this->mNewid );
2198 } else {
2199 $this->mNewRevisionRecord = $this->revisionStore->getRevisionByTitle( $this->getTitle() );
2200 }
2201
2202 if ( !$this->mNewRevisionRecord instanceof RevisionRecord ) {
2203 return false;
2204 }
2205
2206 // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
2207 $this->mNewid = $this->mNewRevisionRecord->getId();
2208 $this->mNewPage = $this->mNewid ?
2209 Title::newFromLinkTarget( $this->mNewRevisionRecord->getPageAsLinkTarget() ) :
2210 null;
2211
2212 // Load the old RevisionRecord object
2213 $this->mOldRevisionRecord = false;
2214 if ( $this->mOldid ) {
2215 $this->mOldRevisionRecord = $this->revisionStore->getRevisionById( $this->mOldid );
2216 } elseif ( $this->mOldid === 0 ) {
2217 $revRecord = $this->revisionStore->getPreviousRevision( $this->mNewRevisionRecord );
2218 // No previous revision; mark to show as first-version only.
2219 $this->mOldid = $revRecord ? $revRecord->getId() : false;
2220 $this->mOldRevisionRecord = $revRecord ?? false;
2221 } /* elseif ( $this->mOldid === false ) leave mOldRevisionRecord false; */
2222
2223 if ( $this->mOldRevisionRecord === null ) {
2224 return false;
2225 }
2226
2227 if ( $this->mOldRevisionRecord && $this->mOldRevisionRecord->getId() ) {
2228 $this->mOldPage = Title::newFromLinkTarget(
2229 $this->mOldRevisionRecord->getPageAsLinkTarget()
2230 );
2231 } else {
2232 $this->mOldPage = null;
2233 }
2234
2235 // Load tags information for both revisions
2236 $dbr = $this->dbProvider->getReplicaDatabase();
2237 $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
2238 if ( $this->mOldid !== false ) {
2239 $tagIds = $dbr->newSelectQueryBuilder()
2240 ->select( 'ct_tag_id' )
2241 ->from( 'change_tag' )
2242 ->where( [ 'ct_rev_id' => $this->mOldid ] )
2243 ->caller( __METHOD__ )->fetchFieldValues();
2244 $tags = [];
2245 foreach ( $tagIds as $tagId ) {
2246 try {
2247 $tags[] = $changeTagDefStore->getName( (int)$tagId );
2248 } catch ( NameTableAccessException $exception ) {
2249 continue;
2250 }
2251 }
2252 $this->mOldTags = implode( ',', $tags );
2253 } else {
2254 $this->mOldTags = false;
2255 }
2256
2257 $tagIds = $dbr->newSelectQueryBuilder()
2258 ->select( 'ct_tag_id' )
2259 ->from( 'change_tag' )
2260 ->where( [ 'ct_rev_id' => $this->mNewid ] )
2261 ->caller( __METHOD__ )->fetchFieldValues();
2262 $tags = [];
2263 foreach ( $tagIds as $tagId ) {
2264 try {
2265 $tags[] = $changeTagDefStore->getName( (int)$tagId );
2266 } catch ( NameTableAccessException $exception ) {
2267 continue;
2268 }
2269 }
2270 $this->mNewTags = implode( ',', $tags );
2271
2272 return true;
2273 }
2274
2283 public function loadText() {
2284 if ( $this->mTextLoaded == 2 ) {
2285 return $this->loadRevisionData() &&
2286 ( $this->mOldRevisionRecord === false || $this->mOldContent )
2287 && $this->mNewContent;
2288 }
2289
2290 // Whether it succeeds or fails, we don't want to try again
2291 $this->mTextLoaded = 2;
2292
2293 if ( !$this->loadRevisionData() ) {
2294 return false;
2295 }
2296
2297 if ( $this->mOldRevisionRecord ) {
2298 $this->mOldContent = $this->mOldRevisionRecord->getContent(
2299 SlotRecord::MAIN,
2300 RevisionRecord::FOR_THIS_USER,
2301 $this->getAuthority()
2302 );
2303 if ( $this->mOldContent === null ) {
2304 return false;
2305 }
2306 }
2307
2308 $this->mNewContent = $this->mNewRevisionRecord->getContent(
2309 SlotRecord::MAIN,
2310 RevisionRecord::FOR_THIS_USER,
2311 $this->getAuthority()
2312 );
2313 $this->hookRunner->onDifferenceEngineLoadTextAfterNewContentIsLoaded( $this );
2314 if ( $this->mNewContent === null ) {
2315 return false;
2316 }
2317
2318 return true;
2319 }
2320
2326 public function loadNewText() {
2327 if ( $this->mTextLoaded >= 1 ) {
2328 return $this->loadRevisionData();
2329 }
2330
2331 $this->mTextLoaded = 1;
2332
2333 if ( !$this->loadRevisionData() ) {
2334 return false;
2335 }
2336
2337 $this->mNewContent = $this->mNewRevisionRecord->getContent(
2338 SlotRecord::MAIN,
2339 RevisionRecord::FOR_THIS_USER,
2340 $this->getAuthority()
2341 );
2342
2343 $this->hookRunner->onDifferenceEngineAfterLoadNewText( $this );
2344
2345 return true;
2346 }
2347
2353 protected function getTextDiffer() {
2354 if ( $this->textDiffer === null ) {
2355 $this->textDiffer = new ManifoldTextDiffer(
2356 $this->getContext(),
2357 $this->getDiffLang(),
2358 $this->getConfig()->get( MainConfigNames::DiffEngine ),
2359 $this->getConfig()->get( MainConfigNames::ExternalDiffEngine ),
2360 $this->getConfig()->get( MainConfigNames::Wikidiff2Options )
2361 );
2362 }
2363 return $this->textDiffer;
2364 }
2365
2372 public function getSupportedFormats() {
2373 return $this->getTextDiffer()->getFormats();
2374 }
2375
2382 public function getTextDiffFormat() {
2383 return $this->slotDiffOptions['diff-type'] ?? 'table';
2384 }
2385
2386}
const NS_SPECIAL
Definition Defines.php:54
const CONTENT_MODEL_TEXT
Definition Defines.php:231
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfHostname()
Get host name of the current machine, for use in error reporting.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
array $params
The job parameters.
static formatSummaryRow( $tags, $unused, ?MessageLocalizer $localizer=null)
Creates HTML for the given tags.
B/C adapter for turning a DifferenceEngine into a SlotDiffRenderer.
DifferenceEngine is responsible for rendering the difference between two revisions as HTML.
bool $enableDebugComment
Set this to true to add debug info to the HTML output.
bool $unhide
Show rev_deleted content if allowed.
bool $isContentOverridden
Was the content overridden via setContent()? If the content was overridden, most internal state (e....
getExtraCacheKeys()
Implements DifferenceEngineSlotDiffRenderer::getExtraCacheKeys().
markAsSlotDiffRenderer()
Mark this DifferenceEngine as a slot renderer (as opposed to a page renderer).
getSlotHeader( $headerText)
Get a slot header for inclusion in a diff body (as a table row).
setSlotDiffOptions( $options)
hasDeletedRevision()
Checks whether one of the given Revisions was deleted.
int $mTextLoaded
How many text blobs have been loaded, 0, 1 or 2?
deletedIdMarker( $id)
Build a wikitext link toward a deleted revision, if viewable.
SlotDiffRenderer[] null $slotDiffRenderers
DifferenceEngine classes for the slots, keyed by role name.
getDiffBodyForRole( $role)
Get the diff table body for one slot, without header.
getTextDiffer()
Get the TextDiffer which will be used for rendering text.
getDefaultLanguage()
Get the language to use if none has been set by setTextLanguage().
getOldid()
Get the ID of old revision (left pane) of the diff.
setRevisions(?RevisionRecord $oldRevision, RevisionRecord $newRevision)
Use specified text instead of loading from the database.
bool $isSlotDiffRenderer
Temporary hack for B/C while slot diff related methods of DifferenceEngine are being deprecated.
generateTextDiffBody( $otext, $ntext)
Generate a diff, no caching.
loadNewText()
Load the text of the new revision, not the old one.
showDiffPage( $diffOnly=false)
loadText()
Load the text of the revisions, as well as revision data.
int string false null $mNewid
Revision ID for the new revision.
mapDiffPrevNext( $old, $new)
Maps a revision pair definition as accepted by DifferenceEngine constructor to a pair of actual integ...
getPermissionErrors(Authority $performer)
Get the permission errors associated with the revisions for the current diff.
getDiffBody()
Get the diff table body, without header.
getTitle()
1.18 to override Title|null
loadRevisionData()
Load revision metadata for the specified revisions.
static getEngine()
Process DiffEngine config and get a sensible, usable engine.
bool $mRevisionsLoaded
Have the revisions been loaded.
getNewRevision()
Get the right side of the diff.
showDiff( $otitle, $ntitle, $notice='')
Get the diff text, send it to the OutputPage object Returns false if the diff could not be generated,...
getTextDiffFormat()
Get the selected text diff format.
localiseLineNumbers( $text)
Replace a common convention for language-independent line numbers with the text in the user's languag...
getSlotContents()
Get the old and new content objects for all slots.
string $mMarkPatrolledLink
Link to action=markpatrolled.
deletedLink( $id)
Look up a special:Undelete link to the given deleted revision id, as a workaround for being unable to...
bool $mReducedLineNumbers
If true, line X is not displayed when X is 1, for example to increase readability and conserve space ...
__construct( $context=null, $old=0, $new=0, $rcid=0, $refreshCache=false, $unhide=false)
Title null $mNewPage
Title of new revision or null if the new revision does not exist or does not belong to a page.
bool $mCacheHit
Was the diff fetched from cache?
getMultiNotice()
If there are revisions between the ones being compared, return a note saying so.
isUserAllowedToSeeRevisions(Authority $performer)
Checks whether the current user has permission for accessing the revisions of the diff.
int false null $mOldid
Revision ID for the old revision.
debug( $generator="internal")
Generate a debug comment indicating diff generating time, server node, and generator backend.
addHeader( $diff, $otitle, $ntitle, $multi='', $notice='')
Add the header to a diff body.
bool $mRefreshCache
Refresh the diff cache.
LinkRenderer $linkRenderer
getDiffBodyCacheKeyParams()
Get the cache key parameters.
getDiff( $otitle, $ntitle, $notice='')
Get complete diff table, including header.
setExtraQueryParams( $params)
Set query parameters to append to diff page links.
static intermediateEditsMsg( $numEdits, $numUsers, $limit, $lastUser='[HIDDEN]')
Get a notice about how many intermediate edits and users there are.
getNewid()
Get the ID of new revision (right pane) of the diff.
renderNewRevision()
Show the new revision of the page.
setContent(Content $oldContent, Content $newContent)
Use specified text instead of loading from the database.
setTextLanguage(Language $lang)
Set the language in which the diff text is written.
generateContentDiffBody(Content $old, Content $new)
Generate a diff, no caching.
getSlotError( $errorText)
Get an error message for inclusion in a diff body (as a table row).
shouldBeHiddenFromUser(Authority $performer)
Checks whether the diff should be hidden from the current user This is based on whether the user is a...
getRevisionHeader(RevisionRecord $rev, $complete='')
Get a header for a specified revision.
getMarkPatrolledLinkInfo()
Returns an array of meta data needed to build a "mark as patrolled" link and adds a JS module to the ...
setReducedLineNumbers( $value=true)
Set reduced line numbers mode.
getSupportedFormats()
Get the list of supported text diff formats.
Title null $mOldPage
Title of old revision or null if the old revision does not exist or does not belong to a page.
getDiffLang()
Get the language in which the diff text is written.
showDiffStyle()
Add style sheets for diff display.
markPatrolledLink()
Build a link to mark a change as patrolled.
getRevisionLoadErrors()
If errors were encountered while loading the revision contents, this will return an array of Messages...
hasSuppressedRevision()
Checks whether one of the given Revisions was suppressed.
getOldRevision()
Get the left side of the diff.
Exception thrown when trying to render a diff between two content types which cannot be compared (thi...
getMessageObject()
Return a Message object for this exception.
This is the main service interface for converting single-line comments from various DB comment fields...
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
setContext(IContextSource $context)
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
getContext()
Get the base IContextSource object.
A TextDiffer which acts as a container for other TextDiffers, and dispatches requests to them.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
Base class for language-specific code.
Definition Language.php:81
Class that generates HTML for internal links.
Some internal bits split of from Skin.php.
Definition Linker.php:63
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:155
Service for getting rendered output of a given page.
Service for creating WikiPage objects.
A StatusValue for permission errors.
Exception raised when the text of a revision is permanently missing or corrupt.
Page revision base class.
getContent( $role, $audience=self::FOR_PUBLIC, ?Authority $performer=null)
Returns the Content of the given slot of this revision.
getPage()
Returns the page this revision belongs to.
isCurrent()
Checks whether the revision record is a stored current revision.
getTimestamp()
MCR migration note: this replaced Revision::getTimestamp.
getPageAsLinkTarget()
Returns the title of the page this revision is associated with as a LinkTarget object.
userCan( $field, Authority $performer)
Determine if the give authority is allowed to view a particular field of this revision,...
isDeleted( $field)
MCR migration note: this replaced Revision::isDeleted.
getId( $wikiId=self::LOCAL)
Get revision ID.
Service for looking up page revisions.
Value object representing a content slot associated with a page revision.
getContent()
Returns the Content of the given slot.
Parent class for all special pages.
Exception representing a failure to look up a row from a name table.
Represents a title within MediaWiki.
Definition Title.php:78
Provides access to user options.
Track info about user edit counts and timings.
Manage user group memberships.
Represents the membership of one user in one user group.
Convenience functions for interpreting UserIdentity objects using additional services or config.
Show an error when a user tries to do something they do not have the necessary permissions for.
Renders a diff for a single slot (that is, a diff between two content objects).
Renders a slot diff by doing a text diff on the native representation.
Base interface for representing page content.
Definition Content.php:39
getContentHandler()
Convenience method that returns the ContentHandler singleton for handling the content model that this...
Interface for objects which can provide a MediaWiki context on request.
This interface represents the authority associated with the current execution context,...
Definition Authority.php:37
authorizeRead(string $action, PageIdentity $target, ?PermissionStatus $status=null)
Authorize read access.
Interface for objects representing user identity.
Provide primary and replica IDatabase connections.
$header