Completed
Branch master (726f70)
by
unknown
25:29
created

DifferenceEngine::markPatrolledLink()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 25
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 17
nc 3
nop 0
dl 0
loc 25
rs 8.8571
c 0
b 0
f 0
1
<?php
2
/**
3
 * User interface for the difference engine.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @ingroup DifferenceEngine
22
 */
23
24
// Deprecated, use class constant instead
25
define( 'MW_DIFF_VERSION', '1.11a' );
26
27
/**
28
 * @todo document
29
 * @ingroup DifferenceEngine
30
 */
31
class DifferenceEngine extends ContextSource {
32
	/**
33
	 * Constant to indicate diff cache compatibility.
34
	 * Bump this when changing the diff formatting in a way that
35
	 * fixes important bugs or such to force cached diff views to
36
	 * clear.
37
	 */
38
	const DIFF_VERSION = MW_DIFF_VERSION;
39
40
	/** @var int */
41
	public $mOldid;
42
43
	/** @var int */
44
	public $mNewid;
45
46
	private $mOldTags;
47
	private $mNewTags;
48
49
	/** @var Content */
50
	public $mOldContent;
51
52
	/** @var Content */
53
	public $mNewContent;
54
55
	/** @var Language */
56
	protected $mDiffLang;
57
58
	/** @var Title */
59
	public $mOldPage;
60
61
	/** @var Title */
62
	public $mNewPage;
63
64
	/** @var Revision */
65
	public $mOldRev;
66
67
	/** @var Revision */
68
	public $mNewRev;
69
70
	/** @var bool Have the revisions IDs been loaded */
71
	private $mRevisionsIdsLoaded = false;
72
73
	/** @var bool Have the revisions been loaded */
74
	public $mRevisionsLoaded = false;
75
76
	/** @var int How many text blobs have been loaded, 0, 1 or 2? */
77
	public $mTextLoaded = 0;
78
79
	/** @var bool Was the diff fetched from cache? */
80
	public $mCacheHit = false;
81
82
	/**
83
	 * Set this to true to add debug info to the HTML output.
84
	 * Warning: this may cause RSS readers to spuriously mark articles as "new"
85
	 * (bug 20601)
86
	 */
87
	public $enableDebugComment = false;
88
89
	/** @var bool If true, line X is not displayed when X is 1, for example
90
	 *    to increase readability and conserve space with many small diffs.
91
	 */
92
	protected $mReducedLineNumbers = false;
93
94
	/** @var string Link to action=markpatrolled */
95
	protected $mMarkPatrolledLink = null;
96
97
	/** @var bool Show rev_deleted content if allowed */
98
	protected $unhide = false;
99
100
	/** @var bool Refresh the diff cache */
101
	protected $mRefreshCache = false;
102
103
	/**#@-*/
104
105
	/**
106
	 * Constructor
107
	 * @param IContextSource $context Context to use, anything else will be ignored
108
	 * @param int $old Old ID we want to show and diff with.
109
	 * @param string|int $new Either revision ID or 'prev' or 'next'. Default: 0.
110
	 * @param int $rcid Deprecated, no longer used!
111
	 * @param bool $refreshCache If set, refreshes the diff cache
112
	 * @param bool $unhide If set, allow viewing deleted revs
113
	 */
114
	public function __construct( $context = null, $old = 0, $new = 0, $rcid = 0,
115
		$refreshCache = false, $unhide = false
116
	) {
117
		if ( $context instanceof IContextSource ) {
118
			$this->setContext( $context );
119
		}
120
121
		wfDebug( "DifferenceEngine old '$old' new '$new' rcid '$rcid'\n" );
122
123
		$this->mOldid = $old;
124
		$this->mNewid = $new;
125
		$this->mRefreshCache = $refreshCache;
126
		$this->unhide = $unhide;
127
	}
128
129
	/**
130
	 * @param bool $value
131
	 */
132
	public function setReducedLineNumbers( $value = true ) {
133
		$this->mReducedLineNumbers = $value;
134
	}
135
136
	/**
137
	 * @return Language
138
	 */
139
	public function getDiffLang() {
140
		if ( $this->mDiffLang === null ) {
141
			# Default language in which the diff text is written.
142
			$this->mDiffLang = $this->getTitle()->getPageLanguage();
143
		}
144
145
		return $this->mDiffLang;
146
	}
147
148
	/**
149
	 * @return bool
150
	 */
151
	public function wasCacheHit() {
152
		return $this->mCacheHit;
153
	}
154
155
	/**
156
	 * @return int
157
	 */
158
	public function getOldid() {
159
		$this->loadRevisionIds();
160
161
		return $this->mOldid;
162
	}
163
164
	/**
165
	 * @return bool|int
166
	 */
167
	public function getNewid() {
168
		$this->loadRevisionIds();
169
170
		return $this->mNewid;
171
	}
172
173
	/**
174
	 * Look up a special:Undelete link to the given deleted revision id,
175
	 * as a workaround for being unable to load deleted diffs in currently.
176
	 *
177
	 * @param int $id Revision ID
178
	 *
179
	 * @return mixed URL or false
180
	 */
181
	public function deletedLink( $id ) {
182
		if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) {
183
			$dbr = wfGetDB( DB_SLAVE );
184
			$row = $dbr->selectRow( 'archive', '*',
185
				[ 'ar_rev_id' => $id ],
186
				__METHOD__ );
187
			if ( $row ) {
188
				$rev = Revision::newFromArchiveRow( $row );
0 ignored issues
show
Bug introduced by
It seems like $row defined by $dbr->selectRow('archive...d' => $id), __METHOD__) on line 184 can also be of type boolean; however, Revision::newFromArchiveRow() does only seem to accept object, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
189
				$title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
190
191
				return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( [
192
					'target' => $title->getPrefixedText(),
193
					'timestamp' => $rev->getTimestamp()
194
				] );
195
			}
196
		}
197
198
		return false;
199
	}
200
201
	/**
202
	 * Build a wikitext link toward a deleted revision, if viewable.
203
	 *
204
	 * @param int $id Revision ID
205
	 *
206
	 * @return string Wikitext fragment
207
	 */
208
	public function deletedIdMarker( $id ) {
209
		$link = $this->deletedLink( $id );
210
		if ( $link ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $link of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
211
			return "[$link $id]";
212
		} else {
213
			return $id;
214
		}
215
	}
216
217
	private function showMissingRevision() {
218
		$out = $this->getOutput();
219
220
		$missing = [];
221
		if ( $this->mOldRev === null ||
222
			( $this->mOldRev && $this->mOldContent === null )
223
		) {
224
			$missing[] = $this->deletedIdMarker( $this->mOldid );
225
		}
226
		if ( $this->mNewRev === null ||
227
			( $this->mNewRev && $this->mNewContent === null )
228
		) {
229
			$missing[] = $this->deletedIdMarker( $this->mNewid );
230
		}
231
232
		$out->setPageTitle( $this->msg( 'errorpagetitle' ) );
233
		$msg = $this->msg( 'difference-missing-revision' )
234
			->params( $this->getLanguage()->listToText( $missing ) )
235
			->numParams( count( $missing ) )
236
			->parseAsBlock();
237
		$out->addHTML( $msg );
238
	}
239
240
	public function showDiffPage( $diffOnly = false ) {
241
		# Allow frames except in certain special cases
242
		$out = $this->getOutput();
243
		$out->allowClickjacking();
244
		$out->setRobotPolicy( 'noindex,nofollow' );
245
246
		// Allow extensions to add any extra output here
247
		Hooks::run( 'DifferenceEngineShowDiffPage', [ $out ] );
248
249
		if ( !$this->loadRevisionData() ) {
250
			$this->showMissingRevision();
251
252
			return;
253
		}
254
255
		$user = $this->getUser();
256
		$permErrors = $this->mNewPage->getUserPermissionsErrors( 'read', $user );
257
		if ( $this->mOldPage ) { # mOldPage might not be set, see below.
258
			$permErrors = wfMergeErrorArrays( $permErrors,
259
				$this->mOldPage->getUserPermissionsErrors( 'read', $user ) );
260
		}
261
		if ( count( $permErrors ) ) {
262
			throw new PermissionsError( 'read', $permErrors );
263
		}
264
265
		$rollback = '';
266
267
		$query = [];
268
		# Carry over 'diffonly' param via navigation links
269
		if ( $diffOnly != $user->getBoolOption( 'diffonly' ) ) {
270
			$query['diffonly'] = $diffOnly;
271
		}
272
		# Cascade unhide param in links for easy deletion browsing
273
		if ( $this->unhide ) {
274
			$query['unhide'] = 1;
275
		}
276
277
		# Check if one of the revisions is deleted/suppressed
278
		$deleted = $suppressed = false;
279
		$allowed = $this->mNewRev->userCan( Revision::DELETED_TEXT, $user );
280
281
		$revisionTools = [];
282
283
		# mOldRev is false if the difference engine is called with a "vague" query for
284
		# a diff between a version V and its previous version V' AND the version V
285
		# is the first version of that article. In that case, V' does not exist.
286
		if ( $this->mOldRev === false ) {
287
			$out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
288
			$samePage = true;
289
			$oldHeader = '';
290
			// Allow extensions to change the $oldHeader variable
291
			Hooks::run( 'DifferenceEngineOldHeaderNoOldRev', [ &$oldHeader ] );
292
		} else {
293
			Hooks::run( 'DiffViewHeader', [ $this, $this->mOldRev, $this->mNewRev ] );
294
295
			if ( $this->mNewPage->equals( $this->mOldPage ) ) {
296
				$out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
297
				$samePage = true;
298
			} else {
299
				$out->setPageTitle( $this->msg( 'difference-title-multipage',
300
					$this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) );
301
				$out->addSubtitle( $this->msg( 'difference-multipage' ) );
302
				$samePage = false;
303
			}
304
305
			if ( $samePage && $this->mNewPage->quickUserCan( 'edit', $user ) ) {
306
				if ( $this->mNewRev->isCurrent() && $this->mNewPage->userCan( 'rollback', $user ) ) {
307
					$rollbackLink = Linker::generateRollback( $this->mNewRev, $this->getContext() );
308
					if ( $rollbackLink ) {
309
						$out->preventClickjacking();
310
						$rollback = '&#160;&#160;&#160;' . $rollbackLink;
311
					}
312
				}
313
314
				if ( !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) &&
315
					!$this->mNewRev->isDeleted( Revision::DELETED_TEXT )
316
				) {
317
					$undoLink = Html::element( 'a', [
318
							'href' => $this->mNewPage->getLocalURL( [
319
								'action' => 'edit',
320
								'undoafter' => $this->mOldid,
321
								'undo' => $this->mNewid
322
							] ),
323
							'title' => Linker::titleAttrib( 'undo' ),
324
						],
325
						$this->msg( 'editundo' )->text()
326
					);
327
					$revisionTools['mw-diff-undo'] = $undoLink;
328
				}
329
			}
330
331
			# Make "previous revision link"
332 View Code Duplication
			if ( $samePage && $this->mOldRev->getPrevious() ) {
333
				$prevlink = Linker::linkKnown(
334
					$this->mOldPage,
335
					$this->msg( 'previousdiff' )->escaped(),
336
					[ 'id' => 'differences-prevlink' ],
337
					[ 'diff' => 'prev', 'oldid' => $this->mOldid ] + $query
338
				);
339
			} else {
340
				$prevlink = '&#160;';
341
			}
342
343
			if ( $this->mOldRev->isMinor() ) {
344
				$oldminor = ChangesList::flag( 'minor' );
345
			} else {
346
				$oldminor = '';
347
			}
348
349
			$ldel = $this->revisionDeleteLink( $this->mOldRev );
350
			$oldRevisionHeader = $this->getRevisionHeader( $this->mOldRev, 'complete' );
351
			$oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff', $this->getContext() );
352
353
			$oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' .
354
				'<div id="mw-diff-otitle2">' .
355
				Linker::revUserTools( $this->mOldRev, !$this->unhide ) . '</div>' .
356
				'<div id="mw-diff-otitle3">' . $oldminor .
357
				Linker::revComment( $this->mOldRev, !$diffOnly, !$this->unhide ) . $ldel . '</div>' .
358
				'<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' .
359
				'<div id="mw-diff-otitle4">' . $prevlink . '</div>';
360
361
			// Allow extensions to change the $oldHeader variable
362
			Hooks::run( 'DifferenceEngineOldHeader', [ $this, &$oldHeader, $prevlink, $oldminor,
363
				$diffOnly, $ldel, $this->unhide ] );
364
365 View Code Duplication
			if ( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) {
366
				$deleted = true; // old revisions text is hidden
367
				if ( $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
368
					$suppressed = true; // also suppressed
369
				}
370
			}
371
372
			# Check if this user can see the revisions
373
			if ( !$this->mOldRev->userCan( Revision::DELETED_TEXT, $user ) ) {
374
				$allowed = false;
375
			}
376
		}
377
378
		# Make "next revision link"
379
		# Skip next link on the top revision
380 View Code Duplication
		if ( $samePage && !$this->mNewRev->isCurrent() ) {
381
			$nextlink = Linker::linkKnown(
382
				$this->mNewPage,
383
				$this->msg( 'nextdiff' )->escaped(),
384
				[ 'id' => 'differences-nextlink' ],
385
				[ 'diff' => 'next', 'oldid' => $this->mNewid ] + $query
386
			);
387
		} else {
388
			$nextlink = '&#160;';
389
		}
390
391
		if ( $this->mNewRev->isMinor() ) {
392
			$newminor = ChangesList::flag( 'minor' );
393
		} else {
394
			$newminor = '';
395
		}
396
397
		# Handle RevisionDelete links...
398
		$rdel = $this->revisionDeleteLink( $this->mNewRev );
399
400
		# Allow extensions to define their own revision tools
401
		Hooks::run( 'DiffRevisionTools',
402
			[ $this->mNewRev, &$revisionTools, $this->mOldRev, $user ] );
403
		$formattedRevisionTools = [];
404
		// Put each one in parentheses (poor man's button)
405
		foreach ( $revisionTools as $key => $tool ) {
406
			$toolClass = is_string( $key ) ? $key : 'mw-diff-tool';
407
			$element = Html::rawElement(
408
				'span',
409
				[ 'class' => $toolClass ],
410
				$this->msg( 'parentheses' )->rawParams( $tool )->escaped()
411
			);
412
			$formattedRevisionTools[] = $element;
413
		}
414
		$newRevisionHeader = $this->getRevisionHeader( $this->mNewRev, 'complete' ) .
415
			' ' . implode( ' ', $formattedRevisionTools );
416
		$newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff', $this->getContext() );
417
418
		$newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' .
419
			'<div id="mw-diff-ntitle2">' . Linker::revUserTools( $this->mNewRev, !$this->unhide ) .
420
			" $rollback</div>" .
421
			'<div id="mw-diff-ntitle3">' . $newminor .
422
			Linker::revComment( $this->mNewRev, !$diffOnly, !$this->unhide ) . $rdel . '</div>' .
423
			'<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' .
424
			'<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
425
426
		// Allow extensions to change the $newHeader variable
427
		Hooks::run( 'DifferenceEngineNewHeader', [ $this, &$newHeader, $formattedRevisionTools,
428
			$nextlink, $rollback, $newminor, $diffOnly, $rdel, $this->unhide ] );
429
430 View Code Duplication
		if ( $this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
431
			$deleted = true; // new revisions text is hidden
432
			if ( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
433
				$suppressed = true; // also suppressed
434
			}
435
		}
436
437
		# If the diff cannot be shown due to a deleted revision, then output
438
		# the diff header and links to unhide (if available)...
439
		if ( $deleted && ( !$this->unhide || !$allowed ) ) {
440
			$this->showDiffStyle();
441
			$multi = $this->getMultiNotice();
442
			$out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
443
			if ( !$allowed ) {
444
				$msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff';
445
				# Give explanation for why revision is not visible
446
				$out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
447
					[ $msg ] );
448
			} else {
449
				# Give explanation and add a link to view the diff...
450
				$query = $this->getRequest()->appendQueryValue( 'unhide', '1' );
451
				$link = $this->getTitle()->getFullURL( $query );
452
				$msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff';
453
				$out->wrapWikiMsg(
454
					"<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
455
					[ $msg, $link ]
456
				);
457
			}
458
		# Otherwise, output a regular diff...
459
		} else {
460
			# Add deletion notice if the user is viewing deleted content
461
			$notice = '';
462
			if ( $deleted ) {
463
				$msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
464
				$notice = "<div id='mw-$msg' class='mw-warning plainlinks'>\n" .
465
					$this->msg( $msg )->parse() .
466
					"</div>\n";
467
			}
468
			$this->showDiff( $oldHeader, $newHeader, $notice );
469
			if ( !$diffOnly ) {
470
				$this->renderNewRevision();
471
			}
472
		}
473
	}
474
475
	/**
476
	 * Build a link to mark a change as patrolled.
477
	 *
478
	 * Returns empty string if there's either no revision to patrol or the user is not allowed to.
479
	 * Side effect: When the patrol link is build, this method will call
480
	 * OutputPage::preventClickjacking() and load mediawiki.page.patrol.ajax.
481
	 *
482
	 * @return string HTML or empty string
483
	 */
484
	protected function markPatrolledLink() {
485
		if ( $this->mMarkPatrolledLink === null ) {
486
			$linkInfo = $this->getMarkPatrolledLinkInfo();
487
			// If false, there is no patrol link needed/allowed
488
			if ( !$linkInfo ) {
489
				$this->mMarkPatrolledLink = '';
490
			} else {
491
				$this->mMarkPatrolledLink = ' <span class="patrollink" data-mw="interface">[' .
492
					Linker::linkKnown(
493
						$this->mNewPage,
494
						$this->msg( 'markaspatrolleddiff' )->escaped(),
495
						[],
496
						[
497
							'action' => 'markpatrolled',
498
							'rcid' => $linkInfo['rcid'],
499
							'token' => $linkInfo['token'],
500
						]
501
					) . ']</span>';
502
				// Allow extensions to change the markpatrolled link
503
				Hooks::run( 'DifferenceEngineMarkPatrolledLink', [ $this,
504
					&$this->mMarkPatrolledLink, $linkInfo['rcid'], $linkInfo['token'] ] );
505
			}
506
		}
507
		return $this->mMarkPatrolledLink;
508
	}
509
510
	/**
511
	 * Returns an array of meta data needed to build a "mark as patrolled" link and
512
	 * adds the mediawiki.page.patrol.ajax to the output.
513
	 *
514
	 * @return array|false An array of meta data for a patrol link (rcid & token)
515
	 *  or false if no link is needed
516
	 */
517
	protected function getMarkPatrolledLinkInfo() {
518
		global $wgUseRCPatrol, $wgEnableAPI, $wgEnableWriteAPI;
519
520
		$user = $this->getUser();
521
522
		// Prepare a change patrol link, if applicable
523
		if (
524
			// Is patrolling enabled and the user allowed to?
525
			$wgUseRCPatrol && $this->mNewPage->quickUserCan( 'patrol', $user ) &&
526
			// Only do this if the revision isn't more than 6 hours older
527
			// than the Max RC age (6h because the RC might not be cleaned out regularly)
528
			RecentChange::isInRCLifespan( $this->mNewRev->getTimestamp(), 21600 )
529
		) {
530
			// Look for an unpatrolled change corresponding to this diff
531
			$db = wfGetDB( DB_SLAVE );
532
			$change = RecentChange::newFromConds(
533
				[
534
					'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
535
					'rc_this_oldid' => $this->mNewid,
536
					'rc_patrolled' => 0
537
				],
538
				__METHOD__
539
			);
540
541
			if ( $change && !$change->getPerformer()->equals( $user ) ) {
542
				$rcid = $change->getAttribute( 'rc_id' );
543
			} else {
544
				// None found or the page has been created by the current user.
545
				// If the user could patrol this it already would be patrolled
546
				$rcid = 0;
547
			}
548
549
			// Allow extensions to possibly change the rcid here
550
			// For example the rcid might be set to zero due to the user
551
			// being the same as the performer of the change but an extension
552
			// might still want to show it under certain conditions
553
			Hooks::run( 'DifferenceEngineMarkPatrolledRCID', [ &$rcid, $this, $change, $user ] );
554
555
			// Build the link
556
			if ( $rcid ) {
557
				$this->getOutput()->preventClickjacking();
558
				$this->getOutput()->addModuleStyles( 'mediawiki.page.patrol' );
559
				if ( $wgEnableAPI && $wgEnableWriteAPI
560
					&& $user->isAllowed( 'writeapi' )
561
				) {
562
					$this->getOutput()->addModules( 'mediawiki.page.patrol.ajax' );
563
				}
564
565
				$token = $user->getEditToken( $rcid );
566
				return [
567
					'rcid' => $rcid,
568
					'token' => $token,
569
				];
570
			}
571
		}
572
573
		// No mark as patrolled link applicable
574
		return false;
575
	}
576
577
	/**
578
	 * @param Revision $rev
579
	 *
580
	 * @return string
581
	 */
582
	protected function revisionDeleteLink( $rev ) {
583
		$link = Linker::getRevDeleteLink( $this->getUser(), $rev, $rev->getTitle() );
0 ignored issues
show
Bug introduced by
It seems like $rev->getTitle() can be null; however, getRevDeleteLink() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
584
		if ( $link !== '' ) {
585
			$link = '&#160;&#160;&#160;' . $link . ' ';
586
		}
587
588
		return $link;
589
	}
590
591
	/**
592
	 * Show the new revision of the page.
593
	 */
594
	public function renderNewRevision() {
595
		$out = $this->getOutput();
596
		$revHeader = $this->getRevisionHeader( $this->mNewRev );
597
		# Add "current version as of X" title
598
		$out->addHTML( "<hr class='diff-hr' id='mw-oldid' />
599
		<h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
600
		# Page content may be handled by a hooked call instead...
601
		# @codingStandardsIgnoreStart Ignoring long lines.
602
		if ( Hooks::run( 'ArticleContentOnDiff', [ $this, $out ] ) ) {
603
			$this->loadNewText();
604
			$out->setRevisionId( $this->mNewid );
605
			$out->setRevisionTimestamp( $this->mNewRev->getTimestamp() );
0 ignored issues
show
Security Bug introduced by
It seems like $this->mNewRev->getTimestamp() targeting Revision::getTimestamp() can also be of type false; however, OutputPage::setRevisionTimestamp() does only seem to accept string|null, did you maybe forget to handle an error condition?
Loading history...
606
			$out->setArticleFlag( true );
607
608
			// NOTE: only needed for B/C: custom rendering of JS/CSS via hook
609
			if ( $this->mNewPage->isCssJsSubpage() || $this->mNewPage->isCssOrJsPage() ) {
610
				// This needs to be synchronised with Article::showCssOrJsPage(), which sucks
611
				// Give hooks a chance to customise the output
612
				// @todo standardize this crap into one function
613
				if ( ContentHandler::runLegacyHooks( 'ShowRawCssJs', [ $this->mNewContent, $this->mNewPage, $out ] ) ) {
614
					// NOTE: deprecated hook, B/C only
615
					// use the content object's own rendering
616
					$cnt = $this->mNewRev->getContent();
617
					$po = $cnt ? $cnt->getParserOutput( $this->mNewRev->getTitle(), $this->mNewRev->getId() ) : null;
0 ignored issues
show
Bug introduced by
It seems like $this->mNewRev->getTitle() can be null; however, getParserOutput() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
618
					if ( $po ) {
619
						$out->addParserOutputContent( $po );
620
					}
621
				}
622
			} elseif ( !Hooks::run( 'ArticleContentViewCustom', [ $this->mNewContent, $this->mNewPage, $out ] ) ) {
623
				// Handled by extension
624
			} elseif ( !ContentHandler::runLegacyHooks( 'ArticleViewCustom', [ $this->mNewContent, $this->mNewPage, $out ] ) ) {
625
				// NOTE: deprecated hook, B/C only
626
				// Handled by extension
627
			} else {
628
				// Normal page
629
				if ( $this->getTitle()->equals( $this->mNewPage ) ) {
630
					// If the Title stored in the context is the same as the one
631
					// of the new revision, we can use its associated WikiPage
632
					// object.
633
					$wikiPage = $this->getWikiPage();
634
				} else {
635
					// Otherwise we need to create our own WikiPage object
636
					$wikiPage = WikiPage::factory( $this->mNewPage );
637
				}
638
639
				$parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRev );
640
641
				# WikiPage::getParserOutput() should not return false, but just in case
642
				if ( $parserOutput ) {
643
					// Allow extensions to change parser output here
644
					if ( Hooks::run( 'DifferenceEngineRenderRevisionAddParserOutput', [ $this, $out, $parserOutput, $wikiPage ] ) ) {
645
						$out->addParserOutput( $parserOutput );
0 ignored issues
show
Bug introduced by
It seems like $parserOutput defined by $this->getParserOutput($wikiPage, $this->mNewRev) on line 639 can also be of type boolean; however, OutputPage::addParserOutput() does only seem to accept object<ParserOutput>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
646
					}
647
				}
648
			}
649
		}
650
		# @codingStandardsIgnoreEnd
651
652
		// Allow extensions to optionally not show the final patrolled link
653
		if ( Hooks::run( 'DifferenceEngineRenderRevisionShowFinalPatrolLink' ) ) {
654
			# Add redundant patrol link on bottom...
655
			$out->addHTML( $this->markPatrolledLink() );
656
		}
657
	}
658
659
	protected function getParserOutput( WikiPage $page, Revision $rev ) {
660
		$parserOptions = $page->makeParserOptions( $this->getContext() );
661
662
		if ( !$rev->isCurrent() || !$rev->getTitle()->quickUserCan( 'edit', $this->getUser() ) ) {
663
			$parserOptions->setEditSection( false );
664
		}
665
666
		$parserOutput = $page->getParserOutput( $parserOptions, $rev->getId() );
667
668
		return $parserOutput;
669
	}
670
671
	/**
672
	 * Get the diff text, send it to the OutputPage object
673
	 * Returns false if the diff could not be generated, otherwise returns true
674
	 *
675
	 * @param string|bool $otitle Header for old text or false
676
	 * @param string|bool $ntitle Header for new text or false
677
	 * @param string $notice HTML between diff header and body
678
	 *
679
	 * @return bool
680
	 */
681
	public function showDiff( $otitle, $ntitle, $notice = '' ) {
682
		// Allow extensions to affect the output here
683
		Hooks::run( 'DifferenceEngineShowDiff', [ $this ] );
684
685
		$diff = $this->getDiff( $otitle, $ntitle, $notice );
686
		if ( $diff === false ) {
687
			$this->showMissingRevision();
688
689
			return false;
690
		} else {
691
			$this->showDiffStyle();
692
			$this->getOutput()->addHTML( $diff );
693
694
			return true;
695
		}
696
	}
697
698
	/**
699
	 * Add style sheets and supporting JS for diff display.
700
	 */
701
	public function showDiffStyle() {
702
		$this->getOutput()->addModuleStyles( 'mediawiki.action.history.diff' );
703
	}
704
705
	/**
706
	 * Get complete diff table, including header
707
	 *
708
	 * @param string|bool $otitle Header for old text or false
709
	 * @param string|bool $ntitle Header for new text or false
710
	 * @param string $notice HTML between diff header and body
711
	 *
712
	 * @return mixed
713
	 */
714
	public function getDiff( $otitle, $ntitle, $notice = '' ) {
715
		$body = $this->getDiffBody();
716
		if ( $body === false ) {
717
			return false;
718
		}
719
720
		$multi = $this->getMultiNotice();
721
		// Display a message when the diff is empty
722
		if ( $body === '' ) {
723
			$notice .= '<div class="mw-diff-empty">' .
724
				$this->msg( 'diff-empty' )->parse() .
725
				"</div>\n";
726
		}
727
728
		return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
0 ignored issues
show
Bug introduced by
It seems like $otitle defined by parameter $otitle on line 714 can also be of type boolean; however, DifferenceEngine::addHeader() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
Bug introduced by
It seems like $ntitle defined by parameter $ntitle on line 714 can also be of type boolean; however, DifferenceEngine::addHeader() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
729
	}
730
731
	/**
732
	 * Get the diff table body, without header
733
	 *
734
	 * @return mixed (string/false)
735
	 */
736
	public function getDiffBody() {
737
		$this->mCacheHit = true;
738
		// Check if the diff should be hidden from this user
739
		if ( !$this->loadRevisionData() ) {
740
			return false;
741
		} elseif ( $this->mOldRev &&
742
			!$this->mOldRev->userCan( Revision::DELETED_TEXT, $this->getUser() )
743
		) {
744
			return false;
745
		} elseif ( $this->mNewRev &&
746
			!$this->mNewRev->userCan( Revision::DELETED_TEXT, $this->getUser() )
747
		) {
748
			return false;
749
		}
750
		// Short-circuit
751
		if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev
752
			&& $this->mOldRev->getId() == $this->mNewRev->getId() )
753
		) {
754
			if ( Hooks::run( 'DifferenceEngineShowEmptyOldContent', [ $this ] ) ) {
755
				return '';
756
			}
757
		}
758
		// Cacheable?
759
		$key = false;
760
		$cache = ObjectCache::getMainWANInstance();
761
		if ( $this->mOldid && $this->mNewid ) {
762
			$key = $this->getDiffBodyCacheKey();
763
764
			// Try cache
765
			if ( !$this->mRefreshCache ) {
766
				$difftext = $cache->get( $key );
767
				if ( $difftext ) {
768
					wfIncrStats( 'diff_cache.hit' );
769
					$difftext = $this->localiseLineNumbers( $difftext );
770
					$difftext .= "\n<!-- diff cache key $key -->\n";
771
772
					return $difftext;
773
				}
774
			} // don't try to load but save the result
775
		}
776
		$this->mCacheHit = false;
777
778
		// Loadtext is permission safe, this just clears out the diff
779
		if ( !$this->loadText() ) {
780
			return false;
781
		}
782
783
		$difftext = $this->generateContentDiffBody( $this->mOldContent, $this->mNewContent );
784
785
		// Save to cache for 7 days
786
		if ( !Hooks::run( 'AbortDiffCache', [ &$this ] ) ) {
787
			wfIncrStats( 'diff_cache.uncacheable' );
788
		} elseif ( $key !== false && $difftext !== false ) {
789
			wfIncrStats( 'diff_cache.miss' );
790
			$cache->set( $key, $difftext, 7 * 86400 );
791
		} else {
792
			wfIncrStats( 'diff_cache.uncacheable' );
793
		}
794
		// Replace line numbers with the text in the user's language
795
		if ( $difftext !== false ) {
796
			$difftext = $this->localiseLineNumbers( $difftext );
0 ignored issues
show
Bug introduced by
It seems like $difftext can also be of type boolean; however, DifferenceEngine::localiseLineNumbers() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
797
		}
798
799
		return $difftext;
800
	}
801
802
	/**
803
	 * Returns the cache key for diff body text or content.
804
	 *
805
	 * @since 1.23
806
	 *
807
	 * @throws MWException
808
	 * @return string
809
	 */
810
	protected function getDiffBodyCacheKey() {
811
		if ( !$this->mOldid || !$this->mNewid ) {
812
			throw new MWException( 'mOldid and mNewid must be set to get diff cache key.' );
813
		}
814
815
		return wfMemcKey( 'diff', 'version', self::DIFF_VERSION,
816
			'oldid', $this->mOldid, 'newid', $this->mNewid );
817
	}
818
819
	/**
820
	 * Generate a diff, no caching.
821
	 *
822
	 * This implementation uses generateTextDiffBody() to generate a diff based on the default
823
	 * serialization of the given Content objects. This will fail if $old or $new are not
824
	 * instances of TextContent.
825
	 *
826
	 * Subclasses may override this to provide a different rendering for the diff,
827
	 * perhaps taking advantage of the content's native form. This is required for all content
828
	 * models that are not text based.
829
	 *
830
	 * @since 1.21
831
	 *
832
	 * @param Content $old Old content
833
	 * @param Content $new New content
834
	 *
835
	 * @throws MWException If old or new content is not an instance of TextContent.
836
	 * @return bool|string
837
	 */
838
	public function generateContentDiffBody( Content $old, Content $new ) {
839 View Code Duplication
		if ( !( $old instanceof TextContent ) ) {
840
			throw new MWException( "Diff not implemented for " . get_class( $old ) . "; " .
841
				"override generateContentDiffBody to fix this." );
842
		}
843
844 View Code Duplication
		if ( !( $new instanceof TextContent ) ) {
845
			throw new MWException( "Diff not implemented for " . get_class( $new ) . "; "
846
				. "override generateContentDiffBody to fix this." );
847
		}
848
849
		$otext = $old->serialize();
850
		$ntext = $new->serialize();
851
852
		return $this->generateTextDiffBody( $otext, $ntext );
853
	}
854
855
	/**
856
	 * Generate a diff, no caching
857
	 *
858
	 * @todo move this to TextDifferenceEngine, make DifferenceEngine abstract. At some point.
859
	 *
860
	 * @param string $otext Old text, must be already segmented
861
	 * @param string $ntext New text, must be already segmented
862
	 *
863
	 * @return bool|string
864
	 */
865
	public function generateTextDiffBody( $otext, $ntext ) {
866
		$diff = function() use ( $otext, $ntext ) {
867
			$time = microtime( true );
868
869
			$result = $this->textDiff( $otext, $ntext );
870
871
			$time = intval( ( microtime( true ) - $time ) * 1000 );
872
			$this->getStats()->timing( 'diff_time', $time );
873
			// Log requests slower than 99th percentile
874
			if ( $time > 100 && $this->mOldPage && $this->mNewPage ) {
875
				wfDebugLog( 'diff',
876
					"$time ms diff: {$this->mOldid} -> {$this->mNewid} {$this->mNewPage}" );
877
			}
878
879
			return $result;
880
		};
881
882
		$error = function( $status ) {
883
			throw new FatalError( $status->getWikiText() );
884
		};
885
886
		// Use PoolCounter if the diff looks like it can be expensive
887
		if ( strlen( $otext ) + strlen( $ntext ) > 20000 ) {
888
			$work = new PoolCounterWorkViaCallback( 'diff',
889
				md5( $otext ) . md5( $ntext ),
890
				[ 'doWork' => $diff, 'error' => $error ]
891
			);
892
			return $work->execute();
893
		}
894
895
		return $diff();
896
	}
897
898
	/**
899
	 * Generates diff, to be wrapped internally in a logging/instrumentation
900
	 *
901
	 * @param string $otext Old text, must be already segmented
902
	 * @param string $ntext New text, must be already segmented
903
	 * @return bool|string
904
	 */
905
	protected function textDiff( $otext, $ntext ) {
906
		global $wgExternalDiffEngine, $wgContLang;
907
908
		$otext = str_replace( "\r\n", "\n", $otext );
909
		$ntext = str_replace( "\r\n", "\n", $ntext );
910
911
		if ( $wgExternalDiffEngine == 'wikidiff' || $wgExternalDiffEngine == 'wikidiff3' ) {
912
			wfDeprecated( "\$wgExternalDiffEngine = '{$wgExternalDiffEngine}'", '1.27' );
913
			$wgExternalDiffEngine = false;
914
		} elseif ( $wgExternalDiffEngine == 'wikidiff2' ) {
915
			// Same as above, but with no deprecation warnings
916
			$wgExternalDiffEngine = false;
917
		} elseif ( !is_string( $wgExternalDiffEngine ) && $wgExternalDiffEngine !== false ) {
918
			// And prevent people from shooting themselves in the foot...
919
			wfWarn( '$wgExternalDiffEngine is set to a non-string value, forcing it to false' );
920
			$wgExternalDiffEngine = false;
921
		}
922
923
		if ( function_exists( 'wikidiff2_do_diff' ) && $wgExternalDiffEngine === false ) {
924
			# Better external diff engine, the 2 may some day be dropped
925
			# This one does the escaping and segmenting itself
926
			$text = wikidiff2_do_diff( $otext, $ntext, 2 );
927
			$text .= $this->debug( 'wikidiff2' );
928
929
			return $text;
930
		} elseif ( $wgExternalDiffEngine !== false && is_executable( $wgExternalDiffEngine ) ) {
931
			# Diff via the shell
932
			$tmpDir = wfTempDir();
933
			$tempName1 = tempnam( $tmpDir, 'diff_' );
934
			$tempName2 = tempnam( $tmpDir, 'diff_' );
935
936
			$tempFile1 = fopen( $tempName1, "w" );
937
			if ( !$tempFile1 ) {
938
				return false;
939
			}
940
			$tempFile2 = fopen( $tempName2, "w" );
941
			if ( !$tempFile2 ) {
942
				return false;
943
			}
944
			fwrite( $tempFile1, $otext );
945
			fwrite( $tempFile2, $ntext );
946
			fclose( $tempFile1 );
947
			fclose( $tempFile2 );
948
			$cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 );
949
			$difftext = wfShellExec( $cmd );
950
			$difftext .= $this->debug( "external $wgExternalDiffEngine" );
951
			unlink( $tempName1 );
952
			unlink( $tempName2 );
953
954
			return $difftext;
955
		}
956
957
		# Native PHP diff
958
		$ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) );
959
		$nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) );
960
		$diffs = new Diff( $ota, $nta );
961
		$formatter = new TableDiffFormatter();
962
		$difftext = $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) );
963
964
		return $difftext;
965
	}
966
967
	/**
968
	 * Generate a debug comment indicating diff generating time,
969
	 * server node, and generator backend.
970
	 *
971
	 * @param string $generator : What diff engine was used
972
	 *
973
	 * @return string
974
	 */
975
	protected function debug( $generator = "internal" ) {
976
		global $wgShowHostnames;
977
		if ( !$this->enableDebugComment ) {
978
			return '';
979
		}
980
		$data = [ $generator ];
981
		if ( $wgShowHostnames ) {
982
			$data[] = wfHostname();
983
		}
984
		$data[] = wfTimestamp( TS_DB );
985
986
		return "<!-- diff generator: " .
987
			implode( " ", array_map( "htmlspecialchars", $data ) ) .
988
			" -->\n";
989
	}
990
991
	/**
992
	 * Replace line numbers with the text in the user's language
993
	 *
994
	 * @param string $text
995
	 *
996
	 * @return mixed
997
	 */
998
	public function localiseLineNumbers( $text ) {
999
		return preg_replace_callback(
1000
			'/<!--LINE (\d+)-->/',
1001
			[ &$this, 'localiseLineNumbersCb' ],
1002
			$text
1003
		);
1004
	}
1005
1006
	public function localiseLineNumbersCb( $matches ) {
1007
		if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
1008
			return '';
1009
		}
1010
1011
		return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
1012
	}
1013
1014
	/**
1015
	 * If there are revisions between the ones being compared, return a note saying so.
1016
	 *
1017
	 * @return string
1018
	 */
1019
	public function getMultiNotice() {
1020
		if ( !is_object( $this->mOldRev ) || !is_object( $this->mNewRev ) ) {
1021
			return '';
1022
		} elseif ( !$this->mOldPage->equals( $this->mNewPage ) ) {
1023
			// Comparing two different pages? Count would be meaningless.
1024
			return '';
1025
		}
1026
1027
		if ( $this->mOldRev->getTimestamp() > $this->mNewRev->getTimestamp() ) {
1028
			$oldRev = $this->mNewRev; // flip
1029
			$newRev = $this->mOldRev; // flip
1030
		} else { // normal case
1031
			$oldRev = $this->mOldRev;
1032
			$newRev = $this->mNewRev;
1033
		}
1034
1035
		// Sanity: don't show the notice if too many rows must be scanned
1036
		// @todo show some special message for that case
1037
		$nEdits = $this->mNewPage->countRevisionsBetween( $oldRev, $newRev, 1000 );
1038
		if ( $nEdits > 0 && $nEdits <= 1000 ) {
1039
			$limit = 100; // use diff-multi-manyusers if too many users
1040
			$users = $this->mNewPage->getAuthorsBetween( $oldRev, $newRev, $limit );
1041
			$numUsers = count( $users );
1042
1043
			if ( $numUsers == 1 && $users[0] == $newRev->getUserText( Revision::RAW ) ) {
1044
				$numUsers = 0; // special case to say "by the same user" instead of "by one other user"
1045
			}
1046
1047
			return self::intermediateEditsMsg( $nEdits, $numUsers, $limit );
1048
		}
1049
1050
		return ''; // nothing
1051
	}
1052
1053
	/**
1054
	 * Get a notice about how many intermediate edits and users there are
1055
	 *
1056
	 * @param int $numEdits
1057
	 * @param int $numUsers
1058
	 * @param int $limit
1059
	 *
1060
	 * @return string
1061
	 */
1062
	public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) {
1063
		if ( $numUsers === 0 ) {
1064
			$msg = 'diff-multi-sameuser';
1065
		} elseif ( $numUsers > $limit ) {
1066
			$msg = 'diff-multi-manyusers';
1067
			$numUsers = $limit;
1068
		} else {
1069
			$msg = 'diff-multi-otherusers';
1070
		}
1071
1072
		return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1073
	}
1074
1075
	/**
1076
	 * Get a header for a specified revision.
1077
	 *
1078
	 * @param Revision $rev
1079
	 * @param string $complete 'complete' to get the header wrapped depending
1080
	 *        the visibility of the revision and a link to edit the page.
1081
	 *
1082
	 * @return string HTML fragment
1083
	 */
1084
	protected function getRevisionHeader( Revision $rev, $complete = '' ) {
1085
		$lang = $this->getLanguage();
1086
		$user = $this->getUser();
1087
		$revtimestamp = $rev->getTimestamp();
1088
		$timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
1089
		$dateofrev = $lang->userDate( $revtimestamp, $user );
1090
		$timeofrev = $lang->userTime( $revtimestamp, $user );
1091
1092
		$header = $this->msg(
1093
			$rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
1094
			$timestamp,
1095
			$dateofrev,
1096
			$timeofrev
1097
		)->escaped();
1098
1099
		if ( $complete !== 'complete' ) {
1100
			return $header;
1101
		}
1102
1103
		$title = $rev->getTitle();
1104
1105
		$header = Linker::linkKnown( $title, $header, [],
1106
			[ 'oldid' => $rev->getId() ] );
1107
1108
		if ( $rev->userCan( Revision::DELETED_TEXT, $user ) ) {
1109
			$editQuery = [ 'action' => 'edit' ];
1110
			if ( !$rev->isCurrent() ) {
1111
				$editQuery['oldid'] = $rev->getId();
1112
			}
1113
1114
			$key = $title->quickUserCan( 'edit', $user ) ? 'editold' : 'viewsourceold';
1115
			$msg = $this->msg( $key )->escaped();
1116
			$editLink = $this->msg( 'parentheses' )->rawParams(
1117
				Linker::linkKnown( $title, $msg, [], $editQuery ) )->escaped();
1118
			$header .= ' ' . Html::rawElement(
1119
				'span',
1120
				[ 'class' => 'mw-diff-edit' ],
1121
				$editLink
1122
			);
1123
			if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
1124
				$header = Html::rawElement(
1125
					'span',
1126
					[ 'class' => 'history-deleted' ],
1127
					$header
1128
				);
1129
			}
1130
		} else {
1131
			$header = Html::rawElement( 'span', [ 'class' => 'history-deleted' ], $header );
1132
		}
1133
1134
		return $header;
1135
	}
1136
1137
	/**
1138
	 * Add the header to a diff body
1139
	 *
1140
	 * @param string $diff Diff body
1141
	 * @param string $otitle Old revision header
1142
	 * @param string $ntitle New revision header
1143
	 * @param string $multi Notice telling user that there are intermediate
1144
	 *   revisions between the ones being compared
1145
	 * @param string $notice Other notices, e.g. that user is viewing deleted content
1146
	 *
1147
	 * @return string
1148
	 */
1149
	public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
1150
		// shared.css sets diff in interface language/dir, but the actual content
1151
		// is often in a different language, mostly the page content language/dir
1152
		$header = Html::openElement( 'table', [
1153
			'class' => [ 'diff', 'diff-contentalign-' . $this->getDiffLang()->alignStart() ],
1154
			'data-mw' => 'interface',
1155
		] );
1156
		$userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
1157
1158
		if ( !$diff && !$otitle ) {
1159
			$header .= "
1160
			<tr style='vertical-align: top;' lang='{$userLang}'>
1161
			<td class='diff-ntitle'>{$ntitle}</td>
1162
			</tr>";
1163
			$multiColspan = 1;
1164
		} else {
1165
			if ( $diff ) { // Safari/Chrome show broken output if cols not used
1166
				$header .= "
1167
				<col class='diff-marker' />
1168
				<col class='diff-content' />
1169
				<col class='diff-marker' />
1170
				<col class='diff-content' />";
1171
				$colspan = 2;
1172
				$multiColspan = 4;
1173
			} else {
1174
				$colspan = 1;
1175
				$multiColspan = 2;
1176
			}
1177
			if ( $otitle || $ntitle ) {
1178
				$header .= "
1179
				<tr style='vertical-align: top;' lang='{$userLang}'>
1180
				<td colspan='$colspan' class='diff-otitle'>{$otitle}</td>
1181
				<td colspan='$colspan' class='diff-ntitle'>{$ntitle}</td>
1182
				</tr>";
1183
			}
1184
		}
1185
1186
		if ( $multi != '' ) {
1187
			$header .= "<tr><td colspan='{$multiColspan}' style='text-align: center;' " .
1188
				"class='diff-multi' lang='{$userLang}'>{$multi}</td></tr>";
1189
		}
1190
		if ( $notice != '' ) {
1191
			$header .= "<tr><td colspan='{$multiColspan}' style='text-align: center;' " .
1192
				"lang='{$userLang}'>{$notice}</td></tr>";
1193
		}
1194
1195
		return $header . $diff . "</table>";
1196
	}
1197
1198
	/**
1199
	 * Use specified text instead of loading from the database
1200
	 * @param Content $oldContent
1201
	 * @param Content $newContent
1202
	 * @since 1.21
1203
	 */
1204
	public function setContent( Content $oldContent, Content $newContent ) {
1205
		$this->mOldContent = $oldContent;
1206
		$this->mNewContent = $newContent;
1207
1208
		$this->mTextLoaded = 2;
1209
		$this->mRevisionsLoaded = true;
1210
	}
1211
1212
	/**
1213
	 * Set the language in which the diff text is written
1214
	 * (Defaults to page content language).
1215
	 * @param Language|string $lang
1216
	 * @since 1.19
1217
	 */
1218
	public function setTextLanguage( $lang ) {
1219
		$this->mDiffLang = wfGetLangObj( $lang );
1220
	}
1221
1222
	/**
1223
	 * Maps a revision pair definition as accepted by DifferenceEngine constructor
1224
	 * to a pair of actual integers representing revision ids.
1225
	 *
1226
	 * @param int $old Revision id, e.g. from URL parameter 'oldid'
1227
	 * @param int|string $new Revision id or strings 'next' or 'prev', e.g. from URL parameter 'diff'
1228
	 *
1229
	 * @return int[] List of two revision ids, older first, later second.
1230
	 *     Zero signifies invalid argument passed.
1231
	 *     false signifies that there is no previous/next revision ($old is the oldest/newest one).
1232
	 */
1233
	public function mapDiffPrevNext( $old, $new ) {
1234
		if ( $new === 'prev' ) {
1235
			// Show diff between revision $old and the previous one. Get previous one from DB.
1236
			$newid = intval( $old );
1237
			$oldid = $this->getTitle()->getPreviousRevisionID( $newid );
1238
		} elseif ( $new === 'next' ) {
1239
			// Show diff between revision $old and the next one. Get next one from DB.
1240
			$oldid = intval( $old );
1241
			$newid = $this->getTitle()->getNextRevisionID( $oldid );
1242
		} else {
1243
			$oldid = intval( $old );
1244
			$newid = intval( $new );
1245
		}
1246
1247
		return [ $oldid, $newid ];
1248
	}
1249
1250
	/**
1251
	 * Load revision IDs
1252
	 */
1253
	private function loadRevisionIds() {
1254
		if ( $this->mRevisionsIdsLoaded ) {
1255
			return;
1256
		}
1257
1258
		$this->mRevisionsIdsLoaded = true;
1259
1260
		$old = $this->mOldid;
1261
		$new = $this->mNewid;
1262
1263
		list( $this->mOldid, $this->mNewid ) = self::mapDiffPrevNext( $old, $new );
1264
		if ( $new === 'next' && $this->mNewid === false ) {
1265
			# if no result, NewId points to the newest old revision. The only newer
1266
			# revision is cur, which is "0".
1267
			$this->mNewid = 0;
1268
		}
1269
1270
		Hooks::run(
1271
			'NewDifferenceEngine',
1272
			[ $this->getTitle(), &$this->mOldid, &$this->mNewid, $old, $new ]
1273
		);
1274
	}
1275
1276
	/**
1277
	 * Load revision metadata for the specified articles. If newid is 0, then compare
1278
	 * the old article in oldid to the current article; if oldid is 0, then
1279
	 * compare the current article to the immediately previous one (ignoring the
1280
	 * value of newid).
1281
	 *
1282
	 * If oldid is false, leave the corresponding revision object set
1283
	 * to false. This is impossible via ordinary user input, and is provided for
1284
	 * API convenience.
1285
	 *
1286
	 * @return bool
1287
	 */
1288
	public function loadRevisionData() {
1289
		if ( $this->mRevisionsLoaded ) {
1290
			return true;
1291
		}
1292
1293
		// Whether it succeeds or fails, we don't want to try again
1294
		$this->mRevisionsLoaded = true;
1295
1296
		$this->loadRevisionIds();
1297
1298
		// Load the new revision object
1299
		if ( $this->mNewid ) {
1300
			$this->mNewRev = Revision::newFromId( $this->mNewid );
1301
		} else {
1302
			$this->mNewRev = Revision::newFromTitle(
1303
				$this->getTitle(),
0 ignored issues
show
Bug introduced by
It seems like $this->getTitle() can be null; however, newFromTitle() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1304
				false,
1305
				Revision::READ_NORMAL
1306
			);
1307
		}
1308
1309
		if ( !$this->mNewRev instanceof Revision ) {
1310
			return false;
1311
		}
1312
1313
		// Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
1314
		$this->mNewid = $this->mNewRev->getId();
1315
		$this->mNewPage = $this->mNewRev->getTitle();
1316
1317
		// Load the old revision object
1318
		$this->mOldRev = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type object<Revision> of property $mOldRev.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
1319
		if ( $this->mOldid ) {
1320
			$this->mOldRev = Revision::newFromId( $this->mOldid );
1321
		} elseif ( $this->mOldid === 0 ) {
1322
			$rev = $this->mNewRev->getPrevious();
1323
			if ( $rev ) {
1324
				$this->mOldid = $rev->getId();
1325
				$this->mOldRev = $rev;
1326
			} else {
1327
				// No previous revision; mark to show as first-version only.
1328
				$this->mOldid = false;
0 ignored issues
show
Documentation Bug introduced by
The property $mOldid was declared of type integer, but false is of type false. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
1329
				$this->mOldRev = false;
1330
			}
1331
		} /* elseif ( $this->mOldid === false ) leave mOldRev false; */
1332
1333
		if ( is_null( $this->mOldRev ) ) {
1334
			return false;
1335
		}
1336
1337
		if ( $this->mOldRev ) {
1338
			$this->mOldPage = $this->mOldRev->getTitle();
1339
		}
1340
1341
		// Load tags information for both revisions
1342
		$dbr = wfGetDB( DB_SLAVE );
1343
		if ( $this->mOldid !== false ) {
1344
			$this->mOldTags = $dbr->selectField(
1345
				'tag_summary',
1346
				'ts_tags',
1347
				[ 'ts_rev_id' => $this->mOldid ],
1348
				__METHOD__
1349
			);
1350
		} else {
1351
			$this->mOldTags = false;
1352
		}
1353
		$this->mNewTags = $dbr->selectField(
1354
			'tag_summary',
1355
			'ts_tags',
1356
			[ 'ts_rev_id' => $this->mNewid ],
1357
			__METHOD__
1358
		);
1359
1360
		return true;
1361
	}
1362
1363
	/**
1364
	 * Load the text of the revisions, as well as revision data.
1365
	 *
1366
	 * @return bool
1367
	 */
1368
	public function loadText() {
1369
		if ( $this->mTextLoaded == 2 ) {
1370
			return true;
1371
		}
1372
1373
		// Whether it succeeds or fails, we don't want to try again
1374
		$this->mTextLoaded = 2;
1375
1376
		if ( !$this->loadRevisionData() ) {
1377
			return false;
1378
		}
1379
1380 View Code Duplication
		if ( $this->mOldRev ) {
1381
			$this->mOldContent = $this->mOldRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
1382
			if ( $this->mOldContent === null ) {
1383
				return false;
1384
			}
1385
		}
1386
1387 View Code Duplication
		if ( $this->mNewRev ) {
1388
			$this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
1389
			if ( $this->mNewContent === null ) {
1390
				return false;
1391
			}
1392
		}
1393
1394
		return true;
1395
	}
1396
1397
	/**
1398
	 * Load the text of the new revision, not the old one
1399
	 *
1400
	 * @return bool
1401
	 */
1402
	public function loadNewText() {
1403
		if ( $this->mTextLoaded >= 1 ) {
1404
			return true;
1405
		}
1406
1407
		$this->mTextLoaded = 1;
1408
1409
		if ( !$this->loadRevisionData() ) {
1410
			return false;
1411
		}
1412
1413
		$this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
1414
1415
		return true;
1416
	}
1417
1418
}
1419