MediaWiki master
DifferenceEngine.php
Go to the documentation of this file.
1<?php
54
78
79 use DeprecationHelper;
80
87 private const DIFF_VERSION = '1.41';
88
95 protected $mOldid;
96
103 protected $mNewid;
104
115 private $mOldRevisionRecord;
116
125 private $mNewRevisionRecord;
126
131 protected $mOldPage;
132
137 protected $mNewPage;
138
143 private $mOldTags;
144
149 private $mNewTags;
150
156 private $mOldContent;
157
163 private $mNewContent;
164
166 protected $mDiffLang;
167
169 private $mRevisionsIdsLoaded = false;
170
172 protected $mRevisionsLoaded = false;
173
175 protected $mTextLoaded = 0;
176
185 protected $isContentOverridden = false;
186
188 protected $mCacheHit = false;
189
191 private $cacheHitKey = null;
192
199 public $enableDebugComment = false;
200
204 protected $mReducedLineNumbers = false;
205
207 protected $mMarkPatrolledLink = null;
208
210 protected $unhide = false;
211
213 protected $mRefreshCache = false;
214
216 protected $slotDiffRenderers = null;
217
224 protected $isSlotDiffRenderer = false;
225
230 private $slotDiffOptions = [];
231
235 private $extraQueryParams = [];
236
238 private $textDiffer;
239
241 private IContentHandlerFactory $contentHandlerFactory;
242 private RevisionStore $revisionStore;
243 private ArchivedRevisionLookup $archivedRevisionLookup;
244 private HookRunner $hookRunner;
245 private WikiPageFactory $wikiPageFactory;
246 private UserOptionsLookup $userOptionsLookup;
247 private CommentFormatter $commentFormatter;
248 private IConnectionProvider $dbProvider;
249 private UserGroupManager $userGroupManager;
250 private UserEditTracker $userEditTracker;
251 private UserIdentityUtils $userIdentityUtils;
252
254 private $revisionLoadErrors = [];
255
264 public function __construct( $context = null, $old = 0, $new = 0, $rcid = 0,
265 $refreshCache = false, $unhide = false
266 ) {
267 if ( $context instanceof IContextSource ) {
268 $this->setContext( $context );
269 }
270
271 wfDebug( "DifferenceEngine old '$old' new '$new' rcid '$rcid'" );
272
273 $this->mOldid = $old;
274 $this->mNewid = $new;
275 $this->mRefreshCache = $refreshCache;
276 $this->unhide = $unhide;
277
278 $services = MediaWikiServices::getInstance();
279 $this->linkRenderer = $services->getLinkRenderer();
280 $this->contentHandlerFactory = $services->getContentHandlerFactory();
281 $this->revisionStore = $services->getRevisionStore();
282 $this->archivedRevisionLookup = $services->getArchivedRevisionLookup();
283 $this->hookRunner = new HookRunner( $services->getHookContainer() );
284 $this->wikiPageFactory = $services->getWikiPageFactory();
285 $this->userOptionsLookup = $services->getUserOptionsLookup();
286 $this->commentFormatter = $services->getCommentFormatter();
287 $this->dbProvider = $services->getConnectionProvider();
288 $this->userGroupManager = $services->getUserGroupManager();
289 $this->userEditTracker = $services->getUserEditTracker();
290 $this->userIdentityUtils = $services->getUserIdentityUtils();
291 }
292
298 protected function getSlotDiffRenderers() {
299 if ( $this->isSlotDiffRenderer ) {
300 throw new LogicException( __METHOD__ . ' called in slot diff renderer mode' );
301 }
302
303 if ( $this->slotDiffRenderers === null ) {
304 if ( !$this->loadRevisionData() ) {
305 return [];
306 }
307
308 $slotContents = $this->getSlotContents();
309 $this->slotDiffRenderers = [];
310 foreach ( $slotContents as $role => $contents ) {
311 if ( $contents['new'] && $contents['old']
312 && $contents['new']->equals( $contents['old'] )
313 ) {
314 // Do not produce a diff of identical content
315 continue;
316 }
317 $handler = ( $contents['new'] ?: $contents['old'] )->getContentHandler();
318 $this->slotDiffRenderers[$role] = $handler->getSlotDiffRenderer(
319 $this->getContext(),
320 $this->slotDiffOptions + [
321 'contentLanguage' => $this->getDiffLang()->getCode(),
322 'textDiffer' => $this->getTextDiffer()
323 ]
324 );
325 }
326 }
327
328 return $this->slotDiffRenderers;
329 }
330
337 public function markAsSlotDiffRenderer() {
338 $this->isSlotDiffRenderer = true;
339 }
340
346 protected function getSlotContents() {
347 if ( $this->isContentOverridden ) {
348 return [
349 SlotRecord::MAIN => [ 'old' => $this->mOldContent, 'new' => $this->mNewContent ]
350 ];
351 } elseif ( !$this->loadRevisionData() ) {
352 return [];
353 }
354
355 $newSlots = $this->mNewRevisionRecord->getPrimarySlots()->getSlots();
356 $oldSlots = $this->mOldRevisionRecord ?
357 $this->mOldRevisionRecord->getPrimarySlots()->getSlots() :
358 [];
359 // The order here will determine the visual order of the diff. The current logic is
360 // slots of the new revision first in natural order, then deleted ones. This is ad hoc
361 // and should not be relied on - in the future we may want the ordering to depend
362 // on the page type.
363 $roles = array_keys( array_merge( $newSlots, $oldSlots ) );
364
365 $slots = [];
366 foreach ( $roles as $role ) {
367 $slots[$role] = [
368 'old' => $this->loadSingleSlot(
369 $oldSlots[$role] ?? null,
370 'old'
371 ),
372 'new' => $this->loadSingleSlot(
373 $newSlots[$role] ?? null,
374 'new'
375 )
376 ];
377 }
378 // move main slot to front
379 if ( isset( $slots[SlotRecord::MAIN] ) ) {
380 $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
381 }
382 return $slots;
383 }
384
392 private function loadSingleSlot( ?SlotRecord $slot, string $which ) {
393 if ( !$slot ) {
394 return null;
395 }
396 try {
397 return $slot->getContent();
398 } catch ( BadRevisionException $e ) {
399 $this->addRevisionLoadError( $which );
400 return null;
401 }
402 }
403
409 private function addRevisionLoadError( $which ) {
410 $this->revisionLoadErrors[] = $this->msg( $which === 'new'
411 ? 'difference-bad-new-revision' : 'difference-bad-old-revision'
412 );
413 }
414
421 public function getRevisionLoadErrors() {
422 return $this->revisionLoadErrors;
423 }
424
429 private function hasNewRevisionLoadError() {
430 foreach ( $this->revisionLoadErrors as $error ) {
431 if ( $error->getKey() === 'difference-bad-new-revision' ) {
432 return true;
433 }
434 }
435 return false;
436 }
437
439 public function getTitle() {
440 // T202454 avoid errors when there is no title
441 return parent::getTitle() ?: Title::makeTitle( NS_SPECIAL, 'BadTitle/DifferenceEngine' );
442 }
443
450 public function setReducedLineNumbers( $value = true ) {
451 $this->mReducedLineNumbers = $value;
452 }
453
459 public function getDiffLang() {
460 # Default language in which the diff text is written.
461 $this->mDiffLang ??= $this->getDefaultLanguage();
462 return $this->mDiffLang;
463 }
464
471 protected function getDefaultLanguage() {
472 return $this->getTitle()->getPageLanguage();
473 }
474
478 public function wasCacheHit() {
479 return $this->mCacheHit;
480 }
481
489 public function getOldid() {
490 $this->loadRevisionIds();
491
492 return $this->mOldid;
493 }
494
501 public function getNewid() {
502 $this->loadRevisionIds();
503
504 return $this->mNewid;
505 }
506
513 public function getOldRevision() {
514 return $this->mOldRevisionRecord ?: null;
515 }
516
522 public function getNewRevision() {
523 return $this->mNewRevisionRecord;
524 }
525
534 public function deletedLink( $id ) {
535 if ( $this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
536 $revRecord = $this->archivedRevisionLookup->getArchivedRevisionRecord( null, $id );
537 if ( $revRecord ) {
538 $title = Title::newFromPageIdentity( $revRecord->getPage() );
539
540 return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( [
541 'target' => $title->getPrefixedText(),
542 'timestamp' => $revRecord->getTimestamp()
543 ] );
544 }
545 }
546
547 return false;
548 }
549
557 public function deletedIdMarker( $id ) {
558 $link = $this->deletedLink( $id );
559 if ( $link ) {
560 return "[$link $id]";
561 } else {
562 return (string)$id;
563 }
564 }
565
566 private function showMissingRevision() {
567 $out = $this->getOutput();
568
569 $missing = [];
570 if ( $this->mOldid && ( !$this->mOldRevisionRecord || !$this->mOldContent ) ) {
571 $missing[] = $this->deletedIdMarker( $this->mOldid );
572 }
573 if ( !$this->mNewRevisionRecord || !$this->mNewContent ) {
574 $missing[] = $this->deletedIdMarker( $this->mNewid );
575 }
576
577 $out->setPageTitleMsg( $this->msg( 'errorpagetitle' ) );
578 $msg = $this->msg( 'difference-missing-revision' )
579 ->params( $this->getLanguage()->listToText( $missing ) )
580 ->numParams( count( $missing ) )
581 ->parseAsBlock();
582 $out->addHTML( $msg );
583 }
584
590 public function hasDeletedRevision() {
591 $this->loadRevisionData();
592 return (
593 $this->mNewRevisionRecord &&
594 $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
595 ) ||
596 (
597 $this->mOldRevisionRecord &&
598 $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
599 );
600 }
601
608 public function getPermissionErrors( Authority $performer ) {
609 $this->loadRevisionData();
610 $permStatus = PermissionStatus::newEmpty();
611 if ( $this->mNewPage ) {
612 $performer->authorizeRead( 'read', $this->mNewPage, $permStatus );
613 }
614 if ( $this->mOldPage ) {
615 $performer->authorizeRead( 'read', $this->mOldPage, $permStatus );
616 }
617 return $permStatus->toLegacyErrorArray();
618 }
619
625 public function hasSuppressedRevision() {
626 return $this->hasDeletedRevision() && (
627 ( $this->mOldRevisionRecord &&
628 $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) ||
629 ( $this->mNewRevisionRecord &&
630 $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) )
631 );
632 }
633
640 private function getUserEditCount( $user ): string {
641 $editCount = $this->userEditTracker->getUserEditCount( $user );
642 if ( $editCount === null ) {
643 return '';
644 }
645
646 return Html::rawElement( 'div', [
647 'class' => 'mw-diff-usereditcount',
648 ],
649 $this->msg(
650 'diff-user-edits',
651 $this->getLanguage()->formatNum( $editCount )
652 )->parse()
653 );
654 }
655
662 private function getUserRoles( UserIdentity $user ) {
663 if ( !$this->userIdentityUtils->isNamed( $user ) ) {
664 return '';
665 }
666 $userGroups = $this->userGroupManager->getUserGroups( $user );
667 $userGroupLinks = [];
668 foreach ( $userGroups as $group ) {
669 $userGroupLinks[] = UserGroupMembership::getLinkHTML( $group, $this->getContext() );
670 }
671 return Html::rawElement( 'div', [
672 'class' => 'mw-diff-userroles',
673 ], $this->getLanguage()->commaList( $userGroupLinks ) );
674 }
675
682 private function getUserMetaData( ?UserIdentity $user ) {
683 if ( !$user ) {
684 return '';
685 }
686 return Html::rawElement( 'div', [
687 'class' => 'mw-diff-usermetadata',
688 ], $this->getUserRoles( $user ) . $this->getUserEditCount( $user ) );
689 }
690
702 public function isUserAllowedToSeeRevisions( Authority $performer ) {
703 $this->loadRevisionData();
704
705 if ( $this->mOldRevisionRecord && !$this->mOldRevisionRecord->userCan(
706 RevisionRecord::DELETED_TEXT,
707 $performer
708 ) ) {
709 return false;
710 }
711
712 // $this->mNewRev will only be falsy if a loading error occurred
713 // (in which case the user is allowed to see).
714 return !$this->mNewRevisionRecord || $this->mNewRevisionRecord->userCan(
715 RevisionRecord::DELETED_TEXT,
716 $performer
717 );
718 }
719
727 public function shouldBeHiddenFromUser( Authority $performer ) {
728 return $this->hasDeletedRevision() && ( !$this->unhide ||
729 !$this->isUserAllowedToSeeRevisions( $performer ) );
730 }
731
735 public function showDiffPage( $diffOnly = false ) {
736 # Allow frames except in certain special cases
737 $out = $this->getOutput();
738 $out->setPreventClickjacking( false );
739 $out->setRobotPolicy( 'noindex,nofollow' );
740
741 // Allow extensions to add any extra output here
742 $this->hookRunner->onDifferenceEngineShowDiffPage( $out );
743
744 if ( !$this->loadRevisionData() ) {
745 if ( $this->hookRunner->onDifferenceEngineShowDiffPageMaybeShowMissingRevision( $this ) ) {
746 $this->showMissingRevision();
747 }
748 return;
749 }
750
751 $user = $this->getUser();
752 $permErrors = $this->getPermissionErrors( $this->getAuthority() );
753 if ( $permErrors ) {
754 throw new PermissionsError( 'read', $permErrors );
755 }
756
757 $rollback = '';
758
759 $query = $this->extraQueryParams;
760 # Carry over 'diffonly' param via navigation links
761 if ( $diffOnly != MediaWikiServices::getInstance()
762 ->getUserOptionsLookup()->getBoolOption( $user, 'diffonly' )
763 ) {
764 $query['diffonly'] = $diffOnly;
765 }
766 # Cascade unhide param in links for easy deletion browsing
767 if ( $this->unhide ) {
768 $query['unhide'] = 1;
769 }
770
771 # Check if one of the revisions is deleted/suppressed
772 $deleted = $this->hasDeletedRevision();
773 $suppressed = $this->hasSuppressedRevision();
774 $allowed = $this->isUserAllowedToSeeRevisions( $this->getAuthority() );
775
776 $revisionTools = [];
777 $breadCrumbs = '';
778
779 # mOldRevisionRecord is false if the difference engine is called with a "vague" query for
780 # a diff between a version V and its previous version V' AND the version V
781 # is the first version of that article. In that case, V' does not exist.
782 if ( $this->mOldRevisionRecord === false ) {
783 if ( $this->mNewPage ) {
784 $out->setPageTitleMsg(
785 $this->msg( 'difference-title' )->plaintextParams( $this->mNewPage->getPrefixedText() )
786 );
787 }
788 $samePage = true;
789 $oldHeader = '';
790 // Allow extensions to change the $oldHeader variable
791 $this->hookRunner->onDifferenceEngineOldHeaderNoOldRev( $oldHeader );
792 } else {
793 $this->hookRunner->onDifferenceEngineViewHeader( $this );
794
795 if ( !$this->mOldPage || !$this->mNewPage ) {
796 // XXX say something to the user?
797 $samePage = false;
798 } elseif ( $this->mNewPage->equals( $this->mOldPage ) ) {
799 $out->setPageTitleMsg(
800 $this->msg( 'difference-title' )->plaintextParams( $this->mNewPage->getPrefixedText() )
801 );
802 $samePage = true;
803 } else {
804 $out->setPageTitleMsg( $this->msg( 'difference-title-multipage' )->plaintextParams(
805 $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) );
806 $out->addSubtitle( $this->msg( 'difference-multipage' ) );
807 $samePage = false;
808 }
809
810 if ( $samePage && $this->mNewPage &&
811 $this->getAuthority()->probablyCan( 'edit', $this->mNewPage )
812 ) {
813 if ( $this->mNewRevisionRecord->isCurrent() &&
814 $this->getAuthority()->probablyCan( 'rollback', $this->mNewPage )
815 ) {
816 $rollbackLink = Linker::generateRollback(
817 $this->mNewRevisionRecord,
818 $this->getContext(),
819 [ 'noBrackets' ]
820 );
821 if ( $rollbackLink ) {
822 $out->setPreventClickjacking( true );
823 $rollback = "\u{00A0}\u{00A0}\u{00A0}" . $rollbackLink;
824 }
825 }
826
827 if ( $this->userCanEdit( $this->mOldRevisionRecord ) &&
828 $this->userCanEdit( $this->mNewRevisionRecord )
829 ) {
830 $undoLink = $this->linkRenderer->makeKnownLink(
831 $this->mNewPage,
832 $this->msg( 'editundo' )->text(),
833 [ 'title' => Linker::titleAttrib( 'undo' ) ],
834 [
835 'action' => 'edit',
836 'undoafter' => $this->mOldid,
837 'undo' => $this->mNewid
838 ]
839 );
840 $revisionTools['mw-diff-undo'] = $undoLink;
841 }
842 }
843 # Make "previous revision link"
844 $hasPrevious = $samePage && $this->mOldPage &&
845 $this->revisionStore->getPreviousRevision( $this->mOldRevisionRecord );
846 if ( $hasPrevious ) {
847 $prevlinkQuery = [ 'diff' => 'prev', 'oldid' => $this->mOldid ] + $query;
848 $prevlink = $this->linkRenderer->makeKnownLink(
849 $this->mOldPage,
850 $this->msg( 'previousdiff' )->text(),
851 [ 'id' => 'differences-prevlink' ],
852 $prevlinkQuery
853 );
854 $breadCrumbs .= $this->linkRenderer->makeKnownLink(
855 $this->mOldPage,
856 $this->msg( 'previousdiff' )->text(),
857 [
858 'class' => 'mw-diff-revision-history-link-previous'
859 ],
860 $prevlinkQuery
861 );
862 } else {
863 $prevlink = "\u{00A0}";
864 }
865
866 if ( $this->mOldRevisionRecord->isMinor() ) {
867 $oldminor = ChangesList::flag( 'minor' );
868 } else {
869 $oldminor = '';
870 }
871
872 $oldRevRecord = $this->mOldRevisionRecord;
873
874 $ldel = $this->revisionDeleteLink( $oldRevRecord );
875 $oldRevisionHeader = $this->getRevisionHeader( $oldRevRecord, 'complete' );
876 $oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff', $this->getContext() );
877 $oldRevComment = $this->commentFormatter
878 ->formatRevision(
879 $oldRevRecord, $user, !$diffOnly, !$this->unhide, false
880 );
881
882 if ( $oldRevComment === '' ) {
883 $defaultComment = $this->msg( 'changeslist-nocomment' )->escaped();
884 $oldRevComment = "<span class=\"comment mw-comment-none\">$defaultComment</span>";
885 }
886
887 $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' .
888 '<div id="mw-diff-otitle2">' .
889 Linker::revUserTools( $oldRevRecord, !$this->unhide ) .
890 $this->getUserMetaData( $oldRevRecord->getUser() ) .
891 '</div>' .
892 '<div id="mw-diff-otitle3">' . $oldminor . $oldRevComment . $ldel . '</div>' .
893 '<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' .
894 '<div id="mw-diff-otitle4">' . $prevlink . '</div>';
895
896 // Allow extensions to change the $oldHeader variable
897 $this->hookRunner->onDifferenceEngineOldHeader(
898 $this, $oldHeader, $prevlink, $oldminor, $diffOnly, $ldel, $this->unhide );
899 }
900
901 $out->addJsConfigVars( [
902 'wgDiffOldId' => $this->mOldid,
903 'wgDiffNewId' => $this->mNewid,
904 ] );
905
906 # Make "next revision link"
907 # Skip next link on the top revision
908 if ( $samePage && $this->mNewPage && !$this->mNewRevisionRecord->isCurrent() ) {
909 $nextlinkQuery = [ 'diff' => 'next', 'oldid' => $this->mNewid ] + $query;
910 $nextlink = $this->linkRenderer->makeKnownLink(
911 $this->mNewPage,
912 $this->msg( 'nextdiff' )->text(),
913 [ 'id' => 'differences-nextlink' ],
914 $nextlinkQuery
915 );
916 $breadCrumbs .= $this->linkRenderer->makeKnownLink(
917 $this->mNewPage,
918 $this->msg( 'nextdiff' )->text(),
919 [
920 'class' => 'mw-diff-revision-history-link-next'
921 ],
922 $nextlinkQuery
923 );
924 } else {
925 $nextlink = "\u{00A0}";
926 }
927
928 if ( $this->mNewRevisionRecord->isMinor() ) {
929 $newminor = ChangesList::flag( 'minor' );
930 } else {
931 $newminor = '';
932 }
933
934 # Handle RevisionDelete links...
935 $rdel = $this->revisionDeleteLink( $this->mNewRevisionRecord );
936
937 # Allow extensions to define their own revision tools
938 $this->hookRunner->onDiffTools(
939 $this->mNewRevisionRecord,
940 $revisionTools,
941 $this->mOldRevisionRecord ?: null,
942 $user
943 );
944
945 $formattedRevisionTools = [];
946 // Put each one in parentheses (poor man's button)
947 foreach ( $revisionTools as $key => $tool ) {
948 $toolClass = is_string( $key ) ? $key : 'mw-diff-tool';
949 $element = Html::rawElement(
950 'span',
951 [ 'class' => $toolClass ],
952 $tool
953 );
954 $formattedRevisionTools[] = $element;
955 }
956
957 $newRevRecord = $this->mNewRevisionRecord;
958
959 $newRevisionHeader = $this->getRevisionHeader( $newRevRecord, 'complete' ) .
960 ' ' . implode( ' ', $formattedRevisionTools );
961 $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff', $this->getContext() );
962 $newRevComment = $this->commentFormatter->formatRevision(
963 $newRevRecord, $user, !$diffOnly, !$this->unhide, false
964 );
965
966 if ( $newRevComment === '' ) {
967 $defaultComment = $this->msg( 'changeslist-nocomment' )->escaped();
968 $newRevComment = "<span class=\"comment mw-comment-none\">$defaultComment</span>";
969 }
970
971 $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' .
972 '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $newRevRecord, !$this->unhide ) .
973 $rollback .
974 $this->getUserMetaData( $newRevRecord->getUser() ) .
975 '</div>' .
976 '<div id="mw-diff-ntitle3">' . $newminor . $newRevComment . $rdel . '</div>' .
977 '<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' .
978 '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
979
980 // Allow extensions to change the $newHeader variable
981 $this->hookRunner->onDifferenceEngineNewHeader( $this, $newHeader,
982 $formattedRevisionTools, $nextlink, $rollback, $newminor, $diffOnly,
983 $rdel, $this->unhide );
984
985 $out->addHTML(
986 Html::rawElement( 'div', [
987 'class' => 'mw-diff-revision-history-links'
988 ], $breadCrumbs )
989 );
990 # If the diff cannot be shown due to a deleted revision, then output
991 # the diff header and links to unhide (if available)...
992 if ( $this->shouldBeHiddenFromUser( $this->getAuthority() ) ) {
993 $this->showDiffStyle();
994 $multi = $this->getMultiNotice();
995 $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
996 if ( !$allowed ) {
997 # Give explanation for why revision is not visible
998 $msg = [ $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff' ];
999 } else {
1000 # Give explanation and add a link to view the diff...
1001 $query = $this->getRequest()->appendQueryValue( 'unhide', '1' );
1002 $msg = [
1003 $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff',
1004 $this->getTitle()->getFullURL( $query )
1005 ];
1006 }
1007 $out->addHTML( Html::warningBox( $this->msg( ...$msg )->parse(), 'plainlinks' ) );
1008 # Otherwise, output a regular diff...
1009 } else {
1010 # Add deletion notice if the user is viewing deleted content
1011 $notice = '';
1012 if ( $deleted ) {
1013 $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
1014 $notice = Html::warningBox( $this->msg( $msg )->parse(), 'plainlinks' );
1015 }
1016
1017 # Add an error if the content can't be loaded
1018 $this->getSlotContents();
1019 foreach ( $this->getRevisionLoadErrors() as $msg ) {
1020 $notice .= Html::warningBox( $msg->parse() );
1021 }
1022
1023 // Check if inline switcher will be needed
1024 if ( $this->getTextDiffer()->hasFormat( 'inline' ) ) {
1025 $out->enableOOUI();
1026 }
1027
1028 $this->showTablePrefixes();
1029 $this->showDiff( $oldHeader, $newHeader, $notice );
1030 if ( !$diffOnly ) {
1031 $this->renderNewRevision();
1032 }
1033
1034 // Allow extensions to optionally not show the final patrolled link
1035 if ( $this->hookRunner->onDifferenceEngineRenderRevisionShowFinalPatrolLink() ) {
1036 # Add redundant patrol link on bottom...
1037 $out->addHTML( $this->markPatrolledLink() );
1038 }
1039 }
1040 }
1041
1045 private function showTablePrefixes() {
1046 $parts = [];
1047 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1048 $parts += $slotDiffRenderer->getTablePrefix( $this->getContext(), $this->mNewPage );
1049 }
1050 ksort( $parts );
1051 if ( count( array_filter( $parts ) ) > 0 ) {
1052 $language = $this->getLanguage();
1053 $attrs = [
1054 'class' => 'mw-diff-table-prefix',
1055 'dir' => $language->getDir(),
1056 'lang' => $language->getCode(),
1057 ];
1058 $this->getOutput()->addHTML(
1059 Html::rawElement( 'div', $attrs, implode( '', $parts ) ) );
1060 }
1061 }
1062
1073 public function markPatrolledLink() {
1074 if ( $this->mMarkPatrolledLink === null ) {
1075 $linkInfo = $this->getMarkPatrolledLinkInfo();
1076 // If false, there is no patrol link needed/allowed
1077 if ( !$linkInfo || !$this->mNewPage ) {
1078 $this->mMarkPatrolledLink = '';
1079 } else {
1080 $patrolLinkClass = 'patrollink';
1081 $this->mMarkPatrolledLink = ' <span class="' . $patrolLinkClass . '" data-mw="interface">[' .
1082 $this->linkRenderer->makeKnownLink(
1083 $this->mNewPage,
1084 $this->msg( 'markaspatrolleddiff' )->text(),
1085 [],
1086 [
1087 'action' => 'markpatrolled',
1088 'rcid' => $linkInfo['rcid'],
1089 ]
1090 ) . ']</span>';
1091 // Allow extensions to change the markpatrolled link
1092 $this->hookRunner->onDifferenceEngineMarkPatrolledLink( $this,
1093 $this->mMarkPatrolledLink, $linkInfo['rcid'] );
1094 }
1095 }
1096 return $this->mMarkPatrolledLink;
1097 }
1098
1106 protected function getMarkPatrolledLinkInfo() {
1107 $user = $this->getUser();
1108 $config = $this->getConfig();
1109
1110 // Prepare a change patrol link, if applicable
1111 if (
1112 // Is patrolling enabled and the user allowed to?
1113 $config->get( MainConfigNames::UseRCPatrol ) &&
1114 $this->mNewPage &&
1115 $this->getAuthority()->probablyCan( 'patrol', $this->mNewPage ) &&
1116 // Only do this if the revision isn't more than 6 hours older
1117 // than the Max RC age (6h because the RC might not be cleaned out regularly)
1118 RecentChange::isInRCLifespan( $this->mNewRevisionRecord->getTimestamp(), 21600 )
1119 ) {
1120 // Look for an unpatrolled change corresponding to this diff
1121 $change = RecentChange::newFromConds(
1122 [
1123 'rc_this_oldid' => $this->mNewid,
1124 'rc_patrolled' => RecentChange::PRC_UNPATROLLED
1125 ],
1126 __METHOD__
1127 );
1128
1129 if ( $change && !$change->getPerformerIdentity()->equals( $user ) ) {
1130 $rcid = $change->getAttribute( 'rc_id' );
1131 } else {
1132 // None found or the page has been created by the current user.
1133 // If the user could patrol this it already would be patrolled
1134 $rcid = 0;
1135 }
1136
1137 // Allow extensions to possibly change the rcid here
1138 // For example the rcid might be set to zero due to the user
1139 // being the same as the performer of the change but an extension
1140 // might still want to show it under certain conditions
1141 $this->hookRunner->onDifferenceEngineMarkPatrolledRCID( $rcid, $this, $change, $user );
1142
1143 // Build the link
1144 if ( $rcid ) {
1145 $this->getOutput()->setPreventClickjacking( true );
1146 if ( $this->getAuthority()->isAllowed( 'writeapi' ) ) {
1147 $this->getOutput()->addModules( 'mediawiki.misc-authed-curate' );
1148 }
1149
1150 return [ 'rcid' => $rcid ];
1151 }
1152 }
1153
1154 // No mark as patrolled link applicable
1155 return false;
1156 }
1157
1163 private function revisionDeleteLink( RevisionRecord $revRecord ) {
1164 $link = Linker::getRevDeleteLink(
1165 $this->getAuthority(),
1166 $revRecord,
1167 $revRecord->getPageAsLinkTarget()
1168 );
1169 if ( $link !== '' ) {
1170 $link = "\u{00A0}\u{00A0}\u{00A0}" . $link . ' ';
1171 }
1172
1173 return $link;
1174 }
1175
1181 public function renderNewRevision() {
1182 if ( $this->isContentOverridden ) {
1183 // The code below only works with a RevisionRecord object. We could construct a
1184 // fake RevisionRecord (here or in setContent), but since this does not seem
1185 // needed at the moment, we'll just fail for now.
1186 throw new LogicException(
1187 __METHOD__
1188 . ' is not supported after calling setContent(). Use setRevisions() instead.'
1189 );
1190 }
1191
1192 $out = $this->getOutput();
1193 $revHeader = $this->getRevisionHeader( $this->mNewRevisionRecord );
1194 # Add "current version as of X" title
1195 $out->addHTML( "<hr class='diff-hr' id='mw-oldid' />
1196 <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
1197 # Page content may be handled by a hooked call instead...
1198 if ( $this->hookRunner->onArticleContentOnDiff( $this, $out ) ) {
1199 $this->loadNewText();
1200 if ( !$this->mNewPage ) {
1201 // New revision is unsaved; bail out.
1202 // TODO in theory rendering the new revision is a meaningful thing to do
1203 // even if it's unsaved, but a lot of untangling is required to do it safely.
1204 return;
1205 }
1206 if ( $this->hasNewRevisionLoadError() ) {
1207 // There was an error loading the new revision
1208 return;
1209 }
1210
1211 $out->setRevisionId( $this->mNewid );
1212 $out->setRevisionIsCurrent( $this->mNewRevisionRecord->isCurrent() );
1213 $out->setRevisionTimestamp( $this->mNewRevisionRecord->getTimestamp() );
1214 $out->setArticleFlag( true );
1215
1216 if ( !$this->hookRunner->onArticleRevisionViewCustom(
1217 $this->mNewRevisionRecord, $this->mNewPage, $this->mOldid, $out )
1218 ) {
1219 // Handled by extension
1220 // NOTE: sync with hooks called in Article::view()
1221 } else {
1222 // Normal page
1223 if ( $this->getTitle()->equals( $this->mNewPage ) ) {
1224 // If the Title stored in the context is the same as the one
1225 // of the new revision, we can use its associated WikiPage
1226 // object.
1227 $wikiPage = $this->getWikiPage();
1228 } else {
1229 // Otherwise we need to create our own WikiPage object
1230 $wikiPage = $this->wikiPageFactory->newFromTitle( $this->mNewPage );
1231 }
1232
1233 $parserOptions = $wikiPage->makeParserOptions( $this->getContext() );
1234 $parserOptions->setRenderReason( 'diff-page' );
1235
1236 $parserOutputAccess = MediaWikiServices::getInstance()->getParserOutputAccess();
1237 $status = $parserOutputAccess->getParserOutput(
1238 $wikiPage,
1239 $parserOptions,
1240 $this->mNewRevisionRecord,
1241 // we already checked
1242 ParserOutputAccess::OPT_NO_AUDIENCE_CHECK |
1243 // Update cascading protection
1244 ParserOutputAccess::OPT_LINKS_UPDATE
1245 );
1246 if ( $status->isOK() ) {
1247 $parserOutput = $status->getValue();
1248 // Allow extensions to change parser output here
1249 if ( $this->hookRunner->onDifferenceEngineRenderRevisionAddParserOutput(
1250 $this, $out, $parserOutput, $wikiPage )
1251 ) {
1252 $out->addParserOutput( $parserOutput, [
1253 'enableSectionEditLinks' => $this->mNewRevisionRecord->isCurrent()
1254 && $this->getAuthority()->probablyCan(
1255 'edit',
1256 $this->mNewRevisionRecord->getPage()
1257 ),
1258 'absoluteURLs' => $this->slotDiffOptions['expand-url'] ?? false
1259 ] );
1260 }
1261 } else {
1262 $out->addHTML(
1263 Html::errorBox(
1264 $out->parseAsInterface(
1265 $status->getWikiText( false, false, $this->getLanguage() )
1266 )
1267 )
1268 );
1269 }
1270 }
1271 }
1272 }
1273
1284 public function showDiff( $otitle, $ntitle, $notice = '' ) {
1285 // Allow extensions to affect the output here
1286 $this->hookRunner->onDifferenceEngineShowDiff( $this );
1287
1288 $diff = $this->getDiff( $otitle, $ntitle, $notice );
1289 if ( $diff === false ) {
1290 $this->showMissingRevision();
1291 return false;
1292 }
1293
1294 $this->showDiffStyle();
1295 if ( $this->slotDiffOptions['expand-url'] ?? false ) {
1296 $diff = Linker::expandLocalLinks( $diff );
1297 }
1298 $this->getOutput()->addHTML( $diff );
1299 return true;
1300 }
1301
1305 public function showDiffStyle() {
1306 if ( !$this->isSlotDiffRenderer ) {
1307 $this->getOutput()->addModules( 'mediawiki.diff' );
1308 $this->getOutput()->addModuleStyles( [
1309 'mediawiki.interface.helpers.styles',
1310 'mediawiki.diff.styles'
1311 ] );
1312 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1313 $slotDiffRenderer->addModules( $this->getOutput() );
1314 }
1315 }
1316 }
1317
1327 public function getDiff( $otitle, $ntitle, $notice = '' ) {
1328 $body = $this->getDiffBody();
1329 if ( $body === false ) {
1330 return false;
1331 }
1332
1333 $multi = $this->getMultiNotice();
1334 // Display a message when the diff is empty
1335 if ( $body === '' ) {
1336 $notice .= '<div class="mw-diff-empty">' .
1337 $this->msg( 'diff-empty' )->parse() .
1338 "</div>\n";
1339 }
1340
1341 if ( $this->cacheHitKey !== null ) {
1342 $body .= "\n<!-- diff cache key " . htmlspecialchars( $this->cacheHitKey ) . " -->\n";
1343 }
1344
1345 return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
1346 }
1347
1353 public function getDiffBody() {
1354 $this->mCacheHit = true;
1355 // Check if the diff should be hidden from this user
1356 if ( !$this->isContentOverridden ) {
1357 if ( !$this->loadRevisionData() ) {
1358 return false;
1359 } elseif ( $this->mOldRevisionRecord &&
1360 !$this->mOldRevisionRecord->userCan(
1361 RevisionRecord::DELETED_TEXT,
1362 $this->getAuthority()
1363 )
1364 ) {
1365 return false;
1366 } elseif ( $this->mNewRevisionRecord &&
1367 !$this->mNewRevisionRecord->userCan(
1368 RevisionRecord::DELETED_TEXT,
1369 $this->getAuthority()
1370 ) ) {
1371 return false;
1372 }
1373 // Short-circuit
1374 if ( $this->mOldRevisionRecord === false || (
1375 $this->mOldRevisionRecord &&
1376 $this->mNewRevisionRecord &&
1377 $this->mOldRevisionRecord->getId() &&
1378 $this->mOldRevisionRecord->getId() == $this->mNewRevisionRecord->getId()
1379 ) ) {
1380 if ( $this->hookRunner->onDifferenceEngineShowEmptyOldContent( $this ) ) {
1381 return '';
1382 }
1383 }
1384 }
1385
1386 // Cacheable?
1387 $key = false;
1388 $services = MediaWikiServices::getInstance();
1389 $cache = $services->getMainWANObjectCache();
1390 $stats = $services->getStatsdDataFactory();
1391 if ( $this->mOldid && $this->mNewid ) {
1392 $key = $cache->makeKey( ...$this->getDiffBodyCacheKeyParams() );
1393
1394 // Try cache
1395 if ( !$this->mRefreshCache ) {
1396 $difftext = $cache->get( $key );
1397 if ( is_string( $difftext ) ) {
1398 $stats->updateCount( 'diff_cache.hit', 1 );
1399 $difftext = $this->localiseDiff( $difftext );
1400 $this->cacheHitKey = $key;
1401 return $difftext;
1402 }
1403 } // don't try to load but save the result
1404 }
1405 $this->mCacheHit = false;
1406 $this->cacheHitKey = null;
1407
1408 // Loadtext is permission safe, this just clears out the diff
1409 if ( !$this->loadText() ) {
1410 return false;
1411 }
1412
1413 $difftext = '';
1414 // We've checked for revdelete at the beginning of this method; it's OK to ignore
1415 // read permissions here.
1416 $slotContents = $this->getSlotContents();
1417 foreach ( $this->getSlotDiffRenderers() as $role => $slotDiffRenderer ) {
1418 try {
1419 $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role]['old'],
1420 $slotContents[$role]['new'] );
1421 } catch ( IncompatibleDiffTypesException $e ) {
1422 $slotDiff = $this->getSlotError( $e->getMessageObject()->parse() );
1423 }
1424 if ( $slotDiff && $role !== SlotRecord::MAIN ) {
1425 // FIXME: ask SlotRoleHandler::getSlotNameMessage
1426 $slotTitle = $role;
1427 $difftext .= $this->getSlotHeader( $slotTitle );
1428 }
1429 $difftext .= $slotDiff;
1430 }
1431
1432 // Save to cache for 7 days
1433 if ( !$this->hookRunner->onAbortDiffCache( $this ) ) {
1434 $stats->updateCount( 'diff_cache.uncacheable', 1 );
1435 } elseif ( $key !== false ) {
1436 $stats->updateCount( 'diff_cache.miss', 1 );
1437 $cache->set( $key, $difftext, 7 * 86400 );
1438 } else {
1439 $stats->updateCount( 'diff_cache.uncacheable', 1 );
1440 }
1441 // localise line numbers and title attribute text
1442 $difftext = $this->localiseDiff( $difftext );
1443
1444 return $difftext;
1445 }
1446
1453 public function getDiffBodyForRole( $role ) {
1454 $diffRenderers = $this->getSlotDiffRenderers();
1455 if ( !isset( $diffRenderers[$role] ) ) {
1456 return false;
1457 }
1458
1459 $slotContents = $this->getSlotContents();
1460 try {
1461 $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role]['old'],
1462 $slotContents[$role]['new'] );
1463 } catch ( IncompatibleDiffTypesException $e ) {
1464 $slotDiff = $this->getSlotError( $e->getMessageObject()->parse() );
1465 }
1466 if ( $slotDiff === '' ) {
1467 return false;
1468 }
1469
1470 if ( $role !== SlotRecord::MAIN ) {
1471 // TODO use human-readable role name at least
1472 $slotTitle = $role;
1473 $slotDiff = $this->getSlotHeader( $slotTitle ) . $slotDiff;
1474 }
1475
1476 return $this->localiseDiff( $slotDiff );
1477 }
1478
1486 protected function getSlotHeader( $headerText ) {
1487 // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
1488 $columnCount = $this->mOldRevisionRecord ? 4 : 2;
1489 $userLang = $this->getLanguage()->getHtmlCode();
1490 return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-header', 'lang' => $userLang ],
1491 Html::element( 'th', [ 'colspan' => $columnCount ], $headerText ) );
1492 }
1493
1500 protected function getSlotError( $errorText ) {
1501 // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
1502 $columnCount = $this->mOldRevisionRecord ? 4 : 2;
1503 $userLang = $this->getLanguage()->getHtmlCode();
1504 return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-error', 'lang' => $userLang ],
1505 Html::rawElement( 'td', [ 'colspan' => $columnCount ], $errorText ) );
1506 }
1507
1521 protected function getDiffBodyCacheKeyParams() {
1522 if ( !$this->mOldid || !$this->mNewid ) {
1523 throw new BadMethodCallException( 'mOldid and mNewid must be set to get diff cache key.' );
1524 }
1525
1526 $params = [
1527 'diff',
1528 self::DIFF_VERSION,
1529 "old-{$this->mOldid}",
1530 "rev-{$this->mNewid}"
1531 ];
1532
1533 $extraKeys = [];
1534 if ( !$this->isSlotDiffRenderer ) {
1535 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1536 $extraKeys = array_merge( $extraKeys, $slotDiffRenderer->getExtraCacheKeys() );
1537 }
1538 }
1539 ksort( $extraKeys );
1540 return array_merge( $params, array_values( $extraKeys ) );
1541 }
1542
1550 public function getExtraCacheKeys() {
1551 // This method is called when the DifferenceEngine is used for a slot diff. We only care
1552 // about special things, not the revision IDs, which are added to the cache key by the
1553 // page-level DifferenceEngine, and which might not have a valid value for this object.
1554 $this->mOldid = 123456789;
1555 $this->mNewid = 987654321;
1556
1557 // This will repeat a bunch of unnecessary key fields for each slot. Not nice but harmless.
1558 $params = $this->getDiffBodyCacheKeyParams();
1559
1560 // Try to get rid of the standard keys to keep the cache key human-readable:
1561 // call the getDiffBodyCacheKeyParams implementation of the base class, and if
1562 // the child class includes the same keys, drop them.
1563 // Uses an obscure PHP feature where static calls to non-static methods are allowed
1564 // as long as we are already in a non-static method of the same class, and the call context
1565 // ($this) will be inherited.
1566 // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
1568 if ( array_slice( $params, 0, count( $standardParams ) ) === $standardParams ) {
1569 $params = array_slice( $params, count( $standardParams ) );
1570 }
1571
1572 return $params;
1573 }
1574
1585 public function setSlotDiffOptions( $options ) {
1586 $validatedOptions = [];
1587 if ( isset( $options['diff-type'] )
1588 && $this->getTextDiffer()->hasFormat( $options['diff-type'] )
1589 ) {
1590 $validatedOptions['diff-type'] = $options['diff-type'];
1591 }
1592 if ( !empty( $options['expand-url'] ) ) {
1593 $validatedOptions['expand-url'] = true;
1594 }
1595 if ( !empty( $options['inline-toggle'] ) ) {
1596 $validatedOptions['inline-toggle'] = true;
1597 }
1598 $this->slotDiffOptions = $validatedOptions;
1599 }
1600
1608 public function setExtraQueryParams( $params ) {
1609 $this->extraQueryParams = $params;
1610 }
1611
1625 public function generateContentDiffBody( Content $old, Content $new ) {
1626 $slotDiffRenderer = $new->getContentHandler()->getSlotDiffRenderer( $this->getContext() );
1627 if (
1628 $slotDiffRenderer instanceof DifferenceEngineSlotDiffRenderer
1629 && $this->isSlotDiffRenderer
1630 ) {
1631 // Oops, we are just about to enter an infinite loop (the slot-level DifferenceEngine
1632 // called a DifferenceEngineSlotDiffRenderer that wraps the same DifferenceEngine class).
1633 // This will happen when a content model has no custom slot diff renderer, it does have
1634 // a custom difference engine, but that does not override this method.
1635 throw new LogicException( get_class( $this ) . ': could not maintain backwards compatibility. '
1636 . 'Please use a SlotDiffRenderer.' );
1637 }
1638 return $slotDiffRenderer->getDiff( $old, $new ) . $this->getDebugString();
1639 }
1640
1653 public function generateTextDiffBody( $otext, $ntext ) {
1654 $slotDiffRenderer = $this->contentHandlerFactory
1655 ->getContentHandler( CONTENT_MODEL_TEXT )
1656 ->getSlotDiffRenderer( $this->getContext() );
1657 if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1658 // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1659 // This is too unlikely to happen to bother handling properly.
1660 throw new LogicException( 'The slot diff renderer for text content should be a '
1661 . 'TextSlotDiffRenderer subclass' );
1662 }
1663 return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1664 }
1665
1672 public static function getEngine() {
1673 $differenceEngine = new self;
1674 $engine = $differenceEngine->getTextDiffer()->getEngineForFormat( 'table' );
1675 if ( $engine === 'external' ) {
1676 return MediaWikiServices::getInstance()->getMainConfig()
1677 ->get( MainConfigNames::ExternalDiffEngine );
1678 } else {
1679 return $engine;
1680 }
1681 }
1682
1691 protected function debug( $generator = "internal" ) {
1692 if ( !$this->enableDebugComment ) {
1693 return '';
1694 }
1695 $data = [ $generator ];
1696 if ( $this->getConfig()->get( MainConfigNames::ShowHostnames ) ) {
1697 $data[] = wfHostname();
1698 }
1699 $data[] = wfTimestamp( TS_DB );
1700
1701 return "<!-- diff generator: " .
1702 implode( " ", array_map( "htmlspecialchars", $data ) ) .
1703 " -->\n";
1704 }
1705
1709 private function getDebugString() {
1710 $engine = self::getEngine();
1711 if ( $engine === 'wikidiff2' ) {
1712 return $this->debug( 'wikidiff2' );
1713 } elseif ( $engine === 'php' ) {
1714 return $this->debug( 'native PHP' );
1715 } else {
1716 return $this->debug( "external $engine" );
1717 }
1718 }
1719
1726 private function localiseDiff( $text ) {
1727 return $this->getTextDiffer()->localize( $this->getTextDiffFormat(), $text );
1728 }
1729
1738 public function localiseLineNumbers( $text ) {
1739 return preg_replace_callback( '/<!--LINE (\d+)-->/',
1740 function ( array $matches ) {
1741 if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
1742 return '';
1743 }
1744 return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
1745 }, $text );
1746 }
1747
1753 public function getMultiNotice() {
1754 // The notice only make sense if we are diffing two saved revisions of the same page.
1755 if (
1756 !$this->mOldRevisionRecord || !$this->mNewRevisionRecord
1757 || !$this->mOldPage || !$this->mNewPage
1758 || !$this->mOldPage->equals( $this->mNewPage )
1759 || $this->mOldRevisionRecord->getId() === null
1760 || $this->mNewRevisionRecord->getId() === null
1761 // (T237709) Deleted revs might have different page IDs
1762 || $this->mNewPage->getArticleID() !== $this->mOldRevisionRecord->getPageId()
1763 || $this->mNewPage->getArticleID() !== $this->mNewRevisionRecord->getPageId()
1764 ) {
1765 return '';
1766 }
1767
1768 if ( $this->mOldRevisionRecord->getTimestamp() > $this->mNewRevisionRecord->getTimestamp() ) {
1769 $oldRevRecord = $this->mNewRevisionRecord; // flip
1770 $newRevRecord = $this->mOldRevisionRecord; // flip
1771 } else { // normal case
1772 $oldRevRecord = $this->mOldRevisionRecord;
1773 $newRevRecord = $this->mNewRevisionRecord;
1774 }
1775
1776 // Don't show the notice if too many rows must be scanned
1777 // @todo show some special message for that case
1778 $nEdits = 0;
1779 $revisionIdList = $this->revisionStore->getRevisionIdsBetween(
1780 $this->mNewPage->getArticleID(),
1781 $oldRevRecord,
1782 $newRevRecord,
1783 1000
1784 );
1785 // only count revisions that are visible
1786 if ( count( $revisionIdList ) > 0 ) {
1787 foreach ( $revisionIdList as $revisionId ) {
1788 $revision = $this->revisionStore->getRevisionById( $revisionId );
1789 if ( $revision->getUser( RevisionRecord::FOR_THIS_USER, $this->getAuthority() ) ) {
1790 $nEdits++;
1791 }
1792 }
1793 }
1794 if ( $nEdits > 0 && $nEdits <= 1000 ) {
1795 // Use an invalid username to get the wiki's default gender (as fallback)
1796 $newRevUserForGender = '[HIDDEN]';
1797 $limit = 100; // use diff-multi-manyusers if too many users
1798 try {
1799 $users = $this->revisionStore->getAuthorsBetween(
1800 $this->mNewPage->getArticleID(),
1801 $oldRevRecord,
1802 $newRevRecord,
1803 null,
1804 $limit
1805 );
1806 $numUsers = count( $users );
1807
1808 $newRevUser = $newRevRecord->getUser( RevisionRecord::RAW );
1809 $newRevUserText = $newRevUser ? $newRevUser->getName() : '';
1810 $newRevUserSafe = $newRevRecord->getUser(
1811 RevisionRecord::FOR_THIS_USER,
1812 $this->getAuthority()
1813 );
1814 $newRevUserForGender = $newRevUserSafe ? $newRevUserSafe->getName() : '[HIDDEN]';
1815 if ( $numUsers == 1 && $users[0]->getName() == $newRevUserText ) {
1816 $numUsers = 0; // special case to say "by the same user" instead of "by one other user"
1817 }
1818 } catch ( InvalidArgumentException $e ) {
1819 $numUsers = 0;
1820 }
1821
1822 return self::intermediateEditsMsg( $nEdits, $numUsers, $limit, $newRevUserForGender );
1823 }
1824
1825 return '';
1826 }
1827
1838 public static function intermediateEditsMsg( $numEdits, $numUsers, $limit, $lastUser = '[HIDDEN]' ) {
1839 if ( $numUsers === 0 ) {
1840 $msg = 'diff-multi-sameuser';
1841 return wfMessage( $msg )
1842 ->numParams( $numEdits, $numUsers )
1843 ->params( $lastUser )
1844 ->parse();
1845 } elseif ( $numUsers > $limit ) {
1846 $msg = 'diff-multi-manyusers';
1847 $numUsers = $limit;
1848 } else {
1849 $msg = 'diff-multi-otherusers';
1850 }
1851
1852 return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1853 }
1854
1859 private function userCanEdit( RevisionRecord $revRecord ) {
1860 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
1861 return false;
1862 }
1863
1864 return true;
1865 }
1866
1876 public function getRevisionHeader( RevisionRecord $rev, $complete = '' ) {
1877 $lang = $this->getLanguage();
1878 $user = $this->getUser();
1879 $revtimestamp = $rev->getTimestamp();
1880 $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
1881 $dateofrev = $lang->userDate( $revtimestamp, $user );
1882 $timeofrev = $lang->userTime( $revtimestamp, $user );
1883
1884 $header = $this->msg(
1885 $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
1886 $timestamp,
1887 $dateofrev,
1888 $timeofrev
1889 );
1890
1891 if ( $complete !== 'complete' ) {
1892 return $header->escaped();
1893 }
1894
1895 $title = $rev->getPageAsLinkTarget();
1896
1897 if ( $this->userCanEdit( $rev ) ) {
1898 $header = $this->linkRenderer->makeKnownLink(
1899 $title,
1900 $header->text(),
1901 [],
1902 [ 'oldid' => $rev->getId() ]
1903 );
1904 $editQuery = [ 'action' => 'edit' ];
1905 if ( !$rev->isCurrent() ) {
1906 $editQuery['oldid'] = $rev->getId();
1907 }
1908
1909 $key = $this->getAuthority()->probablyCan( 'edit', $rev->getPage() ) ? 'editold' : 'viewsourceold';
1910 $msg = $this->msg( $key )->text();
1911 $editLink = $this->linkRenderer->makeKnownLink( $title, $msg, [], $editQuery );
1912 $header .= ' ' . Html::rawElement(
1913 'span',
1914 [ 'class' => 'mw-diff-edit' ],
1915 $editLink
1916 );
1917 } else {
1918 $header = $header->escaped();
1919 }
1920
1921 // Machine readable information
1922 $header .= Html::element( 'span',
1923 [
1924 'class' => 'mw-diff-timestamp',
1925 'data-timestamp' => wfTimestamp( TS_ISO_8601, $revtimestamp ),
1926 ], ''
1927 );
1928
1929 if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1930 return Html::rawElement(
1931 'span',
1932 [ 'class' => Linker::getRevisionDeletedClass( $rev ) ],
1933 $header
1934 );
1935 }
1936
1937 return $header;
1938 }
1939
1952 public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
1953 // shared.css sets diff in interface language/dir, but the actual content
1954 // is often in a different language, mostly the page content language/dir
1955 $header = Html::openElement( 'table', [
1956 'class' => [
1957 'diff',
1958 // The following classes are used here:
1959 // * diff-type-table
1960 // * diff-type-inline
1961 'diff-type-' . $this->getTextDiffFormat(),
1962 // The following classes are used here:
1963 // * diff-contentalign-left
1964 // * diff-contentalign-right
1965 'diff-contentalign-' . $this->getDiffLang()->alignStart(),
1966 // The following classes are used here:
1967 // * diff-editfont-monospace
1968 // * diff-editfont-sans-serif
1969 // * diff-editfont-serif
1970 'diff-editfont-' . $this->userOptionsLookup->getOption(
1971 $this->getUser(),
1972 'editfont'
1973 )
1974 ],
1975 'data-mw' => 'interface',
1976 ] );
1977 $userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
1978
1979 if ( !$diff && !$otitle ) {
1980 $header .= "
1981 <tr class=\"diff-title\" lang=\"{$userLang}\">
1982 <td class=\"diff-ntitle\">{$ntitle}</td>
1983 </tr>";
1984 $multiColspan = 1;
1985 } else {
1986 if ( $diff ) { // Safari/Chrome show broken output if cols not used
1987 $header .= "
1988 <col class=\"diff-marker\" />
1989 <col class=\"diff-content\" />
1990 <col class=\"diff-marker\" />
1991 <col class=\"diff-content\" />";
1992 $colspan = 2;
1993 $multiColspan = 4;
1994 } else {
1995 $colspan = 1;
1996 $multiColspan = 2;
1997 }
1998 if ( $otitle || $ntitle ) {
1999 // FIXME Hardcoding values from TableDiffFormatter.
2000 $deletedClass = 'diff-side-deleted';
2001 $addedClass = 'diff-side-added';
2002 $header .= "
2003 <tr class=\"diff-title\" lang=\"{$userLang}\">
2004 <td colspan=\"$colspan\" class=\"diff-otitle {$deletedClass}\">{$otitle}</td>
2005 <td colspan=\"$colspan\" class=\"diff-ntitle {$addedClass}\">{$ntitle}</td>
2006 </tr>";
2007 }
2008 }
2009
2010 if ( $multi != '' ) {
2011 $header .= "<tr><td colspan=\"{$multiColspan}\" " .
2012 "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
2013 }
2014 if ( $notice != '' ) {
2015 $header .= "<tr><td colspan=\"{$multiColspan}\" " .
2016 "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
2017 }
2018
2019 return $header . $diff . "</table>";
2020 }
2021
2029 public function setContent( Content $oldContent, Content $newContent ) {
2030 $this->mOldContent = $oldContent;
2031 $this->mNewContent = $newContent;
2032
2033 $this->mTextLoaded = 2;
2034 $this->mRevisionsLoaded = true;
2035 $this->isContentOverridden = true;
2036 $this->slotDiffRenderers = null;
2037 }
2038
2044 public function setRevisions(
2045 ?RevisionRecord $oldRevision, RevisionRecord $newRevision
2046 ) {
2047 if ( $oldRevision ) {
2048 $this->mOldRevisionRecord = $oldRevision;
2049 $this->mOldid = $oldRevision->getId();
2050 $this->mOldPage = Title::newFromLinkTarget( $oldRevision->getPageAsLinkTarget() );
2051 // This method is meant for edit diffs and such so there is no reason to provide a
2052 // revision that's not readable to the user, but check it just in case.
2053 $this->mOldContent = $oldRevision->getContent( SlotRecord::MAIN,
2054 RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
2055 if ( !$this->mOldContent ) {
2056 $this->addRevisionLoadError( 'old' );
2057 }
2058 } else {
2059 $this->mOldPage = null;
2060 $this->mOldRevisionRecord = $this->mOldid = false;
2061 }
2062 $this->mNewRevisionRecord = $newRevision;
2063 $this->mNewid = $newRevision->getId();
2064 $this->mNewPage = Title::newFromLinkTarget( $newRevision->getPageAsLinkTarget() );
2065 $this->mNewContent = $newRevision->getContent( SlotRecord::MAIN,
2066 RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
2067 if ( !$this->mNewContent ) {
2068 $this->addRevisionLoadError( 'new' );
2069 }
2070
2071 $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded = true;
2072 $this->mTextLoaded = $oldRevision ? 2 : 1;
2073 $this->isContentOverridden = false;
2074 $this->slotDiffRenderers = null;
2075 }
2076
2083 public function setTextLanguage( Language $lang ) {
2084 $this->mDiffLang = $lang;
2085 }
2086
2099 public function mapDiffPrevNext( $old, $new ) {
2100 if ( $new === 'prev' ) {
2101 // Show diff between revision $old and the previous one. Get previous one from DB.
2102 $newid = intval( $old );
2103 $oldid = false;
2104 $newRev = $this->revisionStore->getRevisionById( $newid );
2105 if ( $newRev ) {
2106 $oldRev = $this->revisionStore->getPreviousRevision( $newRev );
2107 if ( $oldRev ) {
2108 $oldid = $oldRev->getId();
2109 }
2110 }
2111 } elseif ( $new === 'next' ) {
2112 // Show diff between revision $old and the next one. Get next one from DB.
2113 $oldid = intval( $old );
2114 $newid = false;
2115 $oldRev = $this->revisionStore->getRevisionById( $oldid );
2116 if ( $oldRev ) {
2117 $newRev = $this->revisionStore->getNextRevision( $oldRev );
2118 if ( $newRev ) {
2119 $newid = $newRev->getId();
2120 }
2121 }
2122 } else {
2123 $oldid = intval( $old );
2124 $newid = intval( $new );
2125 }
2126
2127 // @phan-suppress-next-line PhanTypeMismatchReturn getId does not return null here
2128 return [ $oldid, $newid ];
2129 }
2130
2131 private function loadRevisionIds() {
2132 if ( $this->mRevisionsIdsLoaded ) {
2133 return;
2134 }
2135
2136 $this->mRevisionsIdsLoaded = true;
2137
2138 $old = $this->mOldid;
2139 $new = $this->mNewid;
2140
2141 [ $this->mOldid, $this->mNewid ] = self::mapDiffPrevNext( $old, $new );
2142 if ( $new === 'next' && $this->mNewid === false ) {
2143 # if no result, NewId points to the newest old revision. The only newer
2144 # revision is cur, which is "0".
2145 $this->mNewid = 0;
2146 }
2147
2148 $this->hookRunner->onNewDifferenceEngine(
2149 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
2150 $this->getTitle(), $this->mOldid, $this->mNewid, $old, $new );
2151 }
2152
2166 public function loadRevisionData() {
2167 if ( $this->mRevisionsLoaded ) {
2168 return $this->isContentOverridden ||
2169 ( $this->mOldRevisionRecord !== null && $this->mNewRevisionRecord !== null );
2170 }
2171
2172 // Whether it succeeds or fails, we don't want to try again
2173 $this->mRevisionsLoaded = true;
2174
2175 $this->loadRevisionIds();
2176
2177 // Load the new RevisionRecord object
2178 if ( $this->mNewid ) {
2179 $this->mNewRevisionRecord = $this->revisionStore->getRevisionById( $this->mNewid );
2180 } else {
2181 $this->mNewRevisionRecord = $this->revisionStore->getRevisionByTitle( $this->getTitle() );
2182 }
2183
2184 if ( !$this->mNewRevisionRecord instanceof RevisionRecord ) {
2185 return false;
2186 }
2187
2188 // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
2189 $this->mNewid = $this->mNewRevisionRecord->getId();
2190 $this->mNewPage = $this->mNewid ?
2191 Title::newFromLinkTarget( $this->mNewRevisionRecord->getPageAsLinkTarget() ) :
2192 null;
2193
2194 // Load the old RevisionRecord object
2195 $this->mOldRevisionRecord = false;
2196 if ( $this->mOldid ) {
2197 $this->mOldRevisionRecord = $this->revisionStore->getRevisionById( $this->mOldid );
2198 } elseif ( $this->mOldid === 0 ) {
2199 $revRecord = $this->revisionStore->getPreviousRevision( $this->mNewRevisionRecord );
2200 // No previous revision; mark to show as first-version only.
2201 $this->mOldid = $revRecord ? $revRecord->getId() : false;
2202 $this->mOldRevisionRecord = $revRecord ?? false;
2203 } /* elseif ( $this->mOldid === false ) leave mOldRevisionRecord false; */
2204
2205 if ( $this->mOldRevisionRecord === null ) {
2206 return false;
2207 }
2208
2209 if ( $this->mOldRevisionRecord && $this->mOldRevisionRecord->getId() ) {
2210 $this->mOldPage = Title::newFromLinkTarget(
2211 $this->mOldRevisionRecord->getPageAsLinkTarget()
2212 );
2213 } else {
2214 $this->mOldPage = null;
2215 }
2216
2217 // Load tags information for both revisions
2218 $dbr = $this->dbProvider->getReplicaDatabase();
2219 $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
2220 if ( $this->mOldid !== false ) {
2221 $tagIds = $dbr->newSelectQueryBuilder()
2222 ->select( 'ct_tag_id' )
2223 ->from( 'change_tag' )
2224 ->where( [ 'ct_rev_id' => $this->mOldid ] )
2225 ->caller( __METHOD__ )->fetchFieldValues();
2226 $tags = [];
2227 foreach ( $tagIds as $tagId ) {
2228 try {
2229 $tags[] = $changeTagDefStore->getName( (int)$tagId );
2230 } catch ( NameTableAccessException $exception ) {
2231 continue;
2232 }
2233 }
2234 $this->mOldTags = implode( ',', $tags );
2235 } else {
2236 $this->mOldTags = false;
2237 }
2238
2239 $tagIds = $dbr->newSelectQueryBuilder()
2240 ->select( 'ct_tag_id' )
2241 ->from( 'change_tag' )
2242 ->where( [ 'ct_rev_id' => $this->mNewid ] )
2243 ->caller( __METHOD__ )->fetchFieldValues();
2244 $tags = [];
2245 foreach ( $tagIds as $tagId ) {
2246 try {
2247 $tags[] = $changeTagDefStore->getName( (int)$tagId );
2248 } catch ( NameTableAccessException $exception ) {
2249 continue;
2250 }
2251 }
2252 $this->mNewTags = implode( ',', $tags );
2253
2254 return true;
2255 }
2256
2265 public function loadText() {
2266 if ( $this->mTextLoaded == 2 ) {
2267 return $this->loadRevisionData() &&
2268 ( $this->mOldRevisionRecord === false || $this->mOldContent )
2269 && $this->mNewContent;
2270 }
2271
2272 // Whether it succeeds or fails, we don't want to try again
2273 $this->mTextLoaded = 2;
2274
2275 if ( !$this->loadRevisionData() ) {
2276 return false;
2277 }
2278
2279 if ( $this->mOldRevisionRecord ) {
2280 $this->mOldContent = $this->mOldRevisionRecord->getContent(
2281 SlotRecord::MAIN,
2282 RevisionRecord::FOR_THIS_USER,
2283 $this->getAuthority()
2284 );
2285 if ( $this->mOldContent === null ) {
2286 return false;
2287 }
2288 }
2289
2290 $this->mNewContent = $this->mNewRevisionRecord->getContent(
2291 SlotRecord::MAIN,
2292 RevisionRecord::FOR_THIS_USER,
2293 $this->getAuthority()
2294 );
2295 $this->hookRunner->onDifferenceEngineLoadTextAfterNewContentIsLoaded( $this );
2296 if ( $this->mNewContent === null ) {
2297 return false;
2298 }
2299
2300 return true;
2301 }
2302
2308 public function loadNewText() {
2309 if ( $this->mTextLoaded >= 1 ) {
2310 return $this->loadRevisionData();
2311 }
2312
2313 $this->mTextLoaded = 1;
2314
2315 if ( !$this->loadRevisionData() ) {
2316 return false;
2317 }
2318
2319 $this->mNewContent = $this->mNewRevisionRecord->getContent(
2320 SlotRecord::MAIN,
2321 RevisionRecord::FOR_THIS_USER,
2322 $this->getAuthority()
2323 );
2324
2325 $this->hookRunner->onDifferenceEngineAfterLoadNewText( $this );
2326
2327 return true;
2328 }
2329
2335 protected function getTextDiffer() {
2336 if ( $this->textDiffer === null ) {
2337 $this->textDiffer = new ManifoldTextDiffer(
2338 $this->getContext(),
2339 $this->getDiffLang(),
2340 $this->getConfig()->get( MainConfigNames::DiffEngine ),
2341 $this->getConfig()->get( MainConfigNames::ExternalDiffEngine ),
2342 $this->getConfig()->get( MainConfigNames::Wikidiff2Options )
2343 );
2344 }
2345 return $this->textDiffer;
2346 }
2347
2354 public function getSupportedFormats() {
2355 return $this->getTextDiffer()->getFormats();
2356 }
2357
2364 public function getTextDiffFormat() {
2365 return $this->slotDiffOptions['diff-type'] ?? 'table';
2366 }
2367
2368}
getUser()
getRequest()
getAuthority()
const NS_SPECIAL
Definition Defines.php:53
const CONTENT_MODEL_TEXT
Definition Defines.php:223
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfHostname()
Get host name of the current machine, for use in error reporting.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
getContext()
array $params
The job parameters.
static formatSummaryRow( $tags, $unused, MessageLocalizer $localizer=null)
Creates HTML for the given tags.
B/C adapter for turning a DifferenceEngine into a SlotDiffRenderer.
DifferenceEngine is responsible for rendering the difference between two revisions as HTML.
bool $enableDebugComment
Set this to true to add debug info to the HTML output.
bool $unhide
Show rev_deleted content if allowed.
bool $isContentOverridden
Was the content overridden via setContent()? If the content was overridden, most internal state (e....
getExtraCacheKeys()
Implements DifferenceEngineSlotDiffRenderer::getExtraCacheKeys().
markAsSlotDiffRenderer()
Mark this DifferenceEngine as a slot renderer (as opposed to a page renderer).
getSlotHeader( $headerText)
Get a slot header for inclusion in a diff body (as a table row).
setSlotDiffOptions( $options)
hasDeletedRevision()
Checks whether one of the given Revisions was deleted.
int $mTextLoaded
How many text blobs have been loaded, 0, 1 or 2?
deletedIdMarker( $id)
Build a wikitext link toward a deleted revision, if viewable.
SlotDiffRenderer[] null $slotDiffRenderers
DifferenceEngine classes for the slots, keyed by role name.
getDiffBodyForRole( $role)
Get the diff table body for one slot, without header.
getTextDiffer()
Get the TextDiffer which will be used for rendering text.
getDefaultLanguage()
Get the language to use if none has been set by setTextLanguage().
getOldid()
Get the ID of old revision (left pane) of the diff.
setRevisions(?RevisionRecord $oldRevision, RevisionRecord $newRevision)
Use specified text instead of loading from the database.
bool $isSlotDiffRenderer
Temporary hack for B/C while slot diff related methods of DifferenceEngine are being deprecated.
generateTextDiffBody( $otext, $ntext)
Generate a diff, no caching.
loadNewText()
Load the text of the new revision, not the old one.
showDiffPage( $diffOnly=false)
loadText()
Load the text of the revisions, as well as revision data.
int string false null $mNewid
Revision ID for the new revision.
mapDiffPrevNext( $old, $new)
Maps a revision pair definition as accepted by DifferenceEngine constructor to a pair of actual integ...
getPermissionErrors(Authority $performer)
Get the permission errors associated with the revisions for the current diff.
getDiffBody()
Get the diff table body, without header.
getTitle()
1.18 to override Title|null
loadRevisionData()
Load revision metadata for the specified revisions.
static getEngine()
Process DiffEngine config and get a sensible, usable engine.
bool $mRevisionsLoaded
Have the revisions been loaded.
getNewRevision()
Get the right side of the diff.
showDiff( $otitle, $ntitle, $notice='')
Get the diff text, send it to the OutputPage object Returns false if the diff could not be generated,...
getTextDiffFormat()
Get the selected text diff format.
localiseLineNumbers( $text)
Replace a common convention for language-independent line numbers with the text in the user's languag...
getSlotContents()
Get the old and new content objects for all slots.
string $mMarkPatrolledLink
Link to action=markpatrolled.
deletedLink( $id)
Look up a special:Undelete link to the given deleted revision id, as a workaround for being unable to...
bool $mReducedLineNumbers
If true, line X is not displayed when X is 1, for example to increase readability and conserve space ...
__construct( $context=null, $old=0, $new=0, $rcid=0, $refreshCache=false, $unhide=false)
Title null $mNewPage
Title of new revision or null if the new revision does not exist or does not belong to a page.
bool $mCacheHit
Was the diff fetched from cache?
getMultiNotice()
If there are revisions between the ones being compared, return a note saying so.
isUserAllowedToSeeRevisions(Authority $performer)
Checks whether the current user has permission for accessing the revisions of the diff.
int false null $mOldid
Revision ID for the old revision.
debug( $generator="internal")
Generate a debug comment indicating diff generating time, server node, and generator backend.
addHeader( $diff, $otitle, $ntitle, $multi='', $notice='')
Add the header to a diff body.
bool $mRefreshCache
Refresh the diff cache.
LinkRenderer $linkRenderer
getDiffBodyCacheKeyParams()
Get the cache key parameters.
getDiff( $otitle, $ntitle, $notice='')
Get complete diff table, including header.
setExtraQueryParams( $params)
Set query parameters to append to diff page links.
static intermediateEditsMsg( $numEdits, $numUsers, $limit, $lastUser='[HIDDEN]')
Get a notice about how many intermediate edits and users there are.
getNewid()
Get the ID of new revision (right pane) of the diff.
renderNewRevision()
Show the new revision of the page.
setContent(Content $oldContent, Content $newContent)
Use specified text instead of loading from the database.
setTextLanguage(Language $lang)
Set the language in which the diff text is written.
generateContentDiffBody(Content $old, Content $new)
Generate a diff, no caching.
getSlotError( $errorText)
Get an error message for inclusion in a diff body (as a table row).
shouldBeHiddenFromUser(Authority $performer)
Checks whether the diff should be hidden from the current user This is based on whether the user is a...
getRevisionHeader(RevisionRecord $rev, $complete='')
Get a header for a specified revision.
getMarkPatrolledLinkInfo()
Returns an array of meta data needed to build a "mark as patrolled" link and adds a JS module to the ...
setReducedLineNumbers( $value=true)
Set reduced line numbers mode.
getSupportedFormats()
Get the list of supported text diff formats.
Title null $mOldPage
Title of old revision or null if the old revision does not exist or does not belong to a page.
getDiffLang()
Get the language in which the diff text is written.
showDiffStyle()
Add style sheets for diff display.
markPatrolledLink()
Build a link to mark a change as patrolled.
getRevisionLoadErrors()
If errors were encountered while loading the revision contents, this will return an array of Messages...
hasSuppressedRevision()
Checks whether one of the given Revisions was suppressed.
getOldRevision()
Get the left side of the diff.
Exception thrown when trying to render a diff between two content types which cannot be compared (thi...
Base class for language-specific code.
Definition Language.php:63
getMessageObject()
Return a Message object for this exception.
This is the main service interface for converting single-line comments from various DB comment fields...
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()
A TextDiffer which acts as a container for other TextDiffers, and dispatches requests to them.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
Class that generates HTML for internal links.
Some internal bits split of from Skin.php.
Definition Linker.php:65
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Service for getting rendered output of a given page.
Service for creating WikiPage objects.
A StatusValue for permission errors.
Exception raised when the text of a revision is permanently missing or corrupt.
Page revision base class.
getPage()
Returns the page this revision belongs to.
isCurrent()
Checks whether the revision record is a stored current revision.
getTimestamp()
MCR migration note: this replaced Revision::getTimestamp.
getContent( $role, $audience=self::FOR_PUBLIC, Authority $performer=null)
Returns the Content of the given slot of this revision.
getPageAsLinkTarget()
Returns the title of the page this revision is associated with as a LinkTarget object.
userCan( $field, Authority $performer)
Determine if the give authority is allowed to view a particular field of this revision,...
isDeleted( $field)
MCR migration note: this replaced Revision::isDeleted.
getId( $wikiId=self::LOCAL)
Get revision ID.
Service for looking up page revisions.
Value object representing a content slot associated with a page revision.
getContent()
Returns the Content of the given slot.
Parent class for all special pages.
Exception representing a failure to look up a row from a name table.
Represents a title within MediaWiki.
Definition Title.php:78
Provides access to user options.
Track info about user edit counts and timings.
Represents a "user group membership" – a specific instance of a user belonging to a group.
Convenience functions for interpreting UserIdentity objects using additional services or config.
Show an error when a user tries to do something they do not have the necessary permissions for.
Renders a diff for a single slot (that is, a diff between two content objects).
Renders a slot diff by doing a text diff on the native representation.
Base interface for representing page content.
Definition Content.php:37
getContentHandler()
Convenience method that returns the ContentHandler singleton for handling the content model that this...
Interface for objects which can provide a MediaWiki context on request.
This interface represents the authority associated with the current execution context,...
Definition Authority.php:37
authorizeRead(string $action, PageIdentity $target, PermissionStatus $status=null)
Authorize read access.
Interface for objects representing user identity.
Provide primary and replica IDatabase connections.
$header