MediaWiki 1.43.1
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 # If the diff cannot be shown due to a deleted revision, then output
996 # the diff header and links to unhide (if available)...
997 if ( $this->shouldBeHiddenFromUser( $this->getAuthority() ) ) {
998 $this->showDiffStyle();
999 $multi = $this->getMultiNotice();
1000 $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
1001 if ( !$allowed ) {
1002 # Give explanation for why revision is not visible
1003 $msg = [ $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff' ];
1004 } else {
1005 # Give explanation and add a link to view the diff...
1006 $query = $this->getRequest()->appendQueryValue( 'unhide', '1' );
1007 $msg = [
1008 $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff',
1009 $this->getTitle()->getFullURL( $query )
1010 ];
1011 }
1012 $out->addHTML( Html::warningBox( $this->msg( ...$msg )->parse(), 'plainlinks' ) );
1013 # Otherwise, output a regular diff...
1014 } else {
1015 # Add deletion notice if the user is viewing deleted content
1016 $notice = '';
1017 if ( $deleted ) {
1018 $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
1019 $notice = Html::warningBox( $this->msg( $msg )->parse(), 'plainlinks' );
1020 }
1021
1022 # Add an error if the content can't be loaded
1023 $this->getSlotContents();
1024 foreach ( $this->getRevisionLoadErrors() as $msg ) {
1025 $notice .= Html::warningBox( $msg->parse() );
1026 }
1027
1028 // Check if inline switcher will be needed
1029 if ( $this->getTextDiffer()->hasFormat( 'inline' ) ) {
1030 $out->enableOOUI();
1031 }
1032
1033 $this->showTablePrefixes();
1034 $this->showDiff( $oldHeader, $newHeader, $notice );
1035 if ( !$diffOnly ) {
1036 $this->renderNewRevision();
1037 }
1038
1039 // Allow extensions to optionally not show the final patrolled link
1040 if ( $this->hookRunner->onDifferenceEngineRenderRevisionShowFinalPatrolLink() ) {
1041 # Add redundant patrol link on bottom...
1042 $out->addHTML( $this->markPatrolledLink() );
1043 }
1044 }
1045 }
1046
1050 private function showTablePrefixes() {
1051 $parts = [];
1052 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1053 $parts += $slotDiffRenderer->getTablePrefix( $this->getContext(), $this->mNewPage );
1054 }
1055 ksort( $parts );
1056 if ( count( array_filter( $parts ) ) > 0 ) {
1057 $language = $this->getLanguage();
1058 $attrs = [
1059 'class' => 'mw-diff-table-prefix',
1060 'dir' => $language->getDir(),
1061 'lang' => $language->getCode(),
1062 ];
1063 $this->getOutput()->addHTML(
1064 Html::rawElement( 'div', $attrs, implode( '', $parts ) ) );
1065 }
1066 }
1067
1079 public function markPatrolledLink() {
1080 if ( $this->mMarkPatrolledLink === null ) {
1081 $linkInfo = $this->getMarkPatrolledLinkInfo();
1082 // If false, there is no patrol link needed/allowed
1083 if ( !$linkInfo || !$this->mNewPage ) {
1084 $this->mMarkPatrolledLink = '';
1085 } else {
1086 $patrolLinkClass = 'patrollink';
1087 $this->mMarkPatrolledLink = ' <span class="' . $patrolLinkClass . '" data-mw="interface">[' .
1088 $this->linkRenderer->makeKnownLink(
1089 $this->mNewPage,
1090 $this->msg( 'markaspatrolleddiff' )->text(),
1091 [],
1092 [
1093 'action' => 'markpatrolled',
1094 'rcid' => $linkInfo['rcid'],
1095 ]
1096 ) . ']</span>';
1097 // Allow extensions to change the markpatrolled link
1098 $this->hookRunner->onDifferenceEngineMarkPatrolledLink( $this,
1099 $this->mMarkPatrolledLink, $linkInfo['rcid'] );
1100 }
1101 }
1102 return $this->mMarkPatrolledLink;
1103 }
1104
1112 protected function getMarkPatrolledLinkInfo() {
1113 $user = $this->getUser();
1114 $config = $this->getConfig();
1115
1116 // Prepare a change patrol link, if applicable
1117 if (
1118 // Is patrolling enabled and the user allowed to?
1119 $config->get( MainConfigNames::UseRCPatrol ) &&
1120 $this->mNewPage &&
1121 $this->getAuthority()->probablyCan( 'patrol', $this->mNewPage ) &&
1122 // Only do this if the revision isn't more than 6 hours older
1123 // than the Max RC age (6h because the RC might not be cleaned out regularly)
1124 RecentChange::isInRCLifespan( $this->mNewRevisionRecord->getTimestamp(), 21600 )
1125 ) {
1126 // Look for an unpatrolled change corresponding to this diff
1127 $change = RecentChange::newFromConds(
1128 [
1129 'rc_this_oldid' => $this->mNewid,
1130 'rc_patrolled' => RecentChange::PRC_UNPATROLLED
1131 ],
1132 __METHOD__
1133 );
1134
1135 if ( $change && !$change->getPerformerIdentity()->equals( $user ) ) {
1136 $rcid = $change->getAttribute( 'rc_id' );
1137 } else {
1138 // None found or the page has been created by the current user.
1139 // If the user could patrol this it already would be patrolled
1140 $rcid = 0;
1141 }
1142
1143 // Allow extensions to possibly change the rcid here
1144 // For example the rcid might be set to zero due to the user
1145 // being the same as the performer of the change but an extension
1146 // might still want to show it under certain conditions
1147 $this->hookRunner->onDifferenceEngineMarkPatrolledRCID( $rcid, $this, $change, $user );
1148
1149 // Build the link
1150 if ( $rcid ) {
1151 $this->getOutput()->getMetadata()->setPreventClickjacking( true );
1152 $this->getOutput()->addModules( 'mediawiki.misc-authed-curate' );
1153
1154 return [ 'rcid' => $rcid ];
1155 }
1156 }
1157
1158 // No mark as patrolled link applicable
1159 return false;
1160 }
1161
1167 private function revisionDeleteLink( RevisionRecord $revRecord ) {
1168 $link = Linker::getRevDeleteLink(
1169 $this->getAuthority(),
1170 $revRecord,
1171 $revRecord->getPageAsLinkTarget()
1172 );
1173 if ( $link !== '' ) {
1174 $link = "\u{00A0}\u{00A0}\u{00A0}" . $link . ' ';
1175 }
1176
1177 return $link;
1178 }
1179
1185 public function renderNewRevision() {
1186 if ( $this->isContentOverridden ) {
1187 // The code below only works with a RevisionRecord object. We could construct a
1188 // fake RevisionRecord (here or in setContent), but since this does not seem
1189 // needed at the moment, we'll just fail for now.
1190 throw new LogicException(
1191 __METHOD__
1192 . ' is not supported after calling setContent(). Use setRevisions() instead.'
1193 );
1194 }
1195
1196 $out = $this->getOutput();
1197 $revHeader = $this->getRevisionHeader( $this->mNewRevisionRecord );
1198 # Add "current version as of X" title
1199 $out->addHTML( "<hr class='diff-hr' id='mw-oldid' />
1200 <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
1201 # Page content may be handled by a hooked call instead...
1202 if ( $this->hookRunner->onArticleContentOnDiff( $this, $out ) ) {
1203 $this->loadNewText();
1204 if ( !$this->mNewPage ) {
1205 // New revision is unsaved; bail out.
1206 // TODO in theory rendering the new revision is a meaningful thing to do
1207 // even if it's unsaved, but a lot of untangling is required to do it safely.
1208 return;
1209 }
1210 if ( $this->hasNewRevisionLoadError() ) {
1211 // There was an error loading the new revision
1212 return;
1213 }
1214
1215 $out->setRevisionId( $this->mNewid );
1216 $out->setRevisionIsCurrent( $this->mNewRevisionRecord->isCurrent() );
1217 $out->setRevisionTimestamp( $this->mNewRevisionRecord->getTimestamp() );
1218 $out->setArticleFlag( true );
1219
1220 if ( !$this->hookRunner->onArticleRevisionViewCustom(
1221 $this->mNewRevisionRecord, $this->mNewPage, $this->mOldid, $out )
1222 ) {
1223 // Handled by extension
1224 // NOTE: sync with hooks called in Article::view()
1225 } else {
1226 // Normal page
1227 if ( $this->getTitle()->equals( $this->mNewPage ) ) {
1228 // If the Title stored in the context is the same as the one
1229 // of the new revision, we can use its associated WikiPage
1230 // object.
1231 $wikiPage = $this->getWikiPage();
1232 } else {
1233 // Otherwise we need to create our own WikiPage object
1234 $wikiPage = $this->wikiPageFactory->newFromTitle( $this->mNewPage );
1235 }
1236
1237 $parserOptions = $wikiPage->makeParserOptions( $this->getContext() );
1238 $parserOptions->setRenderReason( 'diff-page' );
1239
1240 $parserOutputAccess = MediaWikiServices::getInstance()->getParserOutputAccess();
1241 $status = $parserOutputAccess->getParserOutput(
1242 $wikiPage,
1243 $parserOptions,
1244 $this->mNewRevisionRecord,
1245 // we already checked
1246 ParserOutputAccess::OPT_NO_AUDIENCE_CHECK |
1247 // Update cascading protection
1248 ParserOutputAccess::OPT_LINKS_UPDATE
1249 );
1250 if ( $status->isOK() ) {
1251 $parserOutput = $status->getValue();
1252 // Allow extensions to change parser output here
1253 if ( $this->hookRunner->onDifferenceEngineRenderRevisionAddParserOutput(
1254 $this, $out, $parserOutput, $wikiPage )
1255 ) {
1256 $out->addParserOutput( $parserOutput, [
1257 'enableSectionEditLinks' => $this->mNewRevisionRecord->isCurrent()
1258 && $this->getAuthority()->probablyCan(
1259 'edit',
1260 $this->mNewRevisionRecord->getPage()
1261 ),
1262 'absoluteURLs' => $this->slotDiffOptions['expand-url'] ?? false
1263 ] );
1264 }
1265 } else {
1266 $out->addHTML(
1267 Html::errorBox(
1268 $out->parseAsInterface(
1269 $status->getWikiText( false, false, $this->getLanguage() )
1270 )
1271 )
1272 );
1273 }
1274 }
1275 }
1276 }
1277
1288 public function showDiff( $otitle, $ntitle, $notice = '' ) {
1289 // Allow extensions to affect the output here
1290 $this->hookRunner->onDifferenceEngineShowDiff( $this );
1291
1292 $diff = $this->getDiff( $otitle, $ntitle, $notice );
1293 if ( $diff === false ) {
1294 $this->showMissingRevision();
1295 return false;
1296 }
1297
1298 $this->showDiffStyle();
1299 if ( $this->slotDiffOptions['expand-url'] ?? false ) {
1300 $diff = Linker::expandLocalLinks( $diff );
1301 }
1302 $this->getOutput()->addHTML( $diff );
1303 return true;
1304 }
1305
1309 public function showDiffStyle() {
1310 if ( !$this->isSlotDiffRenderer ) {
1311 $this->getOutput()->addModules( 'mediawiki.diff' );
1312 $this->getOutput()->addModuleStyles( [
1313 'mediawiki.interface.helpers.styles',
1314 'mediawiki.diff.styles'
1315 ] );
1316 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1317 $slotDiffRenderer->addModules( $this->getOutput() );
1318 }
1319 }
1320 }
1321
1331 public function getDiff( $otitle, $ntitle, $notice = '' ) {
1332 $body = $this->getDiffBody();
1333 if ( $body === false ) {
1334 return false;
1335 }
1336
1337 $multi = $this->getMultiNotice();
1338 // Display a message when the diff is empty
1339 if ( $body === '' ) {
1340 $notice .= '<div class="mw-diff-empty">' .
1341 $this->msg( 'diff-empty' )->parse() .
1342 "</div>\n";
1343 }
1344
1345 if ( $this->cacheHitKey !== null ) {
1346 $body .= "\n<!-- diff cache key " . htmlspecialchars( $this->cacheHitKey ) . " -->\n";
1347 }
1348
1349 return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
1350 }
1351
1352 private function incrementStats( string $cacheStatus ): void {
1353 $stats = MediaWikiServices::getInstance()->getStatsFactory();
1354 $stats->getCounter( 'diff_cache_total' )
1355 ->setLabel( 'status', $cacheStatus )
1356 ->copyToStatsdAt( 'diff_cache.' . $cacheStatus )
1357 ->increment();
1358 }
1359
1365 public function getDiffBody() {
1366 $this->mCacheHit = true;
1367 // Check if the diff should be hidden from this user
1368 if ( !$this->isContentOverridden ) {
1369 if ( !$this->loadRevisionData() ) {
1370 return false;
1371 } elseif ( $this->mOldRevisionRecord &&
1372 !$this->mOldRevisionRecord->userCan(
1373 RevisionRecord::DELETED_TEXT,
1374 $this->getAuthority()
1375 )
1376 ) {
1377 return false;
1378 } elseif ( $this->mNewRevisionRecord &&
1379 !$this->mNewRevisionRecord->userCan(
1380 RevisionRecord::DELETED_TEXT,
1381 $this->getAuthority()
1382 ) ) {
1383 return false;
1384 }
1385 // Short-circuit
1386 if ( $this->mOldRevisionRecord === false || (
1387 $this->mOldRevisionRecord &&
1388 $this->mNewRevisionRecord &&
1389 $this->mOldRevisionRecord->getId() &&
1390 $this->mOldRevisionRecord->getId() == $this->mNewRevisionRecord->getId()
1391 ) ) {
1392 if ( $this->hookRunner->onDifferenceEngineShowEmptyOldContent( $this ) ) {
1393 return '';
1394 }
1395 }
1396 }
1397
1398 // Cacheable?
1399 $key = false;
1400 $services = MediaWikiServices::getInstance();
1401 $cache = $services->getMainWANObjectCache();
1402 $stats = $services->getStatsdDataFactory();
1403 if ( $this->mOldid && $this->mNewid ) {
1404 $key = $cache->makeKey( ...$this->getDiffBodyCacheKeyParams() );
1405
1406 // Try cache
1407 if ( !$this->mRefreshCache ) {
1408 $difftext = $cache->get( $key );
1409 if ( is_string( $difftext ) ) {
1410 $this->incrementStats( 'hit' );
1411 $difftext = $this->localiseDiff( $difftext );
1412 $this->cacheHitKey = $key;
1413 return $difftext;
1414 }
1415 } // don't try to load but save the result
1416 }
1417 $this->mCacheHit = false;
1418 $this->cacheHitKey = null;
1419
1420 // Loadtext is permission safe, this just clears out the diff
1421 if ( !$this->loadText() ) {
1422 return false;
1423 }
1424
1425 $difftext = '';
1426 // We've checked for revdelete at the beginning of this method; it's OK to ignore
1427 // read permissions here.
1428 $slotContents = $this->getSlotContents();
1429 foreach ( $this->getSlotDiffRenderers() as $role => $slotDiffRenderer ) {
1430 try {
1431 $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role]['old'],
1432 $slotContents[$role]['new'] );
1433 } catch ( IncompatibleDiffTypesException $e ) {
1434 $slotDiff = $this->getSlotError( $e->getMessageObject()->parse() );
1435 }
1436 if ( $slotDiff && $role !== SlotRecord::MAIN ) {
1437 // FIXME: ask SlotRoleHandler::getSlotNameMessage
1438 $slotTitle = $role;
1439 $difftext .= $this->getSlotHeader( $slotTitle );
1440 }
1441 $difftext .= $slotDiff;
1442 }
1443
1444 // Save to cache for 7 days
1445 if ( !$this->hookRunner->onAbortDiffCache( $this ) ) {
1446 $this->incrementStats( 'uncacheable' );
1447 } elseif ( $key !== false ) {
1448 $this->incrementStats( 'miss' );
1449 $cache->set( $key, $difftext, 7 * 86400 );
1450 } else {
1451 $this->incrementStats( 'uncacheable' );
1452 }
1453 // localise line numbers and title attribute text
1454 $difftext = $this->localiseDiff( $difftext );
1455
1456 return $difftext;
1457 }
1458
1465 public function getDiffBodyForRole( $role ) {
1466 $diffRenderers = $this->getSlotDiffRenderers();
1467 if ( !isset( $diffRenderers[$role] ) ) {
1468 return false;
1469 }
1470
1471 $slotContents = $this->getSlotContents();
1472 try {
1473 $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role]['old'],
1474 $slotContents[$role]['new'] );
1475 } catch ( IncompatibleDiffTypesException $e ) {
1476 $slotDiff = $this->getSlotError( $e->getMessageObject()->parse() );
1477 }
1478 if ( $slotDiff === '' ) {
1479 return false;
1480 }
1481
1482 if ( $role !== SlotRecord::MAIN ) {
1483 // TODO use human-readable role name at least
1484 $slotTitle = $role;
1485 $slotDiff = $this->getSlotHeader( $slotTitle ) . $slotDiff;
1486 }
1487
1488 return $this->localiseDiff( $slotDiff );
1489 }
1490
1498 protected function getSlotHeader( $headerText ) {
1499 // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
1500 $columnCount = $this->mOldRevisionRecord ? 4 : 2;
1501 $userLang = $this->getLanguage()->getHtmlCode();
1502 return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-header', 'lang' => $userLang ],
1503 Html::element( 'th', [ 'colspan' => $columnCount ], $headerText ) );
1504 }
1505
1512 protected function getSlotError( $errorText ) {
1513 // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
1514 $columnCount = $this->mOldRevisionRecord ? 4 : 2;
1515 $userLang = $this->getLanguage()->getHtmlCode();
1516 return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-error', 'lang' => $userLang ],
1517 Html::rawElement( 'td', [ 'colspan' => $columnCount ], $errorText ) );
1518 }
1519
1533 protected function getDiffBodyCacheKeyParams() {
1534 if ( !$this->mOldid || !$this->mNewid ) {
1535 throw new BadMethodCallException( 'mOldid and mNewid must be set to get diff cache key.' );
1536 }
1537
1538 $params = [
1539 'diff',
1540 self::DIFF_VERSION,
1541 "old-{$this->mOldid}",
1542 "rev-{$this->mNewid}"
1543 ];
1544
1545 $extraKeys = [];
1546 if ( !$this->isSlotDiffRenderer ) {
1547 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1548 $extraKeys = array_merge( $extraKeys, $slotDiffRenderer->getExtraCacheKeys() );
1549 }
1550 }
1551 ksort( $extraKeys );
1552 return array_merge( $params, array_values( $extraKeys ) );
1553 }
1554
1562 public function getExtraCacheKeys() {
1563 // This method is called when the DifferenceEngine is used for a slot diff. We only care
1564 // about special things, not the revision IDs, which are added to the cache key by the
1565 // page-level DifferenceEngine, and which might not have a valid value for this object.
1566 $this->mOldid = 123456789;
1567 $this->mNewid = 987654321;
1568
1569 // This will repeat a bunch of unnecessary key fields for each slot. Not nice but harmless.
1570 $params = $this->getDiffBodyCacheKeyParams();
1571
1572 // Try to get rid of the standard keys to keep the cache key human-readable:
1573 // call the getDiffBodyCacheKeyParams implementation of the base class, and if
1574 // the child class includes the same keys, drop them.
1575 // Uses an obscure PHP feature where static calls to non-static methods are allowed
1576 // as long as we are already in a non-static method of the same class, and the call context
1577 // ($this) will be inherited.
1578 // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
1580 if ( array_slice( $params, 0, count( $standardParams ) ) === $standardParams ) {
1581 $params = array_slice( $params, count( $standardParams ) );
1582 }
1583
1584 return $params;
1585 }
1586
1597 public function setSlotDiffOptions( $options ) {
1598 $validatedOptions = [];
1599 if ( isset( $options['diff-type'] )
1600 && $this->getTextDiffer()->hasFormat( $options['diff-type'] )
1601 ) {
1602 $validatedOptions['diff-type'] = $options['diff-type'];
1603 }
1604 if ( !empty( $options['expand-url'] ) ) {
1605 $validatedOptions['expand-url'] = true;
1606 }
1607 if ( !empty( $options['inline-toggle'] ) ) {
1608 $validatedOptions['inline-toggle'] = true;
1609 }
1610 $this->slotDiffOptions = $validatedOptions;
1611 }
1612
1620 public function setExtraQueryParams( $params ) {
1621 $this->extraQueryParams = $params;
1622 }
1623
1637 public function generateContentDiffBody( Content $old, Content $new ) {
1638 $slotDiffRenderer = $new->getContentHandler()->getSlotDiffRenderer( $this->getContext() );
1639 if (
1640 $slotDiffRenderer instanceof DifferenceEngineSlotDiffRenderer
1641 && $this->isSlotDiffRenderer
1642 ) {
1643 // Oops, we are just about to enter an infinite loop (the slot-level DifferenceEngine
1644 // called a DifferenceEngineSlotDiffRenderer that wraps the same DifferenceEngine class).
1645 // This will happen when a content model has no custom slot diff renderer, it does have
1646 // a custom difference engine, but that does not override this method.
1647 throw new LogicException( get_class( $this ) . ': could not maintain backwards compatibility. '
1648 . 'Please use a SlotDiffRenderer.' );
1649 }
1650 return $slotDiffRenderer->getDiff( $old, $new ) . $this->getDebugString();
1651 }
1652
1665 public function generateTextDiffBody( $otext, $ntext ) {
1666 $slotDiffRenderer = $this->contentHandlerFactory
1667 ->getContentHandler( CONTENT_MODEL_TEXT )
1668 ->getSlotDiffRenderer( $this->getContext() );
1669 if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1670 // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1671 // This is too unlikely to happen to bother handling properly.
1672 throw new LogicException( 'The slot diff renderer for text content should be a '
1673 . 'TextSlotDiffRenderer subclass' );
1674 }
1675 return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1676 }
1677
1684 public static function getEngine() {
1685 $differenceEngine = new self;
1686 $engine = $differenceEngine->getTextDiffer()->getEngineForFormat( 'table' );
1687 if ( $engine === 'external' ) {
1688 return MediaWikiServices::getInstance()->getMainConfig()
1689 ->get( MainConfigNames::ExternalDiffEngine );
1690 } else {
1691 return $engine;
1692 }
1693 }
1694
1703 protected function debug( $generator = "internal" ) {
1704 if ( !$this->enableDebugComment ) {
1705 return '';
1706 }
1707 $data = [ $generator ];
1708 if ( $this->getConfig()->get( MainConfigNames::ShowHostnames ) ) {
1709 $data[] = wfHostname();
1710 }
1711 $data[] = wfTimestamp( TS_DB );
1712
1713 return "<!-- diff generator: " .
1714 implode( " ", array_map( "htmlspecialchars", $data ) ) .
1715 " -->\n";
1716 }
1717
1721 private function getDebugString() {
1722 $engine = self::getEngine();
1723 if ( $engine === 'wikidiff2' ) {
1724 return $this->debug( 'wikidiff2' );
1725 } elseif ( $engine === 'php' ) {
1726 return $this->debug( 'native PHP' );
1727 } else {
1728 return $this->debug( "external $engine" );
1729 }
1730 }
1731
1738 private function localiseDiff( $text ) {
1739 return $this->getTextDiffer()->localize( $this->getTextDiffFormat(), $text );
1740 }
1741
1750 public function localiseLineNumbers( $text ) {
1751 return preg_replace_callback( '/<!--LINE (\d+)-->/',
1752 function ( array $matches ) {
1753 if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
1754 return '';
1755 }
1756 return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
1757 }, $text );
1758 }
1759
1765 public function getMultiNotice() {
1766 // The notice only make sense if we are diffing two saved revisions of the same page.
1767 if (
1768 !$this->mOldRevisionRecord || !$this->mNewRevisionRecord
1769 || !$this->mOldPage || !$this->mNewPage
1770 || !$this->mOldPage->equals( $this->mNewPage )
1771 || $this->mOldRevisionRecord->getId() === null
1772 || $this->mNewRevisionRecord->getId() === null
1773 // (T237709) Deleted revs might have different page IDs
1774 || $this->mNewPage->getArticleID() !== $this->mOldRevisionRecord->getPageId()
1775 || $this->mNewPage->getArticleID() !== $this->mNewRevisionRecord->getPageId()
1776 ) {
1777 return '';
1778 }
1779
1780 if ( $this->mOldRevisionRecord->getTimestamp() > $this->mNewRevisionRecord->getTimestamp() ) {
1781 $oldRevRecord = $this->mNewRevisionRecord; // flip
1782 $newRevRecord = $this->mOldRevisionRecord; // flip
1783 } else { // normal case
1784 $oldRevRecord = $this->mOldRevisionRecord;
1785 $newRevRecord = $this->mNewRevisionRecord;
1786 }
1787
1788 // Don't show the notice if too many rows must be scanned
1789 // @todo show some special message for that case
1790 $nEdits = 0;
1791 $revisionIdList = $this->revisionStore->getRevisionIdsBetween(
1792 $this->mNewPage->getArticleID(),
1793 $oldRevRecord,
1794 $newRevRecord,
1795 1000
1796 );
1797 // only count revisions that are visible
1798 if ( count( $revisionIdList ) > 0 ) {
1799 foreach ( $revisionIdList as $revisionId ) {
1800 $revision = $this->revisionStore->getRevisionById( $revisionId );
1801 if ( $revision->getUser( RevisionRecord::FOR_THIS_USER, $this->getAuthority() ) ) {
1802 $nEdits++;
1803 }
1804 }
1805 }
1806 if ( $nEdits > 0 && $nEdits <= 1000 ) {
1807 // Use an invalid username to get the wiki's default gender (as fallback)
1808 $newRevUserForGender = '[HIDDEN]';
1809 $limit = 100; // use diff-multi-manyusers if too many users
1810 try {
1811 $users = $this->revisionStore->getAuthorsBetween(
1812 $this->mNewPage->getArticleID(),
1813 $oldRevRecord,
1814 $newRevRecord,
1815 null,
1816 $limit
1817 );
1818 $numUsers = count( $users );
1819
1820 $newRevUser = $newRevRecord->getUser( RevisionRecord::RAW );
1821 $newRevUserText = $newRevUser ? $newRevUser->getName() : '';
1822 $newRevUserSafe = $newRevRecord->getUser(
1823 RevisionRecord::FOR_THIS_USER,
1824 $this->getAuthority()
1825 );
1826 $newRevUserForGender = $newRevUserSafe ? $newRevUserSafe->getName() : '[HIDDEN]';
1827 if ( $numUsers == 1 && $users[0]->getName() == $newRevUserText ) {
1828 $numUsers = 0; // special case to say "by the same user" instead of "by one other user"
1829 }
1830 } catch ( InvalidArgumentException $e ) {
1831 $numUsers = 0;
1832 }
1833
1834 return self::intermediateEditsMsg( $nEdits, $numUsers, $limit, $newRevUserForGender );
1835 }
1836
1837 return '';
1838 }
1839
1850 public static function intermediateEditsMsg( $numEdits, $numUsers, $limit, $lastUser = '[HIDDEN]' ) {
1851 if ( $numUsers === 0 ) {
1852 $msg = 'diff-multi-sameuser';
1853 return wfMessage( $msg )
1854 ->numParams( $numEdits, $numUsers )
1855 ->params( $lastUser )
1856 ->parse();
1857 } elseif ( $numUsers > $limit ) {
1858 $msg = 'diff-multi-manyusers';
1859 $numUsers = $limit;
1860 } else {
1861 $msg = 'diff-multi-otherusers';
1862 }
1863
1864 return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1865 }
1866
1871 private function userCanEdit( RevisionRecord $revRecord ) {
1872 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
1873 return false;
1874 }
1875
1876 return true;
1877 }
1878
1888 public function getRevisionHeader( RevisionRecord $rev, $complete = '' ) {
1889 $lang = $this->getLanguage();
1890 $user = $this->getUser();
1891 $revtimestamp = $rev->getTimestamp();
1892 $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
1893 $dateofrev = $lang->userDate( $revtimestamp, $user );
1894 $timeofrev = $lang->userTime( $revtimestamp, $user );
1895
1896 $header = $this->msg(
1897 $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
1898 $timestamp,
1899 $dateofrev,
1900 $timeofrev
1901 );
1902
1903 if ( $complete !== 'complete' ) {
1904 return $header->escaped();
1905 }
1906
1907 $title = $rev->getPageAsLinkTarget();
1908
1909 if ( $this->userCanEdit( $rev ) ) {
1910 $header = $this->linkRenderer->makeKnownLink(
1911 $title,
1912 $header->text(),
1913 [],
1914 [ 'oldid' => $rev->getId() ]
1915 );
1916 $editQuery = [ 'action' => 'edit' ];
1917 if ( !$rev->isCurrent() ) {
1918 $editQuery['oldid'] = $rev->getId();
1919 }
1920
1921 $key = $this->getAuthority()->probablyCan( 'edit', $rev->getPage() ) ? 'editold' : 'viewsourceold';
1922 $msg = $this->msg( $key )->text();
1923 $editLink = $this->linkRenderer->makeKnownLink( $title, $msg, [], $editQuery );
1924 $header .= ' ' . Html::rawElement(
1925 'span',
1926 [ 'class' => 'mw-diff-edit' ],
1927 $editLink
1928 );
1929 } else {
1930 $header = $header->escaped();
1931 }
1932
1933 // Machine readable information
1934 $header .= Html::element( 'span',
1935 [
1936 'class' => 'mw-diff-timestamp',
1937 'data-timestamp' => wfTimestamp( TS_ISO_8601, $revtimestamp ),
1938 ], ''
1939 );
1940
1941 if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1942 return Html::rawElement(
1943 'span',
1944 [ 'class' => Linker::getRevisionDeletedClass( $rev ) ],
1945 $header
1946 );
1947 }
1948
1949 return $header;
1950 }
1951
1964 public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
1965 // shared.css sets diff in interface language/dir, but the actual content
1966 // is often in a different language, mostly the page content language/dir
1967 $header = Html::openElement( 'table', [
1968 'class' => [
1969 'diff',
1970 // The following classes are used here:
1971 // * diff-type-table
1972 // * diff-type-inline
1973 'diff-type-' . $this->getTextDiffFormat(),
1974 // The following classes are used here:
1975 // * diff-contentalign-left
1976 // * diff-contentalign-right
1977 'diff-contentalign-' . $this->getDiffLang()->alignStart(),
1978 // The following classes are used here:
1979 // * diff-editfont-monospace
1980 // * diff-editfont-sans-serif
1981 // * diff-editfont-serif
1982 'diff-editfont-' . $this->userOptionsLookup->getOption(
1983 $this->getUser(),
1984 'editfont'
1985 )
1986 ],
1987 'data-mw' => 'interface',
1988 ] );
1989 $userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
1990
1991 if ( !$diff && !$otitle ) {
1992 $header .= "
1993 <tr class=\"diff-title\" lang=\"{$userLang}\">
1994 <td class=\"diff-ntitle\">{$ntitle}</td>
1995 </tr>";
1996 $multiColspan = 1;
1997 } else {
1998 if ( $diff ) { // Safari/Chrome show broken output if cols not used
1999 $header .= "
2000 <col class=\"diff-marker\" />
2001 <col class=\"diff-content\" />
2002 <col class=\"diff-marker\" />
2003 <col class=\"diff-content\" />";
2004 $colspan = 2;
2005 $multiColspan = 4;
2006 } else {
2007 $colspan = 1;
2008 $multiColspan = 2;
2009 }
2010 if ( $otitle || $ntitle ) {
2011 // FIXME Hardcoding values from TableDiffFormatter.
2012 $deletedClass = 'diff-side-deleted';
2013 $addedClass = 'diff-side-added';
2014 $header .= "
2015 <tr class=\"diff-title\" lang=\"{$userLang}\">
2016 <td colspan=\"$colspan\" class=\"diff-otitle {$deletedClass}\">{$otitle}</td>
2017 <td colspan=\"$colspan\" class=\"diff-ntitle {$addedClass}\">{$ntitle}</td>
2018 </tr>";
2019 }
2020 }
2021
2022 if ( $multi != '' ) {
2023 $header .= "<tr><td colspan=\"{$multiColspan}\" " .
2024 "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
2025 }
2026 if ( $notice != '' ) {
2027 $header .= "<tr><td colspan=\"{$multiColspan}\" " .
2028 "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
2029 }
2030
2031 return $header . $diff . "</table>";
2032 }
2033
2041 public function setContent( Content $oldContent, Content $newContent ) {
2042 $this->mOldContent = $oldContent;
2043 $this->mNewContent = $newContent;
2044
2045 $this->mTextLoaded = 2;
2046 $this->mRevisionsLoaded = true;
2047 $this->isContentOverridden = true;
2048 $this->slotDiffRenderers = null;
2049 }
2050
2056 public function setRevisions(
2057 ?RevisionRecord $oldRevision, RevisionRecord $newRevision
2058 ) {
2059 if ( $oldRevision ) {
2060 $this->mOldRevisionRecord = $oldRevision;
2061 $this->mOldid = $oldRevision->getId();
2062 $this->mOldPage = Title::newFromLinkTarget( $oldRevision->getPageAsLinkTarget() );
2063 // This method is meant for edit diffs and such so there is no reason to provide a
2064 // revision that's not readable to the user, but check it just in case.
2065 $this->mOldContent = $oldRevision->getContent( SlotRecord::MAIN,
2066 RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
2067 if ( !$this->mOldContent ) {
2068 $this->addRevisionLoadError( 'old' );
2069 }
2070 } else {
2071 $this->mOldPage = null;
2072 $this->mOldRevisionRecord = $this->mOldid = false;
2073 }
2074 $this->mNewRevisionRecord = $newRevision;
2075 $this->mNewid = $newRevision->getId();
2076 $this->mNewPage = Title::newFromLinkTarget( $newRevision->getPageAsLinkTarget() );
2077 $this->mNewContent = $newRevision->getContent( SlotRecord::MAIN,
2078 RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
2079 if ( !$this->mNewContent ) {
2080 $this->addRevisionLoadError( 'new' );
2081 }
2082
2083 $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded = true;
2084 $this->mTextLoaded = $oldRevision ? 2 : 1;
2085 $this->isContentOverridden = false;
2086 $this->slotDiffRenderers = null;
2087 }
2088
2095 public function setTextLanguage( Language $lang ) {
2096 $this->mDiffLang = $lang;
2097 }
2098
2111 public function mapDiffPrevNext( $old, $new ) {
2112 if ( $new === 'prev' ) {
2113 // Show diff between revision $old and the previous one. Get previous one from DB.
2114 $newid = intval( $old );
2115 $oldid = false;
2116 $newRev = $this->revisionStore->getRevisionById( $newid );
2117 if ( $newRev ) {
2118 $oldRev = $this->revisionStore->getPreviousRevision( $newRev );
2119 if ( $oldRev ) {
2120 $oldid = $oldRev->getId();
2121 }
2122 }
2123 } elseif ( $new === 'next' ) {
2124 // Show diff between revision $old and the next one. Get next one from DB.
2125 $oldid = intval( $old );
2126 $newid = false;
2127 $oldRev = $this->revisionStore->getRevisionById( $oldid );
2128 if ( $oldRev ) {
2129 $newRev = $this->revisionStore->getNextRevision( $oldRev );
2130 if ( $newRev ) {
2131 $newid = $newRev->getId();
2132 }
2133 }
2134 } else {
2135 $oldid = intval( $old );
2136 $newid = intval( $new );
2137 }
2138
2139 // @phan-suppress-next-line PhanTypeMismatchReturn getId does not return null here
2140 return [ $oldid, $newid ];
2141 }
2142
2143 private function loadRevisionIds() {
2144 if ( $this->mRevisionsIdsLoaded ) {
2145 return;
2146 }
2147
2148 $this->mRevisionsIdsLoaded = true;
2149
2150 $old = $this->mOldid;
2151 $new = $this->mNewid;
2152
2153 [ $this->mOldid, $this->mNewid ] = self::mapDiffPrevNext( $old, $new );
2154 if ( $new === 'next' && $this->mNewid === false ) {
2155 # if no result, NewId points to the newest old revision. The only newer
2156 # revision is cur, which is "0".
2157 $this->mNewid = 0;
2158 }
2159
2160 $this->hookRunner->onNewDifferenceEngine(
2161 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
2162 $this->getTitle(), $this->mOldid, $this->mNewid, $old, $new );
2163 }
2164
2178 public function loadRevisionData() {
2179 if ( $this->mRevisionsLoaded ) {
2180 return $this->isContentOverridden ||
2181 ( $this->mOldRevisionRecord !== null && $this->mNewRevisionRecord !== null );
2182 }
2183
2184 // Whether it succeeds or fails, we don't want to try again
2185 $this->mRevisionsLoaded = true;
2186
2187 $this->loadRevisionIds();
2188
2189 // Load the new RevisionRecord object
2190 if ( $this->mNewid ) {
2191 $this->mNewRevisionRecord = $this->revisionStore->getRevisionById( $this->mNewid );
2192 } else {
2193 $this->mNewRevisionRecord = $this->revisionStore->getRevisionByTitle( $this->getTitle() );
2194 }
2195
2196 if ( !$this->mNewRevisionRecord instanceof RevisionRecord ) {
2197 return false;
2198 }
2199
2200 // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
2201 $this->mNewid = $this->mNewRevisionRecord->getId();
2202 $this->mNewPage = $this->mNewid ?
2203 Title::newFromLinkTarget( $this->mNewRevisionRecord->getPageAsLinkTarget() ) :
2204 null;
2205
2206 // Load the old RevisionRecord object
2207 $this->mOldRevisionRecord = false;
2208 if ( $this->mOldid ) {
2209 $this->mOldRevisionRecord = $this->revisionStore->getRevisionById( $this->mOldid );
2210 } elseif ( $this->mOldid === 0 ) {
2211 $revRecord = $this->revisionStore->getPreviousRevision( $this->mNewRevisionRecord );
2212 // No previous revision; mark to show as first-version only.
2213 $this->mOldid = $revRecord ? $revRecord->getId() : false;
2214 $this->mOldRevisionRecord = $revRecord ?? false;
2215 } /* elseif ( $this->mOldid === false ) leave mOldRevisionRecord false; */
2216
2217 if ( $this->mOldRevisionRecord === null ) {
2218 return false;
2219 }
2220
2221 if ( $this->mOldRevisionRecord && $this->mOldRevisionRecord->getId() ) {
2222 $this->mOldPage = Title::newFromLinkTarget(
2223 $this->mOldRevisionRecord->getPageAsLinkTarget()
2224 );
2225 } else {
2226 $this->mOldPage = null;
2227 }
2228
2229 // Load tags information for both revisions
2230 $dbr = $this->dbProvider->getReplicaDatabase();
2231 $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
2232 if ( $this->mOldid !== false ) {
2233 $tagIds = $dbr->newSelectQueryBuilder()
2234 ->select( 'ct_tag_id' )
2235 ->from( 'change_tag' )
2236 ->where( [ 'ct_rev_id' => $this->mOldid ] )
2237 ->caller( __METHOD__ )->fetchFieldValues();
2238 $tags = [];
2239 foreach ( $tagIds as $tagId ) {
2240 try {
2241 $tags[] = $changeTagDefStore->getName( (int)$tagId );
2242 } catch ( NameTableAccessException $exception ) {
2243 continue;
2244 }
2245 }
2246 $this->mOldTags = implode( ',', $tags );
2247 } else {
2248 $this->mOldTags = false;
2249 }
2250
2251 $tagIds = $dbr->newSelectQueryBuilder()
2252 ->select( 'ct_tag_id' )
2253 ->from( 'change_tag' )
2254 ->where( [ 'ct_rev_id' => $this->mNewid ] )
2255 ->caller( __METHOD__ )->fetchFieldValues();
2256 $tags = [];
2257 foreach ( $tagIds as $tagId ) {
2258 try {
2259 $tags[] = $changeTagDefStore->getName( (int)$tagId );
2260 } catch ( NameTableAccessException $exception ) {
2261 continue;
2262 }
2263 }
2264 $this->mNewTags = implode( ',', $tags );
2265
2266 return true;
2267 }
2268
2277 public function loadText() {
2278 if ( $this->mTextLoaded == 2 ) {
2279 return $this->loadRevisionData() &&
2280 ( $this->mOldRevisionRecord === false || $this->mOldContent )
2281 && $this->mNewContent;
2282 }
2283
2284 // Whether it succeeds or fails, we don't want to try again
2285 $this->mTextLoaded = 2;
2286
2287 if ( !$this->loadRevisionData() ) {
2288 return false;
2289 }
2290
2291 if ( $this->mOldRevisionRecord ) {
2292 $this->mOldContent = $this->mOldRevisionRecord->getContent(
2293 SlotRecord::MAIN,
2294 RevisionRecord::FOR_THIS_USER,
2295 $this->getAuthority()
2296 );
2297 if ( $this->mOldContent === null ) {
2298 return false;
2299 }
2300 }
2301
2302 $this->mNewContent = $this->mNewRevisionRecord->getContent(
2303 SlotRecord::MAIN,
2304 RevisionRecord::FOR_THIS_USER,
2305 $this->getAuthority()
2306 );
2307 $this->hookRunner->onDifferenceEngineLoadTextAfterNewContentIsLoaded( $this );
2308 if ( $this->mNewContent === null ) {
2309 return false;
2310 }
2311
2312 return true;
2313 }
2314
2320 public function loadNewText() {
2321 if ( $this->mTextLoaded >= 1 ) {
2322 return $this->loadRevisionData();
2323 }
2324
2325 $this->mTextLoaded = 1;
2326
2327 if ( !$this->loadRevisionData() ) {
2328 return false;
2329 }
2330
2331 $this->mNewContent = $this->mNewRevisionRecord->getContent(
2332 SlotRecord::MAIN,
2333 RevisionRecord::FOR_THIS_USER,
2334 $this->getAuthority()
2335 );
2336
2337 $this->hookRunner->onDifferenceEngineAfterLoadNewText( $this );
2338
2339 return true;
2340 }
2341
2347 protected function getTextDiffer() {
2348 if ( $this->textDiffer === null ) {
2349 $this->textDiffer = new ManifoldTextDiffer(
2350 $this->getContext(),
2351 $this->getDiffLang(),
2352 $this->getConfig()->get( MainConfigNames::DiffEngine ),
2353 $this->getConfig()->get( MainConfigNames::ExternalDiffEngine ),
2354 $this->getConfig()->get( MainConfigNames::Wikidiff2Options )
2355 );
2356 }
2357 return $this->textDiffer;
2358 }
2359
2366 public function getSupportedFormats() {
2367 return $this->getTextDiffer()->getFormats();
2368 }
2369
2376 public function getTextDiffFormat() {
2377 return $this->slotDiffOptions['diff-type'] ?? 'table';
2378 }
2379
2380}
const NS_SPECIAL
Definition Defines.php:54
const CONTENT_MODEL_TEXT
Definition Defines.php:225
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:57
Base class for language-specific code.
Definition Language.php:79
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:156
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.
Represents a "user group membership" – a specific instance of a user belonging to a 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