MediaWiki master
DifferenceEngine.php
Go to the documentation of this file.
1<?php
17use MediaWiki\Debug\DeprecationHelper;
50
74
75 use DeprecationHelper;
76
83 private const DIFF_VERSION = '1.41';
84
91 protected $mOldid;
92
99 protected $mNewid;
100
111 private $mOldRevisionRecord;
112
121 private $mNewRevisionRecord;
122
127 protected $mOldPage;
128
133 protected $mNewPage;
134
139 private $mOldTags;
140
145 private $mNewTags;
146
152 private $mOldContent;
153
159 private $mNewContent;
160
162 protected $mDiffLang;
163
165 private $mRevisionsIdsLoaded = false;
166
168 protected $mRevisionsLoaded = false;
169
171 protected $mTextLoaded = 0;
172
181 protected $isContentOverridden = false;
182
184 protected $mCacheHit = false;
185
187 private $cacheHitKey = null;
188
195 public $enableDebugComment = false;
196
200 protected $mReducedLineNumbers = false;
201
203 protected $mMarkPatrolledLink = null;
204
206 protected $unhide = false;
207
209 protected $mRefreshCache = false;
210
212 protected $slotDiffRenderers = null;
213
220 protected $isSlotDiffRenderer = false;
221
226 private $slotDiffOptions = [];
227
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 private RecentChangeLookup $recentChangeLookup;
250
252 private $revisionLoadErrors = [];
253
262 public function __construct( $context = null, $old = 0, $new = 0, $rcid = 0,
263 $refreshCache = false, $unhide = false
264 ) {
265 if ( $context instanceof IContextSource ) {
266 $this->setContext( $context );
267 }
268
269 wfDebug( "DifferenceEngine old '$old' new '$new' rcid '$rcid'" );
270
271 $this->mOldid = $old;
272 $this->mNewid = $new;
273 $this->mRefreshCache = $refreshCache;
274 $this->unhide = $unhide;
275
276 $services = MediaWikiServices::getInstance();
277 $this->linkRenderer = $services->getLinkRenderer();
278 $this->contentHandlerFactory = $services->getContentHandlerFactory();
279 $this->revisionStore = $services->getRevisionStore();
280 $this->archivedRevisionLookup = $services->getArchivedRevisionLookup();
281 $this->hookRunner = new HookRunner( $services->getHookContainer() );
282 $this->wikiPageFactory = $services->getWikiPageFactory();
283 $this->userOptionsLookup = $services->getUserOptionsLookup();
284 $this->commentFormatter = $services->getCommentFormatter();
285 $this->dbProvider = $services->getConnectionProvider();
286 $this->userGroupManager = $services->getUserGroupManager();
287 $this->userEditTracker = $services->getUserEditTracker();
288 $this->userIdentityUtils = $services->getUserIdentityUtils();
289 $this->recentChangeLookup = $services->getRecentChangeLookup();
290 }
291
297 protected function getSlotDiffRenderers() {
298 if ( $this->isSlotDiffRenderer ) {
299 throw new LogicException( __METHOD__ . ' called in slot diff renderer mode' );
300 }
301
302 if ( $this->slotDiffRenderers === null ) {
303 if ( !$this->loadRevisionData() ) {
304 return [];
305 }
306
307 $slotContents = $this->getSlotContents();
308 $this->slotDiffRenderers = [];
309 foreach ( $slotContents as $role => $contents ) {
310 if ( $contents['new'] && $contents['old']
311 && $contents['new']->equals( $contents['old'] )
312 ) {
313 // Do not produce a diff of identical content
314 continue;
315 }
316 if ( !$contents['new'] && !$contents['old'] ) {
317 // Nothing to diff (i.e both revisions are corrupted), just ignore
318 continue;
319 }
320 $handler = ( $contents['new'] ?: $contents['old'] )->getContentHandler();
321 $this->slotDiffRenderers[$role] = $handler->getSlotDiffRenderer(
322 $this->getContext(),
323 $this->slotDiffOptions + [
324 'contentLanguage' => $this->getDiffLang()->getCode(),
325 'textDiffer' => $this->getTextDiffer()
326 ]
327 );
328 }
329 }
330
331 return $this->slotDiffRenderers;
332 }
333
340 public function markAsSlotDiffRenderer() {
341 $this->isSlotDiffRenderer = true;
342 }
343
349 protected function getSlotContents() {
350 if ( $this->isContentOverridden ) {
351 return [
352 SlotRecord::MAIN => [ 'old' => $this->mOldContent, 'new' => $this->mNewContent ]
353 ];
354 } elseif ( !$this->loadRevisionData() ) {
355 return [];
356 }
357
358 $newSlots = $this->mNewRevisionRecord->getPrimarySlots()->getSlots();
359 $oldSlots = $this->mOldRevisionRecord ?
360 $this->mOldRevisionRecord->getPrimarySlots()->getSlots() :
361 [];
362 // The order here will determine the visual order of the diff. The current logic is
363 // slots of the new revision first in natural order, then deleted ones. This is ad hoc
364 // and should not be relied on - in the future we may want the ordering to depend
365 // on the page type.
366 $roles = array_keys( array_merge( $newSlots, $oldSlots ) );
367
368 $slots = [];
369 foreach ( $roles as $role ) {
370 $slots[$role] = [
371 'old' => $this->loadSingleSlot(
372 $oldSlots[$role] ?? null,
373 'old'
374 ),
375 'new' => $this->loadSingleSlot(
376 $newSlots[$role] ?? null,
377 'new'
378 )
379 ];
380 }
381 // move main slot to front
382 if ( isset( $slots[SlotRecord::MAIN] ) ) {
383 $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
384 }
385 return $slots;
386 }
387
395 private function loadSingleSlot( ?SlotRecord $slot, string $which ) {
396 if ( !$slot ) {
397 return null;
398 }
399 try {
400 return $slot->getContent();
401 } catch ( BadRevisionException ) {
402 $this->addRevisionLoadError( $which );
403 return null;
404 }
405 }
406
412 private function addRevisionLoadError( $which ) {
413 $this->revisionLoadErrors[] = $this->msg( $which === 'new'
414 ? 'difference-bad-new-revision' : 'difference-bad-old-revision'
415 );
416 }
417
424 public function getRevisionLoadErrors() {
425 return $this->revisionLoadErrors;
426 }
427
432 private function hasNewRevisionLoadError() {
433 foreach ( $this->revisionLoadErrors as $error ) {
434 if ( $error->getKey() === 'difference-bad-new-revision' ) {
435 return true;
436 }
437 }
438 return false;
439 }
440
442 public function getTitle() {
443 // T202454 avoid errors when there is no title
444 return parent::getTitle() ?: Title::makeTitle( NS_SPECIAL, 'BadTitle/DifferenceEngine' );
445 }
446
453 public function setReducedLineNumbers( $value = true ) {
454 $this->mReducedLineNumbers = $value;
455 }
456
462 public function getDiffLang() {
463 # Default language in which the diff text is written.
464 $this->mDiffLang ??= $this->getDefaultLanguage();
465 return $this->mDiffLang;
466 }
467
474 protected function getDefaultLanguage() {
475 return $this->getTitle()->getPageLanguage();
476 }
477
481 public function wasCacheHit() {
482 return $this->mCacheHit;
483 }
484
492 public function getOldid() {
493 $this->loadRevisionIds();
494
495 return $this->mOldid;
496 }
497
504 public function getNewid() {
505 $this->loadRevisionIds();
506
507 return $this->mNewid;
508 }
509
516 public function getOldRevision() {
517 return $this->mOldRevisionRecord ?: null;
518 }
519
525 public function getNewRevision() {
526 return $this->mNewRevisionRecord;
527 }
528
537 public function deletedLink( $id ) {
538 if ( $this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
539 $revRecord = $this->archivedRevisionLookup->getArchivedRevisionRecord( null, $id );
540 if ( $revRecord ) {
541 $title = Title::newFromPageIdentity( $revRecord->getPage() );
542
543 return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( [
544 'target' => $title->getPrefixedText(),
545 'timestamp' => $revRecord->getTimestamp()
546 ] );
547 }
548 }
549
550 return false;
551 }
552
560 public function deletedIdMarker( $id ) {
561 $link = $this->deletedLink( $id );
562 if ( $link ) {
563 return "[$link $id]";
564 } else {
565 return (string)$id;
566 }
567 }
568
569 private function showMissingRevision() {
570 $out = $this->getOutput();
571
572 $missing = [];
573 if ( $this->mOldid && ( !$this->mOldRevisionRecord || !$this->mOldContent ) ) {
574 $missing[] = $this->deletedIdMarker( $this->mOldid );
575 }
576 if ( !$this->mNewRevisionRecord || !$this->mNewContent ) {
577 $missing[] = $this->deletedIdMarker( $this->mNewid );
578 }
579
580 $out->setPageTitleMsg( $this->msg( 'errorpagetitle' ) );
581 $msg = $this->msg( 'difference-missing-revision' )
582 ->params( $this->getLanguage()->listToText( $missing ) )
583 ->numParams( count( $missing ) )
584 ->parseAsBlock();
585 $out->addHTML( $msg );
586 }
587
593 public function hasDeletedRevision() {
594 $this->loadRevisionData();
595 return (
596 $this->mNewRevisionRecord &&
597 $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
598 ) ||
599 (
600 $this->mOldRevisionRecord &&
601 $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
602 );
603 }
604
612 public function authorizeView( Authority $performer ): PermissionStatus {
613 $this->loadRevisionData();
614 $permStatus = PermissionStatus::newEmpty();
615 if ( $this->mNewPage ) {
616 $performer->authorizeRead( 'read', $this->mNewPage, $permStatus );
617 }
618 if ( $this->mOldPage ) {
619 $performer->authorizeRead( 'read', $this->mOldPage, $permStatus );
620 }
621 return $permStatus;
622 }
623
629 public function hasSuppressedRevision() {
630 return $this->hasDeletedRevision() && (
631 ( $this->mOldRevisionRecord &&
632 $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) ||
633 ( $this->mNewRevisionRecord &&
634 $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) )
635 );
636 }
637
644 private function getUserEditCount( $user ): string {
645 $editCount = $this->userEditTracker->getUserEditCount( $user );
646 if ( $editCount === null ) {
647 return '';
648 }
649
650 return Html::rawElement( 'div', [
651 'class' => 'mw-diff-usereditcount',
652 ],
653 $this->msg(
654 'diff-user-edits',
655 $this->getLanguage()->formatNum( $editCount )
656 )->parse()
657 );
658 }
659
666 private function getUserRoles( UserIdentity $user ) {
667 if ( !$this->userIdentityUtils->isNamed( $user ) ) {
668 return '';
669 }
670 $userGroups = $this->userGroupManager->getUserGroups( $user );
671 $userGroupLinks = [];
672 foreach ( $userGroups as $group ) {
673 $userGroupLinks[] = UserGroupMembership::getLinkHTML( $group, $this->getContext() );
674 }
675 return Html::rawElement( 'div', [
676 'class' => 'mw-diff-userroles',
677 ], $this->getLanguage()->commaList( $userGroupLinks ) );
678 }
679
686 private function getUserMetaData( ?UserIdentity $user ) {
687 if ( !$user ) {
688 return '';
689 }
690 return Html::rawElement( 'div', [
691 'class' => 'mw-diff-usermetadata',
692 ], $this->getUserRoles( $user ) . $this->getUserEditCount( $user ) );
693 }
694
706 public function isUserAllowedToSeeRevisions( Authority $performer ) {
707 $this->loadRevisionData();
708
709 if ( $this->mOldRevisionRecord && !$this->mOldRevisionRecord->userCan(
710 RevisionRecord::DELETED_TEXT,
711 $performer
712 ) ) {
713 return false;
714 }
715
716 // $this->mNewRev will only be falsy if a loading error occurred
717 // (in which case the user is allowed to see).
718 return !$this->mNewRevisionRecord || $this->mNewRevisionRecord->userCan(
719 RevisionRecord::DELETED_TEXT,
720 $performer
721 );
722 }
723
731 public function shouldBeHiddenFromUser( Authority $performer ) {
732 return $this->hasDeletedRevision() && ( !$this->unhide ||
733 !$this->isUserAllowedToSeeRevisions( $performer ) );
734 }
735
739 public function showDiffPage( $diffOnly = false ) {
740 # Allow frames except in certain special cases
741 $out = $this->getOutput();
742 $out->getMetadata()->setPreventClickjacking( false );
743 $out->setRobotPolicy( 'noindex,nofollow' );
744
745 // Allow extensions to add any extra output here
746 $this->hookRunner->onDifferenceEngineShowDiffPage( $out );
747
748 if ( !$this->loadRevisionData() ) {
749 if ( $this->hookRunner->onDifferenceEngineShowDiffPageMaybeShowMissingRevision( $this ) ) {
750 $this->showMissingRevision();
751 }
752 return;
753 }
754
755 $user = $this->getUser();
756 $permStatus = $this->authorizeView( $this->getAuthority() );
757 if ( !$permStatus->isGood() ) {
758 throw new PermissionsError( 'read', $permStatus );
759 }
760
761 $rollback = '';
762
763 $query = $this->extraQueryParams;
764 # Carry over 'diffonly' param via navigation links
765 if ( $diffOnly != MediaWikiServices::getInstance()
766 ->getUserOptionsLookup()->getBoolOption( $user, 'diffonly' )
767 ) {
768 $query['diffonly'] = $diffOnly;
769 }
770 # Cascade unhide param in links for easy deletion browsing
771 if ( $this->unhide ) {
772 $query['unhide'] = 1;
773 }
774
775 # Check if one of the revisions is deleted/suppressed
776 $deleted = $this->hasDeletedRevision();
777 $suppressed = $this->hasSuppressedRevision();
778 $allowed = $this->isUserAllowedToSeeRevisions( $this->getAuthority() );
779
780 $revisionTools = [];
781 $breadCrumbs = '';
782 $newMobileFooter = '';
783
784 # mOldRevisionRecord is false if the difference engine is called with a "vague" query for
785 # a diff between a version V and its previous version V' AND the version V
786 # is the first version of that article. In that case, V' does not exist.
787 if ( $this->mOldRevisionRecord === false ) {
788 if ( $this->mNewPage ) {
789 $out->setPageTitleMsg(
790 $this->msg( 'difference-title' )->plaintextParams( $this->mNewPage->getPrefixedText() )
791 );
792 }
793 $samePage = true;
794 $oldHeader = '';
795 // Allow extensions to change the $oldHeader variable
796 $this->hookRunner->onDifferenceEngineOldHeaderNoOldRev( $oldHeader );
797 } else {
798 $this->hookRunner->onDifferenceEngineViewHeader( $this );
799
800 if ( !$this->mOldPage || !$this->mNewPage ) {
801 // XXX say something to the user?
802 $samePage = false;
803 } elseif ( $this->mNewPage->equals( $this->mOldPage ) ) {
804 $out->setPageTitleMsg(
805 $this->msg( 'difference-title' )->plaintextParams( $this->mNewPage->getPrefixedText() )
806 );
807 $samePage = true;
808 } else {
809 $out->setPageTitleMsg( $this->msg( 'difference-title-multipage' )->plaintextParams(
810 $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) );
811 $out->addSubtitle( $this->msg( 'difference-multipage' ) );
812 $samePage = false;
813 }
814
815 if ( $samePage && $this->mNewPage &&
816 $this->getAuthority()->probablyCan( 'edit', $this->mNewPage )
817 ) {
818 if ( $this->mNewRevisionRecord->isCurrent() &&
819 $this->getAuthority()->probablyCan( 'rollback', $this->mNewPage )
820 ) {
821 $rollbackLink = Linker::generateRollback(
822 $this->mNewRevisionRecord,
823 $this->getContext(),
824 [ 'noBrackets' ]
825 );
826 if ( $rollbackLink ) {
827 $out->getMetadata()->setPreventClickjacking( true );
828 $rollback = "\u{00A0}\u{00A0}\u{00A0}" . $rollbackLink;
829 $newMobileFooter .= $rollback;
830 }
831 }
832
833 if ( $this->userCanEdit( $this->mOldRevisionRecord ) &&
834 $this->userCanEdit( $this->mNewRevisionRecord )
835 ) {
836 $undoLink = $this->linkRenderer->makeKnownLink(
837 $this->mNewPage,
838 $this->msg( 'editundo' )->text(),
839 [ 'title' => Linker::titleAttrib( 'undo' ) ],
840 [
841 'action' => 'edit',
842 'undoafter' => $this->mOldid,
843 'undo' => $this->mNewid
844 ]
845 );
846 $revisionTools['mw-diff-undo'] = $undoLink;
847 }
848 }
849 # Make "previous revision link"
850 $hasPrevious = $samePage && $this->mOldPage &&
851 $this->revisionStore->getPreviousRevision( $this->mOldRevisionRecord );
852 if ( $hasPrevious ) {
853 $prevlinkQuery = [ 'diff' => 'prev', 'oldid' => $this->mOldid ] + $query;
854 $prevlink = $this->linkRenderer->makeKnownLink(
855 $this->mOldPage,
856 $this->msg( 'previousdiff' )->text(),
857 [ 'id' => 'differences-prevlink' ],
858 $prevlinkQuery
859 );
860 $breadCrumbs .= $this->linkRenderer->makeKnownLink(
861 $this->mOldPage,
862 $this->msg( 'previousdiff' )->text(),
863 [
864 'class' => 'mw-diff-revision-history-link-previous'
865 ],
866 $prevlinkQuery
867 );
868 } else {
869 $prevlink = "\u{00A0}";
870 }
871
872 if ( $this->mOldRevisionRecord->isMinor() ) {
873 $oldminor = ChangesList::flag( 'minor' );
874 } else {
875 $oldminor = '';
876 }
877
878 $oldRevRecord = $this->mOldRevisionRecord;
879
880 $ldel = $this->revisionDeleteLink( $oldRevRecord );
881 $oldRevisionHeader = $this->getRevisionHeader( $oldRevRecord, 'complete' );
882 $oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff', $this->getContext() );
883 $oldRevComment = $this->commentFormatter
884 ->formatRevision(
885 $oldRevRecord, $user, !$diffOnly, !$this->unhide, false
886 );
887
888 if ( $oldRevComment === '' ) {
889 $defaultComment = $this->msg( 'changeslist-nocomment' )->escaped();
890 $oldRevComment = "<span class=\"comment mw-comment-none\">$defaultComment</span>";
891 }
892
893 $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' .
894 '<div id="mw-diff-otitle2">' .
895 Linker::revUserTools( $oldRevRecord, !$this->unhide ) .
896 $this->getUserMetaData( $oldRevRecord->getUser() ) .
897 '</div>' .
898 '<div id="mw-diff-otitle3">' . $oldminor . $oldRevComment . $ldel . '</div>' .
899 '<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' .
900 '<div id="mw-diff-otitle4">' . $prevlink . '</div>';
901
902 // Allow extensions to change the $oldHeader variable
903 $this->hookRunner->onDifferenceEngineOldHeader(
904 $this, $oldHeader, $prevlink, $oldminor, $diffOnly, $ldel, $this->unhide );
905 }
906
907 $out->addJsConfigVars( [
908 'wgDiffOldId' => $this->mOldid,
909 'wgDiffNewId' => $this->mNewid,
910 ] );
911
912 # Make "next revision link"
913 # Skip next link on the top revision
914 if ( $samePage && $this->mNewPage && !$this->mNewRevisionRecord->isCurrent() ) {
915 $nextlinkQuery = [ 'diff' => 'next', 'oldid' => $this->mNewid ] + $query;
916 $nextlink = $this->linkRenderer->makeKnownLink(
917 $this->mNewPage,
918 $this->msg( 'nextdiff' )->text(),
919 [ 'id' => 'differences-nextlink' ],
920 $nextlinkQuery
921 );
922 $breadCrumbs .= $this->linkRenderer->makeKnownLink(
923 $this->mNewPage,
924 $this->msg( 'nextdiff' )->text(),
925 [
926 'class' => 'mw-diff-revision-history-link-next'
927 ],
928 $nextlinkQuery
929 );
930 } else {
931 $nextlink = "\u{00A0}";
932 }
933
934 if ( $this->mNewRevisionRecord->isMinor() ) {
935 $newminor = ChangesList::flag( 'minor' );
936 } else {
937 $newminor = '';
938 }
939
940 # Handle RevisionDelete links...
941 $rdel = $this->revisionDeleteLink( $this->mNewRevisionRecord );
942
943 # Allow extensions to define their own revision tools
944 $this->hookRunner->onDiffTools(
945 $this->mNewRevisionRecord,
946 $revisionTools,
947 $this->mOldRevisionRecord ?: null,
948 $user
949 );
950
951 $formattedRevisionTools = [];
952 // Put each one in parentheses (poor man's button)
953 foreach ( $revisionTools as $key => $tool ) {
954 $toolClass = is_string( $key ) ? $key : 'mw-diff-tool';
955 $element = Html::rawElement(
956 'span',
957 [ 'class' => $toolClass ],
958 $tool
959 );
960 $formattedRevisionTools[] = $element;
961 $newMobileFooter .= $element;
962 }
963
964 $newRevRecord = $this->mNewRevisionRecord;
965
966 $newRevisionHeader = $this->getRevisionHeader( $newRevRecord, 'complete' ) .
967 ' ' . implode( ' ', $formattedRevisionTools );
968 $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff', $this->getContext() );
969 $newRevComment = $this->commentFormatter->formatRevision(
970 $newRevRecord, $user, !$diffOnly, !$this->unhide, false
971 );
972
973 if ( $newRevComment === '' ) {
974 $defaultComment = $this->msg( 'changeslist-nocomment' )->escaped();
975 $newRevComment = "<span class=\"comment mw-comment-none\">$defaultComment</span>";
976 }
977
978 $newMobileFooter .= Linker::revUserTools( $newRevRecord, !$this->unhide ) .
979 $this->getUserMetaData( $newRevRecord->getUser() );
980
981 $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' .
982 '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $newRevRecord, !$this->unhide ) .
983 $rollback .
984 $this->getUserMetaData( $newRevRecord->getUser() ) .
985 '</div>' .
986 '<div id="mw-diff-ntitle3">' . $newminor . $newRevComment . $rdel . '</div>' .
987 '<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' .
988 '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
989
990 // Allow extensions to change the $newHeader variable
991 $this->hookRunner->onDifferenceEngineNewHeader( $this, $newHeader,
992 $formattedRevisionTools, $nextlink, $rollback, $newminor, $diffOnly,
993 $rdel, $this->unhide );
994
995 $out->addHTML(
996 Html::rawElement( 'div', [
997 'class' => 'mw-diff-revision-history-links'
998 ], $breadCrumbs )
999 );
1000
1001 $out->addHTML(
1002 Html::rawElement( 'div', [
1003 'class' => 'mw-diff-mobile-footer'
1004 ], $newMobileFooter )
1005 );
1006 $addMessageBoxStyles = false;
1007 # If the diff cannot be shown due to a deleted revision, then output
1008 # the diff header and links to unhide (if available)...
1009 if ( $this->shouldBeHiddenFromUser( $this->getAuthority() ) ) {
1010 $this->showDiffStyle();
1011 $multi = $this->getMultiNotice();
1012 $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
1013 if ( !$allowed ) {
1014 # Give explanation for why revision is not visible
1015 $msg = [ $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff' ];
1016 } else {
1017 # Give explanation and add a link to view the diff...
1018 $query = $this->getRequest()->appendQueryValue( 'unhide', '1' );
1019 $msg = [
1020 $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff',
1021 $this->getTitle()->getFullURL( $query )
1022 ];
1023 }
1024 $out->addHTML( Html::warningBox( $this->msg( ...$msg )->parse(), 'plainlinks' ) );
1025 $addMessageBoxStyles = true;
1026 # Otherwise, output a regular diff...
1027 } else {
1028 # Add deletion notice if the user is viewing deleted content
1029 $notice = '';
1030 if ( $deleted ) {
1031 $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
1032 $notice = Html::warningBox( $this->msg( $msg )->parse(), 'plainlinks' );
1033 $addMessageBoxStyles = true;
1034 }
1035
1036 # Add an error if the content can't be loaded
1037 $this->getSlotContents();
1038 foreach ( $this->getRevisionLoadErrors() as $msg ) {
1039 $notice .= Html::warningBox( $msg->parse() );
1040 $addMessageBoxStyles = true;
1041 }
1042
1043 // Check if inline switcher will be needed
1044 if ( $this->getTextDiffer()->hasFormat( 'inline' ) ) {
1045 $out->enableOOUI();
1046 }
1047
1048 $this->showTablePrefixes();
1049 $this->showDiff( $oldHeader, $newHeader, $notice );
1050 if ( !$diffOnly ) {
1051 $this->renderNewRevision();
1052 }
1053
1054 // Allow extensions to optionally not show the final patrolled link
1055 if ( $this->hookRunner->onDifferenceEngineRenderRevisionShowFinalPatrolLink() ) {
1056 # Add redundant patrol link on bottom...
1057 $out->addHTML( $this->markPatrolledLink() );
1058 }
1059 }
1060 if ( $addMessageBoxStyles ) {
1061 $out->addModuleStyles( 'mediawiki.codex.messagebox.styles' );
1062 }
1063 }
1064
1068 private function showTablePrefixes() {
1069 $parts = [];
1070 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1071 $parts += $slotDiffRenderer->getTablePrefix( $this->getContext(), $this->mNewPage );
1072 }
1073 ksort( $parts );
1074 if ( count( array_filter( $parts ) ) > 0 ) {
1075 $language = $this->getLanguage();
1076 $attrs = [
1077 'class' => 'mw-diff-table-prefix',
1078 'dir' => $language->getDir(),
1079 'lang' => $language->getCode(),
1080 ];
1081 $this->getOutput()->addHTML(
1082 Html::rawElement( 'div', $attrs, implode( '', $parts ) ) );
1083 }
1084 }
1085
1097 public function markPatrolledLink() {
1098 if ( $this->mMarkPatrolledLink === null ) {
1099 $linkInfo = $this->getMarkPatrolledLinkInfo();
1100 // If false, there is no patrol link needed/allowed
1101 if ( !$linkInfo || !$this->mNewPage ) {
1102 $this->mMarkPatrolledLink = '';
1103 } else {
1104 $patrolLinkClass = 'patrollink';
1105 $this->mMarkPatrolledLink = ' <span class="' . $patrolLinkClass . '" data-mw="interface">[' .
1106 $this->linkRenderer->makeKnownLink(
1107 $this->mNewPage,
1108 $this->msg( 'markaspatrolleddiff' )->text(),
1109 [],
1110 [
1111 'action' => 'markpatrolled',
1112 'rcid' => $linkInfo['rcid'],
1113 ]
1114 ) . ']</span>';
1115 // Allow extensions to change the markpatrolled link
1116 $this->hookRunner->onDifferenceEngineMarkPatrolledLink( $this,
1117 $this->mMarkPatrolledLink, $linkInfo['rcid'] );
1118 }
1119 }
1120 return $this->mMarkPatrolledLink;
1121 }
1122
1130 protected function getMarkPatrolledLinkInfo() {
1131 $user = $this->getUser();
1132 $config = $this->getConfig();
1133
1134 // Prepare a change patrol link, if applicable
1135 if (
1136 // Is patrolling enabled and the user allowed to?
1137 $config->get( MainConfigNames::UseRCPatrol ) &&
1138 $this->mNewPage &&
1139 $this->getAuthority()->probablyCan( 'patrol', $this->mNewPage ) &&
1140 // Only do this if the revision isn't more than 6 hours older
1141 // than the Max RC age (6h because the RC might not be cleaned out regularly)
1142 RecentChange::isInRCLifespan( $this->mNewRevisionRecord->getTimestamp(), 21600 )
1143 ) {
1144 // Look for an unpatrolled change corresponding to this diff
1145 $change = $this->recentChangeLookup->getRecentChangeByConds(
1146 [
1147 'rc_this_oldid' => $this->mNewid,
1148 'rc_patrolled' => RecentChange::PRC_UNPATROLLED
1149 ],
1150 __METHOD__
1151 );
1152
1153 if ( $change && !$change->getPerformerIdentity()->equals( $user ) ) {
1154 $rcid = $change->getAttribute( 'rc_id' );
1155 } else {
1156 // None found or the page has been created by the current user.
1157 // If the user could patrol this it already would be patrolled
1158 $rcid = 0;
1159 }
1160
1161 // Allow extensions to possibly change the rcid here
1162 // For example the rcid might be set to zero due to the user
1163 // being the same as the performer of the change but an extension
1164 // might still want to show it under certain conditions
1165 $this->hookRunner->onDifferenceEngineMarkPatrolledRCID( $rcid, $this, $change, $user );
1166
1167 // Build the link
1168 if ( $rcid ) {
1169 $this->getOutput()->getMetadata()->setPreventClickjacking( true );
1170 $this->getOutput()->addModules( 'mediawiki.misc-authed-curate' );
1171
1172 return [ 'rcid' => $rcid ];
1173 }
1174 }
1175
1176 // No mark as patrolled link applicable
1177 return false;
1178 }
1179
1185 private function revisionDeleteLink( RevisionRecord $revRecord ) {
1186 $link = Linker::getRevDeleteLink(
1187 $this->getAuthority(),
1188 $revRecord,
1189 $revRecord->getPageAsLinkTarget()
1190 );
1191 if ( $link !== '' ) {
1192 $link = "\u{00A0}\u{00A0}\u{00A0}" . $link . ' ';
1193 }
1194
1195 return $link;
1196 }
1197
1203 public function renderNewRevision() {
1204 if ( $this->isContentOverridden ) {
1205 // The code below only works with a RevisionRecord object. We could construct a
1206 // fake RevisionRecord (here or in setContent), but since this does not seem
1207 // needed at the moment, we'll just fail for now.
1208 throw new LogicException(
1209 __METHOD__
1210 . ' is not supported after calling setContent(). Use setRevisions() instead.'
1211 );
1212 }
1213
1214 $out = $this->getOutput();
1215 $revHeader = $this->getRevisionHeader( $this->mNewRevisionRecord );
1216 # Add "current version as of X" title
1217 $out->addHTML( "<hr class='diff-hr' id='mw-oldid' />
1218 <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
1219 # Page content may be handled by a hooked call instead...
1220 if ( $this->hookRunner->onArticleContentOnDiff( $this, $out ) ) {
1221 $this->loadNewText();
1222 if ( !$this->mNewPage ) {
1223 // New revision is unsaved; bail out.
1224 // TODO in theory rendering the new revision is a meaningful thing to do
1225 // even if it's unsaved, but a lot of untangling is required to do it safely.
1226 return;
1227 }
1228 if ( $this->hasNewRevisionLoadError() ) {
1229 // There was an error loading the new revision
1230 return;
1231 }
1232
1233 $out->setRevisionId( $this->mNewid );
1234 $out->setRevisionIsCurrent( $this->mNewRevisionRecord->isCurrent() );
1235 $out->getMetadata()->setRevisionTimestamp( $this->mNewRevisionRecord->getTimestamp() );
1236 $out->setArticleFlag( true );
1237
1238 if ( !$this->hookRunner->onArticleRevisionViewCustom(
1239 $this->mNewRevisionRecord, $this->mNewPage, $this->mOldid, $out )
1240 ) {
1241 // Handled by extension
1242 // NOTE: sync with hooks called in Article::view()
1243 } else {
1244 // Normal page
1245 if ( $this->getTitle()->equals( $this->mNewPage ) ) {
1246 // If the Title stored in the context is the same as the one
1247 // of the new revision, we can use its associated WikiPage
1248 // object.
1249 $wikiPage = $this->getWikiPage();
1250 } else {
1251 // Otherwise we need to create our own WikiPage object
1252 $wikiPage = $this->wikiPageFactory->newFromTitle( $this->mNewPage );
1253 }
1254
1255 $parserOptions = $wikiPage->makeParserOptions( $this->getContext() );
1256 $parserOptions->setRenderReason( 'diff-page' );
1257
1258 $parserOutputAccess = MediaWikiServices::getInstance()->getParserOutputAccess();
1259 $status = $parserOutputAccess->getParserOutput(
1260 $wikiPage,
1261 $parserOptions,
1262 $this->mNewRevisionRecord,
1263 // we already checked
1264 ParserOutputAccess::OPT_NO_AUDIENCE_CHECK |
1265 // Update cascading protection
1266 ParserOutputAccess::OPT_LINKS_UPDATE
1267 );
1268 if ( $status->isOK() ) {
1269 $parserOutput = $status->getValue();
1270 // Allow extensions to change parser output here
1271 if ( $this->hookRunner->onDifferenceEngineRenderRevisionAddParserOutput(
1272 $this, $out, $parserOutput, $wikiPage )
1273 ) {
1274 $out->addParserOutput( $parserOutput, $parserOptions, [
1275 'enableSectionEditLinks' => $this->mNewRevisionRecord->isCurrent()
1276 && $this->getAuthority()->probablyCan(
1277 'edit',
1278 $this->mNewRevisionRecord->getPage()
1279 ),
1280 'absoluteURLs' => $this->slotDiffOptions['expand-url'] ?? false
1281 ] );
1282 }
1283 } else {
1284 $out->addModuleStyles( 'mediawiki.codex.messagebox.styles' );
1285 foreach ( $status->getMessages() as $msg ) {
1286 $out->addHTML( Html::errorBox(
1287 $this->msg( $msg )->parse()
1288 ) );
1289 }
1290 }
1291 }
1292 }
1293 }
1294
1305 public function showDiff( $otitle, $ntitle, $notice = '' ) {
1306 // Allow extensions to affect the output here
1307 $this->hookRunner->onDifferenceEngineShowDiff( $this );
1308
1309 $diff = $this->getDiff( $otitle, $ntitle, $notice );
1310 if ( $diff === false ) {
1311 $this->showMissingRevision();
1312 return false;
1313 }
1314
1315 $this->showDiffStyle();
1316 if ( $this->slotDiffOptions['expand-url'] ?? false ) {
1317 $diff = Linker::expandLocalLinks( $diff );
1318 }
1319 $this->getOutput()->addHTML( $diff );
1320 return true;
1321 }
1322
1326 public function showDiffStyle() {
1327 if ( !$this->isSlotDiffRenderer ) {
1328 $this->getOutput()->addModules( 'mediawiki.diff' );
1329 $this->getOutput()->addModuleStyles( [
1330 'mediawiki.interface.helpers.styles',
1331 'mediawiki.diff.styles'
1332 ] );
1333 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1334 $slotDiffRenderer->addModules( $this->getOutput() );
1335 }
1336 }
1337 }
1338
1348 public function getDiff( $otitle, $ntitle, $notice = '' ) {
1349 $body = $this->getDiffBody();
1350 if ( $body === false ) {
1351 return false;
1352 }
1353
1354 $multi = $this->getMultiNotice();
1355 // Display a message when the diff is empty
1356 if ( $body === '' ) {
1357 $notice .= '<div class="mw-diff-empty">' .
1358 $this->msg( 'diff-empty' )->parse() .
1359 "</div>\n";
1360 }
1361
1362 if ( $this->cacheHitKey !== null ) {
1363 $body .= "\n<!-- diff cache key " . htmlspecialchars( $this->cacheHitKey ) . " -->\n";
1364 }
1365
1366 return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
1367 }
1368
1369 private function incrementStats( string $cacheStatus ): void {
1370 $stats = MediaWikiServices::getInstance()->getStatsFactory();
1371 $stats->getCounter( 'diff_cache_total' )
1372 ->setLabel( 'status', $cacheStatus )
1373 ->copyToStatsdAt( 'diff_cache.' . $cacheStatus )
1374 ->increment();
1375 }
1376
1382 public function getDiffBody() {
1383 $this->mCacheHit = true;
1384 // Check if the diff should be hidden from this user
1385 if ( !$this->isContentOverridden ) {
1386 if ( !$this->loadRevisionData() ) {
1387 return false;
1388 } elseif ( $this->mOldRevisionRecord &&
1389 !$this->mOldRevisionRecord->userCan(
1390 RevisionRecord::DELETED_TEXT,
1391 $this->getAuthority()
1392 )
1393 ) {
1394 return false;
1395 } elseif ( $this->mNewRevisionRecord &&
1396 !$this->mNewRevisionRecord->userCan(
1397 RevisionRecord::DELETED_TEXT,
1398 $this->getAuthority()
1399 ) ) {
1400 return false;
1401 }
1402 // Short-circuit
1403 if ( $this->mOldRevisionRecord === false || (
1404 $this->mOldRevisionRecord &&
1405 $this->mNewRevisionRecord &&
1406 $this->mOldRevisionRecord->getId() &&
1407 $this->mOldRevisionRecord->getId() == $this->mNewRevisionRecord->getId()
1408 ) ) {
1409 if ( $this->hookRunner->onDifferenceEngineShowEmptyOldContent( $this ) ) {
1410 return '';
1411 }
1412 }
1413 }
1414
1415 // Cacheable?
1416 $key = false;
1417 $services = MediaWikiServices::getInstance();
1418 $cache = $services->getMainWANObjectCache();
1419 $stats = $services->getStatsdDataFactory();
1420 if ( $this->mOldid && $this->mNewid ) {
1421 $key = $cache->makeKey( ...$this->getDiffBodyCacheKeyParams() );
1422
1423 // Try cache
1424 if ( !$this->mRefreshCache ) {
1425 $difftext = $cache->get( $key );
1426 if ( is_string( $difftext ) ) {
1427 $this->incrementStats( 'hit' );
1428 $difftext = $this->localiseDiff( $difftext );
1429 $this->cacheHitKey = $key;
1430 return $difftext;
1431 }
1432 } // don't try to load but save the result
1433 }
1434 $this->mCacheHit = false;
1435 $this->cacheHitKey = null;
1436
1437 // Loadtext is permission safe, this just clears out the diff
1438 if ( !$this->loadText() ) {
1439 return false;
1440 }
1441
1442 $difftext = '';
1443 // We've checked for revdelete at the beginning of this method; it's OK to ignore
1444 // read permissions here.
1445 $slotContents = $this->getSlotContents();
1446 foreach ( $this->getSlotDiffRenderers() as $role => $slotDiffRenderer ) {
1447 try {
1448 $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role]['old'],
1449 $slotContents[$role]['new'] );
1450 } catch ( IncompatibleDiffTypesException $e ) {
1451 $slotDiff = $this->getSlotError( $e->getMessageObject()->parse() );
1452 }
1453 if ( $slotDiff && $role !== SlotRecord::MAIN ) {
1454 // FIXME: ask SlotRoleHandler::getSlotNameMessage
1455 $slotTitle = $role;
1456 $difftext .= $this->getSlotHeader( $slotTitle );
1457 }
1458 $difftext .= $slotDiff;
1459 }
1460
1461 // Save to cache for 7 days
1462 if ( !$this->hookRunner->onAbortDiffCache( $this ) ) {
1463 $this->incrementStats( 'uncacheable' );
1464 } elseif ( $key !== false ) {
1465 $this->incrementStats( 'miss' );
1466 $cache->set( $key, $difftext, 7 * 86400 );
1467 } else {
1468 $this->incrementStats( 'uncacheable' );
1469 }
1470 // localise line numbers and title attribute text
1471 $difftext = $this->localiseDiff( $difftext );
1472
1473 return $difftext;
1474 }
1475
1482 public function getDiffBodyForRole( $role ) {
1483 $diffRenderers = $this->getSlotDiffRenderers();
1484 if ( !isset( $diffRenderers[$role] ) ) {
1485 return false;
1486 }
1487
1488 $slotContents = $this->getSlotContents();
1489 try {
1490 $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role]['old'],
1491 $slotContents[$role]['new'] );
1492 } catch ( IncompatibleDiffTypesException $e ) {
1493 $slotDiff = $this->getSlotError( $e->getMessageObject()->parse() );
1494 }
1495 if ( $slotDiff === '' ) {
1496 return false;
1497 }
1498
1499 if ( $role !== SlotRecord::MAIN ) {
1500 // TODO use human-readable role name at least
1501 $slotTitle = $role;
1502 $slotDiff = $this->getSlotHeader( $slotTitle ) . $slotDiff;
1503 }
1504
1505 return $this->localiseDiff( $slotDiff );
1506 }
1507
1514 protected function getSlotHeader( $headerText ) {
1515 // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
1516 $columnCount = $this->mOldRevisionRecord ? 4 : 2;
1517 $userLang = $this->getLanguage()->getHtmlCode();
1518 return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-header', 'lang' => $userLang ],
1519 Html::element( 'th', [ 'colspan' => $columnCount ], $headerText ) );
1520 }
1521
1528 protected function getSlotError( $errorText ) {
1529 // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
1530 $columnCount = $this->mOldRevisionRecord ? 4 : 2;
1531 $userLang = $this->getLanguage()->getHtmlCode();
1532 return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-error', 'lang' => $userLang ],
1533 Html::rawElement( 'td', [ 'colspan' => $columnCount ], $errorText ) );
1534 }
1535
1549 protected function getDiffBodyCacheKeyParams() {
1550 if ( !$this->mOldid || !$this->mNewid ) {
1551 throw new BadMethodCallException( 'mOldid and mNewid must be set to get diff cache key.' );
1552 }
1553
1554 $params = [
1555 'diff',
1556 self::DIFF_VERSION,
1557 "old-{$this->mOldid}",
1558 "rev-{$this->mNewid}"
1559 ];
1560
1561 $extraKeys = [];
1562 if ( !$this->isSlotDiffRenderer ) {
1563 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1564 $extraKeys = array_merge( $extraKeys, $slotDiffRenderer->getExtraCacheKeys() );
1565 }
1566 }
1567 ksort( $extraKeys );
1568 return array_merge( $params, array_values( $extraKeys ) );
1569 }
1570
1578 public function getExtraCacheKeys() {
1579 // This method is called when the DifferenceEngine is used for a slot diff. We only care
1580 // about special things, not the revision IDs, which are added to the cache key by the
1581 // page-level DifferenceEngine, and which might not have a valid value for this object.
1582 $this->mOldid = 123456789;
1583 $this->mNewid = 987654321;
1584
1585 // This will repeat a bunch of unnecessary key fields for each slot. Not nice but harmless.
1586 $params = $this->getDiffBodyCacheKeyParams();
1587
1588 // Try to get rid of the standard keys to keep the cache key human-readable:
1589 // call the getDiffBodyCacheKeyParams implementation of the base class, and if
1590 // the child class includes the same keys, drop them.
1591 // Uses an obscure PHP feature where static calls to non-static methods are allowed
1592 // as long as we are already in a non-static method of the same class, and the call context
1593 // ($this) will be inherited.
1594 // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
1596 if ( array_slice( $params, 0, count( $standardParams ) ) === $standardParams ) {
1597 $params = array_slice( $params, count( $standardParams ) );
1598 }
1599
1600 return $params;
1601 }
1602
1613 public function setSlotDiffOptions( $options ) {
1614 $validatedOptions = [];
1615 if ( isset( $options['diff-type'] )
1616 && $this->getTextDiffer()->hasFormat( $options['diff-type'] )
1617 ) {
1618 $validatedOptions['diff-type'] = $options['diff-type'];
1619 }
1620 if ( !empty( $options['expand-url'] ) ) {
1621 $validatedOptions['expand-url'] = true;
1622 }
1623 if ( !empty( $options['inline-toggle'] ) ) {
1624 $validatedOptions['inline-toggle'] = true;
1625 }
1626 $this->slotDiffOptions = $validatedOptions;
1627 }
1628
1636 public function setExtraQueryParams( $params ) {
1637 $this->extraQueryParams = $params;
1638 }
1639
1653 public function generateContentDiffBody( Content $old, Content $new ) {
1654 $slotDiffRenderer = $new->getContentHandler()->getSlotDiffRenderer( $this->getContext() );
1655 if (
1656 $slotDiffRenderer instanceof DifferenceEngineSlotDiffRenderer
1657 && $this->isSlotDiffRenderer
1658 ) {
1659 // Oops, we are just about to enter an infinite loop (the slot-level DifferenceEngine
1660 // called a DifferenceEngineSlotDiffRenderer that wraps the same DifferenceEngine class).
1661 // This will happen when a content model has no custom slot diff renderer, it does have
1662 // a custom difference engine, but that does not override this method.
1663 throw new LogicException( get_class( $this ) . ': could not maintain backwards compatibility. '
1664 . 'Please use a SlotDiffRenderer.' );
1665 }
1666 return $slotDiffRenderer->getDiff( $old, $new ) . $this->getDebugString();
1667 }
1668
1681 public function generateTextDiffBody( $otext, $ntext ) {
1682 $slotDiffRenderer = $this->contentHandlerFactory
1683 ->getContentHandler( CONTENT_MODEL_TEXT )
1684 ->getSlotDiffRenderer( $this->getContext() );
1685 if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1686 // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1687 // This is too unlikely to happen to bother handling properly.
1688 throw new LogicException( 'The slot diff renderer for text content should be a '
1689 . 'TextSlotDiffRenderer subclass' );
1690 }
1691 return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1692 }
1693
1700 public static function getEngine() {
1701 $differenceEngine = new self;
1702 $engine = $differenceEngine->getTextDiffer()->getEngineForFormat( 'table' );
1703 if ( $engine === 'external' ) {
1704 return MediaWikiServices::getInstance()->getMainConfig()
1705 ->get( MainConfigNames::ExternalDiffEngine );
1706 } else {
1707 return $engine;
1708 }
1709 }
1710
1719 protected function debug( $generator = "internal" ) {
1720 if ( !$this->enableDebugComment ) {
1721 return '';
1722 }
1723 $data = [ $generator ];
1724 if ( $this->getConfig()->get( MainConfigNames::ShowHostnames ) ) {
1725 $data[] = wfHostname();
1726 }
1727 $data[] = wfTimestamp( TS_DB );
1728
1729 return "<!-- diff generator: " .
1730 implode( " ", array_map( "htmlspecialchars", $data ) ) .
1731 " -->\n";
1732 }
1733
1737 private function getDebugString() {
1738 $engine = self::getEngine();
1739 if ( $engine === 'wikidiff2' ) {
1740 return $this->debug( 'wikidiff2' );
1741 } elseif ( $engine === 'php' ) {
1742 return $this->debug( 'native PHP' );
1743 } else {
1744 return $this->debug( "external $engine" );
1745 }
1746 }
1747
1754 private function localiseDiff( $text ) {
1755 return $this->getTextDiffer()->localize( $this->getTextDiffFormat(), $text );
1756 }
1757
1766 public function localiseLineNumbers( $text ) {
1767 return preg_replace_callback( '/<!--LINE (\d+)-->/',
1768 function ( array $matches ) {
1769 if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
1770 return '';
1771 }
1772 return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
1773 }, $text );
1774 }
1775
1781 public function getMultiNotice() {
1782 // The notice only make sense if we are diffing two saved revisions of the same page.
1783 if (
1784 !$this->mOldRevisionRecord || !$this->mNewRevisionRecord
1785 || !$this->mOldPage || !$this->mNewPage
1786 || !$this->mOldPage->equals( $this->mNewPage )
1787 || $this->mOldRevisionRecord->getId() === null
1788 || $this->mNewRevisionRecord->getId() === null
1789 // (T237709) Deleted revs might have different page IDs
1790 || $this->mNewPage->getArticleID() !== $this->mOldRevisionRecord->getPageId()
1791 || $this->mNewPage->getArticleID() !== $this->mNewRevisionRecord->getPageId()
1792 ) {
1793 return '';
1794 }
1795
1796 if ( $this->mOldRevisionRecord->getTimestamp() > $this->mNewRevisionRecord->getTimestamp() ) {
1797 $oldRevRecord = $this->mNewRevisionRecord; // flip
1798 $newRevRecord = $this->mOldRevisionRecord; // flip
1799 } else { // normal case
1800 $oldRevRecord = $this->mOldRevisionRecord;
1801 $newRevRecord = $this->mNewRevisionRecord;
1802 }
1803
1804 // Don't show the notice if too many rows must be scanned
1805 // @todo show some special message for that case
1806 $nEdits = 0;
1807 $revisionIdList = $this->revisionStore->getRevisionIdsBetween(
1808 $this->mNewPage->getArticleID(),
1809 $oldRevRecord,
1810 $newRevRecord,
1811 1000
1812 );
1813 // only count revisions that are visible
1814 if ( count( $revisionIdList ) > 0 ) {
1815 foreach ( $revisionIdList as $revisionId ) {
1816 $revision = $this->revisionStore->getRevisionById( $revisionId );
1817 if ( $revision->getUser( RevisionRecord::FOR_THIS_USER, $this->getAuthority() ) ) {
1818 $nEdits++;
1819 }
1820 }
1821 }
1822 if ( $nEdits > 0 && $nEdits <= 1000 ) {
1823 // Use an invalid username to get the wiki's default gender (as fallback)
1824 $newRevUserForGender = '[HIDDEN]';
1825 $limit = 100; // use diff-multi-manyusers if too many users
1826 try {
1827 $users = $this->revisionStore->getAuthorsBetween(
1828 $this->mNewPage->getArticleID(),
1829 $oldRevRecord,
1830 $newRevRecord,
1831 null,
1832 $limit
1833 );
1834 $numUsers = count( $users );
1835
1836 $newRevUser = $newRevRecord->getUser( RevisionRecord::RAW );
1837 $newRevUserText = $newRevUser ? $newRevUser->getName() : '';
1838 $newRevUserSafe = $newRevRecord->getUser(
1839 RevisionRecord::FOR_THIS_USER,
1840 $this->getAuthority()
1841 );
1842 $newRevUserForGender = $newRevUserSafe ? $newRevUserSafe->getName() : '[HIDDEN]';
1843 if ( $numUsers == 1 && $users[0]->getName() == $newRevUserText ) {
1844 $numUsers = 0; // special case to say "by the same user" instead of "by one other user"
1845 }
1846 } catch ( InvalidArgumentException ) {
1847 $numUsers = 0;
1848 }
1849
1850 return self::intermediateEditsMsg( $nEdits, $numUsers, $limit, $newRevUserForGender );
1851 }
1852
1853 return '';
1854 }
1855
1866 public static function intermediateEditsMsg( $numEdits, $numUsers, $limit, $lastUser = '[HIDDEN]' ) {
1867 if ( $numUsers === 0 ) {
1868 $msg = 'diff-multi-sameuser';
1869 return wfMessage( $msg )
1870 ->numParams( $numEdits, $numUsers )
1871 ->params( $lastUser )
1872 ->parse();
1873 } elseif ( $numUsers > $limit ) {
1874 $msg = 'diff-multi-manyusers';
1875 $numUsers = $limit;
1876 } else {
1877 $msg = 'diff-multi-otherusers';
1878 }
1879
1880 return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1881 }
1882
1887 private function userCanEdit( RevisionRecord $revRecord ) {
1888 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
1889 return false;
1890 }
1891
1892 return true;
1893 }
1894
1904 public function getRevisionHeader( RevisionRecord $rev, $complete = '' ) {
1905 $lang = $this->getLanguage();
1906 $user = $this->getUser();
1907 $revtimestamp = $rev->getTimestamp();
1908 $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
1909 $dateofrev = $lang->userDate( $revtimestamp, $user );
1910 $timeofrev = $lang->userTime( $revtimestamp, $user );
1911
1912 $header = $this->msg(
1913 $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
1914 $timestamp,
1915 $dateofrev,
1916 $timeofrev
1917 );
1918
1919 if ( $complete !== 'complete' ) {
1920 return $header->escaped();
1921 }
1922
1923 $title = $rev->getPageAsLinkTarget();
1924
1925 if ( $this->userCanEdit( $rev ) ) {
1926 $header = $this->linkRenderer->makeKnownLink(
1927 $title,
1928 $header->text(),
1929 [],
1930 [ 'oldid' => $rev->getId() ]
1931 );
1932 $editQuery = [ 'action' => 'edit' ];
1933 if ( !$rev->isCurrent() ) {
1934 $editQuery['oldid'] = $rev->getId();
1935 }
1936
1937 $key = $this->getAuthority()->probablyCan( 'edit', $rev->getPage() ) ? 'editold' : 'viewsourceold';
1938 $msg = $this->msg( $key )->text();
1939 $editLink = $this->linkRenderer->makeKnownLink( $title, $msg, [], $editQuery );
1940 $header .= ' ' . Html::rawElement(
1941 'span',
1942 [ 'class' => 'mw-diff-edit' ],
1943 $editLink
1944 );
1945 } else {
1946 $header = $header->escaped();
1947 }
1948
1949 // Machine readable information
1950 $header .= Html::element( 'span',
1951 [
1952 'class' => 'mw-diff-timestamp',
1953 'data-timestamp' => wfTimestamp( TS_ISO_8601, $revtimestamp ),
1954 ], ''
1955 );
1956
1957 if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1958 return Html::rawElement(
1959 'span',
1960 [ 'class' => Linker::getRevisionDeletedClass( $rev ) ],
1961 $header
1962 );
1963 }
1964
1965 return $header;
1966 }
1967
1980 public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
1981 // shared.css sets diff in interface language/dir, but the actual content
1982 // is often in a different language, mostly the page content language/dir
1983 $header = Html::openElement( 'table', [
1984 'class' => [
1985 'diff',
1986 // The following classes are used here:
1987 // * diff-type-table
1988 // * diff-type-inline
1989 'diff-type-' . $this->getTextDiffFormat(),
1990 // The following classes are used here:
1991 // * diff-contentalign-left
1992 // * diff-contentalign-right
1993 'diff-contentalign-' . $this->getDiffLang()->alignStart(),
1994 // The following classes are used here:
1995 // * diff-editfont-monospace
1996 // * diff-editfont-sans-serif
1997 // * diff-editfont-serif
1998 'diff-editfont-' . $this->userOptionsLookup->getOption(
1999 $this->getUser(),
2000 'editfont'
2001 )
2002 ],
2003 'data-mw' => 'interface',
2004 ] );
2005 $userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
2006
2007 if ( !$diff && !$otitle ) {
2008 $header .= "
2009 <tr class=\"diff-title\" lang=\"{$userLang}\">
2010 <td class=\"diff-ntitle\">{$ntitle}</td>
2011 </tr>";
2012 $multiColspan = 1;
2013 } else {
2014 if ( $diff ) { // Safari/Chrome show broken output if cols not used
2015 $header .= "
2016 <col class=\"diff-marker\" />
2017 <col class=\"diff-content\" />
2018 <col class=\"diff-marker\" />
2019 <col class=\"diff-content\" />";
2020 $colspan = 2;
2021 $multiColspan = 4;
2022 } else {
2023 $colspan = 1;
2024 $multiColspan = 2;
2025 }
2026 if ( $otitle || $ntitle ) {
2027 // FIXME Hardcoding values from TableDiffFormatter.
2028 $deletedClass = 'diff-side-deleted';
2029 $addedClass = 'diff-side-added';
2030 $header .= "
2031 <tr class=\"diff-title\" lang=\"{$userLang}\">
2032 <td colspan=\"$colspan\" class=\"diff-otitle {$deletedClass}\">{$otitle}</td>
2033 <td colspan=\"$colspan\" class=\"diff-ntitle {$addedClass}\">{$ntitle}</td>
2034 </tr>";
2035 }
2036 }
2037
2038 if ( $multi != '' ) {
2039 $header .= "<tr><td colspan=\"{$multiColspan}\" " .
2040 "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
2041 }
2042 if ( $notice != '' ) {
2043 $header .= "<tr><td colspan=\"{$multiColspan}\" " .
2044 "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
2045 }
2046
2047 return $header . $diff . "</table>";
2048 }
2049
2057 public function setContent( Content $oldContent, Content $newContent ) {
2058 $this->mOldContent = $oldContent;
2059 $this->mNewContent = $newContent;
2060
2061 $this->mTextLoaded = 2;
2062 $this->mRevisionsLoaded = true;
2063 $this->isContentOverridden = true;
2064 $this->slotDiffRenderers = null;
2065 }
2066
2072 public function setRevisions(
2073 ?RevisionRecord $oldRevision, RevisionRecord $newRevision
2074 ) {
2075 if ( $oldRevision ) {
2076 $this->mOldRevisionRecord = $oldRevision;
2077 $this->mOldid = $oldRevision->getId();
2078 $this->mOldPage = Title::newFromPageIdentity( $oldRevision->getPage() );
2079 // This method is meant for edit diffs and such so there is no reason to provide a
2080 // revision that's not readable to the user, but check it just in case.
2081 $this->mOldContent = $oldRevision->getContent( SlotRecord::MAIN,
2082 RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
2083 if ( !$this->mOldContent ) {
2084 $this->addRevisionLoadError( 'old' );
2085 }
2086 } else {
2087 $this->mOldPage = null;
2088 $this->mOldRevisionRecord = $this->mOldid = false;
2089 }
2090 $this->mNewRevisionRecord = $newRevision;
2091 $this->mNewid = $newRevision->getId();
2092 $this->mNewPage = Title::newFromPageIdentity( $newRevision->getPage() );
2093 $this->mNewContent = $newRevision->getContent( SlotRecord::MAIN,
2094 RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
2095 if ( !$this->mNewContent ) {
2096 $this->addRevisionLoadError( 'new' );
2097 }
2098
2099 $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded = true;
2100 $this->mTextLoaded = $oldRevision ? 2 : 1;
2101 $this->isContentOverridden = false;
2102 $this->slotDiffRenderers = null;
2103 }
2104
2111 public function setTextLanguage( Language $lang ) {
2112 $this->mDiffLang = $lang;
2113 }
2114
2127 public function mapDiffPrevNext( $old, $new ) {
2128 if ( $new === 'prev' ) {
2129 // Show diff between revision $old and the previous one. Get previous one from DB.
2130 $newid = intval( $old );
2131 $oldid = false;
2132 $newRev = $this->revisionStore->getRevisionById( $newid );
2133 if ( $newRev ) {
2134 $oldRev = $this->revisionStore->getPreviousRevision( $newRev );
2135 if ( $oldRev ) {
2136 $oldid = $oldRev->getId();
2137 }
2138 }
2139 } elseif ( $new === 'next' ) {
2140 // Show diff between revision $old and the next one. Get next one from DB.
2141 $oldid = intval( $old );
2142 $newid = false;
2143 $oldRev = $this->revisionStore->getRevisionById( $oldid );
2144 if ( $oldRev ) {
2145 $newRev = $this->revisionStore->getNextRevision( $oldRev );
2146 if ( $newRev ) {
2147 $newid = $newRev->getId();
2148 }
2149 }
2150 } else {
2151 $oldid = intval( $old );
2152 $newid = intval( $new );
2153 }
2154
2155 // @phan-suppress-next-line PhanTypeMismatchReturn getId does not return null here
2156 return [ $oldid, $newid ];
2157 }
2158
2159 private function loadRevisionIds() {
2160 if ( $this->mRevisionsIdsLoaded ) {
2161 return;
2162 }
2163
2164 $this->mRevisionsIdsLoaded = true;
2165
2166 $old = $this->mOldid;
2167 $new = $this->mNewid;
2168
2169 [ $this->mOldid, $this->mNewid ] = self::mapDiffPrevNext( $old, $new );
2170 if ( $new === 'next' && $this->mNewid === false ) {
2171 # if no result, NewId points to the newest old revision. The only newer
2172 # revision is cur, which is "0".
2173 $this->mNewid = 0;
2174 }
2175
2176 $this->hookRunner->onNewDifferenceEngine(
2177 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
2178 $this->getTitle(), $this->mOldid, $this->mNewid, $old, $new );
2179 }
2180
2194 public function loadRevisionData() {
2195 if ( $this->mRevisionsLoaded ) {
2196 return $this->isContentOverridden ||
2197 ( $this->mOldRevisionRecord !== null && $this->mNewRevisionRecord !== null );
2198 }
2199
2200 // Whether it succeeds or fails, we don't want to try again
2201 $this->mRevisionsLoaded = true;
2202
2203 $this->loadRevisionIds();
2204
2205 // Load the new RevisionRecord object
2206 if ( $this->mNewid ) {
2207 $this->mNewRevisionRecord = $this->revisionStore->getRevisionById( $this->mNewid );
2208 } else {
2209 $this->mNewRevisionRecord = $this->revisionStore->getRevisionByTitle( $this->getTitle() );
2210 }
2211
2212 if ( !$this->mNewRevisionRecord instanceof RevisionRecord ) {
2213 return false;
2214 }
2215
2216 // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
2217 $this->mNewid = $this->mNewRevisionRecord->getId();
2218 $this->mNewPage = $this->mNewid ?
2219 Title::newFromPageIdentity( $this->mNewRevisionRecord->getPage() ) :
2220 null;
2221
2222 // Load the old RevisionRecord object
2223 $this->mOldRevisionRecord = false;
2224 if ( $this->mOldid ) {
2225 $this->mOldRevisionRecord = $this->revisionStore->getRevisionById( $this->mOldid );
2226 } elseif ( $this->mOldid === 0 ) {
2227 $revRecord = $this->revisionStore->getPreviousRevision( $this->mNewRevisionRecord );
2228 // No previous revision; mark to show as first-version only.
2229 $this->mOldid = $revRecord ? $revRecord->getId() : false;
2230 $this->mOldRevisionRecord = $revRecord ?? false;
2231 } /* elseif ( $this->mOldid === false ) leave mOldRevisionRecord false; */
2232
2233 if ( $this->mOldRevisionRecord === null ) {
2234 return false;
2235 }
2236
2237 if ( $this->mOldRevisionRecord && $this->mOldRevisionRecord->getId() ) {
2238 $this->mOldPage = Title::newFromPageIdentity( $this->mOldRevisionRecord->getPage() );
2239 } else {
2240 $this->mOldPage = null;
2241 }
2242
2243 // Load tags information for both revisions
2244 $dbr = $this->dbProvider->getReplicaDatabase();
2245 $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
2246 if ( $this->mOldid !== false ) {
2247 $tagIds = $dbr->newSelectQueryBuilder()
2248 ->select( 'ct_tag_id' )
2249 ->from( 'change_tag' )
2250 ->where( [ 'ct_rev_id' => $this->mOldid ] )
2251 ->caller( __METHOD__ )->fetchFieldValues();
2252 $tags = [];
2253 foreach ( $tagIds as $tagId ) {
2254 try {
2255 $tags[] = $changeTagDefStore->getName( (int)$tagId );
2256 } catch ( NameTableAccessException ) {
2257 continue;
2258 }
2259 }
2260 $this->mOldTags = implode( ',', $tags );
2261 } else {
2262 $this->mOldTags = false;
2263 }
2264
2265 $tagIds = $dbr->newSelectQueryBuilder()
2266 ->select( 'ct_tag_id' )
2267 ->from( 'change_tag' )
2268 ->where( [ 'ct_rev_id' => $this->mNewid ] )
2269 ->caller( __METHOD__ )->fetchFieldValues();
2270 $tags = [];
2271 foreach ( $tagIds as $tagId ) {
2272 try {
2273 $tags[] = $changeTagDefStore->getName( (int)$tagId );
2274 } catch ( NameTableAccessException ) {
2275 continue;
2276 }
2277 }
2278 $this->mNewTags = implode( ',', $tags );
2279
2280 return true;
2281 }
2282
2290 public function loadText() {
2291 if ( $this->mTextLoaded == 2 ) {
2292 return $this->loadRevisionData() &&
2293 ( $this->mOldRevisionRecord === false || $this->mOldContent )
2294 && $this->mNewContent;
2295 }
2296
2297 // Whether it succeeds or fails, we don't want to try again
2298 $this->mTextLoaded = 2;
2299
2300 if ( !$this->loadRevisionData() ) {
2301 return false;
2302 }
2303
2304 if ( $this->mOldRevisionRecord ) {
2305 $this->mOldContent = $this->mOldRevisionRecord->getContent(
2306 SlotRecord::MAIN,
2307 RevisionRecord::FOR_THIS_USER,
2308 $this->getAuthority()
2309 );
2310 if ( $this->mOldContent === null ) {
2311 return false;
2312 }
2313 }
2314
2315 $this->mNewContent = $this->mNewRevisionRecord->getContent(
2316 SlotRecord::MAIN,
2317 RevisionRecord::FOR_THIS_USER,
2318 $this->getAuthority()
2319 );
2320 $this->hookRunner->onDifferenceEngineLoadTextAfterNewContentIsLoaded( $this );
2321 if ( $this->mNewContent === null ) {
2322 return false;
2323 }
2324
2325 return true;
2326 }
2327
2333 public function loadNewText() {
2334 if ( $this->mTextLoaded >= 1 ) {
2335 return $this->loadRevisionData();
2336 }
2337
2338 $this->mTextLoaded = 1;
2339
2340 if ( !$this->loadRevisionData() ) {
2341 return false;
2342 }
2343
2344 $this->mNewContent = $this->mNewRevisionRecord->getContent(
2345 SlotRecord::MAIN,
2346 RevisionRecord::FOR_THIS_USER,
2347 $this->getAuthority()
2348 );
2349
2350 $this->hookRunner->onDifferenceEngineAfterLoadNewText( $this );
2351
2352 return true;
2353 }
2354
2360 protected function getTextDiffer() {
2361 if ( $this->textDiffer === null ) {
2362 $this->textDiffer = new ManifoldTextDiffer(
2363 $this->getContext(),
2364 $this->getDiffLang(),
2365 $this->getConfig()->get( MainConfigNames::DiffEngine ),
2366 $this->getConfig()->get( MainConfigNames::ExternalDiffEngine ),
2367 $this->getConfig()->get( MainConfigNames::Wikidiff2Options )
2368 );
2369 }
2370 return $this->textDiffer;
2371 }
2372
2379 public function getSupportedFormats() {
2380 return $this->getTextDiffer()->getFormats();
2381 }
2382
2389 public function getTextDiffFormat() {
2390 return $this->slotDiffOptions['diff-type'] ?? 'table';
2391 }
2392
2393}
const NS_SPECIAL
Definition Defines.php:40
const CONTENT_MODEL_TEXT
Definition Defines.php:238
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.
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...
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.Message
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:43
Base class for language-specific code.
Definition Language.php:69
Class that generates HTML for internal links.
Some internal bits split of from Skin.php.
Definition Linker.php:47
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:144
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:69
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:28
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:23
authorizeRead(string $action, PageIdentity $target, ?PermissionStatus $status=null)
Authorize read access.
Interface for objects representing user identity.
Provide primary and replica IDatabase connections.
msg( $key,... $params)