MediaWiki master
DifferenceEngine.php
Go to the documentation of this file.
1<?php
51
75
77
84 private const DIFF_VERSION = '1.41';
85
92 protected $mOldid;
93
100 protected $mNewid;
101
112 private $mOldRevisionRecord;
113
122 private $mNewRevisionRecord;
123
128 protected $mOldPage;
129
134 protected $mNewPage;
135
140 private $mOldTags;
141
146 private $mNewTags;
147
153 private $mOldContent;
154
160 private $mNewContent;
161
163 protected $mDiffLang;
164
166 private $mRevisionsIdsLoaded = false;
167
169 protected $mRevisionsLoaded = false;
170
172 protected $mTextLoaded = 0;
173
182 protected $isContentOverridden = false;
183
185 protected $mCacheHit = false;
186
188 private $cacheHitKey = null;
189
196 public $enableDebugComment = false;
197
201 protected $mReducedLineNumbers = false;
202
204 protected $mMarkPatrolledLink = null;
205
207 protected $unhide = false;
208
210 protected $mRefreshCache = false;
211
213 protected $slotDiffRenderers = null;
214
221 protected $isSlotDiffRenderer = false;
222
227 private $slotDiffOptions = [];
228
232 private $extraQueryParams = [];
233
235 private $textDiffer;
236
238 private IContentHandlerFactory $contentHandlerFactory;
239 private RevisionStore $revisionStore;
240 private ArchivedRevisionLookup $archivedRevisionLookup;
241 private HookRunner $hookRunner;
242 private WikiPageFactory $wikiPageFactory;
243 private UserOptionsLookup $userOptionsLookup;
244 private CommentFormatter $commentFormatter;
245 private IConnectionProvider $dbProvider;
246 private UserGroupManager $userGroupManager;
247 private UserEditTracker $userEditTracker;
248 private UserIdentityUtils $userIdentityUtils;
249
251 private $revisionLoadErrors = [];
252
261 public function __construct( $context = null, $old = 0, $new = 0, $rcid = 0,
262 $refreshCache = false, $unhide = false
263 ) {
264 if ( $context instanceof IContextSource ) {
265 $this->setContext( $context );
266 }
267
268 wfDebug( "DifferenceEngine old '$old' new '$new' rcid '$rcid'" );
269
270 $this->mOldid = $old;
271 $this->mNewid = $new;
272 $this->mRefreshCache = $refreshCache;
273 $this->unhide = $unhide;
274
275 $services = MediaWikiServices::getInstance();
276 $this->linkRenderer = $services->getLinkRenderer();
277 $this->contentHandlerFactory = $services->getContentHandlerFactory();
278 $this->revisionStore = $services->getRevisionStore();
279 $this->archivedRevisionLookup = $services->getArchivedRevisionLookup();
280 $this->hookRunner = new HookRunner( $services->getHookContainer() );
281 $this->wikiPageFactory = $services->getWikiPageFactory();
282 $this->userOptionsLookup = $services->getUserOptionsLookup();
283 $this->commentFormatter = $services->getCommentFormatter();
284 $this->dbProvider = $services->getDBLoadBalancerFactory();
285 $this->userGroupManager = $services->getUserGroupManager();
286 $this->userEditTracker = $services->getUserEditTracker();
287 $this->userIdentityUtils = $services->getUserIdentityUtils();
288 }
289
295 protected function getSlotDiffRenderers() {
296 if ( $this->isSlotDiffRenderer ) {
297 throw new LogicException( __METHOD__ . ' called in slot diff renderer mode' );
298 }
299
300 if ( $this->slotDiffRenderers === null ) {
301 if ( !$this->loadRevisionData() ) {
302 return [];
303 }
304
305 $slotContents = $this->getSlotContents();
306 $this->slotDiffRenderers = [];
307 foreach ( $slotContents as $role => $contents ) {
308 if ( $contents['new'] && $contents['old']
309 && $contents['new']->equals( $contents['old'] )
310 ) {
311 // Do not produce a diff of identical content
312 continue;
313 }
314 $handler = ( $contents['new'] ?: $contents['old'] )->getContentHandler();
315 $this->slotDiffRenderers[$role] = $handler->getSlotDiffRenderer(
316 $this->getContext(),
317 $this->slotDiffOptions + [
318 'contentLanguage' => $this->getDiffLang()->getCode(),
319 'textDiffer' => $this->getTextDiffer()
320 ]
321 );
322 }
323 }
324
325 return $this->slotDiffRenderers;
326 }
327
334 public function markAsSlotDiffRenderer() {
335 $this->isSlotDiffRenderer = true;
336 }
337
343 protected function getSlotContents() {
344 if ( $this->isContentOverridden ) {
345 return [
346 SlotRecord::MAIN => [ 'old' => $this->mOldContent, 'new' => $this->mNewContent ]
347 ];
348 } elseif ( !$this->loadRevisionData() ) {
349 return [];
350 }
351
352 $newSlots = $this->mNewRevisionRecord->getPrimarySlots()->getSlots();
353 $oldSlots = $this->mOldRevisionRecord ?
354 $this->mOldRevisionRecord->getPrimarySlots()->getSlots() :
355 [];
356 // The order here will determine the visual order of the diff. The current logic is
357 // slots of the new revision first in natural order, then deleted ones. This is ad hoc
358 // and should not be relied on - in the future we may want the ordering to depend
359 // on the page type.
360 $roles = array_keys( array_merge( $newSlots, $oldSlots ) );
361
362 $slots = [];
363 foreach ( $roles as $role ) {
364 $slots[$role] = [
365 'old' => $this->loadSingleSlot(
366 $oldSlots[$role] ?? null,
367 'old'
368 ),
369 'new' => $this->loadSingleSlot(
370 $newSlots[$role] ?? null,
371 'new'
372 )
373 ];
374 }
375 // move main slot to front
376 if ( isset( $slots[SlotRecord::MAIN] ) ) {
377 $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
378 }
379 return $slots;
380 }
381
389 private function loadSingleSlot( ?SlotRecord $slot, string $which ) {
390 if ( !$slot ) {
391 return null;
392 }
393 try {
394 return $slot->getContent();
395 } catch ( BadRevisionException $e ) {
396 $this->addRevisionLoadError( $which );
397 return null;
398 }
399 }
400
406 private function addRevisionLoadError( $which ) {
407 $this->revisionLoadErrors[] = $this->msg( $which === 'new'
408 ? 'difference-bad-new-revision' : 'difference-bad-old-revision'
409 );
410 }
411
418 public function getRevisionLoadErrors() {
419 return $this->revisionLoadErrors;
420 }
421
426 private function hasNewRevisionLoadError() {
427 foreach ( $this->revisionLoadErrors as $error ) {
428 if ( $error->getKey() === 'difference-bad-new-revision' ) {
429 return true;
430 }
431 }
432 return false;
433 }
434
436 public function getTitle() {
437 // T202454 avoid errors when there is no title
438 return parent::getTitle() ?: Title::makeTitle( NS_SPECIAL, 'BadTitle/DifferenceEngine' );
439 }
440
447 public function setReducedLineNumbers( $value = true ) {
448 $this->mReducedLineNumbers = $value;
449 }
450
456 public function getDiffLang() {
457 # Default language in which the diff text is written.
458 $this->mDiffLang ??= $this->getDefaultLanguage();
459 return $this->mDiffLang;
460 }
461
468 protected function getDefaultLanguage() {
469 return $this->getTitle()->getPageLanguage();
470 }
471
475 public function wasCacheHit() {
476 return $this->mCacheHit;
477 }
478
486 public function getOldid() {
487 $this->loadRevisionIds();
488
489 return $this->mOldid;
490 }
491
498 public function getNewid() {
499 $this->loadRevisionIds();
500
501 return $this->mNewid;
502 }
503
510 public function getOldRevision() {
511 return $this->mOldRevisionRecord ?: null;
512 }
513
519 public function getNewRevision() {
520 return $this->mNewRevisionRecord;
521 }
522
531 public function deletedLink( $id ) {
532 if ( $this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
533 $revRecord = $this->archivedRevisionLookup->getArchivedRevisionRecord( null, $id );
534 if ( $revRecord ) {
535 $title = Title::newFromPageIdentity( $revRecord->getPage() );
536
537 return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( [
538 'target' => $title->getPrefixedText(),
539 'timestamp' => $revRecord->getTimestamp()
540 ] );
541 }
542 }
543
544 return false;
545 }
546
554 public function deletedIdMarker( $id ) {
555 $link = $this->deletedLink( $id );
556 if ( $link ) {
557 return "[$link $id]";
558 } else {
559 return (string)$id;
560 }
561 }
562
563 private function showMissingRevision() {
564 $out = $this->getOutput();
565
566 $missing = [];
567 if ( $this->mOldid && ( !$this->mOldRevisionRecord || !$this->mOldContent ) ) {
568 $missing[] = $this->deletedIdMarker( $this->mOldid );
569 }
570 if ( !$this->mNewRevisionRecord || !$this->mNewContent ) {
571 $missing[] = $this->deletedIdMarker( $this->mNewid );
572 }
573
574 $out->setPageTitleMsg( $this->msg( 'errorpagetitle' ) );
575 $msg = $this->msg( 'difference-missing-revision' )
576 ->params( $this->getLanguage()->listToText( $missing ) )
577 ->numParams( count( $missing ) )
578 ->parseAsBlock();
579 $out->addHTML( $msg );
580 }
581
587 public function hasDeletedRevision() {
588 $this->loadRevisionData();
589 return (
590 $this->mNewRevisionRecord &&
591 $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
592 ) ||
593 (
594 $this->mOldRevisionRecord &&
595 $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
596 );
597 }
598
605 public function getPermissionErrors( Authority $performer ) {
606 $this->loadRevisionData();
607 $permStatus = PermissionStatus::newEmpty();
608 if ( $this->mNewPage ) {
609 $performer->authorizeRead( 'read', $this->mNewPage, $permStatus );
610 }
611 if ( $this->mOldPage ) {
612 $performer->authorizeRead( 'read', $this->mOldPage, $permStatus );
613 }
614 return $permStatus->toLegacyErrorArray();
615 }
616
622 public function hasSuppressedRevision() {
623 return $this->hasDeletedRevision() && (
624 ( $this->mOldRevisionRecord &&
625 $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) ||
626 ( $this->mNewRevisionRecord &&
627 $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) )
628 );
629 }
630
637 private function getUserEditCount( $user ): string {
638 $editCount = $this->userEditTracker->getUserEditCount( $user );
639 if ( $editCount === null ) {
640 return '';
641 }
642
643 return Html::rawElement( 'div', [
644 'class' => 'mw-diff-usereditcount',
645 ],
646 $this->msg(
647 'diff-user-edits',
648 $this->getLanguage()->formatNum( $editCount )
649 )->parse()
650 );
651 }
652
659 private function getUserRoles( UserIdentity $user ) {
660 if ( !$this->userIdentityUtils->isNamed( $user ) ) {
661 return '';
662 }
663 $userGroups = $this->userGroupManager->getUserGroups( $user );
664 $userGroupLinks = [];
665 foreach ( $userGroups as $group ) {
666 $userGroupLinks[] = UserGroupMembership::getLinkHTML( $group, $this->getContext() );
667 }
668 return Html::rawElement( 'div', [
669 'class' => 'mw-diff-userroles',
670 ], $this->getLanguage()->commaList( $userGroupLinks ) );
671 }
672
679 private function getUserMetaData( ?UserIdentity $user ) {
680 if ( !$user ) {
681 return '';
682 }
683 return Html::rawElement( 'div', [
684 'class' => 'mw-diff-usermetadata',
685 ], $this->getUserRoles( $user ) . $this->getUserEditCount( $user ) );
686 }
687
699 public function isUserAllowedToSeeRevisions( Authority $performer ) {
700 $this->loadRevisionData();
701
702 if ( $this->mOldRevisionRecord && !$this->mOldRevisionRecord->userCan(
703 RevisionRecord::DELETED_TEXT,
704 $performer
705 ) ) {
706 return false;
707 }
708
709 // $this->mNewRev will only be falsy if a loading error occurred
710 // (in which case the user is allowed to see).
711 return !$this->mNewRevisionRecord || $this->mNewRevisionRecord->userCan(
712 RevisionRecord::DELETED_TEXT,
713 $performer
714 );
715 }
716
724 public function shouldBeHiddenFromUser( Authority $performer ) {
725 return $this->hasDeletedRevision() && ( !$this->unhide ||
726 !$this->isUserAllowedToSeeRevisions( $performer ) );
727 }
728
732 public function showDiffPage( $diffOnly = false ) {
733 # Allow frames except in certain special cases
734 $out = $this->getOutput();
735 $out->setPreventClickjacking( false );
736 $out->setRobotPolicy( 'noindex,nofollow' );
737
738 // Allow extensions to add any extra output here
739 $this->hookRunner->onDifferenceEngineShowDiffPage( $out );
740
741 if ( !$this->loadRevisionData() ) {
742 if ( $this->hookRunner->onDifferenceEngineShowDiffPageMaybeShowMissingRevision( $this ) ) {
743 $this->showMissingRevision();
744 }
745 return;
746 }
747
748 $user = $this->getUser();
749 $permErrors = $this->getPermissionErrors( $this->getAuthority() );
750 if ( $permErrors ) {
751 throw new PermissionsError( 'read', $permErrors );
752 }
753
754 $rollback = '';
755
756 $query = $this->extraQueryParams;
757 # Carry over 'diffonly' param via navigation links
758 if ( $diffOnly != MediaWikiServices::getInstance()
759 ->getUserOptionsLookup()->getBoolOption( $user, 'diffonly' )
760 ) {
761 $query['diffonly'] = $diffOnly;
762 }
763 # Cascade unhide param in links for easy deletion browsing
764 if ( $this->unhide ) {
765 $query['unhide'] = 1;
766 }
767
768 # Check if one of the revisions is deleted/suppressed
769 $deleted = $this->hasDeletedRevision();
770 $suppressed = $this->hasSuppressedRevision();
771 $allowed = $this->isUserAllowedToSeeRevisions( $this->getAuthority() );
772
773 $revisionTools = [];
774 $breadCrumbs = '';
775
776 # mOldRevisionRecord is false if the difference engine is called with a "vague" query for
777 # a diff between a version V and its previous version V' AND the version V
778 # is the first version of that article. In that case, V' does not exist.
779 if ( $this->mOldRevisionRecord === false ) {
780 if ( $this->mNewPage ) {
781 $out->setPageTitleMsg(
782 $this->msg( 'difference-title' )->plaintextParams( $this->mNewPage->getPrefixedText() )
783 );
784 }
785 $samePage = true;
786 $oldHeader = '';
787 // Allow extensions to change the $oldHeader variable
788 $this->hookRunner->onDifferenceEngineOldHeaderNoOldRev( $oldHeader );
789 } else {
790 $this->hookRunner->onDifferenceEngineViewHeader( $this );
791
792 if ( !$this->mOldPage || !$this->mNewPage ) {
793 // XXX say something to the user?
794 $samePage = false;
795 } elseif ( $this->mNewPage->equals( $this->mOldPage ) ) {
796 $out->setPageTitleMsg(
797 $this->msg( 'difference-title' )->plaintextParams( $this->mNewPage->getPrefixedText() )
798 );
799 $samePage = true;
800 } else {
801 $out->setPageTitleMsg( $this->msg( 'difference-title-multipage' )->plaintextParams(
802 $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) );
803 $out->addSubtitle( $this->msg( 'difference-multipage' ) );
804 $samePage = false;
805 }
806
807 if ( $samePage && $this->mNewPage &&
808 $this->getAuthority()->probablyCan( 'edit', $this->mNewPage )
809 ) {
810 if ( $this->mNewRevisionRecord->isCurrent() &&
811 $this->getAuthority()->probablyCan( 'rollback', $this->mNewPage )
812 ) {
813 $rollbackLink = Linker::generateRollback(
814 $this->mNewRevisionRecord,
815 $this->getContext(),
816 [ 'noBrackets' ]
817 );
818 if ( $rollbackLink ) {
819 $out->setPreventClickjacking( true );
820 $rollback = "\u{00A0}\u{00A0}\u{00A0}" . $rollbackLink;
821 }
822 }
823
824 if ( $this->userCanEdit( $this->mOldRevisionRecord ) &&
825 $this->userCanEdit( $this->mNewRevisionRecord )
826 ) {
827 $undoLink = $this->linkRenderer->makeKnownLink(
828 $this->mNewPage,
829 $this->msg( 'editundo' )->text(),
830 [ 'title' => Linker::titleAttrib( 'undo' ) ],
831 [
832 'action' => 'edit',
833 'undoafter' => $this->mOldid,
834 'undo' => $this->mNewid
835 ]
836 );
837 $revisionTools['mw-diff-undo'] = $undoLink;
838 }
839 }
840 # Make "previous revision link"
841 $hasPrevious = $samePage && $this->mOldPage &&
842 $this->revisionStore->getPreviousRevision( $this->mOldRevisionRecord );
843 if ( $hasPrevious ) {
844 $prevlinkQuery = [ 'diff' => 'prev', 'oldid' => $this->mOldid ] + $query;
845 $prevlink = $this->linkRenderer->makeKnownLink(
846 $this->mOldPage,
847 $this->msg( 'previousdiff' )->text(),
848 [ 'id' => 'differences-prevlink' ],
849 $prevlinkQuery
850 );
851 $breadCrumbs .= $this->linkRenderer->makeKnownLink(
852 $this->mOldPage,
853 $this->msg( 'previousdiff' )->text(),
854 [
855 'class' => 'mw-diff-revision-history-link-previous'
856 ],
857 $prevlinkQuery
858 );
859 } else {
860 $prevlink = "\u{00A0}";
861 }
862
863 if ( $this->mOldRevisionRecord->isMinor() ) {
864 $oldminor = ChangesList::flag( 'minor' );
865 } else {
866 $oldminor = '';
867 }
868
869 $oldRevRecord = $this->mOldRevisionRecord;
870
871 $ldel = $this->revisionDeleteLink( $oldRevRecord );
872 $oldRevisionHeader = $this->getRevisionHeader( $oldRevRecord, 'complete' );
873 $oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff', $this->getContext() );
874 $oldRevComment = $this->commentFormatter
875 ->formatRevision(
876 $oldRevRecord, $user, !$diffOnly, !$this->unhide, false
877 );
878
879 if ( $oldRevComment === '' ) {
880 $defaultComment = $this->msg( 'changeslist-nocomment' )->escaped();
881 $oldRevComment = "<span class=\"comment mw-comment-none\">$defaultComment</span>";
882 }
883
884 $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' .
885 '<div id="mw-diff-otitle2">' .
886 Linker::revUserTools( $oldRevRecord, !$this->unhide ) .
887 $this->getUserMetaData( $oldRevRecord->getUser() ) .
888 '</div>' .
889 '<div id="mw-diff-otitle3">' . $oldminor . $oldRevComment . $ldel . '</div>' .
890 '<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' .
891 '<div id="mw-diff-otitle4">' . $prevlink . '</div>';
892
893 // Allow extensions to change the $oldHeader variable
894 $this->hookRunner->onDifferenceEngineOldHeader(
895 $this, $oldHeader, $prevlink, $oldminor, $diffOnly, $ldel, $this->unhide );
896 }
897
898 $out->addJsConfigVars( [
899 'wgDiffOldId' => $this->mOldid,
900 'wgDiffNewId' => $this->mNewid,
901 ] );
902
903 # Make "next revision link"
904 # Skip next link on the top revision
905 if ( $samePage && $this->mNewPage && !$this->mNewRevisionRecord->isCurrent() ) {
906 $nextlinkQuery = [ 'diff' => 'next', 'oldid' => $this->mNewid ] + $query;
907 $nextlink = $this->linkRenderer->makeKnownLink(
908 $this->mNewPage,
909 $this->msg( 'nextdiff' )->text(),
910 [ 'id' => 'differences-nextlink' ],
911 $nextlinkQuery
912 );
913 $breadCrumbs .= $this->linkRenderer->makeKnownLink(
914 $this->mNewPage,
915 $this->msg( 'nextdiff' )->text(),
916 [
917 'class' => 'mw-diff-revision-history-link-next'
918 ],
919 $nextlinkQuery
920 );
921 } else {
922 $nextlink = "\u{00A0}";
923 }
924
925 if ( $this->mNewRevisionRecord->isMinor() ) {
926 $newminor = ChangesList::flag( 'minor' );
927 } else {
928 $newminor = '';
929 }
930
931 # Handle RevisionDelete links...
932 $rdel = $this->revisionDeleteLink( $this->mNewRevisionRecord );
933
934 # Allow extensions to define their own revision tools
935 $this->hookRunner->onDiffTools(
936 $this->mNewRevisionRecord,
937 $revisionTools,
938 $this->mOldRevisionRecord ?: null,
939 $user
940 );
941
942 $formattedRevisionTools = [];
943 // Put each one in parentheses (poor man's button)
944 foreach ( $revisionTools as $key => $tool ) {
945 $toolClass = is_string( $key ) ? $key : 'mw-diff-tool';
946 $element = Html::rawElement(
947 'span',
948 [ 'class' => $toolClass ],
949 $tool
950 );
951 $formattedRevisionTools[] = $element;
952 }
953
954 $newRevRecord = $this->mNewRevisionRecord;
955
956 $newRevisionHeader = $this->getRevisionHeader( $newRevRecord, 'complete' ) .
957 ' ' . implode( ' ', $formattedRevisionTools );
958 $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff', $this->getContext() );
959 $newRevComment = $this->commentFormatter->formatRevision(
960 $newRevRecord, $user, !$diffOnly, !$this->unhide, false
961 );
962
963 if ( $newRevComment === '' ) {
964 $defaultComment = $this->msg( 'changeslist-nocomment' )->escaped();
965 $newRevComment = "<span class=\"comment mw-comment-none\">$defaultComment</span>";
966 }
967
968 $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' .
969 '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $newRevRecord, !$this->unhide ) .
970 $rollback .
971 $this->getUserMetaData( $newRevRecord->getUser() ) .
972 '</div>' .
973 '<div id="mw-diff-ntitle3">' . $newminor . $newRevComment . $rdel . '</div>' .
974 '<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' .
975 '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
976
977 // Allow extensions to change the $newHeader variable
978 $this->hookRunner->onDifferenceEngineNewHeader( $this, $newHeader,
979 $formattedRevisionTools, $nextlink, $rollback, $newminor, $diffOnly,
980 $rdel, $this->unhide );
981
982 $out->addHTML(
983 Html::rawElement( 'div', [
984 'class' => 'mw-diff-revision-history-links'
985 ], $breadCrumbs )
986 );
987 # If the diff cannot be shown due to a deleted revision, then output
988 # the diff header and links to unhide (if available)...
989 if ( $this->shouldBeHiddenFromUser( $this->getAuthority() ) ) {
990 $this->showDiffStyle();
991 $multi = $this->getMultiNotice();
992 $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
993 if ( !$allowed ) {
994 # Give explanation for why revision is not visible
995 $msg = [ $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff' ];
996 } else {
997 # Give explanation and add a link to view the diff...
998 $query = $this->getRequest()->appendQueryValue( 'unhide', '1' );
999 $msg = [
1000 $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff',
1001 $this->getTitle()->getFullURL( $query )
1002 ];
1003 }
1004 $out->addHTML( Html::warningBox( $this->msg( ...$msg )->parse(), 'plainlinks' ) );
1005 # Otherwise, output a regular diff...
1006 } else {
1007 # Add deletion notice if the user is viewing deleted content
1008 $notice = '';
1009 if ( $deleted ) {
1010 $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
1011 $notice = Html::warningBox( $this->msg( $msg )->parse(), 'plainlinks' );
1012 }
1013
1014 # Add an error if the content can't be loaded
1015 $this->getSlotContents();
1016 foreach ( $this->getRevisionLoadErrors() as $msg ) {
1017 $notice .= Html::warningBox( $msg->parse() );
1018 }
1019
1020 // Check if inline switcher will be needed
1021 if ( $this->getTextDiffer()->hasFormat( 'inline' ) ) {
1022 $out->enableOOUI();
1023 }
1024
1025 $this->showTablePrefixes();
1026 $this->showDiff( $oldHeader, $newHeader, $notice );
1027 if ( !$diffOnly ) {
1028 $this->renderNewRevision();
1029 }
1030 }
1031 }
1032
1036 private function showTablePrefixes() {
1037 $parts = [];
1038 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1039 $parts += $slotDiffRenderer->getTablePrefix( $this->getContext(), $this->mNewPage );
1040 }
1041 ksort( $parts );
1042 if ( count( array_filter( $parts ) ) > 0 ) {
1043 $language = $this->getLanguage();
1044 $attrs = [
1045 'class' => 'mw-diff-table-prefix',
1046 'dir' => $language->getDir(),
1047 'lang' => $language->getCode(),
1048 ];
1049 $this->getOutput()->addHTML(
1050 Html::rawElement( 'div', $attrs, implode( '', $parts ) ) );
1051 }
1052 }
1053
1064 public function markPatrolledLink() {
1065 if ( $this->mMarkPatrolledLink === null ) {
1066 $linkInfo = $this->getMarkPatrolledLinkInfo();
1067 // If false, there is no patrol link needed/allowed
1068 if ( !$linkInfo || !$this->mNewPage ) {
1069 $this->mMarkPatrolledLink = '';
1070 } else {
1071 $this->mMarkPatrolledLink = ' <span class="patrollink" data-mw="interface">[' .
1072 $this->linkRenderer->makeKnownLink(
1073 $this->mNewPage,
1074 $this->msg( 'markaspatrolleddiff' )->text(),
1075 [],
1076 [
1077 'action' => 'markpatrolled',
1078 'rcid' => $linkInfo['rcid'],
1079 ]
1080 ) . ']</span>';
1081 // Allow extensions to change the markpatrolled link
1082 $this->hookRunner->onDifferenceEngineMarkPatrolledLink( $this,
1083 $this->mMarkPatrolledLink, $linkInfo['rcid'] );
1084 }
1085 }
1086 return $this->mMarkPatrolledLink;
1087 }
1088
1096 protected function getMarkPatrolledLinkInfo() {
1097 $user = $this->getUser();
1098 $config = $this->getConfig();
1099
1100 // Prepare a change patrol link, if applicable
1101 if (
1102 // Is patrolling enabled and the user allowed to?
1103 $config->get( MainConfigNames::UseRCPatrol ) &&
1104 $this->mNewPage &&
1105 $this->getAuthority()->probablyCan( 'patrol', $this->mNewPage ) &&
1106 // Only do this if the revision isn't more than 6 hours older
1107 // than the Max RC age (6h because the RC might not be cleaned out regularly)
1108 RecentChange::isInRCLifespan( $this->mNewRevisionRecord->getTimestamp(), 21600 )
1109 ) {
1110 // Look for an unpatrolled change corresponding to this diff
1111 $change = RecentChange::newFromConds(
1112 [
1113 'rc_this_oldid' => $this->mNewid,
1114 'rc_patrolled' => RecentChange::PRC_UNPATROLLED
1115 ],
1116 __METHOD__
1117 );
1118
1119 if ( $change && !$change->getPerformerIdentity()->equals( $user ) ) {
1120 $rcid = $change->getAttribute( 'rc_id' );
1121 } else {
1122 // None found or the page has been created by the current user.
1123 // If the user could patrol this it already would be patrolled
1124 $rcid = 0;
1125 }
1126
1127 // Allow extensions to possibly change the rcid here
1128 // For example the rcid might be set to zero due to the user
1129 // being the same as the performer of the change but an extension
1130 // might still want to show it under certain conditions
1131 $this->hookRunner->onDifferenceEngineMarkPatrolledRCID( $rcid, $this, $change, $user );
1132
1133 // Build the link
1134 if ( $rcid ) {
1135 $this->getOutput()->setPreventClickjacking( true );
1136 if ( $this->getAuthority()->isAllowed( 'writeapi' ) ) {
1137 $this->getOutput()->addModules( 'mediawiki.misc-authed-curate' );
1138 }
1139
1140 return [ 'rcid' => $rcid ];
1141 }
1142 }
1143
1144 // No mark as patrolled link applicable
1145 return false;
1146 }
1147
1153 private function revisionDeleteLink( RevisionRecord $revRecord ) {
1154 $link = Linker::getRevDeleteLink(
1155 $this->getAuthority(),
1156 $revRecord,
1157 $revRecord->getPageAsLinkTarget()
1158 );
1159 if ( $link !== '' ) {
1160 $link = "\u{00A0}\u{00A0}\u{00A0}" . $link . ' ';
1161 }
1162
1163 return $link;
1164 }
1165
1171 public function renderNewRevision() {
1172 if ( $this->isContentOverridden ) {
1173 // The code below only works with a RevisionRecord object. We could construct a
1174 // fake RevisionRecord (here or in setContent), but since this does not seem
1175 // needed at the moment, we'll just fail for now.
1176 throw new LogicException(
1177 __METHOD__
1178 . ' is not supported after calling setContent(). Use setRevisions() instead.'
1179 );
1180 }
1181
1182 $out = $this->getOutput();
1183 $revHeader = $this->getRevisionHeader( $this->mNewRevisionRecord );
1184 # Add "current version as of X" title
1185 $out->addHTML( "<hr class='diff-hr' id='mw-oldid' />
1186 <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
1187 # Page content may be handled by a hooked call instead...
1188 if ( $this->hookRunner->onArticleContentOnDiff( $this, $out ) ) {
1189 $this->loadNewText();
1190 if ( !$this->mNewPage ) {
1191 // New revision is unsaved; bail out.
1192 // TODO in theory rendering the new revision is a meaningful thing to do
1193 // even if it's unsaved, but a lot of untangling is required to do it safely.
1194 return;
1195 }
1196 if ( $this->hasNewRevisionLoadError() ) {
1197 // There was an error loading the new revision
1198 return;
1199 }
1200
1201 $out->setRevisionId( $this->mNewid );
1202 $out->setRevisionIsCurrent( $this->mNewRevisionRecord->isCurrent() );
1203 $out->setRevisionTimestamp( $this->mNewRevisionRecord->getTimestamp() );
1204 $out->setArticleFlag( true );
1205
1206 if ( !$this->hookRunner->onArticleRevisionViewCustom(
1207 $this->mNewRevisionRecord, $this->mNewPage, $this->mOldid, $out )
1208 ) {
1209 // Handled by extension
1210 // NOTE: sync with hooks called in Article::view()
1211 } else {
1212 // Normal page
1213 if ( $this->getTitle()->equals( $this->mNewPage ) ) {
1214 // If the Title stored in the context is the same as the one
1215 // of the new revision, we can use its associated WikiPage
1216 // object.
1217 $wikiPage = $this->getWikiPage();
1218 } else {
1219 // Otherwise we need to create our own WikiPage object
1220 $wikiPage = $this->wikiPageFactory->newFromTitle( $this->mNewPage );
1221 }
1222
1223 $parserOptions = $wikiPage->makeParserOptions( $this->getContext() );
1224 $parserOptions->setRenderReason( 'diff-page' );
1225
1226 $parserOutputAccess = MediaWikiServices::getInstance()->getParserOutputAccess();
1227 $status = $parserOutputAccess->getParserOutput(
1228 $wikiPage,
1229 $parserOptions,
1230 $this->mNewRevisionRecord,
1231 // we already checked
1232 ParserOutputAccess::OPT_NO_AUDIENCE_CHECK |
1233 // Update cascading protection
1234 ParserOutputAccess::OPT_LINKS_UPDATE
1235 );
1236 if ( $status->isOK() ) {
1237 $parserOutput = $status->getValue();
1238 // Allow extensions to change parser output here
1239 if ( $this->hookRunner->onDifferenceEngineRenderRevisionAddParserOutput(
1240 $this, $out, $parserOutput, $wikiPage )
1241 ) {
1242 $out->addParserOutput( $parserOutput, [
1243 'enableSectionEditLinks' => $this->mNewRevisionRecord->isCurrent()
1244 && $this->getAuthority()->probablyCan(
1245 'edit',
1246 $this->mNewRevisionRecord->getPage()
1247 ),
1248 'absoluteURLs' => $this->slotDiffOptions['expand-url'] ?? false
1249 ] );
1250 }
1251 } else {
1252 $out->addHTML(
1253 Html::errorBox(
1254 $out->parseAsInterface(
1255 $status->getWikiText( false, false, $this->getLanguage() )
1256 )
1257 )
1258 );
1259 }
1260 }
1261 }
1262
1263 // Allow extensions to optionally not show the final patrolled link
1264 if ( $this->hookRunner->onDifferenceEngineRenderRevisionShowFinalPatrolLink() ) {
1265 # Add redundant patrol link on bottom...
1266 $out->addHTML( $this->markPatrolledLink() );
1267 }
1268 }
1269
1280 public function showDiff( $otitle, $ntitle, $notice = '' ) {
1281 // Allow extensions to affect the output here
1282 $this->hookRunner->onDifferenceEngineShowDiff( $this );
1283
1284 $diff = $this->getDiff( $otitle, $ntitle, $notice );
1285 if ( $diff === false ) {
1286 $this->showMissingRevision();
1287 return false;
1288 }
1289
1290 $this->showDiffStyle();
1291 if ( $this->slotDiffOptions['expand-url'] ?? false ) {
1292 $diff = Linker::expandLocalLinks( $diff );
1293 }
1294 $this->getOutput()->addHTML( $diff );
1295 return true;
1296 }
1297
1301 public function showDiffStyle() {
1302 if ( !$this->isSlotDiffRenderer ) {
1303 $this->getOutput()->addModules( 'mediawiki.diff' );
1304 $this->getOutput()->addModuleStyles( [
1305 'mediawiki.interface.helpers.styles',
1306 'mediawiki.diff.styles'
1307 ] );
1308 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1309 $slotDiffRenderer->addModules( $this->getOutput() );
1310 }
1311 }
1312 }
1313
1323 public function getDiff( $otitle, $ntitle, $notice = '' ) {
1324 $body = $this->getDiffBody();
1325 if ( $body === false ) {
1326 return false;
1327 }
1328
1329 $multi = $this->getMultiNotice();
1330 // Display a message when the diff is empty
1331 if ( $body === '' ) {
1332 $notice .= '<div class="mw-diff-empty">' .
1333 $this->msg( 'diff-empty' )->parse() .
1334 "</div>\n";
1335 }
1336
1337 if ( $this->cacheHitKey !== null ) {
1338 $body .= "\n<!-- diff cache key " . htmlspecialchars( $this->cacheHitKey ) . " -->\n";
1339 }
1340
1341 return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
1342 }
1343
1349 public function getDiffBody() {
1350 $this->mCacheHit = true;
1351 // Check if the diff should be hidden from this user
1352 if ( !$this->isContentOverridden ) {
1353 if ( !$this->loadRevisionData() ) {
1354 return false;
1355 } elseif ( $this->mOldRevisionRecord &&
1356 !$this->mOldRevisionRecord->userCan(
1357 RevisionRecord::DELETED_TEXT,
1358 $this->getAuthority()
1359 )
1360 ) {
1361 return false;
1362 } elseif ( $this->mNewRevisionRecord &&
1363 !$this->mNewRevisionRecord->userCan(
1364 RevisionRecord::DELETED_TEXT,
1365 $this->getAuthority()
1366 ) ) {
1367 return false;
1368 }
1369 // Short-circuit
1370 if ( $this->mOldRevisionRecord === false || (
1371 $this->mOldRevisionRecord &&
1372 $this->mNewRevisionRecord &&
1373 $this->mOldRevisionRecord->getId() &&
1374 $this->mOldRevisionRecord->getId() == $this->mNewRevisionRecord->getId()
1375 ) ) {
1376 if ( $this->hookRunner->onDifferenceEngineShowEmptyOldContent( $this ) ) {
1377 return '';
1378 }
1379 }
1380 }
1381
1382 // Cacheable?
1383 $key = false;
1384 $services = MediaWikiServices::getInstance();
1385 $cache = $services->getMainWANObjectCache();
1386 $stats = $services->getStatsdDataFactory();
1387 if ( $this->mOldid && $this->mNewid ) {
1388 $key = $cache->makeKey( ...$this->getDiffBodyCacheKeyParams() );
1389
1390 // Try cache
1391 if ( !$this->mRefreshCache ) {
1392 $difftext = $cache->get( $key );
1393 if ( is_string( $difftext ) ) {
1394 $stats->updateCount( 'diff_cache.hit', 1 );
1395 $difftext = $this->localiseDiff( $difftext );
1396 $this->cacheHitKey = $key;
1397 return $difftext;
1398 }
1399 } // don't try to load but save the result
1400 }
1401 $this->mCacheHit = false;
1402 $this->cacheHitKey = null;
1403
1404 // Loadtext is permission safe, this just clears out the diff
1405 if ( !$this->loadText() ) {
1406 return false;
1407 }
1408
1409 $difftext = '';
1410 // We've checked for revdelete at the beginning of this method; it's OK to ignore
1411 // read permissions here.
1412 $slotContents = $this->getSlotContents();
1413 foreach ( $this->getSlotDiffRenderers() as $role => $slotDiffRenderer ) {
1414 try {
1415 $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role]['old'],
1416 $slotContents[$role]['new'] );
1417 } catch ( IncompatibleDiffTypesException $e ) {
1418 $slotDiff = $this->getSlotError( $e->getMessageObject()->parse() );
1419 }
1420 if ( $slotDiff && $role !== SlotRecord::MAIN ) {
1421 // FIXME: ask SlotRoleHandler::getSlotNameMessage
1422 $slotTitle = $role;
1423 $difftext .= $this->getSlotHeader( $slotTitle );
1424 }
1425 $difftext .= $slotDiff;
1426 }
1427
1428 // Save to cache for 7 days
1429 if ( !$this->hookRunner->onAbortDiffCache( $this ) ) {
1430 $stats->updateCount( 'diff_cache.uncacheable', 1 );
1431 } elseif ( $key !== false ) {
1432 $stats->updateCount( 'diff_cache.miss', 1 );
1433 $cache->set( $key, $difftext, 7 * 86400 );
1434 } else {
1435 $stats->updateCount( 'diff_cache.uncacheable', 1 );
1436 }
1437 // localise line numbers and title attribute text
1438 $difftext = $this->localiseDiff( $difftext );
1439
1440 return $difftext;
1441 }
1442
1449 public function getDiffBodyForRole( $role ) {
1450 $diffRenderers = $this->getSlotDiffRenderers();
1451 if ( !isset( $diffRenderers[$role] ) ) {
1452 return false;
1453 }
1454
1455 $slotContents = $this->getSlotContents();
1456 try {
1457 $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role]['old'],
1458 $slotContents[$role]['new'] );
1459 } catch ( IncompatibleDiffTypesException $e ) {
1460 $slotDiff = $this->getSlotError( $e->getMessageObject()->parse() );
1461 }
1462 if ( $slotDiff === '' ) {
1463 return false;
1464 }
1465
1466 if ( $role !== SlotRecord::MAIN ) {
1467 // TODO use human-readable role name at least
1468 $slotTitle = $role;
1469 $slotDiff = $this->getSlotHeader( $slotTitle ) . $slotDiff;
1470 }
1471
1472 return $this->localiseDiff( $slotDiff );
1473 }
1474
1482 protected function getSlotHeader( $headerText ) {
1483 // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
1484 $columnCount = $this->mOldRevisionRecord ? 4 : 2;
1485 $userLang = $this->getLanguage()->getHtmlCode();
1486 return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-header', 'lang' => $userLang ],
1487 Html::element( 'th', [ 'colspan' => $columnCount ], $headerText ) );
1488 }
1489
1496 protected function getSlotError( $errorText ) {
1497 // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
1498 $columnCount = $this->mOldRevisionRecord ? 4 : 2;
1499 $userLang = $this->getLanguage()->getHtmlCode();
1500 return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-error', 'lang' => $userLang ],
1501 Html::rawElement( 'td', [ 'colspan' => $columnCount ], $errorText ) );
1502 }
1503
1517 protected function getDiffBodyCacheKeyParams() {
1518 if ( !$this->mOldid || !$this->mNewid ) {
1519 throw new BadMethodCallException( 'mOldid and mNewid must be set to get diff cache key.' );
1520 }
1521
1522 $params = [
1523 'diff',
1524 self::DIFF_VERSION,
1525 "old-{$this->mOldid}",
1526 "rev-{$this->mNewid}"
1527 ];
1528
1529 $extraKeys = [];
1530 if ( !$this->isSlotDiffRenderer ) {
1531 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1532 $extraKeys = array_merge( $extraKeys, $slotDiffRenderer->getExtraCacheKeys() );
1533 }
1534 }
1535 ksort( $extraKeys );
1536 return array_merge( $params, array_values( $extraKeys ) );
1537 }
1538
1546 public function getExtraCacheKeys() {
1547 // This method is called when the DifferenceEngine is used for a slot diff. We only care
1548 // about special things, not the revision IDs, which are added to the cache key by the
1549 // page-level DifferenceEngine, and which might not have a valid value for this object.
1550 $this->mOldid = 123456789;
1551 $this->mNewid = 987654321;
1552
1553 // This will repeat a bunch of unnecessary key fields for each slot. Not nice but harmless.
1554 $params = $this->getDiffBodyCacheKeyParams();
1555
1556 // Try to get rid of the standard keys to keep the cache key human-readable:
1557 // call the getDiffBodyCacheKeyParams implementation of the base class, and if
1558 // the child class includes the same keys, drop them.
1559 // Uses an obscure PHP feature where static calls to non-static methods are allowed
1560 // as long as we are already in a non-static method of the same class, and the call context
1561 // ($this) will be inherited.
1562 // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
1564 if ( array_slice( $params, 0, count( $standardParams ) ) === $standardParams ) {
1565 $params = array_slice( $params, count( $standardParams ) );
1566 }
1567
1568 return $params;
1569 }
1570
1581 public function setSlotDiffOptions( $options ) {
1582 $validatedOptions = [];
1583 if ( isset( $options['diff-type'] )
1584 && $this->getTextDiffer()->hasFormat( $options['diff-type'] )
1585 ) {
1586 $validatedOptions['diff-type'] = $options['diff-type'];
1587 }
1588 if ( !empty( $options['expand-url'] ) ) {
1589 $validatedOptions['expand-url'] = true;
1590 }
1591 if ( !empty( $options['inline-toggle'] ) ) {
1592 $validatedOptions['inline-toggle'] = true;
1593 }
1594 $this->slotDiffOptions = $validatedOptions;
1595 }
1596
1604 public function setExtraQueryParams( $params ) {
1605 $this->extraQueryParams = $params;
1606 }
1607
1621 public function generateContentDiffBody( Content $old, Content $new ) {
1622 $slotDiffRenderer = $new->getContentHandler()->getSlotDiffRenderer( $this->getContext() );
1623 if (
1624 $slotDiffRenderer instanceof DifferenceEngineSlotDiffRenderer
1625 && $this->isSlotDiffRenderer
1626 ) {
1627 // Oops, we are just about to enter an infinite loop (the slot-level DifferenceEngine
1628 // called a DifferenceEngineSlotDiffRenderer that wraps the same DifferenceEngine class).
1629 // This will happen when a content model has no custom slot diff renderer, it does have
1630 // a custom difference engine, but that does not override this method.
1631 throw new Exception( get_class( $this ) . ': could not maintain backwards compatibility. '
1632 . 'Please use a SlotDiffRenderer.' );
1633 }
1634 return $slotDiffRenderer->getDiff( $old, $new ) . $this->getDebugString();
1635 }
1636
1649 public function generateTextDiffBody( $otext, $ntext ) {
1650 $slotDiffRenderer = $this->contentHandlerFactory
1651 ->getContentHandler( CONTENT_MODEL_TEXT )
1652 ->getSlotDiffRenderer( $this->getContext() );
1653 if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1654 // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1655 // This is too unlikely to happen to bother handling properly.
1656 throw new Exception( 'The slot diff renderer for text content should be a '
1657 . 'TextSlotDiffRenderer subclass' );
1658 }
1659 return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1660 }
1661
1668 public static function getEngine() {
1669 $differenceEngine = new self;
1670 $engine = $differenceEngine->getTextDiffer()->getEngineForFormat( 'table' );
1671 if ( $engine === 'external' ) {
1672 return MediaWikiServices::getInstance()->getMainConfig()
1673 ->get( MainConfigNames::ExternalDiffEngine );
1674 } else {
1675 return $engine;
1676 }
1677 }
1678
1687 protected function debug( $generator = "internal" ) {
1688 if ( !$this->enableDebugComment ) {
1689 return '';
1690 }
1691 $data = [ $generator ];
1692 if ( $this->getConfig()->get( MainConfigNames::ShowHostnames ) ) {
1693 $data[] = wfHostname();
1694 }
1695 $data[] = wfTimestamp( TS_DB );
1696
1697 return "<!-- diff generator: " .
1698 implode( " ", array_map( "htmlspecialchars", $data ) ) .
1699 " -->\n";
1700 }
1701
1705 private function getDebugString() {
1706 $engine = self::getEngine();
1707 if ( $engine === 'wikidiff2' ) {
1708 return $this->debug( 'wikidiff2' );
1709 } elseif ( $engine === 'php' ) {
1710 return $this->debug( 'native PHP' );
1711 } else {
1712 return $this->debug( "external $engine" );
1713 }
1714 }
1715
1722 private function localiseDiff( $text ) {
1723 return $this->getTextDiffer()->localize( $this->getTextDiffFormat(), $text );
1724 }
1725
1734 public function localiseLineNumbers( $text ) {
1735 return preg_replace_callback( '/<!--LINE (\d+)-->/',
1736 function ( array $matches ) {
1737 if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
1738 return '';
1739 }
1740 return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
1741 }, $text );
1742 }
1743
1749 public function getMultiNotice() {
1750 // The notice only make sense if we are diffing two saved revisions of the same page.
1751 if (
1752 !$this->mOldRevisionRecord || !$this->mNewRevisionRecord
1753 || !$this->mOldPage || !$this->mNewPage
1754 || !$this->mOldPage->equals( $this->mNewPage )
1755 || $this->mOldRevisionRecord->getId() === null
1756 || $this->mNewRevisionRecord->getId() === null
1757 // (T237709) Deleted revs might have different page IDs
1758 || $this->mNewPage->getArticleID() !== $this->mOldRevisionRecord->getPageId()
1759 || $this->mNewPage->getArticleID() !== $this->mNewRevisionRecord->getPageId()
1760 ) {
1761 return '';
1762 }
1763
1764 if ( $this->mOldRevisionRecord->getTimestamp() > $this->mNewRevisionRecord->getTimestamp() ) {
1765 $oldRevRecord = $this->mNewRevisionRecord; // flip
1766 $newRevRecord = $this->mOldRevisionRecord; // flip
1767 } else { // normal case
1768 $oldRevRecord = $this->mOldRevisionRecord;
1769 $newRevRecord = $this->mNewRevisionRecord;
1770 }
1771
1772 // Don't show the notice if too many rows must be scanned
1773 // @todo show some special message for that case
1774 $nEdits = 0;
1775 $revisionIdList = $this->revisionStore->getRevisionIdsBetween(
1776 $this->mNewPage->getArticleID(),
1777 $oldRevRecord,
1778 $newRevRecord,
1779 1000
1780 );
1781 // only count revisions that are visible
1782 if ( count( $revisionIdList ) > 0 ) {
1783 foreach ( $revisionIdList as $revisionId ) {
1784 $revision = $this->revisionStore->getRevisionById( $revisionId );
1785 if ( $revision->getUser( RevisionRecord::FOR_THIS_USER, $this->getAuthority() ) ) {
1786 $nEdits++;
1787 }
1788 }
1789 }
1790 if ( $nEdits > 0 && $nEdits <= 1000 ) {
1791 // Use an invalid username to get the wiki's default gender (as fallback)
1792 $newRevUserForGender = '[HIDDEN]';
1793 $limit = 100; // use diff-multi-manyusers if too many users
1794 try {
1795 $users = $this->revisionStore->getAuthorsBetween(
1796 $this->mNewPage->getArticleID(),
1797 $oldRevRecord,
1798 $newRevRecord,
1799 null,
1800 $limit
1801 );
1802 $numUsers = count( $users );
1803
1804 $newRevUser = $newRevRecord->getUser( RevisionRecord::RAW );
1805 $newRevUserText = $newRevUser ? $newRevUser->getName() : '';
1806 $newRevUserSafe = $newRevRecord->getUser(
1807 RevisionRecord::FOR_THIS_USER,
1808 $this->getAuthority()
1809 );
1810 $newRevUserForGender = $newRevUserSafe ? $newRevUserSafe->getName() : '[HIDDEN]';
1811 if ( $numUsers == 1 && $users[0]->getName() == $newRevUserText ) {
1812 $numUsers = 0; // special case to say "by the same user" instead of "by one other user"
1813 }
1814 } catch ( InvalidArgumentException $e ) {
1815 $numUsers = 0;
1816 }
1817
1818 return self::intermediateEditsMsg( $nEdits, $numUsers, $limit, $newRevUserForGender );
1819 }
1820
1821 return '';
1822 }
1823
1834 public static function intermediateEditsMsg( $numEdits, $numUsers, $limit, $lastUser = '[HIDDEN]' ) {
1835 if ( $numUsers === 0 ) {
1836 $msg = 'diff-multi-sameuser';
1837 return wfMessage( $msg )
1838 ->numParams( $numEdits, $numUsers )
1839 ->params( $lastUser )
1840 ->parse();
1841 } elseif ( $numUsers > $limit ) {
1842 $msg = 'diff-multi-manyusers';
1843 $numUsers = $limit;
1844 } else {
1845 $msg = 'diff-multi-otherusers';
1846 }
1847
1848 return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1849 }
1850
1855 private function userCanEdit( RevisionRecord $revRecord ) {
1856 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
1857 return false;
1858 }
1859
1860 return true;
1861 }
1862
1872 public function getRevisionHeader( RevisionRecord $rev, $complete = '' ) {
1873 $lang = $this->getLanguage();
1874 $user = $this->getUser();
1875 $revtimestamp = $rev->getTimestamp();
1876 $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
1877 $dateofrev = $lang->userDate( $revtimestamp, $user );
1878 $timeofrev = $lang->userTime( $revtimestamp, $user );
1879
1880 $header = $this->msg(
1881 $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
1882 $timestamp,
1883 $dateofrev,
1884 $timeofrev
1885 );
1886
1887 if ( $complete !== 'complete' ) {
1888 return $header->escaped();
1889 }
1890
1891 $title = $rev->getPageAsLinkTarget();
1892
1893 if ( $this->userCanEdit( $rev ) ) {
1894 $header = $this->linkRenderer->makeKnownLink(
1895 $title,
1896 $header->text(),
1897 [],
1898 [ 'oldid' => $rev->getId() ]
1899 );
1900 $editQuery = [ 'action' => 'edit' ];
1901 if ( !$rev->isCurrent() ) {
1902 $editQuery['oldid'] = $rev->getId();
1903 }
1904
1905 $key = $this->getAuthority()->probablyCan( 'edit', $rev->getPage() ) ? 'editold' : 'viewsourceold';
1906 $msg = $this->msg( $key )->text();
1907 $editLink = $this->linkRenderer->makeKnownLink( $title, $msg, [], $editQuery );
1908 $header .= ' ' . Html::rawElement(
1909 'span',
1910 [ 'class' => 'mw-diff-edit' ],
1911 $editLink
1912 );
1913 } else {
1914 $header = $header->escaped();
1915 }
1916
1917 // Machine readable information
1918 $header .= Html::element( 'span',
1919 [
1920 'class' => 'mw-diff-timestamp',
1921 'data-timestamp' => wfTimestamp( TS_ISO_8601, $revtimestamp ),
1922 ], ''
1923 );
1924
1925 if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1926 return Html::rawElement(
1927 'span',
1928 [ 'class' => Linker::getRevisionDeletedClass( $rev ) ],
1929 $header
1930 );
1931 }
1932
1933 return $header;
1934 }
1935
1948 public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
1949 // shared.css sets diff in interface language/dir, but the actual content
1950 // is often in a different language, mostly the page content language/dir
1951 $header = Html::openElement( 'table', [
1952 'class' => [
1953 'diff',
1954 // The following classes are used here:
1955 // * diff-type-table
1956 // * diff-type-inline
1957 'diff-type-' . $this->getTextDiffFormat(),
1958 // The following classes are used here:
1959 // * diff-contentalign-left
1960 // * diff-contentalign-right
1961 'diff-contentalign-' . $this->getDiffLang()->alignStart(),
1962 // The following classes are used here:
1963 // * diff-editfont-monospace
1964 // * diff-editfont-sans-serif
1965 // * diff-editfont-serif
1966 'diff-editfont-' . $this->userOptionsLookup->getOption(
1967 $this->getUser(),
1968 'editfont'
1969 )
1970 ],
1971 'data-mw' => 'interface',
1972 ] );
1973 $userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
1974
1975 if ( !$diff && !$otitle ) {
1976 $header .= "
1977 <tr class=\"diff-title\" lang=\"{$userLang}\">
1978 <td class=\"diff-ntitle\">{$ntitle}</td>
1979 </tr>";
1980 $multiColspan = 1;
1981 } else {
1982 if ( $diff ) { // Safari/Chrome show broken output if cols not used
1983 $header .= "
1984 <col class=\"diff-marker\" />
1985 <col class=\"diff-content\" />
1986 <col class=\"diff-marker\" />
1987 <col class=\"diff-content\" />";
1988 $colspan = 2;
1989 $multiColspan = 4;
1990 } else {
1991 $colspan = 1;
1992 $multiColspan = 2;
1993 }
1994 if ( $otitle || $ntitle ) {
1995 // FIXME Hardcoding values from TableDiffFormatter.
1996 $deletedClass = 'diff-side-deleted';
1997 $addedClass = 'diff-side-added';
1998 $header .= "
1999 <tr class=\"diff-title\" lang=\"{$userLang}\">
2000 <td colspan=\"$colspan\" class=\"diff-otitle {$deletedClass}\">{$otitle}</td>
2001 <td colspan=\"$colspan\" class=\"diff-ntitle {$addedClass}\">{$ntitle}</td>
2002 </tr>";
2003 }
2004 }
2005
2006 if ( $multi != '' ) {
2007 $header .= "<tr><td colspan=\"{$multiColspan}\" " .
2008 "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
2009 }
2010 if ( $notice != '' ) {
2011 $header .= "<tr><td colspan=\"{$multiColspan}\" " .
2012 "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
2013 }
2014
2015 return $header . $diff . "</table>";
2016 }
2017
2025 public function setContent( Content $oldContent, Content $newContent ) {
2026 $this->mOldContent = $oldContent;
2027 $this->mNewContent = $newContent;
2028
2029 $this->mTextLoaded = 2;
2030 $this->mRevisionsLoaded = true;
2031 $this->isContentOverridden = true;
2032 $this->slotDiffRenderers = null;
2033 }
2034
2040 public function setRevisions(
2041 ?RevisionRecord $oldRevision, RevisionRecord $newRevision
2042 ) {
2043 if ( $oldRevision ) {
2044 $this->mOldRevisionRecord = $oldRevision;
2045 $this->mOldid = $oldRevision->getId();
2046 $this->mOldPage = Title::newFromLinkTarget( $oldRevision->getPageAsLinkTarget() );
2047 // This method is meant for edit diffs and such so there is no reason to provide a
2048 // revision that's not readable to the user, but check it just in case.
2049 $this->mOldContent = $oldRevision->getContent( SlotRecord::MAIN,
2050 RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
2051 if ( !$this->mOldContent ) {
2052 $this->addRevisionLoadError( 'old' );
2053 }
2054 } else {
2055 $this->mOldPage = null;
2056 $this->mOldRevisionRecord = $this->mOldid = false;
2057 }
2058 $this->mNewRevisionRecord = $newRevision;
2059 $this->mNewid = $newRevision->getId();
2060 $this->mNewPage = Title::newFromLinkTarget( $newRevision->getPageAsLinkTarget() );
2061 $this->mNewContent = $newRevision->getContent( SlotRecord::MAIN,
2062 RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
2063 if ( !$this->mNewContent ) {
2064 $this->addRevisionLoadError( 'new' );
2065 }
2066
2067 $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded = true;
2068 $this->mTextLoaded = $oldRevision ? 2 : 1;
2069 $this->isContentOverridden = false;
2070 $this->slotDiffRenderers = null;
2071 }
2072
2079 public function setTextLanguage( Language $lang ) {
2080 $this->mDiffLang = $lang;
2081 }
2082
2095 public function mapDiffPrevNext( $old, $new ) {
2096 if ( $new === 'prev' ) {
2097 // Show diff between revision $old and the previous one. Get previous one from DB.
2098 $newid = intval( $old );
2099 $oldid = false;
2100 $newRev = $this->revisionStore->getRevisionById( $newid );
2101 if ( $newRev ) {
2102 $oldRev = $this->revisionStore->getPreviousRevision( $newRev );
2103 if ( $oldRev ) {
2104 $oldid = $oldRev->getId();
2105 }
2106 }
2107 } elseif ( $new === 'next' ) {
2108 // Show diff between revision $old and the next one. Get next one from DB.
2109 $oldid = intval( $old );
2110 $newid = false;
2111 $oldRev = $this->revisionStore->getRevisionById( $oldid );
2112 if ( $oldRev ) {
2113 $newRev = $this->revisionStore->getNextRevision( $oldRev );
2114 if ( $newRev ) {
2115 $newid = $newRev->getId();
2116 }
2117 }
2118 } else {
2119 $oldid = intval( $old );
2120 $newid = intval( $new );
2121 }
2122
2123 // @phan-suppress-next-line PhanTypeMismatchReturn getId does not return null here
2124 return [ $oldid, $newid ];
2125 }
2126
2127 private function loadRevisionIds() {
2128 if ( $this->mRevisionsIdsLoaded ) {
2129 return;
2130 }
2131
2132 $this->mRevisionsIdsLoaded = true;
2133
2134 $old = $this->mOldid;
2135 $new = $this->mNewid;
2136
2137 [ $this->mOldid, $this->mNewid ] = self::mapDiffPrevNext( $old, $new );
2138 if ( $new === 'next' && $this->mNewid === false ) {
2139 # if no result, NewId points to the newest old revision. The only newer
2140 # revision is cur, which is "0".
2141 $this->mNewid = 0;
2142 }
2143
2144 $this->hookRunner->onNewDifferenceEngine(
2145 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
2146 $this->getTitle(), $this->mOldid, $this->mNewid, $old, $new );
2147 }
2148
2162 public function loadRevisionData() {
2163 if ( $this->mRevisionsLoaded ) {
2164 return $this->isContentOverridden ||
2165 ( $this->mOldRevisionRecord !== null && $this->mNewRevisionRecord !== null );
2166 }
2167
2168 // Whether it succeeds or fails, we don't want to try again
2169 $this->mRevisionsLoaded = true;
2170
2171 $this->loadRevisionIds();
2172
2173 // Load the new RevisionRecord object
2174 if ( $this->mNewid ) {
2175 $this->mNewRevisionRecord = $this->revisionStore->getRevisionById( $this->mNewid );
2176 } else {
2177 $this->mNewRevisionRecord = $this->revisionStore->getRevisionByTitle( $this->getTitle() );
2178 }
2179
2180 if ( !$this->mNewRevisionRecord instanceof RevisionRecord ) {
2181 return false;
2182 }
2183
2184 // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
2185 $this->mNewid = $this->mNewRevisionRecord->getId();
2186 $this->mNewPage = $this->mNewid ?
2187 Title::newFromLinkTarget( $this->mNewRevisionRecord->getPageAsLinkTarget() ) :
2188 null;
2189
2190 // Load the old RevisionRecord object
2191 $this->mOldRevisionRecord = false;
2192 if ( $this->mOldid ) {
2193 $this->mOldRevisionRecord = $this->revisionStore->getRevisionById( $this->mOldid );
2194 } elseif ( $this->mOldid === 0 ) {
2195 $revRecord = $this->revisionStore->getPreviousRevision( $this->mNewRevisionRecord );
2196 // No previous revision; mark to show as first-version only.
2197 $this->mOldid = $revRecord ? $revRecord->getId() : false;
2198 $this->mOldRevisionRecord = $revRecord ?? false;
2199 } /* elseif ( $this->mOldid === false ) leave mOldRevisionRecord false; */
2200
2201 if ( $this->mOldRevisionRecord === null ) {
2202 return false;
2203 }
2204
2205 if ( $this->mOldRevisionRecord && $this->mOldRevisionRecord->getId() ) {
2206 $this->mOldPage = Title::newFromLinkTarget(
2207 $this->mOldRevisionRecord->getPageAsLinkTarget()
2208 );
2209 } else {
2210 $this->mOldPage = null;
2211 }
2212
2213 // Load tags information for both revisions
2214 $dbr = $this->dbProvider->getReplicaDatabase();
2215 $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
2216 if ( $this->mOldid !== false ) {
2217 $tagIds = $dbr->newSelectQueryBuilder()
2218 ->select( 'ct_tag_id' )
2219 ->from( 'change_tag' )
2220 ->where( [ 'ct_rev_id' => $this->mOldid ] )
2221 ->caller( __METHOD__ )->fetchFieldValues();
2222 $tags = [];
2223 foreach ( $tagIds as $tagId ) {
2224 try {
2225 $tags[] = $changeTagDefStore->getName( (int)$tagId );
2226 } catch ( NameTableAccessException $exception ) {
2227 continue;
2228 }
2229 }
2230 $this->mOldTags = implode( ',', $tags );
2231 } else {
2232 $this->mOldTags = false;
2233 }
2234
2235 $tagIds = $dbr->newSelectQueryBuilder()
2236 ->select( 'ct_tag_id' )
2237 ->from( 'change_tag' )
2238 ->where( [ 'ct_rev_id' => $this->mNewid ] )
2239 ->caller( __METHOD__ )->fetchFieldValues();
2240 $tags = [];
2241 foreach ( $tagIds as $tagId ) {
2242 try {
2243 $tags[] = $changeTagDefStore->getName( (int)$tagId );
2244 } catch ( NameTableAccessException $exception ) {
2245 continue;
2246 }
2247 }
2248 $this->mNewTags = implode( ',', $tags );
2249
2250 return true;
2251 }
2252
2261 public function loadText() {
2262 if ( $this->mTextLoaded == 2 ) {
2263 return $this->loadRevisionData() &&
2264 ( $this->mOldRevisionRecord === false || $this->mOldContent )
2265 && $this->mNewContent;
2266 }
2267
2268 // Whether it succeeds or fails, we don't want to try again
2269 $this->mTextLoaded = 2;
2270
2271 if ( !$this->loadRevisionData() ) {
2272 return false;
2273 }
2274
2275 if ( $this->mOldRevisionRecord ) {
2276 $this->mOldContent = $this->mOldRevisionRecord->getContent(
2277 SlotRecord::MAIN,
2278 RevisionRecord::FOR_THIS_USER,
2279 $this->getAuthority()
2280 );
2281 if ( $this->mOldContent === null ) {
2282 return false;
2283 }
2284 }
2285
2286 $this->mNewContent = $this->mNewRevisionRecord->getContent(
2287 SlotRecord::MAIN,
2288 RevisionRecord::FOR_THIS_USER,
2289 $this->getAuthority()
2290 );
2291 $this->hookRunner->onDifferenceEngineLoadTextAfterNewContentIsLoaded( $this );
2292 if ( $this->mNewContent === null ) {
2293 return false;
2294 }
2295
2296 return true;
2297 }
2298
2304 public function loadNewText() {
2305 if ( $this->mTextLoaded >= 1 ) {
2306 return $this->loadRevisionData();
2307 }
2308
2309 $this->mTextLoaded = 1;
2310
2311 if ( !$this->loadRevisionData() ) {
2312 return false;
2313 }
2314
2315 $this->mNewContent = $this->mNewRevisionRecord->getContent(
2316 SlotRecord::MAIN,
2317 RevisionRecord::FOR_THIS_USER,
2318 $this->getAuthority()
2319 );
2320
2321 $this->hookRunner->onDifferenceEngineAfterLoadNewText( $this );
2322
2323 return true;
2324 }
2325
2331 protected function getTextDiffer() {
2332 if ( $this->textDiffer === null ) {
2333 $this->textDiffer = new ManifoldTextDiffer(
2334 $this->getContext(),
2335 $this->getDiffLang(),
2336 $this->getConfig()->get( MainConfigNames::DiffEngine ),
2337 $this->getConfig()->get( MainConfigNames::ExternalDiffEngine ),
2338 $this->getConfig()->get( MainConfigNames::Wikidiff2Options )
2339 );
2340 }
2341 return $this->textDiffer;
2342 }
2343
2350 public function getSupportedFormats() {
2351 return $this->getTextDiffer()->getFormats();
2352 }
2353
2360 public function getTextDiffFormat() {
2361 return $this->slotDiffOptions['diff-type'] ?? 'table';
2362 }
2363
2364}
getUser()
getRequest()
getAuthority()
const NS_SPECIAL
Definition Defines.php:53
const CONTENT_MODEL_TEXT
Definition Defines.php:212
trait DeprecationHelper
Trait for issuing warnings on deprecated access.
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.
getContext()
static formatSummaryRow( $tags, $unused, MessageLocalizer $localizer=null)
Creates HTML for the given tags.
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
setContext(IContextSource $context)
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...
Base class for language-specific code.
Definition Language.php:63
getMessageObject()
Return a Message object for this exception.
This is the main service interface for converting single-line comments from various DB comment fields...
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
Class that generates HTML for internal links.
Some internal bits split of from Skin.php.
Definition Linker.php:65
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
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.
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.
getContent( $role, $audience=self::FOR_PUBLIC, Authority $performer=null)
Returns the Content of the given slot of this revision.
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:79
Provides access to user options.
Track info about user edit counts and timings.
Convenience functions for interpreting UserIdentity objects using additional services or config.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:144
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:37
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 the current execution context, such as a web reque...
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