Completed
Branch master (bbf110)
by
unknown
25:51
created

DifferenceEngine::loadRevisionData()   C

Complexity

Conditions 10
Paths 43

Size

Total Lines 74
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 46
nc 43
nop 0
dl 0
loc 74
rs 5.8102
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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

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

    return array();
}

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

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

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

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

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

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

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

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

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

    return array();
}

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

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

Loading history...
645
					}
646
				}
647
			}
648
		}
649
		# @codingStandardsIgnoreEnd
650
651
		// Allow extensions to optionally not show the final patrolled link
652
		if ( Hooks::run( 'DifferenceEngineRenderRevisionShowFinalPatrolLink' ) ) {
653
			# Add redundant patrol link on bottom...
654
			$out->addHTML( $this->markPatrolledLink() );
655
		}
656
	}
657
658
	protected function getParserOutput( WikiPage $page, Revision $rev ) {
659
		$parserOptions = $page->makeParserOptions( $this->getContext() );
660
661
		if ( !$rev->isCurrent() || !$rev->getTitle()->quickUserCan( 'edit', $this->getUser() ) ) {
662
			$parserOptions->setEditSection( false );
663
		}
664
665
		$parserOutput = $page->getParserOutput( $parserOptions, $rev->getId() );
666
667
		return $parserOutput;
668
	}
669
670
	/**
671
	 * Get the diff text, send it to the OutputPage object
672
	 * Returns false if the diff could not be generated, otherwise returns true
673
	 *
674
	 * @param string|bool $otitle Header for old text or false
675
	 * @param string|bool $ntitle Header for new text or false
676
	 * @param string $notice HTML between diff header and body
677
	 *
678
	 * @return bool
679
	 */
680
	public function showDiff( $otitle, $ntitle, $notice = '' ) {
681
		// Allow extensions to affect the output here
682
		Hooks::run( 'DifferenceEngineShowDiff', [ $this ] );
683
684
		$diff = $this->getDiff( $otitle, $ntitle, $notice );
685
		if ( $diff === false ) {
686
			$this->showMissingRevision();
687
688
			return false;
689
		} else {
690
			$this->showDiffStyle();
691
			$this->getOutput()->addHTML( $diff );
692
693
			return true;
694
		}
695
	}
696
697
	/**
698
	 * Add style sheets and supporting JS for diff display.
699
	 */
700
	public function showDiffStyle() {
701
		$this->getOutput()->addModuleStyles( 'mediawiki.action.history.diff' );
702
	}
703
704
	/**
705
	 * Get complete diff table, including header
706
	 *
707
	 * @param string|bool $otitle Header for old text or false
708
	 * @param string|bool $ntitle Header for new text or false
709
	 * @param string $notice HTML between diff header and body
710
	 *
711
	 * @return mixed
712
	 */
713
	public function getDiff( $otitle, $ntitle, $notice = '' ) {
714
		$body = $this->getDiffBody();
715
		if ( $body === false ) {
716
			return false;
717
		}
718
719
		$multi = $this->getMultiNotice();
720
		// Display a message when the diff is empty
721
		if ( $body === '' ) {
722
			$notice .= '<div class="mw-diff-empty">' .
723
				$this->msg( 'diff-empty' )->parse() .
724
				"</div>\n";
725
		}
726
727
		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 713 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 713 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...
728
	}
729
730
	/**
731
	 * Get the diff table body, without header
732
	 *
733
	 * @return mixed (string/false)
734
	 */
735
	public function getDiffBody() {
736
		$this->mCacheHit = true;
737
		// Check if the diff should be hidden from this user
738
		if ( !$this->loadRevisionData() ) {
739
			return false;
740
		} elseif ( $this->mOldRev &&
741
			!$this->mOldRev->userCan( Revision::DELETED_TEXT, $this->getUser() )
742
		) {
743
			return false;
744
		} elseif ( $this->mNewRev &&
745
			!$this->mNewRev->userCan( Revision::DELETED_TEXT, $this->getUser() )
746
		) {
747
			return false;
748
		}
749
		// Short-circuit
750
		if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev
751
			&& $this->mOldRev->getId() == $this->mNewRev->getId() )
752
		) {
753
			if ( Hooks::run( 'DifferenceEngineShowEmptyOldContent', [ $this ] ) ) {
754
				return '';
755
			}
756
		}
757
		// Cacheable?
758
		$key = false;
759
		$cache = ObjectCache::getMainWANInstance();
760
		if ( $this->mOldid && $this->mNewid ) {
761
			$key = $this->getDiffBodyCacheKey();
762
763
			// Try cache
764
			if ( !$this->mRefreshCache ) {
765
				$difftext = $cache->get( $key );
766
				if ( $difftext ) {
767
					wfIncrStats( 'diff_cache.hit' );
768
					$difftext = $this->localiseLineNumbers( $difftext );
769
					$difftext .= "\n<!-- diff cache key $key -->\n";
770
771
					return $difftext;
772
				}
773
			} // don't try to load but save the result
774
		}
775
		$this->mCacheHit = false;
776
777
		// Loadtext is permission safe, this just clears out the diff
778
		if ( !$this->loadText() ) {
779
			return false;
780
		}
781
782
		$difftext = $this->generateContentDiffBody( $this->mOldContent, $this->mNewContent );
783
784
		// Save to cache for 7 days
785
		if ( !Hooks::run( 'AbortDiffCache', [ &$this ] ) ) {
786
			wfIncrStats( 'diff_cache.uncacheable' );
787
		} elseif ( $key !== false && $difftext !== false ) {
788
			wfIncrStats( 'diff_cache.miss' );
789
			$cache->set( $key, $difftext, 7 * 86400 );
790
		} else {
791
			wfIncrStats( 'diff_cache.uncacheable' );
792
		}
793
		// Replace line numbers with the text in the user's language
794
		if ( $difftext !== false ) {
795
			$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...
796
		}
797
798
		return $difftext;
799
	}
800
801
	/**
802
	 * Returns the cache key for diff body text or content.
803
	 *
804
	 * @since 1.23
805
	 *
806
	 * @throws MWException
807
	 * @return string
808
	 */
809
	protected function getDiffBodyCacheKey() {
810
		if ( !$this->mOldid || !$this->mNewid ) {
811
			throw new MWException( 'mOldid and mNewid must be set to get diff cache key.' );
812
		}
813
814
		return wfMemcKey( 'diff', 'version', self::DIFF_VERSION,
815
			'oldid', $this->mOldid, 'newid', $this->mNewid );
816
	}
817
818
	/**
819
	 * Generate a diff, no caching.
820
	 *
821
	 * This implementation uses generateTextDiffBody() to generate a diff based on the default
822
	 * serialization of the given Content objects. This will fail if $old or $new are not
823
	 * instances of TextContent.
824
	 *
825
	 * Subclasses may override this to provide a different rendering for the diff,
826
	 * perhaps taking advantage of the content's native form. This is required for all content
827
	 * models that are not text based.
828
	 *
829
	 * @since 1.21
830
	 *
831
	 * @param Content $old Old content
832
	 * @param Content $new New content
833
	 *
834
	 * @throws MWException If old or new content is not an instance of TextContent.
835
	 * @return bool|string
836
	 */
837
	public function generateContentDiffBody( Content $old, Content $new ) {
838 View Code Duplication
		if ( !( $old instanceof TextContent ) ) {
839
			throw new MWException( "Diff not implemented for " . get_class( $old ) . "; " .
840
				"override generateContentDiffBody to fix this." );
841
		}
842
843 View Code Duplication
		if ( !( $new instanceof TextContent ) ) {
844
			throw new MWException( "Diff not implemented for " . get_class( $new ) . "; "
845
				. "override generateContentDiffBody to fix this." );
846
		}
847
848
		$otext = $old->serialize();
849
		$ntext = $new->serialize();
850
851
		return $this->generateTextDiffBody( $otext, $ntext );
852
	}
853
854
	/**
855
	 * Generate a diff, no caching
856
	 *
857
	 * @todo move this to TextDifferenceEngine, make DifferenceEngine abstract. At some point.
858
	 *
859
	 * @param string $otext Old text, must be already segmented
860
	 * @param string $ntext New text, must be already segmented
861
	 *
862
	 * @return bool|string
863
	 */
864
	public function generateTextDiffBody( $otext, $ntext ) {
865
		$diff = function() use ( $otext, $ntext ) {
866
			$time = microtime( true );
867
868
			$result = $this->textDiff( $otext, $ntext );
869
870
			$time = intval( ( microtime( true ) - $time ) * 1000 );
871
			$this->getStats()->timing( 'diff_time', $time );
872
			// Log requests slower than 99th percentile
873
			if ( $time > 100 && $this->mOldPage && $this->mNewPage ) {
874
				wfDebugLog( 'diff',
875
					"$time ms diff: {$this->mOldid} -> {$this->mNewid} {$this->mNewPage}" );
876
			}
877
878
			return $result;
879
		};
880
881
		$error = function( $status ) {
882
			throw new FatalError( $status->getWikiText() );
883
		};
884
885
		// Use PoolCounter if the diff looks like it can be expensive
886
		if ( strlen( $otext ) + strlen( $ntext ) > 20000 ) {
887
			$work = new PoolCounterWorkViaCallback( 'diff',
888
				md5( $otext ) . md5( $ntext ),
889
				[ 'doWork' => $diff, 'error' => $error ]
890
			);
891
			return $work->execute();
892
		}
893
894
		return $diff();
895
	}
896
897
	/**
898
	 * Generates diff, to be wrapped internally in a logging/instrumentation
899
	 *
900
	 * @param string $otext Old text, must be already segmented
901
	 * @param string $ntext New text, must be already segmented
902
	 * @return bool|string
903
	 */
904
	protected function textDiff( $otext, $ntext ) {
905
		global $wgExternalDiffEngine, $wgContLang;
906
907
		$otext = str_replace( "\r\n", "\n", $otext );
908
		$ntext = str_replace( "\r\n", "\n", $ntext );
909
910
		if ( $wgExternalDiffEngine == 'wikidiff' || $wgExternalDiffEngine == 'wikidiff3' ) {
911
			wfDeprecated( "\$wgExternalDiffEngine = '{$wgExternalDiffEngine}'", '1.27' );
912
			$wgExternalDiffEngine = false;
913
		} elseif ( $wgExternalDiffEngine == 'wikidiff2' ) {
914
			// Same as above, but with no deprecation warnings
915
			$wgExternalDiffEngine = false;
916
		} elseif ( !is_string( $wgExternalDiffEngine ) && $wgExternalDiffEngine !== false ) {
917
			// And prevent people from shooting themselves in the foot...
918
			wfWarn( '$wgExternalDiffEngine is set to a non-string value, forcing it to false' );
919
			$wgExternalDiffEngine = false;
920
		}
921
922
		if ( function_exists( 'wikidiff2_do_diff' ) && $wgExternalDiffEngine === false ) {
923
			# Better external diff engine, the 2 may some day be dropped
924
			# This one does the escaping and segmenting itself
925
			$text = wikidiff2_do_diff( $otext, $ntext, 2 );
926
			$text .= $this->debug( 'wikidiff2' );
927
928
			return $text;
929
		} elseif ( $wgExternalDiffEngine !== false && is_executable( $wgExternalDiffEngine ) ) {
930
			# Diff via the shell
931
			$tmpDir = wfTempDir();
932
			$tempName1 = tempnam( $tmpDir, 'diff_' );
933
			$tempName2 = tempnam( $tmpDir, 'diff_' );
934
935
			$tempFile1 = fopen( $tempName1, "w" );
936
			if ( !$tempFile1 ) {
937
				return false;
938
			}
939
			$tempFile2 = fopen( $tempName2, "w" );
940
			if ( !$tempFile2 ) {
941
				return false;
942
			}
943
			fwrite( $tempFile1, $otext );
944
			fwrite( $tempFile2, $ntext );
945
			fclose( $tempFile1 );
946
			fclose( $tempFile2 );
947
			$cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 );
948
			$difftext = wfShellExec( $cmd );
949
			$difftext .= $this->debug( "external $wgExternalDiffEngine" );
950
			unlink( $tempName1 );
951
			unlink( $tempName2 );
952
953
			return $difftext;
954
		}
955
956
		# Native PHP diff
957
		$ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) );
958
		$nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) );
959
		$diffs = new Diff( $ota, $nta );
960
		$formatter = new TableDiffFormatter();
961
		$difftext = $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) );
962
963
		return $difftext;
964
	}
965
966
	/**
967
	 * Generate a debug comment indicating diff generating time,
968
	 * server node, and generator backend.
969
	 *
970
	 * @param string $generator : What diff engine was used
971
	 *
972
	 * @return string
973
	 */
974
	protected function debug( $generator = "internal" ) {
975
		global $wgShowHostnames;
976
		if ( !$this->enableDebugComment ) {
977
			return '';
978
		}
979
		$data = [ $generator ];
980
		if ( $wgShowHostnames ) {
981
			$data[] = wfHostname();
982
		}
983
		$data[] = wfTimestamp( TS_DB );
984
985
		return "<!-- diff generator: " .
986
			implode( " ", array_map( "htmlspecialchars", $data ) ) .
987
			" -->\n";
988
	}
989
990
	/**
991
	 * Replace line numbers with the text in the user's language
992
	 *
993
	 * @param string $text
994
	 *
995
	 * @return mixed
996
	 */
997
	public function localiseLineNumbers( $text ) {
998
		return preg_replace_callback(
999
			'/<!--LINE (\d+)-->/',
1000
			[ &$this, 'localiseLineNumbersCb' ],
1001
			$text
1002
		);
1003
	}
1004
1005
	public function localiseLineNumbersCb( $matches ) {
1006
		if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
1007
			return '';
1008
		}
1009
1010
		return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
1011
	}
1012
1013
	/**
1014
	 * If there are revisions between the ones being compared, return a note saying so.
1015
	 *
1016
	 * @return string
1017
	 */
1018
	public function getMultiNotice() {
1019
		if ( !is_object( $this->mOldRev ) || !is_object( $this->mNewRev ) ) {
1020
			return '';
1021
		} elseif ( !$this->mOldPage->equals( $this->mNewPage ) ) {
1022
			// Comparing two different pages? Count would be meaningless.
1023
			return '';
1024
		}
1025
1026
		if ( $this->mOldRev->getTimestamp() > $this->mNewRev->getTimestamp() ) {
1027
			$oldRev = $this->mNewRev; // flip
1028
			$newRev = $this->mOldRev; // flip
1029
		} else { // normal case
1030
			$oldRev = $this->mOldRev;
1031
			$newRev = $this->mNewRev;
1032
		}
1033
1034
		// Sanity: don't show the notice if too many rows must be scanned
1035
		// @todo show some special message for that case
1036
		$nEdits = $this->mNewPage->countRevisionsBetween( $oldRev, $newRev, 1000 );
1037
		if ( $nEdits > 0 && $nEdits <= 1000 ) {
1038
			$limit = 100; // use diff-multi-manyusers if too many users
1039
			$users = $this->mNewPage->getAuthorsBetween( $oldRev, $newRev, $limit );
1040
			$numUsers = count( $users );
1041
1042
			if ( $numUsers == 1 && $users[0] == $newRev->getUserText( Revision::RAW ) ) {
1043
				$numUsers = 0; // special case to say "by the same user" instead of "by one other user"
1044
			}
1045
1046
			return self::intermediateEditsMsg( $nEdits, $numUsers, $limit );
1047
		}
1048
1049
		return ''; // nothing
1050
	}
1051
1052
	/**
1053
	 * Get a notice about how many intermediate edits and users there are
1054
	 *
1055
	 * @param int $numEdits
1056
	 * @param int $numUsers
1057
	 * @param int $limit
1058
	 *
1059
	 * @return string
1060
	 */
1061
	public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) {
1062
		if ( $numUsers === 0 ) {
1063
			$msg = 'diff-multi-sameuser';
1064
		} elseif ( $numUsers > $limit ) {
1065
			$msg = 'diff-multi-manyusers';
1066
			$numUsers = $limit;
1067
		} else {
1068
			$msg = 'diff-multi-otherusers';
1069
		}
1070
1071
		return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1072
	}
1073
1074
	/**
1075
	 * Get a header for a specified revision.
1076
	 *
1077
	 * @param Revision $rev
1078
	 * @param string $complete 'complete' to get the header wrapped depending
1079
	 *        the visibility of the revision and a link to edit the page.
1080
	 *
1081
	 * @return string HTML fragment
1082
	 */
1083
	protected function getRevisionHeader( Revision $rev, $complete = '' ) {
1084
		$lang = $this->getLanguage();
1085
		$user = $this->getUser();
1086
		$revtimestamp = $rev->getTimestamp();
1087
		$timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
1088
		$dateofrev = $lang->userDate( $revtimestamp, $user );
1089
		$timeofrev = $lang->userTime( $revtimestamp, $user );
1090
1091
		$header = $this->msg(
1092
			$rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
1093
			$timestamp,
1094
			$dateofrev,
1095
			$timeofrev
1096
		)->escaped();
1097
1098
		if ( $complete !== 'complete' ) {
1099
			return $header;
1100
		}
1101
1102
		$title = $rev->getTitle();
1103
1104
		$header = Linker::linkKnown( $title, $header, [],
1105
			[ 'oldid' => $rev->getId() ] );
1106
1107
		if ( $rev->userCan( Revision::DELETED_TEXT, $user ) ) {
1108
			$editQuery = [ 'action' => 'edit' ];
1109
			if ( !$rev->isCurrent() ) {
1110
				$editQuery['oldid'] = $rev->getId();
1111
			}
1112
1113
			$key = $title->quickUserCan( 'edit', $user ) ? 'editold' : 'viewsourceold';
1114
			$msg = $this->msg( $key )->escaped();
1115
			$editLink = $this->msg( 'parentheses' )->rawParams(
1116
				Linker::linkKnown( $title, $msg, [], $editQuery ) )->escaped();
1117
			$header .= ' ' . Html::rawElement(
1118
				'span',
1119
				[ 'class' => 'mw-diff-edit' ],
1120
				$editLink
1121
			);
1122
			if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
1123
				$header = Html::rawElement(
1124
					'span',
1125
					[ 'class' => 'history-deleted' ],
1126
					$header
1127
				);
1128
			}
1129
		} else {
1130
			$header = Html::rawElement( 'span', [ 'class' => 'history-deleted' ], $header );
1131
		}
1132
1133
		return $header;
1134
	}
1135
1136
	/**
1137
	 * Add the header to a diff body
1138
	 *
1139
	 * @param string $diff Diff body
1140
	 * @param string $otitle Old revision header
1141
	 * @param string $ntitle New revision header
1142
	 * @param string $multi Notice telling user that there are intermediate
1143
	 *   revisions between the ones being compared
1144
	 * @param string $notice Other notices, e.g. that user is viewing deleted content
1145
	 *
1146
	 * @return string
1147
	 */
1148
	public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
1149
		// shared.css sets diff in interface language/dir, but the actual content
1150
		// is often in a different language, mostly the page content language/dir
1151
		$header = Html::openElement( 'table', [
1152
			'class' => [ 'diff', 'diff-contentalign-' . $this->getDiffLang()->alignStart() ],
1153
			'data-mw' => 'interface',
1154
		] );
1155
		$userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
1156
1157
		if ( !$diff && !$otitle ) {
1158
			$header .= "
1159
			<tr style='vertical-align: top;' lang='{$userLang}'>
1160
			<td class='diff-ntitle'>{$ntitle}</td>
1161
			</tr>";
1162
			$multiColspan = 1;
1163
		} else {
1164
			if ( $diff ) { // Safari/Chrome show broken output if cols not used
1165
				$header .= "
1166
				<col class='diff-marker' />
1167
				<col class='diff-content' />
1168
				<col class='diff-marker' />
1169
				<col class='diff-content' />";
1170
				$colspan = 2;
1171
				$multiColspan = 4;
1172
			} else {
1173
				$colspan = 1;
1174
				$multiColspan = 2;
1175
			}
1176
			if ( $otitle || $ntitle ) {
1177
				$header .= "
1178
				<tr style='vertical-align: top;' lang='{$userLang}'>
1179
				<td colspan='$colspan' class='diff-otitle'>{$otitle}</td>
1180
				<td colspan='$colspan' class='diff-ntitle'>{$ntitle}</td>
1181
				</tr>";
1182
			}
1183
		}
1184
1185
		if ( $multi != '' ) {
1186
			$header .= "<tr><td colspan='{$multiColspan}' style='text-align: center;' " .
1187
				"class='diff-multi' lang='{$userLang}'>{$multi}</td></tr>";
1188
		}
1189
		if ( $notice != '' ) {
1190
			$header .= "<tr><td colspan='{$multiColspan}' style='text-align: center;' " .
1191
				"lang='{$userLang}'>{$notice}</td></tr>";
1192
		}
1193
1194
		return $header . $diff . "</table>";
1195
	}
1196
1197
	/**
1198
	 * Use specified text instead of loading from the database
1199
	 * @param Content $oldContent
1200
	 * @param Content $newContent
1201
	 * @since 1.21
1202
	 */
1203
	public function setContent( Content $oldContent, Content $newContent ) {
1204
		$this->mOldContent = $oldContent;
1205
		$this->mNewContent = $newContent;
1206
1207
		$this->mTextLoaded = 2;
1208
		$this->mRevisionsLoaded = true;
1209
	}
1210
1211
	/**
1212
	 * Set the language in which the diff text is written
1213
	 * (Defaults to page content language).
1214
	 * @param Language|string $lang
1215
	 * @since 1.19
1216
	 */
1217
	public function setTextLanguage( $lang ) {
1218
		$this->mDiffLang = wfGetLangObj( $lang );
1219
	}
1220
1221
	/**
1222
	 * Maps a revision pair definition as accepted by DifferenceEngine constructor
1223
	 * to a pair of actual integers representing revision ids.
1224
	 *
1225
	 * @param int $old Revision id, e.g. from URL parameter 'oldid'
1226
	 * @param int|string $new Revision id or strings 'next' or 'prev', e.g. from URL parameter 'diff'
1227
	 *
1228
	 * @return int[] List of two revision ids, older first, later second.
1229
	 *     Zero signifies invalid argument passed.
1230
	 *     false signifies that there is no previous/next revision ($old is the oldest/newest one).
1231
	 */
1232
	public function mapDiffPrevNext( $old, $new ) {
1233
		if ( $new === 'prev' ) {
1234
			// Show diff between revision $old and the previous one. Get previous one from DB.
1235
			$newid = intval( $old );
1236
			$oldid = $this->getTitle()->getPreviousRevisionID( $newid );
1237
		} elseif ( $new === 'next' ) {
1238
			// Show diff between revision $old and the next one. Get next one from DB.
1239
			$oldid = intval( $old );
1240
			$newid = $this->getTitle()->getNextRevisionID( $oldid );
1241
		} else {
1242
			$oldid = intval( $old );
1243
			$newid = intval( $new );
1244
		}
1245
1246
		return [ $oldid, $newid ];
1247
	}
1248
1249
	/**
1250
	 * Load revision IDs
1251
	 */
1252
	private function loadRevisionIds() {
1253
		if ( $this->mRevisionsIdsLoaded ) {
1254
			return;
1255
		}
1256
1257
		$this->mRevisionsIdsLoaded = true;
1258
1259
		$old = $this->mOldid;
1260
		$new = $this->mNewid;
1261
1262
		list( $this->mOldid, $this->mNewid ) = self::mapDiffPrevNext( $old, $new );
1263
		if ( $new === 'next' && $this->mNewid === false ) {
1264
			# if no result, NewId points to the newest old revision. The only newer
1265
			# revision is cur, which is "0".
1266
			$this->mNewid = 0;
1267
		}
1268
1269
		Hooks::run(
1270
			'NewDifferenceEngine',
1271
			[ $this->getTitle(), &$this->mOldid, &$this->mNewid, $old, $new ]
1272
		);
1273
	}
1274
1275
	/**
1276
	 * Load revision metadata for the specified articles. If newid is 0, then compare
1277
	 * the old article in oldid to the current article; if oldid is 0, then
1278
	 * compare the current article to the immediately previous one (ignoring the
1279
	 * value of newid).
1280
	 *
1281
	 * If oldid is false, leave the corresponding revision object set
1282
	 * to false. This is impossible via ordinary user input, and is provided for
1283
	 * API convenience.
1284
	 *
1285
	 * @return bool
1286
	 */
1287
	public function loadRevisionData() {
1288
		if ( $this->mRevisionsLoaded ) {
1289
			return true;
1290
		}
1291
1292
		// Whether it succeeds or fails, we don't want to try again
1293
		$this->mRevisionsLoaded = true;
1294
1295
		$this->loadRevisionIds();
1296
1297
		// Load the new revision object
1298
		if ( $this->mNewid ) {
1299
			$this->mNewRev = Revision::newFromId( $this->mNewid );
1300
		} else {
1301
			$this->mNewRev = Revision::newFromTitle(
1302
				$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...
1303
				false,
1304
				Revision::READ_NORMAL
1305
			);
1306
		}
1307
1308
		if ( !$this->mNewRev instanceof Revision ) {
1309
			return false;
1310
		}
1311
1312
		// Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
1313
		$this->mNewid = $this->mNewRev->getId();
1314
		$this->mNewPage = $this->mNewRev->getTitle();
1315
1316
		// Load the old revision object
1317
		$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...
1318
		if ( $this->mOldid ) {
1319
			$this->mOldRev = Revision::newFromId( $this->mOldid );
1320
		} elseif ( $this->mOldid === 0 ) {
1321
			$rev = $this->mNewRev->getPrevious();
1322
			if ( $rev ) {
1323
				$this->mOldid = $rev->getId();
1324
				$this->mOldRev = $rev;
1325
			} else {
1326
				// No previous revision; mark to show as first-version only.
1327
				$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...
1328
				$this->mOldRev = false;
1329
			}
1330
		} /* elseif ( $this->mOldid === false ) leave mOldRev false; */
1331
1332
		if ( is_null( $this->mOldRev ) ) {
1333
			return false;
1334
		}
1335
1336
		if ( $this->mOldRev ) {
1337
			$this->mOldPage = $this->mOldRev->getTitle();
1338
		}
1339
1340
		// Load tags information for both revisions
1341
		$dbr = wfGetDB( DB_SLAVE );
1342
		if ( $this->mOldid !== false ) {
1343
			$this->mOldTags = $dbr->selectField(
1344
				'tag_summary',
1345
				'ts_tags',
1346
				[ 'ts_rev_id' => $this->mOldid ],
1347
				__METHOD__
1348
			);
1349
		} else {
1350
			$this->mOldTags = false;
1351
		}
1352
		$this->mNewTags = $dbr->selectField(
1353
			'tag_summary',
1354
			'ts_tags',
1355
			[ 'ts_rev_id' => $this->mNewid ],
1356
			__METHOD__
1357
		);
1358
1359
		return true;
1360
	}
1361
1362
	/**
1363
	 * Load the text of the revisions, as well as revision data.
1364
	 *
1365
	 * @return bool
1366
	 */
1367
	public function loadText() {
1368
		if ( $this->mTextLoaded == 2 ) {
1369
			return true;
1370
		}
1371
1372
		// Whether it succeeds or fails, we don't want to try again
1373
		$this->mTextLoaded = 2;
1374
1375
		if ( !$this->loadRevisionData() ) {
1376
			return false;
1377
		}
1378
1379 View Code Duplication
		if ( $this->mOldRev ) {
1380
			$this->mOldContent = $this->mOldRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
1381
			if ( $this->mOldContent === null ) {
1382
				return false;
1383
			}
1384
		}
1385
1386 View Code Duplication
		if ( $this->mNewRev ) {
1387
			$this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
1388
			if ( $this->mNewContent === null ) {
1389
				return false;
1390
			}
1391
		}
1392
1393
		return true;
1394
	}
1395
1396
	/**
1397
	 * Load the text of the new revision, not the old one
1398
	 *
1399
	 * @return bool
1400
	 */
1401
	public function loadNewText() {
1402
		if ( $this->mTextLoaded >= 1 ) {
1403
			return true;
1404
		}
1405
1406
		$this->mTextLoaded = 1;
1407
1408
		if ( !$this->loadRevisionData() ) {
1409
			return false;
1410
		}
1411
1412
		$this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
1413
1414
		return true;
1415
	}
1416
1417
}
1418