MediaWiki master
DifferenceEngine.php
Go to the documentation of this file.
1<?php
30use MediaWiki\Debug\DeprecationHelper;
62
86
87 use DeprecationHelper;
88
95 private const DIFF_VERSION = '1.41';
96
103 protected $mOldid;
104
111 protected $mNewid;
112
123 private $mOldRevisionRecord;
124
133 private $mNewRevisionRecord;
134
139 protected $mOldPage;
140
145 protected $mNewPage;
146
151 private $mOldTags;
152
157 private $mNewTags;
158
164 private $mOldContent;
165
171 private $mNewContent;
172
174 protected $mDiffLang;
175
177 private $mRevisionsIdsLoaded = false;
178
180 protected $mRevisionsLoaded = false;
181
183 protected $mTextLoaded = 0;
184
193 protected $isContentOverridden = false;
194
196 protected $mCacheHit = false;
197
199 private $cacheHitKey = null;
200
207 public $enableDebugComment = false;
208
212 protected $mReducedLineNumbers = false;
213
215 protected $mMarkPatrolledLink = null;
216
218 protected $unhide = false;
219
221 protected $mRefreshCache = false;
222
224 protected $slotDiffRenderers = null;
225
232 protected $isSlotDiffRenderer = false;
233
238 private $slotDiffOptions = [];
239
244 private $extraQueryParams = [];
245
247 private $textDiffer;
248
250 private IContentHandlerFactory $contentHandlerFactory;
251 private RevisionStore $revisionStore;
252 private ArchivedRevisionLookup $archivedRevisionLookup;
253 private HookRunner $hookRunner;
254 private WikiPageFactory $wikiPageFactory;
255 private UserOptionsLookup $userOptionsLookup;
256 private CommentFormatter $commentFormatter;
257 private IConnectionProvider $dbProvider;
258 private UserGroupManager $userGroupManager;
259 private UserEditTracker $userEditTracker;
260 private UserIdentityUtils $userIdentityUtils;
261
263 private $revisionLoadErrors = [];
264
273 public function __construct( $context = null, $old = 0, $new = 0, $rcid = 0,
274 $refreshCache = false, $unhide = false
275 ) {
276 if ( $context instanceof IContextSource ) {
277 $this->setContext( $context );
278 }
279
280 wfDebug( "DifferenceEngine old '$old' new '$new' rcid '$rcid'" );
281
282 $this->mOldid = $old;
283 $this->mNewid = $new;
284 $this->mRefreshCache = $refreshCache;
285 $this->unhide = $unhide;
286
287 $services = MediaWikiServices::getInstance();
288 $this->linkRenderer = $services->getLinkRenderer();
289 $this->contentHandlerFactory = $services->getContentHandlerFactory();
290 $this->revisionStore = $services->getRevisionStore();
291 $this->archivedRevisionLookup = $services->getArchivedRevisionLookup();
292 $this->hookRunner = new HookRunner( $services->getHookContainer() );
293 $this->wikiPageFactory = $services->getWikiPageFactory();
294 $this->userOptionsLookup = $services->getUserOptionsLookup();
295 $this->commentFormatter = $services->getCommentFormatter();
296 $this->dbProvider = $services->getConnectionProvider();
297 $this->userGroupManager = $services->getUserGroupManager();
298 $this->userEditTracker = $services->getUserEditTracker();
299 $this->userIdentityUtils = $services->getUserIdentityUtils();
300 }
301
307 protected function getSlotDiffRenderers() {
308 if ( $this->isSlotDiffRenderer ) {
309 throw new LogicException( __METHOD__ . ' called in slot diff renderer mode' );
310 }
311
312 if ( $this->slotDiffRenderers === null ) {
313 if ( !$this->loadRevisionData() ) {
314 return [];
315 }
316
317 $slotContents = $this->getSlotContents();
318 $this->slotDiffRenderers = [];
319 foreach ( $slotContents as $role => $contents ) {
320 if ( $contents['new'] && $contents['old']
321 && $contents['new']->equals( $contents['old'] )
322 ) {
323 // Do not produce a diff of identical content
324 continue;
325 }
326 if ( !$contents['new'] && !$contents['old'] ) {
327 // Nothing to diff (i.e both revisions are corrupted), just ignore
328 continue;
329 }
330 $handler = ( $contents['new'] ?: $contents['old'] )->getContentHandler();
331 $this->slotDiffRenderers[$role] = $handler->getSlotDiffRenderer(
332 $this->getContext(),
333 $this->slotDiffOptions + [
334 'contentLanguage' => $this->getDiffLang()->getCode(),
335 'textDiffer' => $this->getTextDiffer()
336 ]
337 );
338 }
339 }
340
341 return $this->slotDiffRenderers;
342 }
343
350 public function markAsSlotDiffRenderer() {
351 $this->isSlotDiffRenderer = true;
352 }
353
359 protected function getSlotContents() {
360 if ( $this->isContentOverridden ) {
361 return [
362 SlotRecord::MAIN => [ 'old' => $this->mOldContent, 'new' => $this->mNewContent ]
363 ];
364 } elseif ( !$this->loadRevisionData() ) {
365 return [];
366 }
367
368 $newSlots = $this->mNewRevisionRecord->getPrimarySlots()->getSlots();
369 $oldSlots = $this->mOldRevisionRecord ?
370 $this->mOldRevisionRecord->getPrimarySlots()->getSlots() :
371 [];
372 // The order here will determine the visual order of the diff. The current logic is
373 // slots of the new revision first in natural order, then deleted ones. This is ad hoc
374 // and should not be relied on - in the future we may want the ordering to depend
375 // on the page type.
376 $roles = array_keys( array_merge( $newSlots, $oldSlots ) );
377
378 $slots = [];
379 foreach ( $roles as $role ) {
380 $slots[$role] = [
381 'old' => $this->loadSingleSlot(
382 $oldSlots[$role] ?? null,
383 'old'
384 ),
385 'new' => $this->loadSingleSlot(
386 $newSlots[$role] ?? null,
387 'new'
388 )
389 ];
390 }
391 // move main slot to front
392 if ( isset( $slots[SlotRecord::MAIN] ) ) {
393 $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
394 }
395 return $slots;
396 }
397
405 private function loadSingleSlot( ?SlotRecord $slot, string $which ) {
406 if ( !$slot ) {
407 return null;
408 }
409 try {
410 return $slot->getContent();
411 } catch ( BadRevisionException ) {
412 $this->addRevisionLoadError( $which );
413 return null;
414 }
415 }
416
422 private function addRevisionLoadError( $which ) {
423 $this->revisionLoadErrors[] = $this->msg( $which === 'new'
424 ? 'difference-bad-new-revision' : 'difference-bad-old-revision'
425 );
426 }
427
434 public function getRevisionLoadErrors() {
435 return $this->revisionLoadErrors;
436 }
437
442 private function hasNewRevisionLoadError() {
443 foreach ( $this->revisionLoadErrors as $error ) {
444 if ( $error->getKey() === 'difference-bad-new-revision' ) {
445 return true;
446 }
447 }
448 return false;
449 }
450
452 public function getTitle() {
453 // T202454 avoid errors when there is no title
454 return parent::getTitle() ?: Title::makeTitle( NS_SPECIAL, 'BadTitle/DifferenceEngine' );
455 }
456
463 public function setReducedLineNumbers( $value = true ) {
464 $this->mReducedLineNumbers = $value;
465 }
466
472 public function getDiffLang() {
473 # Default language in which the diff text is written.
474 $this->mDiffLang ??= $this->getDefaultLanguage();
475 return $this->mDiffLang;
476 }
477
484 protected function getDefaultLanguage() {
485 return $this->getTitle()->getPageLanguage();
486 }
487
491 public function wasCacheHit() {
492 return $this->mCacheHit;
493 }
494
502 public function getOldid() {
503 $this->loadRevisionIds();
504
505 return $this->mOldid;
506 }
507
514 public function getNewid() {
515 $this->loadRevisionIds();
516
517 return $this->mNewid;
518 }
519
526 public function getOldRevision() {
527 return $this->mOldRevisionRecord ?: null;
528 }
529
535 public function getNewRevision() {
536 return $this->mNewRevisionRecord;
537 }
538
547 public function deletedLink( $id ) {
548 if ( $this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
549 $revRecord = $this->archivedRevisionLookup->getArchivedRevisionRecord( null, $id );
550 if ( $revRecord ) {
551 $title = Title::newFromPageIdentity( $revRecord->getPage() );
552
553 return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( [
554 'target' => $title->getPrefixedText(),
555 'timestamp' => $revRecord->getTimestamp()
556 ] );
557 }
558 }
559
560 return false;
561 }
562
570 public function deletedIdMarker( $id ) {
571 $link = $this->deletedLink( $id );
572 if ( $link ) {
573 return "[$link $id]";
574 } else {
575 return (string)$id;
576 }
577 }
578
579 private function showMissingRevision() {
580 $out = $this->getOutput();
581
582 $missing = [];
583 if ( $this->mOldid && ( !$this->mOldRevisionRecord || !$this->mOldContent ) ) {
584 $missing[] = $this->deletedIdMarker( $this->mOldid );
585 }
586 if ( !$this->mNewRevisionRecord || !$this->mNewContent ) {
587 $missing[] = $this->deletedIdMarker( $this->mNewid );
588 }
589
590 $out->setPageTitleMsg( $this->msg( 'errorpagetitle' ) );
591 $msg = $this->msg( 'difference-missing-revision' )
592 ->params( $this->getLanguage()->listToText( $missing ) )
593 ->numParams( count( $missing ) )
594 ->parseAsBlock();
595 $out->addHTML( $msg );
596 }
597
603 public function hasDeletedRevision() {
604 $this->loadRevisionData();
605 return (
606 $this->mNewRevisionRecord &&
607 $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
608 ) ||
609 (
610 $this->mOldRevisionRecord &&
611 $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
612 );
613 }
614
622 public function getPermissionErrors( Authority $performer ) {
623 wfDeprecated( __METHOD__, '1.44' );
624 return $this->authorizeView( $performer )->toLegacyErrorArray();
625 }
626
634 public function authorizeView( Authority $performer ): PermissionStatus {
635 $this->loadRevisionData();
636 $permStatus = PermissionStatus::newEmpty();
637 if ( $this->mNewPage ) {
638 $performer->authorizeRead( 'read', $this->mNewPage, $permStatus );
639 }
640 if ( $this->mOldPage ) {
641 $performer->authorizeRead( 'read', $this->mOldPage, $permStatus );
642 }
643 return $permStatus;
644 }
645
651 public function hasSuppressedRevision() {
652 return $this->hasDeletedRevision() && (
653 ( $this->mOldRevisionRecord &&
654 $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) ||
655 ( $this->mNewRevisionRecord &&
656 $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) )
657 );
658 }
659
666 private function getUserEditCount( $user ): string {
667 $editCount = $this->userEditTracker->getUserEditCount( $user );
668 if ( $editCount === null ) {
669 return '';
670 }
671
672 return Html::rawElement( 'div', [
673 'class' => 'mw-diff-usereditcount',
674 ],
675 $this->msg(
676 'diff-user-edits',
677 $this->getLanguage()->formatNum( $editCount )
678 )->parse()
679 );
680 }
681
688 private function getUserRoles( UserIdentity $user ) {
689 if ( !$this->userIdentityUtils->isNamed( $user ) ) {
690 return '';
691 }
692 $userGroups = $this->userGroupManager->getUserGroups( $user );
693 $userGroupLinks = [];
694 foreach ( $userGroups as $group ) {
695 $userGroupLinks[] = UserGroupMembership::getLinkHTML( $group, $this->getContext() );
696 }
697 return Html::rawElement( 'div', [
698 'class' => 'mw-diff-userroles',
699 ], $this->getLanguage()->commaList( $userGroupLinks ) );
700 }
701
708 private function getUserMetaData( ?UserIdentity $user ) {
709 if ( !$user ) {
710 return '';
711 }
712 return Html::rawElement( 'div', [
713 'class' => 'mw-diff-usermetadata',
714 ], $this->getUserRoles( $user ) . $this->getUserEditCount( $user ) );
715 }
716
728 public function isUserAllowedToSeeRevisions( Authority $performer ) {
729 $this->loadRevisionData();
730
731 if ( $this->mOldRevisionRecord && !$this->mOldRevisionRecord->userCan(
732 RevisionRecord::DELETED_TEXT,
733 $performer
734 ) ) {
735 return false;
736 }
737
738 // $this->mNewRev will only be falsy if a loading error occurred
739 // (in which case the user is allowed to see).
740 return !$this->mNewRevisionRecord || $this->mNewRevisionRecord->userCan(
741 RevisionRecord::DELETED_TEXT,
742 $performer
743 );
744 }
745
753 public function shouldBeHiddenFromUser( Authority $performer ) {
754 return $this->hasDeletedRevision() && ( !$this->unhide ||
755 !$this->isUserAllowedToSeeRevisions( $performer ) );
756 }
757
761 public function showDiffPage( $diffOnly = false ) {
762 # Allow frames except in certain special cases
763 $out = $this->getOutput();
764 $out->getMetadata()->setPreventClickjacking( false );
765 $out->setRobotPolicy( 'noindex,nofollow' );
766
767 // Allow extensions to add any extra output here
768 $this->hookRunner->onDifferenceEngineShowDiffPage( $out );
769
770 if ( !$this->loadRevisionData() ) {
771 if ( $this->hookRunner->onDifferenceEngineShowDiffPageMaybeShowMissingRevision( $this ) ) {
772 $this->showMissingRevision();
773 }
774 return;
775 }
776
777 $user = $this->getUser();
778 $permStatus = $this->authorizeView( $this->getAuthority() );
779 if ( !$permStatus->isGood() ) {
780 throw new PermissionsError( 'read', $permStatus );
781 }
782
783 $rollback = '';
784
785 $query = $this->extraQueryParams;
786 # Carry over 'diffonly' param via navigation links
787 if ( $diffOnly != MediaWikiServices::getInstance()
788 ->getUserOptionsLookup()->getBoolOption( $user, 'diffonly' )
789 ) {
790 $query['diffonly'] = $diffOnly;
791 }
792 # Cascade unhide param in links for easy deletion browsing
793 if ( $this->unhide ) {
794 $query['unhide'] = 1;
795 }
796
797 # Check if one of the revisions is deleted/suppressed
798 $deleted = $this->hasDeletedRevision();
799 $suppressed = $this->hasSuppressedRevision();
800 $allowed = $this->isUserAllowedToSeeRevisions( $this->getAuthority() );
801
802 $revisionTools = [];
803 $breadCrumbs = '';
804
805 # mOldRevisionRecord is false if the difference engine is called with a "vague" query for
806 # a diff between a version V and its previous version V' AND the version V
807 # is the first version of that article. In that case, V' does not exist.
808 if ( $this->mOldRevisionRecord === false ) {
809 if ( $this->mNewPage ) {
810 $out->setPageTitleMsg(
811 $this->msg( 'difference-title' )->plaintextParams( $this->mNewPage->getPrefixedText() )
812 );
813 }
814 $samePage = true;
815 $oldHeader = '';
816 // Allow extensions to change the $oldHeader variable
817 $this->hookRunner->onDifferenceEngineOldHeaderNoOldRev( $oldHeader );
818 } else {
819 $this->hookRunner->onDifferenceEngineViewHeader( $this );
820
821 if ( !$this->mOldPage || !$this->mNewPage ) {
822 // XXX say something to the user?
823 $samePage = false;
824 } elseif ( $this->mNewPage->equals( $this->mOldPage ) ) {
825 $out->setPageTitleMsg(
826 $this->msg( 'difference-title' )->plaintextParams( $this->mNewPage->getPrefixedText() )
827 );
828 $samePage = true;
829 } else {
830 $out->setPageTitleMsg( $this->msg( 'difference-title-multipage' )->plaintextParams(
831 $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) );
832 $out->addSubtitle( $this->msg( 'difference-multipage' ) );
833 $samePage = false;
834 }
835
836 if ( $samePage && $this->mNewPage &&
837 $this->getAuthority()->probablyCan( 'edit', $this->mNewPage )
838 ) {
839 if ( $this->mNewRevisionRecord->isCurrent() &&
840 $this->getAuthority()->probablyCan( 'rollback', $this->mNewPage )
841 ) {
842 $rollbackLink = Linker::generateRollback(
843 $this->mNewRevisionRecord,
844 $this->getContext(),
845 [ 'noBrackets' ]
846 );
847 if ( $rollbackLink ) {
848 $out->getMetadata()->setPreventClickjacking( true );
849 $rollback = "\u{00A0}\u{00A0}\u{00A0}" . $rollbackLink;
850 }
851 }
852
853 if ( $this->userCanEdit( $this->mOldRevisionRecord ) &&
854 $this->userCanEdit( $this->mNewRevisionRecord )
855 ) {
856 $undoLink = $this->linkRenderer->makeKnownLink(
857 $this->mNewPage,
858 $this->msg( 'editundo' )->text(),
859 [ 'title' => Linker::titleAttrib( 'undo' ) ],
860 [
861 'action' => 'edit',
862 'undoafter' => $this->mOldid,
863 'undo' => $this->mNewid
864 ]
865 );
866 $revisionTools['mw-diff-undo'] = $undoLink;
867 }
868 }
869 # Make "previous revision link"
870 $hasPrevious = $samePage && $this->mOldPage &&
871 $this->revisionStore->getPreviousRevision( $this->mOldRevisionRecord );
872 if ( $hasPrevious ) {
873 $prevlinkQuery = [ 'diff' => 'prev', 'oldid' => $this->mOldid ] + $query;
874 $prevlink = $this->linkRenderer->makeKnownLink(
875 $this->mOldPage,
876 $this->msg( 'previousdiff' )->text(),
877 [ 'id' => 'differences-prevlink' ],
878 $prevlinkQuery
879 );
880 $breadCrumbs .= $this->linkRenderer->makeKnownLink(
881 $this->mOldPage,
882 $this->msg( 'previousdiff' )->text(),
883 [
884 'class' => 'mw-diff-revision-history-link-previous'
885 ],
886 $prevlinkQuery
887 );
888 } else {
889 $prevlink = "\u{00A0}";
890 }
891
892 if ( $this->mOldRevisionRecord->isMinor() ) {
893 $oldminor = ChangesList::flag( 'minor' );
894 } else {
895 $oldminor = '';
896 }
897
898 $oldRevRecord = $this->mOldRevisionRecord;
899
900 $ldel = $this->revisionDeleteLink( $oldRevRecord );
901 $oldRevisionHeader = $this->getRevisionHeader( $oldRevRecord, 'complete' );
902 $oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff', $this->getContext() );
903 $oldRevComment = $this->commentFormatter
904 ->formatRevision(
905 $oldRevRecord, $user, !$diffOnly, !$this->unhide, false
906 );
907
908 if ( $oldRevComment === '' ) {
909 $defaultComment = $this->msg( 'changeslist-nocomment' )->escaped();
910 $oldRevComment = "<span class=\"comment mw-comment-none\">$defaultComment</span>";
911 }
912
913 $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' .
914 '<div id="mw-diff-otitle2">' .
915 Linker::revUserTools( $oldRevRecord, !$this->unhide ) .
916 $this->getUserMetaData( $oldRevRecord->getUser() ) .
917 '</div>' .
918 '<div id="mw-diff-otitle3">' . $oldminor . $oldRevComment . $ldel . '</div>' .
919 '<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' .
920 '<div id="mw-diff-otitle4">' . $prevlink . '</div>';
921
922 // Allow extensions to change the $oldHeader variable
923 $this->hookRunner->onDifferenceEngineOldHeader(
924 $this, $oldHeader, $prevlink, $oldminor, $diffOnly, $ldel, $this->unhide );
925 }
926
927 $out->addJsConfigVars( [
928 'wgDiffOldId' => $this->mOldid,
929 'wgDiffNewId' => $this->mNewid,
930 ] );
931
932 # Make "next revision link"
933 # Skip next link on the top revision
934 if ( $samePage && $this->mNewPage && !$this->mNewRevisionRecord->isCurrent() ) {
935 $nextlinkQuery = [ 'diff' => 'next', 'oldid' => $this->mNewid ] + $query;
936 $nextlink = $this->linkRenderer->makeKnownLink(
937 $this->mNewPage,
938 $this->msg( 'nextdiff' )->text(),
939 [ 'id' => 'differences-nextlink' ],
940 $nextlinkQuery
941 );
942 $breadCrumbs .= $this->linkRenderer->makeKnownLink(
943 $this->mNewPage,
944 $this->msg( 'nextdiff' )->text(),
945 [
946 'class' => 'mw-diff-revision-history-link-next'
947 ],
948 $nextlinkQuery
949 );
950 } else {
951 $nextlink = "\u{00A0}";
952 }
953
954 if ( $this->mNewRevisionRecord->isMinor() ) {
955 $newminor = ChangesList::flag( 'minor' );
956 } else {
957 $newminor = '';
958 }
959
960 # Handle RevisionDelete links...
961 $rdel = $this->revisionDeleteLink( $this->mNewRevisionRecord );
962
963 # Allow extensions to define their own revision tools
964 $this->hookRunner->onDiffTools(
965 $this->mNewRevisionRecord,
966 $revisionTools,
967 $this->mOldRevisionRecord ?: null,
968 $user
969 );
970
971 $formattedRevisionTools = [];
972 // Put each one in parentheses (poor man's button)
973 foreach ( $revisionTools as $key => $tool ) {
974 $toolClass = is_string( $key ) ? $key : 'mw-diff-tool';
975 $element = Html::rawElement(
976 'span',
977 [ 'class' => $toolClass ],
978 $tool
979 );
980 $formattedRevisionTools[] = $element;
981 }
982
983 $newRevRecord = $this->mNewRevisionRecord;
984
985 $newRevisionHeader = $this->getRevisionHeader( $newRevRecord, 'complete' ) .
986 ' ' . implode( ' ', $formattedRevisionTools );
987 $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff', $this->getContext() );
988 $newRevComment = $this->commentFormatter->formatRevision(
989 $newRevRecord, $user, !$diffOnly, !$this->unhide, false
990 );
991
992 if ( $newRevComment === '' ) {
993 $defaultComment = $this->msg( 'changeslist-nocomment' )->escaped();
994 $newRevComment = "<span class=\"comment mw-comment-none\">$defaultComment</span>";
995 }
996
997 $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' .
998 '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $newRevRecord, !$this->unhide ) .
999 $rollback .
1000 $this->getUserMetaData( $newRevRecord->getUser() ) .
1001 '</div>' .
1002 '<div id="mw-diff-ntitle3">' . $newminor . $newRevComment . $rdel . '</div>' .
1003 '<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' .
1004 '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
1005
1006 // Allow extensions to change the $newHeader variable
1007 $this->hookRunner->onDifferenceEngineNewHeader( $this, $newHeader,
1008 $formattedRevisionTools, $nextlink, $rollback, $newminor, $diffOnly,
1009 $rdel, $this->unhide );
1010
1011 $out->addHTML(
1012 Html::rawElement( 'div', [
1013 'class' => 'mw-diff-revision-history-links'
1014 ], $breadCrumbs )
1015 );
1016 $addMessageBoxStyles = false;
1017 # If the diff cannot be shown due to a deleted revision, then output
1018 # the diff header and links to unhide (if available)...
1019 if ( $this->shouldBeHiddenFromUser( $this->getAuthority() ) ) {
1020 $this->showDiffStyle();
1021 $multi = $this->getMultiNotice();
1022 $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
1023 if ( !$allowed ) {
1024 # Give explanation for why revision is not visible
1025 $msg = [ $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff' ];
1026 } else {
1027 # Give explanation and add a link to view the diff...
1028 $query = $this->getRequest()->appendQueryValue( 'unhide', '1' );
1029 $msg = [
1030 $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff',
1031 $this->getTitle()->getFullURL( $query )
1032 ];
1033 }
1034 $out->addHTML( Html::warningBox( $this->msg( ...$msg )->parse(), 'plainlinks' ) );
1035 $addMessageBoxStyles = true;
1036 # Otherwise, output a regular diff...
1037 } else {
1038 # Add deletion notice if the user is viewing deleted content
1039 $notice = '';
1040 if ( $deleted ) {
1041 $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
1042 $notice = Html::warningBox( $this->msg( $msg )->parse(), 'plainlinks' );
1043 $addMessageBoxStyles = true;
1044 }
1045
1046 # Add an error if the content can't be loaded
1047 $this->getSlotContents();
1048 foreach ( $this->getRevisionLoadErrors() as $msg ) {
1049 $notice .= Html::warningBox( $msg->parse() );
1050 $addMessageBoxStyles = true;
1051 }
1052
1053 // Check if inline switcher will be needed
1054 if ( $this->getTextDiffer()->hasFormat( 'inline' ) ) {
1055 $out->enableOOUI();
1056 }
1057
1058 $this->showTablePrefixes();
1059 $this->showDiff( $oldHeader, $newHeader, $notice );
1060 if ( !$diffOnly ) {
1061 $this->renderNewRevision();
1062 }
1063
1064 // Allow extensions to optionally not show the final patrolled link
1065 if ( $this->hookRunner->onDifferenceEngineRenderRevisionShowFinalPatrolLink() ) {
1066 # Add redundant patrol link on bottom...
1067 $out->addHTML( $this->markPatrolledLink() );
1068 }
1069 }
1070 if ( $addMessageBoxStyles ) {
1071 $out->addModuleStyles( 'mediawiki.codex.messagebox.styles' );
1072 }
1073 }
1074
1078 private function showTablePrefixes() {
1079 $parts = [];
1080 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1081 $parts += $slotDiffRenderer->getTablePrefix( $this->getContext(), $this->mNewPage );
1082 }
1083 ksort( $parts );
1084 if ( count( array_filter( $parts ) ) > 0 ) {
1085 $language = $this->getLanguage();
1086 $attrs = [
1087 'class' => 'mw-diff-table-prefix',
1088 'dir' => $language->getDir(),
1089 'lang' => $language->getCode(),
1090 ];
1091 $this->getOutput()->addHTML(
1092 Html::rawElement( 'div', $attrs, implode( '', $parts ) ) );
1093 }
1094 }
1095
1107 public function markPatrolledLink() {
1108 if ( $this->mMarkPatrolledLink === null ) {
1109 $linkInfo = $this->getMarkPatrolledLinkInfo();
1110 // If false, there is no patrol link needed/allowed
1111 if ( !$linkInfo || !$this->mNewPage ) {
1112 $this->mMarkPatrolledLink = '';
1113 } else {
1114 $patrolLinkClass = 'patrollink';
1115 $this->mMarkPatrolledLink = ' <span class="' . $patrolLinkClass . '" data-mw="interface">[' .
1116 $this->linkRenderer->makeKnownLink(
1117 $this->mNewPage,
1118 $this->msg( 'markaspatrolleddiff' )->text(),
1119 [],
1120 [
1121 'action' => 'markpatrolled',
1122 'rcid' => $linkInfo['rcid'],
1123 ]
1124 ) . ']</span>';
1125 // Allow extensions to change the markpatrolled link
1126 $this->hookRunner->onDifferenceEngineMarkPatrolledLink( $this,
1127 $this->mMarkPatrolledLink, $linkInfo['rcid'] );
1128 }
1129 }
1130 return $this->mMarkPatrolledLink;
1131 }
1132
1140 protected function getMarkPatrolledLinkInfo() {
1141 $user = $this->getUser();
1142 $config = $this->getConfig();
1143
1144 // Prepare a change patrol link, if applicable
1145 if (
1146 // Is patrolling enabled and the user allowed to?
1147 $config->get( MainConfigNames::UseRCPatrol ) &&
1148 $this->mNewPage &&
1149 $this->getAuthority()->probablyCan( 'patrol', $this->mNewPage ) &&
1150 // Only do this if the revision isn't more than 6 hours older
1151 // than the Max RC age (6h because the RC might not be cleaned out regularly)
1152 RecentChange::isInRCLifespan( $this->mNewRevisionRecord->getTimestamp(), 21600 )
1153 ) {
1154 // Look for an unpatrolled change corresponding to this diff
1155 $change = RecentChange::newFromConds(
1156 [
1157 'rc_this_oldid' => $this->mNewid,
1158 'rc_patrolled' => RecentChange::PRC_UNPATROLLED
1159 ],
1160 __METHOD__
1161 );
1162
1163 if ( $change && !$change->getPerformerIdentity()->equals( $user ) ) {
1164 $rcid = $change->getAttribute( 'rc_id' );
1165 } else {
1166 // None found or the page has been created by the current user.
1167 // If the user could patrol this it already would be patrolled
1168 $rcid = 0;
1169 }
1170
1171 // Allow extensions to possibly change the rcid here
1172 // For example the rcid might be set to zero due to the user
1173 // being the same as the performer of the change but an extension
1174 // might still want to show it under certain conditions
1175 $this->hookRunner->onDifferenceEngineMarkPatrolledRCID( $rcid, $this, $change, $user );
1176
1177 // Build the link
1178 if ( $rcid ) {
1179 $this->getOutput()->getMetadata()->setPreventClickjacking( true );
1180 $this->getOutput()->addModules( 'mediawiki.misc-authed-curate' );
1181
1182 return [ 'rcid' => $rcid ];
1183 }
1184 }
1185
1186 // No mark as patrolled link applicable
1187 return false;
1188 }
1189
1195 private function revisionDeleteLink( RevisionRecord $revRecord ) {
1196 $link = Linker::getRevDeleteLink(
1197 $this->getAuthority(),
1198 $revRecord,
1199 $revRecord->getPageAsLinkTarget()
1200 );
1201 if ( $link !== '' ) {
1202 $link = "\u{00A0}\u{00A0}\u{00A0}" . $link . ' ';
1203 }
1204
1205 return $link;
1206 }
1207
1213 public function renderNewRevision() {
1214 if ( $this->isContentOverridden ) {
1215 // The code below only works with a RevisionRecord object. We could construct a
1216 // fake RevisionRecord (here or in setContent), but since this does not seem
1217 // needed at the moment, we'll just fail for now.
1218 throw new LogicException(
1219 __METHOD__
1220 . ' is not supported after calling setContent(). Use setRevisions() instead.'
1221 );
1222 }
1223
1224 $out = $this->getOutput();
1225 $revHeader = $this->getRevisionHeader( $this->mNewRevisionRecord );
1226 # Add "current version as of X" title
1227 $out->addHTML( "<hr class='diff-hr' id='mw-oldid' />
1228 <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
1229 # Page content may be handled by a hooked call instead...
1230 if ( $this->hookRunner->onArticleContentOnDiff( $this, $out ) ) {
1231 $this->loadNewText();
1232 if ( !$this->mNewPage ) {
1233 // New revision is unsaved; bail out.
1234 // TODO in theory rendering the new revision is a meaningful thing to do
1235 // even if it's unsaved, but a lot of untangling is required to do it safely.
1236 return;
1237 }
1238 if ( $this->hasNewRevisionLoadError() ) {
1239 // There was an error loading the new revision
1240 return;
1241 }
1242
1243 $out->setRevisionId( $this->mNewid );
1244 $out->setRevisionIsCurrent( $this->mNewRevisionRecord->isCurrent() );
1245 $out->getMetadata()->setRevisionTimestamp( $this->mNewRevisionRecord->getTimestamp() );
1246 $out->setArticleFlag( true );
1247
1248 if ( !$this->hookRunner->onArticleRevisionViewCustom(
1249 $this->mNewRevisionRecord, $this->mNewPage, $this->mOldid, $out )
1250 ) {
1251 // Handled by extension
1252 // NOTE: sync with hooks called in Article::view()
1253 } else {
1254 // Normal page
1255 if ( $this->getTitle()->equals( $this->mNewPage ) ) {
1256 // If the Title stored in the context is the same as the one
1257 // of the new revision, we can use its associated WikiPage
1258 // object.
1259 $wikiPage = $this->getWikiPage();
1260 } else {
1261 // Otherwise we need to create our own WikiPage object
1262 $wikiPage = $this->wikiPageFactory->newFromTitle( $this->mNewPage );
1263 }
1264
1265 $parserOptions = $wikiPage->makeParserOptions( $this->getContext() );
1266 $parserOptions->setRenderReason( 'diff-page' );
1267
1268 $parserOutputAccess = MediaWikiServices::getInstance()->getParserOutputAccess();
1269 $status = $parserOutputAccess->getParserOutput(
1270 $wikiPage,
1271 $parserOptions,
1272 $this->mNewRevisionRecord,
1273 // we already checked
1274 ParserOutputAccess::OPT_NO_AUDIENCE_CHECK |
1275 // Update cascading protection
1276 ParserOutputAccess::OPT_LINKS_UPDATE
1277 );
1278 if ( $status->isOK() ) {
1279 $parserOutput = $status->getValue();
1280 // Allow extensions to change parser output here
1281 if ( $this->hookRunner->onDifferenceEngineRenderRevisionAddParserOutput(
1282 $this, $out, $parserOutput, $wikiPage )
1283 ) {
1284 $out->addParserOutput( $parserOutput, $parserOptions, [
1285 'enableSectionEditLinks' => $this->mNewRevisionRecord->isCurrent()
1286 && $this->getAuthority()->probablyCan(
1287 'edit',
1288 $this->mNewRevisionRecord->getPage()
1289 ),
1290 'absoluteURLs' => $this->slotDiffOptions['expand-url'] ?? false
1291 ] );
1292 }
1293 } else {
1294 $out->addModuleStyles( 'mediawiki.codex.messagebox.styles' );
1295 foreach ( $status->getMessages() as $msg ) {
1296 $out->addHTML( Html::errorBox(
1297 $this->msg( $msg )->parse()
1298 ) );
1299 }
1300 }
1301 }
1302 }
1303 }
1304
1315 public function showDiff( $otitle, $ntitle, $notice = '' ) {
1316 // Allow extensions to affect the output here
1317 $this->hookRunner->onDifferenceEngineShowDiff( $this );
1318
1319 $diff = $this->getDiff( $otitle, $ntitle, $notice );
1320 if ( $diff === false ) {
1321 $this->showMissingRevision();
1322 return false;
1323 }
1324
1325 $this->showDiffStyle();
1326 if ( $this->slotDiffOptions['expand-url'] ?? false ) {
1327 $diff = Linker::expandLocalLinks( $diff );
1328 }
1329 $this->getOutput()->addHTML( $diff );
1330 return true;
1331 }
1332
1336 public function showDiffStyle() {
1337 if ( !$this->isSlotDiffRenderer ) {
1338 $this->getOutput()->addModules( 'mediawiki.diff' );
1339 $this->getOutput()->addModuleStyles( [
1340 'mediawiki.interface.helpers.styles',
1341 'mediawiki.diff.styles'
1342 ] );
1343 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1344 $slotDiffRenderer->addModules( $this->getOutput() );
1345 }
1346 }
1347 }
1348
1358 public function getDiff( $otitle, $ntitle, $notice = '' ) {
1359 $body = $this->getDiffBody();
1360 if ( $body === false ) {
1361 return false;
1362 }
1363
1364 $multi = $this->getMultiNotice();
1365 // Display a message when the diff is empty
1366 if ( $body === '' ) {
1367 $notice .= '<div class="mw-diff-empty">' .
1368 $this->msg( 'diff-empty' )->parse() .
1369 "</div>\n";
1370 }
1371
1372 if ( $this->cacheHitKey !== null ) {
1373 $body .= "\n<!-- diff cache key " . htmlspecialchars( $this->cacheHitKey ) . " -->\n";
1374 }
1375
1376 return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
1377 }
1378
1379 private function incrementStats( string $cacheStatus ): void {
1380 $stats = MediaWikiServices::getInstance()->getStatsFactory();
1381 $stats->getCounter( 'diff_cache_total' )
1382 ->setLabel( 'status', $cacheStatus )
1383 ->copyToStatsdAt( 'diff_cache.' . $cacheStatus )
1384 ->increment();
1385 }
1386
1392 public function getDiffBody() {
1393 $this->mCacheHit = true;
1394 // Check if the diff should be hidden from this user
1395 if ( !$this->isContentOverridden ) {
1396 if ( !$this->loadRevisionData() ) {
1397 return false;
1398 } elseif ( $this->mOldRevisionRecord &&
1399 !$this->mOldRevisionRecord->userCan(
1400 RevisionRecord::DELETED_TEXT,
1401 $this->getAuthority()
1402 )
1403 ) {
1404 return false;
1405 } elseif ( $this->mNewRevisionRecord &&
1406 !$this->mNewRevisionRecord->userCan(
1407 RevisionRecord::DELETED_TEXT,
1408 $this->getAuthority()
1409 ) ) {
1410 return false;
1411 }
1412 // Short-circuit
1413 if ( $this->mOldRevisionRecord === false || (
1414 $this->mOldRevisionRecord &&
1415 $this->mNewRevisionRecord &&
1416 $this->mOldRevisionRecord->getId() &&
1417 $this->mOldRevisionRecord->getId() == $this->mNewRevisionRecord->getId()
1418 ) ) {
1419 if ( $this->hookRunner->onDifferenceEngineShowEmptyOldContent( $this ) ) {
1420 return '';
1421 }
1422 }
1423 }
1424
1425 // Cacheable?
1426 $key = false;
1427 $services = MediaWikiServices::getInstance();
1428 $cache = $services->getMainWANObjectCache();
1429 $stats = $services->getStatsdDataFactory();
1430 if ( $this->mOldid && $this->mNewid ) {
1431 $key = $cache->makeKey( ...$this->getDiffBodyCacheKeyParams() );
1432
1433 // Try cache
1434 if ( !$this->mRefreshCache ) {
1435 $difftext = $cache->get( $key );
1436 if ( is_string( $difftext ) ) {
1437 $this->incrementStats( 'hit' );
1438 $difftext = $this->localiseDiff( $difftext );
1439 $this->cacheHitKey = $key;
1440 return $difftext;
1441 }
1442 } // don't try to load but save the result
1443 }
1444 $this->mCacheHit = false;
1445 $this->cacheHitKey = null;
1446
1447 // Loadtext is permission safe, this just clears out the diff
1448 if ( !$this->loadText() ) {
1449 return false;
1450 }
1451
1452 $difftext = '';
1453 // We've checked for revdelete at the beginning of this method; it's OK to ignore
1454 // read permissions here.
1455 $slotContents = $this->getSlotContents();
1456 foreach ( $this->getSlotDiffRenderers() as $role => $slotDiffRenderer ) {
1457 try {
1458 $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role]['old'],
1459 $slotContents[$role]['new'] );
1460 } catch ( IncompatibleDiffTypesException $e ) {
1461 $slotDiff = $this->getSlotError( $e->getMessageObject()->parse() );
1462 }
1463 if ( $slotDiff && $role !== SlotRecord::MAIN ) {
1464 // FIXME: ask SlotRoleHandler::getSlotNameMessage
1465 $slotTitle = $role;
1466 $difftext .= $this->getSlotHeader( $slotTitle );
1467 }
1468 $difftext .= $slotDiff;
1469 }
1470
1471 // Save to cache for 7 days
1472 if ( !$this->hookRunner->onAbortDiffCache( $this ) ) {
1473 $this->incrementStats( 'uncacheable' );
1474 } elseif ( $key !== false ) {
1475 $this->incrementStats( 'miss' );
1476 $cache->set( $key, $difftext, 7 * 86400 );
1477 } else {
1478 $this->incrementStats( 'uncacheable' );
1479 }
1480 // localise line numbers and title attribute text
1481 $difftext = $this->localiseDiff( $difftext );
1482
1483 return $difftext;
1484 }
1485
1492 public function getDiffBodyForRole( $role ) {
1493 $diffRenderers = $this->getSlotDiffRenderers();
1494 if ( !isset( $diffRenderers[$role] ) ) {
1495 return false;
1496 }
1497
1498 $slotContents = $this->getSlotContents();
1499 try {
1500 $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role]['old'],
1501 $slotContents[$role]['new'] );
1502 } catch ( IncompatibleDiffTypesException $e ) {
1503 $slotDiff = $this->getSlotError( $e->getMessageObject()->parse() );
1504 }
1505 if ( $slotDiff === '' ) {
1506 return false;
1507 }
1508
1509 if ( $role !== SlotRecord::MAIN ) {
1510 // TODO use human-readable role name at least
1511 $slotTitle = $role;
1512 $slotDiff = $this->getSlotHeader( $slotTitle ) . $slotDiff;
1513 }
1514
1515 return $this->localiseDiff( $slotDiff );
1516 }
1517
1525 protected function getSlotHeader( $headerText ) {
1526 // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
1527 $columnCount = $this->mOldRevisionRecord ? 4 : 2;
1528 $userLang = $this->getLanguage()->getHtmlCode();
1529 return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-header', 'lang' => $userLang ],
1530 Html::element( 'th', [ 'colspan' => $columnCount ], $headerText ) );
1531 }
1532
1539 protected function getSlotError( $errorText ) {
1540 // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
1541 $columnCount = $this->mOldRevisionRecord ? 4 : 2;
1542 $userLang = $this->getLanguage()->getHtmlCode();
1543 return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-error', 'lang' => $userLang ],
1544 Html::rawElement( 'td', [ 'colspan' => $columnCount ], $errorText ) );
1545 }
1546
1560 protected function getDiffBodyCacheKeyParams() {
1561 if ( !$this->mOldid || !$this->mNewid ) {
1562 throw new BadMethodCallException( 'mOldid and mNewid must be set to get diff cache key.' );
1563 }
1564
1565 $params = [
1566 'diff',
1567 self::DIFF_VERSION,
1568 "old-{$this->mOldid}",
1569 "rev-{$this->mNewid}"
1570 ];
1571
1572 $extraKeys = [];
1573 if ( !$this->isSlotDiffRenderer ) {
1574 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1575 $extraKeys = array_merge( $extraKeys, $slotDiffRenderer->getExtraCacheKeys() );
1576 }
1577 }
1578 ksort( $extraKeys );
1579 return array_merge( $params, array_values( $extraKeys ) );
1580 }
1581
1589 public function getExtraCacheKeys() {
1590 // This method is called when the DifferenceEngine is used for a slot diff. We only care
1591 // about special things, not the revision IDs, which are added to the cache key by the
1592 // page-level DifferenceEngine, and which might not have a valid value for this object.
1593 $this->mOldid = 123456789;
1594 $this->mNewid = 987654321;
1595
1596 // This will repeat a bunch of unnecessary key fields for each slot. Not nice but harmless.
1597 $params = $this->getDiffBodyCacheKeyParams();
1598
1599 // Try to get rid of the standard keys to keep the cache key human-readable:
1600 // call the getDiffBodyCacheKeyParams implementation of the base class, and if
1601 // the child class includes the same keys, drop them.
1602 // Uses an obscure PHP feature where static calls to non-static methods are allowed
1603 // as long as we are already in a non-static method of the same class, and the call context
1604 // ($this) will be inherited.
1605 // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
1607 if ( array_slice( $params, 0, count( $standardParams ) ) === $standardParams ) {
1608 $params = array_slice( $params, count( $standardParams ) );
1609 }
1610
1611 return $params;
1612 }
1613
1624 public function setSlotDiffOptions( $options ) {
1625 $validatedOptions = [];
1626 if ( isset( $options['diff-type'] )
1627 && $this->getTextDiffer()->hasFormat( $options['diff-type'] )
1628 ) {
1629 $validatedOptions['diff-type'] = $options['diff-type'];
1630 }
1631 if ( !empty( $options['expand-url'] ) ) {
1632 $validatedOptions['expand-url'] = true;
1633 }
1634 if ( !empty( $options['inline-toggle'] ) ) {
1635 $validatedOptions['inline-toggle'] = true;
1636 }
1637 $this->slotDiffOptions = $validatedOptions;
1638 }
1639
1647 public function setExtraQueryParams( $params ) {
1648 $this->extraQueryParams = $params;
1649 }
1650
1664 public function generateContentDiffBody( Content $old, Content $new ) {
1665 $slotDiffRenderer = $new->getContentHandler()->getSlotDiffRenderer( $this->getContext() );
1666 if (
1667 $slotDiffRenderer instanceof DifferenceEngineSlotDiffRenderer
1668 && $this->isSlotDiffRenderer
1669 ) {
1670 // Oops, we are just about to enter an infinite loop (the slot-level DifferenceEngine
1671 // called a DifferenceEngineSlotDiffRenderer that wraps the same DifferenceEngine class).
1672 // This will happen when a content model has no custom slot diff renderer, it does have
1673 // a custom difference engine, but that does not override this method.
1674 throw new LogicException( get_class( $this ) . ': could not maintain backwards compatibility. '
1675 . 'Please use a SlotDiffRenderer.' );
1676 }
1677 return $slotDiffRenderer->getDiff( $old, $new ) . $this->getDebugString();
1678 }
1679
1692 public function generateTextDiffBody( $otext, $ntext ) {
1693 $slotDiffRenderer = $this->contentHandlerFactory
1694 ->getContentHandler( CONTENT_MODEL_TEXT )
1695 ->getSlotDiffRenderer( $this->getContext() );
1696 if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1697 // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1698 // This is too unlikely to happen to bother handling properly.
1699 throw new LogicException( 'The slot diff renderer for text content should be a '
1700 . 'TextSlotDiffRenderer subclass' );
1701 }
1702 return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1703 }
1704
1711 public static function getEngine() {
1712 $differenceEngine = new self;
1713 $engine = $differenceEngine->getTextDiffer()->getEngineForFormat( 'table' );
1714 if ( $engine === 'external' ) {
1715 return MediaWikiServices::getInstance()->getMainConfig()
1716 ->get( MainConfigNames::ExternalDiffEngine );
1717 } else {
1718 return $engine;
1719 }
1720 }
1721
1730 protected function debug( $generator = "internal" ) {
1731 if ( !$this->enableDebugComment ) {
1732 return '';
1733 }
1734 $data = [ $generator ];
1735 if ( $this->getConfig()->get( MainConfigNames::ShowHostnames ) ) {
1736 $data[] = wfHostname();
1737 }
1738 $data[] = wfTimestamp( TS_DB );
1739
1740 return "<!-- diff generator: " .
1741 implode( " ", array_map( "htmlspecialchars", $data ) ) .
1742 " -->\n";
1743 }
1744
1748 private function getDebugString() {
1749 $engine = self::getEngine();
1750 if ( $engine === 'wikidiff2' ) {
1751 return $this->debug( 'wikidiff2' );
1752 } elseif ( $engine === 'php' ) {
1753 return $this->debug( 'native PHP' );
1754 } else {
1755 return $this->debug( "external $engine" );
1756 }
1757 }
1758
1765 private function localiseDiff( $text ) {
1766 return $this->getTextDiffer()->localize( $this->getTextDiffFormat(), $text );
1767 }
1768
1777 public function localiseLineNumbers( $text ) {
1778 return preg_replace_callback( '/<!--LINE (\d+)-->/',
1779 function ( array $matches ) {
1780 if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
1781 return '';
1782 }
1783 return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
1784 }, $text );
1785 }
1786
1792 public function getMultiNotice() {
1793 // The notice only make sense if we are diffing two saved revisions of the same page.
1794 if (
1795 !$this->mOldRevisionRecord || !$this->mNewRevisionRecord
1796 || !$this->mOldPage || !$this->mNewPage
1797 || !$this->mOldPage->equals( $this->mNewPage )
1798 || $this->mOldRevisionRecord->getId() === null
1799 || $this->mNewRevisionRecord->getId() === null
1800 // (T237709) Deleted revs might have different page IDs
1801 || $this->mNewPage->getArticleID() !== $this->mOldRevisionRecord->getPageId()
1802 || $this->mNewPage->getArticleID() !== $this->mNewRevisionRecord->getPageId()
1803 ) {
1804 return '';
1805 }
1806
1807 if ( $this->mOldRevisionRecord->getTimestamp() > $this->mNewRevisionRecord->getTimestamp() ) {
1808 $oldRevRecord = $this->mNewRevisionRecord; // flip
1809 $newRevRecord = $this->mOldRevisionRecord; // flip
1810 } else { // normal case
1811 $oldRevRecord = $this->mOldRevisionRecord;
1812 $newRevRecord = $this->mNewRevisionRecord;
1813 }
1814
1815 // Don't show the notice if too many rows must be scanned
1816 // @todo show some special message for that case
1817 $nEdits = 0;
1818 $revisionIdList = $this->revisionStore->getRevisionIdsBetween(
1819 $this->mNewPage->getArticleID(),
1820 $oldRevRecord,
1821 $newRevRecord,
1822 1000
1823 );
1824 // only count revisions that are visible
1825 if ( count( $revisionIdList ) > 0 ) {
1826 foreach ( $revisionIdList as $revisionId ) {
1827 $revision = $this->revisionStore->getRevisionById( $revisionId );
1828 if ( $revision->getUser( RevisionRecord::FOR_THIS_USER, $this->getAuthority() ) ) {
1829 $nEdits++;
1830 }
1831 }
1832 }
1833 if ( $nEdits > 0 && $nEdits <= 1000 ) {
1834 // Use an invalid username to get the wiki's default gender (as fallback)
1835 $newRevUserForGender = '[HIDDEN]';
1836 $limit = 100; // use diff-multi-manyusers if too many users
1837 try {
1838 $users = $this->revisionStore->getAuthorsBetween(
1839 $this->mNewPage->getArticleID(),
1840 $oldRevRecord,
1841 $newRevRecord,
1842 null,
1843 $limit
1844 );
1845 $numUsers = count( $users );
1846
1847 $newRevUser = $newRevRecord->getUser( RevisionRecord::RAW );
1848 $newRevUserText = $newRevUser ? $newRevUser->getName() : '';
1849 $newRevUserSafe = $newRevRecord->getUser(
1850 RevisionRecord::FOR_THIS_USER,
1851 $this->getAuthority()
1852 );
1853 $newRevUserForGender = $newRevUserSafe ? $newRevUserSafe->getName() : '[HIDDEN]';
1854 if ( $numUsers == 1 && $users[0]->getName() == $newRevUserText ) {
1855 $numUsers = 0; // special case to say "by the same user" instead of "by one other user"
1856 }
1857 } catch ( InvalidArgumentException ) {
1858 $numUsers = 0;
1859 }
1860
1861 return self::intermediateEditsMsg( $nEdits, $numUsers, $limit, $newRevUserForGender );
1862 }
1863
1864 return '';
1865 }
1866
1877 public static function intermediateEditsMsg( $numEdits, $numUsers, $limit, $lastUser = '[HIDDEN]' ) {
1878 if ( $numUsers === 0 ) {
1879 $msg = 'diff-multi-sameuser';
1880 return wfMessage( $msg )
1881 ->numParams( $numEdits, $numUsers )
1882 ->params( $lastUser )
1883 ->parse();
1884 } elseif ( $numUsers > $limit ) {
1885 $msg = 'diff-multi-manyusers';
1886 $numUsers = $limit;
1887 } else {
1888 $msg = 'diff-multi-otherusers';
1889 }
1890
1891 return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1892 }
1893
1898 private function userCanEdit( RevisionRecord $revRecord ) {
1899 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
1900 return false;
1901 }
1902
1903 return true;
1904 }
1905
1915 public function getRevisionHeader( RevisionRecord $rev, $complete = '' ) {
1916 $lang = $this->getLanguage();
1917 $user = $this->getUser();
1918 $revtimestamp = $rev->getTimestamp();
1919 $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
1920 $dateofrev = $lang->userDate( $revtimestamp, $user );
1921 $timeofrev = $lang->userTime( $revtimestamp, $user );
1922
1923 $header = $this->msg(
1924 $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
1925 $timestamp,
1926 $dateofrev,
1927 $timeofrev
1928 );
1929
1930 if ( $complete !== 'complete' ) {
1931 return $header->escaped();
1932 }
1933
1934 $title = $rev->getPageAsLinkTarget();
1935
1936 if ( $this->userCanEdit( $rev ) ) {
1937 $header = $this->linkRenderer->makeKnownLink(
1938 $title,
1939 $header->text(),
1940 [],
1941 [ 'oldid' => $rev->getId() ]
1942 );
1943 $editQuery = [ 'action' => 'edit' ];
1944 if ( !$rev->isCurrent() ) {
1945 $editQuery['oldid'] = $rev->getId();
1946 }
1947
1948 $key = $this->getAuthority()->probablyCan( 'edit', $rev->getPage() ) ? 'editold' : 'viewsourceold';
1949 $msg = $this->msg( $key )->text();
1950 $editLink = $this->linkRenderer->makeKnownLink( $title, $msg, [], $editQuery );
1951 $header .= ' ' . Html::rawElement(
1952 'span',
1953 [ 'class' => 'mw-diff-edit' ],
1954 $editLink
1955 );
1956 } else {
1957 $header = $header->escaped();
1958 }
1959
1960 // Machine readable information
1961 $header .= Html::element( 'span',
1962 [
1963 'class' => 'mw-diff-timestamp',
1964 'data-timestamp' => wfTimestamp( TS_ISO_8601, $revtimestamp ),
1965 ], ''
1966 );
1967
1968 if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1969 return Html::rawElement(
1970 'span',
1971 [ 'class' => Linker::getRevisionDeletedClass( $rev ) ],
1972 $header
1973 );
1974 }
1975
1976 return $header;
1977 }
1978
1991 public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
1992 // shared.css sets diff in interface language/dir, but the actual content
1993 // is often in a different language, mostly the page content language/dir
1994 $header = Html::openElement( 'table', [
1995 'class' => [
1996 'diff',
1997 // The following classes are used here:
1998 // * diff-type-table
1999 // * diff-type-inline
2000 'diff-type-' . $this->getTextDiffFormat(),
2001 // The following classes are used here:
2002 // * diff-contentalign-left
2003 // * diff-contentalign-right
2004 'diff-contentalign-' . $this->getDiffLang()->alignStart(),
2005 // The following classes are used here:
2006 // * diff-editfont-monospace
2007 // * diff-editfont-sans-serif
2008 // * diff-editfont-serif
2009 'diff-editfont-' . $this->userOptionsLookup->getOption(
2010 $this->getUser(),
2011 'editfont'
2012 )
2013 ],
2014 'data-mw' => 'interface',
2015 ] );
2016 $userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
2017
2018 if ( !$diff && !$otitle ) {
2019 $header .= "
2020 <tr class=\"diff-title\" lang=\"{$userLang}\">
2021 <td class=\"diff-ntitle\">{$ntitle}</td>
2022 </tr>";
2023 $multiColspan = 1;
2024 } else {
2025 if ( $diff ) { // Safari/Chrome show broken output if cols not used
2026 $header .= "
2027 <col class=\"diff-marker\" />
2028 <col class=\"diff-content\" />
2029 <col class=\"diff-marker\" />
2030 <col class=\"diff-content\" />";
2031 $colspan = 2;
2032 $multiColspan = 4;
2033 } else {
2034 $colspan = 1;
2035 $multiColspan = 2;
2036 }
2037 if ( $otitle || $ntitle ) {
2038 // FIXME Hardcoding values from TableDiffFormatter.
2039 $deletedClass = 'diff-side-deleted';
2040 $addedClass = 'diff-side-added';
2041 $header .= "
2042 <tr class=\"diff-title\" lang=\"{$userLang}\">
2043 <td colspan=\"$colspan\" class=\"diff-otitle {$deletedClass}\">{$otitle}</td>
2044 <td colspan=\"$colspan\" class=\"diff-ntitle {$addedClass}\">{$ntitle}</td>
2045 </tr>";
2046 }
2047 }
2048
2049 if ( $multi != '' ) {
2050 $header .= "<tr><td colspan=\"{$multiColspan}\" " .
2051 "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
2052 }
2053 if ( $notice != '' ) {
2054 $header .= "<tr><td colspan=\"{$multiColspan}\" " .
2055 "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
2056 }
2057
2058 return $header . $diff . "</table>";
2059 }
2060
2068 public function setContent( Content $oldContent, Content $newContent ) {
2069 $this->mOldContent = $oldContent;
2070 $this->mNewContent = $newContent;
2071
2072 $this->mTextLoaded = 2;
2073 $this->mRevisionsLoaded = true;
2074 $this->isContentOverridden = true;
2075 $this->slotDiffRenderers = null;
2076 }
2077
2083 public function setRevisions(
2084 ?RevisionRecord $oldRevision, RevisionRecord $newRevision
2085 ) {
2086 if ( $oldRevision ) {
2087 $this->mOldRevisionRecord = $oldRevision;
2088 $this->mOldid = $oldRevision->getId();
2089 $this->mOldPage = Title::newFromPageIdentity( $oldRevision->getPage() );
2090 // This method is meant for edit diffs and such so there is no reason to provide a
2091 // revision that's not readable to the user, but check it just in case.
2092 $this->mOldContent = $oldRevision->getContent( SlotRecord::MAIN,
2093 RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
2094 if ( !$this->mOldContent ) {
2095 $this->addRevisionLoadError( 'old' );
2096 }
2097 } else {
2098 $this->mOldPage = null;
2099 $this->mOldRevisionRecord = $this->mOldid = false;
2100 }
2101 $this->mNewRevisionRecord = $newRevision;
2102 $this->mNewid = $newRevision->getId();
2103 $this->mNewPage = Title::newFromPageIdentity( $newRevision->getPage() );
2104 $this->mNewContent = $newRevision->getContent( SlotRecord::MAIN,
2105 RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
2106 if ( !$this->mNewContent ) {
2107 $this->addRevisionLoadError( 'new' );
2108 }
2109
2110 $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded = true;
2111 $this->mTextLoaded = $oldRevision ? 2 : 1;
2112 $this->isContentOverridden = false;
2113 $this->slotDiffRenderers = null;
2114 }
2115
2122 public function setTextLanguage( Language $lang ) {
2123 $this->mDiffLang = $lang;
2124 }
2125
2138 public function mapDiffPrevNext( $old, $new ) {
2139 if ( $new === 'prev' ) {
2140 // Show diff between revision $old and the previous one. Get previous one from DB.
2141 $newid = intval( $old );
2142 $oldid = false;
2143 $newRev = $this->revisionStore->getRevisionById( $newid );
2144 if ( $newRev ) {
2145 $oldRev = $this->revisionStore->getPreviousRevision( $newRev );
2146 if ( $oldRev ) {
2147 $oldid = $oldRev->getId();
2148 }
2149 }
2150 } elseif ( $new === 'next' ) {
2151 // Show diff between revision $old and the next one. Get next one from DB.
2152 $oldid = intval( $old );
2153 $newid = false;
2154 $oldRev = $this->revisionStore->getRevisionById( $oldid );
2155 if ( $oldRev ) {
2156 $newRev = $this->revisionStore->getNextRevision( $oldRev );
2157 if ( $newRev ) {
2158 $newid = $newRev->getId();
2159 }
2160 }
2161 } else {
2162 $oldid = intval( $old );
2163 $newid = intval( $new );
2164 }
2165
2166 // @phan-suppress-next-line PhanTypeMismatchReturn getId does not return null here
2167 return [ $oldid, $newid ];
2168 }
2169
2170 private function loadRevisionIds() {
2171 if ( $this->mRevisionsIdsLoaded ) {
2172 return;
2173 }
2174
2175 $this->mRevisionsIdsLoaded = true;
2176
2177 $old = $this->mOldid;
2178 $new = $this->mNewid;
2179
2180 [ $this->mOldid, $this->mNewid ] = self::mapDiffPrevNext( $old, $new );
2181 if ( $new === 'next' && $this->mNewid === false ) {
2182 # if no result, NewId points to the newest old revision. The only newer
2183 # revision is cur, which is "0".
2184 $this->mNewid = 0;
2185 }
2186
2187 $this->hookRunner->onNewDifferenceEngine(
2188 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
2189 $this->getTitle(), $this->mOldid, $this->mNewid, $old, $new );
2190 }
2191
2205 public function loadRevisionData() {
2206 if ( $this->mRevisionsLoaded ) {
2207 return $this->isContentOverridden ||
2208 ( $this->mOldRevisionRecord !== null && $this->mNewRevisionRecord !== null );
2209 }
2210
2211 // Whether it succeeds or fails, we don't want to try again
2212 $this->mRevisionsLoaded = true;
2213
2214 $this->loadRevisionIds();
2215
2216 // Load the new RevisionRecord object
2217 if ( $this->mNewid ) {
2218 $this->mNewRevisionRecord = $this->revisionStore->getRevisionById( $this->mNewid );
2219 } else {
2220 $this->mNewRevisionRecord = $this->revisionStore->getRevisionByTitle( $this->getTitle() );
2221 }
2222
2223 if ( !$this->mNewRevisionRecord instanceof RevisionRecord ) {
2224 return false;
2225 }
2226
2227 // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
2228 $this->mNewid = $this->mNewRevisionRecord->getId();
2229 $this->mNewPage = $this->mNewid ?
2230 Title::newFromPageIdentity( $this->mNewRevisionRecord->getPage() ) :
2231 null;
2232
2233 // Load the old RevisionRecord object
2234 $this->mOldRevisionRecord = false;
2235 if ( $this->mOldid ) {
2236 $this->mOldRevisionRecord = $this->revisionStore->getRevisionById( $this->mOldid );
2237 } elseif ( $this->mOldid === 0 ) {
2238 $revRecord = $this->revisionStore->getPreviousRevision( $this->mNewRevisionRecord );
2239 // No previous revision; mark to show as first-version only.
2240 $this->mOldid = $revRecord ? $revRecord->getId() : false;
2241 $this->mOldRevisionRecord = $revRecord ?? false;
2242 } /* elseif ( $this->mOldid === false ) leave mOldRevisionRecord false; */
2243
2244 if ( $this->mOldRevisionRecord === null ) {
2245 return false;
2246 }
2247
2248 if ( $this->mOldRevisionRecord && $this->mOldRevisionRecord->getId() ) {
2249 $this->mOldPage = Title::newFromPageIdentity( $this->mOldRevisionRecord->getPage() );
2250 } else {
2251 $this->mOldPage = null;
2252 }
2253
2254 // Load tags information for both revisions
2255 $dbr = $this->dbProvider->getReplicaDatabase();
2256 $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
2257 if ( $this->mOldid !== false ) {
2258 $tagIds = $dbr->newSelectQueryBuilder()
2259 ->select( 'ct_tag_id' )
2260 ->from( 'change_tag' )
2261 ->where( [ 'ct_rev_id' => $this->mOldid ] )
2262 ->caller( __METHOD__ )->fetchFieldValues();
2263 $tags = [];
2264 foreach ( $tagIds as $tagId ) {
2265 try {
2266 $tags[] = $changeTagDefStore->getName( (int)$tagId );
2267 } catch ( NameTableAccessException ) {
2268 continue;
2269 }
2270 }
2271 $this->mOldTags = implode( ',', $tags );
2272 } else {
2273 $this->mOldTags = false;
2274 }
2275
2276 $tagIds = $dbr->newSelectQueryBuilder()
2277 ->select( 'ct_tag_id' )
2278 ->from( 'change_tag' )
2279 ->where( [ 'ct_rev_id' => $this->mNewid ] )
2280 ->caller( __METHOD__ )->fetchFieldValues();
2281 $tags = [];
2282 foreach ( $tagIds as $tagId ) {
2283 try {
2284 $tags[] = $changeTagDefStore->getName( (int)$tagId );
2285 } catch ( NameTableAccessException ) {
2286 continue;
2287 }
2288 }
2289 $this->mNewTags = implode( ',', $tags );
2290
2291 return true;
2292 }
2293
2302 public function loadText() {
2303 if ( $this->mTextLoaded == 2 ) {
2304 return $this->loadRevisionData() &&
2305 ( $this->mOldRevisionRecord === false || $this->mOldContent )
2306 && $this->mNewContent;
2307 }
2308
2309 // Whether it succeeds or fails, we don't want to try again
2310 $this->mTextLoaded = 2;
2311
2312 if ( !$this->loadRevisionData() ) {
2313 return false;
2314 }
2315
2316 if ( $this->mOldRevisionRecord ) {
2317 $this->mOldContent = $this->mOldRevisionRecord->getContent(
2318 SlotRecord::MAIN,
2319 RevisionRecord::FOR_THIS_USER,
2320 $this->getAuthority()
2321 );
2322 if ( $this->mOldContent === null ) {
2323 return false;
2324 }
2325 }
2326
2327 $this->mNewContent = $this->mNewRevisionRecord->getContent(
2328 SlotRecord::MAIN,
2329 RevisionRecord::FOR_THIS_USER,
2330 $this->getAuthority()
2331 );
2332 $this->hookRunner->onDifferenceEngineLoadTextAfterNewContentIsLoaded( $this );
2333 if ( $this->mNewContent === null ) {
2334 return false;
2335 }
2336
2337 return true;
2338 }
2339
2345 public function loadNewText() {
2346 if ( $this->mTextLoaded >= 1 ) {
2347 return $this->loadRevisionData();
2348 }
2349
2350 $this->mTextLoaded = 1;
2351
2352 if ( !$this->loadRevisionData() ) {
2353 return false;
2354 }
2355
2356 $this->mNewContent = $this->mNewRevisionRecord->getContent(
2357 SlotRecord::MAIN,
2358 RevisionRecord::FOR_THIS_USER,
2359 $this->getAuthority()
2360 );
2361
2362 $this->hookRunner->onDifferenceEngineAfterLoadNewText( $this );
2363
2364 return true;
2365 }
2366
2372 protected function getTextDiffer() {
2373 if ( $this->textDiffer === null ) {
2374 $this->textDiffer = new ManifoldTextDiffer(
2375 $this->getContext(),
2376 $this->getDiffLang(),
2377 $this->getConfig()->get( MainConfigNames::DiffEngine ),
2378 $this->getConfig()->get( MainConfigNames::ExternalDiffEngine ),
2379 $this->getConfig()->get( MainConfigNames::Wikidiff2Options )
2380 );
2381 }
2382 return $this->textDiffer;
2383 }
2384
2391 public function getSupportedFormats() {
2392 return $this->getTextDiffer()->getFormats();
2393 }
2394
2401 public function getTextDiffFormat() {
2402 return $this->slotDiffOptions['diff-type'] ?? 'table';
2403 }
2404
2405}
const NS_SPECIAL
Definition Defines.php:54
const CONTENT_MODEL_TEXT
Definition Defines.php:252
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.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
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?
authorizeView(Authority $performer)
Check whether the user can read both of the pages for the current diff.
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...
Recent changes tagging.
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.
getMessageObject()
Return a Message object for this exception.
Show an error when a user tries to do something they do not have the necessary permissions for.
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:81
Class that generates HTML for internal links.
Some internal bits split of from Skin.php.
Definition Linker.php:61
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:157
Service for getting rendered output of a given page.
Service for creating WikiPage objects.
A StatusValue for permission errors.
Base class for lists of recent changes shown on special pages.
Utility class for creating and reading rows in the recentchanges table.
Exception raised when the text of a revision is permanently missing or corrupt.
Page revision base class.
getContent( $role, $audience=self::FOR_PUBLIC, ?Authority $performer=null)
Returns the Content of the given slot of this revision.
getPage()
Returns the page this revision belongs to.
isCurrent()
Checks whether the revision record is a stored current revision.
getTimestamp()
MCR migration note: this replaced Revision::getTimestamp.
getPageAsLinkTarget()
Returns the title of the page this revision is associated with as a LinkTarget object.
userCan( $field, Authority $performer)
Determine if the give authority is allowed to view a particular field of this revision,...
isDeleted( $field)
MCR migration note: this replaced Revision::isDeleted.
getId( $wikiId=self::LOCAL)
Get revision ID.
Service for looking up page revisions.
Value object representing a content slot associated with a page revision.
getContent()
Returns the Content of the given slot.
Parent class for all special pages.
Exception representing a failure to look up a row from a name table.
Represents a title within MediaWiki.
Definition Title.php:78
Provides access to user options.
Track info about user edit counts and timings.
Manage user group memberships.
Represents the membership of one user in one user group.
Convenience functions for interpreting UserIdentity objects using additional services or config.
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.
Content objects represent page content, e.g.
Definition Content.php:42
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.