Completed
Branch master (a553dc)
by
unknown
26:33
created

DifferenceEngine::deletedLink()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 13
nc 3
nop 1
dl 0
loc 19
rs 9.4285
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
/**
25
 * Constant to indicate diff cache compatibility.
26
 * Bump this when changing the diff formatting in a way that
27
 * fixes important bugs or such to force cached diff views to
28
 * clear.
29
 */
30
define( 'MW_DIFF_VERSION', '1.11a' );
31
32
/**
33
 * @todo document
34
 * @ingroup DifferenceEngine
35
 */
36
class DifferenceEngine extends ContextSource {
37
38
	/** @var int */
39
	public $mOldid;
40
41
	/** @var int */
42
	public $mNewid;
43
44
	private $mOldTags;
45
	private $mNewTags;
46
47
	/** @var Content */
48
	public $mOldContent;
49
50
	/** @var Content */
51
	public $mNewContent;
52
53
	/** @var Language */
54
	protected $mDiffLang;
55
56
	/** @var Title */
57
	public $mOldPage;
58
59
	/** @var Title */
60
	public $mNewPage;
61
62
	/** @var Revision */
63
	public $mOldRev;
64
65
	/** @var Revision */
66
	public $mNewRev;
67
68
	/** @var bool Have the revisions IDs been loaded */
69
	private $mRevisionsIdsLoaded = false;
70
71
	/** @var bool Have the revisions been loaded */
72
	public $mRevisionsLoaded = false;
73
74
	/** @var int How many text blobs have been loaded, 0, 1 or 2? */
75
	public $mTextLoaded = 0;
76
77
	/** @var bool Was the diff fetched from cache? */
78
	public $mCacheHit = false;
79
80
	/**
81
	 * Set this to true to add debug info to the HTML output.
82
	 * Warning: this may cause RSS readers to spuriously mark articles as "new"
83
	 * (bug 20601)
84
	 */
85
	public $enableDebugComment = false;
86
87
	/** @var bool If true, line X is not displayed when X is 1, for example
88
	 *    to increase readability and conserve space with many small diffs.
89
	 */
90
	protected $mReducedLineNumbers = false;
91
92
	/** @var string Link to action=markpatrolled */
93
	protected $mMarkPatrolledLink = null;
94
95
	/** @var bool Show rev_deleted content if allowed */
96
	protected $unhide = false;
97
98
	/** @var bool Refresh the diff cache */
99
	protected $mRefreshCache = false;
100
101
	/**#@-*/
102
103
	/**
104
	 * Constructor
105
	 * @param IContextSource $context Context to use, anything else will be ignored
106
	 * @param int $old Old ID we want to show and diff with.
107
	 * @param string|int $new Either revision ID or 'prev' or 'next'. Default: 0.
108
	 * @param int $rcid Deprecated, no longer used!
109
	 * @param bool $refreshCache If set, refreshes the diff cache
110
	 * @param bool $unhide If set, allow viewing deleted revs
111
	 */
112
	public function __construct( $context = null, $old = 0, $new = 0, $rcid = 0,
113
		$refreshCache = false, $unhide = false
114
	) {
115
		if ( $context instanceof IContextSource ) {
116
			$this->setContext( $context );
117
		}
118
119
		wfDebug( "DifferenceEngine old '$old' new '$new' rcid '$rcid'\n" );
120
121
		$this->mOldid = $old;
122
		$this->mNewid = $new;
123
		$this->mRefreshCache = $refreshCache;
124
		$this->unhide = $unhide;
125
	}
126
127
	/**
128
	 * @param bool $value
129
	 */
130
	public function setReducedLineNumbers( $value = true ) {
131
		$this->mReducedLineNumbers = $value;
132
	}
133
134
	/**
135
	 * @return Language
136
	 */
137
	public function getDiffLang() {
138
		if ( $this->mDiffLang === null ) {
139
			# Default language in which the diff text is written.
140
			$this->mDiffLang = $this->getTitle()->getPageLanguage();
141
		}
142
143
		return $this->mDiffLang;
144
	}
145
146
	/**
147
	 * @return bool
148
	 */
149
	public function wasCacheHit() {
150
		return $this->mCacheHit;
151
	}
152
153
	/**
154
	 * @return int
155
	 */
156
	public function getOldid() {
157
		$this->loadRevisionIds();
158
159
		return $this->mOldid;
160
	}
161
162
	/**
163
	 * @return bool|int
164
	 */
165
	public function getNewid() {
166
		$this->loadRevisionIds();
167
168
		return $this->mNewid;
169
	}
170
171
	/**
172
	 * Look up a special:Undelete link to the given deleted revision id,
173
	 * as a workaround for being unable to load deleted diffs in currently.
174
	 *
175
	 * @param int $id Revision ID
176
	 *
177
	 * @return mixed URL or false
178
	 */
179
	public function deletedLink( $id ) {
180
		if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) {
181
			$dbr = wfGetDB( DB_SLAVE );
182
			$row = $dbr->selectRow( 'archive', '*',
183
				[ 'ar_rev_id' => $id ],
184
				__METHOD__ );
185
			if ( $row ) {
186
				$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 182 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...
187
				$title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
188
189
				return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( [
190
					'target' => $title->getPrefixedText(),
191
					'timestamp' => $rev->getTimestamp()
192
				] );
193
			}
194
		}
195
196
		return false;
197
	}
198
199
	/**
200
	 * Build a wikitext link toward a deleted revision, if viewable.
201
	 *
202
	 * @param int $id Revision ID
203
	 *
204
	 * @return string Wikitext fragment
205
	 */
206
	public function deletedIdMarker( $id ) {
207
		$link = $this->deletedLink( $id );
208
		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...
209
			return "[$link $id]";
210
		} else {
211
			return $id;
212
		}
213
	}
214
215
	private function showMissingRevision() {
216
		$out = $this->getOutput();
217
218
		$missing = [];
219
		if ( $this->mOldRev === null ||
220
			( $this->mOldRev && $this->mOldContent === null )
221
		) {
222
			$missing[] = $this->deletedIdMarker( $this->mOldid );
223
		}
224
		if ( $this->mNewRev === null ||
225
			( $this->mNewRev && $this->mNewContent === null )
226
		) {
227
			$missing[] = $this->deletedIdMarker( $this->mNewid );
228
		}
229
230
		$out->setPageTitle( $this->msg( 'errorpagetitle' ) );
231
		$msg = $this->msg( 'difference-missing-revision' )
232
			->params( $this->getLanguage()->listToText( $missing ) )
233
			->numParams( count( $missing ) )
234
			->parseAsBlock();
235
		$out->addHTML( $msg );
236
	}
237
238
	public function showDiffPage( $diffOnly = false ) {
239
		# Allow frames except in certain special cases
240
		$out = $this->getOutput();
241
		$out->allowClickjacking();
242
		$out->setRobotPolicy( 'noindex,nofollow' );
243
244
		// Allow extensions to add any extra output here
245
		Hooks::run( 'DifferenceEngineShowDiffPage', [ $out ] );
246
247
		if ( !$this->loadRevisionData() ) {
248
			$this->showMissingRevision();
249
250
			return;
251
		}
252
253
		$user = $this->getUser();
254
		$permErrors = $this->mNewPage->getUserPermissionsErrors( 'read', $user );
255
		if ( $this->mOldPage ) { # mOldPage might not be set, see below.
256
			$permErrors = wfMergeErrorArrays( $permErrors,
257
				$this->mOldPage->getUserPermissionsErrors( 'read', $user ) );
258
		}
259
		if ( count( $permErrors ) ) {
260
			throw new PermissionsError( 'read', $permErrors );
261
		}
262
263
		$rollback = '';
264
265
		$query = [];
266
		# Carry over 'diffonly' param via navigation links
267
		if ( $diffOnly != $user->getBoolOption( 'diffonly' ) ) {
268
			$query['diffonly'] = $diffOnly;
269
		}
270
		# Cascade unhide param in links for easy deletion browsing
271
		if ( $this->unhide ) {
272
			$query['unhide'] = 1;
273
		}
274
275
		# Check if one of the revisions is deleted/suppressed
276
		$deleted = $suppressed = false;
277
		$allowed = $this->mNewRev->userCan( Revision::DELETED_TEXT, $user );
278
279
		$revisionTools = [];
280
281
		# mOldRev is false if the difference engine is called with a "vague" query for
282
		# a diff between a version V and its previous version V' AND the version V
283
		# is the first version of that article. In that case, V' does not exist.
284
		if ( $this->mOldRev === false ) {
285
			$out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
286
			$samePage = true;
287
			$oldHeader = '';
288
			// Allow extensions to change the $oldHeader variable
289
			Hooks::run( 'DifferenceEngineOldHeaderNoOldRev', [ &$oldHeader ] );
290
		} else {
291
			Hooks::run( 'DiffViewHeader', [ $this, $this->mOldRev, $this->mNewRev ] );
292
293
			if ( $this->mNewPage->equals( $this->mOldPage ) ) {
294
				$out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
295
				$samePage = true;
296
			} else {
297
				$out->setPageTitle( $this->msg( 'difference-title-multipage',
298
					$this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) );
299
				$out->addSubtitle( $this->msg( 'difference-multipage' ) );
300
				$samePage = false;
301
			}
302
303
			if ( $samePage && $this->mNewPage->quickUserCan( 'edit', $user ) ) {
304
				if ( $this->mNewRev->isCurrent() && $this->mNewPage->userCan( 'rollback', $user ) ) {
305
					$rollbackLink = Linker::generateRollback( $this->mNewRev, $this->getContext() );
306
					if ( $rollbackLink ) {
307
						$out->preventClickjacking();
308
						$rollback = '&#160;&#160;&#160;' . $rollbackLink;
309
					}
310
				}
311
312
				if ( !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) &&
313
					!$this->mNewRev->isDeleted( Revision::DELETED_TEXT )
314
				) {
315
					$undoLink = Html::element( 'a', [
316
							'href' => $this->mNewPage->getLocalURL( [
317
								'action' => 'edit',
318
								'undoafter' => $this->mOldid,
319
								'undo' => $this->mNewid
320
							] ),
321
							'title' => Linker::titleAttrib( 'undo' ),
322
						],
323
						$this->msg( 'editundo' )->text()
324
					);
325
					$revisionTools['mw-diff-undo'] = $undoLink;
326
				}
327
			}
328
329
			# Make "previous revision link"
330 View Code Duplication
			if ( $samePage && $this->mOldRev->getPrevious() ) {
331
				$prevlink = Linker::linkKnown(
332
					$this->mOldPage,
333
					$this->msg( 'previousdiff' )->escaped(),
334
					[ 'id' => 'differences-prevlink' ],
335
					[ 'diff' => 'prev', 'oldid' => $this->mOldid ] + $query
336
				);
337
			} else {
338
				$prevlink = '&#160;';
339
			}
340
341
			if ( $this->mOldRev->isMinor() ) {
342
				$oldminor = ChangesList::flag( 'minor' );
343
			} else {
344
				$oldminor = '';
345
			}
346
347
			$ldel = $this->revisionDeleteLink( $this->mOldRev );
348
			$oldRevisionHeader = $this->getRevisionHeader( $this->mOldRev, 'complete' );
349
			$oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff', $this->getContext() );
350
351
			$oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' .
352
				'<div id="mw-diff-otitle2">' .
353
				Linker::revUserTools( $this->mOldRev, !$this->unhide ) . '</div>' .
354
				'<div id="mw-diff-otitle3">' . $oldminor .
355
				Linker::revComment( $this->mOldRev, !$diffOnly, !$this->unhide ) . $ldel . '</div>' .
356
				'<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' .
357
				'<div id="mw-diff-otitle4">' . $prevlink . '</div>';
358
359
			// Allow extensions to change the $oldHeader variable
360
			Hooks::run( 'DifferenceEngineOldHeader', [ $this, &$oldHeader, $prevlink, $oldminor,
361
				$diffOnly, $ldel, $this->unhide ] );
362
363 View Code Duplication
			if ( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) {
364
				$deleted = true; // old revisions text is hidden
365
				if ( $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
366
					$suppressed = true; // also suppressed
367
				}
368
			}
369
370
			# Check if this user can see the revisions
371
			if ( !$this->mOldRev->userCan( Revision::DELETED_TEXT, $user ) ) {
372
				$allowed = false;
373
			}
374
		}
375
376
		# Make "next revision link"
377
		# Skip next link on the top revision
378 View Code Duplication
		if ( $samePage && !$this->mNewRev->isCurrent() ) {
379
			$nextlink = Linker::linkKnown(
380
				$this->mNewPage,
381
				$this->msg( 'nextdiff' )->escaped(),
382
				[ 'id' => 'differences-nextlink' ],
383
				[ 'diff' => 'next', 'oldid' => $this->mNewid ] + $query
384
			);
385
		} else {
386
			$nextlink = '&#160;';
387
		}
388
389
		if ( $this->mNewRev->isMinor() ) {
390
			$newminor = ChangesList::flag( 'minor' );
391
		} else {
392
			$newminor = '';
393
		}
394
395
		# Handle RevisionDelete links...
396
		$rdel = $this->revisionDeleteLink( $this->mNewRev );
397
398
		# Allow extensions to define their own revision tools
399
		Hooks::run( 'DiffRevisionTools',
400
			[ $this->mNewRev, &$revisionTools, $this->mOldRev, $user ] );
401
		$formattedRevisionTools = [];
402
		// Put each one in parentheses (poor man's button)
403
		foreach ( $revisionTools as $key => $tool ) {
404
			$toolClass = is_string( $key ) ? $key : 'mw-diff-tool';
405
			$element = Html::rawElement(
406
				'span',
407
				[ 'class' => $toolClass ],
408
				$this->msg( 'parentheses' )->rawParams( $tool )->escaped()
409
			);
410
			$formattedRevisionTools[] = $element;
411
		}
412
		$newRevisionHeader = $this->getRevisionHeader( $this->mNewRev, 'complete' ) .
413
			' ' . implode( ' ', $formattedRevisionTools );
414
		$newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff', $this->getContext() );
415
416
		$newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' .
417
			'<div id="mw-diff-ntitle2">' . Linker::revUserTools( $this->mNewRev, !$this->unhide ) .
418
			" $rollback</div>" .
419
			'<div id="mw-diff-ntitle3">' . $newminor .
420
			Linker::revComment( $this->mNewRev, !$diffOnly, !$this->unhide ) . $rdel . '</div>' .
421
			'<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' .
422
			'<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
423
424
		// Allow extensions to change the $newHeader variable
425
		Hooks::run( 'DifferenceEngineNewHeader', [ $this, &$newHeader, $formattedRevisionTools,
426
			$nextlink, $rollback, $newminor, $diffOnly, $rdel, $this->unhide ] );
427
428 View Code Duplication
		if ( $this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
429
			$deleted = true; // new revisions text is hidden
430
			if ( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
431
				$suppressed = true; // also suppressed
432
			}
433
		}
434
435
		# If the diff cannot be shown due to a deleted revision, then output
436
		# the diff header and links to unhide (if available)...
437
		if ( $deleted && ( !$this->unhide || !$allowed ) ) {
438
			$this->showDiffStyle();
439
			$multi = $this->getMultiNotice();
440
			$out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
441
			if ( !$allowed ) {
442
				$msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff';
443
				# Give explanation for why revision is not visible
444
				$out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
445
					[ $msg ] );
446
			} else {
447
				# Give explanation and add a link to view the diff...
448
				$query = $this->getRequest()->appendQueryValue( 'unhide', '1' );
449
				$link = $this->getTitle()->getFullURL( $query );
450
				$msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff';
451
				$out->wrapWikiMsg(
452
					"<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
453
					[ $msg, $link ]
454
				);
455
			}
456
		# Otherwise, output a regular diff...
457
		} else {
458
			# Add deletion notice if the user is viewing deleted content
459
			$notice = '';
460
			if ( $deleted ) {
461
				$msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
462
				$notice = "<div id='mw-$msg' class='mw-warning plainlinks'>\n" .
463
					$this->msg( $msg )->parse() .
464
					"</div>\n";
465
			}
466
			$this->showDiff( $oldHeader, $newHeader, $notice );
467
			if ( !$diffOnly ) {
468
				$this->renderNewRevision();
469
			}
470
		}
471
	}
472
473
	/**
474
	 * Build a link to mark a change as patrolled.
475
	 *
476
	 * Returns empty string if there's either no revision to patrol or the user is not allowed to.
477
	 * Side effect: When the patrol link is build, this method will call
478
	 * OutputPage::preventClickjacking() and load mediawiki.page.patrol.ajax.
479
	 *
480
	 * @return string HTML or empty string
481
	 */
482
	protected function markPatrolledLink() {
483
		if ( $this->mMarkPatrolledLink === null ) {
484
			$linkInfo = $this->getMarkPatrolledLinkInfo();
485
			// If false, there is no patrol link needed/allowed
486
			if ( !$linkInfo ) {
487
				$this->mMarkPatrolledLink = '';
488
			} else {
489
				$this->mMarkPatrolledLink = ' <span class="patrollink" data-mw="interface">[' .
490
					Linker::linkKnown(
491
						$this->mNewPage,
492
						$this->msg( 'markaspatrolleddiff' )->escaped(),
493
						[],
494
						[
495
							'action' => 'markpatrolled',
496
							'rcid' => $linkInfo['rcid'],
497
							'token' => $linkInfo['token'],
498
						]
499
					) . ']</span>';
500
				// Allow extensions to change the markpatrolled link
501
				Hooks::run( 'DifferenceEngineMarkPatrolledLink', [ $this,
502
					&$this->mMarkPatrolledLink, $linkInfo['rcid'], $linkInfo['token'] ] );
503
			}
504
		}
505
		return $this->mMarkPatrolledLink;
506
	}
507
508
	/**
509
	 * Returns an array of meta data needed to build a "mark as patrolled" link and
510
	 * adds the mediawiki.page.patrol.ajax to the output.
511
	 *
512
	 * @return array|false An array of meta data for a patrol link (rcid & token)
513
	 *  or false if no link is needed
514
	 */
515
	protected function getMarkPatrolledLinkInfo() {
516
		global $wgUseRCPatrol, $wgEnableAPI, $wgEnableWriteAPI;
517
518
		$user = $this->getUser();
519
520
		// Prepare a change patrol link, if applicable
521
		if (
522
			// Is patrolling enabled and the user allowed to?
523
			$wgUseRCPatrol && $this->mNewPage->quickUserCan( 'patrol', $user ) &&
524
			// Only do this if the revision isn't more than 6 hours older
525
			// than the Max RC age (6h because the RC might not be cleaned out regularly)
526
			RecentChange::isInRCLifespan( $this->mNewRev->getTimestamp(), 21600 )
527
		) {
528
			// Look for an unpatrolled change corresponding to this diff
529
			$db = wfGetDB( DB_SLAVE );
530
			$change = RecentChange::newFromConds(
531
				[
532
					'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
533
					'rc_this_oldid' => $this->mNewid,
534
					'rc_patrolled' => 0
535
				],
536
				__METHOD__
537
			);
538
539
			if ( $change && !$change->getPerformer()->equals( $user ) ) {
540
				$rcid = $change->getAttribute( 'rc_id' );
541
			} else {
542
				// None found or the page has been created by the current user.
543
				// If the user could patrol this it already would be patrolled
544
				$rcid = 0;
545
			}
546
547
			// Allow extensions to possibly change the rcid here
548
			// For example the rcid might be set to zero due to the user
549
			// being the same as the performer of the change but an extension
550
			// might still want to show it under certain conditions
551
			Hooks::run( 'DifferenceEngineMarkPatrolledRCID', [ &$rcid, $this, $change, $user ] );
552
553
			// Build the link
554
			if ( $rcid ) {
555
				$this->getOutput()->preventClickjacking();
556
				if ( $wgEnableAPI && $wgEnableWriteAPI
557
					&& $user->isAllowed( 'writeapi' )
558
				) {
559
					$this->getOutput()->addModules( 'mediawiki.page.patrol.ajax' );
560
				}
561
562
				$token = $user->getEditToken( $rcid );
563
				return [
564
					'rcid' => $rcid,
565
					'token' => $token,
566
				];
567
			}
568
		}
569
570
		// No mark as patrolled link applicable
571
		return false;
572
	}
573
574
	/**
575
	 * @param Revision $rev
576
	 *
577
	 * @return string
578
	 */
579
	protected function revisionDeleteLink( $rev ) {
580
		$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...
581
		if ( $link !== '' ) {
582
			$link = '&#160;&#160;&#160;' . $link . ' ';
583
		}
584
585
		return $link;
586
	}
587
588
	/**
589
	 * Show the new revision of the page.
590
	 */
591
	public function renderNewRevision() {
592
		$out = $this->getOutput();
593
		$revHeader = $this->getRevisionHeader( $this->mNewRev );
594
		# Add "current version as of X" title
595
		$out->addHTML( "<hr class='diff-hr' id='mw-oldid' />
596
		<h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
597
		# Page content may be handled by a hooked call instead...
598
		# @codingStandardsIgnoreStart Ignoring long lines.
599
		if ( Hooks::run( 'ArticleContentOnDiff', [ $this, $out ] ) ) {
600
			$this->loadNewText();
601
			$out->setRevisionId( $this->mNewid );
602
			$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...
603
			$out->setArticleFlag( true );
604
605
			// NOTE: only needed for B/C: custom rendering of JS/CSS via hook
606
			if ( $this->mNewPage->isCssJsSubpage() || $this->mNewPage->isCssOrJsPage() ) {
607
				// This needs to be synchronised with Article::showCssOrJsPage(), which sucks
608
				// Give hooks a chance to customise the output
609
				// @todo standardize this crap into one function
610
				if ( ContentHandler::runLegacyHooks( 'ShowRawCssJs', [ $this->mNewContent, $this->mNewPage, $out ] ) ) {
611
					// NOTE: deprecated hook, B/C only
612
					// use the content object's own rendering
613
					$cnt = $this->mNewRev->getContent();
614
					$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...
615
					if ( $po ) {
616
						$out->addParserOutputContent( $po );
617
					}
618
				}
619
			} elseif ( !Hooks::run( 'ArticleContentViewCustom', [ $this->mNewContent, $this->mNewPage, $out ] ) ) {
620
				// Handled by extension
621
			} elseif ( !ContentHandler::runLegacyHooks( 'ArticleViewCustom', [ $this->mNewContent, $this->mNewPage, $out ] ) ) {
622
				// NOTE: deprecated hook, B/C only
623
				// Handled by extension
624
			} else {
625
				// Normal page
626
				if ( $this->getTitle()->equals( $this->mNewPage ) ) {
627
					// If the Title stored in the context is the same as the one
628
					// of the new revision, we can use its associated WikiPage
629
					// object.
630
					$wikiPage = $this->getWikiPage();
631
				} else {
632
					// Otherwise we need to create our own WikiPage object
633
					$wikiPage = WikiPage::factory( $this->mNewPage );
634
				}
635
636
				$parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRev );
637
638
				# WikiPage::getParserOutput() should not return false, but just in case
639
				if ( $parserOutput ) {
640
					// Allow extensions to change parser output here
641
					if ( !Hooks::run( 'DifferenceEngineRenderRevisionAddParserOutput', [ $this, $out, $parserOutput, $wikiPage ] ) ) {
642
						$out->addParserOutput( $parserOutput );
643
					}
644
				}
645
			}
646
		}
647
		# @codingStandardsIgnoreEnd
648
649
		// Allow extensions to optionally not show the final patrolled link
650
		if ( Hooks::run( 'DifferenceEngineRenderRevisionShowFinalPatrolLink' ) ) {
651
			# Add redundant patrol link on bottom...
652
			$out->addHTML( $this->markPatrolledLink() );
653
		}
654
	}
655
656
	protected function getParserOutput( WikiPage $page, Revision $rev ) {
657
		$parserOptions = $page->makeParserOptions( $this->getContext() );
658
659
		if ( !$rev->isCurrent() || !$rev->getTitle()->quickUserCan( 'edit', $this->getUser() ) ) {
660
			$parserOptions->setEditSection( false );
661
		}
662
663
		$parserOutput = $page->getParserOutput( $parserOptions, $rev->getId() );
664
665
		return $parserOutput;
666
	}
667
668
	/**
669
	 * Get the diff text, send it to the OutputPage object
670
	 * Returns false if the diff could not be generated, otherwise returns true
671
	 *
672
	 * @param string|bool $otitle Header for old text or false
673
	 * @param string|bool $ntitle Header for new text or false
674
	 * @param string $notice HTML between diff header and body
675
	 *
676
	 * @return bool
677
	 */
678
	public function showDiff( $otitle, $ntitle, $notice = '' ) {
679
		// Allow extensions to affect the output here
680
		Hooks::run( 'DifferenceEngineShowDiff', [ $this ] );
681
682
		$diff = $this->getDiff( $otitle, $ntitle, $notice );
683
		if ( $diff === false ) {
684
			$this->showMissingRevision();
685
686
			return false;
687
		} else {
688
			$this->showDiffStyle();
689
			$this->getOutput()->addHTML( $diff );
690
691
			return true;
692
		}
693
	}
694
695
	/**
696
	 * Add style sheets and supporting JS for diff display.
697
	 */
698
	public function showDiffStyle() {
699
		$this->getOutput()->addModuleStyles( 'mediawiki.action.history.diff' );
700
	}
701
702
	/**
703
	 * Get complete diff table, including header
704
	 *
705
	 * @param string|bool $otitle Header for old text or false
706
	 * @param string|bool $ntitle Header for new text or false
707
	 * @param string $notice HTML between diff header and body
708
	 *
709
	 * @return mixed
710
	 */
711
	public function getDiff( $otitle, $ntitle, $notice = '' ) {
712
		$body = $this->getDiffBody();
713
		if ( $body === false ) {
714
			return false;
715
		}
716
717
		$multi = $this->getMultiNotice();
718
		// Display a message when the diff is empty
719
		if ( $body === '' ) {
720
			$notice .= '<div class="mw-diff-empty">' .
721
				$this->msg( 'diff-empty' )->parse() .
722
				"</div>\n";
723
		}
724
725
		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 711 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 711 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...
726
	}
727
728
	/**
729
	 * Get the diff table body, without header
730
	 *
731
	 * @return mixed (string/false)
732
	 */
733
	public function getDiffBody() {
734
		$this->mCacheHit = true;
735
		// Check if the diff should be hidden from this user
736
		if ( !$this->loadRevisionData() ) {
737
			return false;
738
		} elseif ( $this->mOldRev &&
739
			!$this->mOldRev->userCan( Revision::DELETED_TEXT, $this->getUser() )
740
		) {
741
			return false;
742
		} elseif ( $this->mNewRev &&
743
			!$this->mNewRev->userCan( Revision::DELETED_TEXT, $this->getUser() )
744
		) {
745
			return false;
746
		}
747
		// Short-circuit
748
		if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev
749
			&& $this->mOldRev->getId() == $this->mNewRev->getId() )
750
		) {
751
			if ( !Hooks::run( 'DifferenceEngineShowEmptyOldContent', [ $this ] ) ) {
752
				return '';
753
			}
754
		}
755
		// Cacheable?
756
		$key = false;
757
		$cache = ObjectCache::getMainWANInstance();
758
		if ( $this->mOldid && $this->mNewid ) {
759
			$key = $this->getDiffBodyCacheKey();
760
761
			// Try cache
762
			if ( !$this->mRefreshCache ) {
763
				$difftext = $cache->get( $key );
764
				if ( $difftext ) {
765
					wfIncrStats( 'diff_cache.hit' );
766
					$difftext = $this->localiseLineNumbers( $difftext );
767
					$difftext .= "\n<!-- diff cache key $key -->\n";
768
769
					return $difftext;
770
				}
771
			} // don't try to load but save the result
772
		}
773
		$this->mCacheHit = false;
774
775
		// Loadtext is permission safe, this just clears out the diff
776
		if ( !$this->loadText() ) {
777
			return false;
778
		}
779
780
		$difftext = $this->generateContentDiffBody( $this->mOldContent, $this->mNewContent );
781
782
		// Save to cache for 7 days
783
		if ( !Hooks::run( 'AbortDiffCache', [ &$this ] ) ) {
784
			wfIncrStats( 'diff_cache.uncacheable' );
785
		} elseif ( $key !== false && $difftext !== false ) {
786
			wfIncrStats( 'diff_cache.miss' );
787
			$cache->set( $key, $difftext, 7 * 86400 );
788
		} else {
789
			wfIncrStats( 'diff_cache.uncacheable' );
790
		}
791
		// Replace line numbers with the text in the user's language
792
		if ( $difftext !== false ) {
793
			$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...
794
		}
795
796
		return $difftext;
797
	}
798
799
	/**
800
	 * Returns the cache key for diff body text or content.
801
	 *
802
	 * @since 1.23
803
	 *
804
	 * @throws MWException
805
	 * @return string
806
	 */
807
	protected function getDiffBodyCacheKey() {
808
		if ( !$this->mOldid || !$this->mNewid ) {
809
			throw new MWException( 'mOldid and mNewid must be set to get diff cache key.' );
810
		}
811
812
		return wfMemcKey( 'diff', 'version', MW_DIFF_VERSION,
813
			'oldid', $this->mOldid, 'newid', $this->mNewid );
814
	}
815
816
	/**
817
	 * Generate a diff, no caching.
818
	 *
819
	 * This implementation uses generateTextDiffBody() to generate a diff based on the default
820
	 * serialization of the given Content objects. This will fail if $old or $new are not
821
	 * instances of TextContent.
822
	 *
823
	 * Subclasses may override this to provide a different rendering for the diff,
824
	 * perhaps taking advantage of the content's native form. This is required for all content
825
	 * models that are not text based.
826
	 *
827
	 * @since 1.21
828
	 *
829
	 * @param Content $old Old content
830
	 * @param Content $new New content
831
	 *
832
	 * @throws MWException If old or new content is not an instance of TextContent.
833
	 * @return bool|string
834
	 */
835
	public function generateContentDiffBody( Content $old, Content $new ) {
836 View Code Duplication
		if ( !( $old instanceof TextContent ) ) {
837
			throw new MWException( "Diff not implemented for " . get_class( $old ) . "; " .
838
				"override generateContentDiffBody to fix this." );
839
		}
840
841 View Code Duplication
		if ( !( $new instanceof TextContent ) ) {
842
			throw new MWException( "Diff not implemented for " . get_class( $new ) . "; "
843
				. "override generateContentDiffBody to fix this." );
844
		}
845
846
		$otext = $old->serialize();
847
		$ntext = $new->serialize();
848
849
		return $this->generateTextDiffBody( $otext, $ntext );
850
	}
851
852
	/**
853
	 * Generate a diff, no caching
854
	 *
855
	 * @param string $otext Old text, must be already segmented
856
	 * @param string $ntext New text, must be already segmented
857
	 *
858
	 * @return bool|string
859
	 * @deprecated since 1.21, use generateContentDiffBody() instead!
860
	 */
861
	public function generateDiffBody( $otext, $ntext ) {
862
		ContentHandler::deprecated( __METHOD__, "1.21" );
863
864
		return $this->generateTextDiffBody( $otext, $ntext );
865
	}
866
867
	/**
868
	 * Generate a diff, no caching
869
	 *
870
	 * @todo move this to TextDifferenceEngine, make DifferenceEngine abstract. At some point.
871
	 *
872
	 * @param string $otext Old text, must be already segmented
873
	 * @param string $ntext New text, must be already segmented
874
	 *
875
	 * @return bool|string
876
	 */
877
	public function generateTextDiffBody( $otext, $ntext ) {
878
		$diff = function() use ( $otext, $ntext ) {
879
			$time = microtime( true );
880
881
			$result = $this->textDiff( $otext, $ntext );
882
883
			$time = intval( ( microtime( true ) - $time ) * 1000 );
884
			$this->getStats()->timing( 'diff_time', $time );
0 ignored issues
show
Deprecated Code introduced by
The method ContextSource::getStats() has been deprecated with message: since 1.27 use a StatsdDataFactory from MediaWikiServices (preferably injected)

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
885
			// Log requests slower than 99th percentile
886
			if ( $time > 100 && $this->mOldPage && $this->mNewPage ) {
887
				wfDebugLog( 'diff',
888
					"$time ms diff: {$this->mOldid} -> {$this->mNewid} {$this->mNewPage}" );
889
			}
890
891
			return $result;
892
		};
893
894
		$error = function( $status ) {
895
			throw new FatalError( $status->getWikiText() );
896
		};
897
898
		// Use PoolCounter if the diff looks like it can be expensive
899
		if ( strlen( $otext ) + strlen( $ntext ) > 20000 ) {
900
			$work = new PoolCounterWorkViaCallback( 'diff',
901
				md5( $otext ) . md5( $ntext ),
902
				[ 'doWork' => $diff, 'error' => $error ]
903
			);
904
			return $work->execute();
905
		}
906
907
		return $diff();
908
	}
909
910
	/**
911
	 * Generates diff, to be wrapped internally in a logging/instrumentation
912
	 *
913
	 * @param string $otext Old text, must be already segmented
914
	 * @param string $ntext New text, must be already segmented
915
	 * @return bool|string
916
	 */
917
	protected function textDiff( $otext, $ntext ) {
918
		global $wgExternalDiffEngine, $wgContLang;
919
920
		$otext = str_replace( "\r\n", "\n", $otext );
921
		$ntext = str_replace( "\r\n", "\n", $ntext );
922
923
		if ( $wgExternalDiffEngine == 'wikidiff' || $wgExternalDiffEngine == 'wikidiff3' ) {
924
			wfDeprecated( "\$wgExternalDiffEngine = '{$wgExternalDiffEngine}'", '1.27' );
925
			$wgExternalDiffEngine = false;
926
		}
927
928
		if ( $wgExternalDiffEngine == 'wikidiff2' ) {
929
			if ( function_exists( 'wikidiff2_do_diff' ) ) {
930
				# Better external diff engine, the 2 may some day be dropped
931
				# This one does the escaping and segmenting itself
932
				$text = wikidiff2_do_diff( $otext, $ntext, 2 );
933
				$text .= $this->debug( 'wikidiff2' );
934
935
				return $text;
936
			}
937
		} elseif ( $wgExternalDiffEngine !== false ) {
938
			# Diff via the shell
939
			$tmpDir = wfTempDir();
940
			$tempName1 = tempnam( $tmpDir, 'diff_' );
941
			$tempName2 = tempnam( $tmpDir, 'diff_' );
942
943
			$tempFile1 = fopen( $tempName1, "w" );
944
			if ( !$tempFile1 ) {
945
				return false;
946
			}
947
			$tempFile2 = fopen( $tempName2, "w" );
948
			if ( !$tempFile2 ) {
949
				return false;
950
			}
951
			fwrite( $tempFile1, $otext );
952
			fwrite( $tempFile2, $ntext );
953
			fclose( $tempFile1 );
954
			fclose( $tempFile2 );
955
			$cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 );
956
			$difftext = wfShellExec( $cmd );
957
			$difftext .= $this->debug( "external $wgExternalDiffEngine" );
958
			unlink( $tempName1 );
959
			unlink( $tempName2 );
960
961
			return $difftext;
962
		}
963
964
		# Native PHP diff
965
		$ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) );
966
		$nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) );
967
		$diffs = new Diff( $ota, $nta );
968
		$formatter = new TableDiffFormatter();
969
		$difftext = $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) );
970
971
		return $difftext;
972
	}
973
974
	/**
975
	 * Generate a debug comment indicating diff generating time,
976
	 * server node, and generator backend.
977
	 *
978
	 * @param string $generator : What diff engine was used
979
	 *
980
	 * @return string
981
	 */
982
	protected function debug( $generator = "internal" ) {
983
		global $wgShowHostnames;
984
		if ( !$this->enableDebugComment ) {
985
			return '';
986
		}
987
		$data = [ $generator ];
988
		if ( $wgShowHostnames ) {
989
			$data[] = wfHostname();
990
		}
991
		$data[] = wfTimestamp( TS_DB );
992
993
		return "<!-- diff generator: " .
994
			implode( " ", array_map( "htmlspecialchars", $data ) ) .
995
			" -->\n";
996
	}
997
998
	/**
999
	 * Replace line numbers with the text in the user's language
1000
	 *
1001
	 * @param string $text
1002
	 *
1003
	 * @return mixed
1004
	 */
1005
	public function localiseLineNumbers( $text ) {
1006
		return preg_replace_callback(
1007
			'/<!--LINE (\d+)-->/',
1008
			[ &$this, 'localiseLineNumbersCb' ],
1009
			$text
1010
		);
1011
	}
1012
1013
	public function localiseLineNumbersCb( $matches ) {
1014
		if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
1015
			return '';
1016
		}
1017
1018
		return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
1019
	}
1020
1021
	/**
1022
	 * If there are revisions between the ones being compared, return a note saying so.
1023
	 *
1024
	 * @return string
1025
	 */
1026
	public function getMultiNotice() {
1027
		if ( !is_object( $this->mOldRev ) || !is_object( $this->mNewRev ) ) {
1028
			return '';
1029
		} elseif ( !$this->mOldPage->equals( $this->mNewPage ) ) {
1030
			// Comparing two different pages? Count would be meaningless.
1031
			return '';
1032
		}
1033
1034
		if ( $this->mOldRev->getTimestamp() > $this->mNewRev->getTimestamp() ) {
1035
			$oldRev = $this->mNewRev; // flip
1036
			$newRev = $this->mOldRev; // flip
1037
		} else { // normal case
1038
			$oldRev = $this->mOldRev;
1039
			$newRev = $this->mNewRev;
1040
		}
1041
1042
		// Sanity: don't show the notice if too many rows must be scanned
1043
		// @todo show some special message for that case
1044
		$nEdits = $this->mNewPage->countRevisionsBetween( $oldRev, $newRev, 1000 );
1045
		if ( $nEdits > 0 && $nEdits <= 1000 ) {
1046
			$limit = 100; // use diff-multi-manyusers if too many users
1047
			$users = $this->mNewPage->getAuthorsBetween( $oldRev, $newRev, $limit );
1048
			$numUsers = count( $users );
1049
1050
			if ( $numUsers == 1 && $users[0] == $newRev->getUserText( Revision::RAW ) ) {
1051
				$numUsers = 0; // special case to say "by the same user" instead of "by one other user"
1052
			}
1053
1054
			return self::intermediateEditsMsg( $nEdits, $numUsers, $limit );
1055
		}
1056
1057
		return ''; // nothing
1058
	}
1059
1060
	/**
1061
	 * Get a notice about how many intermediate edits and users there are
1062
	 *
1063
	 * @param int $numEdits
1064
	 * @param int $numUsers
1065
	 * @param int $limit
1066
	 *
1067
	 * @return string
1068
	 */
1069
	public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) {
1070
		if ( $numUsers === 0 ) {
1071
			$msg = 'diff-multi-sameuser';
1072
		} elseif ( $numUsers > $limit ) {
1073
			$msg = 'diff-multi-manyusers';
1074
			$numUsers = $limit;
1075
		} else {
1076
			$msg = 'diff-multi-otherusers';
1077
		}
1078
1079
		return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1080
	}
1081
1082
	/**
1083
	 * Get a header for a specified revision.
1084
	 *
1085
	 * @param Revision $rev
1086
	 * @param string $complete 'complete' to get the header wrapped depending
1087
	 *        the visibility of the revision and a link to edit the page.
1088
	 *
1089
	 * @return string HTML fragment
1090
	 */
1091
	protected function getRevisionHeader( Revision $rev, $complete = '' ) {
1092
		$lang = $this->getLanguage();
1093
		$user = $this->getUser();
1094
		$revtimestamp = $rev->getTimestamp();
1095
		$timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
1096
		$dateofrev = $lang->userDate( $revtimestamp, $user );
1097
		$timeofrev = $lang->userTime( $revtimestamp, $user );
1098
1099
		$header = $this->msg(
1100
			$rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
1101
			$timestamp,
1102
			$dateofrev,
1103
			$timeofrev
1104
		)->escaped();
1105
1106
		if ( $complete !== 'complete' ) {
1107
			return $header;
1108
		}
1109
1110
		$title = $rev->getTitle();
1111
1112
		$header = Linker::linkKnown( $title, $header, [],
1113
			[ 'oldid' => $rev->getId() ] );
1114
1115
		if ( $rev->userCan( Revision::DELETED_TEXT, $user ) ) {
1116
			$editQuery = [ 'action' => 'edit' ];
1117
			if ( !$rev->isCurrent() ) {
1118
				$editQuery['oldid'] = $rev->getId();
1119
			}
1120
1121
			$key = $title->quickUserCan( 'edit', $user ) ? 'editold' : 'viewsourceold';
1122
			$msg = $this->msg( $key )->escaped();
1123
			$editLink = $this->msg( 'parentheses' )->rawParams(
1124
				Linker::linkKnown( $title, $msg, [], $editQuery ) )->escaped();
1125
			$header .= ' ' . Html::rawElement(
1126
				'span',
1127
				[ 'class' => 'mw-diff-edit' ],
1128
				$editLink
1129
			);
1130
			if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
1131
				$header = Html::rawElement(
1132
					'span',
1133
					[ 'class' => 'history-deleted' ],
1134
					$header
1135
				);
1136
			}
1137
		} else {
1138
			$header = Html::rawElement( 'span', [ 'class' => 'history-deleted' ], $header );
1139
		}
1140
1141
		return $header;
1142
	}
1143
1144
	/**
1145
	 * Add the header to a diff body
1146
	 *
1147
	 * @param string $diff Diff body
1148
	 * @param string $otitle Old revision header
1149
	 * @param string $ntitle New revision header
1150
	 * @param string $multi Notice telling user that there are intermediate
1151
	 *   revisions between the ones being compared
1152
	 * @param string $notice Other notices, e.g. that user is viewing deleted content
1153
	 *
1154
	 * @return string
1155
	 */
1156
	public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
1157
		// shared.css sets diff in interface language/dir, but the actual content
1158
		// is often in a different language, mostly the page content language/dir
1159
		$header = Html::openElement( 'table', [
1160
			'class' => [ 'diff', 'diff-contentalign-' . $this->getDiffLang()->alignStart() ],
1161
			'data-mw' => 'interface',
1162
		] );
1163
		$userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
1164
1165
		if ( !$diff && !$otitle ) {
1166
			$header .= "
1167
			<tr style='vertical-align: top;' lang='{$userLang}'>
1168
			<td class='diff-ntitle'>{$ntitle}</td>
1169
			</tr>";
1170
			$multiColspan = 1;
1171
		} else {
1172
			if ( $diff ) { // Safari/Chrome show broken output if cols not used
1173
				$header .= "
1174
				<col class='diff-marker' />
1175
				<col class='diff-content' />
1176
				<col class='diff-marker' />
1177
				<col class='diff-content' />";
1178
				$colspan = 2;
1179
				$multiColspan = 4;
1180
			} else {
1181
				$colspan = 1;
1182
				$multiColspan = 2;
1183
			}
1184
			if ( $otitle || $ntitle ) {
1185
				$header .= "
1186
				<tr style='vertical-align: top;' lang='{$userLang}'>
1187
				<td colspan='$colspan' class='diff-otitle'>{$otitle}</td>
1188
				<td colspan='$colspan' class='diff-ntitle'>{$ntitle}</td>
1189
				</tr>";
1190
			}
1191
		}
1192
1193
		if ( $multi != '' ) {
1194
			$header .= "<tr><td colspan='{$multiColspan}' style='text-align: center;' " .
1195
				"class='diff-multi' lang='{$userLang}'>{$multi}</td></tr>";
1196
		}
1197
		if ( $notice != '' ) {
1198
			$header .= "<tr><td colspan='{$multiColspan}' style='text-align: center;' " .
1199
				"lang='{$userLang}'>{$notice}</td></tr>";
1200
		}
1201
1202
		return $header . $diff . "</table>";
1203
	}
1204
1205
	/**
1206
	 * Use specified text instead of loading from the database
1207
	 * @param Content $oldContent
1208
	 * @param Content $newContent
1209
	 * @since 1.21
1210
	 */
1211
	public function setContent( Content $oldContent, Content $newContent ) {
1212
		$this->mOldContent = $oldContent;
1213
		$this->mNewContent = $newContent;
1214
1215
		$this->mTextLoaded = 2;
1216
		$this->mRevisionsLoaded = true;
1217
	}
1218
1219
	/**
1220
	 * Set the language in which the diff text is written
1221
	 * (Defaults to page content language).
1222
	 * @param Language|string $lang
1223
	 * @since 1.19
1224
	 */
1225
	public function setTextLanguage( $lang ) {
1226
		$this->mDiffLang = wfGetLangObj( $lang );
1227
	}
1228
1229
	/**
1230
	 * Maps a revision pair definition as accepted by DifferenceEngine constructor
1231
	 * to a pair of actual integers representing revision ids.
1232
	 *
1233
	 * @param int $old Revision id, e.g. from URL parameter 'oldid'
1234
	 * @param int|string $new Revision id or strings 'next' or 'prev', e.g. from URL parameter 'diff'
1235
	 *
1236
	 * @return int[] List of two revision ids, older first, later second.
1237
	 *     Zero signifies invalid argument passed.
1238
	 *     false signifies that there is no previous/next revision ($old is the oldest/newest one).
1239
	 */
1240
	public function mapDiffPrevNext( $old, $new ) {
1241
		if ( $new === 'prev' ) {
1242
			// Show diff between revision $old and the previous one. Get previous one from DB.
1243
			$newid = intval( $old );
1244
			$oldid = $this->getTitle()->getPreviousRevisionID( $newid );
1245
		} elseif ( $new === 'next' ) {
1246
			// Show diff between revision $old and the next one. Get next one from DB.
1247
			$oldid = intval( $old );
1248
			$newid = $this->getTitle()->getNextRevisionID( $oldid );
1249
		} else {
1250
			$oldid = intval( $old );
1251
			$newid = intval( $new );
1252
		}
1253
1254
		return [ $oldid, $newid ];
1255
	}
1256
1257
	/**
1258
	 * Load revision IDs
1259
	 */
1260
	private function loadRevisionIds() {
1261
		if ( $this->mRevisionsIdsLoaded ) {
1262
			return;
1263
		}
1264
1265
		$this->mRevisionsIdsLoaded = true;
1266
1267
		$old = $this->mOldid;
1268
		$new = $this->mNewid;
1269
1270
		list( $this->mOldid, $this->mNewid ) = self::mapDiffPrevNext( $old, $new );
1271
		if ( $new === 'next' && $this->mNewid === false ) {
1272
			# if no result, NewId points to the newest old revision. The only newer
1273
			# revision is cur, which is "0".
1274
			$this->mNewid = 0;
1275
		}
1276
1277
		Hooks::run(
1278
			'NewDifferenceEngine',
1279
			[ $this->getTitle(), &$this->mOldid, &$this->mNewid, $old, $new ]
1280
		);
1281
	}
1282
1283
	/**
1284
	 * Load revision metadata for the specified articles. If newid is 0, then compare
1285
	 * the old article in oldid to the current article; if oldid is 0, then
1286
	 * compare the current article to the immediately previous one (ignoring the
1287
	 * value of newid).
1288
	 *
1289
	 * If oldid is false, leave the corresponding revision object set
1290
	 * to false. This is impossible via ordinary user input, and is provided for
1291
	 * API convenience.
1292
	 *
1293
	 * @return bool
1294
	 */
1295
	public function loadRevisionData() {
1296
		if ( $this->mRevisionsLoaded ) {
1297
			return true;
1298
		}
1299
1300
		// Whether it succeeds or fails, we don't want to try again
1301
		$this->mRevisionsLoaded = true;
1302
1303
		$this->loadRevisionIds();
1304
1305
		// Load the new revision object
1306
		if ( $this->mNewid ) {
1307
			$this->mNewRev = Revision::newFromId( $this->mNewid );
1308
		} else {
1309
			$this->mNewRev = Revision::newFromTitle(
1310
				$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...
1311
				false,
1312
				Revision::READ_NORMAL
1313
			);
1314
		}
1315
1316
		if ( !$this->mNewRev instanceof Revision ) {
1317
			return false;
1318
		}
1319
1320
		// Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
1321
		$this->mNewid = $this->mNewRev->getId();
1322
		$this->mNewPage = $this->mNewRev->getTitle();
1323
1324
		// Load the old revision object
1325
		$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...
1326
		if ( $this->mOldid ) {
1327
			$this->mOldRev = Revision::newFromId( $this->mOldid );
1328
		} elseif ( $this->mOldid === 0 ) {
1329
			$rev = $this->mNewRev->getPrevious();
1330
			if ( $rev ) {
1331
				$this->mOldid = $rev->getId();
1332
				$this->mOldRev = $rev;
1333
			} else {
1334
				// No previous revision; mark to show as first-version only.
1335
				$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...
1336
				$this->mOldRev = false;
1337
			}
1338
		} /* elseif ( $this->mOldid === false ) leave mOldRev false; */
1339
1340
		if ( is_null( $this->mOldRev ) ) {
1341
			return false;
1342
		}
1343
1344
		if ( $this->mOldRev ) {
1345
			$this->mOldPage = $this->mOldRev->getTitle();
1346
		}
1347
1348
		// Load tags information for both revisions
1349
		$dbr = wfGetDB( DB_SLAVE );
1350
		if ( $this->mOldid !== false ) {
1351
			$this->mOldTags = $dbr->selectField(
1352
				'tag_summary',
1353
				'ts_tags',
1354
				[ 'ts_rev_id' => $this->mOldid ],
1355
				__METHOD__
1356
			);
1357
		} else {
1358
			$this->mOldTags = false;
1359
		}
1360
		$this->mNewTags = $dbr->selectField(
1361
			'tag_summary',
1362
			'ts_tags',
1363
			[ 'ts_rev_id' => $this->mNewid ],
1364
			__METHOD__
1365
		);
1366
1367
		return true;
1368
	}
1369
1370
	/**
1371
	 * Load the text of the revisions, as well as revision data.
1372
	 *
1373
	 * @return bool
1374
	 */
1375
	public function loadText() {
1376
		if ( $this->mTextLoaded == 2 ) {
1377
			return true;
1378
		}
1379
1380
		// Whether it succeeds or fails, we don't want to try again
1381
		$this->mTextLoaded = 2;
1382
1383
		if ( !$this->loadRevisionData() ) {
1384
			return false;
1385
		}
1386
1387 View Code Duplication
		if ( $this->mOldRev ) {
1388
			$this->mOldContent = $this->mOldRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
1389
			if ( $this->mOldContent === null ) {
1390
				return false;
1391
			}
1392
		}
1393
1394 View Code Duplication
		if ( $this->mNewRev ) {
1395
			$this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
1396
			if ( $this->mNewContent === null ) {
1397
				return false;
1398
			}
1399
		}
1400
1401
		return true;
1402
	}
1403
1404
	/**
1405
	 * Load the text of the new revision, not the old one
1406
	 *
1407
	 * @return bool
1408
	 */
1409
	public function loadNewText() {
1410
		if ( $this->mTextLoaded >= 1 ) {
1411
			return true;
1412
		}
1413
1414
		$this->mTextLoaded = 1;
1415
1416
		if ( !$this->loadRevisionData() ) {
1417
			return false;
1418
		}
1419
1420
		$this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
1421
1422
		return true;
1423
	}
1424
1425
}
1426