MediaWiki REL1_32
DifferenceEngine.php
Go to the documentation of this file.
1<?php
26
50
52
59 const DIFF_VERSION = '1.12';
60
67 protected $mOldid;
68
75 protected $mNewid;
76
88 protected $mOldRev;
89
99 protected $mNewRev;
100
106 protected $mOldPage;
107
113 protected $mNewPage;
114
119 private $mOldTags;
120
125 private $mNewTags;
126
133
140
142 protected $mDiffLang;
143
145 private $mRevisionsIdsLoaded = false;
146
148 protected $mRevisionsLoaded = false;
149
151 protected $mTextLoaded = 0;
152
161 protected $isContentOverridden = false;
162
164 protected $mCacheHit = false;
165
171 public $enableDebugComment = false;
172
176 protected $mReducedLineNumbers = false;
177
179 protected $mMarkPatrolledLink = null;
180
182 protected $unhide = false;
183
185 protected $mRefreshCache = false;
186
188 protected $slotDiffRenderers = null;
189
196 protected $isSlotDiffRenderer = false;
197
208 public function __construct( $context = null, $old = 0, $new = 0, $rcid = 0,
209 $refreshCache = false, $unhide = false
210 ) {
211 $this->deprecatePublicProperty( 'mOldid', '1.32', __CLASS__ );
212 $this->deprecatePublicProperty( 'mNewid', '1.32', __CLASS__ );
213 $this->deprecatePublicProperty( 'mOldRev', '1.32', __CLASS__ );
214 $this->deprecatePublicProperty( 'mNewRev', '1.32', __CLASS__ );
215 $this->deprecatePublicProperty( 'mOldPage', '1.32', __CLASS__ );
216 $this->deprecatePublicProperty( 'mNewPage', '1.32', __CLASS__ );
217 $this->deprecatePublicProperty( 'mOldContent', '1.32', __CLASS__ );
218 $this->deprecatePublicProperty( 'mNewContent', '1.32', __CLASS__ );
219 $this->deprecatePublicProperty( 'mRevisionsLoaded', '1.32', __CLASS__ );
220 $this->deprecatePublicProperty( 'mTextLoaded', '1.32', __CLASS__ );
221 $this->deprecatePublicProperty( 'mCacheHit', '1.32', __CLASS__ );
222
223 if ( $context instanceof IContextSource ) {
224 $this->setContext( $context );
225 }
226
227 wfDebug( "DifferenceEngine old '$old' new '$new' rcid '$rcid'\n" );
228
229 $this->mOldid = $old;
230 $this->mNewid = $new;
231 $this->mRefreshCache = $refreshCache;
232 $this->unhide = $unhide;
233 }
234
239 protected function getSlotDiffRenderers() {
240 if ( $this->isSlotDiffRenderer ) {
241 throw new LogicException( __METHOD__ . ' called in slot diff renderer mode' );
242 }
243
244 if ( $this->slotDiffRenderers === null ) {
245 if ( !$this->loadRevisionData() ) {
246 return [];
247 }
248
249 $slotContents = $this->getSlotContents();
250 $this->slotDiffRenderers = array_map( function ( $contents ) {
252 $content = $contents['new'] ?: $contents['old'];
253 return $content->getContentHandler()->getSlotDiffRenderer( $this->getContext() );
254 }, $slotContents );
255 }
257 }
258
265 public function markAsSlotDiffRenderer() {
266 $this->isSlotDiffRenderer = true;
267 }
268
274 protected function getSlotContents() {
275 if ( $this->isContentOverridden ) {
276 return [
277 SlotRecord::MAIN => [
278 'old' => $this->mOldContent,
279 'new' => $this->mNewContent,
280 ]
281 ];
282 } elseif ( !$this->loadRevisionData() ) {
283 return [];
284 }
285
286 $newSlots = $this->mNewRev->getRevisionRecord()->getSlots()->getSlots();
287 if ( $this->mOldRev ) {
288 $oldSlots = $this->mOldRev->getRevisionRecord()->getSlots()->getSlots();
289 } else {
290 $oldSlots = [];
291 }
292 // The order here will determine the visual order of the diff. The current logic is
293 // slots of the new revision first in natural order, then deleted ones. This is ad hoc
294 // and should not be relied on - in the future we may want the ordering to depend
295 // on the page type.
296 $roles = array_merge( array_keys( $newSlots ), array_keys( $oldSlots ) );
297
298 $slots = [];
299 foreach ( $roles as $role ) {
300 $slots[$role] = [
301 'old' => isset( $oldSlots[$role] ) ? $oldSlots[$role]->getContent() : null,
302 'new' => isset( $newSlots[$role] ) ? $newSlots[$role]->getContent() : null,
303 ];
304 }
305 // move main slot to front
306 if ( isset( $slots[SlotRecord::MAIN] ) ) {
307 $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
308 }
309 return $slots;
310 }
311
312 public function getTitle() {
313 // T202454 avoid errors when there is no title
314 return parent::getTitle() ?: Title::makeTitle( NS_SPECIAL, 'BadTitle/DifferenceEngine' );
315 }
316
323 public function setReducedLineNumbers( $value = true ) {
324 $this->mReducedLineNumbers = $value;
325 }
326
332 public function getDiffLang() {
333 if ( $this->mDiffLang === null ) {
334 # Default language in which the diff text is written.
335 $this->mDiffLang = $this->getTitle()->getPageLanguage();
336 }
337
338 return $this->mDiffLang;
339 }
340
344 public function wasCacheHit() {
345 return $this->mCacheHit;
346 }
347
355 public function getOldid() {
356 $this->loadRevisionIds();
357
358 return $this->mOldid;
359 }
360
367 public function getNewid() {
368 $this->loadRevisionIds();
369
370 return $this->mNewid;
371 }
372
379 public function getOldRevision() {
380 return $this->mOldRev ? $this->mOldRev->getRevisionRecord() : null;
381 }
382
388 public function getNewRevision() {
389 return $this->mNewRev ? $this->mNewRev->getRevisionRecord() : null;
390 }
391
400 public function deletedLink( $id ) {
401 if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) {
404 $row = $dbr->selectRow(
405 $arQuery['tables'],
406 array_merge( $arQuery['fields'], [ 'ar_namespace', 'ar_title' ] ),
407 [ 'ar_rev_id' => $id ],
408 __METHOD__,
409 [],
410 $arQuery['joins']
411 );
412 if ( $row ) {
414 $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
415
416 return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( [
417 'target' => $title->getPrefixedText(),
418 'timestamp' => $rev->getTimestamp()
419 ] );
420 }
421 }
422
423 return false;
424 }
425
433 public function deletedIdMarker( $id ) {
434 $link = $this->deletedLink( $id );
435 if ( $link ) {
436 return "[$link $id]";
437 } else {
438 return (string)$id;
439 }
440 }
441
442 private function showMissingRevision() {
443 $out = $this->getOutput();
444
445 $missing = [];
446 if ( $this->mOldRev === null ||
447 ( $this->mOldRev && $this->mOldContent === null )
448 ) {
449 $missing[] = $this->deletedIdMarker( $this->mOldid );
450 }
451 if ( $this->mNewRev === null ||
452 ( $this->mNewRev && $this->mNewContent === null )
453 ) {
454 $missing[] = $this->deletedIdMarker( $this->mNewid );
455 }
456
457 $out->setPageTitle( $this->msg( 'errorpagetitle' ) );
458 $msg = $this->msg( 'difference-missing-revision' )
459 ->params( $this->getLanguage()->listToText( $missing ) )
460 ->numParams( count( $missing ) )
461 ->parseAsBlock();
462 $out->addHTML( $msg );
463 }
464
465 public function showDiffPage( $diffOnly = false ) {
466 # Allow frames except in certain special cases
467 $out = $this->getOutput();
468 $out->allowClickjacking();
469 $out->setRobotPolicy( 'noindex,nofollow' );
470
471 // Allow extensions to add any extra output here
472 Hooks::run( 'DifferenceEngineShowDiffPage', [ $out ] );
473
474 if ( !$this->loadRevisionData() ) {
475 if ( Hooks::run( 'DifferenceEngineShowDiffPageMaybeShowMissingRevision', [ $this ] ) ) {
476 $this->showMissingRevision();
477 }
478 return;
479 }
480
481 $user = $this->getUser();
482 $permErrors = [];
483 if ( $this->mNewPage ) {
484 $permErrors = $this->mNewPage->getUserPermissionsErrors( 'read', $user );
485 }
486 if ( $this->mOldPage ) {
487 $permErrors = wfMergeErrorArrays( $permErrors,
488 $this->mOldPage->getUserPermissionsErrors( 'read', $user ) );
489 }
490 if ( count( $permErrors ) ) {
491 throw new PermissionsError( 'read', $permErrors );
492 }
493
494 $rollback = '';
495
496 $query = [];
497 # Carry over 'diffonly' param via navigation links
498 if ( $diffOnly != $user->getBoolOption( 'diffonly' ) ) {
499 $query['diffonly'] = $diffOnly;
500 }
501 # Cascade unhide param in links for easy deletion browsing
502 if ( $this->unhide ) {
503 $query['unhide'] = 1;
504 }
505
506 # Check if one of the revisions is deleted/suppressed
507 $deleted = $suppressed = false;
508 $allowed = $this->mNewRev->userCan( Revision::DELETED_TEXT, $user );
509
510 $revisionTools = [];
511
512 # mOldRev is false if the difference engine is called with a "vague" query for
513 # a diff between a version V and its previous version V' AND the version V
514 # is the first version of that article. In that case, V' does not exist.
515 if ( $this->mOldRev === false ) {
516 if ( $this->mNewPage ) {
517 $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
518 }
519 $samePage = true;
520 $oldHeader = '';
521 // Allow extensions to change the $oldHeader variable
522 Hooks::run( 'DifferenceEngineOldHeaderNoOldRev', [ &$oldHeader ] );
523 } else {
524 Hooks::run( 'DiffViewHeader', [ $this, $this->mOldRev, $this->mNewRev ] );
525
526 if ( !$this->mOldPage || !$this->mNewPage ) {
527 // XXX say something to the user?
528 $samePage = false;
529 } elseif ( $this->mNewPage->equals( $this->mOldPage ) ) {
530 $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
531 $samePage = true;
532 } else {
533 $out->setPageTitle( $this->msg( 'difference-title-multipage',
534 $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) );
535 $out->addSubtitle( $this->msg( 'difference-multipage' ) );
536 $samePage = false;
537 }
538
539 if ( $samePage && $this->mNewPage && $this->mNewPage->quickUserCan( 'edit', $user ) ) {
540 if ( $this->mNewRev->isCurrent() && $this->mNewPage->userCan( 'rollback', $user ) ) {
541 $rollbackLink = Linker::generateRollback( $this->mNewRev, $this->getContext() );
542 if ( $rollbackLink ) {
543 $out->preventClickjacking();
544 $rollback = "\u{00A0}\u{00A0}\u{00A0}" . $rollbackLink;
545 }
546 }
547
548 if ( !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) &&
549 !$this->mNewRev->isDeleted( Revision::DELETED_TEXT )
550 ) {
551 $undoLink = Html::element( 'a', [
552 'href' => $this->mNewPage->getLocalURL( [
553 'action' => 'edit',
554 'undoafter' => $this->mOldid,
555 'undo' => $this->mNewid
556 ] ),
557 'title' => Linker::titleAttrib( 'undo' ),
558 ],
559 $this->msg( 'editundo' )->text()
560 );
561 $revisionTools['mw-diff-undo'] = $undoLink;
562 }
563 }
564
565 # Make "previous revision link"
566 if ( $samePage && $this->mOldPage && $this->mOldRev->getPrevious() ) {
567 $prevlink = Linker::linkKnown(
568 $this->mOldPage,
569 $this->msg( 'previousdiff' )->escaped(),
570 [ 'id' => 'differences-prevlink' ],
571 [ 'diff' => 'prev', 'oldid' => $this->mOldid ] + $query
572 );
573 } else {
574 $prevlink = "\u{00A0}";
575 }
576
577 if ( $this->mOldRev->isMinor() ) {
578 $oldminor = ChangesList::flag( 'minor' );
579 } else {
580 $oldminor = '';
581 }
582
583 $ldel = $this->revisionDeleteLink( $this->mOldRev );
584 $oldRevisionHeader = $this->getRevisionHeader( $this->mOldRev, 'complete' );
585 $oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff', $this->getContext() );
586
587 $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' .
588 '<div id="mw-diff-otitle2">' .
589 Linker::revUserTools( $this->mOldRev, !$this->unhide ) . '</div>' .
590 '<div id="mw-diff-otitle3">' . $oldminor .
591 Linker::revComment( $this->mOldRev, !$diffOnly, !$this->unhide ) . $ldel . '</div>' .
592 '<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' .
593 '<div id="mw-diff-otitle4">' . $prevlink . '</div>';
594
595 // Allow extensions to change the $oldHeader variable
596 Hooks::run( 'DifferenceEngineOldHeader', [ $this, &$oldHeader, $prevlink, $oldminor,
597 $diffOnly, $ldel, $this->unhide ] );
598
599 if ( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) {
600 $deleted = true; // old revisions text is hidden
601 if ( $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
602 $suppressed = true; // also suppressed
603 }
604 }
605
606 # Check if this user can see the revisions
607 if ( !$this->mOldRev->userCan( Revision::DELETED_TEXT, $user ) ) {
608 $allowed = false;
609 }
610 }
611
612 $out->addJsConfigVars( [
613 'wgDiffOldId' => $this->mOldid,
614 'wgDiffNewId' => $this->mNewid,
615 ] );
616
617 # Make "next revision link"
618 # Skip next link on the top revision
619 if ( $samePage && $this->mNewPage && !$this->mNewRev->isCurrent() ) {
620 $nextlink = Linker::linkKnown(
621 $this->mNewPage,
622 $this->msg( 'nextdiff' )->escaped(),
623 [ 'id' => 'differences-nextlink' ],
624 [ 'diff' => 'next', 'oldid' => $this->mNewid ] + $query
625 );
626 } else {
627 $nextlink = "\u{00A0}";
628 }
629
630 if ( $this->mNewRev->isMinor() ) {
631 $newminor = ChangesList::flag( 'minor' );
632 } else {
633 $newminor = '';
634 }
635
636 # Handle RevisionDelete links...
637 $rdel = $this->revisionDeleteLink( $this->mNewRev );
638
639 # Allow extensions to define their own revision tools
640 Hooks::run( 'DiffRevisionTools',
641 [ $this->mNewRev, &$revisionTools, $this->mOldRev, $user ] );
642 $formattedRevisionTools = [];
643 // Put each one in parentheses (poor man's button)
644 foreach ( $revisionTools as $key => $tool ) {
645 $toolClass = is_string( $key ) ? $key : 'mw-diff-tool';
646 $element = Html::rawElement(
647 'span',
648 [ 'class' => $toolClass ],
649 $this->msg( 'parentheses' )->rawParams( $tool )->escaped()
650 );
651 $formattedRevisionTools[] = $element;
652 }
653 $newRevisionHeader = $this->getRevisionHeader( $this->mNewRev, 'complete' ) .
654 ' ' . implode( ' ', $formattedRevisionTools );
655 $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff', $this->getContext() );
656
657 $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' .
658 '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $this->mNewRev, !$this->unhide ) .
659 " $rollback</div>" .
660 '<div id="mw-diff-ntitle3">' . $newminor .
661 Linker::revComment( $this->mNewRev, !$diffOnly, !$this->unhide ) . $rdel . '</div>' .
662 '<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' .
663 '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
664
665 // Allow extensions to change the $newHeader variable
666 Hooks::run( 'DifferenceEngineNewHeader', [ $this, &$newHeader, $formattedRevisionTools,
667 $nextlink, $rollback, $newminor, $diffOnly, $rdel, $this->unhide ] );
668
669 if ( $this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
670 $deleted = true; // new revisions text is hidden
671 if ( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
672 $suppressed = true; // also suppressed
673 }
674 }
675
676 # If the diff cannot be shown due to a deleted revision, then output
677 # the diff header and links to unhide (if available)...
678 if ( $deleted && ( !$this->unhide || !$allowed ) ) {
679 $this->showDiffStyle();
680 $multi = $this->getMultiNotice();
681 $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
682 if ( !$allowed ) {
683 $msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff';
684 # Give explanation for why revision is not visible
685 $out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
686 [ $msg ] );
687 } else {
688 # Give explanation and add a link to view the diff...
689 $query = $this->getRequest()->appendQueryValue( 'unhide', '1' );
690 $link = $this->getTitle()->getFullURL( $query );
691 $msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff';
692 $out->wrapWikiMsg(
693 "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
694 [ $msg, $link ]
695 );
696 }
697 # Otherwise, output a regular diff...
698 } else {
699 # Add deletion notice if the user is viewing deleted content
700 $notice = '';
701 if ( $deleted ) {
702 $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
703 $notice = "<div id='mw-$msg' class='mw-warning plainlinks'>\n" .
704 $this->msg( $msg )->parse() .
705 "</div>\n";
706 }
707 $this->showDiff( $oldHeader, $newHeader, $notice );
708 if ( !$diffOnly ) {
709 $this->renderNewRevision();
710 }
711 }
712 }
713
723 public function markPatrolledLink() {
724 if ( $this->mMarkPatrolledLink === null ) {
725 $linkInfo = $this->getMarkPatrolledLinkInfo();
726 // If false, there is no patrol link needed/allowed
727 if ( !$linkInfo || !$this->mNewPage ) {
728 $this->mMarkPatrolledLink = '';
729 } else {
730 $this->mMarkPatrolledLink = ' <span class="patrollink" data-mw="interface">[' .
732 $this->mNewPage,
733 $this->msg( 'markaspatrolleddiff' )->escaped(),
734 [],
735 [
736 'action' => 'markpatrolled',
737 'rcid' => $linkInfo['rcid'],
738 ]
739 ) . ']</span>';
740 // Allow extensions to change the markpatrolled link
741 Hooks::run( 'DifferenceEngineMarkPatrolledLink', [ $this,
742 &$this->mMarkPatrolledLink, $linkInfo['rcid'] ] );
743 }
744 }
746 }
747
755 protected function getMarkPatrolledLinkInfo() {
756 global $wgUseRCPatrol;
757
758 $user = $this->getUser();
759
760 // Prepare a change patrol link, if applicable
761 if (
762 // Is patrolling enabled and the user allowed to?
763 $wgUseRCPatrol && $this->mNewPage && $this->mNewPage->quickUserCan( 'patrol', $user ) &&
764 // Only do this if the revision isn't more than 6 hours older
765 // than the Max RC age (6h because the RC might not be cleaned out regularly)
766 RecentChange::isInRCLifespan( $this->mNewRev->getTimestamp(), 21600 )
767 ) {
768 // Look for an unpatrolled change corresponding to this diff
769 $db = wfGetDB( DB_REPLICA );
770 $change = RecentChange::newFromConds(
771 [
772 'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
773 'rc_this_oldid' => $this->mNewid,
774 'rc_patrolled' => RecentChange::PRC_UNPATROLLED
775 ],
776 __METHOD__
777 );
778
779 if ( $change && !$change->getPerformer()->equals( $user ) ) {
780 $rcid = $change->getAttribute( 'rc_id' );
781 } else {
782 // None found or the page has been created by the current user.
783 // If the user could patrol this it already would be patrolled
784 $rcid = 0;
785 }
786
787 // Allow extensions to possibly change the rcid here
788 // For example the rcid might be set to zero due to the user
789 // being the same as the performer of the change but an extension
790 // might still want to show it under certain conditions
791 Hooks::run( 'DifferenceEngineMarkPatrolledRCID', [ &$rcid, $this, $change, $user ] );
792
793 // Build the link
794 if ( $rcid ) {
795 $this->getOutput()->preventClickjacking();
796 if ( $user->isAllowed( 'writeapi' ) ) {
797 $this->getOutput()->addModules( 'mediawiki.page.patrol.ajax' );
798 }
799
800 return [
801 'rcid' => $rcid,
802 ];
803 }
804 }
805
806 // No mark as patrolled link applicable
807 return false;
808 }
809
815 protected function revisionDeleteLink( $rev ) {
816 $link = Linker::getRevDeleteLink( $this->getUser(), $rev, $rev->getTitle() );
817 if ( $link !== '' ) {
818 $link = "\u{00A0}\u{00A0}\u{00A0}" . $link . ' ';
819 }
820
821 return $link;
822 }
823
829 public function renderNewRevision() {
830 if ( $this->isContentOverridden ) {
831 // The code below only works with a Revision object. We could construct a fake revision
832 // (here or in setContent), but since this does not seem needed at the moment,
833 // we'll just fail for now.
834 throw new LogicException(
835 __METHOD__
836 . ' is not supported after calling setContent(). Use setRevisions() instead.'
837 );
838 }
839
840 $out = $this->getOutput();
841 $revHeader = $this->getRevisionHeader( $this->mNewRev );
842 # Add "current version as of X" title
843 $out->addHTML( "<hr class='diff-hr' id='mw-oldid' />
844 <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
845 # Page content may be handled by a hooked call instead...
846 if ( Hooks::run( 'ArticleContentOnDiff', [ $this, $out ] ) ) {
847 $this->loadNewText();
848 if ( !$this->mNewPage ) {
849 // New revision is unsaved; bail out.
850 // TODO in theory rendering the new revision is a meaningful thing to do
851 // even if it's unsaved, but a lot of untangling is required to do it safely.
852 return;
853 }
854
855 $out->setRevisionId( $this->mNewid );
856 $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() );
857 $out->setArticleFlag( true );
858
859 if ( !Hooks::run( 'ArticleRevisionViewCustom',
860 [ $this->mNewRev->getRevisionRecord(), $this->mNewPage, $this->mOldid, $out ] )
861 ) {
862 // Handled by extension
863 // NOTE: sync with hooks called in Article::view()
864 } elseif ( !Hooks::run( 'ArticleContentViewCustom',
865 [ $this->mNewContent, $this->mNewPage, $out ], '1.32' )
866 ) {
867 // Handled by extension
868 // NOTE: sync with hooks called in Article::view()
869 } else {
870 // Normal page
871 if ( $this->getTitle()->equals( $this->mNewPage ) ) {
872 // If the Title stored in the context is the same as the one
873 // of the new revision, we can use its associated WikiPage
874 // object.
875 $wikiPage = $this->getWikiPage();
876 } else {
877 // Otherwise we need to create our own WikiPage object
878 $wikiPage = WikiPage::factory( $this->mNewPage );
879 }
880
881 $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRev );
882
883 # WikiPage::getParserOutput() should not return false, but just in case
884 if ( $parserOutput ) {
885 // Allow extensions to change parser output here
886 if ( Hooks::run( 'DifferenceEngineRenderRevisionAddParserOutput',
887 [ $this, $out, $parserOutput, $wikiPage ] )
888 ) {
889 $out->addParserOutput( $parserOutput, [
890 'enableSectionEditLinks' => $this->mNewRev->isCurrent()
891 && $this->mNewRev->getTitle()->quickUserCan( 'edit', $this->getUser() ),
892 ] );
893 }
894 }
895 }
896 }
897
898 // Allow extensions to optionally not show the final patrolled link
899 if ( Hooks::run( 'DifferenceEngineRenderRevisionShowFinalPatrolLink' ) ) {
900 # Add redundant patrol link on bottom...
901 $out->addHTML( $this->markPatrolledLink() );
902 }
903 }
904
911 protected function getParserOutput( WikiPage $page, Revision $rev ) {
912 if ( !$rev->getId() ) {
913 // WikiPage::getParserOutput wants a revision ID. Passing 0 will incorrectly show
914 // the current revision, so fail instead. If need be, WikiPage::getParserOutput
915 // could be made to accept a Revision or RevisionRecord instead of the id.
916 return false;
917 }
918
919 $parserOptions = $page->makeParserOptions( $this->getContext() );
920 $parserOutput = $page->getParserOutput( $parserOptions, $rev->getId() );
921
922 return $parserOutput;
923 }
924
935 public function showDiff( $otitle, $ntitle, $notice = '' ) {
936 // Allow extensions to affect the output here
937 Hooks::run( 'DifferenceEngineShowDiff', [ $this ] );
938
939 $diff = $this->getDiff( $otitle, $ntitle, $notice );
940 if ( $diff === false ) {
941 $this->showMissingRevision();
942
943 return false;
944 } else {
945 $this->showDiffStyle();
946 $this->getOutput()->addHTML( $diff );
947
948 return true;
949 }
950 }
951
955 public function showDiffStyle() {
956 if ( !$this->isSlotDiffRenderer ) {
957 $this->getOutput()->addModuleStyles( 'mediawiki.diff.styles' );
958 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
959 $slotDiffRenderer->addModules( $this->getOutput() );
960 }
961 }
962 }
963
973 public function getDiff( $otitle, $ntitle, $notice = '' ) {
974 $body = $this->getDiffBody();
975 if ( $body === false ) {
976 return false;
977 }
978
979 $multi = $this->getMultiNotice();
980 // Display a message when the diff is empty
981 if ( $body === '' ) {
982 $notice .= '<div class="mw-diff-empty">' .
983 $this->msg( 'diff-empty' )->parse() .
984 "</div>\n";
985 }
986
987 return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
988 }
989
995 public function getDiffBody() {
996 $this->mCacheHit = true;
997 // Check if the diff should be hidden from this user
998 if ( !$this->isContentOverridden ) {
999 if ( !$this->loadRevisionData() ) {
1000 return false;
1001 } elseif ( $this->mOldRev &&
1002 !$this->mOldRev->userCan( Revision::DELETED_TEXT, $this->getUser() )
1003 ) {
1004 return false;
1005 } elseif ( $this->mNewRev &&
1006 !$this->mNewRev->userCan( Revision::DELETED_TEXT, $this->getUser() )
1007 ) {
1008 return false;
1009 }
1010 // Short-circuit
1011 if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev &&
1012 $this->mOldRev->getId() && $this->mOldRev->getId() == $this->mNewRev->getId() )
1013 ) {
1014 if ( Hooks::run( 'DifferenceEngineShowEmptyOldContent', [ $this ] ) ) {
1015 return '';
1016 }
1017 }
1018 }
1019
1020 // Cacheable?
1021 $key = false;
1022 $cache = ObjectCache::getMainWANInstance();
1023 if ( $this->mOldid && $this->mNewid ) {
1024 // Check if subclass is still using the old way
1025 // for backwards-compatibility
1026 $key = $this->getDiffBodyCacheKey();
1027 if ( $key === null ) {
1028 $key = $cache->makeKey( ...$this->getDiffBodyCacheKeyParams() );
1029 }
1030
1031 // Try cache
1032 if ( !$this->mRefreshCache ) {
1033 $difftext = $cache->get( $key );
1034 if ( $difftext ) {
1035 wfIncrStats( 'diff_cache.hit' );
1036 $difftext = $this->localiseDiff( $difftext );
1037 $difftext .= "\n<!-- diff cache key $key -->\n";
1038
1039 return $difftext;
1040 }
1041 } // don't try to load but save the result
1042 }
1043 $this->mCacheHit = false;
1044
1045 // Loadtext is permission safe, this just clears out the diff
1046 if ( !$this->loadText() ) {
1047 return false;
1048 }
1049
1050 $difftext = '';
1051 // We've checked for revdelete at the beginning of this method; it's OK to ignore
1052 // read permissions here.
1053 $slotContents = $this->getSlotContents();
1054 foreach ( $this->getSlotDiffRenderers() as $role => $slotDiffRenderer ) {
1055 $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role]['old'],
1056 $slotContents[$role]['new'] );
1057 if ( $slotDiff && $role !== SlotRecord::MAIN ) {
1058 // TODO use human-readable role name at least
1059 $slotTitle = $role;
1060 $difftext .= $this->getSlotHeader( $slotTitle );
1061 }
1062 $difftext .= $slotDiff;
1063 }
1064
1065 // Avoid PHP 7.1 warning from passing $this by reference
1066 $diffEngine = $this;
1067
1068 // Save to cache for 7 days
1069 if ( !Hooks::run( 'AbortDiffCache', [ &$diffEngine ] ) ) {
1070 wfIncrStats( 'diff_cache.uncacheable' );
1071 } elseif ( $key !== false && $difftext !== false ) {
1072 wfIncrStats( 'diff_cache.miss' );
1073 $cache->set( $key, $difftext, 7 * 86400 );
1074 } else {
1075 wfIncrStats( 'diff_cache.uncacheable' );
1076 }
1077 // localise line numbers and title attribute text
1078 if ( $difftext !== false ) {
1079 $difftext = $this->localiseDiff( $difftext );
1080 }
1081
1082 return $difftext;
1083 }
1084
1091 public function getDiffBodyForRole( $role ) {
1092 $diffRenderers = $this->getSlotDiffRenderers();
1093 if ( !isset( $diffRenderers[$role] ) ) {
1094 return false;
1095 }
1096
1097 $slotContents = $this->getSlotContents();
1098 $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role]['old'],
1099 $slotContents[$role]['new'] );
1100 if ( !$slotDiff ) {
1101 return false;
1102 }
1103
1104 if ( $role !== SlotRecord::MAIN ) {
1105 // TODO use human-readable role name at least
1106 $slotTitle = $role;
1107 $slotDiff = $this->getSlotHeader( $slotTitle ) . $slotDiff;
1108 }
1109
1110 return $this->localiseDiff( $slotDiff );
1111 }
1112
1120 protected function getSlotHeader( $headerText ) {
1121 // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
1122 $columnCount = $this->mOldRev ? 4 : 2;
1123 $userLang = $this->getLanguage()->getHtmlCode();
1124 return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-header', 'lang' => $userLang ],
1125 Html::element( 'th', [ 'colspan' => $columnCount ], $headerText ) );
1126 }
1127
1137 protected function getDiffBodyCacheKey() {
1138 return null;
1139 }
1140
1154 protected function getDiffBodyCacheKeyParams() {
1155 if ( !$this->mOldid || !$this->mNewid ) {
1156 throw new MWException( 'mOldid and mNewid must be set to get diff cache key.' );
1157 }
1158
1159 $engine = $this->getEngine();
1160 $params = [
1161 'diff',
1162 $engine,
1164 "old-{$this->mOldid}",
1165 "rev-{$this->mNewid}"
1166 ];
1167
1168 if ( $engine === 'wikidiff2' ) {
1169 $params[] = phpversion( 'wikidiff2' );
1170 $params[] = $this->getConfig()->get( 'WikiDiff2MovedParagraphDetectionCutoff' );
1171 }
1172
1173 if ( !$this->isSlotDiffRenderer ) {
1174 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1175 $params = array_merge( $params, $slotDiffRenderer->getExtraCacheKeys() );
1176 }
1177 }
1178
1179 return $params;
1180 }
1181
1189 public function getExtraCacheKeys() {
1190 // This method is called when the DifferenceEngine is used for a slot diff. We only care
1191 // about special things, not the revision IDs, which are added to the cache key by the
1192 // page-level DifferenceEngine, and which might not have a valid value for this object.
1193 $this->mOldid = 123456789;
1194 $this->mNewid = 987654321;
1195
1196 // This will repeat a bunch of unnecessary key fields for each slot. Not nice but harmless.
1197 $cacheString = $this->getDiffBodyCacheKey();
1198 if ( $cacheString ) {
1199 return [ $cacheString ];
1200 }
1201
1203
1204 // Try to get rid of the standard keys to keep the cache key human-readable:
1205 // call the getDiffBodyCacheKeyParams implementation of the base class, and if
1206 // the child class includes the same keys, drop them.
1207 // Uses an obscure PHP feature where static calls to non-static methods are allowed
1208 // as long as we are already in a non-static method of the same class, and the call context
1209 // ($this) will be inherited.
1210 // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
1212 if ( array_slice( $params, 0, count( $standardParams ) ) === $standardParams ) {
1213 $params = array_slice( $params, count( $standardParams ) );
1214 }
1215
1216 return $params;
1217 }
1218
1232 public function generateContentDiffBody( Content $old, Content $new ) {
1233 $slotDiffRenderer = $new->getContentHandler()->getSlotDiffRenderer( $this->getContext() );
1234 if (
1235 $slotDiffRenderer instanceof DifferenceEngineSlotDiffRenderer
1236 && $this->isSlotDiffRenderer
1237 ) {
1238 // Oops, we are just about to enter an infinite loop (the slot-level DifferenceEngine
1239 // called a DifferenceEngineSlotDiffRenderer that wraps the same DifferenceEngine class).
1240 // This will happen when a content model has no custom slot diff renderer, it does have
1241 // a custom difference engine, but that does not override this method.
1242 throw new Exception( get_class( $this ) . ': could not maintain backwards compatibility. '
1243 . 'Please use a SlotDiffRenderer.' );
1244 }
1245 return $slotDiffRenderer->getDiff( $old, $new ) . $this->getDebugString();
1246 }
1247
1260 public function generateTextDiffBody( $otext, $ntext ) {
1261 $slotDiffRenderer = ContentHandler::getForModelID( CONTENT_MODEL_TEXT )
1262 ->getSlotDiffRenderer( $this->getContext() );
1263 if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1264 // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1265 // This is too unlikely to happen to bother handling properly.
1266 throw new Exception( 'The slot diff renderer for text content should be a '
1267 . 'TextSlotDiffRenderer subclass' );
1268 }
1269 return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1270 }
1271
1278 public static function getEngine() {
1279 global $wgExternalDiffEngine;
1280 // We use the global here instead of Config because we write to the value,
1281 // and Config is not mutable.
1282 if ( $wgExternalDiffEngine == 'wikidiff' || $wgExternalDiffEngine == 'wikidiff3' ) {
1283 wfDeprecated( "\$wgExternalDiffEngine = '{$wgExternalDiffEngine}'", '1.27' );
1284 $wgExternalDiffEngine = false;
1285 } elseif ( $wgExternalDiffEngine == 'wikidiff2' ) {
1286 wfDeprecated( "\$wgExternalDiffEngine = '{$wgExternalDiffEngine}'", '1.32' );
1287 $wgExternalDiffEngine = false;
1288 } elseif ( !is_string( $wgExternalDiffEngine ) && $wgExternalDiffEngine !== false ) {
1289 // And prevent people from shooting themselves in the foot...
1290 wfWarn( '$wgExternalDiffEngine is set to a non-string value, forcing it to false' );
1291 $wgExternalDiffEngine = false;
1292 }
1293
1294 if ( is_string( $wgExternalDiffEngine ) && is_executable( $wgExternalDiffEngine ) ) {
1295 return $wgExternalDiffEngine;
1296 } elseif ( $wgExternalDiffEngine === false && function_exists( 'wikidiff2_do_diff' ) ) {
1297 return 'wikidiff2';
1298 } else {
1299 // Native PHP
1300 return false;
1301 }
1302 }
1303
1316 protected function textDiff( $otext, $ntext ) {
1317 $slotDiffRenderer = ContentHandler::getForModelID( CONTENT_MODEL_TEXT )
1318 ->getSlotDiffRenderer( $this->getContext() );
1319 if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1320 // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1321 // This is too unlikely to happen to bother handling properly.
1322 throw new Exception( 'The slot diff renderer for text content should be a '
1323 . 'TextSlotDiffRenderer subclass' );
1324 }
1325 return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1326 }
1327
1336 protected function debug( $generator = "internal" ) {
1337 global $wgShowHostnames;
1338 if ( !$this->enableDebugComment ) {
1339 return '';
1340 }
1341 $data = [ $generator ];
1342 if ( $wgShowHostnames ) {
1343 $data[] = wfHostname();
1344 }
1345 $data[] = wfTimestamp( TS_DB );
1346
1347 return "<!-- diff generator: " .
1348 implode( " ", array_map( "htmlspecialchars", $data ) ) .
1349 " -->\n";
1350 }
1351
1352 private function getDebugString() {
1354 if ( $engine === 'wikidiff2' ) {
1355 return $this->debug( 'wikidiff2' );
1356 } elseif ( $engine === false ) {
1357 return $this->debug( 'native PHP' );
1358 } else {
1359 return $this->debug( "external $engine" );
1360 }
1361 }
1362
1369 private function localiseDiff( $text ) {
1370 $text = $this->localiseLineNumbers( $text );
1371 if ( $this->getEngine() === 'wikidiff2' &&
1372 version_compare( phpversion( 'wikidiff2' ), '1.5.1', '>=' )
1373 ) {
1374 $text = $this->addLocalisedTitleTooltips( $text );
1375 }
1376 return $text;
1377 }
1378
1386 public function localiseLineNumbers( $text ) {
1387 return preg_replace_callback(
1388 '/<!--LINE (\d+)-->/',
1389 [ $this, 'localiseLineNumbersCb' ],
1390 $text
1391 );
1392 }
1393
1394 public function localiseLineNumbersCb( $matches ) {
1395 if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
1396 return '';
1397 }
1398
1399 return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
1400 }
1401
1408 private function addLocalisedTitleTooltips( $text ) {
1409 return preg_replace_callback(
1410 '/class="mw-diff-movedpara-(left|right)"/',
1411 [ $this, 'addLocalisedTitleTooltipsCb' ],
1412 $text
1413 );
1414 }
1415
1421 $key = $matches[1] === 'right' ?
1422 'diff-paragraph-moved-toold' :
1423 'diff-paragraph-moved-tonew';
1424 return $matches[0] . ' title="' . $this->msg( $key )->escaped() . '"';
1425 }
1426
1432 public function getMultiNotice() {
1433 // The notice only make sense if we are diffing two saved revisions of the same page.
1434 if (
1435 !$this->mOldRev || !$this->mNewRev
1436 || !$this->mOldPage || !$this->mNewPage
1437 || !$this->mOldPage->equals( $this->mNewPage )
1438 ) {
1439 return '';
1440 }
1441
1442 if ( $this->mOldRev->getTimestamp() > $this->mNewRev->getTimestamp() ) {
1443 $oldRev = $this->mNewRev; // flip
1444 $newRev = $this->mOldRev; // flip
1445 } else { // normal case
1446 $oldRev = $this->mOldRev;
1448 }
1449
1450 // Sanity: don't show the notice if too many rows must be scanned
1451 // @todo show some special message for that case
1452 $nEdits = $this->mNewPage->countRevisionsBetween( $oldRev, $newRev, 1000 );
1453 if ( $nEdits > 0 && $nEdits <= 1000 ) {
1454 $limit = 100; // use diff-multi-manyusers if too many users
1455 $users = $this->mNewPage->getAuthorsBetween( $oldRev, $newRev, $limit );
1456 $numUsers = count( $users );
1457
1458 if ( $numUsers == 1 && $users[0] == $newRev->getUserText( Revision::RAW ) ) {
1459 $numUsers = 0; // special case to say "by the same user" instead of "by one other user"
1460 }
1461
1462 return self::intermediateEditsMsg( $nEdits, $numUsers, $limit );
1463 }
1464
1465 return ''; // nothing
1466 }
1467
1477 public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) {
1478 if ( $numUsers === 0 ) {
1479 $msg = 'diff-multi-sameuser';
1480 } elseif ( $numUsers > $limit ) {
1481 $msg = 'diff-multi-manyusers';
1482 $numUsers = $limit;
1483 } else {
1484 $msg = 'diff-multi-otherusers';
1485 }
1486
1487 return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1488 }
1489
1499 public function getRevisionHeader( Revision $rev, $complete = '' ) {
1500 $lang = $this->getLanguage();
1501 $user = $this->getUser();
1502 $revtimestamp = $rev->getTimestamp();
1503 $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
1504 $dateofrev = $lang->userDate( $revtimestamp, $user );
1505 $timeofrev = $lang->userTime( $revtimestamp, $user );
1506
1507 $header = $this->msg(
1508 $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
1509 $timestamp,
1510 $dateofrev,
1511 $timeofrev
1512 )->escaped();
1513
1514 if ( $complete !== 'complete' ) {
1515 return $header;
1516 }
1517
1518 $title = $rev->getTitle();
1519
1520 $header = Linker::linkKnown( $title, $header, [],
1521 [ 'oldid' => $rev->getId() ] );
1522
1523 if ( $rev->userCan( Revision::DELETED_TEXT, $user ) ) {
1524 $editQuery = [ 'action' => 'edit' ];
1525 if ( !$rev->isCurrent() ) {
1526 $editQuery['oldid'] = $rev->getId();
1527 }
1528
1529 $key = $title->quickUserCan( 'edit', $user ) ? 'editold' : 'viewsourceold';
1530 $msg = $this->msg( $key )->escaped();
1531 $editLink = $this->msg( 'parentheses' )->rawParams(
1532 Linker::linkKnown( $title, $msg, [], $editQuery ) )->escaped();
1533 $header .= ' ' . Html::rawElement(
1534 'span',
1535 [ 'class' => 'mw-diff-edit' ],
1536 $editLink
1537 );
1538 if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
1539 $header = Html::rawElement(
1540 'span',
1541 [ 'class' => 'history-deleted' ],
1542 $header
1543 );
1544 }
1545 } else {
1546 $header = Html::rawElement( 'span', [ 'class' => 'history-deleted' ], $header );
1547 }
1548
1549 return $header;
1550 }
1551
1564 public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
1565 // shared.css sets diff in interface language/dir, but the actual content
1566 // is often in a different language, mostly the page content language/dir
1567 $header = Html::openElement( 'table', [
1568 'class' => [ 'diff', 'diff-contentalign-' . $this->getDiffLang()->alignStart() ],
1569 'data-mw' => 'interface',
1570 ] );
1571 $userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
1572
1573 if ( !$diff && !$otitle ) {
1574 $header .= "
1575 <tr class=\"diff-title\" lang=\"{$userLang}\">
1576 <td class=\"diff-ntitle\">{$ntitle}</td>
1577 </tr>";
1578 $multiColspan = 1;
1579 } else {
1580 if ( $diff ) { // Safari/Chrome show broken output if cols not used
1581 $header .= "
1582 <col class=\"diff-marker\" />
1583 <col class=\"diff-content\" />
1584 <col class=\"diff-marker\" />
1585 <col class=\"diff-content\" />";
1586 $colspan = 2;
1587 $multiColspan = 4;
1588 } else {
1589 $colspan = 1;
1590 $multiColspan = 2;
1591 }
1592 if ( $otitle || $ntitle ) {
1593 $header .= "
1594 <tr class=\"diff-title\" lang=\"{$userLang}\">
1595 <td colspan=\"$colspan\" class=\"diff-otitle\">{$otitle}</td>
1596 <td colspan=\"$colspan\" class=\"diff-ntitle\">{$ntitle}</td>
1597 </tr>";
1598 }
1599 }
1600
1601 if ( $multi != '' ) {
1602 $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1603 "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
1604 }
1605 if ( $notice != '' ) {
1606 $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1607 "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
1608 }
1609
1610 return $header . $diff . "</table>";
1611 }
1612
1620 public function setContent( Content $oldContent, Content $newContent ) {
1621 $this->mOldContent = $oldContent;
1622 $this->mNewContent = $newContent;
1623
1624 $this->mTextLoaded = 2;
1625 $this->mRevisionsLoaded = true;
1626 $this->isContentOverridden = true;
1627 $this->slotDiffRenderers = null;
1628 }
1629
1635 public function setRevisions(
1636 RevisionRecord $oldRevision = null, RevisionRecord $newRevision
1637 ) {
1638 if ( $oldRevision ) {
1639 $this->mOldRev = new Revision( $oldRevision );
1640 $this->mOldid = $oldRevision->getId();
1641 $this->mOldPage = Title::newFromLinkTarget( $oldRevision->getPageAsLinkTarget() );
1642 // This method is meant for edit diffs and such so there is no reason to provide a
1643 // revision that's not readable to the user, but check it just in case.
1644 $this->mOldContent = $oldRevision ? $oldRevision->getContent( SlotRecord::MAIN,
1645 RevisionRecord::FOR_THIS_USER, $this->getUser() ) : null;
1646 } else {
1647 $this->mOldPage = null;
1648 $this->mOldRev = $this->mOldid = false;
1649 }
1650 $this->mNewRev = new Revision( $newRevision );
1651 $this->mNewid = $newRevision->getId();
1652 $this->mNewPage = Title::newFromLinkTarget( $newRevision->getPageAsLinkTarget() );
1653 $this->mNewContent = $newRevision->getContent( SlotRecord::MAIN,
1654 RevisionRecord::FOR_THIS_USER, $this->getUser() );
1655
1656 $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded = true;
1657 $this->mTextLoaded = !!$oldRevision + 1;
1658 $this->isContentOverridden = false;
1659 $this->slotDiffRenderers = null;
1660 }
1661
1668 public function setTextLanguage( $lang ) {
1669 if ( !$lang instanceof Language ) {
1670 wfDeprecated( __METHOD__ . ' with other type than Language for $lang', '1.32' );
1671 }
1672 $this->mDiffLang = wfGetLangObj( $lang );
1673 }
1674
1686 public function mapDiffPrevNext( $old, $new ) {
1687 if ( $new === 'prev' ) {
1688 // Show diff between revision $old and the previous one. Get previous one from DB.
1689 $newid = intval( $old );
1690 $oldid = $this->getTitle()->getPreviousRevisionID( $newid );
1691 } elseif ( $new === 'next' ) {
1692 // Show diff between revision $old and the next one. Get next one from DB.
1693 $oldid = intval( $old );
1694 $newid = $this->getTitle()->getNextRevisionID( $oldid );
1695 } else {
1696 $oldid = intval( $old );
1697 $newid = intval( $new );
1698 }
1699
1700 return [ $oldid, $newid ];
1701 }
1702
1706 private function loadRevisionIds() {
1707 if ( $this->mRevisionsIdsLoaded ) {
1708 return;
1709 }
1710
1711 $this->mRevisionsIdsLoaded = true;
1712
1713 $old = $this->mOldid;
1714 $new = $this->mNewid;
1715
1716 list( $this->mOldid, $this->mNewid ) = self::mapDiffPrevNext( $old, $new );
1717 if ( $new === 'next' && $this->mNewid === false ) {
1718 # if no result, NewId points to the newest old revision. The only newer
1719 # revision is cur, which is "0".
1720 $this->mNewid = 0;
1721 }
1722
1723 Hooks::run(
1724 'NewDifferenceEngine',
1725 [ $this->getTitle(), &$this->mOldid, &$this->mNewid, $old, $new ]
1726 );
1727 }
1728
1742 public function loadRevisionData() {
1743 if ( $this->mRevisionsLoaded ) {
1744 return $this->isContentOverridden || $this->mNewRev && !is_null( $this->mOldRev );
1745 }
1746
1747 // Whether it succeeds or fails, we don't want to try again
1748 $this->mRevisionsLoaded = true;
1749
1750 $this->loadRevisionIds();
1751
1752 // Load the new revision object
1753 if ( $this->mNewid ) {
1754 $this->mNewRev = Revision::newFromId( $this->mNewid );
1755 } else {
1756 $this->mNewRev = Revision::newFromTitle(
1757 $this->getTitle(),
1758 false,
1759 Revision::READ_NORMAL
1760 );
1761 }
1762
1763 if ( !$this->mNewRev instanceof Revision ) {
1764 return false;
1765 }
1766
1767 // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
1768 $this->mNewid = $this->mNewRev->getId();
1769 if ( $this->mNewid ) {
1770 $this->mNewPage = $this->mNewRev->getTitle();
1771 } else {
1772 $this->mNewPage = null;
1773 }
1774
1775 // Load the old revision object
1776 $this->mOldRev = false;
1777 if ( $this->mOldid ) {
1778 $this->mOldRev = Revision::newFromId( $this->mOldid );
1779 } elseif ( $this->mOldid === 0 ) {
1780 $rev = $this->mNewRev->getPrevious();
1781 if ( $rev ) {
1782 $this->mOldid = $rev->getId();
1783 $this->mOldRev = $rev;
1784 } else {
1785 // No previous revision; mark to show as first-version only.
1786 $this->mOldid = false;
1787 $this->mOldRev = false;
1788 }
1789 } /* elseif ( $this->mOldid === false ) leave mOldRev false; */
1790
1791 if ( is_null( $this->mOldRev ) ) {
1792 return false;
1793 }
1794
1795 if ( $this->mOldRev && $this->mOldRev->getId() ) {
1796 $this->mOldPage = $this->mOldRev->getTitle();
1797 } else {
1798 $this->mOldPage = null;
1799 }
1800
1801 // Load tags information for both revisions
1802 $dbr = wfGetDB( DB_REPLICA );
1803 if ( $this->mOldid !== false ) {
1804 $this->mOldTags = $dbr->selectField(
1805 'tag_summary',
1806 'ts_tags',
1807 [ 'ts_rev_id' => $this->mOldid ],
1808 __METHOD__
1809 );
1810 } else {
1811 $this->mOldTags = false;
1812 }
1813 $this->mNewTags = $dbr->selectField(
1814 'tag_summary',
1815 'ts_tags',
1816 [ 'ts_rev_id' => $this->mNewid ],
1817 __METHOD__
1818 );
1819
1820 return true;
1821 }
1822
1831 public function loadText() {
1832 if ( $this->mTextLoaded == 2 ) {
1833 return $this->loadRevisionData() && ( $this->mOldRev === false || $this->mOldContent )
1834 && $this->mNewContent;
1835 }
1836
1837 // Whether it succeeds or fails, we don't want to try again
1838 $this->mTextLoaded = 2;
1839
1840 if ( !$this->loadRevisionData() ) {
1841 return false;
1842 }
1843
1844 if ( $this->mOldRev ) {
1845 $this->mOldContent = $this->mOldRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
1846 if ( $this->mOldContent === null ) {
1847 return false;
1848 }
1849 }
1850
1851 $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
1852 Hooks::run( 'DifferenceEngineLoadTextAfterNewContentIsLoaded', [ $this ] );
1853 if ( $this->mNewContent === null ) {
1854 return false;
1855 }
1856
1857 return true;
1858 }
1859
1865 public function loadNewText() {
1866 if ( $this->mTextLoaded >= 1 ) {
1867 return $this->loadRevisionData();
1868 }
1869
1870 $this->mTextLoaded = 1;
1871
1872 if ( !$this->loadRevisionData() ) {
1873 return false;
1874 }
1875
1876 $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
1877
1878 Hooks::run( 'DifferenceEngineAfterLoadNewText', [ $this ] );
1879
1880 return true;
1881 }
1882
1883}
$wgShowHostnames
Expose backend server host names through the API and various HTML comments.
$wgUseRCPatrol
Use RC Patrolling to check for vandalism (from recent changes and watchlists) New pages and new files...
$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.
wfGetLangObj( $langcode=false)
Return a Language object from $langcode.
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 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.
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.
setTextLanguage( $lang)
Set the language in which the diff text is written.
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:35
static generateRollback( $rev, IContextSource $context=null, $options=[ 'verify'])
Generate a rollback link for a given revision.
Definition Linker.php:1704
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:1967
static linkKnown( $target, $html=null, $customAttribs=[], $query=[], $options=[ 'known'])
Identical to link(), except $options defaults to 'known'.
Definition Linker.php:141
static revComment(Revision $rev, $local=false, $isPublic=false)
Wrap and format the given revision's comment block, if the current user is allowed to view it.
Definition Linker.php:1466
static revUserTools( $rev, $isPublic=false)
Generate a user tool link cluster if the current user is allowed to view it.
Definition Linker.php:1053
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:2051
MediaWiki exception.
Page revision base class.
Value object representing a content slot associated with a page revision.
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:535
static newFromArchiveRow( $row, $overrides=[])
Make a fake revision object from an archive table row.
Definition Revision.php:167
const DELETED_TEXT
Definition Revision.php:47
const DELETED_RESTRICTED
Definition Revision.php:50
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:133
const RAW
Definition Revision.php:57
const FOR_THIS_USER
Definition Revision.php:56
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition Revision.php:114
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:39
Class representing a MediaWiki article and history.
Definition WikiPage.php:44
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
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:1667
also included in $newHeader $rollback
Definition hooks.txt:1308
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:1312
returning false will NOT prevent logging a wrapping ErrorException $suppressed
Definition hooks.txt:2238
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
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub 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:894
usually copyright or history_copyright This message must be in HTML not wikitext & $link
Definition hooks.txt:3106
the value to return A Title object or null for latest all implement SearchIndexField $engine
Definition hooks.txt:2962
also included in $newHeader if any $newminor
Definition hooks.txt:1310
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:1656
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:1818
const NS_SPECIAL
Definition Defines.php:53
const CONTENT_MODEL_TEXT
Definition Defines.php:238
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