Completed
Branch master (62f6c6)
by
unknown
21:31
created

DifferenceEngine   F

Complexity

Total Complexity 194

Size/Duplication

Total Lines 1358
Duplicated Lines 3.83 %

Coupling/Cohesion

Components 1
Dependencies 30

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 52
loc 1358
rs 0.5217
wmc 194
lcom 1
cbo 30

38 Methods

Rating   Name   Duplication   Size   Complexity  
A markPatrolledLink() 0 22 3
B generateTextDiffBody() 0 32 5
A __construct() 0 14 2
A setReducedLineNumbers() 0 3 1
A getDiffLang() 0 8 2
A wasCacheHit() 0 3 1
A getOldid() 0 5 1
A getNewid() 0 5 1
A deletedLink() 0 19 3
A deletedIdMarker() 0 8 2
C showMissingRevision() 0 22 7
F showDiffPage() 32 222 37
C getMarkPatrolledLinkInfo() 0 51 10
A revisionDeleteLink() 0 8 2
C renderNewRevision() 0 59 11
A getParserOutput() 0 11 3
A showDiff() 0 13 2
A showDiffStyle() 0 3 1
A getDiff() 0 16 3
C getDiffBody() 0 63 19
A getDiffBodyCacheKey() 0 8 3
A generateContentDiffBody() 8 16 3
A generateDiffBody() 0 5 1
B textDiff() 0 56 8
A debug() 0 15 3
A localiseLineNumbers() 0 7 1
A localiseLineNumbersCb() 0 7 3
D getMultiNotice() 0 33 9
A intermediateEditsMsg() 0 12 3
B getRevisionHeader() 0 52 7
C addHeader() 0 48 8
A setContent() 0 7 1
A setTextLanguage() 0 3 1
A mapDiffPrevNext() 0 16 3
B loadRevisionIds() 0 22 4
C loadRevisionData() 0 74 10
C loadText() 12 28 7
A loadNewText() 0 15 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like DifferenceEngine often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DifferenceEngine, and based on these observations, apply Extract Interface, too.

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
240
		# Allow frames except in certain special cases
241
		$out = $this->getOutput();
242
		$out->allowClickjacking();
243
		$out->setRobotPolicy( 'noindex,nofollow' );
244
245
		if ( !$this->loadRevisionData() ) {
246
			$this->showMissingRevision();
247
248
			return;
249
		}
250
251
		$user = $this->getUser();
252
		$permErrors = $this->mNewPage->getUserPermissionsErrors( 'read', $user );
253
		if ( $this->mOldPage ) { # mOldPage might not be set, see below.
254
			$permErrors = wfMergeErrorArrays( $permErrors,
255
				$this->mOldPage->getUserPermissionsErrors( 'read', $user ) );
256
		}
257
		if ( count( $permErrors ) ) {
258
			throw new PermissionsError( 'read', $permErrors );
259
		}
260
261
		$rollback = '';
262
263
		$query = [];
264
		# Carry over 'diffonly' param via navigation links
265
		if ( $diffOnly != $user->getBoolOption( 'diffonly' ) ) {
266
			$query['diffonly'] = $diffOnly;
267
		}
268
		# Cascade unhide param in links for easy deletion browsing
269
		if ( $this->unhide ) {
270
			$query['unhide'] = 1;
271
		}
272
273
		# Check if one of the revisions is deleted/suppressed
274
		$deleted = $suppressed = false;
275
		$allowed = $this->mNewRev->userCan( Revision::DELETED_TEXT, $user );
276
277
		$revisionTools = [];
278
279
		# mOldRev is false if the difference engine is called with a "vague" query for
280
		# a diff between a version V and its previous version V' AND the version V
281
		# is the first version of that article. In that case, V' does not exist.
282
		if ( $this->mOldRev === false ) {
283
			$out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
284
			$samePage = true;
285
			$oldHeader = '';
286
		} else {
287
			Hooks::run( 'DiffViewHeader', [ $this, $this->mOldRev, $this->mNewRev ] );
288
289
			if ( $this->mNewPage->equals( $this->mOldPage ) ) {
290
				$out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
291
				$samePage = true;
292
			} else {
293
				$out->setPageTitle( $this->msg( 'difference-title-multipage',
294
					$this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) );
295
				$out->addSubtitle( $this->msg( 'difference-multipage' ) );
296
				$samePage = false;
297
			}
298
299
			if ( $samePage && $this->mNewPage->quickUserCan( 'edit', $user ) ) {
300
				if ( $this->mNewRev->isCurrent() && $this->mNewPage->userCan( 'rollback', $user ) ) {
301
					$rollbackLink = Linker::generateRollback( $this->mNewRev, $this->getContext() );
302
					if ( $rollbackLink ) {
303
						$out->preventClickjacking();
304
						$rollback = '&#160;&#160;&#160;' . $rollbackLink;
305
					}
306
				}
307
308
				if ( !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) &&
309
					!$this->mNewRev->isDeleted( Revision::DELETED_TEXT )
310
				) {
311
					$undoLink = Html::element( 'a', [
312
							'href' => $this->mNewPage->getLocalURL( [
313
								'action' => 'edit',
314
								'undoafter' => $this->mOldid,
315
								'undo' => $this->mNewid
316
							] ),
317
							'title' => Linker::titleAttrib( 'undo' ),
318
						],
319
						$this->msg( 'editundo' )->text()
320
					);
321
					$revisionTools['mw-diff-undo'] = $undoLink;
322
				}
323
			}
324
325
			# Make "previous revision link"
326 View Code Duplication
			if ( $samePage && $this->mOldRev->getPrevious() ) {
327
				$prevlink = Linker::linkKnown(
328
					$this->mOldPage,
329
					$this->msg( 'previousdiff' )->escaped(),
330
					[ 'id' => 'differences-prevlink' ],
331
					[ 'diff' => 'prev', 'oldid' => $this->mOldid ] + $query
332
				);
333
			} else {
334
				$prevlink = '&#160;';
335
			}
336
337
			if ( $this->mOldRev->isMinor() ) {
338
				$oldminor = ChangesList::flag( 'minor' );
339
			} else {
340
				$oldminor = '';
341
			}
342
343
			$ldel = $this->revisionDeleteLink( $this->mOldRev );
344
			$oldRevisionHeader = $this->getRevisionHeader( $this->mOldRev, 'complete' );
345
			$oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff', $this->getContext() );
346
347
			$oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' .
348
				'<div id="mw-diff-otitle2">' .
349
				Linker::revUserTools( $this->mOldRev, !$this->unhide ) . '</div>' .
350
				'<div id="mw-diff-otitle3">' . $oldminor .
351
				Linker::revComment( $this->mOldRev, !$diffOnly, !$this->unhide ) . $ldel . '</div>' .
352
				'<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' .
353
				'<div id="mw-diff-otitle4">' . $prevlink . '</div>';
354
355 View Code Duplication
			if ( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) {
356
				$deleted = true; // old revisions text is hidden
357
				if ( $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
358
					$suppressed = true; // also suppressed
359
				}
360
			}
361
362
			# Check if this user can see the revisions
363
			if ( !$this->mOldRev->userCan( Revision::DELETED_TEXT, $user ) ) {
364
				$allowed = false;
365
			}
366
		}
367
368
		# Make "next revision link"
369
		# Skip next link on the top revision
370 View Code Duplication
		if ( $samePage && !$this->mNewRev->isCurrent() ) {
371
			$nextlink = Linker::linkKnown(
372
				$this->mNewPage,
373
				$this->msg( 'nextdiff' )->escaped(),
374
				[ 'id' => 'differences-nextlink' ],
375
				[ 'diff' => 'next', 'oldid' => $this->mNewid ] + $query
376
			);
377
		} else {
378
			$nextlink = '&#160;';
379
		}
380
381
		if ( $this->mNewRev->isMinor() ) {
382
			$newminor = ChangesList::flag( 'minor' );
383
		} else {
384
			$newminor = '';
385
		}
386
387
		# Handle RevisionDelete links...
388
		$rdel = $this->revisionDeleteLink( $this->mNewRev );
389
390
		# Allow extensions to define their own revision tools
391
		Hooks::run( 'DiffRevisionTools',
392
			[ $this->mNewRev, &$revisionTools, $this->mOldRev, $user ] );
393
		$formattedRevisionTools = [];
394
		// Put each one in parentheses (poor man's button)
395
		foreach ( $revisionTools as $key => $tool ) {
396
			$toolClass = is_string( $key ) ? $key : 'mw-diff-tool';
397
			$element = Html::rawElement(
398
				'span',
399
				[ 'class' => $toolClass ],
400
				$this->msg( 'parentheses' )->rawParams( $tool )->escaped()
401
			);
402
			$formattedRevisionTools[] = $element;
403
		}
404
		$newRevisionHeader = $this->getRevisionHeader( $this->mNewRev, 'complete' ) .
405
			' ' . implode( ' ', $formattedRevisionTools );
406
		$newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff', $this->getContext() );
407
408
		$newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' .
409
			'<div id="mw-diff-ntitle2">' . Linker::revUserTools( $this->mNewRev, !$this->unhide ) .
410
			" $rollback</div>" .
411
			'<div id="mw-diff-ntitle3">' . $newminor .
412
			Linker::revComment( $this->mNewRev, !$diffOnly, !$this->unhide ) . $rdel . '</div>' .
413
			'<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' .
414
			'<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
415
416 View Code Duplication
		if ( $this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
417
			$deleted = true; // new revisions text is hidden
418
			if ( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
419
				$suppressed = true; // also suppressed
420
			}
421
		}
422
423
		# If the diff cannot be shown due to a deleted revision, then output
424
		# the diff header and links to unhide (if available)...
425
		if ( $deleted && ( !$this->unhide || !$allowed ) ) {
426
			$this->showDiffStyle();
427
			$multi = $this->getMultiNotice();
428
			$out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
429
			if ( !$allowed ) {
430
				$msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff';
431
				# Give explanation for why revision is not visible
432
				$out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
433
					[ $msg ] );
434
			} else {
435
				# Give explanation and add a link to view the diff...
436
				$query = $this->getRequest()->appendQueryValue( 'unhide', '1' );
437
				$link = $this->getTitle()->getFullURL( $query );
438
				$msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff';
439
				$out->wrapWikiMsg(
440
					"<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
441
					[ $msg, $link ]
442
				);
443
			}
444
		# Otherwise, output a regular diff...
445
		} else {
446
			# Add deletion notice if the user is viewing deleted content
447
			$notice = '';
448
			if ( $deleted ) {
449
				$msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
450
				$notice = "<div id='mw-$msg' class='mw-warning plainlinks'>\n" .
451
					$this->msg( $msg )->parse() .
452
					"</div>\n";
453
			}
454
			$this->showDiff( $oldHeader, $newHeader, $notice );
455
			if ( !$diffOnly ) {
456
				$this->renderNewRevision();
457
			}
458
		}
459
	}
460
461
	/**
462
	 * Build a link to mark a change as patrolled.
463
	 *
464
	 * Returns empty string if there's either no revision to patrol or the user is not allowed to.
465
	 * Side effect: When the patrol link is build, this method will call
466
	 * OutputPage::preventClickjacking() and load mediawiki.page.patrol.ajax.
467
	 *
468
	 * @return string HTML or empty string
469
	 */
470
	protected function markPatrolledLink() {
471
		if ( $this->mMarkPatrolledLink === null ) {
472
			$linkInfo = $this->getMarkPatrolledLinkInfo();
473
			// If false, there is no patrol link needed/allowed
474
			if ( !$linkInfo ) {
475
				$this->mMarkPatrolledLink = '';
476
			} else {
477
				$this->mMarkPatrolledLink = ' <span class="patrollink" data-mw="interface">[' .
478
					Linker::linkKnown(
479
						$this->mNewPage,
480
						$this->msg( 'markaspatrolleddiff' )->escaped(),
481
						[],
482
						[
483
							'action' => 'markpatrolled',
484
							'rcid' => $linkInfo['rcid'],
485
							'token' => $linkInfo['token'],
486
						]
487
					) . ']</span>';
488
			}
489
		}
490
		return $this->mMarkPatrolledLink;
491
	}
492
493
	/**
494
	 * Returns an array of meta data needed to build a "mark as patrolled" link and
495
	 * adds the mediawiki.page.patrol.ajax to the output.
496
	 *
497
	 * @return array|false An array of meta data for a patrol link (rcid & token)
498
	 *  or false if no link is needed
499
	 */
500
	protected function getMarkPatrolledLinkInfo() {
501
		global $wgUseRCPatrol, $wgEnableAPI, $wgEnableWriteAPI;
502
503
		$user = $this->getUser();
504
505
		// Prepare a change patrol link, if applicable
506
		if (
507
			// Is patrolling enabled and the user allowed to?
508
			$wgUseRCPatrol && $this->mNewPage->quickUserCan( 'patrol', $user ) &&
509
			// Only do this if the revision isn't more than 6 hours older
510
			// than the Max RC age (6h because the RC might not be cleaned out regularly)
511
			RecentChange::isInRCLifespan( $this->mNewRev->getTimestamp(), 21600 )
512
		) {
513
			// Look for an unpatrolled change corresponding to this diff
514
			$db = wfGetDB( DB_SLAVE );
515
			$change = RecentChange::newFromConds(
516
				[
517
					'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
518
					'rc_this_oldid' => $this->mNewid,
519
					'rc_patrolled' => 0
520
				],
521
				__METHOD__
522
			);
523
524
			if ( $change && !$change->getPerformer()->equals( $user ) ) {
525
				$rcid = $change->getAttribute( 'rc_id' );
526
			} else {
527
				// None found or the page has been created by the current user.
528
				// If the user could patrol this it already would be patrolled
529
				$rcid = 0;
530
			}
531
			// Build the link
532
			if ( $rcid ) {
533
				$this->getOutput()->preventClickjacking();
534
				if ( $wgEnableAPI && $wgEnableWriteAPI
535
					&& $user->isAllowed( 'writeapi' )
536
				) {
537
					$this->getOutput()->addModules( 'mediawiki.page.patrol.ajax' );
538
				}
539
540
				$token = $user->getEditToken( $rcid );
541
				return [
542
					'rcid' => $rcid,
543
					'token' => $token,
544
				];
545
			}
546
		}
547
548
		// No mark as patrolled link applicable
549
		return false;
550
	}
551
552
	/**
553
	 * @param Revision $rev
554
	 *
555
	 * @return string
556
	 */
557
	protected function revisionDeleteLink( $rev ) {
558
		$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...
559
		if ( $link !== '' ) {
560
			$link = '&#160;&#160;&#160;' . $link . ' ';
561
		}
562
563
		return $link;
564
	}
565
566
	/**
567
	 * Show the new revision of the page.
568
	 */
569
	public function renderNewRevision() {
570
		$out = $this->getOutput();
571
		$revHeader = $this->getRevisionHeader( $this->mNewRev );
572
		# Add "current version as of X" title
573
		$out->addHTML( "<hr class='diff-hr' id='mw-oldid' />
574
		<h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
575
		# Page content may be handled by a hooked call instead...
576
		# @codingStandardsIgnoreStart Ignoring long lines.
577
		if ( Hooks::run( 'ArticleContentOnDiff', [ $this, $out ] ) ) {
578
			$this->loadNewText();
579
			$out->setRevisionId( $this->mNewid );
580
			$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...
581
			$out->setArticleFlag( true );
582
583
			// NOTE: only needed for B/C: custom rendering of JS/CSS via hook
584
			if ( $this->mNewPage->isCssJsSubpage() || $this->mNewPage->isCssOrJsPage() ) {
585
				// This needs to be synchronised with Article::showCssOrJsPage(), which sucks
586
				// Give hooks a chance to customise the output
587
				// @todo standardize this crap into one function
588
				if ( ContentHandler::runLegacyHooks( 'ShowRawCssJs', [ $this->mNewContent, $this->mNewPage, $out ] ) ) {
589
					// NOTE: deprecated hook, B/C only
590
					// use the content object's own rendering
591
					$cnt = $this->mNewRev->getContent();
592
					$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...
593
					if ( $po ) {
594
						$out->addParserOutputContent( $po );
595
					}
596
				}
597
			} elseif ( !Hooks::run( 'ArticleContentViewCustom', [ $this->mNewContent, $this->mNewPage, $out ] ) ) {
598
				// Handled by extension
599
			} elseif ( !ContentHandler::runLegacyHooks( 'ArticleViewCustom', [ $this->mNewContent, $this->mNewPage, $out ] ) ) {
600
				// NOTE: deprecated hook, B/C only
601
				// Handled by extension
602
			} else {
603
				// Normal page
604
				if ( $this->getTitle()->equals( $this->mNewPage ) ) {
605
					// If the Title stored in the context is the same as the one
606
					// of the new revision, we can use its associated WikiPage
607
					// object.
608
					$wikiPage = $this->getWikiPage();
609
				} else {
610
					// Otherwise we need to create our own WikiPage object
611
					$wikiPage = WikiPage::factory( $this->mNewPage );
612
				}
613
614
				$parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRev );
615
616
				# WikiPage::getParserOutput() should not return false, but just in case
617
				if ( $parserOutput ) {
618
					$out->addParserOutput( $parserOutput );
619
				}
620
			}
621
		}
622
		# @codingStandardsIgnoreEnd
623
624
		# Add redundant patrol link on bottom...
625
		$out->addHTML( $this->markPatrolledLink() );
626
627
	}
628
629
	protected function getParserOutput( WikiPage $page, Revision $rev ) {
630
		$parserOptions = $page->makeParserOptions( $this->getContext() );
631
632
		if ( !$rev->isCurrent() || !$rev->getTitle()->quickUserCan( 'edit', $this->getUser() ) ) {
633
			$parserOptions->setEditSection( false );
634
		}
635
636
		$parserOutput = $page->getParserOutput( $parserOptions, $rev->getId() );
637
638
		return $parserOutput;
639
	}
640
641
	/**
642
	 * Get the diff text, send it to the OutputPage object
643
	 * Returns false if the diff could not be generated, otherwise returns true
644
	 *
645
	 * @param string|bool $otitle Header for old text or false
646
	 * @param string|bool $ntitle Header for new text or false
647
	 * @param string $notice HTML between diff header and body
648
	 *
649
	 * @return bool
650
	 */
651
	public function showDiff( $otitle, $ntitle, $notice = '' ) {
652
		$diff = $this->getDiff( $otitle, $ntitle, $notice );
653
		if ( $diff === false ) {
654
			$this->showMissingRevision();
655
656
			return false;
657
		} else {
658
			$this->showDiffStyle();
659
			$this->getOutput()->addHTML( $diff );
660
661
			return true;
662
		}
663
	}
664
665
	/**
666
	 * Add style sheets and supporting JS for diff display.
667
	 */
668
	public function showDiffStyle() {
669
		$this->getOutput()->addModuleStyles( 'mediawiki.action.history.diff' );
670
	}
671
672
	/**
673
	 * Get complete diff table, including header
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 mixed
680
	 */
681
	public function getDiff( $otitle, $ntitle, $notice = '' ) {
682
		$body = $this->getDiffBody();
683
		if ( $body === false ) {
684
			return false;
685
		}
686
687
		$multi = $this->getMultiNotice();
688
		// Display a message when the diff is empty
689
		if ( $body === '' ) {
690
			$notice .= '<div class="mw-diff-empty">' .
691
				$this->msg( 'diff-empty' )->parse() .
692
				"</div>\n";
693
		}
694
695
		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 681 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 681 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...
696
	}
697
698
	/**
699
	 * Get the diff table body, without header
700
	 *
701
	 * @return mixed (string/false)
702
	 */
703
	public function getDiffBody() {
704
		$this->mCacheHit = true;
705
		// Check if the diff should be hidden from this user
706
		if ( !$this->loadRevisionData() ) {
707
			return false;
708
		} elseif ( $this->mOldRev &&
709
			!$this->mOldRev->userCan( Revision::DELETED_TEXT, $this->getUser() )
710
		) {
711
			return false;
712
		} elseif ( $this->mNewRev &&
713
			!$this->mNewRev->userCan( Revision::DELETED_TEXT, $this->getUser() )
714
		) {
715
			return false;
716
		}
717
		// Short-circuit
718
		if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev
719
			&& $this->mOldRev->getId() == $this->mNewRev->getId() )
720
		) {
721
			return '';
722
		}
723
		// Cacheable?
724
		$key = false;
725
		$cache = ObjectCache::getMainWANInstance();
726
		if ( $this->mOldid && $this->mNewid ) {
727
			$key = $this->getDiffBodyCacheKey();
728
729
			// Try cache
730
			if ( !$this->mRefreshCache ) {
731
				$difftext = $cache->get( $key );
732
				if ( $difftext ) {
733
					wfIncrStats( 'diff_cache.hit' );
734
					$difftext = $this->localiseLineNumbers( $difftext );
735
					$difftext .= "\n<!-- diff cache key $key -->\n";
736
737
					return $difftext;
738
				}
739
			} // don't try to load but save the result
740
		}
741
		$this->mCacheHit = false;
742
743
		// Loadtext is permission safe, this just clears out the diff
744
		if ( !$this->loadText() ) {
745
			return false;
746
		}
747
748
		$difftext = $this->generateContentDiffBody( $this->mOldContent, $this->mNewContent );
749
750
		// Save to cache for 7 days
751
		if ( !Hooks::run( 'AbortDiffCache', [ &$this ] ) ) {
752
			wfIncrStats( 'diff_cache.uncacheable' );
753
		} elseif ( $key !== false && $difftext !== false ) {
754
			wfIncrStats( 'diff_cache.miss' );
755
			$cache->set( $key, $difftext, 7 * 86400 );
756
		} else {
757
			wfIncrStats( 'diff_cache.uncacheable' );
758
		}
759
		// Replace line numbers with the text in the user's language
760
		if ( $difftext !== false ) {
761
			$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...
762
		}
763
764
		return $difftext;
765
	}
766
767
	/**
768
	 * Returns the cache key for diff body text or content.
769
	 *
770
	 * @since 1.23
771
	 *
772
	 * @throws MWException
773
	 * @return string
774
	 */
775
	protected function getDiffBodyCacheKey() {
776
		if ( !$this->mOldid || !$this->mNewid ) {
777
			throw new MWException( 'mOldid and mNewid must be set to get diff cache key.' );
778
		}
779
780
		return wfMemcKey( 'diff', 'version', MW_DIFF_VERSION,
781
			'oldid', $this->mOldid, 'newid', $this->mNewid );
782
	}
783
784
	/**
785
	 * Generate a diff, no caching.
786
	 *
787
	 * This implementation uses generateTextDiffBody() to generate a diff based on the default
788
	 * serialization of the given Content objects. This will fail if $old or $new are not
789
	 * instances of TextContent.
790
	 *
791
	 * Subclasses may override this to provide a different rendering for the diff,
792
	 * perhaps taking advantage of the content's native form. This is required for all content
793
	 * models that are not text based.
794
	 *
795
	 * @since 1.21
796
	 *
797
	 * @param Content $old Old content
798
	 * @param Content $new New content
799
	 *
800
	 * @throws MWException If old or new content is not an instance of TextContent.
801
	 * @return bool|string
802
	 */
803
	public function generateContentDiffBody( Content $old, Content $new ) {
804 View Code Duplication
		if ( !( $old instanceof TextContent ) ) {
805
			throw new MWException( "Diff not implemented for " . get_class( $old ) . "; " .
806
				"override generateContentDiffBody to fix this." );
807
		}
808
809 View Code Duplication
		if ( !( $new instanceof TextContent ) ) {
810
			throw new MWException( "Diff not implemented for " . get_class( $new ) . "; "
811
				. "override generateContentDiffBody to fix this." );
812
		}
813
814
		$otext = $old->serialize();
815
		$ntext = $new->serialize();
816
817
		return $this->generateTextDiffBody( $otext, $ntext );
818
	}
819
820
	/**
821
	 * Generate a diff, no caching
822
	 *
823
	 * @param string $otext Old text, must be already segmented
824
	 * @param string $ntext New text, must be already segmented
825
	 *
826
	 * @return bool|string
827
	 * @deprecated since 1.21, use generateContentDiffBody() instead!
828
	 */
829
	public function generateDiffBody( $otext, $ntext ) {
830
		ContentHandler::deprecated( __METHOD__, "1.21" );
831
832
		return $this->generateTextDiffBody( $otext, $ntext );
833
	}
834
835
	/**
836
	 * Generate a diff, no caching
837
	 *
838
	 * @todo move this to TextDifferenceEngine, make DifferenceEngine abstract. At some point.
839
	 *
840
	 * @param string $otext Old text, must be already segmented
841
	 * @param string $ntext New text, must be already segmented
842
	 *
843
	 * @return bool|string
844
	 */
845
	public function generateTextDiffBody( $otext, $ntext ) {
846
		$diff = function() use ( $otext, $ntext ) {
847
			$time = microtime( true );
848
849
			$result = $this->textDiff( $otext, $ntext );
850
851
			$time = intval( ( microtime( true ) - $time ) * 1000 );
852
			$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...
853
			// Log requests slower than 99th percentile
854
			if ( $time > 100 && $this->mOldPage && $this->mNewPage ) {
855
				wfDebugLog( 'diff',
856
					"$time ms diff: {$this->mOldid} -> {$this->mNewid} {$this->mNewPage}" );
857
			}
858
859
			return $result;
860
		};
861
862
		$error = function( $status ) {
863
			throw new FatalError( $status->getWikiText() );
864
		};
865
866
		// Use PoolCounter if the diff looks like it can be expensive
867
		if ( strlen( $otext ) + strlen( $ntext ) > 20000 ) {
868
			$work = new PoolCounterWorkViaCallback( 'diff',
869
				md5( $otext ) . md5( $ntext ),
870
				[ 'doWork' => $diff, 'error' => $error ]
871
			);
872
			return $work->execute();
873
		}
874
875
		return $diff();
876
	}
877
878
	/**
879
	 * Generates diff, to be wrapped internally in a logging/instrumentation
880
	 *
881
	 * @param string $otext Old text, must be already segmented
882
	 * @param string $ntext New text, must be already segmented
883
	 * @return bool|string
884
	 */
885
	protected function textDiff( $otext, $ntext ) {
886
		global $wgExternalDiffEngine, $wgContLang;
887
888
		$otext = str_replace( "\r\n", "\n", $otext );
889
		$ntext = str_replace( "\r\n", "\n", $ntext );
890
891
		if ( $wgExternalDiffEngine == 'wikidiff' || $wgExternalDiffEngine == 'wikidiff3' ) {
892
			wfDeprecated( "\$wgExternalDiffEngine = '{$wgExternalDiffEngine}'", '1.27' );
893
			$wgExternalDiffEngine = false;
894
		}
895
896
		if ( $wgExternalDiffEngine == 'wikidiff2' ) {
897
			if ( function_exists( 'wikidiff2_do_diff' ) ) {
898
				# Better external diff engine, the 2 may some day be dropped
899
				# This one does the escaping and segmenting itself
900
				$text = wikidiff2_do_diff( $otext, $ntext, 2 );
901
				$text .= $this->debug( 'wikidiff2' );
902
903
				return $text;
904
			}
905
		} elseif ( $wgExternalDiffEngine !== false ) {
906
			# Diff via the shell
907
			$tmpDir = wfTempDir();
908
			$tempName1 = tempnam( $tmpDir, 'diff_' );
909
			$tempName2 = tempnam( $tmpDir, 'diff_' );
910
911
			$tempFile1 = fopen( $tempName1, "w" );
912
			if ( !$tempFile1 ) {
913
				return false;
914
			}
915
			$tempFile2 = fopen( $tempName2, "w" );
916
			if ( !$tempFile2 ) {
917
				return false;
918
			}
919
			fwrite( $tempFile1, $otext );
920
			fwrite( $tempFile2, $ntext );
921
			fclose( $tempFile1 );
922
			fclose( $tempFile2 );
923
			$cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 );
924
			$difftext = wfShellExec( $cmd );
925
			$difftext .= $this->debug( "external $wgExternalDiffEngine" );
926
			unlink( $tempName1 );
927
			unlink( $tempName2 );
928
929
			return $difftext;
930
		}
931
932
		# Native PHP diff
933
		$ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) );
934
		$nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) );
935
		$diffs = new Diff( $ota, $nta );
936
		$formatter = new TableDiffFormatter();
937
		$difftext = $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) );
938
939
		return $difftext;
940
	}
941
942
	/**
943
	 * Generate a debug comment indicating diff generating time,
944
	 * server node, and generator backend.
945
	 *
946
	 * @param string $generator : What diff engine was used
947
	 *
948
	 * @return string
949
	 */
950
	protected function debug( $generator = "internal" ) {
951
		global $wgShowHostnames;
952
		if ( !$this->enableDebugComment ) {
953
			return '';
954
		}
955
		$data = [ $generator ];
956
		if ( $wgShowHostnames ) {
957
			$data[] = wfHostname();
958
		}
959
		$data[] = wfTimestamp( TS_DB );
960
961
		return "<!-- diff generator: " .
962
			implode( " ", array_map( "htmlspecialchars", $data ) ) .
963
			" -->\n";
964
	}
965
966
	/**
967
	 * Replace line numbers with the text in the user's language
968
	 *
969
	 * @param string $text
970
	 *
971
	 * @return mixed
972
	 */
973
	public function localiseLineNumbers( $text ) {
974
		return preg_replace_callback(
975
			'/<!--LINE (\d+)-->/',
976
			[ &$this, 'localiseLineNumbersCb' ],
977
			$text
978
		);
979
	}
980
981
	public function localiseLineNumbersCb( $matches ) {
982
		if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
983
			return '';
984
		}
985
986
		return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
987
	}
988
989
	/**
990
	 * If there are revisions between the ones being compared, return a note saying so.
991
	 *
992
	 * @return string
993
	 */
994
	public function getMultiNotice() {
995
		if ( !is_object( $this->mOldRev ) || !is_object( $this->mNewRev ) ) {
996
			return '';
997
		} elseif ( !$this->mOldPage->equals( $this->mNewPage ) ) {
998
			// Comparing two different pages? Count would be meaningless.
999
			return '';
1000
		}
1001
1002
		if ( $this->mOldRev->getTimestamp() > $this->mNewRev->getTimestamp() ) {
1003
			$oldRev = $this->mNewRev; // flip
1004
			$newRev = $this->mOldRev; // flip
1005
		} else { // normal case
1006
			$oldRev = $this->mOldRev;
1007
			$newRev = $this->mNewRev;
1008
		}
1009
1010
		// Sanity: don't show the notice if too many rows must be scanned
1011
		// @todo show some special message for that case
1012
		$nEdits = $this->mNewPage->countRevisionsBetween( $oldRev, $newRev, 1000 );
1013
		if ( $nEdits > 0 && $nEdits <= 1000 ) {
1014
			$limit = 100; // use diff-multi-manyusers if too many users
1015
			$users = $this->mNewPage->getAuthorsBetween( $oldRev, $newRev, $limit );
1016
			$numUsers = count( $users );
1017
1018
			if ( $numUsers == 1 && $users[0] == $newRev->getUserText( Revision::RAW ) ) {
1019
				$numUsers = 0; // special case to say "by the same user" instead of "by one other user"
1020
			}
1021
1022
			return self::intermediateEditsMsg( $nEdits, $numUsers, $limit );
1023
		}
1024
1025
		return ''; // nothing
1026
	}
1027
1028
	/**
1029
	 * Get a notice about how many intermediate edits and users there are
1030
	 *
1031
	 * @param int $numEdits
1032
	 * @param int $numUsers
1033
	 * @param int $limit
1034
	 *
1035
	 * @return string
1036
	 */
1037
	public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) {
1038
		if ( $numUsers === 0 ) {
1039
			$msg = 'diff-multi-sameuser';
1040
		} elseif ( $numUsers > $limit ) {
1041
			$msg = 'diff-multi-manyusers';
1042
			$numUsers = $limit;
1043
		} else {
1044
			$msg = 'diff-multi-otherusers';
1045
		}
1046
1047
		return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1048
	}
1049
1050
	/**
1051
	 * Get a header for a specified revision.
1052
	 *
1053
	 * @param Revision $rev
1054
	 * @param string $complete 'complete' to get the header wrapped depending
1055
	 *        the visibility of the revision and a link to edit the page.
1056
	 *
1057
	 * @return string HTML fragment
1058
	 */
1059
	protected function getRevisionHeader( Revision $rev, $complete = '' ) {
1060
		$lang = $this->getLanguage();
1061
		$user = $this->getUser();
1062
		$revtimestamp = $rev->getTimestamp();
1063
		$timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
1064
		$dateofrev = $lang->userDate( $revtimestamp, $user );
1065
		$timeofrev = $lang->userTime( $revtimestamp, $user );
1066
1067
		$header = $this->msg(
1068
			$rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
1069
			$timestamp,
1070
			$dateofrev,
1071
			$timeofrev
1072
		)->escaped();
1073
1074
		if ( $complete !== 'complete' ) {
1075
			return $header;
1076
		}
1077
1078
		$title = $rev->getTitle();
1079
1080
		$header = Linker::linkKnown( $title, $header, [],
1081
			[ 'oldid' => $rev->getId() ] );
1082
1083
		if ( $rev->userCan( Revision::DELETED_TEXT, $user ) ) {
1084
			$editQuery = [ 'action' => 'edit' ];
1085
			if ( !$rev->isCurrent() ) {
1086
				$editQuery['oldid'] = $rev->getId();
1087
			}
1088
1089
			$key = $title->quickUserCan( 'edit', $user ) ? 'editold' : 'viewsourceold';
1090
			$msg = $this->msg( $key )->escaped();
1091
			$editLink = $this->msg( 'parentheses' )->rawParams(
1092
				Linker::linkKnown( $title, $msg, [], $editQuery ) )->escaped();
1093
			$header .= ' ' . Html::rawElement(
1094
				'span',
1095
				[ 'class' => 'mw-diff-edit' ],
1096
				$editLink
1097
			);
1098
			if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
1099
				$header = Html::rawElement(
1100
					'span',
1101
					[ 'class' => 'history-deleted' ],
1102
					$header
1103
				);
1104
			}
1105
		} else {
1106
			$header = Html::rawElement( 'span', [ 'class' => 'history-deleted' ], $header );
1107
		}
1108
1109
		return $header;
1110
	}
1111
1112
	/**
1113
	 * Add the header to a diff body
1114
	 *
1115
	 * @param string $diff Diff body
1116
	 * @param string $otitle Old revision header
1117
	 * @param string $ntitle New revision header
1118
	 * @param string $multi Notice telling user that there are intermediate
1119
	 *   revisions between the ones being compared
1120
	 * @param string $notice Other notices, e.g. that user is viewing deleted content
1121
	 *
1122
	 * @return string
1123
	 */
1124
	public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
1125
		// shared.css sets diff in interface language/dir, but the actual content
1126
		// is often in a different language, mostly the page content language/dir
1127
		$header = Html::openElement( 'table', [
1128
			'class' => [ 'diff', 'diff-contentalign-' . $this->getDiffLang()->alignStart() ],
1129
			'data-mw' => 'interface',
1130
		] );
1131
		$userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
1132
1133
		if ( !$diff && !$otitle ) {
1134
			$header .= "
1135
			<tr style='vertical-align: top;' lang='{$userLang}'>
1136
			<td class='diff-ntitle'>{$ntitle}</td>
1137
			</tr>";
1138
			$multiColspan = 1;
1139
		} else {
1140
			if ( $diff ) { // Safari/Chrome show broken output if cols not used
1141
				$header .= "
1142
				<col class='diff-marker' />
1143
				<col class='diff-content' />
1144
				<col class='diff-marker' />
1145
				<col class='diff-content' />";
1146
				$colspan = 2;
1147
				$multiColspan = 4;
1148
			} else {
1149
				$colspan = 1;
1150
				$multiColspan = 2;
1151
			}
1152
			if ( $otitle || $ntitle ) {
1153
				$header .= "
1154
				<tr style='vertical-align: top;' lang='{$userLang}'>
1155
				<td colspan='$colspan' class='diff-otitle'>{$otitle}</td>
1156
				<td colspan='$colspan' class='diff-ntitle'>{$ntitle}</td>
1157
				</tr>";
1158
			}
1159
		}
1160
1161
		if ( $multi != '' ) {
1162
			$header .= "<tr><td colspan='{$multiColspan}' style='text-align: center;' " .
1163
				"class='diff-multi' lang='{$userLang}'>{$multi}</td></tr>";
1164
		}
1165
		if ( $notice != '' ) {
1166
			$header .= "<tr><td colspan='{$multiColspan}' style='text-align: center;' " .
1167
				"lang='{$userLang}'>{$notice}</td></tr>";
1168
		}
1169
1170
		return $header . $diff . "</table>";
1171
	}
1172
1173
	/**
1174
	 * Use specified text instead of loading from the database
1175
	 * @param Content $oldContent
1176
	 * @param Content $newContent
1177
	 * @since 1.21
1178
	 */
1179
	public function setContent( Content $oldContent, Content $newContent ) {
1180
		$this->mOldContent = $oldContent;
1181
		$this->mNewContent = $newContent;
1182
1183
		$this->mTextLoaded = 2;
1184
		$this->mRevisionsLoaded = true;
1185
	}
1186
1187
	/**
1188
	 * Set the language in which the diff text is written
1189
	 * (Defaults to page content language).
1190
	 * @param Language|string $lang
1191
	 * @since 1.19
1192
	 */
1193
	public function setTextLanguage( $lang ) {
1194
		$this->mDiffLang = wfGetLangObj( $lang );
1195
	}
1196
1197
	/**
1198
	 * Maps a revision pair definition as accepted by DifferenceEngine constructor
1199
	 * to a pair of actual integers representing revision ids.
1200
	 *
1201
	 * @param int $old Revision id, e.g. from URL parameter 'oldid'
1202
	 * @param int|string $new Revision id or strings 'next' or 'prev', e.g. from URL parameter 'diff'
1203
	 *
1204
	 * @return int[] List of two revision ids, older first, later second.
1205
	 *     Zero signifies invalid argument passed.
1206
	 *     false signifies that there is no previous/next revision ($old is the oldest/newest one).
1207
	 */
1208
	public function mapDiffPrevNext( $old, $new ) {
1209
		if ( $new === 'prev' ) {
1210
			// Show diff between revision $old and the previous one. Get previous one from DB.
1211
			$newid = intval( $old );
1212
			$oldid = $this->getTitle()->getPreviousRevisionID( $newid );
1213
		} elseif ( $new === 'next' ) {
1214
			// Show diff between revision $old and the next one. Get next one from DB.
1215
			$oldid = intval( $old );
1216
			$newid = $this->getTitle()->getNextRevisionID( $oldid );
1217
		} else {
1218
			$oldid = intval( $old );
1219
			$newid = intval( $new );
1220
		}
1221
1222
		return [ $oldid, $newid ];
1223
	}
1224
1225
	/**
1226
	 * Load revision IDs
1227
	 */
1228
	private function loadRevisionIds() {
1229
		if ( $this->mRevisionsIdsLoaded ) {
1230
			return;
1231
		}
1232
1233
		$this->mRevisionsIdsLoaded = true;
1234
1235
		$old = $this->mOldid;
1236
		$new = $this->mNewid;
1237
1238
		list( $this->mOldid, $this->mNewid ) = self::mapDiffPrevNext( $old, $new );
1239
		if ( $new === 'next' && $this->mNewid === false ) {
1240
			# if no result, NewId points to the newest old revision. The only newer
1241
			# revision is cur, which is "0".
1242
			$this->mNewid = 0;
1243
		}
1244
1245
		Hooks::run(
1246
			'NewDifferenceEngine',
1247
			[ $this->getTitle(), &$this->mOldid, &$this->mNewid, $old, $new ]
1248
		);
1249
	}
1250
1251
	/**
1252
	 * Load revision metadata for the specified articles. If newid is 0, then compare
1253
	 * the old article in oldid to the current article; if oldid is 0, then
1254
	 * compare the current article to the immediately previous one (ignoring the
1255
	 * value of newid).
1256
	 *
1257
	 * If oldid is false, leave the corresponding revision object set
1258
	 * to false. This is impossible via ordinary user input, and is provided for
1259
	 * API convenience.
1260
	 *
1261
	 * @return bool
1262
	 */
1263
	public function loadRevisionData() {
1264
		if ( $this->mRevisionsLoaded ) {
1265
			return true;
1266
		}
1267
1268
		// Whether it succeeds or fails, we don't want to try again
1269
		$this->mRevisionsLoaded = true;
1270
1271
		$this->loadRevisionIds();
1272
1273
		// Load the new revision object
1274
		if ( $this->mNewid ) {
1275
			$this->mNewRev = Revision::newFromId( $this->mNewid );
1276
		} else {
1277
			$this->mNewRev = Revision::newFromTitle(
1278
				$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...
1279
				false,
1280
				Revision::READ_NORMAL
1281
			);
1282
		}
1283
1284
		if ( !$this->mNewRev instanceof Revision ) {
1285
			return false;
1286
		}
1287
1288
		// Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
1289
		$this->mNewid = $this->mNewRev->getId();
1290
		$this->mNewPage = $this->mNewRev->getTitle();
1291
1292
		// Load the old revision object
1293
		$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...
1294
		if ( $this->mOldid ) {
1295
			$this->mOldRev = Revision::newFromId( $this->mOldid );
1296
		} elseif ( $this->mOldid === 0 ) {
1297
			$rev = $this->mNewRev->getPrevious();
1298
			if ( $rev ) {
1299
				$this->mOldid = $rev->getId();
1300
				$this->mOldRev = $rev;
1301
			} else {
1302
				// No previous revision; mark to show as first-version only.
1303
				$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...
1304
				$this->mOldRev = false;
1305
			}
1306
		} /* elseif ( $this->mOldid === false ) leave mOldRev false; */
1307
1308
		if ( is_null( $this->mOldRev ) ) {
1309
			return false;
1310
		}
1311
1312
		if ( $this->mOldRev ) {
1313
			$this->mOldPage = $this->mOldRev->getTitle();
1314
		}
1315
1316
		// Load tags information for both revisions
1317
		$dbr = wfGetDB( DB_SLAVE );
1318
		if ( $this->mOldid !== false ) {
1319
			$this->mOldTags = $dbr->selectField(
1320
				'tag_summary',
1321
				'ts_tags',
1322
				[ 'ts_rev_id' => $this->mOldid ],
1323
				__METHOD__
1324
			);
1325
		} else {
1326
			$this->mOldTags = false;
1327
		}
1328
		$this->mNewTags = $dbr->selectField(
1329
			'tag_summary',
1330
			'ts_tags',
1331
			[ 'ts_rev_id' => $this->mNewid ],
1332
			__METHOD__
1333
		);
1334
1335
		return true;
1336
	}
1337
1338
	/**
1339
	 * Load the text of the revisions, as well as revision data.
1340
	 *
1341
	 * @return bool
1342
	 */
1343
	public function loadText() {
1344
		if ( $this->mTextLoaded == 2 ) {
1345
			return true;
1346
		}
1347
1348
		// Whether it succeeds or fails, we don't want to try again
1349
		$this->mTextLoaded = 2;
1350
1351
		if ( !$this->loadRevisionData() ) {
1352
			return false;
1353
		}
1354
1355 View Code Duplication
		if ( $this->mOldRev ) {
1356
			$this->mOldContent = $this->mOldRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
1357
			if ( $this->mOldContent === null ) {
1358
				return false;
1359
			}
1360
		}
1361
1362 View Code Duplication
		if ( $this->mNewRev ) {
1363
			$this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
1364
			if ( $this->mNewContent === null ) {
1365
				return false;
1366
			}
1367
		}
1368
1369
		return true;
1370
	}
1371
1372
	/**
1373
	 * Load the text of the new revision, not the old one
1374
	 *
1375
	 * @return bool
1376
	 */
1377
	public function loadNewText() {
1378
		if ( $this->mTextLoaded >= 1 ) {
1379
			return true;
1380
		}
1381
1382
		$this->mTextLoaded = 1;
1383
1384
		if ( !$this->loadRevisionData() ) {
1385
			return false;
1386
		}
1387
1388
		$this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
1389
1390
		return true;
1391
	}
1392
1393
}
1394