Completed
Branch master (4b8315)
by
unknown
17:52
created

includes/actions/HistoryAction.php (17 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Page history
4
 *
5
 * Split off from Article.php and Skin.php, 2003-12-22
6
 *
7
 * This program is free software; you can redistribute it and/or modify
8
 * it under the terms of the GNU General Public License as published by
9
 * the Free Software Foundation; either version 2 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
 * GNU General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU General Public License along
18
 * with this program; if not, write to the Free Software Foundation, Inc.,
19
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20
 * http://www.gnu.org/copyleft/gpl.html
21
 *
22
 * @file
23
 * @ingroup Actions
24
 */
25
26
/**
27
 * This class handles printing the history page for an article. In order to
28
 * be efficient, it uses timestamps rather than offsets for paging, to avoid
29
 * costly LIMIT,offset queries.
30
 *
31
 * Construct it by passing in an Article, and call $h->history() to print the
32
 * history.
33
 *
34
 * @ingroup Actions
35
 */
36
class HistoryAction extends FormlessAction {
37
	const DIR_PREV = 0;
38
	const DIR_NEXT = 1;
39
40
	/** @var array Array of message keys and strings */
41
	public $message;
42
43
	public function getName() {
44
		return 'history';
45
	}
46
47
	public function requiresWrite() {
48
		return false;
49
	}
50
51
	public function requiresUnblock() {
52
		return false;
53
	}
54
55
	protected function getPageTitle() {
56
		return $this->msg( 'history-title', $this->getTitle()->getPrefixedText() )->text();
57
	}
58
59
	protected function getDescription() {
60
		// Creation of a subtitle link pointing to [[Special:Log]]
61
		return Linker::linkKnown(
62
			SpecialPage::getTitleFor( 'Log' ),
63
			$this->msg( 'viewpagelogs' )->escaped(),
64
			[],
65
			[ 'page' => $this->getTitle()->getPrefixedText() ]
66
		);
67
	}
68
69
	/**
70
	 * @return WikiPage|Article|ImagePage|CategoryPage|Page The Article object we are working on.
71
	 */
72
	public function getArticle() {
73
		return $this->page;
74
	}
75
76
	/**
77
	 * As we use the same small set of messages in various methods and that
78
	 * they are called often, we call them once and save them in $this->message
79
	 */
80
	private function preCacheMessages() {
81
		// Precache various messages
82
		if ( !isset( $this->message ) ) {
83
			$msgs = [ 'cur', 'last', 'pipe-separator' ];
84
			foreach ( $msgs as $msg ) {
85
				$this->message[$msg] = $this->msg( $msg )->escaped();
86
			}
87
		}
88
	}
89
90
	/**
91
	 * Print the history page for an article.
92
	 */
93
	function onView() {
94
		$out = $this->getOutput();
95
		$request = $this->getRequest();
96
97
		/**
98
		 * Allow client caching.
99
		 */
100
		if ( $out->checkLastModified( $this->page->getTouched() ) ) {
0 ignored issues
show
The method getTouched does only exist in Article and CategoryPage... ImagePage and WikiPage, but not in Page.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
101
			return; // Client cache fresh and headers sent, nothing more to do.
102
		}
103
104
		$this->preCacheMessages();
105
		$config = $this->context->getConfig();
106
107
		# Fill in the file cache if not set already
108
		if ( HTMLFileCache::useFileCache( $this->getContext() ) ) {
109
			$cache = new HTMLFileCache( $this->getTitle(), 'history' );
110
			if ( !$cache->isCacheGood( /* Assume up to date */ ) ) {
111
				ob_start( [ &$cache, 'saveToFileCache' ] );
112
			}
113
		}
114
115
		// Setup page variables.
116
		$out->setFeedAppendQuery( 'action=history' );
117
		$out->addModules( 'mediawiki.action.history' );
118
		$out->addModuleStyles( [
119
			'mediawiki.action.history.styles',
120
			'mediawiki.special.changeslist',
121
		] );
122
		if ( $config->get( 'UseMediaWikiUIEverywhere' ) ) {
123
			$out = $this->getOutput();
124
			$out->addModuleStyles( [
125
				'mediawiki.ui.input',
126
				'mediawiki.ui.checkbox',
127
			] );
128
		}
129
130
		// Handle atom/RSS feeds.
131
		$feedType = $request->getVal( 'feed' );
132
		if ( $feedType ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $feedType of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null 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...
133
			$this->feed( $feedType );
134
135
			return;
136
		}
137
138
		$this->addHelpLink( '//meta.wikimedia.org/wiki/Special:MyLanguage/Help:Page_history', true );
139
140
		// Fail nicely if article doesn't exist.
141
		if ( !$this->page->exists() ) {
0 ignored issues
show
The method exists does only exist in Article and CategoryPage... ImagePage and WikiPage, but not in Page.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
142
			global $wgSend404Code;
143
			if ( $wgSend404Code ) {
144
				$out->setStatusCode( 404 );
145
			}
146
			$out->addWikiMsg( 'nohistory' );
147
			# show deletion/move log if there is an entry
148
			LogEventsList::showLogExtract(
149
				$out,
150
				[ 'delete', 'move' ],
151
				$this->getTitle(),
152
				'',
153
				[ 'lim' => 10,
154
					'conds' => [ "log_action != 'revision'" ],
155
					'showIfEmpty' => false,
156
					'msgKey' => [ 'moveddeleted-notice' ]
157
				]
158
			);
159
160
			return;
161
		}
162
163
		/**
164
		 * Add date selector to quickly get to a certain time
165
		 */
166
		$year = $request->getInt( 'year' );
167
		$month = $request->getInt( 'month' );
168
		$tagFilter = $request->getVal( 'tagfilter' );
169
		$tagSelector = ChangeTags::buildTagFilterSelector( $tagFilter );
170
171
		/**
172
		 * Option to show only revisions that have been (partially) hidden via RevisionDelete
173
		 */
174
		if ( $request->getBool( 'deleted' ) ) {
175
			$conds = [ 'rev_deleted != 0' ];
176
		} else {
177
			$conds = [];
178
		}
179
		if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) {
180
			$checkDeleted = Xml::checkLabel( $this->msg( 'history-show-deleted' )->text(),
181
				'deleted', 'mw-show-deleted-only', $request->getBool( 'deleted' ) ) . "\n";
182
		} else {
183
			$checkDeleted = '';
184
		}
185
186
		// Add the general form
187
		$action = htmlspecialchars( wfScript() );
188
		$out->addHTML(
189
			"<form action=\"$action\" method=\"get\" id=\"mw-history-searchform\">" .
190
			Xml::fieldset(
191
				$this->msg( 'history-fieldset-title' )->text(),
192
				false,
193
				[ 'id' => 'mw-history-search' ]
194
			) .
195
			Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) . "\n" .
196
			Html::hidden( 'action', 'history' ) . "\n" .
197
			Xml::dateMenu(
198
				( $year == null ? MWTimestamp::getLocalInstance()->format( 'Y' ) : $year ),
199
				$month
200
			) . '&#160;' .
201
			( $tagSelector ? ( implode( '&#160;', $tagSelector ) . '&#160;' ) : '' ) .
202
			$checkDeleted .
203
			Html::submitButton(
204
				$this->msg( 'historyaction-submit' )->text(),
205
				[],
206
				[ 'mw-ui-progressive' ]
207
			) . "\n" .
208
			'</fieldset></form>'
209
		);
210
211
		Hooks::run( 'PageHistoryBeforeList', [ &$this->page, $this->getContext() ] );
212
213
		// Create and output the list.
214
		$pager = new HistoryPager( $this, $year, $month, $tagFilter, $conds );
215
		$out->addHTML(
216
			$pager->getNavigationBar() .
217
			$pager->getBody() .
218
			$pager->getNavigationBar()
219
		);
220
		$out->preventClickjacking( $pager->getPreventClickjacking() );
221
	}
222
223
	/**
224
	 * Fetch an array of revisions, specified by a given limit, offset and
225
	 * direction. This is now only used by the feeds. It was previously
226
	 * used by the main UI but that's now handled by the pager.
227
	 *
228
	 * @param int $limit The limit number of revisions to get
229
	 * @param int $offset
230
	 * @param int $direction Either self::DIR_PREV or self::DIR_NEXT
231
	 * @return ResultWrapper
232
	 */
233
	function fetchRevisions( $limit, $offset, $direction ) {
234
		// Fail if article doesn't exist.
235
		if ( !$this->getTitle()->exists() ) {
236
			return new FakeResultWrapper( [] );
237
		}
238
239
		$dbr = wfGetDB( DB_REPLICA );
240
241
		if ( $direction === self::DIR_PREV ) {
242
			list( $dirs, $oper ) = [ "ASC", ">=" ];
243
		} else { /* $direction === self::DIR_NEXT */
244
			list( $dirs, $oper ) = [ "DESC", "<=" ];
245
		}
246
247
		if ( $offset ) {
248
			$offsets = [ "rev_timestamp $oper " . $dbr->addQuotes( $dbr->timestamp( $offset ) ) ];
249
		} else {
250
			$offsets = [];
251
		}
252
253
		$page_id = $this->page->getId();
0 ignored issues
show
The method getId does only exist in Article and CategoryPage... ImagePage and WikiPage, but not in Page.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
254
255
		return $dbr->select( 'revision',
256
			Revision::selectFields(),
257
			array_merge( [ 'rev_page' => $page_id ], $offsets ),
258
			__METHOD__,
259
			[ 'ORDER BY' => "rev_timestamp $dirs",
260
				'USE INDEX' => 'page_timestamp', 'LIMIT' => $limit ]
261
		);
262
	}
263
264
	/**
265
	 * Output a subscription feed listing recent edits to this page.
266
	 *
267
	 * @param string $type Feed type
268
	 */
269
	function feed( $type ) {
270
		if ( !FeedUtils::checkFeedOutput( $type ) ) {
271
			return;
272
		}
273
		$request = $this->getRequest();
274
275
		$feedClasses = $this->context->getConfig()->get( 'FeedClasses' );
276
		/** @var RSSFeed|AtomFeed $feed */
277
		$feed = new $feedClasses[$type](
278
			$this->getTitle()->getPrefixedText() . ' - ' .
279
			$this->msg( 'history-feed-title' )->inContentLanguage()->text(),
280
			$this->msg( 'history-feed-description' )->inContentLanguage()->text(),
281
			$this->getTitle()->getFullURL( 'action=history' )
282
		);
283
284
		// Get a limit on number of feed entries. Provide a sane default
285
		// of 10 if none is defined (but limit to $wgFeedLimit max)
286
		$limit = $request->getInt( 'limit', 10 );
287
		$limit = min(
288
			max( $limit, 1 ),
289
			$this->context->getConfig()->get( 'FeedLimit' )
290
		);
291
292
		$items = $this->fetchRevisions( $limit, 0, self::DIR_NEXT );
293
294
		// Generate feed elements enclosed between header and footer.
295
		$feed->outHeader();
296
		if ( $items->numRows() ) {
297
			foreach ( $items as $row ) {
298
				$feed->outItem( $this->feedItem( $row ) );
0 ignored issues
show
It seems like $row defined by $row on line 297 can be null; however, HistoryAction::feedItem() 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...
299
			}
300
		} else {
301
			$feed->outItem( $this->feedEmpty() );
302
		}
303
		$feed->outFooter();
304
	}
305
306
	function feedEmpty() {
307
		return new FeedItem(
308
			$this->msg( 'nohistory' )->inContentLanguage()->text(),
309
			$this->msg( 'history-feed-empty' )->inContentLanguage()->parseAsBlock(),
310
			$this->getTitle()->getFullURL(),
311
			wfTimestamp( TS_MW ),
0 ignored issues
show
It seems like wfTimestamp(TS_MW) targeting wfTimestamp() can also be of type false; however, FeedItem::__construct() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
312
			'',
313
			$this->getTitle()->getTalkPage()->getFullURL()
314
		);
315
	}
316
317
	/**
318
	 * Generate a FeedItem object from a given revision table row
319
	 * Borrows Recent Changes' feed generation functions for formatting;
320
	 * includes a diff to the previous revision (if any).
321
	 *
322
	 * @param stdClass|array $row Database row
323
	 * @return FeedItem
324
	 */
325
	function feedItem( $row ) {
326
		$rev = new Revision( $row );
327
		$rev->setTitle( $this->getTitle() );
328
		$text = FeedUtils::formatDiffRow(
329
			$this->getTitle(),
330
			$this->getTitle()->getPreviousRevisionID( $rev->getId() ),
0 ignored issues
show
It seems like $this->getTitle()->getPr...visionID($rev->getId()) targeting Title::getPreviousRevisionID() can also be of type false; however, FeedUtils::formatDiffRow() does only seem to accept integer, did you maybe forget to handle an error condition?
Loading history...
331
			$rev->getId(),
332
			$rev->getTimestamp(),
333
			$rev->getComment()
334
		);
335
		if ( $rev->getComment() == '' ) {
336
			global $wgContLang;
337
			$title = $this->msg( 'history-feed-item-nocomment',
338
				$rev->getUserText(),
339
				$wgContLang->timeanddate( $rev->getTimestamp() ),
340
				$wgContLang->date( $rev->getTimestamp() ),
341
				$wgContLang->time( $rev->getTimestamp() ) )->inContentLanguage()->text();
342
		} else {
343
			$title = $rev->getUserText() .
344
				$this->msg( 'colon-separator' )->inContentLanguage()->text() .
345
				FeedItem::stripComment( $rev->getComment() );
346
		}
347
348
		return new FeedItem(
349
			$title,
350
			$text,
351
			$this->getTitle()->getFullURL( 'diff=' . $rev->getId() . '&oldid=prev' ),
352
			$rev->getTimestamp(),
0 ignored issues
show
It seems like $rev->getTimestamp() targeting Revision::getTimestamp() can also be of type false; however, FeedItem::__construct() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
353
			$rev->getUserText(),
0 ignored issues
show
It seems like $rev->getUserText() targeting Revision::getUserText() can also be of type boolean; however, FeedItem::__construct() does only seem to accept string, maybe add an additional type check?

This check looks at variables that 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...
354
			$this->getTitle()->getTalkPage()->getFullURL()
355
		);
356
	}
357
}
358
359
/**
360
 * @ingroup Pager
361
 * @ingroup Actions
362
 */
363
class HistoryPager extends ReverseChronologicalPager {
364
	/**
365
	 * @var bool|stdClass
366
	 */
367
	public $lastRow = false;
368
369
	public $counter, $historyPage, $buttons, $conds;
0 ignored issues
show
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
370
371
	protected $oldIdChecked;
372
373
	protected $preventClickjacking = false;
374
	/**
375
	 * @var array
376
	 */
377
	protected $parentLens;
378
379
	/** @var bool Whether to show the tag editing UI */
380
	protected $showTagEditUI;
381
382
	/**
383
	 * @param HistoryAction $historyPage
384
	 * @param string $year
385
	 * @param string $month
386
	 * @param string $tagFilter
387
	 * @param array $conds
388
	 */
389
	function __construct( $historyPage, $year = '', $month = '', $tagFilter = '', $conds = [] ) {
390
		parent::__construct( $historyPage->getContext() );
391
		$this->historyPage = $historyPage;
392
		$this->tagFilter = $tagFilter;
0 ignored issues
show
The property tagFilter does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
393
		$this->getDateCond( $year, $month );
394
		$this->conds = $conds;
395
		$this->showTagEditUI = ChangeTags::showTagEditingUI( $this->getUser() );
396
	}
397
398
	// For hook compatibility...
399
	function getArticle() {
400
		return $this->historyPage->getArticle();
401
	}
402
403
	function getSqlComment() {
404
		if ( $this->conds ) {
405
			return 'history page filtered'; // potentially slow, see CR r58153
406
		} else {
407
			return 'history page unfiltered';
408
		}
409
	}
410
411
	function getQueryInfo() {
412
		$queryInfo = [
413
			'tables' => [ 'revision', 'user' ],
414
			'fields' => array_merge( Revision::selectFields(), Revision::selectUserFields() ),
415
			'conds' => array_merge(
416
				[ 'rev_page' => $this->getWikiPage()->getId() ],
417
				$this->conds ),
418
			'options' => [ 'USE INDEX' => [ 'revision' => 'page_timestamp' ] ],
419
			'join_conds' => [ 'user' => Revision::userJoinCond() ],
420
		];
421
		ChangeTags::modifyDisplayQuery(
422
			$queryInfo['tables'],
423
			$queryInfo['fields'],
424
			$queryInfo['conds'],
425
			$queryInfo['join_conds'],
426
			$queryInfo['options'],
427
			$this->tagFilter
428
		);
429
		Hooks::run( 'PageHistoryPager::getQueryInfo', [ &$this, &$queryInfo ] );
430
431
		return $queryInfo;
432
	}
433
434
	function getIndexField() {
435
		return 'rev_timestamp';
436
	}
437
438
	/**
439
	 * @param stdClass $row
440
	 * @return string
441
	 */
442
	function formatRow( $row ) {
443
		if ( $this->lastRow ) {
444
			$latest = ( $this->counter == 1 && $this->mIsFirst );
445
			$firstInList = $this->counter == 1;
446
			$this->counter++;
447
448
			$notifTimestamp = $this->getConfig()->get( 'ShowUpdatedMarker' )
449
				? $this->getTitle()->getNotificationTimestamp( $this->getUser() )
450
				: false;
451
452
			$s = $this->historyLine(
453
				$this->lastRow, $row, $notifTimestamp, $latest, $firstInList );
0 ignored issues
show
It seems like $this->lastRow can also be of type boolean; however, HistoryPager::historyLine() does only seem to accept object<stdClass>, 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...
454
		} else {
455
			$s = '';
456
		}
457
		$this->lastRow = $row;
458
459
		return $s;
460
	}
461
462
	function doBatchLookups() {
463
		if ( !Hooks::run( 'PageHistoryPager::doBatchLookups', [ $this, $this->mResult ] ) ) {
464
			return;
465
		}
466
467
		# Do a link batch query
468
		$this->mResult->seek( 0 );
469
		$batch = new LinkBatch();
470
		$revIds = [];
471
		foreach ( $this->mResult as $row ) {
472
			if ( $row->rev_parent_id ) {
473
				$revIds[] = $row->rev_parent_id;
474
			}
475
			if ( !is_null( $row->user_name ) ) {
476
				$batch->add( NS_USER, $row->user_name );
477
				$batch->add( NS_USER_TALK, $row->user_name );
478
			} else { # for anons or usernames of imported revisions
479
				$batch->add( NS_USER, $row->rev_user_text );
480
				$batch->add( NS_USER_TALK, $row->rev_user_text );
481
			}
482
		}
483
		$this->parentLens = Revision::getParentLengths( $this->mDb, $revIds );
0 ignored issues
show
It seems like $this->mDb can be null; however, getParentLengths() 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...
484
		$batch->execute();
485
		$this->mResult->seek( 0 );
486
	}
487
488
	/**
489
	 * Creates begin of history list with a submit button
490
	 *
491
	 * @return string HTML output
492
	 */
493
	function getStartBody() {
494
		$this->lastRow = false;
495
		$this->counter = 1;
496
		$this->oldIdChecked = 0;
497
498
		$this->getOutput()->wrapWikiMsg( "<div class='mw-history-legend'>\n$1\n</div>", 'histlegend' );
499
		$s = Html::openElement( 'form', [ 'action' => wfScript(),
500
			'id' => 'mw-history-compare' ] ) . "\n";
501
		$s .= Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) . "\n";
502
		$s .= Html::hidden( 'action', 'historysubmit' ) . "\n";
503
		$s .= Html::hidden( 'type', 'revision' ) . "\n";
504
505
		// Button container stored in $this->buttons for re-use in getEndBody()
506
		$this->buttons = '<div>';
507
		$className = 'historysubmit mw-history-compareselectedversions-button';
508
		$attrs = [ 'class' => $className ]
509
			+ Linker::tooltipAndAccesskeyAttribs( 'compareselectedversions' );
510
		$this->buttons .= $this->submitButton( $this->msg( 'compareselectedversions' )->text(),
511
			$attrs
512
		) . "\n";
513
514
		$user = $this->getUser();
515
		$actionButtons = '';
516
		if ( $user->isAllowed( 'deleterevision' ) ) {
517
			$actionButtons .= $this->getRevisionButton( 'revisiondelete', 'showhideselectedversions' );
518
		}
519
		if ( $this->showTagEditUI ) {
520
			$actionButtons .= $this->getRevisionButton( 'editchangetags', 'history-edit-tags' );
521
		}
522
		if ( $actionButtons ) {
523
			$this->buttons .= Xml::tags( 'div', [ 'class' =>
524
				'mw-history-revisionactions' ], $actionButtons );
525
		}
526
527
		if ( $user->isAllowed( 'deleterevision' ) || $this->showTagEditUI ) {
528
			$this->buttons .= ( new ListToggle( $this->getOutput() ) )->getHTML();
529
		}
530
531
		$this->buttons .= '</div>';
532
533
		$s .= $this->buttons;
534
		$s .= '<ul id="pagehistory">' . "\n";
535
536
		return $s;
537
	}
538
539
	private function getRevisionButton( $name, $msg ) {
540
		$this->preventClickjacking();
541
		# Note bug #20966, <button> is non-standard in IE<8
542
		$element = Html::element(
543
			'button',
544
			[
545
				'type' => 'submit',
546
				'name' => $name,
547
				'value' => '1',
548
				'class' => "historysubmit mw-history-$name-button",
549
			],
550
			$this->msg( $msg )->text()
551
		) . "\n";
552
		return $element;
553
	}
554
555
	function getEndBody() {
556
		if ( $this->lastRow ) {
557
			$latest = $this->counter == 1 && $this->mIsFirst;
558
			$firstInList = $this->counter == 1;
559
			if ( $this->mIsBackwards ) {
560
				# Next row is unknown, but for UI reasons, probably exists if an offset has been specified
561
				if ( $this->mOffset == '' ) {
562
					$next = null;
563
				} else {
564
					$next = 'unknown';
565
				}
566
			} else {
567
				# The next row is the past-the-end row
568
				$next = $this->mPastTheEndRow;
569
			}
570
			$this->counter++;
571
572
			$notifTimestamp = $this->getConfig()->get( 'ShowUpdatedMarker' )
573
				? $this->getTitle()->getNotificationTimestamp( $this->getUser() )
574
				: false;
575
576
			$s = $this->historyLine(
577
				$this->lastRow, $next, $notifTimestamp, $latest, $firstInList );
0 ignored issues
show
It seems like $this->lastRow can also be of type boolean; however, HistoryPager::historyLine() does only seem to accept object<stdClass>, 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...
578
		} else {
579
			$s = '';
580
		}
581
		$s .= "</ul>\n";
582
		# Add second buttons only if there is more than one rev
583
		if ( $this->getNumRows() > 2 ) {
584
			$s .= $this->buttons;
585
		}
586
		$s .= '</form>';
587
588
		return $s;
589
	}
590
591
	/**
592
	 * Creates a submit button
593
	 *
594
	 * @param string $message Text of the submit button, will be escaped
595
	 * @param array $attributes Attributes
596
	 * @return string HTML output for the submit button
597
	 */
598
	function submitButton( $message, $attributes = [] ) {
599
		# Disable submit button if history has 1 revision only
600
		if ( $this->getNumRows() > 1 ) {
601
			return Html::submitButton( $message, $attributes );
602
		} else {
603
			return '';
604
		}
605
	}
606
607
	/**
608
	 * Returns a row from the history printout.
609
	 *
610
	 * @todo document some more, and maybe clean up the code (some params redundant?)
611
	 *
612
	 * @param stdClass $row The database row corresponding to the previous line.
613
	 * @param mixed $next The database row corresponding to the next line
614
	 *   (chronologically previous)
615
	 * @param bool|string $notificationtimestamp
616
	 * @param bool $latest Whether this row corresponds to the page's latest revision.
617
	 * @param bool $firstInList Whether this row corresponds to the first
618
	 *   displayed on this history page.
619
	 * @return string HTML output for the row
620
	 */
621
	function historyLine( $row, $next, $notificationtimestamp = false,
622
		$latest = false, $firstInList = false ) {
623
		$rev = new Revision( $row );
624
		$rev->setTitle( $this->getTitle() );
0 ignored issues
show
It seems like $this->getTitle() can be null; however, setTitle() 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...
625
626
		if ( is_object( $next ) ) {
627
			$prevRev = new Revision( $next );
628
			$prevRev->setTitle( $this->getTitle() );
0 ignored issues
show
It seems like $this->getTitle() can be null; however, setTitle() 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...
629
		} else {
630
			$prevRev = null;
631
		}
632
633
		$curlink = $this->curLink( $rev, $latest );
634
		$lastlink = $this->lastLink( $rev, $next );
635
		$curLastlinks = $curlink . $this->historyPage->message['pipe-separator'] . $lastlink;
636
		$histLinks = Html::rawElement(
637
			'span',
638
			[ 'class' => 'mw-history-histlinks' ],
639
			$this->msg( 'parentheses' )->rawParams( $curLastlinks )->escaped()
640
		);
641
642
		$diffButtons = $this->diffButtons( $rev, $firstInList );
643
		$s = $histLinks . $diffButtons;
644
645
		$link = $this->revLink( $rev );
646
		$classes = [];
647
648
		$del = '';
649
		$user = $this->getUser();
650
		$canRevDelete = $user->isAllowed( 'deleterevision' );
651
		// Show checkboxes for each revision, to allow for revision deletion and
652
		// change tags
653
		if ( $canRevDelete || $this->showTagEditUI ) {
654
			$this->preventClickjacking();
655
			// If revision was hidden from sysops and we don't need the checkbox
656
			// for anything else, disable it
657 View Code Duplication
			if ( !$this->showTagEditUI && !$rev->userCan( Revision::DELETED_RESTRICTED, $user ) ) {
658
				$del = Xml::check( 'deleterevisions', false, [ 'disabled' => 'disabled' ] );
659
			// Otherwise, enable the checkbox...
660
			} else {
661
				$del = Xml::check( 'showhiderevisions', false,
662
					[ 'name' => 'ids[' . $rev->getId() . ']' ] );
663
			}
664
		// User can only view deleted revisions...
665
		} elseif ( $rev->getVisibility() && $user->isAllowed( 'deletedhistory' ) ) {
666
			// If revision was hidden from sysops, disable the link
667
			if ( !$rev->userCan( Revision::DELETED_RESTRICTED, $user ) ) {
668
				$del = Linker::revDeleteLinkDisabled( false );
669
			// Otherwise, show the link...
670
			} else {
671
				$query = [ 'type' => 'revision',
672
					'target' => $this->getTitle()->getPrefixedDBkey(), 'ids' => $rev->getId() ];
673
				$del .= Linker::revDeleteLink( $query,
674
					$rev->isDeleted( Revision::DELETED_RESTRICTED ), false );
675
			}
676
		}
677
		if ( $del ) {
678
			$s .= " $del ";
679
		}
680
681
		$lang = $this->getLanguage();
682
		$dirmark = $lang->getDirMark();
683
684
		$s .= " $link";
685
		$s .= $dirmark;
686
		$s .= " <span class='history-user'>" .
687
			Linker::revUserTools( $rev, true ) . "</span>";
688
		$s .= $dirmark;
689
690
		if ( $rev->isMinor() ) {
691
			$s .= ' ' . ChangesList::flag( 'minor', $this->getContext() );
692
		}
693
694
		# Sometimes rev_len isn't populated
695
		if ( $rev->getSize() !== null ) {
696
			# Size is always public data
697
			$prevSize = isset( $this->parentLens[$row->rev_parent_id] )
698
				? $this->parentLens[$row->rev_parent_id]
699
				: 0;
700
			$sDiff = ChangesList::showCharacterDifference( $prevSize, $rev->getSize() );
701
			$fSize = Linker::formatRevisionSize( $rev->getSize() );
702
			$s .= ' <span class="mw-changeslist-separator">. .</span> ' . "$fSize $sDiff";
703
		}
704
705
		# Text following the character difference is added just before running hooks
706
		$s2 = Linker::revComment( $rev, false, true );
707
708
		if ( $notificationtimestamp && ( $row->rev_timestamp >= $notificationtimestamp ) ) {
709
			$s2 .= ' <span class="updatedmarker">' . $this->msg( 'updatedmarker' )->escaped() . '</span>';
710
			$classes[] = 'mw-history-line-updated';
711
		}
712
713
		$tools = [];
714
715
		# Rollback and undo links
716
		if ( $prevRev && $this->getTitle()->quickUserCan( 'edit', $user ) ) {
717
			if ( $latest && $this->getTitle()->quickUserCan( 'rollback', $user ) ) {
718
				// Get a rollback link without the brackets
719
				$rollbackLink = Linker::generateRollback(
720
					$rev,
721
					$this->getContext(),
722
					[ 'verify', 'noBrackets' ]
723
				);
724
				if ( $rollbackLink ) {
725
					$this->preventClickjacking();
726
					$tools[] = $rollbackLink;
727
				}
728
			}
729
730
			if ( !$rev->isDeleted( Revision::DELETED_TEXT )
731
				&& !$prevRev->isDeleted( Revision::DELETED_TEXT )
732
			) {
733
				# Create undo tooltip for the first (=latest) line only
734
				$undoTooltip = $latest
735
					? [ 'title' => $this->msg( 'tooltip-undo' )->text() ]
736
					: [];
737
				$undolink = Linker::linkKnown(
738
					$this->getTitle(),
739
					$this->msg( 'editundo' )->escaped(),
740
					$undoTooltip,
741
					[
742
						'action' => 'edit',
743
						'undoafter' => $prevRev->getId(),
744
						'undo' => $rev->getId()
745
					]
746
				);
747
				$tools[] = "<span class=\"mw-history-undo\">{$undolink}</span>";
748
			}
749
		}
750
		// Allow extension to add their own links here
751
		Hooks::run( 'HistoryRevisionTools', [ $rev, &$tools, $prevRev, $user ] );
752
753
		if ( $tools ) {
754
			$s2 .= ' ' . $this->msg( 'parentheses' )->rawParams( $lang->pipeList( $tools ) )->escaped();
755
		}
756
757
		# Tags
758
		list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow(
759
			$row->ts_tags,
760
			'history',
761
			$this->getContext()
762
		);
763
		$classes = array_merge( $classes, $newClasses );
764
		if ( $tagSummary !== '' ) {
765
			$s2 .= " $tagSummary";
766
		}
767
768
		# Include separator between character difference and following text
769
		if ( $s2 !== '' ) {
770
			$s .= ' <span class="mw-changeslist-separator">. .</span> ' . $s2;
771
		}
772
773
		Hooks::run( 'PageHistoryLineEnding', [ $this, &$row, &$s, &$classes ] );
774
775
		$attribs = [];
776
		if ( $classes ) {
777
			$attribs['class'] = implode( ' ', $classes );
778
		}
779
780
		return Xml::tags( 'li', $attribs, $s ) . "\n";
781
	}
782
783
	/**
784
	 * Create a link to view this revision of the page
785
	 *
786
	 * @param Revision $rev
787
	 * @return string
788
	 */
789
	function revLink( $rev ) {
790
		$date = $this->getLanguage()->userTimeAndDate( $rev->getTimestamp(), $this->getUser() );
791
		$date = htmlspecialchars( $date );
792
		if ( $rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
793
			$link = Linker::linkKnown(
794
				$this->getTitle(),
795
				$date,
796
				[ 'class' => 'mw-changeslist-date' ],
797
				[ 'oldid' => $rev->getId() ]
798
			);
799
		} else {
800
			$link = $date;
801
		}
802
		if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
803
			$link = "<span class=\"history-deleted\">$link</span>";
804
		}
805
806
		return $link;
807
	}
808
809
	/**
810
	 * Create a diff-to-current link for this revision for this page
811
	 *
812
	 * @param Revision $rev
813
	 * @param bool $latest This is the latest revision of the page?
814
	 * @return string
815
	 */
816
	function curLink( $rev, $latest ) {
817
		$cur = $this->historyPage->message['cur'];
818
		if ( $latest || !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
819
			return $cur;
820
		} else {
821
			return Linker::linkKnown(
822
				$this->getTitle(),
823
				$cur,
824
				[],
825
				[
826
					'diff' => $this->getWikiPage()->getLatest(),
827
					'oldid' => $rev->getId()
828
				]
829
			);
830
		}
831
	}
832
833
	/**
834
	 * Create a diff-to-previous link for this revision for this page.
835
	 *
836
	 * @param Revision $prevRev The revision being displayed
837
	 * @param stdClass|string|null $next The next revision in list (that is
838
	 *        the previous one in chronological order).
839
	 *        May either be a row, "unknown" or null.
840
	 * @return string
841
	 */
842
	function lastLink( $prevRev, $next ) {
843
		$last = $this->historyPage->message['last'];
844
845
		if ( $next === null ) {
846
			# Probably no next row
847
			return $last;
848
		}
849
850
		if ( $next === 'unknown' ) {
851
			# Next row probably exists but is unknown, use an oldid=prev link
852
			return Linker::linkKnown(
853
				$this->getTitle(),
854
				$last,
855
				[],
856
				[
857
					'diff' => $prevRev->getId(),
858
					'oldid' => 'prev'
859
				]
860
			);
861
		}
862
863
		$nextRev = new Revision( $next );
0 ignored issues
show
It seems like $next defined by parameter $next on line 842 can also be of type string; however, Revision::__construct() does only seem to accept object|array, 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...
864
865
		if ( !$prevRev->userCan( Revision::DELETED_TEXT, $this->getUser() )
866
			|| !$nextRev->userCan( Revision::DELETED_TEXT, $this->getUser() )
867
		) {
868
			return $last;
869
		}
870
871
		return Linker::linkKnown(
872
			$this->getTitle(),
873
			$last,
874
			[],
875
			[
876
				'diff' => $prevRev->getId(),
877
				'oldid' => $next->rev_id
878
			]
879
		);
880
	}
881
882
	/**
883
	 * Create radio buttons for page history
884
	 *
885
	 * @param Revision $rev
886
	 * @param bool $firstInList Is this version the first one?
887
	 *
888
	 * @return string HTML output for the radio buttons
889
	 */
890
	function diffButtons( $rev, $firstInList ) {
891
		if ( $this->getNumRows() > 1 ) {
892
			$id = $rev->getId();
893
			$radio = [ 'type' => 'radio', 'value' => $id ];
894
			/** @todo Move title texts to javascript */
895
			if ( $firstInList ) {
896
				$first = Xml::element( 'input',
897
					array_merge( $radio, [
898
						'style' => 'visibility:hidden',
899
						'name' => 'oldid',
900
						'id' => 'mw-oldid-null' ] )
901
				);
902
				$checkmark = [ 'checked' => 'checked' ];
903
			} else {
904
				# Check visibility of old revisions
905
				if ( !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
906
					$radio['disabled'] = 'disabled';
907
					$checkmark = []; // We will check the next possible one
908
				} elseif ( !$this->oldIdChecked ) {
909
					$checkmark = [ 'checked' => 'checked' ];
910
					$this->oldIdChecked = $id;
911
				} else {
912
					$checkmark = [];
913
				}
914
				$first = Xml::element( 'input',
915
					array_merge( $radio, $checkmark, [
916
						'name' => 'oldid',
917
						'id' => "mw-oldid-$id" ] ) );
918
				$checkmark = [];
919
			}
920
			$second = Xml::element( 'input',
921
				array_merge( $radio, $checkmark, [
922
					'name' => 'diff',
923
					'id' => "mw-diff-$id" ] ) );
924
925
			return $first . $second;
926
		} else {
927
			return '';
928
		}
929
	}
930
931
	/**
932
	 * This is called if a write operation is possible from the generated HTML
933
	 * @param bool $enable
934
	 */
935
	function preventClickjacking( $enable = true ) {
936
		$this->preventClickjacking = $enable;
937
	}
938
939
	/**
940
	 * Get the "prevent clickjacking" flag
941
	 * @return bool
942
	 */
943
	function getPreventClickjacking() {
944
		return $this->preventClickjacking;
945
	}
946
947
}
948