MediaWiki master
DifferenceEngine.php
Go to the documentation of this file.
1<?php
28use MediaWiki\Debug\DeprecationHelper;
56
80
81 use DeprecationHelper;
82
89 private const DIFF_VERSION = '1.41';
90
97 protected $mOldid;
98
105 protected $mNewid;
106
117 private $mOldRevisionRecord;
118
127 private $mNewRevisionRecord;
128
133 protected $mOldPage;
134
139 protected $mNewPage;
140
145 private $mOldTags;
146
151 private $mNewTags;
152
158 private $mOldContent;
159
165 private $mNewContent;
166
168 protected $mDiffLang;
169
171 private $mRevisionsIdsLoaded = false;
172
174 protected $mRevisionsLoaded = false;
175
177 protected $mTextLoaded = 0;
178
187 protected $isContentOverridden = false;
188
190 protected $mCacheHit = false;
191
193 private $cacheHitKey = null;
194
201 public $enableDebugComment = false;
202
206 protected $mReducedLineNumbers = false;
207
209 protected $mMarkPatrolledLink = null;
210
212 protected $unhide = false;
213
215 protected $mRefreshCache = false;
216
218 protected $slotDiffRenderers = null;
219
226 protected $isSlotDiffRenderer = false;
227
232 private $slotDiffOptions = [];
233
237 private $extraQueryParams = [];
238
240 private $textDiffer;
241
243 private IContentHandlerFactory $contentHandlerFactory;
244 private RevisionStore $revisionStore;
245 private ArchivedRevisionLookup $archivedRevisionLookup;
246 private HookRunner $hookRunner;
247 private WikiPageFactory $wikiPageFactory;
248 private UserOptionsLookup $userOptionsLookup;
249 private CommentFormatter $commentFormatter;
250 private IConnectionProvider $dbProvider;
251 private UserGroupManager $userGroupManager;
252 private UserEditTracker $userEditTracker;
253 private UserIdentityUtils $userIdentityUtils;
254
256 private $revisionLoadErrors = [];
257
266 public function __construct( $context = null, $old = 0, $new = 0, $rcid = 0,
267 $refreshCache = false, $unhide = false
268 ) {
269 if ( $context instanceof IContextSource ) {
270 $this->setContext( $context );
271 }
272
273 wfDebug( "DifferenceEngine old '$old' new '$new' rcid '$rcid'" );
274
275 $this->mOldid = $old;
276 $this->mNewid = $new;
277 $this->mRefreshCache = $refreshCache;
278 $this->unhide = $unhide;
279
280 $services = MediaWikiServices::getInstance();
281 $this->linkRenderer = $services->getLinkRenderer();
282 $this->contentHandlerFactory = $services->getContentHandlerFactory();
283 $this->revisionStore = $services->getRevisionStore();
284 $this->archivedRevisionLookup = $services->getArchivedRevisionLookup();
285 $this->hookRunner = new HookRunner( $services->getHookContainer() );
286 $this->wikiPageFactory = $services->getWikiPageFactory();
287 $this->userOptionsLookup = $services->getUserOptionsLookup();
288 $this->commentFormatter = $services->getCommentFormatter();
289 $this->dbProvider = $services->getConnectionProvider();
290 $this->userGroupManager = $services->getUserGroupManager();
291 $this->userEditTracker = $services->getUserEditTracker();
292 $this->userIdentityUtils = $services->getUserIdentityUtils();
293 }
294
300 protected function getSlotDiffRenderers() {
301 if ( $this->isSlotDiffRenderer ) {
302 throw new LogicException( __METHOD__ . ' called in slot diff renderer mode' );
303 }
304
305 if ( $this->slotDiffRenderers === null ) {
306 if ( !$this->loadRevisionData() ) {
307 return [];
308 }
309
310 $slotContents = $this->getSlotContents();
311 $this->slotDiffRenderers = [];
312 foreach ( $slotContents as $role => $contents ) {
313 if ( $contents['new'] && $contents['old']
314 && $contents['new']->equals( $contents['old'] )
315 ) {
316 // Do not produce a diff of identical content
317 continue;
318 }
319 $handler = ( $contents['new'] ?: $contents['old'] )->getContentHandler();
320 $this->slotDiffRenderers[$role] = $handler->getSlotDiffRenderer(
321 $this->getContext(),
322 $this->slotDiffOptions + [
323 'contentLanguage' => $this->getDiffLang()->getCode(),
324 'textDiffer' => $this->getTextDiffer()
325 ]
326 );
327 }
328 }
329
330 return $this->slotDiffRenderers;
331 }
332
339 public function markAsSlotDiffRenderer() {
340 $this->isSlotDiffRenderer = true;
341 }
342
348 protected function getSlotContents() {
349 if ( $this->isContentOverridden ) {
350 return [
351 SlotRecord::MAIN => [ 'old' => $this->mOldContent, 'new' => $this->mNewContent ]
352 ];
353 } elseif ( !$this->loadRevisionData() ) {
354 return [];
355 }
356
357 $newSlots = $this->mNewRevisionRecord->getPrimarySlots()->getSlots();
358 $oldSlots = $this->mOldRevisionRecord ?
359 $this->mOldRevisionRecord->getPrimarySlots()->getSlots() :
360 [];
361 // The order here will determine the visual order of the diff. The current logic is
362 // slots of the new revision first in natural order, then deleted ones. This is ad hoc
363 // and should not be relied on - in the future we may want the ordering to depend
364 // on the page type.
365 $roles = array_keys( array_merge( $newSlots, $oldSlots ) );
366
367 $slots = [];
368 foreach ( $roles as $role ) {
369 $slots[$role] = [
370 'old' => $this->loadSingleSlot(
371 $oldSlots[$role] ?? null,
372 'old'
373 ),
374 'new' => $this->loadSingleSlot(
375 $newSlots[$role] ?? null,
376 'new'
377 )
378 ];
379 }
380 // move main slot to front
381 if ( isset( $slots[SlotRecord::MAIN] ) ) {
382 $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
383 }
384 return $slots;
385 }
386
394 private function loadSingleSlot( ?SlotRecord $slot, string $which ) {
395 if ( !$slot ) {
396 return null;
397 }
398 try {
399 return $slot->getContent();
400 } catch ( BadRevisionException $e ) {
401 $this->addRevisionLoadError( $which );
402 return null;
403 }
404 }
405
411 private function addRevisionLoadError( $which ) {
412 $this->revisionLoadErrors[] = $this->msg( $which === 'new'
413 ? 'difference-bad-new-revision' : 'difference-bad-old-revision'
414 );
415 }
416
423 public function getRevisionLoadErrors() {
424 return $this->revisionLoadErrors;
425 }
426
431 private function hasNewRevisionLoadError() {
432 foreach ( $this->revisionLoadErrors as $error ) {
433 if ( $error->getKey() === 'difference-bad-new-revision' ) {
434 return true;
435 }
436 }
437 return false;
438 }
439
441 public function getTitle() {
442 // T202454 avoid errors when there is no title
443 return parent::getTitle() ?: Title::makeTitle( NS_SPECIAL, 'BadTitle/DifferenceEngine' );
444 }
445
452 public function setReducedLineNumbers( $value = true ) {
453 $this->mReducedLineNumbers = $value;
454 }
455
461 public function getDiffLang() {
462 # Default language in which the diff text is written.
463 $this->mDiffLang ??= $this->getDefaultLanguage();
464 return $this->mDiffLang;
465 }
466
473 protected function getDefaultLanguage() {
474 return $this->getTitle()->getPageLanguage();
475 }
476
480 public function wasCacheHit() {
481 return $this->mCacheHit;
482 }
483
491 public function getOldid() {
492 $this->loadRevisionIds();
493
494 return $this->mOldid;
495 }
496
503 public function getNewid() {
504 $this->loadRevisionIds();
505
506 return $this->mNewid;
507 }
508
515 public function getOldRevision() {
516 return $this->mOldRevisionRecord ?: null;
517 }
518
524 public function getNewRevision() {
525 return $this->mNewRevisionRecord;
526 }
527
536 public function deletedLink( $id ) {
537 if ( $this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
538 $revRecord = $this->archivedRevisionLookup->getArchivedRevisionRecord( null, $id );
539 if ( $revRecord ) {
540 $title = Title::newFromPageIdentity( $revRecord->getPage() );
541
542 return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( [
543 'target' => $title->getPrefixedText(),
544 'timestamp' => $revRecord->getTimestamp()
545 ] );
546 }
547 }
548
549 return false;
550 }
551
559 public function deletedIdMarker( $id ) {
560 $link = $this->deletedLink( $id );
561 if ( $link ) {
562 return "[$link $id]";
563 } else {
564 return (string)$id;
565 }
566 }
567
568 private function showMissingRevision() {
569 $out = $this->getOutput();
570
571 $missing = [];
572 if ( $this->mOldid && ( !$this->mOldRevisionRecord || !$this->mOldContent ) ) {
573 $missing[] = $this->deletedIdMarker( $this->mOldid );
574 }
575 if ( !$this->mNewRevisionRecord || !$this->mNewContent ) {
576 $missing[] = $this->deletedIdMarker( $this->mNewid );
577 }
578
579 $out->setPageTitleMsg( $this->msg( 'errorpagetitle' ) );
580 $msg = $this->msg( 'difference-missing-revision' )
581 ->params( $this->getLanguage()->listToText( $missing ) )
582 ->numParams( count( $missing ) )
583 ->parseAsBlock();
584 $out->addHTML( $msg );
585 }
586
592 public function hasDeletedRevision() {
593 $this->loadRevisionData();
594 return (
595 $this->mNewRevisionRecord &&
596 $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
597 ) ||
598 (
599 $this->mOldRevisionRecord &&
600 $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
601 );
602 }
603
610 public function getPermissionErrors( Authority $performer ) {
611 $this->loadRevisionData();
612 $permStatus = PermissionStatus::newEmpty();
613 if ( $this->mNewPage ) {
614 $performer->authorizeRead( 'read', $this->mNewPage, $permStatus );
615 }
616 if ( $this->mOldPage ) {
617 $performer->authorizeRead( 'read', $this->mOldPage, $permStatus );
618 }
619 return $permStatus->toLegacyErrorArray();
620 }
621
627 public function hasSuppressedRevision() {
628 return $this->hasDeletedRevision() && (
629 ( $this->mOldRevisionRecord &&
630 $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) ||
631 ( $this->mNewRevisionRecord &&
632 $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) )
633 );
634 }
635
642 private function getUserEditCount( $user ): string {
643 $editCount = $this->userEditTracker->getUserEditCount( $user );
644 if ( $editCount === null ) {
645 return '';
646 }
647
648 return Html::rawElement( 'div', [
649 'class' => 'mw-diff-usereditcount',
650 ],
651 $this->msg(
652 'diff-user-edits',
653 $this->getLanguage()->formatNum( $editCount )
654 )->parse()
655 );
656 }
657
664 private function getUserRoles( UserIdentity $user ) {
665 if ( !$this->userIdentityUtils->isNamed( $user ) ) {
666 return '';
667 }
668 $userGroups = $this->userGroupManager->getUserGroups( $user );
669 $userGroupLinks = [];
670 foreach ( $userGroups as $group ) {
671 $userGroupLinks[] = UserGroupMembership::getLinkHTML( $group, $this->getContext() );
672 }
673 return Html::rawElement( 'div', [
674 'class' => 'mw-diff-userroles',
675 ], $this->getLanguage()->commaList( $userGroupLinks ) );
676 }
677
684 private function getUserMetaData( ?UserIdentity $user ) {
685 if ( !$user ) {
686 return '';
687 }
688 return Html::rawElement( 'div', [
689 'class' => 'mw-diff-usermetadata',
690 ], $this->getUserRoles( $user ) . $this->getUserEditCount( $user ) );
691 }
692
704 public function isUserAllowedToSeeRevisions( Authority $performer ) {
705 $this->loadRevisionData();
706
707 if ( $this->mOldRevisionRecord && !$this->mOldRevisionRecord->userCan(
708 RevisionRecord::DELETED_TEXT,
709 $performer
710 ) ) {
711 return false;
712 }
713
714 // $this->mNewRev will only be falsy if a loading error occurred
715 // (in which case the user is allowed to see).
716 return !$this->mNewRevisionRecord || $this->mNewRevisionRecord->userCan(
717 RevisionRecord::DELETED_TEXT,
718 $performer
719 );
720 }
721
729 public function shouldBeHiddenFromUser( Authority $performer ) {
730 return $this->hasDeletedRevision() && ( !$this->unhide ||
731 !$this->isUserAllowedToSeeRevisions( $performer ) );
732 }
733
737 public function showDiffPage( $diffOnly = false ) {
738 # Allow frames except in certain special cases
739 $out = $this->getOutput();
740 $out->setPreventClickjacking( false );
741 $out->setRobotPolicy( 'noindex,nofollow' );
742
743 // Allow extensions to add any extra output here
744 $this->hookRunner->onDifferenceEngineShowDiffPage( $out );
745
746 if ( !$this->loadRevisionData() ) {
747 if ( $this->hookRunner->onDifferenceEngineShowDiffPageMaybeShowMissingRevision( $this ) ) {
748 $this->showMissingRevision();
749 }
750 return;
751 }
752
753 $user = $this->getUser();
754 $permErrors = $this->getPermissionErrors( $this->getAuthority() );
755 if ( $permErrors ) {
756 throw new PermissionsError( 'read', $permErrors );
757 }
758
759 $rollback = '';
760
761 $query = $this->extraQueryParams;
762 # Carry over 'diffonly' param via navigation links
763 if ( $diffOnly != MediaWikiServices::getInstance()
764 ->getUserOptionsLookup()->getBoolOption( $user, 'diffonly' )
765 ) {
766 $query['diffonly'] = $diffOnly;
767 }
768 # Cascade unhide param in links for easy deletion browsing
769 if ( $this->unhide ) {
770 $query['unhide'] = 1;
771 }
772
773 # Check if one of the revisions is deleted/suppressed
774 $deleted = $this->hasDeletedRevision();
775 $suppressed = $this->hasSuppressedRevision();
776 $allowed = $this->isUserAllowedToSeeRevisions( $this->getAuthority() );
777
778 $revisionTools = [];
779 $breadCrumbs = '';
780
781 # mOldRevisionRecord is false if the difference engine is called with a "vague" query for
782 # a diff between a version V and its previous version V' AND the version V
783 # is the first version of that article. In that case, V' does not exist.
784 if ( $this->mOldRevisionRecord === false ) {
785 if ( $this->mNewPage ) {
786 $out->setPageTitleMsg(
787 $this->msg( 'difference-title' )->plaintextParams( $this->mNewPage->getPrefixedText() )
788 );
789 }
790 $samePage = true;
791 $oldHeader = '';
792 // Allow extensions to change the $oldHeader variable
793 $this->hookRunner->onDifferenceEngineOldHeaderNoOldRev( $oldHeader );
794 } else {
795 $this->hookRunner->onDifferenceEngineViewHeader( $this );
796
797 if ( !$this->mOldPage || !$this->mNewPage ) {
798 // XXX say something to the user?
799 $samePage = false;
800 } elseif ( $this->mNewPage->equals( $this->mOldPage ) ) {
801 $out->setPageTitleMsg(
802 $this->msg( 'difference-title' )->plaintextParams( $this->mNewPage->getPrefixedText() )
803 );
804 $samePage = true;
805 } else {
806 $out->setPageTitleMsg( $this->msg( 'difference-title-multipage' )->plaintextParams(
807 $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) );
808 $out->addSubtitle( $this->msg( 'difference-multipage' ) );
809 $samePage = false;
810 }
811
812 if ( $samePage && $this->mNewPage &&
813 $this->getAuthority()->probablyCan( 'edit', $this->mNewPage )
814 ) {
815 if ( $this->mNewRevisionRecord->isCurrent() &&
816 $this->getAuthority()->probablyCan( 'rollback', $this->mNewPage )
817 ) {
818 $rollbackLink = Linker::generateRollback(
819 $this->mNewRevisionRecord,
820 $this->getContext(),
821 [ 'noBrackets' ]
822 );
823 if ( $rollbackLink ) {
824 $out->setPreventClickjacking( true );
825 $rollback = "\u{00A0}\u{00A0}\u{00A0}" . $rollbackLink;
826 }
827 }
828
829 if ( $this->userCanEdit( $this->mOldRevisionRecord ) &&
830 $this->userCanEdit( $this->mNewRevisionRecord )
831 ) {
832 $undoLink = $this->linkRenderer->makeKnownLink(
833 $this->mNewPage,
834 $this->msg( 'editundo' )->text(),
835 [ 'title' => Linker::titleAttrib( 'undo' ) ],
836 [
837 'action' => 'edit',
838 'undoafter' => $this->mOldid,
839 'undo' => $this->mNewid
840 ]
841 );
842 $revisionTools['mw-diff-undo'] = $undoLink;
843 }
844 }
845 # Make "previous revision link"
846 $hasPrevious = $samePage && $this->mOldPage &&
847 $this->revisionStore->getPreviousRevision( $this->mOldRevisionRecord );
848 if ( $hasPrevious ) {
849 $prevlinkQuery = [ 'diff' => 'prev', 'oldid' => $this->mOldid ] + $query;
850 $prevlink = $this->linkRenderer->makeKnownLink(
851 $this->mOldPage,
852 $this->msg( 'previousdiff' )->text(),
853 [ 'id' => 'differences-prevlink' ],
854 $prevlinkQuery
855 );
856 $breadCrumbs .= $this->linkRenderer->makeKnownLink(
857 $this->mOldPage,
858 $this->msg( 'previousdiff' )->text(),
859 [
860 'class' => 'mw-diff-revision-history-link-previous'
861 ],
862 $prevlinkQuery
863 );
864 } else {
865 $prevlink = "\u{00A0}";
866 }
867
868 if ( $this->mOldRevisionRecord->isMinor() ) {
869 $oldminor = ChangesList::flag( 'minor' );
870 } else {
871 $oldminor = '';
872 }
873
874 $oldRevRecord = $this->mOldRevisionRecord;
875
876 $ldel = $this->revisionDeleteLink( $oldRevRecord );
877 $oldRevisionHeader = $this->getRevisionHeader( $oldRevRecord, 'complete' );
878 $oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff', $this->getContext() );
879 $oldRevComment = $this->commentFormatter
880 ->formatRevision(
881 $oldRevRecord, $user, !$diffOnly, !$this->unhide, false
882 );
883
884 if ( $oldRevComment === '' ) {
885 $defaultComment = $this->msg( 'changeslist-nocomment' )->escaped();
886 $oldRevComment = "<span class=\"comment mw-comment-none\">$defaultComment</span>";
887 }
888
889 $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' .
890 '<div id="mw-diff-otitle2">' .
891 Linker::revUserTools( $oldRevRecord, !$this->unhide ) .
892 $this->getUserMetaData( $oldRevRecord->getUser() ) .
893 '</div>' .
894 '<div id="mw-diff-otitle3">' . $oldminor . $oldRevComment . $ldel . '</div>' .
895 '<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' .
896 '<div id="mw-diff-otitle4">' . $prevlink . '</div>';
897
898 // Allow extensions to change the $oldHeader variable
899 $this->hookRunner->onDifferenceEngineOldHeader(
900 $this, $oldHeader, $prevlink, $oldminor, $diffOnly, $ldel, $this->unhide );
901 }
902
903 $out->addJsConfigVars( [
904 'wgDiffOldId' => $this->mOldid,
905 'wgDiffNewId' => $this->mNewid,
906 ] );
907
908 # Make "next revision link"
909 # Skip next link on the top revision
910 if ( $samePage && $this->mNewPage && !$this->mNewRevisionRecord->isCurrent() ) {
911 $nextlinkQuery = [ 'diff' => 'next', 'oldid' => $this->mNewid ] + $query;
912 $nextlink = $this->linkRenderer->makeKnownLink(
913 $this->mNewPage,
914 $this->msg( 'nextdiff' )->text(),
915 [ 'id' => 'differences-nextlink' ],
916 $nextlinkQuery
917 );
918 $breadCrumbs .= $this->linkRenderer->makeKnownLink(
919 $this->mNewPage,
920 $this->msg( 'nextdiff' )->text(),
921 [
922 'class' => 'mw-diff-revision-history-link-next'
923 ],
924 $nextlinkQuery
925 );
926 } else {
927 $nextlink = "\u{00A0}";
928 }
929
930 if ( $this->mNewRevisionRecord->isMinor() ) {
931 $newminor = ChangesList::flag( 'minor' );
932 } else {
933 $newminor = '';
934 }
935
936 # Handle RevisionDelete links...
937 $rdel = $this->revisionDeleteLink( $this->mNewRevisionRecord );
938
939 # Allow extensions to define their own revision tools
940 $this->hookRunner->onDiffTools(
941 $this->mNewRevisionRecord,
942 $revisionTools,
943 $this->mOldRevisionRecord ?: null,
944 $user
945 );
946
947 $formattedRevisionTools = [];
948 // Put each one in parentheses (poor man's button)
949 foreach ( $revisionTools as $key => $tool ) {
950 $toolClass = is_string( $key ) ? $key : 'mw-diff-tool';
951 $element = Html::rawElement(
952 'span',
953 [ 'class' => $toolClass ],
954 $tool
955 );
956 $formattedRevisionTools[] = $element;
957 }
958
959 $newRevRecord = $this->mNewRevisionRecord;
960
961 $newRevisionHeader = $this->getRevisionHeader( $newRevRecord, 'complete' ) .
962 ' ' . implode( ' ', $formattedRevisionTools );
963 $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff', $this->getContext() );
964 $newRevComment = $this->commentFormatter->formatRevision(
965 $newRevRecord, $user, !$diffOnly, !$this->unhide, false
966 );
967
968 if ( $newRevComment === '' ) {
969 $defaultComment = $this->msg( 'changeslist-nocomment' )->escaped();
970 $newRevComment = "<span class=\"comment mw-comment-none\">$defaultComment</span>";
971 }
972
973 $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' .
974 '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $newRevRecord, !$this->unhide ) .
975 $rollback .
976 $this->getUserMetaData( $newRevRecord->getUser() ) .
977 '</div>' .
978 '<div id="mw-diff-ntitle3">' . $newminor . $newRevComment . $rdel . '</div>' .
979 '<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' .
980 '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
981
982 // Allow extensions to change the $newHeader variable
983 $this->hookRunner->onDifferenceEngineNewHeader( $this, $newHeader,
984 $formattedRevisionTools, $nextlink, $rollback, $newminor, $diffOnly,
985 $rdel, $this->unhide );
986
987 $out->addHTML(
988 Html::rawElement( 'div', [
989 'class' => 'mw-diff-revision-history-links'
990 ], $breadCrumbs )
991 );
992 # If the diff cannot be shown due to a deleted revision, then output
993 # the diff header and links to unhide (if available)...
994 if ( $this->shouldBeHiddenFromUser( $this->getAuthority() ) ) {
995 $this->showDiffStyle();
996 $multi = $this->getMultiNotice();
997 $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
998 if ( !$allowed ) {
999 # Give explanation for why revision is not visible
1000 $msg = [ $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff' ];
1001 } else {
1002 # Give explanation and add a link to view the diff...
1003 $query = $this->getRequest()->appendQueryValue( 'unhide', '1' );
1004 $msg = [
1005 $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff',
1006 $this->getTitle()->getFullURL( $query )
1007 ];
1008 }
1009 $out->addHTML( Html::warningBox( $this->msg( ...$msg )->parse(), 'plainlinks' ) );
1010 # Otherwise, output a regular diff...
1011 } else {
1012 # Add deletion notice if the user is viewing deleted content
1013 $notice = '';
1014 if ( $deleted ) {
1015 $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
1016 $notice = Html::warningBox( $this->msg( $msg )->parse(), 'plainlinks' );
1017 }
1018
1019 # Add an error if the content can't be loaded
1020 $this->getSlotContents();
1021 foreach ( $this->getRevisionLoadErrors() as $msg ) {
1022 $notice .= Html::warningBox( $msg->parse() );
1023 }
1024
1025 // Check if inline switcher will be needed
1026 if ( $this->getTextDiffer()->hasFormat( 'inline' ) ) {
1027 $out->enableOOUI();
1028 }
1029
1030 $this->showTablePrefixes();
1031 $this->showDiff( $oldHeader, $newHeader, $notice );
1032 if ( !$diffOnly ) {
1033 $this->renderNewRevision();
1034 }
1035
1036 // Allow extensions to optionally not show the final patrolled link
1037 if ( $this->hookRunner->onDifferenceEngineRenderRevisionShowFinalPatrolLink() ) {
1038 # Add redundant patrol link on bottom...
1039 $out->addHTML( $this->markPatrolledLink() );
1040 }
1041 }
1042 }
1043
1047 private function showTablePrefixes() {
1048 $parts = [];
1049 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1050 $parts += $slotDiffRenderer->getTablePrefix( $this->getContext(), $this->mNewPage );
1051 }
1052 ksort( $parts );
1053 if ( count( array_filter( $parts ) ) > 0 ) {
1054 $language = $this->getLanguage();
1055 $attrs = [
1056 'class' => 'mw-diff-table-prefix',
1057 'dir' => $language->getDir(),
1058 'lang' => $language->getCode(),
1059 ];
1060 $this->getOutput()->addHTML(
1061 Html::rawElement( 'div', $attrs, implode( '', $parts ) ) );
1062 }
1063 }
1064
1075 public function markPatrolledLink() {
1076 if ( $this->mMarkPatrolledLink === null ) {
1077 $linkInfo = $this->getMarkPatrolledLinkInfo();
1078 // If false, there is no patrol link needed/allowed
1079 if ( !$linkInfo || !$this->mNewPage ) {
1080 $this->mMarkPatrolledLink = '';
1081 } else {
1082 $patrolLinkClass = 'patrollink';
1083 $this->mMarkPatrolledLink = ' <span class="' . $patrolLinkClass . '" data-mw="interface">[' .
1084 $this->linkRenderer->makeKnownLink(
1085 $this->mNewPage,
1086 $this->msg( 'markaspatrolleddiff' )->text(),
1087 [],
1088 [
1089 'action' => 'markpatrolled',
1090 'rcid' => $linkInfo['rcid'],
1091 ]
1092 ) . ']</span>';
1093 // Allow extensions to change the markpatrolled link
1094 $this->hookRunner->onDifferenceEngineMarkPatrolledLink( $this,
1095 $this->mMarkPatrolledLink, $linkInfo['rcid'] );
1096 }
1097 }
1098 return $this->mMarkPatrolledLink;
1099 }
1100
1108 protected function getMarkPatrolledLinkInfo() {
1109 $user = $this->getUser();
1110 $config = $this->getConfig();
1111
1112 // Prepare a change patrol link, if applicable
1113 if (
1114 // Is patrolling enabled and the user allowed to?
1115 $config->get( MainConfigNames::UseRCPatrol ) &&
1116 $this->mNewPage &&
1117 $this->getAuthority()->probablyCan( 'patrol', $this->mNewPage ) &&
1118 // Only do this if the revision isn't more than 6 hours older
1119 // than the Max RC age (6h because the RC might not be cleaned out regularly)
1120 RecentChange::isInRCLifespan( $this->mNewRevisionRecord->getTimestamp(), 21600 )
1121 ) {
1122 // Look for an unpatrolled change corresponding to this diff
1123 $change = RecentChange::newFromConds(
1124 [
1125 'rc_this_oldid' => $this->mNewid,
1126 'rc_patrolled' => RecentChange::PRC_UNPATROLLED
1127 ],
1128 __METHOD__
1129 );
1130
1131 if ( $change && !$change->getPerformerIdentity()->equals( $user ) ) {
1132 $rcid = $change->getAttribute( 'rc_id' );
1133 } else {
1134 // None found or the page has been created by the current user.
1135 // If the user could patrol this it already would be patrolled
1136 $rcid = 0;
1137 }
1138
1139 // Allow extensions to possibly change the rcid here
1140 // For example the rcid might be set to zero due to the user
1141 // being the same as the performer of the change but an extension
1142 // might still want to show it under certain conditions
1143 $this->hookRunner->onDifferenceEngineMarkPatrolledRCID( $rcid, $this, $change, $user );
1144
1145 // Build the link
1146 if ( $rcid ) {
1147 $this->getOutput()->setPreventClickjacking( true );
1148 if ( $this->getAuthority()->isAllowed( 'writeapi' ) ) {
1149 $this->getOutput()->addModules( 'mediawiki.misc-authed-curate' );
1150 }
1151
1152 return [ 'rcid' => $rcid ];
1153 }
1154 }
1155
1156 // No mark as patrolled link applicable
1157 return false;
1158 }
1159
1165 private function revisionDeleteLink( RevisionRecord $revRecord ) {
1166 $link = Linker::getRevDeleteLink(
1167 $this->getAuthority(),
1168 $revRecord,
1169 $revRecord->getPageAsLinkTarget()
1170 );
1171 if ( $link !== '' ) {
1172 $link = "\u{00A0}\u{00A0}\u{00A0}" . $link . ' ';
1173 }
1174
1175 return $link;
1176 }
1177
1183 public function renderNewRevision() {
1184 if ( $this->isContentOverridden ) {
1185 // The code below only works with a RevisionRecord object. We could construct a
1186 // fake RevisionRecord (here or in setContent), but since this does not seem
1187 // needed at the moment, we'll just fail for now.
1188 throw new LogicException(
1189 __METHOD__
1190 . ' is not supported after calling setContent(). Use setRevisions() instead.'
1191 );
1192 }
1193
1194 $out = $this->getOutput();
1195 $revHeader = $this->getRevisionHeader( $this->mNewRevisionRecord );
1196 # Add "current version as of X" title
1197 $out->addHTML( "<hr class='diff-hr' id='mw-oldid' />
1198 <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
1199 # Page content may be handled by a hooked call instead...
1200 if ( $this->hookRunner->onArticleContentOnDiff( $this, $out ) ) {
1201 $this->loadNewText();
1202 if ( !$this->mNewPage ) {
1203 // New revision is unsaved; bail out.
1204 // TODO in theory rendering the new revision is a meaningful thing to do
1205 // even if it's unsaved, but a lot of untangling is required to do it safely.
1206 return;
1207 }
1208 if ( $this->hasNewRevisionLoadError() ) {
1209 // There was an error loading the new revision
1210 return;
1211 }
1212
1213 $out->setRevisionId( $this->mNewid );
1214 $out->setRevisionIsCurrent( $this->mNewRevisionRecord->isCurrent() );
1215 $out->setRevisionTimestamp( $this->mNewRevisionRecord->getTimestamp() );
1216 $out->setArticleFlag( true );
1217
1218 if ( !$this->hookRunner->onArticleRevisionViewCustom(
1219 $this->mNewRevisionRecord, $this->mNewPage, $this->mOldid, $out )
1220 ) {
1221 // Handled by extension
1222 // NOTE: sync with hooks called in Article::view()
1223 } else {
1224 // Normal page
1225 if ( $this->getTitle()->equals( $this->mNewPage ) ) {
1226 // If the Title stored in the context is the same as the one
1227 // of the new revision, we can use its associated WikiPage
1228 // object.
1229 $wikiPage = $this->getWikiPage();
1230 } else {
1231 // Otherwise we need to create our own WikiPage object
1232 $wikiPage = $this->wikiPageFactory->newFromTitle( $this->mNewPage );
1233 }
1234
1235 $parserOptions = $wikiPage->makeParserOptions( $this->getContext() );
1236 $parserOptions->setRenderReason( 'diff-page' );
1237
1238 $parserOutputAccess = MediaWikiServices::getInstance()->getParserOutputAccess();
1239 $status = $parserOutputAccess->getParserOutput(
1240 $wikiPage,
1241 $parserOptions,
1242 $this->mNewRevisionRecord,
1243 // we already checked
1244 ParserOutputAccess::OPT_NO_AUDIENCE_CHECK |
1245 // Update cascading protection
1246 ParserOutputAccess::OPT_LINKS_UPDATE
1247 );
1248 if ( $status->isOK() ) {
1249 $parserOutput = $status->getValue();
1250 // Allow extensions to change parser output here
1251 if ( $this->hookRunner->onDifferenceEngineRenderRevisionAddParserOutput(
1252 $this, $out, $parserOutput, $wikiPage )
1253 ) {
1254 $out->addParserOutput( $parserOutput, [
1255 'enableSectionEditLinks' => $this->mNewRevisionRecord->isCurrent()
1256 && $this->getAuthority()->probablyCan(
1257 'edit',
1258 $this->mNewRevisionRecord->getPage()
1259 ),
1260 'absoluteURLs' => $this->slotDiffOptions['expand-url'] ?? false
1261 ] );
1262 }
1263 } else {
1264 $out->addHTML(
1265 Html::errorBox(
1266 $out->parseAsInterface(
1267 $status->getWikiText( false, false, $this->getLanguage() )
1268 )
1269 )
1270 );
1271 }
1272 }
1273 }
1274 }
1275
1286 public function showDiff( $otitle, $ntitle, $notice = '' ) {
1287 // Allow extensions to affect the output here
1288 $this->hookRunner->onDifferenceEngineShowDiff( $this );
1289
1290 $diff = $this->getDiff( $otitle, $ntitle, $notice );
1291 if ( $diff === false ) {
1292 $this->showMissingRevision();
1293 return false;
1294 }
1295
1296 $this->showDiffStyle();
1297 if ( $this->slotDiffOptions['expand-url'] ?? false ) {
1298 $diff = Linker::expandLocalLinks( $diff );
1299 }
1300 $this->getOutput()->addHTML( $diff );
1301 return true;
1302 }
1303
1307 public function showDiffStyle() {
1308 if ( !$this->isSlotDiffRenderer ) {
1309 $this->getOutput()->addModules( 'mediawiki.diff' );
1310 $this->getOutput()->addModuleStyles( [
1311 'mediawiki.interface.helpers.styles',
1312 'mediawiki.diff.styles'
1313 ] );
1314 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1315 $slotDiffRenderer->addModules( $this->getOutput() );
1316 }
1317 }
1318 }
1319
1329 public function getDiff( $otitle, $ntitle, $notice = '' ) {
1330 $body = $this->getDiffBody();
1331 if ( $body === false ) {
1332 return false;
1333 }
1334
1335 $multi = $this->getMultiNotice();
1336 // Display a message when the diff is empty
1337 if ( $body === '' ) {
1338 $notice .= '<div class="mw-diff-empty">' .
1339 $this->msg( 'diff-empty' )->parse() .
1340 "</div>\n";
1341 }
1342
1343 if ( $this->cacheHitKey !== null ) {
1344 $body .= "\n<!-- diff cache key " . htmlspecialchars( $this->cacheHitKey ) . " -->\n";
1345 }
1346
1347 return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
1348 }
1349
1350 private function incrementStats( string $cacheStatus ): void {
1351 $stats = MediaWikiServices::getInstance()->getStatsFactory();
1352 $stats->getCounter( 'diff_cache_total' )
1353 ->setLabel( 'status', $cacheStatus )
1354 ->copyToStatsdAt( 'diff_cache.' . $cacheStatus )
1355 ->increment();
1356 }
1357
1363 public function getDiffBody() {
1364 $this->mCacheHit = true;
1365 // Check if the diff should be hidden from this user
1366 if ( !$this->isContentOverridden ) {
1367 if ( !$this->loadRevisionData() ) {
1368 return false;
1369 } elseif ( $this->mOldRevisionRecord &&
1370 !$this->mOldRevisionRecord->userCan(
1371 RevisionRecord::DELETED_TEXT,
1372 $this->getAuthority()
1373 )
1374 ) {
1375 return false;
1376 } elseif ( $this->mNewRevisionRecord &&
1377 !$this->mNewRevisionRecord->userCan(
1378 RevisionRecord::DELETED_TEXT,
1379 $this->getAuthority()
1380 ) ) {
1381 return false;
1382 }
1383 // Short-circuit
1384 if ( $this->mOldRevisionRecord === false || (
1385 $this->mOldRevisionRecord &&
1386 $this->mNewRevisionRecord &&
1387 $this->mOldRevisionRecord->getId() &&
1388 $this->mOldRevisionRecord->getId() == $this->mNewRevisionRecord->getId()
1389 ) ) {
1390 if ( $this->hookRunner->onDifferenceEngineShowEmptyOldContent( $this ) ) {
1391 return '';
1392 }
1393 }
1394 }
1395
1396 // Cacheable?
1397 $key = false;
1398 $services = MediaWikiServices::getInstance();
1399 $cache = $services->getMainWANObjectCache();
1400 $stats = $services->getStatsdDataFactory();
1401 if ( $this->mOldid && $this->mNewid ) {
1402 $key = $cache->makeKey( ...$this->getDiffBodyCacheKeyParams() );
1403
1404 // Try cache
1405 if ( !$this->mRefreshCache ) {
1406 $difftext = $cache->get( $key );
1407 if ( is_string( $difftext ) ) {
1408 $this->incrementStats( 'hit' );
1409 $difftext = $this->localiseDiff( $difftext );
1410 $this->cacheHitKey = $key;
1411 return $difftext;
1412 }
1413 } // don't try to load but save the result
1414 }
1415 $this->mCacheHit = false;
1416 $this->cacheHitKey = null;
1417
1418 // Loadtext is permission safe, this just clears out the diff
1419 if ( !$this->loadText() ) {
1420 return false;
1421 }
1422
1423 $difftext = '';
1424 // We've checked for revdelete at the beginning of this method; it's OK to ignore
1425 // read permissions here.
1426 $slotContents = $this->getSlotContents();
1427 foreach ( $this->getSlotDiffRenderers() as $role => $slotDiffRenderer ) {
1428 try {
1429 $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role]['old'],
1430 $slotContents[$role]['new'] );
1431 } catch ( IncompatibleDiffTypesException $e ) {
1432 $slotDiff = $this->getSlotError( $e->getMessageObject()->parse() );
1433 }
1434 if ( $slotDiff && $role !== SlotRecord::MAIN ) {
1435 // FIXME: ask SlotRoleHandler::getSlotNameMessage
1436 $slotTitle = $role;
1437 $difftext .= $this->getSlotHeader( $slotTitle );
1438 }
1439 $difftext .= $slotDiff;
1440 }
1441
1442 // Save to cache for 7 days
1443 if ( !$this->hookRunner->onAbortDiffCache( $this ) ) {
1444 $this->incrementStats( 'uncacheable' );
1445 } elseif ( $key !== false ) {
1446 $this->incrementStats( 'miss' );
1447 $cache->set( $key, $difftext, 7 * 86400 );
1448 } else {
1449 $this->incrementStats( 'uncacheable' );
1450 }
1451 // localise line numbers and title attribute text
1452 $difftext = $this->localiseDiff( $difftext );
1453
1454 return $difftext;
1455 }
1456
1463 public function getDiffBodyForRole( $role ) {
1464 $diffRenderers = $this->getSlotDiffRenderers();
1465 if ( !isset( $diffRenderers[$role] ) ) {
1466 return false;
1467 }
1468
1469 $slotContents = $this->getSlotContents();
1470 try {
1471 $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role]['old'],
1472 $slotContents[$role]['new'] );
1473 } catch ( IncompatibleDiffTypesException $e ) {
1474 $slotDiff = $this->getSlotError( $e->getMessageObject()->parse() );
1475 }
1476 if ( $slotDiff === '' ) {
1477 return false;
1478 }
1479
1480 if ( $role !== SlotRecord::MAIN ) {
1481 // TODO use human-readable role name at least
1482 $slotTitle = $role;
1483 $slotDiff = $this->getSlotHeader( $slotTitle ) . $slotDiff;
1484 }
1485
1486 return $this->localiseDiff( $slotDiff );
1487 }
1488
1496 protected function getSlotHeader( $headerText ) {
1497 // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
1498 $columnCount = $this->mOldRevisionRecord ? 4 : 2;
1499 $userLang = $this->getLanguage()->getHtmlCode();
1500 return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-header', 'lang' => $userLang ],
1501 Html::element( 'th', [ 'colspan' => $columnCount ], $headerText ) );
1502 }
1503
1510 protected function getSlotError( $errorText ) {
1511 // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
1512 $columnCount = $this->mOldRevisionRecord ? 4 : 2;
1513 $userLang = $this->getLanguage()->getHtmlCode();
1514 return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-error', 'lang' => $userLang ],
1515 Html::rawElement( 'td', [ 'colspan' => $columnCount ], $errorText ) );
1516 }
1517
1531 protected function getDiffBodyCacheKeyParams() {
1532 if ( !$this->mOldid || !$this->mNewid ) {
1533 throw new BadMethodCallException( 'mOldid and mNewid must be set to get diff cache key.' );
1534 }
1535
1536 $params = [
1537 'diff',
1538 self::DIFF_VERSION,
1539 "old-{$this->mOldid}",
1540 "rev-{$this->mNewid}"
1541 ];
1542
1543 $extraKeys = [];
1544 if ( !$this->isSlotDiffRenderer ) {
1545 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1546 $extraKeys = array_merge( $extraKeys, $slotDiffRenderer->getExtraCacheKeys() );
1547 }
1548 }
1549 ksort( $extraKeys );
1550 return array_merge( $params, array_values( $extraKeys ) );
1551 }
1552
1560 public function getExtraCacheKeys() {
1561 // This method is called when the DifferenceEngine is used for a slot diff. We only care
1562 // about special things, not the revision IDs, which are added to the cache key by the
1563 // page-level DifferenceEngine, and which might not have a valid value for this object.
1564 $this->mOldid = 123456789;
1565 $this->mNewid = 987654321;
1566
1567 // This will repeat a bunch of unnecessary key fields for each slot. Not nice but harmless.
1568 $params = $this->getDiffBodyCacheKeyParams();
1569
1570 // Try to get rid of the standard keys to keep the cache key human-readable:
1571 // call the getDiffBodyCacheKeyParams implementation of the base class, and if
1572 // the child class includes the same keys, drop them.
1573 // Uses an obscure PHP feature where static calls to non-static methods are allowed
1574 // as long as we are already in a non-static method of the same class, and the call context
1575 // ($this) will be inherited.
1576 // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
1578 if ( array_slice( $params, 0, count( $standardParams ) ) === $standardParams ) {
1579 $params = array_slice( $params, count( $standardParams ) );
1580 }
1581
1582 return $params;
1583 }
1584
1595 public function setSlotDiffOptions( $options ) {
1596 $validatedOptions = [];
1597 if ( isset( $options['diff-type'] )
1598 && $this->getTextDiffer()->hasFormat( $options['diff-type'] )
1599 ) {
1600 $validatedOptions['diff-type'] = $options['diff-type'];
1601 }
1602 if ( !empty( $options['expand-url'] ) ) {
1603 $validatedOptions['expand-url'] = true;
1604 }
1605 if ( !empty( $options['inline-toggle'] ) ) {
1606 $validatedOptions['inline-toggle'] = true;
1607 }
1608 $this->slotDiffOptions = $validatedOptions;
1609 }
1610
1618 public function setExtraQueryParams( $params ) {
1619 $this->extraQueryParams = $params;
1620 }
1621
1635 public function generateContentDiffBody( Content $old, Content $new ) {
1636 $slotDiffRenderer = $new->getContentHandler()->getSlotDiffRenderer( $this->getContext() );
1637 if (
1638 $slotDiffRenderer instanceof DifferenceEngineSlotDiffRenderer
1639 && $this->isSlotDiffRenderer
1640 ) {
1641 // Oops, we are just about to enter an infinite loop (the slot-level DifferenceEngine
1642 // called a DifferenceEngineSlotDiffRenderer that wraps the same DifferenceEngine class).
1643 // This will happen when a content model has no custom slot diff renderer, it does have
1644 // a custom difference engine, but that does not override this method.
1645 throw new LogicException( get_class( $this ) . ': could not maintain backwards compatibility. '
1646 . 'Please use a SlotDiffRenderer.' );
1647 }
1648 return $slotDiffRenderer->getDiff( $old, $new ) . $this->getDebugString();
1649 }
1650
1663 public function generateTextDiffBody( $otext, $ntext ) {
1664 $slotDiffRenderer = $this->contentHandlerFactory
1665 ->getContentHandler( CONTENT_MODEL_TEXT )
1666 ->getSlotDiffRenderer( $this->getContext() );
1667 if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1668 // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1669 // This is too unlikely to happen to bother handling properly.
1670 throw new LogicException( 'The slot diff renderer for text content should be a '
1671 . 'TextSlotDiffRenderer subclass' );
1672 }
1673 return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1674 }
1675
1682 public static function getEngine() {
1683 $differenceEngine = new self;
1684 $engine = $differenceEngine->getTextDiffer()->getEngineForFormat( 'table' );
1685 if ( $engine === 'external' ) {
1686 return MediaWikiServices::getInstance()->getMainConfig()
1687 ->get( MainConfigNames::ExternalDiffEngine );
1688 } else {
1689 return $engine;
1690 }
1691 }
1692
1701 protected function debug( $generator = "internal" ) {
1702 if ( !$this->enableDebugComment ) {
1703 return '';
1704 }
1705 $data = [ $generator ];
1706 if ( $this->getConfig()->get( MainConfigNames::ShowHostnames ) ) {
1707 $data[] = wfHostname();
1708 }
1709 $data[] = wfTimestamp( TS_DB );
1710
1711 return "<!-- diff generator: " .
1712 implode( " ", array_map( "htmlspecialchars", $data ) ) .
1713 " -->\n";
1714 }
1715
1719 private function getDebugString() {
1720 $engine = self::getEngine();
1721 if ( $engine === 'wikidiff2' ) {
1722 return $this->debug( 'wikidiff2' );
1723 } elseif ( $engine === 'php' ) {
1724 return $this->debug( 'native PHP' );
1725 } else {
1726 return $this->debug( "external $engine" );
1727 }
1728 }
1729
1736 private function localiseDiff( $text ) {
1737 return $this->getTextDiffer()->localize( $this->getTextDiffFormat(), $text );
1738 }
1739
1748 public function localiseLineNumbers( $text ) {
1749 return preg_replace_callback( '/<!--LINE (\d+)-->/',
1750 function ( array $matches ) {
1751 if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
1752 return '';
1753 }
1754 return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
1755 }, $text );
1756 }
1757
1763 public function getMultiNotice() {
1764 // The notice only make sense if we are diffing two saved revisions of the same page.
1765 if (
1766 !$this->mOldRevisionRecord || !$this->mNewRevisionRecord
1767 || !$this->mOldPage || !$this->mNewPage
1768 || !$this->mOldPage->equals( $this->mNewPage )
1769 || $this->mOldRevisionRecord->getId() === null
1770 || $this->mNewRevisionRecord->getId() === null
1771 // (T237709) Deleted revs might have different page IDs
1772 || $this->mNewPage->getArticleID() !== $this->mOldRevisionRecord->getPageId()
1773 || $this->mNewPage->getArticleID() !== $this->mNewRevisionRecord->getPageId()
1774 ) {
1775 return '';
1776 }
1777
1778 if ( $this->mOldRevisionRecord->getTimestamp() > $this->mNewRevisionRecord->getTimestamp() ) {
1779 $oldRevRecord = $this->mNewRevisionRecord; // flip
1780 $newRevRecord = $this->mOldRevisionRecord; // flip
1781 } else { // normal case
1782 $oldRevRecord = $this->mOldRevisionRecord;
1783 $newRevRecord = $this->mNewRevisionRecord;
1784 }
1785
1786 // Don't show the notice if too many rows must be scanned
1787 // @todo show some special message for that case
1788 $nEdits = 0;
1789 $revisionIdList = $this->revisionStore->getRevisionIdsBetween(
1790 $this->mNewPage->getArticleID(),
1791 $oldRevRecord,
1792 $newRevRecord,
1793 1000
1794 );
1795 // only count revisions that are visible
1796 if ( count( $revisionIdList ) > 0 ) {
1797 foreach ( $revisionIdList as $revisionId ) {
1798 $revision = $this->revisionStore->getRevisionById( $revisionId );
1799 if ( $revision->getUser( RevisionRecord::FOR_THIS_USER, $this->getAuthority() ) ) {
1800 $nEdits++;
1801 }
1802 }
1803 }
1804 if ( $nEdits > 0 && $nEdits <= 1000 ) {
1805 // Use an invalid username to get the wiki's default gender (as fallback)
1806 $newRevUserForGender = '[HIDDEN]';
1807 $limit = 100; // use diff-multi-manyusers if too many users
1808 try {
1809 $users = $this->revisionStore->getAuthorsBetween(
1810 $this->mNewPage->getArticleID(),
1811 $oldRevRecord,
1812 $newRevRecord,
1813 null,
1814 $limit
1815 );
1816 $numUsers = count( $users );
1817
1818 $newRevUser = $newRevRecord->getUser( RevisionRecord::RAW );
1819 $newRevUserText = $newRevUser ? $newRevUser->getName() : '';
1820 $newRevUserSafe = $newRevRecord->getUser(
1821 RevisionRecord::FOR_THIS_USER,
1822 $this->getAuthority()
1823 );
1824 $newRevUserForGender = $newRevUserSafe ? $newRevUserSafe->getName() : '[HIDDEN]';
1825 if ( $numUsers == 1 && $users[0]->getName() == $newRevUserText ) {
1826 $numUsers = 0; // special case to say "by the same user" instead of "by one other user"
1827 }
1828 } catch ( InvalidArgumentException $e ) {
1829 $numUsers = 0;
1830 }
1831
1832 return self::intermediateEditsMsg( $nEdits, $numUsers, $limit, $newRevUserForGender );
1833 }
1834
1835 return '';
1836 }
1837
1848 public static function intermediateEditsMsg( $numEdits, $numUsers, $limit, $lastUser = '[HIDDEN]' ) {
1849 if ( $numUsers === 0 ) {
1850 $msg = 'diff-multi-sameuser';
1851 return wfMessage( $msg )
1852 ->numParams( $numEdits, $numUsers )
1853 ->params( $lastUser )
1854 ->parse();
1855 } elseif ( $numUsers > $limit ) {
1856 $msg = 'diff-multi-manyusers';
1857 $numUsers = $limit;
1858 } else {
1859 $msg = 'diff-multi-otherusers';
1860 }
1861
1862 return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1863 }
1864
1869 private function userCanEdit( RevisionRecord $revRecord ) {
1870 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
1871 return false;
1872 }
1873
1874 return true;
1875 }
1876
1886 public function getRevisionHeader( RevisionRecord $rev, $complete = '' ) {
1887 $lang = $this->getLanguage();
1888 $user = $this->getUser();
1889 $revtimestamp = $rev->getTimestamp();
1890 $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
1891 $dateofrev = $lang->userDate( $revtimestamp, $user );
1892 $timeofrev = $lang->userTime( $revtimestamp, $user );
1893
1894 $header = $this->msg(
1895 $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
1896 $timestamp,
1897 $dateofrev,
1898 $timeofrev
1899 );
1900
1901 if ( $complete !== 'complete' ) {
1902 return $header->escaped();
1903 }
1904
1905 $title = $rev->getPageAsLinkTarget();
1906
1907 if ( $this->userCanEdit( $rev ) ) {
1908 $header = $this->linkRenderer->makeKnownLink(
1909 $title,
1910 $header->text(),
1911 [],
1912 [ 'oldid' => $rev->getId() ]
1913 );
1914 $editQuery = [ 'action' => 'edit' ];
1915 if ( !$rev->isCurrent() ) {
1916 $editQuery['oldid'] = $rev->getId();
1917 }
1918
1919 $key = $this->getAuthority()->probablyCan( 'edit', $rev->getPage() ) ? 'editold' : 'viewsourceold';
1920 $msg = $this->msg( $key )->text();
1921 $editLink = $this->linkRenderer->makeKnownLink( $title, $msg, [], $editQuery );
1922 $header .= ' ' . Html::rawElement(
1923 'span',
1924 [ 'class' => 'mw-diff-edit' ],
1925 $editLink
1926 );
1927 } else {
1928 $header = $header->escaped();
1929 }
1930
1931 // Machine readable information
1932 $header .= Html::element( 'span',
1933 [
1934 'class' => 'mw-diff-timestamp',
1935 'data-timestamp' => wfTimestamp( TS_ISO_8601, $revtimestamp ),
1936 ], ''
1937 );
1938
1939 if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1940 return Html::rawElement(
1941 'span',
1942 [ 'class' => Linker::getRevisionDeletedClass( $rev ) ],
1943 $header
1944 );
1945 }
1946
1947 return $header;
1948 }
1949
1962 public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
1963 // shared.css sets diff in interface language/dir, but the actual content
1964 // is often in a different language, mostly the page content language/dir
1965 $header = Html::openElement( 'table', [
1966 'class' => [
1967 'diff',
1968 // The following classes are used here:
1969 // * diff-type-table
1970 // * diff-type-inline
1971 'diff-type-' . $this->getTextDiffFormat(),
1972 // The following classes are used here:
1973 // * diff-contentalign-left
1974 // * diff-contentalign-right
1975 'diff-contentalign-' . $this->getDiffLang()->alignStart(),
1976 // The following classes are used here:
1977 // * diff-editfont-monospace
1978 // * diff-editfont-sans-serif
1979 // * diff-editfont-serif
1980 'diff-editfont-' . $this->userOptionsLookup->getOption(
1981 $this->getUser(),
1982 'editfont'
1983 )
1984 ],
1985 'data-mw' => 'interface',
1986 ] );
1987 $userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
1988
1989 if ( !$diff && !$otitle ) {
1990 $header .= "
1991 <tr class=\"diff-title\" lang=\"{$userLang}\">
1992 <td class=\"diff-ntitle\">{$ntitle}</td>
1993 </tr>";
1994 $multiColspan = 1;
1995 } else {
1996 if ( $diff ) { // Safari/Chrome show broken output if cols not used
1997 $header .= "
1998 <col class=\"diff-marker\" />
1999 <col class=\"diff-content\" />
2000 <col class=\"diff-marker\" />
2001 <col class=\"diff-content\" />";
2002 $colspan = 2;
2003 $multiColspan = 4;
2004 } else {
2005 $colspan = 1;
2006 $multiColspan = 2;
2007 }
2008 if ( $otitle || $ntitle ) {
2009 // FIXME Hardcoding values from TableDiffFormatter.
2010 $deletedClass = 'diff-side-deleted';
2011 $addedClass = 'diff-side-added';
2012 $header .= "
2013 <tr class=\"diff-title\" lang=\"{$userLang}\">
2014 <td colspan=\"$colspan\" class=\"diff-otitle {$deletedClass}\">{$otitle}</td>
2015 <td colspan=\"$colspan\" class=\"diff-ntitle {$addedClass}\">{$ntitle}</td>
2016 </tr>";
2017 }
2018 }
2019
2020 if ( $multi != '' ) {
2021 $header .= "<tr><td colspan=\"{$multiColspan}\" " .
2022 "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
2023 }
2024 if ( $notice != '' ) {
2025 $header .= "<tr><td colspan=\"{$multiColspan}\" " .
2026 "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
2027 }
2028
2029 return $header . $diff . "</table>";
2030 }
2031
2039 public function setContent( Content $oldContent, Content $newContent ) {
2040 $this->mOldContent = $oldContent;
2041 $this->mNewContent = $newContent;
2042
2043 $this->mTextLoaded = 2;
2044 $this->mRevisionsLoaded = true;
2045 $this->isContentOverridden = true;
2046 $this->slotDiffRenderers = null;
2047 }
2048
2054 public function setRevisions(
2055 ?RevisionRecord $oldRevision, RevisionRecord $newRevision
2056 ) {
2057 if ( $oldRevision ) {
2058 $this->mOldRevisionRecord = $oldRevision;
2059 $this->mOldid = $oldRevision->getId();
2060 $this->mOldPage = Title::newFromLinkTarget( $oldRevision->getPageAsLinkTarget() );
2061 // This method is meant for edit diffs and such so there is no reason to provide a
2062 // revision that's not readable to the user, but check it just in case.
2063 $this->mOldContent = $oldRevision->getContent( SlotRecord::MAIN,
2064 RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
2065 if ( !$this->mOldContent ) {
2066 $this->addRevisionLoadError( 'old' );
2067 }
2068 } else {
2069 $this->mOldPage = null;
2070 $this->mOldRevisionRecord = $this->mOldid = false;
2071 }
2072 $this->mNewRevisionRecord = $newRevision;
2073 $this->mNewid = $newRevision->getId();
2074 $this->mNewPage = Title::newFromLinkTarget( $newRevision->getPageAsLinkTarget() );
2075 $this->mNewContent = $newRevision->getContent( SlotRecord::MAIN,
2076 RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
2077 if ( !$this->mNewContent ) {
2078 $this->addRevisionLoadError( 'new' );
2079 }
2080
2081 $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded = true;
2082 $this->mTextLoaded = $oldRevision ? 2 : 1;
2083 $this->isContentOverridden = false;
2084 $this->slotDiffRenderers = null;
2085 }
2086
2093 public function setTextLanguage( Language $lang ) {
2094 $this->mDiffLang = $lang;
2095 }
2096
2109 public function mapDiffPrevNext( $old, $new ) {
2110 if ( $new === 'prev' ) {
2111 // Show diff between revision $old and the previous one. Get previous one from DB.
2112 $newid = intval( $old );
2113 $oldid = false;
2114 $newRev = $this->revisionStore->getRevisionById( $newid );
2115 if ( $newRev ) {
2116 $oldRev = $this->revisionStore->getPreviousRevision( $newRev );
2117 if ( $oldRev ) {
2118 $oldid = $oldRev->getId();
2119 }
2120 }
2121 } elseif ( $new === 'next' ) {
2122 // Show diff between revision $old and the next one. Get next one from DB.
2123 $oldid = intval( $old );
2124 $newid = false;
2125 $oldRev = $this->revisionStore->getRevisionById( $oldid );
2126 if ( $oldRev ) {
2127 $newRev = $this->revisionStore->getNextRevision( $oldRev );
2128 if ( $newRev ) {
2129 $newid = $newRev->getId();
2130 }
2131 }
2132 } else {
2133 $oldid = intval( $old );
2134 $newid = intval( $new );
2135 }
2136
2137 // @phan-suppress-next-line PhanTypeMismatchReturn getId does not return null here
2138 return [ $oldid, $newid ];
2139 }
2140
2141 private function loadRevisionIds() {
2142 if ( $this->mRevisionsIdsLoaded ) {
2143 return;
2144 }
2145
2146 $this->mRevisionsIdsLoaded = true;
2147
2148 $old = $this->mOldid;
2149 $new = $this->mNewid;
2150
2151 [ $this->mOldid, $this->mNewid ] = self::mapDiffPrevNext( $old, $new );
2152 if ( $new === 'next' && $this->mNewid === false ) {
2153 # if no result, NewId points to the newest old revision. The only newer
2154 # revision is cur, which is "0".
2155 $this->mNewid = 0;
2156 }
2157
2158 $this->hookRunner->onNewDifferenceEngine(
2159 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
2160 $this->getTitle(), $this->mOldid, $this->mNewid, $old, $new );
2161 }
2162
2176 public function loadRevisionData() {
2177 if ( $this->mRevisionsLoaded ) {
2178 return $this->isContentOverridden ||
2179 ( $this->mOldRevisionRecord !== null && $this->mNewRevisionRecord !== null );
2180 }
2181
2182 // Whether it succeeds or fails, we don't want to try again
2183 $this->mRevisionsLoaded = true;
2184
2185 $this->loadRevisionIds();
2186
2187 // Load the new RevisionRecord object
2188 if ( $this->mNewid ) {
2189 $this->mNewRevisionRecord = $this->revisionStore->getRevisionById( $this->mNewid );
2190 } else {
2191 $this->mNewRevisionRecord = $this->revisionStore->getRevisionByTitle( $this->getTitle() );
2192 }
2193
2194 if ( !$this->mNewRevisionRecord instanceof RevisionRecord ) {
2195 return false;
2196 }
2197
2198 // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
2199 $this->mNewid = $this->mNewRevisionRecord->getId();
2200 $this->mNewPage = $this->mNewid ?
2201 Title::newFromLinkTarget( $this->mNewRevisionRecord->getPageAsLinkTarget() ) :
2202 null;
2203
2204 // Load the old RevisionRecord object
2205 $this->mOldRevisionRecord = false;
2206 if ( $this->mOldid ) {
2207 $this->mOldRevisionRecord = $this->revisionStore->getRevisionById( $this->mOldid );
2208 } elseif ( $this->mOldid === 0 ) {
2209 $revRecord = $this->revisionStore->getPreviousRevision( $this->mNewRevisionRecord );
2210 // No previous revision; mark to show as first-version only.
2211 $this->mOldid = $revRecord ? $revRecord->getId() : false;
2212 $this->mOldRevisionRecord = $revRecord ?? false;
2213 } /* elseif ( $this->mOldid === false ) leave mOldRevisionRecord false; */
2214
2215 if ( $this->mOldRevisionRecord === null ) {
2216 return false;
2217 }
2218
2219 if ( $this->mOldRevisionRecord && $this->mOldRevisionRecord->getId() ) {
2220 $this->mOldPage = Title::newFromLinkTarget(
2221 $this->mOldRevisionRecord->getPageAsLinkTarget()
2222 );
2223 } else {
2224 $this->mOldPage = null;
2225 }
2226
2227 // Load tags information for both revisions
2228 $dbr = $this->dbProvider->getReplicaDatabase();
2229 $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
2230 if ( $this->mOldid !== false ) {
2231 $tagIds = $dbr->newSelectQueryBuilder()
2232 ->select( 'ct_tag_id' )
2233 ->from( 'change_tag' )
2234 ->where( [ 'ct_rev_id' => $this->mOldid ] )
2235 ->caller( __METHOD__ )->fetchFieldValues();
2236 $tags = [];
2237 foreach ( $tagIds as $tagId ) {
2238 try {
2239 $tags[] = $changeTagDefStore->getName( (int)$tagId );
2240 } catch ( NameTableAccessException $exception ) {
2241 continue;
2242 }
2243 }
2244 $this->mOldTags = implode( ',', $tags );
2245 } else {
2246 $this->mOldTags = false;
2247 }
2248
2249 $tagIds = $dbr->newSelectQueryBuilder()
2250 ->select( 'ct_tag_id' )
2251 ->from( 'change_tag' )
2252 ->where( [ 'ct_rev_id' => $this->mNewid ] )
2253 ->caller( __METHOD__ )->fetchFieldValues();
2254 $tags = [];
2255 foreach ( $tagIds as $tagId ) {
2256 try {
2257 $tags[] = $changeTagDefStore->getName( (int)$tagId );
2258 } catch ( NameTableAccessException $exception ) {
2259 continue;
2260 }
2261 }
2262 $this->mNewTags = implode( ',', $tags );
2263
2264 return true;
2265 }
2266
2275 public function loadText() {
2276 if ( $this->mTextLoaded == 2 ) {
2277 return $this->loadRevisionData() &&
2278 ( $this->mOldRevisionRecord === false || $this->mOldContent )
2279 && $this->mNewContent;
2280 }
2281
2282 // Whether it succeeds or fails, we don't want to try again
2283 $this->mTextLoaded = 2;
2284
2285 if ( !$this->loadRevisionData() ) {
2286 return false;
2287 }
2288
2289 if ( $this->mOldRevisionRecord ) {
2290 $this->mOldContent = $this->mOldRevisionRecord->getContent(
2291 SlotRecord::MAIN,
2292 RevisionRecord::FOR_THIS_USER,
2293 $this->getAuthority()
2294 );
2295 if ( $this->mOldContent === null ) {
2296 return false;
2297 }
2298 }
2299
2300 $this->mNewContent = $this->mNewRevisionRecord->getContent(
2301 SlotRecord::MAIN,
2302 RevisionRecord::FOR_THIS_USER,
2303 $this->getAuthority()
2304 );
2305 $this->hookRunner->onDifferenceEngineLoadTextAfterNewContentIsLoaded( $this );
2306 if ( $this->mNewContent === null ) {
2307 return false;
2308 }
2309
2310 return true;
2311 }
2312
2318 public function loadNewText() {
2319 if ( $this->mTextLoaded >= 1 ) {
2320 return $this->loadRevisionData();
2321 }
2322
2323 $this->mTextLoaded = 1;
2324
2325 if ( !$this->loadRevisionData() ) {
2326 return false;
2327 }
2328
2329 $this->mNewContent = $this->mNewRevisionRecord->getContent(
2330 SlotRecord::MAIN,
2331 RevisionRecord::FOR_THIS_USER,
2332 $this->getAuthority()
2333 );
2334
2335 $this->hookRunner->onDifferenceEngineAfterLoadNewText( $this );
2336
2337 return true;
2338 }
2339
2345 protected function getTextDiffer() {
2346 if ( $this->textDiffer === null ) {
2347 $this->textDiffer = new ManifoldTextDiffer(
2348 $this->getContext(),
2349 $this->getDiffLang(),
2350 $this->getConfig()->get( MainConfigNames::DiffEngine ),
2351 $this->getConfig()->get( MainConfigNames::ExternalDiffEngine ),
2352 $this->getConfig()->get( MainConfigNames::Wikidiff2Options )
2353 );
2354 }
2355 return $this->textDiffer;
2356 }
2357
2364 public function getSupportedFormats() {
2365 return $this->getTextDiffer()->getFormats();
2366 }
2367
2374 public function getTextDiffFormat() {
2375 return $this->slotDiffOptions['diff-type'] ?? 'table';
2376 }
2377
2378}
getUser()
getRequest()
getAuthority()
const NS_SPECIAL
Definition Defines.php:54
const CONTENT_MODEL_TEXT
Definition Defines.php:225
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfHostname()
Get host name of the current machine, for use in error reporting.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
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:66
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:63
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:158
Service for getting rendered output of a given page.
Service for creating WikiPage objects.
A StatusValue for permission errors.
Exception raised when the text of a revision is permanently missing or corrupt.
Page revision base class.
getPage()
Returns the page this revision belongs to.
isCurrent()
Checks whether the revision record is a stored current revision.
getTimestamp()
MCR migration note: this replaced Revision::getTimestamp.
getContent( $role, $audience=self::FOR_PUBLIC, Authority $performer=null)
Returns the Content of the given slot of this revision.
getPageAsLinkTarget()
Returns the title of the page this revision is associated with as a LinkTarget object.
userCan( $field, Authority $performer)
Determine if the give authority is allowed to view a particular field of this revision,...
isDeleted( $field)
MCR migration note: this replaced Revision::isDeleted.
getId( $wikiId=self::LOCAL)
Get revision ID.
Service for looking up page revisions.
Value object representing a content slot associated with a page revision.
getContent()
Returns the Content of the given slot.
Parent class for all special pages.
Exception representing a failure to look up a row from a name table.
Represents a title within MediaWiki.
Definition Title.php:79
Provides access to user options.
Track info about user edit counts and timings.
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