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