MediaWiki REL1_35
DifferenceEngine.php
Go to the documentation of this file.
1<?php
33
57
59
66 private const DIFF_VERSION = '1.12';
67
74 protected $mOldid;
75
82 protected $mNewid;
83
95
105
111 protected $mOldPage;
112
118 protected $mNewPage;
119
124 private $mOldTags;
125
130 private $mNewTags;
131
138
145
147 protected $mDiffLang;
148
150 private $mRevisionsIdsLoaded = false;
151
153 protected $mRevisionsLoaded = false;
154
156 protected $mTextLoaded = 0;
157
166 protected $isContentOverridden = false;
167
169 protected $mCacheHit = false;
170
176 public $enableDebugComment = false;
177
181 protected $mReducedLineNumbers = false;
182
184 protected $mMarkPatrolledLink = null;
185
187 protected $unhide = false;
188
190 protected $mRefreshCache = false;
191
193 protected $slotDiffRenderers = null;
194
201 protected $isSlotDiffRenderer = false;
202
203 /* A set of options that will be passed to the SlotDiffRenderer upon creation
204 * @var array
205 */
206 private $slotDiffOptions = [];
207
211 protected $linkRenderer;
212
217
222
224 private $hookRunner;
225
228
239 public function __construct( $context = null, $old = 0, $new = 0, $rcid = 0,
240 $refreshCache = false, $unhide = false
241 ) {
242 $this->deprecatePublicProperty( 'mOldid', '1.32', __CLASS__ );
243 $this->deprecatePublicProperty( 'mNewid', '1.32', __CLASS__ );
244 $this->deprecatePublicProperty( 'mOldPage', '1.32', __CLASS__ );
245 $this->deprecatePublicProperty( 'mNewPage', '1.32', __CLASS__ );
246 $this->deprecatePublicProperty( 'mOldContent', '1.32', __CLASS__ );
247 $this->deprecatePublicProperty( 'mNewContent', '1.32', __CLASS__ );
248 $this->deprecatePublicProperty( 'mRevisionsLoaded', '1.32', __CLASS__ );
249 $this->deprecatePublicProperty( 'mTextLoaded', '1.32', __CLASS__ );
250 $this->deprecatePublicProperty( 'mCacheHit', '1.32', __CLASS__ );
251
252 if ( $context instanceof IContextSource ) {
253 $this->setContext( $context );
254 }
255
256 wfDebug( "DifferenceEngine old '$old' new '$new' rcid '$rcid'" );
257
258 $this->mOldid = $old;
259 $this->mNewid = $new;
260 $this->mRefreshCache = $refreshCache;
261 $this->unhide = $unhide;
262
263 $services = MediaWikiServices::getInstance();
264 $this->linkRenderer = $services->getLinkRenderer();
265 $this->contentHandlerFactory = $services->getContentHandlerFactory();
266 $this->revisionStore = $services->getRevisionStore();
267 $this->hookContainer = $services->getHookContainer();
268 $this->hookRunner = new HookRunner( $this->hookContainer );
269 }
270
275 protected function getSlotDiffRenderers() {
276 if ( $this->isSlotDiffRenderer ) {
277 throw new LogicException( __METHOD__ . ' called in slot diff renderer mode' );
278 }
279
280 if ( $this->slotDiffRenderers === null ) {
281 if ( !$this->loadRevisionData() ) {
282 return [];
283 }
284
285 $slotContents = $this->getSlotContents();
286 $this->slotDiffRenderers = array_map( function ( $contents ) {
288 $content = $contents['new'] ?: $contents['old'];
289 $context = $this->getContext();
290
291 return $content->getContentHandler()->getSlotDiffRenderer(
292 $context,
293 $this->slotDiffOptions
294 );
295 }, $slotContents );
296 }
297 return $this->slotDiffRenderers;
298 }
299
306 public function markAsSlotDiffRenderer() {
307 $this->isSlotDiffRenderer = true;
308 }
309
315 protected function getSlotContents() {
316 if ( $this->isContentOverridden ) {
317 return [
318 SlotRecord::MAIN => [
319 'old' => $this->mOldContent,
320 'new' => $this->mNewContent,
321 ]
322 ];
323 } elseif ( !$this->loadRevisionData() ) {
324 return [];
325 }
326
327 $newSlots = $this->mNewRevisionRecord->getSlots()->getSlots();
328 if ( $this->mOldRevisionRecord ) {
329 $oldSlots = $this->mOldRevisionRecord->getSlots()->getSlots();
330 } else {
331 $oldSlots = [];
332 }
333 // The order here will determine the visual order of the diff. The current logic is
334 // slots of the new revision first in natural order, then deleted ones. This is ad hoc
335 // and should not be relied on - in the future we may want the ordering to depend
336 // on the page type.
337 $roles = array_merge( array_keys( $newSlots ), array_keys( $oldSlots ) );
338
339 $slots = [];
340 foreach ( $roles as $role ) {
341 $slots[$role] = [
342 'old' => isset( $oldSlots[$role] ) ? $oldSlots[$role]->getContent() : null,
343 'new' => isset( $newSlots[$role] ) ? $newSlots[$role]->getContent() : null,
344 ];
345 }
346 // move main slot to front
347 if ( isset( $slots[SlotRecord::MAIN] ) ) {
348 $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
349 }
350 return $slots;
351 }
352
353 public function getTitle() {
354 // T202454 avoid errors when there is no title
355 return parent::getTitle() ?: Title::makeTitle( NS_SPECIAL, 'BadTitle/DifferenceEngine' );
356 }
357
364 public function setReducedLineNumbers( $value = true ) {
365 $this->mReducedLineNumbers = $value;
366 }
367
373 public function getDiffLang() {
374 if ( $this->mDiffLang === null ) {
375 # Default language in which the diff text is written.
376 $this->mDiffLang = $this->getTitle()->getPageLanguage();
377 }
378
379 return $this->mDiffLang;
380 }
381
385 public function wasCacheHit() {
386 return $this->mCacheHit;
387 }
388
396 public function getOldid() {
397 $this->loadRevisionIds();
398
399 return $this->mOldid;
400 }
401
408 public function getNewid() {
409 $this->loadRevisionIds();
410
411 return $this->mNewid;
412 }
413
420 public function getOldRevision() {
421 return $this->mOldRevisionRecord ?: null;
422 }
423
429 public function getNewRevision() {
430 return $this->mNewRevisionRecord;
431 }
432
441 public function deletedLink( $id ) {
442 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
443 if ( $permissionManager->userHasRight( $this->getUser(), 'deletedhistory' ) ) {
445 $revStore = $this->revisionStore;
446 $arQuery = $revStore->getArchiveQueryInfo();
447 $row = $dbr->selectRow(
448 $arQuery['tables'],
449 array_merge( $arQuery['fields'], [ 'ar_namespace', 'ar_title' ] ),
450 [ 'ar_rev_id' => $id ],
451 __METHOD__,
452 [],
453 $arQuery['joins']
454 );
455 if ( $row ) {
456 $revRecord = $revStore->newRevisionFromArchiveRow( $row );
457 $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
458
459 return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( [
460 'target' => $title->getPrefixedText(),
461 'timestamp' => $revRecord->getTimestamp()
462 ] );
463 }
464 }
465
466 return false;
467 }
468
476 public function deletedIdMarker( $id ) {
477 $link = $this->deletedLink( $id );
478 if ( $link ) {
479 return "[$link $id]";
480 } else {
481 return (string)$id;
482 }
483 }
484
485 private function showMissingRevision() {
486 $out = $this->getOutput();
487
488 $missing = [];
489 if ( $this->mOldRevisionRecord === null ||
490 ( $this->mOldRevisionRecord && $this->mOldContent === null )
491 ) {
492 $missing[] = $this->deletedIdMarker( $this->mOldid );
493 }
494 if ( $this->mNewRevisionRecord === null ||
495 ( $this->mNewRevisionRecord && $this->mNewContent === null )
496 ) {
497 $missing[] = $this->deletedIdMarker( $this->mNewid );
498 }
499
500 $out->setPageTitle( $this->msg( 'errorpagetitle' ) );
501 $msg = $this->msg( 'difference-missing-revision' )
502 ->params( $this->getLanguage()->listToText( $missing ) )
503 ->numParams( count( $missing ) )
504 ->parseAsBlock();
505 $out->addHTML( $msg );
506 }
507
513 public function hasDeletedRevision() {
514 $this->loadRevisionData();
515 return (
516 $this->mNewRevisionRecord &&
517 $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
518 ) ||
519 (
520 $this->mOldRevisionRecord &&
521 $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
522 );
523 }
524
531 public function getPermissionErrors( User $user ) {
532 $this->loadRevisionData();
533 $permErrors = [];
534 $permManager = MediaWikiServices::getInstance()->getPermissionManager();
535 if ( $this->mNewPage ) {
536 $permErrors = $permManager->getPermissionErrors( 'read', $user, $this->mNewPage );
537 }
538 if ( $this->mOldPage ) {
539 $permErrors = wfMergeErrorArrays( $permErrors,
540 $permManager->getPermissionErrors( 'read', $user, $this->mOldPage ) );
541 }
542 return $permErrors;
543 }
544
550 public function hasSuppressedRevision() {
551 return $this->hasDeletedRevision() && (
552 ( $this->mOldRevisionRecord &&
553 $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) ||
554 ( $this->mNewRevisionRecord &&
555 $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) )
556 );
557 }
558
570 public function isUserAllowedToSeeRevisions( $user ) {
571 $this->loadRevisionData();
572 // $this->mNewRev will only be falsy if a loading error occurred
573 // (in which case the user is allowed to see).
574 $allowed = !$this->mNewRevisionRecord || RevisionRecord::userCanBitfield(
575 $this->mNewRevisionRecord->getVisibility(),
576 RevisionRecord::DELETED_TEXT,
577 $user
578 );
579 if ( $this->mOldRevisionRecord &&
580 !RevisionRecord::userCanBitfield(
581 $this->mOldRevisionRecord->getVisibility(),
582 RevisionRecord::DELETED_TEXT,
583 $user
584 )
585 ) {
586 $allowed = false;
587 }
588 return $allowed;
589 }
590
598 public function shouldBeHiddenFromUser( $user ) {
599 return $this->hasDeletedRevision() && ( !$this->unhide ||
600 !$this->isUserAllowedToSeeRevisions( $user ) );
601 }
602
603 public function showDiffPage( $diffOnly = false ) {
604 # Allow frames except in certain special cases
605 $out = $this->getOutput();
606 $out->allowClickjacking();
607 $out->setRobotPolicy( 'noindex,nofollow' );
608
609 // Allow extensions to add any extra output here
610 $this->hookRunner->onDifferenceEngineShowDiffPage( $out );
611
612 if ( !$this->loadRevisionData() ) {
613 if ( $this->hookRunner->onDifferenceEngineShowDiffPageMaybeShowMissingRevision( $this ) ) {
614 $this->showMissingRevision();
615 }
616 return;
617 }
618
619 $user = $this->getUser();
620 $permErrors = $this->getPermissionErrors( $user );
621 if ( count( $permErrors ) ) {
622 throw new PermissionsError( 'read', $permErrors );
623 }
624
625 $rollback = '';
626
627 $query = $this->slotDiffOptions;
628 # Carry over 'diffonly' param via navigation links
629 if ( $diffOnly != $user->getBoolOption( 'diffonly' ) ) {
630 $query['diffonly'] = $diffOnly;
631 }
632 # Cascade unhide param in links for easy deletion browsing
633 if ( $this->unhide ) {
634 $query['unhide'] = 1;
635 }
636
637 # Check if one of the revisions is deleted/suppressed
638 $deleted = $this->hasDeletedRevision();
639 $suppressed = $this->hasSuppressedRevision();
640 $allowed = $this->isUserAllowedToSeeRevisions( $user );
641
642 $revisionTools = [];
643
644 # mOldRevisionRecord is false if the difference engine is called with a "vague" query for
645 # a diff between a version V and its previous version V' AND the version V
646 # is the first version of that article. In that case, V' does not exist.
647 if ( $this->mOldRevisionRecord === false ) {
648 if ( $this->mNewPage ) {
649 $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
650 }
651 $samePage = true;
652 $oldHeader = '';
653 // Allow extensions to change the $oldHeader variable
654 $this->hookRunner->onDifferenceEngineOldHeaderNoOldRev( $oldHeader );
655 } else {
656 $this->hookRunner->onDifferenceEngineViewHeader( $this );
657
658 // DiffViewHeader hook is hard deprecated since 1.35
659 if ( $this->hookContainer->isRegistered( 'DiffViewHeader' ) ) {
660 // Only create the Revision object if needed
661 // If old or new are falsey, use null
662 $legacyOldRev = $this->mOldRevisionRecord ?
663 new Revision( $this->mOldRevisionRecord ) :
664 null;
665 $legacyNewRev = $this->mNewRevisionRecord ?
666 new Revision( $this->mNewRevisionRecord ) :
667 null;
668 $this->hookRunner->onDiffViewHeader(
669 $this,
670 $legacyOldRev,
671 $legacyNewRev
672 );
673 }
674
675 if ( !$this->mOldPage || !$this->mNewPage ) {
676 // XXX say something to the user?
677 $samePage = false;
678 } elseif ( $this->mNewPage->equals( $this->mOldPage ) ) {
679 $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
680 $samePage = true;
681 } else {
682 $out->setPageTitle( $this->msg( 'difference-title-multipage',
683 $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) );
684 $out->addSubtitle( $this->msg( 'difference-multipage' ) );
685 $samePage = false;
686 }
687
688 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
689
690 if ( $samePage && $this->mNewPage && $permissionManager->quickUserCan(
691 'edit', $user, $this->mNewPage
692 ) ) {
693 if ( $this->mNewRevisionRecord->isCurrent() && $permissionManager->quickUserCan(
694 'rollback', $user, $this->mNewPage
695 ) ) {
696 $rollbackLink = Linker::generateRollback(
697 $this->mNewRevisionRecord,
698 $this->getContext(),
699 [ 'noBrackets' ]
700 );
701 if ( $rollbackLink ) {
702 $out->preventClickjacking();
703 $rollback = "\u{00A0}\u{00A0}\u{00A0}" . $rollbackLink;
704 }
705 }
706
707 if ( $this->userCanEdit( $this->mOldRevisionRecord ) &&
708 $this->userCanEdit( $this->mNewRevisionRecord )
709 ) {
710 $undoLink = Html::element( 'a', [
711 'href' => $this->mNewPage->getLocalURL( [
712 'action' => 'edit',
713 'undoafter' => $this->mOldid,
714 'undo' => $this->mNewid
715 ] ),
716 'title' => Linker::titleAttrib( 'undo' ),
717 ],
718 $this->msg( 'editundo' )->text()
719 );
720 $revisionTools['mw-diff-undo'] = $undoLink;
721 }
722 }
723 # Make "previous revision link"
724 $hasPrevious = $samePage && $this->mOldPage &&
725 $this->revisionStore->getPreviousRevision( $this->mOldRevisionRecord );
726 if ( $hasPrevious ) {
727 $prevlink = $this->linkRenderer->makeKnownLink(
728 $this->mOldPage,
729 $this->msg( 'previousdiff' )->text(),
730 [ 'id' => 'differences-prevlink' ],
731 [ 'diff' => 'prev', 'oldid' => $this->mOldid ] + $query
732 );
733 } else {
734 $prevlink = "\u{00A0}";
735 }
736
737 if ( $this->mOldRevisionRecord->isMinor() ) {
738 $oldminor = ChangesList::flag( 'minor' );
739 } else {
740 $oldminor = '';
741 }
742
743 $oldRevRecord = $this->mOldRevisionRecord;
744
745 $ldel = $this->revisionDeleteLink( $oldRevRecord );
746 $oldRevisionHeader = $this->getRevisionHeader( $oldRevRecord, 'complete' );
747 $oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff', $this->getContext() );
748
749 $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' .
750 '<div id="mw-diff-otitle2">' .
751 Linker::revUserTools( $oldRevRecord, !$this->unhide ) . '</div>' .
752 '<div id="mw-diff-otitle3">' . $oldminor .
753 Linker::revComment( $oldRevRecord, !$diffOnly, !$this->unhide ) . $ldel . '</div>' .
754 '<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' .
755 '<div id="mw-diff-otitle4">' . $prevlink . '</div>';
756
757 // Allow extensions to change the $oldHeader variable
758 $this->hookRunner->onDifferenceEngineOldHeader(
759 $this, $oldHeader, $prevlink, $oldminor, $diffOnly, $ldel, $this->unhide );
760 }
761
762 $out->addJsConfigVars( [
763 'wgDiffOldId' => $this->mOldid,
764 'wgDiffNewId' => $this->mNewid,
765 ] );
766
767 # Make "next revision link"
768 # Skip next link on the top revision
769 if ( $samePage && $this->mNewPage && !$this->mNewRevisionRecord->isCurrent() ) {
770 $nextlink = $this->linkRenderer->makeKnownLink(
771 $this->mNewPage,
772 $this->msg( 'nextdiff' )->text(),
773 [ 'id' => 'differences-nextlink' ],
774 [ 'diff' => 'next', 'oldid' => $this->mNewid ] + $query
775 );
776 } else {
777 $nextlink = "\u{00A0}";
778 }
779
780 if ( $this->mNewRevisionRecord->isMinor() ) {
781 $newminor = ChangesList::flag( 'minor' );
782 } else {
783 $newminor = '';
784 }
785
786 # Handle RevisionDelete links...
787 $rdel = $this->revisionDeleteLink( $this->mNewRevisionRecord );
788
789 # Allow extensions to define their own revision tools
790 $this->hookRunner->onDiffTools(
791 $this->mNewRevisionRecord,
792 $revisionTools,
793 $this->mOldRevisionRecord ?: null,
794 $user
795 );
796
797 # Hook deprecated since 1.35
798 if ( $this->hookContainer->isRegistered( 'DiffRevisionTools' ) ) {
799 # Only create the Revision objects if they are needed
800 $legacyOldRev = $this->mOldRevisionRecord ?
801 new Revision( $this->mOldRevisionRecord ) :
802 null;
803 $legacyNewRev = $this->mNewRevisionRecord ?
804 new Revision( $this->mNewRevisionRecord ) :
805 null;
806 $this->hookRunner->onDiffRevisionTools(
807 $legacyNewRev,
808 $revisionTools,
809 $legacyOldRev,
810 $user
811 );
812 }
813
814 $formattedRevisionTools = [];
815 // Put each one in parentheses (poor man's button)
816 foreach ( $revisionTools as $key => $tool ) {
817 $toolClass = is_string( $key ) ? $key : 'mw-diff-tool';
818 $element = Html::rawElement(
819 'span',
820 [ 'class' => $toolClass ],
821 $this->msg( 'parentheses' )->rawParams( $tool )->escaped()
822 );
823 $formattedRevisionTools[] = $element;
824 }
825
826 $newRevRecord = $this->mNewRevisionRecord;
827
828 $newRevisionHeader = $this->getRevisionHeader( $newRevRecord, 'complete' ) .
829 ' ' . implode( ' ', $formattedRevisionTools );
830 $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff', $this->getContext() );
831
832 $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' .
833 '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $newRevRecord, !$this->unhide ) .
834 " $rollback</div>" .
835 '<div id="mw-diff-ntitle3">' . $newminor .
836 Linker::revComment( $newRevRecord, !$diffOnly, !$this->unhide ) . $rdel . '</div>' .
837 '<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' .
838 '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
839
840 // Allow extensions to change the $newHeader variable
841 $this->hookRunner->onDifferenceEngineNewHeader( $this, $newHeader,
842 $formattedRevisionTools, $nextlink, $rollback, $newminor, $diffOnly,
843 $rdel, $this->unhide );
844
845 # If the diff cannot be shown due to a deleted revision, then output
846 # the diff header and links to unhide (if available)...
847 if ( $this->shouldBeHiddenFromUser( $user ) ) {
848 $this->showDiffStyle();
849 $multi = $this->getMultiNotice();
850 $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
851 if ( !$allowed ) {
852 $msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff';
853 # Give explanation for why revision is not visible
854 $out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
855 [ $msg ] );
856 } else {
857 # Give explanation and add a link to view the diff...
858 $query = $this->getRequest()->appendQueryValue( 'unhide', '1' );
859 $link = $this->getTitle()->getFullURL( $query );
860 $msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff';
861 $out->wrapWikiMsg(
862 "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
863 [ $msg, $link ]
864 );
865 }
866 # Otherwise, output a regular diff...
867 } else {
868 # Add deletion notice if the user is viewing deleted content
869 $notice = '';
870 if ( $deleted ) {
871 $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
872 $notice = "<div id='mw-$msg' class='mw-warning plainlinks'>\n" .
873 $this->msg( $msg )->parse() .
874 "</div>\n";
875 }
876 $this->showDiff( $oldHeader, $newHeader, $notice );
877 if ( !$diffOnly ) {
878 $this->renderNewRevision();
879 }
880 }
881 }
882
893 public function markPatrolledLink() {
894 if ( $this->mMarkPatrolledLink === null ) {
895 $linkInfo = $this->getMarkPatrolledLinkInfo();
896 // If false, there is no patrol link needed/allowed
897 if ( !$linkInfo || !$this->mNewPage ) {
898 $this->mMarkPatrolledLink = '';
899 } else {
900 $this->mMarkPatrolledLink = ' <span class="patrollink" data-mw="interface">[' .
901 $this->linkRenderer->makeKnownLink(
902 $this->mNewPage,
903 $this->msg( 'markaspatrolleddiff' )->text(),
904 [],
905 [
906 'action' => 'markpatrolled',
907 'rcid' => $linkInfo['rcid'],
908 ]
909 ) . ']</span>';
910 // Allow extensions to change the markpatrolled link
911 $this->hookRunner->onDifferenceEngineMarkPatrolledLink( $this,
912 $this->mMarkPatrolledLink, $linkInfo['rcid'] );
913 }
914 }
915 return $this->mMarkPatrolledLink;
916 }
917
925 protected function getMarkPatrolledLinkInfo() {
926 $user = $this->getUser();
927 $config = $this->getConfig();
928 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
929
930 // Prepare a change patrol link, if applicable
931 if (
932 // Is patrolling enabled and the user allowed to?
933 $config->get( 'UseRCPatrol' ) &&
934 $this->mNewPage &&
935 $permissionManager->quickUserCan( 'patrol', $user, $this->mNewPage ) &&
936 // Only do this if the revision isn't more than 6 hours older
937 // than the Max RC age (6h because the RC might not be cleaned out regularly)
938 RecentChange::isInRCLifespan( $this->mNewRevisionRecord->getTimestamp(), 21600 )
939 ) {
940 // Look for an unpatrolled change corresponding to this diff
941 $change = RecentChange::newFromConds(
942 [
943 'rc_this_oldid' => $this->mNewid,
944 'rc_patrolled' => RecentChange::PRC_UNPATROLLED
945 ],
946 __METHOD__
947 );
948
949 if ( $change && !$change->getPerformer()->equals( $user ) ) {
950 $rcid = $change->getAttribute( 'rc_id' );
951 } else {
952 // None found or the page has been created by the current user.
953 // If the user could patrol this it already would be patrolled
954 $rcid = 0;
955 }
956
957 // Allow extensions to possibly change the rcid here
958 // For example the rcid might be set to zero due to the user
959 // being the same as the performer of the change but an extension
960 // might still want to show it under certain conditions
961 $this->hookRunner->onDifferenceEngineMarkPatrolledRCID( $rcid, $this, $change, $user );
962
963 // Build the link
964 if ( $rcid ) {
965 $this->getOutput()->preventClickjacking();
966 if ( $permissionManager->userHasRight( $user, 'writeapi' ) ) {
967 $this->getOutput()->addModules( 'mediawiki.misc-authed-curate' );
968 }
969
970 return [
971 'rcid' => $rcid,
972 ];
973 }
974 }
975
976 // No mark as patrolled link applicable
977 return false;
978 }
979
985 private function revisionDeleteLink( RevisionRecord $revRecord ) {
987 $this->getUser(),
988 $revRecord,
989 $revRecord->getPageAsLinkTarget()
990 );
991 if ( $link !== '' ) {
992 $link = "\u{00A0}\u{00A0}\u{00A0}" . $link . ' ';
993 }
994
995 return $link;
996 }
997
1003 public function renderNewRevision() {
1004 if ( $this->isContentOverridden ) {
1005 // The code below only works with a Revision object. We could construct a fake revision
1006 // (here or in setContent), but since this does not seem needed at the moment,
1007 // we'll just fail for now.
1008 throw new LogicException(
1009 __METHOD__
1010 . ' is not supported after calling setContent(). Use setRevisions() instead.'
1011 );
1012 }
1013
1014 $out = $this->getOutput();
1015 $revHeader = $this->getRevisionHeader( $this->mNewRevisionRecord );
1016 # Add "current version as of X" title
1017 $out->addHTML( "<hr class='diff-hr' id='mw-oldid' />
1018 <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
1019 # Page content may be handled by a hooked call instead...
1020 if ( $this->hookRunner->onArticleContentOnDiff( $this, $out ) ) {
1021 $this->loadNewText();
1022 if ( !$this->mNewPage ) {
1023 // New revision is unsaved; bail out.
1024 // TODO in theory rendering the new revision is a meaningful thing to do
1025 // even if it's unsaved, but a lot of untangling is required to do it safely.
1026 return;
1027 }
1028
1029 $out->setRevisionId( $this->mNewid );
1030 $out->setRevisionTimestamp( $this->mNewRevisionRecord->getTimestamp() );
1031 $out->setArticleFlag( true );
1032
1033 if ( !$this->hookRunner->onArticleRevisionViewCustom(
1034 $this->mNewRevisionRecord, $this->mNewPage, $this->mOldid, $out )
1035 ) {
1036 // Handled by extension
1037 // NOTE: sync with hooks called in Article::view()
1038 } else {
1039 // Normal page
1040 if ( $this->getTitle()->equals( $this->mNewPage ) ) {
1041 // If the Title stored in the context is the same as the one
1042 // of the new revision, we can use its associated WikiPage
1043 // object.
1044 $wikiPage = $this->getWikiPage();
1045 } else {
1046 // Otherwise we need to create our own WikiPage object
1047 $wikiPage = WikiPage::factory( $this->mNewPage );
1048 }
1049
1050 $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRevisionRecord );
1051
1052 # WikiPage::getParserOutput() should not return false, but just in case
1053 if ( $parserOutput ) {
1054 // Allow extensions to change parser output here
1055 if ( $this->hookRunner->onDifferenceEngineRenderRevisionAddParserOutput(
1056 $this, $out, $parserOutput, $wikiPage )
1057 ) {
1058 $out->addParserOutput( $parserOutput, [
1059 'enableSectionEditLinks' => $this->mNewRevisionRecord->isCurrent()
1060 && MediaWikiServices::getInstance()->getPermissionManager()->quickUserCan(
1061 'edit',
1062 $this->getUser(),
1063 $this->mNewRevisionRecord->getPageAsLinkTarget()
1064 )
1065 ] );
1066 }
1067 }
1068 }
1069 }
1070
1071 // Allow extensions to optionally not show the final patrolled link
1072 if ( $this->hookRunner->onDifferenceEngineRenderRevisionShowFinalPatrolLink() ) {
1073 # Add redundant patrol link on bottom...
1074 $out->addHTML( $this->markPatrolledLink() );
1075 }
1076 }
1077
1084 protected function getParserOutput( WikiPage $page, RevisionRecord $revRecord ) {
1085 if ( !$revRecord->getId() ) {
1086 // WikiPage::getParserOutput wants a revision ID. Passing 0 will incorrectly show
1087 // the current revision, so fail instead. If need be, WikiPage::getParserOutput
1088 // could be made to accept a Revision or RevisionRecord instead of the id.
1089 return false;
1090 }
1091
1092 $parserOptions = $page->makeParserOptions( $this->getContext() );
1093 $parserOutput = $page->getParserOutput( $parserOptions, $revRecord->getId() );
1094
1095 return $parserOutput;
1096 }
1097
1108 public function showDiff( $otitle, $ntitle, $notice = '' ) {
1109 // Allow extensions to affect the output here
1110 $this->hookRunner->onDifferenceEngineShowDiff( $this );
1111
1112 $diff = $this->getDiff( $otitle, $ntitle, $notice );
1113 if ( $diff === false ) {
1114 $this->showMissingRevision();
1115
1116 return false;
1117 } else {
1118 $this->showDiffStyle();
1119 $this->getOutput()->addHTML( $diff );
1120
1121 return true;
1122 }
1123 }
1124
1128 public function showDiffStyle() {
1129 if ( !$this->isSlotDiffRenderer ) {
1130 $this->getOutput()->addModuleStyles( [
1131 'mediawiki.interface.helpers.styles',
1132 'mediawiki.diff.styles'
1133 ] );
1134 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1135 $slotDiffRenderer->addModules( $this->getOutput() );
1136 }
1137 }
1138 }
1139
1149 public function getDiff( $otitle, $ntitle, $notice = '' ) {
1150 $body = $this->getDiffBody();
1151 if ( $body === false ) {
1152 return false;
1153 }
1154
1155 $multi = $this->getMultiNotice();
1156 // Display a message when the diff is empty
1157 if ( $body === '' ) {
1158 $notice .= '<div class="mw-diff-empty">' .
1159 $this->msg( 'diff-empty' )->parse() .
1160 "</div>\n";
1161 }
1162
1163 return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
1164 }
1165
1171 public function getDiffBody() {
1172 $this->mCacheHit = true;
1173 // Check if the diff should be hidden from this user
1174 if ( !$this->isContentOverridden ) {
1175 if ( !$this->loadRevisionData() ) {
1176 return false;
1177 } elseif ( $this->mOldRevisionRecord &&
1178 !RevisionRecord::userCanBitfield(
1179 $this->mOldRevisionRecord->getVisibility(),
1180 RevisionRecord::DELETED_TEXT,
1181 $this->getUser()
1182 )
1183 ) {
1184 return false;
1185 } elseif ( $this->mNewRevisionRecord &&
1186 !RevisionRecord::userCanBitfield(
1187 $this->mNewRevisionRecord->getVisibility(),
1188 RevisionRecord::DELETED_TEXT,
1189 $this->getUser()
1190 )
1191 ) {
1192 return false;
1193 }
1194 // Short-circuit
1195 if ( $this->mOldRevisionRecord === false || (
1196 $this->mOldRevisionRecord &&
1197 $this->mNewRevisionRecord &&
1198 $this->mOldRevisionRecord->getId() &&
1199 $this->mOldRevisionRecord->getId() == $this->mNewRevisionRecord->getId()
1200 ) ) {
1201 if ( $this->hookRunner->onDifferenceEngineShowEmptyOldContent( $this ) ) {
1202 return '';
1203 }
1204 }
1205 }
1206
1207 // Cacheable?
1208 $key = false;
1209 $services = MediaWikiServices::getInstance();
1210 $cache = $services->getMainWANObjectCache();
1211 $stats = $services->getStatsdDataFactory();
1212 if ( $this->mOldid && $this->mNewid ) {
1213 // Check if subclass is still using the old way
1214 // for backwards-compatibility
1215 $key = $this->getDiffBodyCacheKey();
1216 if ( $key === null ) {
1217 $key = $cache->makeKey( ...$this->getDiffBodyCacheKeyParams() );
1218 }
1219
1220 // Try cache
1221 if ( !$this->mRefreshCache ) {
1222 $difftext = $cache->get( $key );
1223 if ( is_string( $difftext ) ) {
1224 $stats->updateCount( 'diff_cache.hit', 1 );
1225 $difftext = $this->localiseDiff( $difftext );
1226 $difftext .= "\n<!-- diff cache key $key -->\n";
1227
1228 return $difftext;
1229 }
1230 } // don't try to load but save the result
1231 }
1232 $this->mCacheHit = false;
1233
1234 // Loadtext is permission safe, this just clears out the diff
1235 if ( !$this->loadText() ) {
1236 return false;
1237 }
1238
1239 $difftext = '';
1240 // We've checked for revdelete at the beginning of this method; it's OK to ignore
1241 // read permissions here.
1242 $slotContents = $this->getSlotContents();
1243 foreach ( $this->getSlotDiffRenderers() as $role => $slotDiffRenderer ) {
1244 $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role]['old'],
1245 $slotContents[$role]['new'] );
1246 if ( $slotDiff && $role !== SlotRecord::MAIN ) {
1247 // FIXME: ask SlotRoleHandler::getSlotNameMessage
1248 $slotTitle = $role;
1249 $difftext .= $this->getSlotHeader( $slotTitle );
1250 }
1251 $difftext .= $slotDiff;
1252 }
1253
1254 // Save to cache for 7 days
1255 if ( !$this->hookRunner->onAbortDiffCache( $this ) ) {
1256 $stats->updateCount( 'diff_cache.uncacheable', 1 );
1257 } elseif ( $key !== false ) {
1258 $stats->updateCount( 'diff_cache.miss', 1 );
1259 $cache->set( $key, $difftext, 7 * 86400 );
1260 } else {
1261 $stats->updateCount( 'diff_cache.uncacheable', 1 );
1262 }
1263 // localise line numbers and title attribute text
1264 $difftext = $this->localiseDiff( $difftext );
1265
1266 return $difftext;
1267 }
1268
1275 public function getDiffBodyForRole( $role ) {
1276 $diffRenderers = $this->getSlotDiffRenderers();
1277 if ( !isset( $diffRenderers[$role] ) ) {
1278 return false;
1279 }
1280
1281 $slotContents = $this->getSlotContents();
1282 $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role]['old'],
1283 $slotContents[$role]['new'] );
1284 if ( !$slotDiff ) {
1285 return false;
1286 }
1287
1288 if ( $role !== SlotRecord::MAIN ) {
1289 // TODO use human-readable role name at least
1290 $slotTitle = $role;
1291 $slotDiff = $this->getSlotHeader( $slotTitle ) . $slotDiff;
1292 }
1293
1294 return $this->localiseDiff( $slotDiff );
1295 }
1296
1304 protected function getSlotHeader( $headerText ) {
1305 // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
1306 $columnCount = $this->mOldRevisionRecord ? 4 : 2;
1307 $userLang = $this->getLanguage()->getHtmlCode();
1308 return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-header', 'lang' => $userLang ],
1309 Html::element( 'th', [ 'colspan' => $columnCount ], $headerText ) );
1310 }
1311
1321 protected function getDiffBodyCacheKey() {
1322 return null;
1323 }
1324
1338 protected function getDiffBodyCacheKeyParams() {
1339 if ( !$this->mOldid || !$this->mNewid ) {
1340 throw new MWException( 'mOldid and mNewid must be set to get diff cache key.' );
1341 }
1342
1343 $engine = $this->getEngine();
1344 $params = [
1345 'diff',
1346 $engine === 'php' ? false : $engine, // Back compat
1347 self::DIFF_VERSION,
1348 "old-{$this->mOldid}",
1349 "rev-{$this->mNewid}"
1350 ];
1351
1352 if ( $engine === 'wikidiff2' ) {
1353 $params[] = phpversion( 'wikidiff2' );
1354 }
1355
1356 if ( !$this->isSlotDiffRenderer ) {
1357 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1358 $params = array_merge( $params, $slotDiffRenderer->getExtraCacheKeys() );
1359 }
1360 }
1361
1362 return $params;
1363 }
1364
1372 public function getExtraCacheKeys() {
1373 // This method is called when the DifferenceEngine is used for a slot diff. We only care
1374 // about special things, not the revision IDs, which are added to the cache key by the
1375 // page-level DifferenceEngine, and which might not have a valid value for this object.
1376 $this->mOldid = 123456789;
1377 $this->mNewid = 987654321;
1378
1379 // This will repeat a bunch of unnecessary key fields for each slot. Not nice but harmless.
1380 $cacheString = $this->getDiffBodyCacheKey();
1381 if ( $cacheString ) {
1382 return [ $cacheString ];
1383 }
1384
1385 $params = $this->getDiffBodyCacheKeyParams();
1386
1387 // Try to get rid of the standard keys to keep the cache key human-readable:
1388 // call the getDiffBodyCacheKeyParams implementation of the base class, and if
1389 // the child class includes the same keys, drop them.
1390 // Uses an obscure PHP feature where static calls to non-static methods are allowed
1391 // as long as we are already in a non-static method of the same class, and the call context
1392 // ($this) will be inherited.
1393 // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
1395 if ( array_slice( $params, 0, count( $standardParams ) ) === $standardParams ) {
1396 $params = array_slice( $params, count( $standardParams ) );
1397 }
1398
1399 return $params;
1400 }
1401
1405 public function setSlotDiffOptions( $options ) {
1406 $this->slotDiffOptions = $options;
1407 }
1408
1422 public function generateContentDiffBody( Content $old, Content $new ) {
1423 $slotDiffRenderer = $new->getContentHandler()->getSlotDiffRenderer( $this->getContext() );
1424 if (
1425 $slotDiffRenderer instanceof DifferenceEngineSlotDiffRenderer
1426 && $this->isSlotDiffRenderer
1427 ) {
1428 // Oops, we are just about to enter an infinite loop (the slot-level DifferenceEngine
1429 // called a DifferenceEngineSlotDiffRenderer that wraps the same DifferenceEngine class).
1430 // This will happen when a content model has no custom slot diff renderer, it does have
1431 // a custom difference engine, but that does not override this method.
1432 throw new Exception( get_class( $this ) . ': could not maintain backwards compatibility. '
1433 . 'Please use a SlotDiffRenderer.' );
1434 }
1435 return $slotDiffRenderer->getDiff( $old, $new ) . $this->getDebugString();
1436 }
1437
1450 public function generateTextDiffBody( $otext, $ntext ) {
1451 $slotDiffRenderer = $this->contentHandlerFactory
1452 ->getContentHandler( CONTENT_MODEL_TEXT )
1453 ->getSlotDiffRenderer( $this->getContext() );
1454 if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1455 // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1456 // This is too unlikely to happen to bother handling properly.
1457 throw new Exception( 'The slot diff renderer for text content should be a '
1458 . 'TextSlotDiffRenderer subclass' );
1459 }
1460 return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1461 }
1462
1469 public static function getEngine() {
1470 $diffEngine = MediaWikiServices::getInstance()->getMainConfig()
1471 ->get( 'DiffEngine' );
1472 $externalDiffEngine = MediaWikiServices::getInstance()->getMainConfig()
1473 ->get( 'ExternalDiffEngine' );
1474
1475 if ( $diffEngine === null ) {
1476 $engines = [ 'external', 'wikidiff2', 'php' ];
1477 } else {
1478 $engines = [ $diffEngine ];
1479 }
1480
1481 $failureReason = null;
1482 foreach ( $engines as $engine ) {
1483 switch ( $engine ) {
1484 case 'external':
1485 if ( is_string( $externalDiffEngine ) ) {
1486 if ( is_executable( $externalDiffEngine ) ) {
1487 return $externalDiffEngine;
1488 }
1489 $failureReason = 'ExternalDiffEngine config points to a non-executable';
1490 if ( $diffEngine === null ) {
1491 wfDebug( "$failureReason, ignoring" );
1492 }
1493 } else {
1494 $failureReason = 'ExternalDiffEngine config is set to a non-string value';
1495 if ( $diffEngine === null && $externalDiffEngine ) {
1496 wfWarn( "$failureReason, ignoring" );
1497 }
1498 }
1499 break;
1500
1501 case 'wikidiff2':
1502 if ( function_exists( 'wikidiff2_do_diff' ) ) {
1503 return 'wikidiff2';
1504 }
1505 $failureReason = 'wikidiff2 is not available';
1506 break;
1507
1508 case 'php':
1509 // Always available.
1510 return 'php';
1511
1512 default:
1513 throw new DomainException( 'Invalid value for $wgDiffEngine: ' . $engine );
1514 }
1515 }
1516 throw new UnexpectedValueException( "Cannot use diff engine '$engine': $failureReason" );
1517 }
1518
1531 protected function textDiff( $otext, $ntext ) {
1532 $slotDiffRenderer = $this->contentHandlerFactory
1533 ->getContentHandler( CONTENT_MODEL_TEXT )
1534 ->getSlotDiffRenderer( $this->getContext() );
1535 if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1536 // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1537 // This is too unlikely to happen to bother handling properly.
1538 throw new Exception( 'The slot diff renderer for text content should be a '
1539 . 'TextSlotDiffRenderer subclass' );
1540 }
1541 return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1542 }
1543
1552 protected function debug( $generator = "internal" ) {
1553 if ( !$this->enableDebugComment ) {
1554 return '';
1555 }
1556 $data = [ $generator ];
1557 if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
1558 $data[] = wfHostname();
1559 }
1560 $data[] = wfTimestamp( TS_DB );
1561
1562 return "<!-- diff generator: " .
1563 implode( " ", array_map( "htmlspecialchars", $data ) ) .
1564 " -->\n";
1565 }
1566
1567 private function getDebugString() {
1568 $engine = self::getEngine();
1569 if ( $engine === 'wikidiff2' ) {
1570 return $this->debug( 'wikidiff2' );
1571 } elseif ( $engine === 'php' ) {
1572 return $this->debug( 'native PHP' );
1573 } else {
1574 return $this->debug( "external $engine" );
1575 }
1576 }
1577
1584 private function localiseDiff( $text ) {
1585 $text = $this->localiseLineNumbers( $text );
1586 if ( $this->getEngine() === 'wikidiff2' &&
1587 version_compare( phpversion( 'wikidiff2' ), '1.5.1', '>=' )
1588 ) {
1589 $text = $this->addLocalisedTitleTooltips( $text );
1590 }
1591 return $text;
1592 }
1593
1601 public function localiseLineNumbers( $text ) {
1602 return preg_replace_callback(
1603 '/<!--LINE (\d+)-->/',
1604 [ $this, 'localiseLineNumbersCb' ],
1605 $text
1606 );
1607 }
1608
1609 public function localiseLineNumbersCb( $matches ) {
1610 if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
1611 return '';
1612 }
1613
1614 return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
1615 }
1616
1623 private function addLocalisedTitleTooltips( $text ) {
1624 return preg_replace_callback(
1625 '/class="mw-diff-movedpara-(left|right)"/',
1626 [ $this, 'addLocalisedTitleTooltipsCb' ],
1627 $text
1628 );
1629 }
1630
1635 private function addLocalisedTitleTooltipsCb( array $matches ) {
1636 $key = $matches[1] === 'right' ?
1637 'diff-paragraph-moved-toold' :
1638 'diff-paragraph-moved-tonew';
1639 return $matches[0] . ' title="' . $this->msg( $key )->escaped() . '"';
1640 }
1641
1647 public function getMultiNotice() {
1648 // The notice only make sense if we are diffing two saved revisions of the same page.
1649 if (
1650 !$this->mOldRevisionRecord || !$this->mNewRevisionRecord
1651 || !$this->mOldPage || !$this->mNewPage
1652 || !$this->mOldPage->equals( $this->mNewPage )
1653 || $this->mOldRevisionRecord->getId() === null
1654 || $this->mNewRevisionRecord->getId() === null
1655 // (T237709) Deleted revs might have different page IDs
1656 || $this->mNewPage->getArticleID() !== $this->mOldRevisionRecord->getPageId()
1657 || $this->mNewPage->getArticleID() !== $this->mNewRevisionRecord->getPageId()
1658 ) {
1659 return '';
1660 }
1661
1662 if ( $this->mOldRevisionRecord->getTimestamp() > $this->mNewRevisionRecord->getTimestamp() ) {
1663 $oldRevRecord = $this->mNewRevisionRecord; // flip
1664 $newRevRecord = $this->mOldRevisionRecord; // flip
1665 } else { // normal case
1666 $oldRevRecord = $this->mOldRevisionRecord;
1667 $newRevRecord = $this->mNewRevisionRecord;
1668 }
1669
1670 // Sanity: don't show the notice if too many rows must be scanned
1671 // @todo show some special message for that case
1672 $nEdits = 0;
1673 $revisionIdList = $this->revisionStore->getRevisionIdsBetween(
1674 $this->mNewPage->getArticleID(),
1675 $oldRevRecord,
1676 $newRevRecord,
1677 1000
1678 );
1679 // only count revisions that are visible
1680 if ( count( $revisionIdList ) > 0 ) {
1681 foreach ( $revisionIdList as $revisionId ) {
1682 $revision = $this->revisionStore->getRevisionById( $revisionId );
1683 if ( $revision->getUser( RevisionRecord::FOR_THIS_USER, $this->getUser() ) ) {
1684 $nEdits++;
1685 }
1686 }
1687 }
1688 if ( $nEdits > 0 && $nEdits <= 1000 ) {
1689 $limit = 100; // use diff-multi-manyusers if too many users
1690 try {
1691 $users = $this->revisionStore->getAuthorsBetween(
1692 $this->mNewPage->getArticleID(),
1693 $oldRevRecord,
1694 $newRevRecord,
1695 null,
1696 $limit
1697 );
1698 $numUsers = count( $users );
1699
1700 $newRevUser = $newRevRecord->getUser( RevisionRecord::RAW );
1701 $newRevUserText = $newRevUser ? $newRevUser->getName() : '';
1702 if ( $numUsers == 1 && $users[0]->getName() == $newRevUserText ) {
1703 $numUsers = 0; // special case to say "by the same user" instead of "by one other user"
1704 }
1705 } catch ( InvalidArgumentException $e ) {
1706 $numUsers = 0;
1707 }
1708
1709 return self::intermediateEditsMsg( $nEdits, $numUsers, $limit );
1710 }
1711
1712 return '';
1713 }
1714
1724 public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) {
1725 if ( $numUsers === 0 ) {
1726 $msg = 'diff-multi-sameuser';
1727 } elseif ( $numUsers > $limit ) {
1728 $msg = 'diff-multi-manyusers';
1729 $numUsers = $limit;
1730 } else {
1731 $msg = 'diff-multi-otherusers';
1732 }
1733
1734 return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1735 }
1736
1741 private function userCanEdit( RevisionRecord $revRecord ) {
1742 $user = $this->getUser();
1743
1744 if ( !RevisionRecord::userCanBitfield(
1745 $revRecord->getVisibility(),
1746 RevisionRecord::DELETED_TEXT,
1747 $user
1748 ) ) {
1749 return false;
1750 }
1751
1752 return true;
1753 }
1754
1764 public function getRevisionHeader( $rev, $complete = '' ) {
1765 if ( $rev instanceof Revision ) {
1766 wfDeprecated( __METHOD__ . ' with a Revision object', '1.35' );
1767 $rev = $rev->getRevisionRecord();
1768 }
1769
1770 $lang = $this->getLanguage();
1771 $user = $this->getUser();
1772 $revtimestamp = $rev->getTimestamp();
1773 $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
1774 $dateofrev = $lang->userDate( $revtimestamp, $user );
1775 $timeofrev = $lang->userTime( $revtimestamp, $user );
1776
1777 $header = $this->msg(
1778 $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
1779 $timestamp,
1780 $dateofrev,
1781 $timeofrev
1782 );
1783
1784 if ( $complete !== 'complete' ) {
1785 return $header->escaped();
1786 }
1787
1788 $title = $rev->getPageAsLinkTarget();
1789
1790 $header = $this->linkRenderer->makeKnownLink( $title, $header->text(), [],
1791 [ 'oldid' => $rev->getId() ] );
1792
1793 if ( $this->userCanEdit( $rev ) ) {
1794 $editQuery = [ 'action' => 'edit' ];
1795 if ( !$rev->isCurrent() ) {
1796 $editQuery['oldid'] = $rev->getId();
1797 }
1798
1799 $key = MediaWikiServices::getInstance()->getPermissionManager()
1800 ->quickUserCan( 'edit', $user, $title ) ? 'editold' : 'viewsourceold';
1801 $msg = $this->msg( $key )->text();
1802 $editLink = $this->msg( 'parentheses' )->rawParams(
1803 $this->linkRenderer->makeKnownLink( $title, $msg, [], $editQuery ) )->escaped();
1804 $header .= ' ' . Html::rawElement(
1805 'span',
1806 [ 'class' => 'mw-diff-edit' ],
1807 $editLink
1808 );
1809 if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1810 $header = Html::rawElement(
1811 'span',
1812 [ 'class' => 'history-deleted' ],
1813 $header
1814 );
1815 }
1816 } else {
1817 $header = Html::rawElement( 'span', [ 'class' => 'history-deleted' ], $header );
1818 }
1819
1820 return $header;
1821 }
1822
1835 public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
1836 // shared.css sets diff in interface language/dir, but the actual content
1837 // is often in a different language, mostly the page content language/dir
1838 $header = Html::openElement( 'table', [
1839 'class' => [
1840 'diff',
1841 'diff-contentalign-' . $this->getDiffLang()->alignStart(),
1842 'diff-editfont-' . $this->getUser()->getOption( 'editfont' )
1843 ],
1844 'data-mw' => 'interface',
1845 ] );
1846 $userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
1847
1848 if ( !$diff && !$otitle ) {
1849 $header .= "
1850 <tr class=\"diff-title\" lang=\"{$userLang}\">
1851 <td class=\"diff-ntitle\">{$ntitle}</td>
1852 </tr>";
1853 $multiColspan = 1;
1854 } else {
1855 if ( $diff ) { // Safari/Chrome show broken output if cols not used
1856 $header .= "
1857 <col class=\"diff-marker\" />
1858 <col class=\"diff-content\" />
1859 <col class=\"diff-marker\" />
1860 <col class=\"diff-content\" />";
1861 $colspan = 2;
1862 $multiColspan = 4;
1863 } else {
1864 $colspan = 1;
1865 $multiColspan = 2;
1866 }
1867 if ( $otitle || $ntitle ) {
1868 $header .= "
1869 <tr class=\"diff-title\" lang=\"{$userLang}\">
1870 <td colspan=\"$colspan\" class=\"diff-otitle\">{$otitle}</td>
1871 <td colspan=\"$colspan\" class=\"diff-ntitle\">{$ntitle}</td>
1872 </tr>";
1873 }
1874 }
1875
1876 if ( $multi != '' ) {
1877 $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1878 "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
1879 }
1880 if ( $notice != '' ) {
1881 $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1882 "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
1883 }
1884
1885 return $header . $diff . "</table>";
1886 }
1887
1895 public function setContent( Content $oldContent, Content $newContent ) {
1896 $this->mOldContent = $oldContent;
1897 $this->mNewContent = $newContent;
1898
1899 $this->mTextLoaded = 2;
1900 $this->mRevisionsLoaded = true;
1901 $this->isContentOverridden = true;
1902 $this->slotDiffRenderers = null;
1903 }
1904
1910 public function setRevisions(
1911 ?RevisionRecord $oldRevision, RevisionRecord $newRevision
1912 ) {
1913 if ( $oldRevision ) {
1914 $this->mOldRevisionRecord = $oldRevision;
1915 $this->mOldid = $oldRevision->getId();
1916 $this->mOldPage = Title::newFromLinkTarget( $oldRevision->getPageAsLinkTarget() );
1917 // This method is meant for edit diffs and such so there is no reason to provide a
1918 // revision that's not readable to the user, but check it just in case.
1919 $this->mOldContent = $oldRevision->getContent( SlotRecord::MAIN,
1920 RevisionRecord::FOR_THIS_USER, $this->getUser() );
1921 } else {
1922 $this->mOldPage = null;
1923 $this->mOldRevisionRecord = $this->mOldid = false;
1924 }
1925 $this->mNewRevisionRecord = $newRevision;
1926 $this->mNewid = $newRevision->getId();
1927 $this->mNewPage = Title::newFromLinkTarget( $newRevision->getPageAsLinkTarget() );
1928 $this->mNewContent = $newRevision->getContent( SlotRecord::MAIN,
1929 RevisionRecord::FOR_THIS_USER, $this->getUser() );
1930
1931 $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded = true;
1932 $this->mTextLoaded = $oldRevision ? 2 : 1;
1933 $this->isContentOverridden = false;
1934 $this->slotDiffRenderers = null;
1935 }
1936
1943 public function setTextLanguage( Language $lang ) {
1944 $this->mDiffLang = $lang;
1945 }
1946
1959 public function mapDiffPrevNext( $old, $new ) {
1960 $rl = MediaWikiServices::getInstance()->getRevisionLookup();
1961 if ( $new === 'prev' ) {
1962 // Show diff between revision $old and the previous one. Get previous one from DB.
1963 $newid = intval( $old );
1964 $oldid = false;
1965 $newRev = $rl->getRevisionById( $newid );
1966 if ( $newRev ) {
1967 $oldRev = $rl->getPreviousRevision( $newRev );
1968 if ( $oldRev ) {
1969 $oldid = $oldRev->getId();
1970 }
1971 }
1972 } elseif ( $new === 'next' ) {
1973 // Show diff between revision $old and the next one. Get next one from DB.
1974 $oldid = intval( $old );
1975 $newid = false;
1976 $oldRev = $rl->getRevisionById( $oldid );
1977 if ( $oldRev ) {
1978 $newRev = $rl->getNextRevision( $oldRev );
1979 if ( $newRev ) {
1980 $newid = $newRev->getId();
1981 }
1982 }
1983 } else {
1984 $oldid = intval( $old );
1985 $newid = intval( $new );
1986 }
1987
1988 return [ $oldid, $newid ];
1989 }
1990
1994 private function loadRevisionIds() {
1995 if ( $this->mRevisionsIdsLoaded ) {
1996 return;
1997 }
1998
1999 $this->mRevisionsIdsLoaded = true;
2000
2001 $old = $this->mOldid;
2002 $new = $this->mNewid;
2003
2004 list( $this->mOldid, $this->mNewid ) = self::mapDiffPrevNext( $old, $new );
2005 if ( $new === 'next' && $this->mNewid === false ) {
2006 # if no result, NewId points to the newest old revision. The only newer
2007 # revision is cur, which is "0".
2008 $this->mNewid = 0;
2009 }
2010
2011 $this->hookRunner->onNewDifferenceEngine(
2012 $this->getTitle(), $this->mOldid, $this->mNewid, $old, $new );
2013 }
2014
2028 public function loadRevisionData() {
2029 if ( $this->mRevisionsLoaded ) {
2030 return $this->isContentOverridden ||
2031 ( $this->mOldRevisionRecord !== null && $this->mNewRevisionRecord !== null );
2032 }
2033
2034 // Whether it succeeds or fails, we don't want to try again
2035 $this->mRevisionsLoaded = true;
2036
2037 $this->loadRevisionIds();
2038
2039 // Load the new revision object
2040 if ( $this->mNewid ) {
2041 $this->mNewRevisionRecord = $this->revisionStore->getRevisionById( $this->mNewid );
2042 } else {
2043 $this->mNewRevisionRecord = $this->revisionStore->getRevisionByTitle( $this->getTitle() );
2044 }
2045
2046 if ( !$this->mNewRevisionRecord instanceof RevisionRecord ) {
2047 return false;
2048 }
2049
2050 // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
2051 $this->mNewid = $this->mNewRevisionRecord->getId();
2052 if ( $this->mNewid ) {
2053 $this->mNewPage = Title::newFromLinkTarget(
2054 $this->mNewRevisionRecord->getPageAsLinkTarget()
2055 );
2056 } else {
2057 $this->mNewPage = null;
2058 }
2059
2060 // Load the old revision object
2061 $this->mOldRevisionRecord = false;
2062 if ( $this->mOldid ) {
2063 $this->mOldRevisionRecord = $this->revisionStore->getRevisionById( $this->mOldid );
2064 } elseif ( $this->mOldid === 0 ) {
2065 $revRecord = $this->revisionStore->getPreviousRevision( $this->mNewRevisionRecord );
2066 if ( $revRecord ) {
2067 $this->mOldid = $revRecord->getId();
2068 $this->mOldRevisionRecord = $revRecord;
2069 } else {
2070 // No previous revision; mark to show as first-version only.
2071 $this->mOldid = false;
2072 $this->mOldRevisionRecord = false;
2073 }
2074 } /* elseif ( $this->mOldid === false ) leave mOldRevisionRecord false; */
2075
2076 if ( $this->mOldRevisionRecord === null ) {
2077 return false;
2078 }
2079
2080 if ( $this->mOldRevisionRecord && $this->mOldRevisionRecord->getId() ) {
2081 $this->mOldPage = Title::newFromLinkTarget(
2082 $this->mOldRevisionRecord->getPageAsLinkTarget()
2083 );
2084 } else {
2085 $this->mOldPage = null;
2086 }
2087
2088 // Load tags information for both revisions
2089 $dbr = wfGetDB( DB_REPLICA );
2090 $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
2091 if ( $this->mOldid !== false ) {
2092 $tagIds = $dbr->selectFieldValues(
2093 'change_tag',
2094 'ct_tag_id',
2095 [ 'ct_rev_id' => $this->mOldid ],
2096 __METHOD__
2097 );
2098 $tags = [];
2099 foreach ( $tagIds as $tagId ) {
2100 try {
2101 $tags[] = $changeTagDefStore->getName( (int)$tagId );
2102 } catch ( NameTableAccessException $exception ) {
2103 continue;
2104 }
2105 }
2106 $this->mOldTags = implode( ',', $tags );
2107 } else {
2108 $this->mOldTags = false;
2109 }
2110
2111 $tagIds = $dbr->selectFieldValues(
2112 'change_tag',
2113 'ct_tag_id',
2114 [ 'ct_rev_id' => $this->mNewid ],
2115 __METHOD__
2116 );
2117 $tags = [];
2118 foreach ( $tagIds as $tagId ) {
2119 try {
2120 $tags[] = $changeTagDefStore->getName( (int)$tagId );
2121 } catch ( NameTableAccessException $exception ) {
2122 continue;
2123 }
2124 }
2125 $this->mNewTags = implode( ',', $tags );
2126
2127 return true;
2128 }
2129
2138 public function loadText() {
2139 if ( $this->mTextLoaded == 2 ) {
2140 return $this->loadRevisionData() &&
2141 ( $this->mOldRevisionRecord === false || $this->mOldContent )
2142 && $this->mNewContent;
2143 }
2144
2145 // Whether it succeeds or fails, we don't want to try again
2146 $this->mTextLoaded = 2;
2147
2148 if ( !$this->loadRevisionData() ) {
2149 return false;
2150 }
2151
2152 if ( $this->mOldRevisionRecord ) {
2153 $this->mOldContent = $this->mOldRevisionRecord->getContent(
2154 SlotRecord::MAIN,
2155 RevisionRecord::FOR_THIS_USER,
2156 $this->getUser()
2157 );
2158 if ( $this->mOldContent === null ) {
2159 return false;
2160 }
2161 }
2162
2163 $this->mNewContent = $this->mNewRevisionRecord->getContent(
2164 SlotRecord::MAIN,
2165 RevisionRecord::FOR_THIS_USER,
2166 $this->getUser()
2167 );
2168 $this->hookRunner->onDifferenceEngineLoadTextAfterNewContentIsLoaded( $this );
2169 if ( $this->mNewContent === null ) {
2170 return false;
2171 }
2172
2173 return true;
2174 }
2175
2181 public function loadNewText() {
2182 if ( $this->mTextLoaded >= 1 ) {
2183 return $this->loadRevisionData();
2184 }
2185
2186 $this->mTextLoaded = 1;
2187
2188 if ( !$this->loadRevisionData() ) {
2189 return false;
2190 }
2191
2192 $this->mNewContent = $this->mNewRevisionRecord->getContent(
2193 SlotRecord::MAIN,
2194 RevisionRecord::FOR_THIS_USER,
2195 $this->getUser()
2196 );
2197
2198 $this->hookRunner->onDifferenceEngineAfterLoadNewText( $this );
2199
2200 return true;
2201 }
2202
2203}
getUser()
deprecatePublicProperty( $property, $version, $class=null, $component=null)
Mark a property as deprecated.
trait DeprecationHelper
Use this trait in classes which have properties for which public access is deprecated.
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfMergeErrorArrays(... $args)
Merge arrays in the style of PermissionManager::getPermissionErrors, with duplicate removal e....
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.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
getContext()
static formatSummaryRow( $tags, $page, MessageLocalizer $localizer=null)
Creates HTML for the given tags.
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
IContextSource $context
getWikiPage()
Get the WikiPage object.
setContext(IContextSource $context)
B/C adapter for turning a DifferenceEngine into a SlotDiffRenderer.
DifferenceEngine is responsible for rendering the difference between two revisions as HTML.
getDiffBodyCacheKey()
Returns the cache key for diff body text or content.
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).
bool $mRevisionsIdsLoaded
Have the revisions IDs been loaded.
string[] null $mOldTags
Change tags of old revision or null if it does not exist / is not saved.
setSlotDiffOptions( $options)
hasDeletedRevision()
Checks whether one of the given Revisions was deleted.
IContentHandlerFactory $contentHandlerFactory
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.
getDiffBodyForRole( $role)
Get the diff table body for one slot, without header.
RevisionStore $revisionStore
revisionDeleteLink(RevisionRecord $revRecord)
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.
const DIFF_VERSION
Constant to indicate diff cache compatibility.
mapDiffPrevNext( $old, $new)
Maps a revision pair definition as accepted by DifferenceEngine constructor to a pair of actual integ...
Content null $mNewContent
getDiffBody()
Get the diff table body, without header.
string[] null $mNewTags
Change tags of new revision or null if it does not exist / is not saved.
getParserOutput(WikiPage $page, RevisionRecord $revRecord)
loadRevisionData()
Load revision metadata for the specified revisions.
static getEngine()
Process DiffEngine config and get a sane, usable engine.
loadRevisionIds()
Load revision IDs.
bool $mRevisionsLoaded
Have the revisions been loaded.
Content null $mOldContent
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,...
localiseLineNumbers( $text)
Replace line numbers with the text in the user's language.
getSlotContents()
Get the old and new content objects for all slots.
string $mMarkPatrolledLink
Link to action=markpatrolled.
localiseLineNumbersCb( $matches)
shouldBeHiddenFromUser( $user)
Checks whether the diff should be hidden from the current user This is based on whether the user is a...
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.
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.
HookContainer $hookContainer
getDiff( $otitle, $ntitle, $notice='')
Get complete diff table, including header.
RevisionRecord null $mNewRevisionRecord
New revision (right pane).
getRevisionHeader( $rev, $complete='')
Get a header for a specified revision.
getNewid()
Get the ID of new revision (right pane) of the diff.
renderNewRevision()
Show the new revision of the page.
addLocalisedTitleTooltips( $text)
Add title attributes for tooltips on moved paragraph indicators.
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.
localiseDiff( $text)
Localise diff output.
isUserAllowedToSeeRevisions( $user)
Checks whether the current user has permission for accessing the revisions of the diff.
getPermissionErrors(User $user)
Get the permission errors associated with the revisions for the current diff.
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.
RevisionRecord null false $mOldRevisionRecord
Old revision (left pane).
textDiff( $otext, $ntext)
Generates diff, to be wrapped internally in a logging/instrumentation.
static intermediateEditsMsg( $numEdits, $numUsers, $limit)
Get a notice about how many intermediate edits and users there are.
Title null $mOldPage
Title of old revision or null if the old revision does not exist or does not belong to a page.
SlotDiffRenderer[] $slotDiffRenderers
DifferenceEngine classes for the slots, keyed by role name.
getDiffLang()
Get the language of the difference engine, defaults to page content language.
showDiffStyle()
Add style sheets for diff display.
addLocalisedTitleTooltipsCb(array $matches)
markPatrolledLink()
Build a link to mark a change as patrolled.
$enableDebugComment
Set this to true to add debug info to the HTML output.
hasSuppressedRevision()
Checks whether one of the given Revisions was suppressed.
getOldRevision()
Get the left side of the diff.
userCanEdit(RevisionRecord $revRecord)
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
Definition Language.php:41
static generateRollback( $rev, IContextSource $context=null, $options=[ 'verify'])
Generate a rollback link for a given revision.
Definition Linker.php:1871
static titleAttrib( $name, $options=null, array $msgParams=[])
Given the id of an interface element, constructs the appropriate title attribute from the system mess...
Definition Linker.php:2120
static revComment( $rev, $local=false, $isPublic=false, $useParentheses=true)
Wrap and format the given revision's comment block, if the current user is allowed to view it.
Definition Linker.php:1615
static revUserTools( $rev, $isPublic=false, $useParentheses=true)
Generate a user tool link cluster if the current user is allowed to view it.
Definition Linker.php:1152
static getRevDeleteLink(User $user, $rev, LinkTarget $title)
Get a revision-deletion link, or disabled link, or nothing, depending on user permissions & the setti...
Definition Linker.php:2205
MediaWiki exception.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Class that generates HTML links for pages.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Page revision base class.
getVisibility()
Get the deletion bitfield of the revision.
getContent( $role, $audience=self::FOR_PUBLIC, User $user=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.
Service for looking up page revisions.
Value object representing a content slot associated with a page revision.
Exception representing a failure to look up a row from a name table.
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).
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
Renders a slot diff by doing a text diff on the native representation.
Represents a title within MediaWiki.
Definition Title.php:42
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:60
Class representing a MediaWiki article and history.
Definition WikiPage.php:51
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
getParserOutput(ParserOptions $parserOptions, $oldid=null, $forceParse=false)
Get a ParserOutput for the given ParserOptions and revision ID.
const NS_SPECIAL
Definition Defines.php:59
const CONTENT_MODEL_TEXT
Definition Defines.php:228
Base interface for content objects.
Definition Content.php:35
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.
$cache
Definition mcc.php:33
const DB_REPLICA
Definition defines.php:25
$content
Definition router.php:76
if(!isset( $args[0])) $lang
$header