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