MediaWiki REL1_34
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 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
404 if ( $permissionManager->userHasRight( $this->getUser(), 'deletedhistory' ) ) {
406 $arQuery = Revision::getArchiveQueryInfo();
407 $row = $dbr->selectRow(
408 $arQuery['tables'],
409 array_merge( $arQuery['fields'], [ 'ar_namespace', 'ar_title' ] ),
410 [ 'ar_rev_id' => $id ],
411 __METHOD__,
412 [],
413 $arQuery['joins']
414 );
415 if ( $row ) {
416 $rev = Revision::newFromArchiveRow( $row );
417 $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
418
419 return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( [
420 'target' => $title->getPrefixedText(),
421 'timestamp' => $rev->getTimestamp()
422 ] );
423 }
424 }
425
426 return false;
427 }
428
436 public function deletedIdMarker( $id ) {
437 $link = $this->deletedLink( $id );
438 if ( $link ) {
439 return "[$link $id]";
440 } else {
441 return (string)$id;
442 }
443 }
444
445 private function showMissingRevision() {
446 $out = $this->getOutput();
447
448 $missing = [];
449 if ( $this->mOldRev === null ||
450 ( $this->mOldRev && $this->mOldContent === null )
451 ) {
452 $missing[] = $this->deletedIdMarker( $this->mOldid );
453 }
454 if ( $this->mNewRev === null ||
455 ( $this->mNewRev && $this->mNewContent === null )
456 ) {
457 $missing[] = $this->deletedIdMarker( $this->mNewid );
458 }
459
460 $out->setPageTitle( $this->msg( 'errorpagetitle' ) );
461 $msg = $this->msg( 'difference-missing-revision' )
462 ->params( $this->getLanguage()->listToText( $missing ) )
463 ->numParams( count( $missing ) )
464 ->parseAsBlock();
465 $out->addHTML( $msg );
466 }
467
468 public function showDiffPage( $diffOnly = false ) {
469 # Allow frames except in certain special cases
470 $out = $this->getOutput();
471 $out->allowClickjacking();
472 $out->setRobotPolicy( 'noindex,nofollow' );
473
474 // Allow extensions to add any extra output here
475 Hooks::run( 'DifferenceEngineShowDiffPage', [ $out ] );
476
477 if ( !$this->loadRevisionData() ) {
478 if ( Hooks::run( 'DifferenceEngineShowDiffPageMaybeShowMissingRevision', [ $this ] ) ) {
479 $this->showMissingRevision();
480 }
481 return;
482 }
483
484 $user = $this->getUser();
485 $permErrors = [];
486 if ( $this->mNewPage ) {
487 $permErrors = $this->mNewPage->getUserPermissionsErrors( 'read', $user );
488 }
489 if ( $this->mOldPage ) {
490 $permErrors = wfMergeErrorArrays( $permErrors,
491 $this->mOldPage->getUserPermissionsErrors( 'read', $user ) );
492 }
493 if ( count( $permErrors ) ) {
494 throw new PermissionsError( 'read', $permErrors );
495 }
496
497 $rollback = '';
498
499 $query = [];
500 # Carry over 'diffonly' param via navigation links
501 if ( $diffOnly != $user->getBoolOption( 'diffonly' ) ) {
502 $query['diffonly'] = $diffOnly;
503 }
504 # Cascade unhide param in links for easy deletion browsing
505 if ( $this->unhide ) {
506 $query['unhide'] = 1;
507 }
508
509 # Check if one of the revisions is deleted/suppressed
510 $deleted = $suppressed = false;
511 $allowed = $this->mNewRev->userCan( RevisionRecord::DELETED_TEXT, $user );
512
513 $revisionTools = [];
514
515 # mOldRev is false if the difference engine is called with a "vague" query for
516 # a diff between a version V and its previous version V' AND the version V
517 # is the first version of that article. In that case, V' does not exist.
518 if ( $this->mOldRev === false ) {
519 if ( $this->mNewPage ) {
520 $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
521 }
522 $samePage = true;
523 $oldHeader = '';
524 // Allow extensions to change the $oldHeader variable
525 Hooks::run( 'DifferenceEngineOldHeaderNoOldRev', [ &$oldHeader ] );
526 } else {
527 Hooks::run( 'DiffViewHeader', [ $this, $this->mOldRev, $this->mNewRev ] );
528
529 if ( !$this->mOldPage || !$this->mNewPage ) {
530 // XXX say something to the user?
531 $samePage = false;
532 } elseif ( $this->mNewPage->equals( $this->mOldPage ) ) {
533 $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
534 $samePage = true;
535 } else {
536 $out->setPageTitle( $this->msg( 'difference-title-multipage',
537 $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) );
538 $out->addSubtitle( $this->msg( 'difference-multipage' ) );
539 $samePage = false;
540 }
541
542 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
543
544 if ( $samePage && $this->mNewPage && $permissionManager->quickUserCan(
545 'edit', $user, $this->mNewPage
546 ) ) {
547 if ( $this->mNewRev->isCurrent() && $permissionManager->quickUserCan(
548 'rollback', $user, $this->mNewPage
549 ) ) {
550 $rollbackLink = Linker::generateRollback( $this->mNewRev, $this->getContext(),
551 [ 'noBrackets' ] );
552 if ( $rollbackLink ) {
553 $out->preventClickjacking();
554 $rollback = "\u{00A0}\u{00A0}\u{00A0}" . $rollbackLink;
555 }
556 }
557
558 if ( $this->userCanEdit( $this->mOldRev ) &&
559 $this->userCanEdit( $this->mNewRev )
560 ) {
561 $undoLink = Html::element( 'a', [
562 'href' => $this->mNewPage->getLocalURL( [
563 'action' => 'edit',
564 'undoafter' => $this->mOldid,
565 'undo' => $this->mNewid
566 ] ),
567 'title' => Linker::titleAttrib( 'undo' ),
568 ],
569 $this->msg( 'editundo' )->text()
570 );
571 $revisionTools['mw-diff-undo'] = $undoLink;
572 }
573 }
574
575 # Make "previous revision link"
576 if ( $samePage && $this->mOldPage && $this->mOldRev->getPrevious() ) {
577 $prevlink = Linker::linkKnown(
578 $this->mOldPage,
579 $this->msg( 'previousdiff' )->escaped(),
580 [ 'id' => 'differences-prevlink' ],
581 [ 'diff' => 'prev', 'oldid' => $this->mOldid ] + $query
582 );
583 } else {
584 $prevlink = "\u{00A0}";
585 }
586
587 if ( $this->mOldRev->isMinor() ) {
588 $oldminor = ChangesList::flag( 'minor' );
589 } else {
590 $oldminor = '';
591 }
592
593 $ldel = $this->revisionDeleteLink( $this->mOldRev );
594 $oldRevisionHeader = $this->getRevisionHeader( $this->mOldRev, 'complete' );
595 $oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff', $this->getContext() );
596
597 $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' .
598 '<div id="mw-diff-otitle2">' .
599 Linker::revUserTools( $this->mOldRev, !$this->unhide ) . '</div>' .
600 '<div id="mw-diff-otitle3">' . $oldminor .
601 Linker::revComment( $this->mOldRev, !$diffOnly, !$this->unhide ) . $ldel . '</div>' .
602 '<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' .
603 '<div id="mw-diff-otitle4">' . $prevlink . '</div>';
604
605 // Allow extensions to change the $oldHeader variable
606 Hooks::run( 'DifferenceEngineOldHeader', [ $this, &$oldHeader, $prevlink, $oldminor,
607 $diffOnly, $ldel, $this->unhide ] );
608
609 if ( $this->mOldRev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
610 $deleted = true; // old revisions text is hidden
611 if ( $this->mOldRev->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
612 $suppressed = true; // also suppressed
613 }
614 }
615
616 # Check if this user can see the revisions
617 if ( !$this->mOldRev->userCan( RevisionRecord::DELETED_TEXT, $user ) ) {
618 $allowed = false;
619 }
620 }
621
622 $out->addJsConfigVars( [
623 'wgDiffOldId' => $this->mOldid,
624 'wgDiffNewId' => $this->mNewid,
625 ] );
626
627 # Make "next revision link"
628 # Skip next link on the top revision
629 if ( $samePage && $this->mNewPage && !$this->mNewRev->isCurrent() ) {
630 $nextlink = Linker::linkKnown(
631 $this->mNewPage,
632 $this->msg( 'nextdiff' )->escaped(),
633 [ 'id' => 'differences-nextlink' ],
634 [ 'diff' => 'next', 'oldid' => $this->mNewid ] + $query
635 );
636 } else {
637 $nextlink = "\u{00A0}";
638 }
639
640 if ( $this->mNewRev->isMinor() ) {
641 $newminor = ChangesList::flag( 'minor' );
642 } else {
643 $newminor = '';
644 }
645
646 # Handle RevisionDelete links...
647 $rdel = $this->revisionDeleteLink( $this->mNewRev );
648
649 # Allow extensions to define their own revision tools
650 Hooks::run( 'DiffRevisionTools',
651 [ $this->mNewRev, &$revisionTools, $this->mOldRev, $user ] );
652 $formattedRevisionTools = [];
653 // Put each one in parentheses (poor man's button)
654 foreach ( $revisionTools as $key => $tool ) {
655 $toolClass = is_string( $key ) ? $key : 'mw-diff-tool';
656 $element = Html::rawElement(
657 'span',
658 [ 'class' => $toolClass ],
659 $this->msg( 'parentheses' )->rawParams( $tool )->escaped()
660 );
661 $formattedRevisionTools[] = $element;
662 }
663 $newRevisionHeader = $this->getRevisionHeader( $this->mNewRev, 'complete' ) .
664 ' ' . implode( ' ', $formattedRevisionTools );
665 $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff', $this->getContext() );
666
667 $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' .
668 '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $this->mNewRev, !$this->unhide ) .
669 " $rollback</div>" .
670 '<div id="mw-diff-ntitle3">' . $newminor .
671 Linker::revComment( $this->mNewRev, !$diffOnly, !$this->unhide ) . $rdel . '</div>' .
672 '<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' .
673 '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
674
675 // Allow extensions to change the $newHeader variable
676 Hooks::run( 'DifferenceEngineNewHeader', [ $this, &$newHeader, $formattedRevisionTools,
677 $nextlink, $rollback, $newminor, $diffOnly, $rdel, $this->unhide ] );
678
679 if ( $this->mNewRev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
680 $deleted = true; // new revisions text is hidden
681 if ( $this->mNewRev->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
682 $suppressed = true; // also suppressed
683 }
684 }
685
686 # If the diff cannot be shown due to a deleted revision, then output
687 # the diff header and links to unhide (if available)...
688 if ( $deleted && ( !$this->unhide || !$allowed ) ) {
689 $this->showDiffStyle();
690 $multi = $this->getMultiNotice();
691 $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
692 if ( !$allowed ) {
693 $msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff';
694 # Give explanation for why revision is not visible
695 $out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
696 [ $msg ] );
697 } else {
698 # Give explanation and add a link to view the diff...
699 $query = $this->getRequest()->appendQueryValue( 'unhide', '1' );
700 $link = $this->getTitle()->getFullURL( $query );
701 $msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff';
702 $out->wrapWikiMsg(
703 "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
704 [ $msg, $link ]
705 );
706 }
707 # Otherwise, output a regular diff...
708 } else {
709 # Add deletion notice if the user is viewing deleted content
710 $notice = '';
711 if ( $deleted ) {
712 $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
713 $notice = "<div id='mw-$msg' class='mw-warning plainlinks'>\n" .
714 $this->msg( $msg )->parse() .
715 "</div>\n";
716 }
717 $this->showDiff( $oldHeader, $newHeader, $notice );
718 if ( !$diffOnly ) {
719 $this->renderNewRevision();
720 }
721 }
722 }
723
733 public function markPatrolledLink() {
734 if ( $this->mMarkPatrolledLink === null ) {
735 $linkInfo = $this->getMarkPatrolledLinkInfo();
736 // If false, there is no patrol link needed/allowed
737 if ( !$linkInfo || !$this->mNewPage ) {
738 $this->mMarkPatrolledLink = '';
739 } else {
740 $this->mMarkPatrolledLink = ' <span class="patrollink" data-mw="interface">[' .
742 $this->mNewPage,
743 $this->msg( 'markaspatrolleddiff' )->escaped(),
744 [],
745 [
746 'action' => 'markpatrolled',
747 'rcid' => $linkInfo['rcid'],
748 ]
749 ) . ']</span>';
750 // Allow extensions to change the markpatrolled link
751 Hooks::run( 'DifferenceEngineMarkPatrolledLink', [ $this,
752 &$this->mMarkPatrolledLink, $linkInfo['rcid'] ] );
753 }
754 }
756 }
757
765 protected function getMarkPatrolledLinkInfo() {
766 $user = $this->getUser();
767 $config = $this->getConfig();
768 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
769
770 // Prepare a change patrol link, if applicable
771 if (
772 // Is patrolling enabled and the user allowed to?
773 $config->get( 'UseRCPatrol' ) &&
774 $this->mNewPage &&
775 $permissionManager->quickUserCan( 'patrol', $user, $this->mNewPage ) &&
776 // Only do this if the revision isn't more than 6 hours older
777 // than the Max RC age (6h because the RC might not be cleaned out regularly)
778 RecentChange::isInRCLifespan( $this->mNewRev->getTimestamp(), 21600 )
779 ) {
780 // Look for an unpatrolled change corresponding to this diff
781 $db = wfGetDB( DB_REPLICA );
782 $change = RecentChange::newFromConds(
783 [
784 'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
785 'rc_this_oldid' => $this->mNewid,
786 'rc_patrolled' => RecentChange::PRC_UNPATROLLED
787 ],
788 __METHOD__
789 );
790
791 if ( $change && !$change->getPerformer()->equals( $user ) ) {
792 $rcid = $change->getAttribute( 'rc_id' );
793 } else {
794 // None found or the page has been created by the current user.
795 // If the user could patrol this it already would be patrolled
796 $rcid = 0;
797 }
798
799 // Allow extensions to possibly change the rcid here
800 // For example the rcid might be set to zero due to the user
801 // being the same as the performer of the change but an extension
802 // might still want to show it under certain conditions
803 Hooks::run( 'DifferenceEngineMarkPatrolledRCID', [ &$rcid, $this, $change, $user ] );
804
805 // Build the link
806 if ( $rcid ) {
807 $this->getOutput()->preventClickjacking();
808 if ( $permissionManager->userHasRight( $user, 'writeapi' ) ) {
809 $this->getOutput()->addModules( 'mediawiki.page.patrol.ajax' );
810 }
811
812 return [
813 'rcid' => $rcid,
814 ];
815 }
816 }
817
818 // No mark as patrolled link applicable
819 return false;
820 }
821
827 protected function revisionDeleteLink( $rev ) {
828 $link = Linker::getRevDeleteLink( $this->getUser(), $rev, $rev->getTitle() );
829 if ( $link !== '' ) {
830 $link = "\u{00A0}\u{00A0}\u{00A0}" . $link . ' ';
831 }
832
833 return $link;
834 }
835
841 public function renderNewRevision() {
842 if ( $this->isContentOverridden ) {
843 // The code below only works with a Revision object. We could construct a fake revision
844 // (here or in setContent), but since this does not seem needed at the moment,
845 // we'll just fail for now.
846 throw new LogicException(
847 __METHOD__
848 . ' is not supported after calling setContent(). Use setRevisions() instead.'
849 );
850 }
851
852 $out = $this->getOutput();
853 $revHeader = $this->getRevisionHeader( $this->mNewRev );
854 # Add "current version as of X" title
855 $out->addHTML( "<hr class='diff-hr' id='mw-oldid' />
856 <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
857 # Page content may be handled by a hooked call instead...
858 if ( Hooks::run( 'ArticleContentOnDiff', [ $this, $out ] ) ) {
859 $this->loadNewText();
860 if ( !$this->mNewPage ) {
861 // New revision is unsaved; bail out.
862 // TODO in theory rendering the new revision is a meaningful thing to do
863 // even if it's unsaved, but a lot of untangling is required to do it safely.
864 return;
865 }
866
867 $out->setRevisionId( $this->mNewid );
868 $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() );
869 $out->setArticleFlag( true );
870
871 if ( !Hooks::run( 'ArticleRevisionViewCustom',
872 [ $this->mNewRev->getRevisionRecord(), $this->mNewPage, $this->mOldid, $out ] )
873 ) {
874 // Handled by extension
875 // NOTE: sync with hooks called in Article::view()
876 } elseif ( !Hooks::run( 'ArticleContentViewCustom',
877 [ $this->mNewContent, $this->mNewPage, $out ], '1.32' )
878 ) {
879 // Handled by extension
880 // NOTE: sync with hooks called in Article::view()
881 } else {
882 // Normal page
883 if ( $this->getTitle()->equals( $this->mNewPage ) ) {
884 // If the Title stored in the context is the same as the one
885 // of the new revision, we can use its associated WikiPage
886 // object.
887 $wikiPage = $this->getWikiPage();
888 } else {
889 // Otherwise we need to create our own WikiPage object
890 $wikiPage = WikiPage::factory( $this->mNewPage );
891 }
892
893 $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRev );
894
895 # WikiPage::getParserOutput() should not return false, but just in case
896 if ( $parserOutput ) {
897 // Allow extensions to change parser output here
898 if ( Hooks::run( 'DifferenceEngineRenderRevisionAddParserOutput',
899 [ $this, $out, $parserOutput, $wikiPage ] )
900 ) {
901 $out->addParserOutput( $parserOutput, [
902 'enableSectionEditLinks' => $this->mNewRev->isCurrent()
903 && MediaWikiServices::getInstance()->getPermissionManager()->quickUserCan(
904 'edit',
905 $this->getUser(),
906 $this->mNewRev->getTitle()
907 )
908 ] );
909 }
910 }
911 }
912 }
913
914 // Allow extensions to optionally not show the final patrolled link
915 if ( Hooks::run( 'DifferenceEngineRenderRevisionShowFinalPatrolLink' ) ) {
916 # Add redundant patrol link on bottom...
917 $out->addHTML( $this->markPatrolledLink() );
918 }
919 }
920
927 protected function getParserOutput( WikiPage $page, Revision $rev ) {
928 if ( !$rev->getId() ) {
929 // WikiPage::getParserOutput wants a revision ID. Passing 0 will incorrectly show
930 // the current revision, so fail instead. If need be, WikiPage::getParserOutput
931 // could be made to accept a Revision or RevisionRecord instead of the id.
932 return false;
933 }
934
935 $parserOptions = $page->makeParserOptions( $this->getContext() );
936 $parserOutput = $page->getParserOutput( $parserOptions, $rev->getId() );
937
938 return $parserOutput;
939 }
940
951 public function showDiff( $otitle, $ntitle, $notice = '' ) {
952 // Allow extensions to affect the output here
953 Hooks::run( 'DifferenceEngineShowDiff', [ $this ] );
954
955 $diff = $this->getDiff( $otitle, $ntitle, $notice );
956 if ( $diff === false ) {
957 $this->showMissingRevision();
958
959 return false;
960 } else {
961 $this->showDiffStyle();
962 $this->getOutput()->addHTML( $diff );
963
964 return true;
965 }
966 }
967
971 public function showDiffStyle() {
972 if ( !$this->isSlotDiffRenderer ) {
973 $this->getOutput()->addModuleStyles( [
974 'mediawiki.interface.helpers.styles',
975 'mediawiki.diff.styles'
976 ] );
977 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
978 $slotDiffRenderer->addModules( $this->getOutput() );
979 }
980 }
981 }
982
992 public function getDiff( $otitle, $ntitle, $notice = '' ) {
993 $body = $this->getDiffBody();
994 if ( $body === false ) {
995 return false;
996 }
997
998 $multi = $this->getMultiNotice();
999 // Display a message when the diff is empty
1000 if ( $body === '' ) {
1001 $notice .= '<div class="mw-diff-empty">' .
1002 $this->msg( 'diff-empty' )->parse() .
1003 "</div>\n";
1004 }
1005
1006 return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
1007 }
1008
1014 public function getDiffBody() {
1015 $this->mCacheHit = true;
1016 // Check if the diff should be hidden from this user
1017 if ( !$this->isContentOverridden ) {
1018 if ( !$this->loadRevisionData() ) {
1019 return false;
1020 } elseif ( $this->mOldRev &&
1021 !$this->mOldRev->userCan( RevisionRecord::DELETED_TEXT, $this->getUser() )
1022 ) {
1023 return false;
1024 } elseif ( $this->mNewRev &&
1025 !$this->mNewRev->userCan( RevisionRecord::DELETED_TEXT, $this->getUser() )
1026 ) {
1027 return false;
1028 }
1029 // Short-circuit
1030 if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev &&
1031 $this->mOldRev->getId() && $this->mOldRev->getId() == $this->mNewRev->getId() )
1032 ) {
1033 if ( Hooks::run( 'DifferenceEngineShowEmptyOldContent', [ $this ] ) ) {
1034 return '';
1035 }
1036 }
1037 }
1038
1039 // Cacheable?
1040 $key = false;
1041 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1042 if ( $this->mOldid && $this->mNewid ) {
1043 // Check if subclass is still using the old way
1044 // for backwards-compatibility
1045 $key = $this->getDiffBodyCacheKey();
1046 if ( $key === null ) {
1047 $key = $cache->makeKey( ...$this->getDiffBodyCacheKeyParams() );
1048 }
1049
1050 // Try cache
1051 if ( !$this->mRefreshCache ) {
1052 $difftext = $cache->get( $key );
1053 if ( is_string( $difftext ) ) {
1054 wfIncrStats( 'diff_cache.hit' );
1055 $difftext = $this->localiseDiff( $difftext );
1056 $difftext .= "\n<!-- diff cache key $key -->\n";
1057
1058 return $difftext;
1059 }
1060 } // don't try to load but save the result
1061 }
1062 $this->mCacheHit = false;
1063
1064 // Loadtext is permission safe, this just clears out the diff
1065 if ( !$this->loadText() ) {
1066 return false;
1067 }
1068
1069 $difftext = '';
1070 // We've checked for revdelete at the beginning of this method; it's OK to ignore
1071 // read permissions here.
1072 $slotContents = $this->getSlotContents();
1073 foreach ( $this->getSlotDiffRenderers() as $role => $slotDiffRenderer ) {
1074 $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role]['old'],
1075 $slotContents[$role]['new'] );
1076 if ( $slotDiff && $role !== SlotRecord::MAIN ) {
1077 // FIXME: ask SlotRoleHandler::getSlotNameMessage
1078 $slotTitle = $role;
1079 $difftext .= $this->getSlotHeader( $slotTitle );
1080 }
1081 $difftext .= $slotDiff;
1082 }
1083
1084 // Avoid PHP 7.1 warning from passing $this by reference
1085 $diffEngine = $this;
1086
1087 // Save to cache for 7 days
1088 if ( !Hooks::run( 'AbortDiffCache', [ &$diffEngine ] ) ) {
1089 wfIncrStats( 'diff_cache.uncacheable' );
1090 } elseif ( $key !== false && $difftext !== false ) {
1091 wfIncrStats( 'diff_cache.miss' );
1092 $cache->set( $key, $difftext, 7 * 86400 );
1093 } else {
1094 wfIncrStats( 'diff_cache.uncacheable' );
1095 }
1096 // localise line numbers and title attribute text
1097 if ( $difftext !== false ) {
1098 $difftext = $this->localiseDiff( $difftext );
1099 }
1100
1101 return $difftext;
1102 }
1103
1110 public function getDiffBodyForRole( $role ) {
1111 $diffRenderers = $this->getSlotDiffRenderers();
1112 if ( !isset( $diffRenderers[$role] ) ) {
1113 return false;
1114 }
1115
1116 $slotContents = $this->getSlotContents();
1117 $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role]['old'],
1118 $slotContents[$role]['new'] );
1119 if ( !$slotDiff ) {
1120 return false;
1121 }
1122
1123 if ( $role !== SlotRecord::MAIN ) {
1124 // TODO use human-readable role name at least
1125 $slotTitle = $role;
1126 $slotDiff = $this->getSlotHeader( $slotTitle ) . $slotDiff;
1127 }
1128
1129 return $this->localiseDiff( $slotDiff );
1130 }
1131
1139 protected function getSlotHeader( $headerText ) {
1140 // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
1141 $columnCount = $this->mOldRev ? 4 : 2;
1142 $userLang = $this->getLanguage()->getHtmlCode();
1143 return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-header', 'lang' => $userLang ],
1144 Html::element( 'th', [ 'colspan' => $columnCount ], $headerText ) );
1145 }
1146
1156 protected function getDiffBodyCacheKey() {
1157 return null;
1158 }
1159
1173 protected function getDiffBodyCacheKeyParams() {
1174 if ( !$this->mOldid || !$this->mNewid ) {
1175 throw new MWException( 'mOldid and mNewid must be set to get diff cache key.' );
1176 }
1177
1178 $engine = $this->getEngine();
1179 $params = [
1180 'diff',
1181 $engine === 'php' ? false : $engine, // Back compat
1183 "old-{$this->mOldid}",
1184 "rev-{$this->mNewid}"
1185 ];
1186
1187 if ( $engine === 'wikidiff2' ) {
1188 $params[] = phpversion( 'wikidiff2' );
1189 }
1190
1191 if ( !$this->isSlotDiffRenderer ) {
1192 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1193 $params = array_merge( $params, $slotDiffRenderer->getExtraCacheKeys() );
1194 }
1195 }
1196
1197 return $params;
1198 }
1199
1207 public function getExtraCacheKeys() {
1208 // This method is called when the DifferenceEngine is used for a slot diff. We only care
1209 // about special things, not the revision IDs, which are added to the cache key by the
1210 // page-level DifferenceEngine, and which might not have a valid value for this object.
1211 $this->mOldid = 123456789;
1212 $this->mNewid = 987654321;
1213
1214 // This will repeat a bunch of unnecessary key fields for each slot. Not nice but harmless.
1215 $cacheString = $this->getDiffBodyCacheKey();
1216 if ( $cacheString ) {
1217 return [ $cacheString ];
1218 }
1219
1220 $params = $this->getDiffBodyCacheKeyParams();
1221
1222 // Try to get rid of the standard keys to keep the cache key human-readable:
1223 // call the getDiffBodyCacheKeyParams implementation of the base class, and if
1224 // the child class includes the same keys, drop them.
1225 // Uses an obscure PHP feature where static calls to non-static methods are allowed
1226 // as long as we are already in a non-static method of the same class, and the call context
1227 // ($this) will be inherited.
1228 // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
1230 if ( array_slice( $params, 0, count( $standardParams ) ) === $standardParams ) {
1231 $params = array_slice( $params, count( $standardParams ) );
1232 }
1233
1234 return $params;
1235 }
1236
1250 public function generateContentDiffBody( Content $old, Content $new ) {
1251 $slotDiffRenderer = $new->getContentHandler()->getSlotDiffRenderer( $this->getContext() );
1252 if (
1253 $slotDiffRenderer instanceof DifferenceEngineSlotDiffRenderer
1254 && $this->isSlotDiffRenderer
1255 ) {
1256 // Oops, we are just about to enter an infinite loop (the slot-level DifferenceEngine
1257 // called a DifferenceEngineSlotDiffRenderer that wraps the same DifferenceEngine class).
1258 // This will happen when a content model has no custom slot diff renderer, it does have
1259 // a custom difference engine, but that does not override this method.
1260 throw new Exception( get_class( $this ) . ': could not maintain backwards compatibility. '
1261 . 'Please use a SlotDiffRenderer.' );
1262 }
1263 return $slotDiffRenderer->getDiff( $old, $new ) . $this->getDebugString();
1264 }
1265
1278 public function generateTextDiffBody( $otext, $ntext ) {
1279 $slotDiffRenderer = ContentHandler::getForModelID( CONTENT_MODEL_TEXT )
1280 ->getSlotDiffRenderer( $this->getContext() );
1281 if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1282 // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1283 // This is too unlikely to happen to bother handling properly.
1284 throw new Exception( 'The slot diff renderer for text content should be a '
1285 . 'TextSlotDiffRenderer subclass' );
1286 }
1287 return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1288 }
1289
1296 public static function getEngine() {
1297 $diffEngine = MediaWikiServices::getInstance()->getMainConfig()
1298 ->get( 'DiffEngine' );
1299 $externalDiffEngine = MediaWikiServices::getInstance()->getMainConfig()
1300 ->get( 'ExternalDiffEngine' );
1301
1302 if ( $diffEngine === null ) {
1303 $engines = [ 'external', 'wikidiff2', 'php' ];
1304 } else {
1305 $engines = [ $diffEngine ];
1306 }
1307
1308 $failureReason = null;
1309 foreach ( $engines as $engine ) {
1310 switch ( $engine ) {
1311 case 'external':
1312 if ( is_string( $externalDiffEngine ) ) {
1313 if ( is_executable( $externalDiffEngine ) ) {
1314 return $externalDiffEngine;
1315 }
1316 $failureReason = 'ExternalDiffEngine config points to a non-executable';
1317 if ( $diffEngine === null ) {
1318 wfDebug( "$failureReason, ignoring" );
1319 }
1320 } else {
1321 $failureReason = 'ExternalDiffEngine config is set to a non-string value';
1322 if ( $diffEngine === null && $externalDiffEngine ) {
1323 wfWarn( "$failureReason, ignoring" );
1324 }
1325 }
1326 break;
1327
1328 case 'wikidiff2':
1329 if ( function_exists( 'wikidiff2_do_diff' ) ) {
1330 return 'wikidiff2';
1331 }
1332 $failureReason = 'wikidiff2 is not available';
1333 break;
1334
1335 case 'php':
1336 // Always available.
1337 return 'php';
1338
1339 default:
1340 throw new DomainException( 'Invalid value for $wgDiffEngine: ' . $engine );
1341 }
1342 }
1343 throw new UnexpectedValueException( "Cannot use diff engine '$engine': $failureReason" );
1344 }
1345
1358 protected function textDiff( $otext, $ntext ) {
1359 $slotDiffRenderer = ContentHandler::getForModelID( CONTENT_MODEL_TEXT )
1360 ->getSlotDiffRenderer( $this->getContext() );
1361 if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1362 // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1363 // This is too unlikely to happen to bother handling properly.
1364 throw new Exception( 'The slot diff renderer for text content should be a '
1365 . 'TextSlotDiffRenderer subclass' );
1366 }
1367 return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1368 }
1369
1378 protected function debug( $generator = "internal" ) {
1379 if ( !$this->enableDebugComment ) {
1380 return '';
1381 }
1382 $data = [ $generator ];
1383 if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
1384 $data[] = wfHostname();
1385 }
1386 $data[] = wfTimestamp( TS_DB );
1387
1388 return "<!-- diff generator: " .
1389 implode( " ", array_map( "htmlspecialchars", $data ) ) .
1390 " -->\n";
1391 }
1392
1393 private function getDebugString() {
1394 $engine = self::getEngine();
1395 if ( $engine === 'wikidiff2' ) {
1396 return $this->debug( 'wikidiff2' );
1397 } elseif ( $engine === 'php' ) {
1398 return $this->debug( 'native PHP' );
1399 } else {
1400 return $this->debug( "external $engine" );
1401 }
1402 }
1403
1410 private function localiseDiff( $text ) {
1411 $text = $this->localiseLineNumbers( $text );
1412 if ( $this->getEngine() === 'wikidiff2' &&
1413 version_compare( phpversion( 'wikidiff2' ), '1.5.1', '>=' )
1414 ) {
1415 $text = $this->addLocalisedTitleTooltips( $text );
1416 }
1417 return $text;
1418 }
1419
1427 public function localiseLineNumbers( $text ) {
1428 return preg_replace_callback(
1429 '/<!--LINE (\d+)-->/',
1430 [ $this, 'localiseLineNumbersCb' ],
1431 $text
1432 );
1433 }
1434
1435 public function localiseLineNumbersCb( $matches ) {
1436 if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
1437 return '';
1438 }
1439
1440 return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
1441 }
1442
1449 private function addLocalisedTitleTooltips( $text ) {
1450 return preg_replace_callback(
1451 '/class="mw-diff-movedpara-(left|right)"/',
1452 [ $this, 'addLocalisedTitleTooltipsCb' ],
1453 $text
1454 );
1455 }
1456
1461 private function addLocalisedTitleTooltipsCb( array $matches ) {
1462 $key = $matches[1] === 'right' ?
1463 'diff-paragraph-moved-toold' :
1464 'diff-paragraph-moved-tonew';
1465 return $matches[0] . ' title="' . $this->msg( $key )->escaped() . '"';
1466 }
1467
1473 public function getMultiNotice() {
1474 // The notice only make sense if we are diffing two saved revisions of the same page.
1475 if (
1476 !$this->mOldRev || !$this->mNewRev
1477 || !$this->mOldPage || !$this->mNewPage
1478 || !$this->mOldPage->equals( $this->mNewPage )
1479 ) {
1480 return '';
1481 }
1482
1483 if ( $this->mOldRev->getTimestamp() > $this->mNewRev->getTimestamp() ) {
1484 $oldRev = $this->mNewRev; // flip
1485 $newRev = $this->mOldRev; // flip
1486 } else { // normal case
1487 $oldRev = $this->mOldRev;
1488 $newRev = $this->mNewRev;
1489 }
1490
1491 // Sanity: don't show the notice if too many rows must be scanned
1492 // @todo show some special message for that case
1493 $nEdits = $this->mNewPage->countRevisionsBetween( $oldRev, $newRev, 1000 );
1494 if ( $nEdits > 0 && $nEdits <= 1000 ) {
1495 $limit = 100; // use diff-multi-manyusers if too many users
1496 $users = $this->mNewPage->getAuthorsBetween( $oldRev, $newRev, $limit );
1497 $numUsers = count( $users );
1498
1499 if ( $numUsers == 1 && $users[0] == $newRev->getUserText( RevisionRecord::RAW ) ) {
1500 $numUsers = 0; // special case to say "by the same user" instead of "by one other user"
1501 }
1502
1503 return self::intermediateEditsMsg( $nEdits, $numUsers, $limit );
1504 }
1505
1506 return '';
1507 }
1508
1518 public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) {
1519 if ( $numUsers === 0 ) {
1520 $msg = 'diff-multi-sameuser';
1521 } elseif ( $numUsers > $limit ) {
1522 $msg = 'diff-multi-manyusers';
1523 $numUsers = $limit;
1524 } else {
1525 $msg = 'diff-multi-otherusers';
1526 }
1527
1528 return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1529 }
1530
1535 private function userCanEdit( Revision $rev ) {
1536 $user = $this->getUser();
1537
1538 if ( !$rev->userCan( RevisionRecord::DELETED_TEXT, $user ) ) {
1539 return false;
1540 }
1541
1542 return true;
1543 }
1544
1554 public function getRevisionHeader( Revision $rev, $complete = '' ) {
1555 $lang = $this->getLanguage();
1556 $user = $this->getUser();
1557 $revtimestamp = $rev->getTimestamp();
1558 $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
1559 $dateofrev = $lang->userDate( $revtimestamp, $user );
1560 $timeofrev = $lang->userTime( $revtimestamp, $user );
1561
1562 $header = $this->msg(
1563 $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
1564 $timestamp,
1565 $dateofrev,
1566 $timeofrev
1567 )->escaped();
1568
1569 if ( $complete !== 'complete' ) {
1570 return $header;
1571 }
1572
1573 $title = $rev->getTitle();
1574
1576 [ 'oldid' => $rev->getId() ] );
1577
1578 if ( $this->userCanEdit( $rev ) ) {
1579 $editQuery = [ 'action' => 'edit' ];
1580 if ( !$rev->isCurrent() ) {
1581 $editQuery['oldid'] = $rev->getId();
1582 }
1583
1584 $key = MediaWikiServices::getInstance()->getPermissionManager()
1585 ->quickUserCan( 'edit', $user, $title ) ? 'editold' : 'viewsourceold';
1586 $msg = $this->msg( $key )->escaped();
1587 $editLink = $this->msg( 'parentheses' )->rawParams(
1588 Linker::linkKnown( $title, $msg, [], $editQuery ) )->escaped();
1589 $header .= ' ' . Html::rawElement(
1590 'span',
1591 [ 'class' => 'mw-diff-edit' ],
1592 $editLink
1593 );
1594 if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1595 $header = Html::rawElement(
1596 'span',
1597 [ 'class' => 'history-deleted' ],
1598 $header
1599 );
1600 }
1601 } else {
1602 $header = Html::rawElement( 'span', [ 'class' => 'history-deleted' ], $header );
1603 }
1604
1605 return $header;
1606 }
1607
1620 public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
1621 // shared.css sets diff in interface language/dir, but the actual content
1622 // is often in a different language, mostly the page content language/dir
1623 $header = Html::openElement( 'table', [
1624 'class' => [ 'diff', 'diff-contentalign-' . $this->getDiffLang()->alignStart() ],
1625 'data-mw' => 'interface',
1626 ] );
1627 $userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
1628
1629 if ( !$diff && !$otitle ) {
1630 $header .= "
1631 <tr class=\"diff-title\" lang=\"{$userLang}\">
1632 <td class=\"diff-ntitle\">{$ntitle}</td>
1633 </tr>";
1634 $multiColspan = 1;
1635 } else {
1636 if ( $diff ) { // Safari/Chrome show broken output if cols not used
1637 $header .= "
1638 <col class=\"diff-marker\" />
1639 <col class=\"diff-content\" />
1640 <col class=\"diff-marker\" />
1641 <col class=\"diff-content\" />";
1642 $colspan = 2;
1643 $multiColspan = 4;
1644 } else {
1645 $colspan = 1;
1646 $multiColspan = 2;
1647 }
1648 if ( $otitle || $ntitle ) {
1649 $header .= "
1650 <tr class=\"diff-title\" lang=\"{$userLang}\">
1651 <td colspan=\"$colspan\" class=\"diff-otitle\">{$otitle}</td>
1652 <td colspan=\"$colspan\" class=\"diff-ntitle\">{$ntitle}</td>
1653 </tr>";
1654 }
1655 }
1656
1657 if ( $multi != '' ) {
1658 $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1659 "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
1660 }
1661 if ( $notice != '' ) {
1662 $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1663 "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
1664 }
1665
1666 return $header . $diff . "</table>";
1667 }
1668
1676 public function setContent( Content $oldContent, Content $newContent ) {
1677 $this->mOldContent = $oldContent;
1678 $this->mNewContent = $newContent;
1679
1680 $this->mTextLoaded = 2;
1681 $this->mRevisionsLoaded = true;
1682 $this->isContentOverridden = true;
1683 $this->slotDiffRenderers = null;
1684 }
1685
1691 public function setRevisions(
1692 RevisionRecord $oldRevision = null, RevisionRecord $newRevision
1693 ) {
1694 if ( $oldRevision ) {
1695 $this->mOldRev = new Revision( $oldRevision );
1696 $this->mOldid = $oldRevision->getId();
1697 $this->mOldPage = Title::newFromLinkTarget( $oldRevision->getPageAsLinkTarget() );
1698 // This method is meant for edit diffs and such so there is no reason to provide a
1699 // revision that's not readable to the user, but check it just in case.
1700 $this->mOldContent = $oldRevision->getContent( SlotRecord::MAIN,
1701 RevisionRecord::FOR_THIS_USER, $this->getUser() );
1702 } else {
1703 $this->mOldPage = null;
1704 $this->mOldRev = $this->mOldid = false;
1705 }
1706 $this->mNewRev = new Revision( $newRevision );
1707 $this->mNewid = $newRevision->getId();
1708 $this->mNewPage = Title::newFromLinkTarget( $newRevision->getPageAsLinkTarget() );
1709 $this->mNewContent = $newRevision->getContent( SlotRecord::MAIN,
1710 RevisionRecord::FOR_THIS_USER, $this->getUser() );
1711
1712 $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded = true;
1713 $this->mTextLoaded = $oldRevision ? 2 : 1;
1714 $this->isContentOverridden = false;
1715 $this->slotDiffRenderers = null;
1716 }
1717
1724 public function setTextLanguage( Language $lang ) {
1725 $this->mDiffLang = $lang;
1726 }
1727
1739 public function mapDiffPrevNext( $old, $new ) {
1740 $rl = MediaWikiServices::getInstance()->getRevisionLookup();
1741 if ( $new === 'prev' ) {
1742 // Show diff between revision $old and the previous one. Get previous one from DB.
1743 $newid = intval( $old );
1744 $oldid = false;
1745 $newRev = $rl->getRevisionById( $newid );
1746 if ( $newRev ) {
1747 $oldRev = $rl->getPreviousRevision( $newRev );
1748 if ( $oldRev ) {
1749 $oldid = $oldRev->getId();
1750 }
1751 }
1752 } elseif ( $new === 'next' ) {
1753 // Show diff between revision $old and the next one. Get next one from DB.
1754 $oldid = intval( $old );
1755 $newid = false;
1756 $oldRev = $rl->getRevisionById( $oldid );
1757 if ( $oldRev ) {
1758 $newRev = $rl->getNextRevision( $oldRev );
1759 if ( $newRev ) {
1760 $newid = $newRev->getId();
1761 }
1762 }
1763 } else {
1764 $oldid = intval( $old );
1765 $newid = intval( $new );
1766 }
1767
1768 return [ $oldid, $newid ];
1769 }
1770
1774 private function loadRevisionIds() {
1775 if ( $this->mRevisionsIdsLoaded ) {
1776 return;
1777 }
1778
1779 $this->mRevisionsIdsLoaded = true;
1780
1781 $old = $this->mOldid;
1782 $new = $this->mNewid;
1783
1784 list( $this->mOldid, $this->mNewid ) = self::mapDiffPrevNext( $old, $new );
1785 if ( $new === 'next' && $this->mNewid === false ) {
1786 # if no result, NewId points to the newest old revision. The only newer
1787 # revision is cur, which is "0".
1788 $this->mNewid = 0;
1789 }
1790
1791 Hooks::run(
1792 'NewDifferenceEngine',
1793 [ $this->getTitle(), &$this->mOldid, &$this->mNewid, $old, $new ]
1794 );
1795 }
1796
1810 public function loadRevisionData() {
1811 if ( $this->mRevisionsLoaded ) {
1812 return $this->isContentOverridden || ( $this->mOldRev !== null && $this->mNewRev !== null );
1813 }
1814
1815 // Whether it succeeds or fails, we don't want to try again
1816 $this->mRevisionsLoaded = true;
1817
1818 $this->loadRevisionIds();
1819
1820 // Load the new revision object
1821 if ( $this->mNewid ) {
1822 $this->mNewRev = Revision::newFromId( $this->mNewid );
1823 } else {
1824 $this->mNewRev = Revision::newFromTitle(
1825 $this->getTitle(),
1826 false,
1827 Revision::READ_NORMAL
1828 );
1829 }
1830
1831 if ( !$this->mNewRev instanceof Revision ) {
1832 return false;
1833 }
1834
1835 // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
1836 $this->mNewid = $this->mNewRev->getId();
1837 if ( $this->mNewid ) {
1838 $this->mNewPage = $this->mNewRev->getTitle();
1839 } else {
1840 $this->mNewPage = null;
1841 }
1842
1843 // Load the old revision object
1844 $this->mOldRev = false;
1845 if ( $this->mOldid ) {
1846 $this->mOldRev = Revision::newFromId( $this->mOldid );
1847 } elseif ( $this->mOldid === 0 ) {
1848 $rev = $this->mNewRev->getPrevious();
1849 if ( $rev ) {
1850 $this->mOldid = $rev->getId();
1851 $this->mOldRev = $rev;
1852 } else {
1853 // No previous revision; mark to show as first-version only.
1854 $this->mOldid = false;
1855 $this->mOldRev = false;
1856 }
1857 } /* elseif ( $this->mOldid === false ) leave mOldRev false; */
1858
1859 if ( is_null( $this->mOldRev ) ) {
1860 return false;
1861 }
1862
1863 if ( $this->mOldRev && $this->mOldRev->getId() ) {
1864 $this->mOldPage = $this->mOldRev->getTitle();
1865 } else {
1866 $this->mOldPage = null;
1867 }
1868
1869 // Load tags information for both revisions
1870 $dbr = wfGetDB( DB_REPLICA );
1871 $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
1872 if ( $this->mOldid !== false ) {
1873 $tagIds = $dbr->selectFieldValues(
1874 'change_tag',
1875 'ct_tag_id',
1876 [ 'ct_rev_id' => $this->mOldid ],
1877 __METHOD__
1878 );
1879 $tags = [];
1880 foreach ( $tagIds as $tagId ) {
1881 try {
1882 $tags[] = $changeTagDefStore->getName( (int)$tagId );
1883 } catch ( NameTableAccessException $exception ) {
1884 continue;
1885 }
1886 }
1887 $this->mOldTags = implode( ',', $tags );
1888 } else {
1889 $this->mOldTags = false;
1890 }
1891
1892 $tagIds = $dbr->selectFieldValues(
1893 'change_tag',
1894 'ct_tag_id',
1895 [ 'ct_rev_id' => $this->mNewid ],
1896 __METHOD__
1897 );
1898 $tags = [];
1899 foreach ( $tagIds as $tagId ) {
1900 try {
1901 $tags[] = $changeTagDefStore->getName( (int)$tagId );
1902 } catch ( NameTableAccessException $exception ) {
1903 continue;
1904 }
1905 }
1906 $this->mNewTags = implode( ',', $tags );
1907
1908 return true;
1909 }
1910
1919 public function loadText() {
1920 if ( $this->mTextLoaded == 2 ) {
1921 return $this->loadRevisionData() && ( $this->mOldRev === false || $this->mOldContent )
1922 && $this->mNewContent;
1923 }
1924
1925 // Whether it succeeds or fails, we don't want to try again
1926 $this->mTextLoaded = 2;
1927
1928 if ( !$this->loadRevisionData() ) {
1929 return false;
1930 }
1931
1932 if ( $this->mOldRev ) {
1933 $this->mOldContent = $this->mOldRev->getContent(
1934 RevisionRecord::FOR_THIS_USER, $this->getUser()
1935 );
1936 if ( $this->mOldContent === null ) {
1937 return false;
1938 }
1939 }
1940
1941 $this->mNewContent = $this->mNewRev->getContent(
1942 RevisionRecord::FOR_THIS_USER, $this->getUser()
1943 );
1944 Hooks::run( 'DifferenceEngineLoadTextAfterNewContentIsLoaded', [ $this ] );
1945 if ( $this->mNewContent === null ) {
1946 return false;
1947 }
1948
1949 return true;
1950 }
1951
1957 public function loadNewText() {
1958 if ( $this->mTextLoaded >= 1 ) {
1959 return $this->loadRevisionData();
1960 }
1961
1962 $this->mTextLoaded = 1;
1963
1964 if ( !$this->loadRevisionData() ) {
1965 return false;
1966 }
1967
1968 $this->mNewContent = $this->mNewRev->getContent(
1969 RevisionRecord::FOR_THIS_USER, $this->getUser()
1970 );
1971
1972 Hooks::run( 'DifferenceEngineAfterLoadNewText', [ $this ] );
1973
1974 return true;
1975 }
1976
1977}
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()
Get host name of the current machine, for use in error reporting.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
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,... $params)
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 DiffEngine config and get a sane, usable engine.
loadRevisionIds()
Load revision IDs.
bool $mRevisionsLoaded
Have the revisions been loaded.
Content null $mOldContent
getNewRevision()
Get the right side of the diff.
showDiff( $otitle, $ntitle, $notice='')
Get the diff text, send it to the OutputPage object Returns false if the diff could not be generated,...
localiseLineNumbers( $text)
Replace line numbers with the text in the user's language.
getSlotContents()
Get the old and new content objects for all slots.
string $mMarkPatrolledLink
Link to action=markpatrolled.
localiseLineNumbersCb( $matches)
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)
userCanEdit(Revision $rev)
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:37
static generateRollback( $rev, IContextSource $context=null, $options=[ 'verify'])
Generate a rollback link for a given revision.
Definition Linker.php:1811
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:2026
static getRevDeleteLink(User $user, Revision $rev, LinkTarget $title)
Get a revision-deletion link, or disabled link, or nothing, depending on user permissions & the setti...
Definition Linker.php:2110
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, $useParentheses=true)
Wrap and format the given revision's comment block, if the current user is allowed to view it.
Definition Linker.php:1577
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:1124
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.
getTitle()
Returns the title of the page associated with this entry.
Definition Revision.php:559
getId()
Get revision ID.
Definition Revision.php:442
getTimestamp()
Definition Revision.php:798
userCan( $field, User $user=null)
Determine if the current user is allowed to view a particular field of this revision,...
isDeleted( $field)
Definition Revision.php:692
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:42
Class representing a MediaWiki article and history.
Definition WikiPage.php:47
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
getParserOutput(ParserOptions $parserOptions, $oldid=null, $forceParse=false)
Get a ParserOutput for the given ParserOptions and revision ID.
const NS_SPECIAL
Definition Defines.php:58
const CONTENT_MODEL_TEXT
Definition Defines.php:227
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
const DB_REPLICA
Definition defines.php:25
$content
Definition router.php:78
if(!isset( $args[0])) $lang
$header