MediaWiki REL1_33
DifferenceEngine.php
Go to the documentation of this file.
1<?php
28
52
54
61 const DIFF_VERSION = '1.12';
62
69 protected $mOldid;
70
77 protected $mNewid;
78
90 protected $mOldRev;
91
101 protected $mNewRev;
102
108 protected $mOldPage;
109
115 protected $mNewPage;
116
121 private $mOldTags;
122
127 private $mNewTags;
128
135
142
144 protected $mDiffLang;
145
147 private $mRevisionsIdsLoaded = false;
148
150 protected $mRevisionsLoaded = false;
151
153 protected $mTextLoaded = 0;
154
163 protected $isContentOverridden = false;
164
166 protected $mCacheHit = false;
167
173 public $enableDebugComment = false;
174
178 protected $mReducedLineNumbers = false;
179
181 protected $mMarkPatrolledLink = null;
182
184 protected $unhide = false;
185
187 protected $mRefreshCache = false;
188
190 protected $slotDiffRenderers = null;
191
198 protected $isSlotDiffRenderer = false;
199
210 public function __construct( $context = null, $old = 0, $new = 0, $rcid = 0,
211 $refreshCache = false, $unhide = false
212 ) {
213 $this->deprecatePublicProperty( 'mOldid', '1.32', __CLASS__ );
214 $this->deprecatePublicProperty( 'mNewid', '1.32', __CLASS__ );
215 $this->deprecatePublicProperty( 'mOldRev', '1.32', __CLASS__ );
216 $this->deprecatePublicProperty( 'mNewRev', '1.32', __CLASS__ );
217 $this->deprecatePublicProperty( 'mOldPage', '1.32', __CLASS__ );
218 $this->deprecatePublicProperty( 'mNewPage', '1.32', __CLASS__ );
219 $this->deprecatePublicProperty( 'mOldContent', '1.32', __CLASS__ );
220 $this->deprecatePublicProperty( 'mNewContent', '1.32', __CLASS__ );
221 $this->deprecatePublicProperty( 'mRevisionsLoaded', '1.32', __CLASS__ );
222 $this->deprecatePublicProperty( 'mTextLoaded', '1.32', __CLASS__ );
223 $this->deprecatePublicProperty( 'mCacheHit', '1.32', __CLASS__ );
224
225 if ( $context instanceof IContextSource ) {
226 $this->setContext( $context );
227 }
228
229 wfDebug( "DifferenceEngine old '$old' new '$new' rcid '$rcid'\n" );
230
231 $this->mOldid = $old;
232 $this->mNewid = $new;
233 $this->mRefreshCache = $refreshCache;
234 $this->unhide = $unhide;
235 }
236
241 protected function getSlotDiffRenderers() {
242 if ( $this->isSlotDiffRenderer ) {
243 throw new LogicException( __METHOD__ . ' called in slot diff renderer mode' );
244 }
245
246 if ( $this->slotDiffRenderers === null ) {
247 if ( !$this->loadRevisionData() ) {
248 return [];
249 }
250
251 $slotContents = $this->getSlotContents();
252 $this->slotDiffRenderers = array_map( function ( $contents ) {
254 $content = $contents['new'] ?: $contents['old'];
255 return $content->getContentHandler()->getSlotDiffRenderer( $this->getContext() );
256 }, $slotContents );
257 }
259 }
260
267 public function markAsSlotDiffRenderer() {
268 $this->isSlotDiffRenderer = true;
269 }
270
276 protected function getSlotContents() {
277 if ( $this->isContentOverridden ) {
278 return [
279 SlotRecord::MAIN => [
280 'old' => $this->mOldContent,
281 'new' => $this->mNewContent,
282 ]
283 ];
284 } elseif ( !$this->loadRevisionData() ) {
285 return [];
286 }
287
288 $newSlots = $this->mNewRev->getRevisionRecord()->getSlots()->getSlots();
289 if ( $this->mOldRev ) {
290 $oldSlots = $this->mOldRev->getRevisionRecord()->getSlots()->getSlots();
291 } else {
292 $oldSlots = [];
293 }
294 // The order here will determine the visual order of the diff. The current logic is
295 // slots of the new revision first in natural order, then deleted ones. This is ad hoc
296 // and should not be relied on - in the future we may want the ordering to depend
297 // on the page type.
298 $roles = array_merge( array_keys( $newSlots ), array_keys( $oldSlots ) );
299
300 $slots = [];
301 foreach ( $roles as $role ) {
302 $slots[$role] = [
303 'old' => isset( $oldSlots[$role] ) ? $oldSlots[$role]->getContent() : null,
304 'new' => isset( $newSlots[$role] ) ? $newSlots[$role]->getContent() : null,
305 ];
306 }
307 // move main slot to front
308 if ( isset( $slots[SlotRecord::MAIN] ) ) {
309 $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
310 }
311 return $slots;
312 }
313
314 public function getTitle() {
315 // T202454 avoid errors when there is no title
316 return parent::getTitle() ?: Title::makeTitle( NS_SPECIAL, 'BadTitle/DifferenceEngine' );
317 }
318
325 public function setReducedLineNumbers( $value = true ) {
326 $this->mReducedLineNumbers = $value;
327 }
328
334 public function getDiffLang() {
335 if ( $this->mDiffLang === null ) {
336 # Default language in which the diff text is written.
337 $this->mDiffLang = $this->getTitle()->getPageLanguage();
338 }
339
340 return $this->mDiffLang;
341 }
342
346 public function wasCacheHit() {
347 return $this->mCacheHit;
348 }
349
357 public function getOldid() {
358 $this->loadRevisionIds();
359
360 return $this->mOldid;
361 }
362
369 public function getNewid() {
370 $this->loadRevisionIds();
371
372 return $this->mNewid;
373 }
374
381 public function getOldRevision() {
382 return $this->mOldRev ? $this->mOldRev->getRevisionRecord() : null;
383 }
384
390 public function getNewRevision() {
391 return $this->mNewRev ? $this->mNewRev->getRevisionRecord() : null;
392 }
393
402 public function deletedLink( $id ) {
403 if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) {
406 $row = $dbr->selectRow(
407 $arQuery['tables'],
408 array_merge( $arQuery['fields'], [ 'ar_namespace', 'ar_title' ] ),
409 [ 'ar_rev_id' => $id ],
410 __METHOD__,
411 [],
412 $arQuery['joins']
413 );
414 if ( $row ) {
416 $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
417
418 return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( [
419 'target' => $title->getPrefixedText(),
420 'timestamp' => $rev->getTimestamp()
421 ] );
422 }
423 }
424
425 return false;
426 }
427
435 public function deletedIdMarker( $id ) {
436 $link = $this->deletedLink( $id );
437 if ( $link ) {
438 return "[$link $id]";
439 } else {
440 return (string)$id;
441 }
442 }
443
444 private function showMissingRevision() {
445 $out = $this->getOutput();
446
447 $missing = [];
448 if ( $this->mOldRev === null ||
449 ( $this->mOldRev && $this->mOldContent === null )
450 ) {
451 $missing[] = $this->deletedIdMarker( $this->mOldid );
452 }
453 if ( $this->mNewRev === null ||
454 ( $this->mNewRev && $this->mNewContent === null )
455 ) {
456 $missing[] = $this->deletedIdMarker( $this->mNewid );
457 }
458
459 $out->setPageTitle( $this->msg( 'errorpagetitle' ) );
460 $msg = $this->msg( 'difference-missing-revision' )
461 ->params( $this->getLanguage()->listToText( $missing ) )
462 ->numParams( count( $missing ) )
463 ->parseAsBlock();
464 $out->addHTML( $msg );
465 }
466
467 public function showDiffPage( $diffOnly = false ) {
468 # Allow frames except in certain special cases
469 $out = $this->getOutput();
470 $out->allowClickjacking();
471 $out->setRobotPolicy( 'noindex,nofollow' );
472
473 // Allow extensions to add any extra output here
474 Hooks::run( 'DifferenceEngineShowDiffPage', [ $out ] );
475
476 if ( !$this->loadRevisionData() ) {
477 if ( Hooks::run( 'DifferenceEngineShowDiffPageMaybeShowMissingRevision', [ $this ] ) ) {
478 $this->showMissingRevision();
479 }
480 return;
481 }
482
483 $user = $this->getUser();
484 $permErrors = [];
485 if ( $this->mNewPage ) {
486 $permErrors = $this->mNewPage->getUserPermissionsErrors( 'read', $user );
487 }
488 if ( $this->mOldPage ) {
489 $permErrors = wfMergeErrorArrays( $permErrors,
490 $this->mOldPage->getUserPermissionsErrors( 'read', $user ) );
491 }
492 if ( count( $permErrors ) ) {
493 throw new PermissionsError( 'read', $permErrors );
494 }
495
496 $rollback = '';
497
498 $query = [];
499 # Carry over 'diffonly' param via navigation links
500 if ( $diffOnly != $user->getBoolOption( 'diffonly' ) ) {
501 $query['diffonly'] = $diffOnly;
502 }
503 # Cascade unhide param in links for easy deletion browsing
504 if ( $this->unhide ) {
505 $query['unhide'] = 1;
506 }
507
508 # Check if one of the revisions is deleted/suppressed
509 $deleted = $suppressed = false;
510 $allowed = $this->mNewRev->userCan( Revision::DELETED_TEXT, $user );
511
512 $revisionTools = [];
513
514 # mOldRev is false if the difference engine is called with a "vague" query for
515 # a diff between a version V and its previous version V' AND the version V
516 # is the first version of that article. In that case, V' does not exist.
517 if ( $this->mOldRev === false ) {
518 if ( $this->mNewPage ) {
519 $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
520 }
521 $samePage = true;
522 $oldHeader = '';
523 // Allow extensions to change the $oldHeader variable
524 Hooks::run( 'DifferenceEngineOldHeaderNoOldRev', [ &$oldHeader ] );
525 } else {
526 Hooks::run( 'DiffViewHeader', [ $this, $this->mOldRev, $this->mNewRev ] );
527
528 if ( !$this->mOldPage || !$this->mNewPage ) {
529 // XXX say something to the user?
530 $samePage = false;
531 } elseif ( $this->mNewPage->equals( $this->mOldPage ) ) {
532 $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
533 $samePage = true;
534 } else {
535 $out->setPageTitle( $this->msg( 'difference-title-multipage',
536 $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) );
537 $out->addSubtitle( $this->msg( 'difference-multipage' ) );
538 $samePage = false;
539 }
540
541 if ( $samePage && $this->mNewPage && $this->mNewPage->quickUserCan( 'edit', $user ) ) {
542 if ( $this->mNewRev->isCurrent() && $this->mNewPage->userCan( 'rollback', $user ) ) {
543 $rollbackLink = Linker::generateRollback( $this->mNewRev, $this->getContext() );
544 if ( $rollbackLink ) {
545 $out->preventClickjacking();
546 $rollback = "\u{00A0}\u{00A0}\u{00A0}" . $rollbackLink;
547 }
548 }
549
550 if ( !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) &&
551 !$this->mNewRev->isDeleted( Revision::DELETED_TEXT )
552 ) {
553 $undoLink = Html::element( 'a', [
554 'href' => $this->mNewPage->getLocalURL( [
555 'action' => 'edit',
556 'undoafter' => $this->mOldid,
557 'undo' => $this->mNewid
558 ] ),
559 'title' => Linker::titleAttrib( 'undo' ),
560 ],
561 $this->msg( 'editundo' )->text()
562 );
563 $revisionTools['mw-diff-undo'] = $undoLink;
564 }
565 }
566
567 # Make "previous revision link"
568 if ( $samePage && $this->mOldPage && $this->mOldRev->getPrevious() ) {
569 $prevlink = Linker::linkKnown(
570 $this->mOldPage,
571 $this->msg( 'previousdiff' )->escaped(),
572 [ 'id' => 'differences-prevlink' ],
573 [ 'diff' => 'prev', 'oldid' => $this->mOldid ] + $query
574 );
575 } else {
576 $prevlink = "\u{00A0}";
577 }
578
579 if ( $this->mOldRev->isMinor() ) {
580 $oldminor = ChangesList::flag( 'minor' );
581 } else {
582 $oldminor = '';
583 }
584
585 $ldel = $this->revisionDeleteLink( $this->mOldRev );
586 $oldRevisionHeader = $this->getRevisionHeader( $this->mOldRev, 'complete' );
587 $oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff', $this->getContext() );
588
589 $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' .
590 '<div id="mw-diff-otitle2">' .
591 Linker::revUserTools( $this->mOldRev, !$this->unhide ) . '</div>' .
592 '<div id="mw-diff-otitle3">' . $oldminor .
593 Linker::revComment( $this->mOldRev, !$diffOnly, !$this->unhide ) . $ldel . '</div>' .
594 '<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' .
595 '<div id="mw-diff-otitle4">' . $prevlink . '</div>';
596
597 // Allow extensions to change the $oldHeader variable
598 Hooks::run( 'DifferenceEngineOldHeader', [ $this, &$oldHeader, $prevlink, $oldminor,
599 $diffOnly, $ldel, $this->unhide ] );
600
601 if ( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) {
602 $deleted = true; // old revisions text is hidden
603 if ( $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
604 $suppressed = true; // also suppressed
605 }
606 }
607
608 # Check if this user can see the revisions
609 if ( !$this->mOldRev->userCan( Revision::DELETED_TEXT, $user ) ) {
610 $allowed = false;
611 }
612 }
613
614 $out->addJsConfigVars( [
615 'wgDiffOldId' => $this->mOldid,
616 'wgDiffNewId' => $this->mNewid,
617 ] );
618
619 # Make "next revision link"
620 # Skip next link on the top revision
621 if ( $samePage && $this->mNewPage && !$this->mNewRev->isCurrent() ) {
622 $nextlink = Linker::linkKnown(
623 $this->mNewPage,
624 $this->msg( 'nextdiff' )->escaped(),
625 [ 'id' => 'differences-nextlink' ],
626 [ 'diff' => 'next', 'oldid' => $this->mNewid ] + $query
627 );
628 } else {
629 $nextlink = "\u{00A0}";
630 }
631
632 if ( $this->mNewRev->isMinor() ) {
633 $newminor = ChangesList::flag( 'minor' );
634 } else {
635 $newminor = '';
636 }
637
638 # Handle RevisionDelete links...
639 $rdel = $this->revisionDeleteLink( $this->mNewRev );
640
641 # Allow extensions to define their own revision tools
642 Hooks::run( 'DiffRevisionTools',
643 [ $this->mNewRev, &$revisionTools, $this->mOldRev, $user ] );
644 $formattedRevisionTools = [];
645 // Put each one in parentheses (poor man's button)
646 foreach ( $revisionTools as $key => $tool ) {
647 $toolClass = is_string( $key ) ? $key : 'mw-diff-tool';
648 $element = Html::rawElement(
649 'span',
650 [ 'class' => $toolClass ],
651 $this->msg( 'parentheses' )->rawParams( $tool )->escaped()
652 );
653 $formattedRevisionTools[] = $element;
654 }
655 $newRevisionHeader = $this->getRevisionHeader( $this->mNewRev, 'complete' ) .
656 ' ' . implode( ' ', $formattedRevisionTools );
657 $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff', $this->getContext() );
658
659 $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' .
660 '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $this->mNewRev, !$this->unhide ) .
661 " $rollback</div>" .
662 '<div id="mw-diff-ntitle3">' . $newminor .
663 Linker::revComment( $this->mNewRev, !$diffOnly, !$this->unhide ) . $rdel . '</div>' .
664 '<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' .
665 '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
666
667 // Allow extensions to change the $newHeader variable
668 Hooks::run( 'DifferenceEngineNewHeader', [ $this, &$newHeader, $formattedRevisionTools,
669 $nextlink, $rollback, $newminor, $diffOnly, $rdel, $this->unhide ] );
670
671 if ( $this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
672 $deleted = true; // new revisions text is hidden
673 if ( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
674 $suppressed = true; // also suppressed
675 }
676 }
677
678 # If the diff cannot be shown due to a deleted revision, then output
679 # the diff header and links to unhide (if available)...
680 if ( $deleted && ( !$this->unhide || !$allowed ) ) {
681 $this->showDiffStyle();
682 $multi = $this->getMultiNotice();
683 $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
684 if ( !$allowed ) {
685 $msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff';
686 # Give explanation for why revision is not visible
687 $out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
688 [ $msg ] );
689 } else {
690 # Give explanation and add a link to view the diff...
691 $query = $this->getRequest()->appendQueryValue( 'unhide', '1' );
692 $link = $this->getTitle()->getFullURL( $query );
693 $msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff';
694 $out->wrapWikiMsg(
695 "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
696 [ $msg, $link ]
697 );
698 }
699 # Otherwise, output a regular diff...
700 } else {
701 # Add deletion notice if the user is viewing deleted content
702 $notice = '';
703 if ( $deleted ) {
704 $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
705 $notice = "<div id='mw-$msg' class='mw-warning plainlinks'>\n" .
706 $this->msg( $msg )->parse() .
707 "</div>\n";
708 }
709 $this->showDiff( $oldHeader, $newHeader, $notice );
710 if ( !$diffOnly ) {
711 $this->renderNewRevision();
712 }
713 }
714 }
715
725 public function markPatrolledLink() {
726 if ( $this->mMarkPatrolledLink === null ) {
727 $linkInfo = $this->getMarkPatrolledLinkInfo();
728 // If false, there is no patrol link needed/allowed
729 if ( !$linkInfo || !$this->mNewPage ) {
730 $this->mMarkPatrolledLink = '';
731 } else {
732 $this->mMarkPatrolledLink = ' <span class="patrollink" data-mw="interface">[' .
734 $this->mNewPage,
735 $this->msg( 'markaspatrolleddiff' )->escaped(),
736 [],
737 [
738 'action' => 'markpatrolled',
739 'rcid' => $linkInfo['rcid'],
740 ]
741 ) . ']</span>';
742 // Allow extensions to change the markpatrolled link
743 Hooks::run( 'DifferenceEngineMarkPatrolledLink', [ $this,
744 &$this->mMarkPatrolledLink, $linkInfo['rcid'] ] );
745 }
746 }
748 }
749
757 protected function getMarkPatrolledLinkInfo() {
758 $user = $this->getUser();
759 $config = $this->getConfig();
760
761 // Prepare a change patrol link, if applicable
762 if (
763 // Is patrolling enabled and the user allowed to?
764 $config->get( 'UseRCPatrol' ) &&
765 $this->mNewPage && $this->mNewPage->quickUserCan( 'patrol', $user ) &&
766 // Only do this if the revision isn't more than 6 hours older
767 // than the Max RC age (6h because the RC might not be cleaned out regularly)
768 RecentChange::isInRCLifespan( $this->mNewRev->getTimestamp(), 21600 )
769 ) {
770 // Look for an unpatrolled change corresponding to this diff
771 $db = wfGetDB( DB_REPLICA );
772 $change = RecentChange::newFromConds(
773 [
774 'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
775 'rc_this_oldid' => $this->mNewid,
776 'rc_patrolled' => RecentChange::PRC_UNPATROLLED
777 ],
778 __METHOD__
779 );
780
781 if ( $change && !$change->getPerformer()->equals( $user ) ) {
782 $rcid = $change->getAttribute( 'rc_id' );
783 } else {
784 // None found or the page has been created by the current user.
785 // If the user could patrol this it already would be patrolled
786 $rcid = 0;
787 }
788
789 // Allow extensions to possibly change the rcid here
790 // For example the rcid might be set to zero due to the user
791 // being the same as the performer of the change but an extension
792 // might still want to show it under certain conditions
793 Hooks::run( 'DifferenceEngineMarkPatrolledRCID', [ &$rcid, $this, $change, $user ] );
794
795 // Build the link
796 if ( $rcid ) {
797 $this->getOutput()->preventClickjacking();
798 if ( $user->isAllowed( 'writeapi' ) ) {
799 $this->getOutput()->addModules( 'mediawiki.page.patrol.ajax' );
800 }
801
802 return [
803 'rcid' => $rcid,
804 ];
805 }
806 }
807
808 // No mark as patrolled link applicable
809 return false;
810 }
811
817 protected function revisionDeleteLink( $rev ) {
818 $link = Linker::getRevDeleteLink( $this->getUser(), $rev, $rev->getTitle() );
819 if ( $link !== '' ) {
820 $link = "\u{00A0}\u{00A0}\u{00A0}" . $link . ' ';
821 }
822
823 return $link;
824 }
825
831 public function renderNewRevision() {
832 if ( $this->isContentOverridden ) {
833 // The code below only works with a Revision object. We could construct a fake revision
834 // (here or in setContent), but since this does not seem needed at the moment,
835 // we'll just fail for now.
836 throw new LogicException(
837 __METHOD__
838 . ' is not supported after calling setContent(). Use setRevisions() instead.'
839 );
840 }
841
842 $out = $this->getOutput();
843 $revHeader = $this->getRevisionHeader( $this->mNewRev );
844 # Add "current version as of X" title
845 $out->addHTML( "<hr class='diff-hr' id='mw-oldid' />
846 <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
847 # Page content may be handled by a hooked call instead...
848 if ( Hooks::run( 'ArticleContentOnDiff', [ $this, $out ] ) ) {
849 $this->loadNewText();
850 if ( !$this->mNewPage ) {
851 // New revision is unsaved; bail out.
852 // TODO in theory rendering the new revision is a meaningful thing to do
853 // even if it's unsaved, but a lot of untangling is required to do it safely.
854 return;
855 }
856
857 $out->setRevisionId( $this->mNewid );
858 $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() );
859 $out->setArticleFlag( true );
860
861 if ( !Hooks::run( 'ArticleRevisionViewCustom',
862 [ $this->mNewRev->getRevisionRecord(), $this->mNewPage, $this->mOldid, $out ] )
863 ) {
864 // Handled by extension
865 // NOTE: sync with hooks called in Article::view()
866 } elseif ( !Hooks::run( 'ArticleContentViewCustom',
867 [ $this->mNewContent, $this->mNewPage, $out ], '1.32' )
868 ) {
869 // Handled by extension
870 // NOTE: sync with hooks called in Article::view()
871 } else {
872 // Normal page
873 if ( $this->getTitle()->equals( $this->mNewPage ) ) {
874 // If the Title stored in the context is the same as the one
875 // of the new revision, we can use its associated WikiPage
876 // object.
877 $wikiPage = $this->getWikiPage();
878 } else {
879 // Otherwise we need to create our own WikiPage object
880 $wikiPage = WikiPage::factory( $this->mNewPage );
881 }
882
883 $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRev );
884
885 # WikiPage::getParserOutput() should not return false, but just in case
886 if ( $parserOutput ) {
887 // Allow extensions to change parser output here
888 if ( Hooks::run( 'DifferenceEngineRenderRevisionAddParserOutput',
889 [ $this, $out, $parserOutput, $wikiPage ] )
890 ) {
891 $out->addParserOutput( $parserOutput, [
892 'enableSectionEditLinks' => $this->mNewRev->isCurrent()
893 && $this->mNewRev->getTitle()->quickUserCan( 'edit', $this->getUser() ),
894 ] );
895 }
896 }
897 }
898 }
899
900 // Allow extensions to optionally not show the final patrolled link
901 if ( Hooks::run( 'DifferenceEngineRenderRevisionShowFinalPatrolLink' ) ) {
902 # Add redundant patrol link on bottom...
903 $out->addHTML( $this->markPatrolledLink() );
904 }
905 }
906
913 protected function getParserOutput( WikiPage $page, Revision $rev ) {
914 if ( !$rev->getId() ) {
915 // WikiPage::getParserOutput wants a revision ID. Passing 0 will incorrectly show
916 // the current revision, so fail instead. If need be, WikiPage::getParserOutput
917 // could be made to accept a Revision or RevisionRecord instead of the id.
918 return false;
919 }
920
921 $parserOptions = $page->makeParserOptions( $this->getContext() );
922 $parserOutput = $page->getParserOutput( $parserOptions, $rev->getId() );
923
924 return $parserOutput;
925 }
926
937 public function showDiff( $otitle, $ntitle, $notice = '' ) {
938 // Allow extensions to affect the output here
939 Hooks::run( 'DifferenceEngineShowDiff', [ $this ] );
940
941 $diff = $this->getDiff( $otitle, $ntitle, $notice );
942 if ( $diff === false ) {
943 $this->showMissingRevision();
944
945 return false;
946 } else {
947 $this->showDiffStyle();
948 $this->getOutput()->addHTML( $diff );
949
950 return true;
951 }
952 }
953
957 public function showDiffStyle() {
958 if ( !$this->isSlotDiffRenderer ) {
959 $this->getOutput()->addModuleStyles( [
960 'mediawiki.interface.helpers.styles',
961 'mediawiki.diff.styles'
962 ] );
963 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
964 $slotDiffRenderer->addModules( $this->getOutput() );
965 }
966 }
967 }
968
978 public function getDiff( $otitle, $ntitle, $notice = '' ) {
979 $body = $this->getDiffBody();
980 if ( $body === false ) {
981 return false;
982 }
983
984 $multi = $this->getMultiNotice();
985 // Display a message when the diff is empty
986 if ( $body === '' ) {
987 $notice .= '<div class="mw-diff-empty">' .
988 $this->msg( 'diff-empty' )->parse() .
989 "</div>\n";
990 }
991
992 return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
993 }
994
1000 public function getDiffBody() {
1001 $this->mCacheHit = true;
1002 // Check if the diff should be hidden from this user
1003 if ( !$this->isContentOverridden ) {
1004 if ( !$this->loadRevisionData() ) {
1005 return false;
1006 } elseif ( $this->mOldRev &&
1007 !$this->mOldRev->userCan( Revision::DELETED_TEXT, $this->getUser() )
1008 ) {
1009 return false;
1010 } elseif ( $this->mNewRev &&
1011 !$this->mNewRev->userCan( Revision::DELETED_TEXT, $this->getUser() )
1012 ) {
1013 return false;
1014 }
1015 // Short-circuit
1016 if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev &&
1017 $this->mOldRev->getId() && $this->mOldRev->getId() == $this->mNewRev->getId() )
1018 ) {
1019 if ( Hooks::run( 'DifferenceEngineShowEmptyOldContent', [ $this ] ) ) {
1020 return '';
1021 }
1022 }
1023 }
1024
1025 // Cacheable?
1026 $key = false;
1027 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1028 if ( $this->mOldid && $this->mNewid ) {
1029 // Check if subclass is still using the old way
1030 // for backwards-compatibility
1031 $key = $this->getDiffBodyCacheKey();
1032 if ( $key === null ) {
1033 $key = $cache->makeKey( ...$this->getDiffBodyCacheKeyParams() );
1034 }
1035
1036 // Try cache
1037 if ( !$this->mRefreshCache ) {
1038 $difftext = $cache->get( $key );
1039 if ( is_string( $difftext ) ) {
1040 wfIncrStats( 'diff_cache.hit' );
1041 $difftext = $this->localiseDiff( $difftext );
1042 $difftext .= "\n<!-- diff cache key $key -->\n";
1043
1044 return $difftext;
1045 }
1046 } // don't try to load but save the result
1047 }
1048 $this->mCacheHit = false;
1049
1050 // Loadtext is permission safe, this just clears out the diff
1051 if ( !$this->loadText() ) {
1052 return false;
1053 }
1054
1055 $difftext = '';
1056 // We've checked for revdelete at the beginning of this method; it's OK to ignore
1057 // read permissions here.
1058 $slotContents = $this->getSlotContents();
1059 foreach ( $this->getSlotDiffRenderers() as $role => $slotDiffRenderer ) {
1060 $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role]['old'],
1061 $slotContents[$role]['new'] );
1062 if ( $slotDiff && $role !== SlotRecord::MAIN ) {
1063 // FIXME: ask SlotRoleHandler::getSlotNameMessage
1064 $slotTitle = $role;
1065 $difftext .= $this->getSlotHeader( $slotTitle );
1066 }
1067 $difftext .= $slotDiff;
1068 }
1069
1070 // Avoid PHP 7.1 warning from passing $this by reference
1071 $diffEngine = $this;
1072
1073 // Save to cache for 7 days
1074 if ( !Hooks::run( 'AbortDiffCache', [ &$diffEngine ] ) ) {
1075 wfIncrStats( 'diff_cache.uncacheable' );
1076 } elseif ( $key !== false && $difftext !== false ) {
1077 wfIncrStats( 'diff_cache.miss' );
1078 $cache->set( $key, $difftext, 7 * 86400 );
1079 } else {
1080 wfIncrStats( 'diff_cache.uncacheable' );
1081 }
1082 // localise line numbers and title attribute text
1083 if ( $difftext !== false ) {
1084 $difftext = $this->localiseDiff( $difftext );
1085 }
1086
1087 return $difftext;
1088 }
1089
1096 public function getDiffBodyForRole( $role ) {
1097 $diffRenderers = $this->getSlotDiffRenderers();
1098 if ( !isset( $diffRenderers[$role] ) ) {
1099 return false;
1100 }
1101
1102 $slotContents = $this->getSlotContents();
1103 $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role]['old'],
1104 $slotContents[$role]['new'] );
1105 if ( !$slotDiff ) {
1106 return false;
1107 }
1108
1109 if ( $role !== SlotRecord::MAIN ) {
1110 // TODO use human-readable role name at least
1111 $slotTitle = $role;
1112 $slotDiff = $this->getSlotHeader( $slotTitle ) . $slotDiff;
1113 }
1114
1115 return $this->localiseDiff( $slotDiff );
1116 }
1117
1125 protected function getSlotHeader( $headerText ) {
1126 // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
1127 $columnCount = $this->mOldRev ? 4 : 2;
1128 $userLang = $this->getLanguage()->getHtmlCode();
1129 return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-header', 'lang' => $userLang ],
1130 Html::element( 'th', [ 'colspan' => $columnCount ], $headerText ) );
1131 }
1132
1142 protected function getDiffBodyCacheKey() {
1143 return null;
1144 }
1145
1159 protected function getDiffBodyCacheKeyParams() {
1160 if ( !$this->mOldid || !$this->mNewid ) {
1161 throw new MWException( 'mOldid and mNewid must be set to get diff cache key.' );
1162 }
1163
1164 $engine = $this->getEngine();
1165 $params = [
1166 'diff',
1167 $engine,
1169 "old-{$this->mOldid}",
1170 "rev-{$this->mNewid}"
1171 ];
1172
1173 if ( $engine === 'wikidiff2' ) {
1174 $params[] = phpversion( 'wikidiff2' );
1175 $params[] = $this->getConfig()->get( 'WikiDiff2MovedParagraphDetectionCutoff' );
1176 }
1177
1178 if ( !$this->isSlotDiffRenderer ) {
1179 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1180 $params = array_merge( $params, $slotDiffRenderer->getExtraCacheKeys() );
1181 }
1182 }
1183
1184 return $params;
1185 }
1186
1194 public function getExtraCacheKeys() {
1195 // This method is called when the DifferenceEngine is used for a slot diff. We only care
1196 // about special things, not the revision IDs, which are added to the cache key by the
1197 // page-level DifferenceEngine, and which might not have a valid value for this object.
1198 $this->mOldid = 123456789;
1199 $this->mNewid = 987654321;
1200
1201 // This will repeat a bunch of unnecessary key fields for each slot. Not nice but harmless.
1202 $cacheString = $this->getDiffBodyCacheKey();
1203 if ( $cacheString ) {
1204 return [ $cacheString ];
1205 }
1206
1208
1209 // Try to get rid of the standard keys to keep the cache key human-readable:
1210 // call the getDiffBodyCacheKeyParams implementation of the base class, and if
1211 // the child class includes the same keys, drop them.
1212 // Uses an obscure PHP feature where static calls to non-static methods are allowed
1213 // as long as we are already in a non-static method of the same class, and the call context
1214 // ($this) will be inherited.
1215 // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
1217 if ( array_slice( $params, 0, count( $standardParams ) ) === $standardParams ) {
1218 $params = array_slice( $params, count( $standardParams ) );
1219 }
1220
1221 return $params;
1222 }
1223
1237 public function generateContentDiffBody( Content $old, Content $new ) {
1238 $slotDiffRenderer = $new->getContentHandler()->getSlotDiffRenderer( $this->getContext() );
1239 if (
1240 $slotDiffRenderer instanceof DifferenceEngineSlotDiffRenderer
1241 && $this->isSlotDiffRenderer
1242 ) {
1243 // Oops, we are just about to enter an infinite loop (the slot-level DifferenceEngine
1244 // called a DifferenceEngineSlotDiffRenderer that wraps the same DifferenceEngine class).
1245 // This will happen when a content model has no custom slot diff renderer, it does have
1246 // a custom difference engine, but that does not override this method.
1247 throw new Exception( get_class( $this ) . ': could not maintain backwards compatibility. '
1248 . 'Please use a SlotDiffRenderer.' );
1249 }
1250 return $slotDiffRenderer->getDiff( $old, $new ) . $this->getDebugString();
1251 }
1252
1265 public function generateTextDiffBody( $otext, $ntext ) {
1266 $slotDiffRenderer = ContentHandler::getForModelID( CONTENT_MODEL_TEXT )
1267 ->getSlotDiffRenderer( $this->getContext() );
1268 if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1269 // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1270 // This is too unlikely to happen to bother handling properly.
1271 throw new Exception( 'The slot diff renderer for text content should be a '
1272 . 'TextSlotDiffRenderer subclass' );
1273 }
1274 return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1275 }
1276
1283 public static function getEngine() {
1284 global $wgExternalDiffEngine;
1285 // We use the global here instead of Config because we write to the value,
1286 // and Config is not mutable.
1287 if ( $wgExternalDiffEngine == 'wikidiff' || $wgExternalDiffEngine == 'wikidiff3' ) {
1288 wfDeprecated( "\$wgExternalDiffEngine = '{$wgExternalDiffEngine}'", '1.27' );
1289 $wgExternalDiffEngine = false;
1290 } elseif ( $wgExternalDiffEngine == 'wikidiff2' ) {
1291 wfDeprecated( "\$wgExternalDiffEngine = '{$wgExternalDiffEngine}'", '1.32' );
1292 $wgExternalDiffEngine = false;
1293 } elseif ( !is_string( $wgExternalDiffEngine ) && $wgExternalDiffEngine !== false ) {
1294 // And prevent people from shooting themselves in the foot...
1295 wfWarn( '$wgExternalDiffEngine is set to a non-string value, forcing it to false' );
1296 $wgExternalDiffEngine = false;
1297 }
1298
1299 if ( is_string( $wgExternalDiffEngine ) && is_executable( $wgExternalDiffEngine ) ) {
1300 return $wgExternalDiffEngine;
1301 } elseif ( $wgExternalDiffEngine === false && function_exists( 'wikidiff2_do_diff' ) ) {
1302 return 'wikidiff2';
1303 } else {
1304 // Native PHP
1305 return false;
1306 }
1307 }
1308
1321 protected function textDiff( $otext, $ntext ) {
1322 $slotDiffRenderer = ContentHandler::getForModelID( CONTENT_MODEL_TEXT )
1323 ->getSlotDiffRenderer( $this->getContext() );
1324 if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1325 // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1326 // This is too unlikely to happen to bother handling properly.
1327 throw new Exception( 'The slot diff renderer for text content should be a '
1328 . 'TextSlotDiffRenderer subclass' );
1329 }
1330 return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1331 }
1332
1341 protected function debug( $generator = "internal" ) {
1342 if ( !$this->enableDebugComment ) {
1343 return '';
1344 }
1345 $data = [ $generator ];
1346 if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
1347 $data[] = wfHostname();
1348 }
1349 $data[] = wfTimestamp( TS_DB );
1350
1351 return "<!-- diff generator: " .
1352 implode( " ", array_map( "htmlspecialchars", $data ) ) .
1353 " -->\n";
1354 }
1355
1356 private function getDebugString() {
1358 if ( $engine === 'wikidiff2' ) {
1359 return $this->debug( 'wikidiff2' );
1360 } elseif ( $engine === false ) {
1361 return $this->debug( 'native PHP' );
1362 } else {
1363 return $this->debug( "external $engine" );
1364 }
1365 }
1366
1373 private function localiseDiff( $text ) {
1374 $text = $this->localiseLineNumbers( $text );
1375 if ( $this->getEngine() === 'wikidiff2' &&
1376 version_compare( phpversion( 'wikidiff2' ), '1.5.1', '>=' )
1377 ) {
1378 $text = $this->addLocalisedTitleTooltips( $text );
1379 }
1380 return $text;
1381 }
1382
1390 public function localiseLineNumbers( $text ) {
1391 return preg_replace_callback(
1392 '/<!--LINE (\d+)-->/',
1393 [ $this, 'localiseLineNumbersCb' ],
1394 $text
1395 );
1396 }
1397
1398 public function localiseLineNumbersCb( $matches ) {
1399 if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
1400 return '';
1401 }
1402
1403 return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
1404 }
1405
1412 private function addLocalisedTitleTooltips( $text ) {
1413 return preg_replace_callback(
1414 '/class="mw-diff-movedpara-(left|right)"/',
1415 [ $this, 'addLocalisedTitleTooltipsCb' ],
1416 $text
1417 );
1418 }
1419
1425 $key = $matches[1] === 'right' ?
1426 'diff-paragraph-moved-toold' :
1427 'diff-paragraph-moved-tonew';
1428 return $matches[0] . ' title="' . $this->msg( $key )->escaped() . '"';
1429 }
1430
1436 public function getMultiNotice() {
1437 // The notice only make sense if we are diffing two saved revisions of the same page.
1438 if (
1439 !$this->mOldRev || !$this->mNewRev
1440 || !$this->mOldPage || !$this->mNewPage
1441 || !$this->mOldPage->equals( $this->mNewPage )
1442 ) {
1443 return '';
1444 }
1445
1446 if ( $this->mOldRev->getTimestamp() > $this->mNewRev->getTimestamp() ) {
1447 $oldRev = $this->mNewRev; // flip
1448 $newRev = $this->mOldRev; // flip
1449 } else { // normal case
1450 $oldRev = $this->mOldRev;
1452 }
1453
1454 // Sanity: don't show the notice if too many rows must be scanned
1455 // @todo show some special message for that case
1456 $nEdits = $this->mNewPage->countRevisionsBetween( $oldRev, $newRev, 1000 );
1457 if ( $nEdits > 0 && $nEdits <= 1000 ) {
1458 $limit = 100; // use diff-multi-manyusers if too many users
1459 $users = $this->mNewPage->getAuthorsBetween( $oldRev, $newRev, $limit );
1460 $numUsers = count( $users );
1461
1462 if ( $numUsers == 1 && $users[0] == $newRev->getUserText( Revision::RAW ) ) {
1463 $numUsers = 0; // special case to say "by the same user" instead of "by one other user"
1464 }
1465
1466 return self::intermediateEditsMsg( $nEdits, $numUsers, $limit );
1467 }
1468
1469 return '';
1470 }
1471
1481 public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) {
1482 if ( $numUsers === 0 ) {
1483 $msg = 'diff-multi-sameuser';
1484 } elseif ( $numUsers > $limit ) {
1485 $msg = 'diff-multi-manyusers';
1486 $numUsers = $limit;
1487 } else {
1488 $msg = 'diff-multi-otherusers';
1489 }
1490
1491 return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1492 }
1493
1503 public function getRevisionHeader( Revision $rev, $complete = '' ) {
1504 $lang = $this->getLanguage();
1505 $user = $this->getUser();
1506 $revtimestamp = $rev->getTimestamp();
1507 $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
1508 $dateofrev = $lang->userDate( $revtimestamp, $user );
1509 $timeofrev = $lang->userTime( $revtimestamp, $user );
1510
1511 $header = $this->msg(
1512 $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
1513 $timestamp,
1514 $dateofrev,
1515 $timeofrev
1516 )->escaped();
1517
1518 if ( $complete !== 'complete' ) {
1519 return $header;
1520 }
1521
1522 $title = $rev->getTitle();
1523
1525 [ 'oldid' => $rev->getId() ] );
1526
1527 if ( $rev->userCan( Revision::DELETED_TEXT, $user ) ) {
1528 $editQuery = [ 'action' => 'edit' ];
1529 if ( !$rev->isCurrent() ) {
1530 $editQuery['oldid'] = $rev->getId();
1531 }
1532
1533 $key = $title->quickUserCan( 'edit', $user ) ? 'editold' : 'viewsourceold';
1534 $msg = $this->msg( $key )->escaped();
1535 $editLink = $this->msg( 'parentheses' )->rawParams(
1536 Linker::linkKnown( $title, $msg, [], $editQuery ) )->escaped();
1537 $header .= ' ' . Html::rawElement(
1538 'span',
1539 [ 'class' => 'mw-diff-edit' ],
1540 $editLink
1541 );
1542 if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
1543 $header = Html::rawElement(
1544 'span',
1545 [ 'class' => 'history-deleted' ],
1546 $header
1547 );
1548 }
1549 } else {
1550 $header = Html::rawElement( 'span', [ 'class' => 'history-deleted' ], $header );
1551 }
1552
1553 return $header;
1554 }
1555
1568 public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
1569 // shared.css sets diff in interface language/dir, but the actual content
1570 // is often in a different language, mostly the page content language/dir
1571 $header = Html::openElement( 'table', [
1572 'class' => [ 'diff', 'diff-contentalign-' . $this->getDiffLang()->alignStart() ],
1573 'data-mw' => 'interface',
1574 ] );
1575 $userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
1576
1577 if ( !$diff && !$otitle ) {
1578 $header .= "
1579 <tr class=\"diff-title\" lang=\"{$userLang}\">
1580 <td class=\"diff-ntitle\">{$ntitle}</td>
1581 </tr>";
1582 $multiColspan = 1;
1583 } else {
1584 if ( $diff ) { // Safari/Chrome show broken output if cols not used
1585 $header .= "
1586 <col class=\"diff-marker\" />
1587 <col class=\"diff-content\" />
1588 <col class=\"diff-marker\" />
1589 <col class=\"diff-content\" />";
1590 $colspan = 2;
1591 $multiColspan = 4;
1592 } else {
1593 $colspan = 1;
1594 $multiColspan = 2;
1595 }
1596 if ( $otitle || $ntitle ) {
1597 $header .= "
1598 <tr class=\"diff-title\" lang=\"{$userLang}\">
1599 <td colspan=\"$colspan\" class=\"diff-otitle\">{$otitle}</td>
1600 <td colspan=\"$colspan\" class=\"diff-ntitle\">{$ntitle}</td>
1601 </tr>";
1602 }
1603 }
1604
1605 if ( $multi != '' ) {
1606 $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1607 "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
1608 }
1609 if ( $notice != '' ) {
1610 $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1611 "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
1612 }
1613
1614 return $header . $diff . "</table>";
1615 }
1616
1624 public function setContent( Content $oldContent, Content $newContent ) {
1625 $this->mOldContent = $oldContent;
1626 $this->mNewContent = $newContent;
1627
1628 $this->mTextLoaded = 2;
1629 $this->mRevisionsLoaded = true;
1630 $this->isContentOverridden = true;
1631 $this->slotDiffRenderers = null;
1632 }
1633
1639 public function setRevisions(
1640 RevisionRecord $oldRevision = null, RevisionRecord $newRevision
1641 ) {
1642 if ( $oldRevision ) {
1643 $this->mOldRev = new Revision( $oldRevision );
1644 $this->mOldid = $oldRevision->getId();
1645 $this->mOldPage = Title::newFromLinkTarget( $oldRevision->getPageAsLinkTarget() );
1646 // This method is meant for edit diffs and such so there is no reason to provide a
1647 // revision that's not readable to the user, but check it just in case.
1648 $this->mOldContent = $oldRevision->getContent( SlotRecord::MAIN,
1649 RevisionRecord::FOR_THIS_USER, $this->getUser() );
1650 } else {
1651 $this->mOldPage = null;
1652 $this->mOldRev = $this->mOldid = false;
1653 }
1654 $this->mNewRev = new Revision( $newRevision );
1655 $this->mNewid = $newRevision->getId();
1656 $this->mNewPage = Title::newFromLinkTarget( $newRevision->getPageAsLinkTarget() );
1657 $this->mNewContent = $newRevision->getContent( SlotRecord::MAIN,
1658 RevisionRecord::FOR_THIS_USER, $this->getUser() );
1659
1660 $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded = true;
1661 $this->mTextLoaded = $oldRevision ? 2 : 1;
1662 $this->isContentOverridden = false;
1663 $this->slotDiffRenderers = null;
1664 }
1665
1672 public function setTextLanguage( Language $lang ) {
1673 $this->mDiffLang = $lang;
1674 }
1675
1687 public function mapDiffPrevNext( $old, $new ) {
1688 if ( $new === 'prev' ) {
1689 // Show diff between revision $old and the previous one. Get previous one from DB.
1690 $newid = intval( $old );
1691 $oldid = $this->getTitle()->getPreviousRevisionID( $newid );
1692 } elseif ( $new === 'next' ) {
1693 // Show diff between revision $old and the next one. Get next one from DB.
1694 $oldid = intval( $old );
1695 $newid = $this->getTitle()->getNextRevisionID( $oldid );
1696 } else {
1697 $oldid = intval( $old );
1698 $newid = intval( $new );
1699 }
1700
1701 return [ $oldid, $newid ];
1702 }
1703
1707 private function loadRevisionIds() {
1708 if ( $this->mRevisionsIdsLoaded ) {
1709 return;
1710 }
1711
1712 $this->mRevisionsIdsLoaded = true;
1713
1714 $old = $this->mOldid;
1715 $new = $this->mNewid;
1716
1717 list( $this->mOldid, $this->mNewid ) = self::mapDiffPrevNext( $old, $new );
1718 if ( $new === 'next' && $this->mNewid === false ) {
1719 # if no result, NewId points to the newest old revision. The only newer
1720 # revision is cur, which is "0".
1721 $this->mNewid = 0;
1722 }
1723
1724 Hooks::run(
1725 'NewDifferenceEngine',
1726 [ $this->getTitle(), &$this->mOldid, &$this->mNewid, $old, $new ]
1727 );
1728 }
1729
1743 public function loadRevisionData() {
1744 if ( $this->mRevisionsLoaded ) {
1745 return $this->isContentOverridden || $this->mNewRev && !is_null( $this->mOldRev );
1746 }
1747
1748 // Whether it succeeds or fails, we don't want to try again
1749 $this->mRevisionsLoaded = true;
1750
1751 $this->loadRevisionIds();
1752
1753 // Load the new revision object
1754 if ( $this->mNewid ) {
1755 $this->mNewRev = Revision::newFromId( $this->mNewid );
1756 } else {
1757 $this->mNewRev = Revision::newFromTitle(
1758 $this->getTitle(),
1759 false,
1760 Revision::READ_NORMAL
1761 );
1762 }
1763
1764 if ( !$this->mNewRev instanceof Revision ) {
1765 return false;
1766 }
1767
1768 // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
1769 $this->mNewid = $this->mNewRev->getId();
1770 if ( $this->mNewid ) {
1771 $this->mNewPage = $this->mNewRev->getTitle();
1772 } else {
1773 $this->mNewPage = null;
1774 }
1775
1776 // Load the old revision object
1777 $this->mOldRev = false;
1778 if ( $this->mOldid ) {
1779 $this->mOldRev = Revision::newFromId( $this->mOldid );
1780 } elseif ( $this->mOldid === 0 ) {
1781 $rev = $this->mNewRev->getPrevious();
1782 if ( $rev ) {
1783 $this->mOldid = $rev->getId();
1784 $this->mOldRev = $rev;
1785 } else {
1786 // No previous revision; mark to show as first-version only.
1787 $this->mOldid = false;
1788 $this->mOldRev = false;
1789 }
1790 } /* elseif ( $this->mOldid === false ) leave mOldRev false; */
1791
1792 if ( is_null( $this->mOldRev ) ) {
1793 return false;
1794 }
1795
1796 if ( $this->mOldRev && $this->mOldRev->getId() ) {
1797 $this->mOldPage = $this->mOldRev->getTitle();
1798 } else {
1799 $this->mOldPage = null;
1800 }
1801
1802 // Load tags information for both revisions
1803 $dbr = wfGetDB( DB_REPLICA );
1804 $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
1805 if ( $this->mOldid !== false ) {
1806 $tagIds = $dbr->selectFieldValues(
1807 'change_tag',
1808 'ct_tag_id',
1809 [ 'ct_rev_id' => $this->mOldid ],
1810 __METHOD__
1811 );
1812 $tags = [];
1813 foreach ( $tagIds as $tagId ) {
1814 try {
1815 $tags[] = $changeTagDefStore->getName( (int)$tagId );
1816 } catch ( NameTableAccessException $exception ) {
1817 continue;
1818 }
1819 }
1820 $this->mOldTags = implode( ',', $tags );
1821 } else {
1822 $this->mOldTags = false;
1823 }
1824
1825 $tagIds = $dbr->selectFieldValues(
1826 'change_tag',
1827 'ct_tag_id',
1828 [ 'ct_rev_id' => $this->mNewid ],
1829 __METHOD__
1830 );
1831 $tags = [];
1832 foreach ( $tagIds as $tagId ) {
1833 try {
1834 $tags[] = $changeTagDefStore->getName( (int)$tagId );
1835 } catch ( NameTableAccessException $exception ) {
1836 continue;
1837 }
1838 }
1839 $this->mNewTags = implode( ',', $tags );
1840
1841 return true;
1842 }
1843
1852 public function loadText() {
1853 if ( $this->mTextLoaded == 2 ) {
1854 return $this->loadRevisionData() && ( $this->mOldRev === false || $this->mOldContent )
1855 && $this->mNewContent;
1856 }
1857
1858 // Whether it succeeds or fails, we don't want to try again
1859 $this->mTextLoaded = 2;
1860
1861 if ( !$this->loadRevisionData() ) {
1862 return false;
1863 }
1864
1865 if ( $this->mOldRev ) {
1866 $this->mOldContent = $this->mOldRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
1867 if ( $this->mOldContent === null ) {
1868 return false;
1869 }
1870 }
1871
1872 $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
1873 Hooks::run( 'DifferenceEngineLoadTextAfterNewContentIsLoaded', [ $this ] );
1874 if ( $this->mNewContent === null ) {
1875 return false;
1876 }
1877
1878 return true;
1879 }
1880
1886 public function loadNewText() {
1887 if ( $this->mTextLoaded >= 1 ) {
1888 return $this->loadRevisionData();
1889 }
1890
1891 $this->mTextLoaded = 1;
1892
1893 if ( !$this->loadRevisionData() ) {
1894 return false;
1895 }
1896
1897 $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
1898
1899 Hooks::run( 'DifferenceEngineAfterLoadNewText', [ $this ] );
1900
1901 return true;
1902 }
1903
1904}
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
$wgExternalDiffEngine
Name of the external diff engine to use.
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.
wfIncrStats( $key, $count=1)
Increment a statistics counter.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfMergeErrorArrays(... $args)
Merge arrays in the style of getUserPermissionsErrors, with duplicate removal e.g.
wfHostname()
Fetch server name for use in error reporting etc.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
static formatSummaryRow( $tags, $page, IContextSource $context=null)
Creates HTML for the given tags.
static flag( $flag, IContextSource $context=null)
Make an "<abbr>" element for a given change flag.
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
msg( $key)
Get a Message object with context set Parameters are the same as wfMessage()
IContextSource $context
getWikiPage()
Get the WikiPage object.
getContext()
Get the base IContextSource 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....
Revision null $mNewRev
New revision (right pane).
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 $mOldRev or null if it does not exist / is not saved.
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.
getOldid()
Get the ID of old revision (left pane) of the diff.
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
getParserOutput(WikiPage $page, Revision $rev)
getDiffBody()
Get the diff table body, without header.
string[] null $mNewTags
Change tags of $mNewRev or null if it does not exist / is not saved.
loadRevisionData()
Load revision metadata for the specified revisions.
static getEngine()
Process $wgExternalDiffEngine 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)
setRevisions(RevisionRecord $oldRevision=null, RevisionRecord $newRevision)
Use specified text instead of loading from the database.
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 ...
getRevisionHeader(Revision $rev, $complete='')
Get a header for a specified revision.
__construct( $context=null, $old=0, $new=0, $rcid=0, $refreshCache=false, $unhide=false)
#-
Title null $mNewPage
Title of $mNewRev 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.
Revision null false $mOldRev
Old revision (left pane).
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.
getDiffBodyCacheKeyParams()
Get the cache key parameters.
getDiff( $otitle, $ntitle, $notice='')
Get complete diff table, including header.
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.
getMarkPatrolledLinkInfo()
Returns an array of meta data needed to build a "mark as patrolled" link and adds the mediawiki....
setReducedLineNumbers( $value=true)
Set reduced line numbers mode.
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 $mOldRev 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.
getOldRevision()
Get the left side of the diff.
Internationalisation code.
Definition Language.php:36
static generateRollback( $rev, IContextSource $context=null, $options=[ 'verify'])
Generate a rollback link for a given revision.
Definition Linker.php:1750
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:1965
static linkKnown( $target, $html=null, $customAttribs=[], $query=[], $options=[ 'known'])
Identical to link(), except $options defaults to 'known'.
Definition Linker.php:146
static revComment(Revision $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:1510
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:1086
static getRevDeleteLink(User $user, Revision $rev, Title $title)
Get a revision-deletion link, or disabled link, or nothing, depending on user permissions & the setti...
Definition Linker.php:2049
MediaWiki exception.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Page revision base class.
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.
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.
static getArchiveQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new archived revision objec...
Definition Revision.php:525
static newFromArchiveRow( $row, $overrides=[])
Make a fake revision object from an archive table row.
Definition Revision.php:171
const DELETED_TEXT
Definition Revision.php:46
const DELETED_RESTRICTED
Definition Revision.php:49
static newFromTitle(LinkTarget $linkTarget, $id=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given link target.
Definition Revision.php:137
const RAW
Definition Revision.php:56
const FOR_THIS_USER
Definition Revision.php:55
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition Revision.php:118
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.
Represents a title within MediaWiki.
Definition Title.php:40
Class representing a MediaWiki article and history.
Definition WikiPage.php:45
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.
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
$data
Utility to generate mapping file used in mw.Title (phpCharToUpper.json)
const NS_SPECIAL
Definition Defines.php:62
const CONTENT_MODEL_TEXT
Definition Defines.php:247
null for the local wiki Added should default to null in handler for backwards compatibility add a value to it if you want to add a cookie that have to vary cache options can modify as strings Extensions should add to this list prev or next $refreshCache
Definition hooks.txt:1628
also included in $newHeader $rollback
Definition hooks.txt:1269
passed in as a query string parameter to the various URLs constructed here(i.e. $nextlink) $rdel also included in $oldHeader $oldminor
Definition hooks.txt:1273
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output $out
Definition hooks.txt:855
returning false will NOT prevent logging a wrapping ErrorException $suppressed
Definition hooks.txt:2188
namespace and then decline to actually register it file or subcat img or subcat $title
Definition hooks.txt:955
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation use $formDescriptor instead default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt;div ...>$1&lt;/div>"). - flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException':Called before an exception(or PHP error) is logged. This is meant for integration with external error aggregation services
usually copyright or history_copyright This message must be in HTML not wikitext & $link
Definition hooks.txt:3069
the value to return A Title object or null for latest all implement SearchIndexField $engine
Definition hooks.txt:2925
also included in $newHeader if any $newminor
Definition hooks.txt:1271
null for the local wiki Added should default to null in handler for backwards compatibility add a value to it if you want to add a cookie that have to vary cache options can modify $query
Definition hooks.txt:1617
return true to allow those checks to and false if checking is done & $user
Definition hooks.txt:1510
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition hooks.txt:1779
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition injection.txt:37
Base interface for content objects.
Definition Content.php:34
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
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
$content
$newRev
const DB_REPLICA
Definition defines.php:25
$params
if(!isset( $args[0])) $lang
$header