Completed
Branch master (939199)
by
unknown
39:35
created

includes/actions/InfoAction.php (15 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
 * Displays information about a page.
4
 *
5
 * Copyright © 2011 Alexandre Emsenhuber
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
18
 * along with this program; if not, write to the Free Software
19
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
20
 *
21
 * @file
22
 * @ingroup Actions
23
 */
24
25
use MediaWiki\MediaWikiServices;
26
27
/**
28
 * Displays information about a page.
29
 *
30
 * @ingroup Actions
31
 */
32
class InfoAction extends FormlessAction {
33
	const VERSION = 1;
34
35
	/**
36
	 * Returns the name of the action this object responds to.
37
	 *
38
	 * @return string Lowercase name
39
	 */
40
	public function getName() {
41
		return 'info';
42
	}
43
44
	/**
45
	 * Whether this action can still be executed by a blocked user.
46
	 *
47
	 * @return bool
48
	 */
49
	public function requiresUnblock() {
50
		return false;
51
	}
52
53
	/**
54
	 * Whether this action requires the wiki not to be locked.
55
	 *
56
	 * @return bool
57
	 */
58
	public function requiresWrite() {
59
		return false;
60
	}
61
62
	/**
63
	 * Clear the info cache for a given Title.
64
	 *
65
	 * @since 1.22
66
	 * @param Title $title Title to clear cache for
67
	 * @param int|null $revid Revision id to clear
68
	 */
69
	public static function invalidateCache( Title $title, $revid = null ) {
70
		if ( !$revid ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $revid of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. 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 integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
71
			$revision = Revision::newFromTitle( $title, 0, Revision::READ_LATEST );
72
			$revid = $revision ? $revision->getId() : null;
73
		}
74
		if ( $revid !== null ) {
75
			$key = self::getCacheKey( $title, $revid );
76
			ObjectCache::getMainWANInstance()->delete( $key );
77
		}
78
	}
79
80
	/**
81
	 * Shows page information on GET request.
82
	 *
83
	 * @return string Page information that will be added to the output
84
	 */
85
	public function onView() {
86
		$content = '';
87
88
		// Validate revision
89
		$oldid = $this->page->getOldID();
0 ignored issues
show
The method getOldID does only exist in Article and CategoryPage and ImagePage, but not in Page and WikiPage.

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...
90
		if ( $oldid ) {
91
			$revision = $this->page->getRevisionFetched();
0 ignored issues
show
The method getRevisionFetched does only exist in Article and CategoryPage and ImagePage, but not in Page and WikiPage.

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...
92
93
			// Revision is missing
94
			if ( $revision === null ) {
95
				return $this->msg( 'missing-revision', $oldid )->parse();
96
			}
97
98
			// Revision is not current
99
			if ( !$revision->isCurrent() ) {
100
				return $this->msg( 'pageinfo-not-current' )->plain();
101
			}
102
		}
103
104
		// Page header
105
		if ( !$this->msg( 'pageinfo-header' )->isDisabled() ) {
106
			$content .= $this->msg( 'pageinfo-header' )->parse();
107
		}
108
109
		// Hide "This page is a member of # hidden categories" explanation
110
		$content .= Html::element( 'style', [],
111
			'.mw-hiddenCategoriesExplanation { display: none; }' ) . "\n";
112
113
		// Hide "Templates used on this page" explanation
114
		$content .= Html::element( 'style', [],
115
			'.mw-templatesUsedExplanation { display: none; }' ) . "\n";
116
117
		// Get page information
118
		$pageInfo = $this->pageInfo();
119
120
		// Allow extensions to add additional information
121
		Hooks::run( 'InfoAction', [ $this->getContext(), &$pageInfo ] );
122
123
		// Render page information
124
		foreach ( $pageInfo as $header => $infoTable ) {
125
			// Messages:
126
			// pageinfo-header-basic, pageinfo-header-edits, pageinfo-header-restrictions,
127
			// pageinfo-header-properties, pageinfo-category-info
128
			$content .= $this->makeHeader( $this->msg( "pageinfo-${header}" )->escaped() ) . "\n";
129
			$table = "\n";
130
			foreach ( $infoTable as $infoRow ) {
131
				$name = ( $infoRow[0] instanceof Message ) ? $infoRow[0]->escaped() : $infoRow[0];
132
				$value = ( $infoRow[1] instanceof Message ) ? $infoRow[1]->escaped() : $infoRow[1];
133
				$id = ( $infoRow[0] instanceof Message ) ? $infoRow[0]->getKey() : null;
134
				$table = $this->addRow( $table, $name, $value, $id ) . "\n";
135
			}
136
			$content = $this->addTable( $content, $table ) . "\n";
137
		}
138
139
		// Page footer
140
		if ( !$this->msg( 'pageinfo-footer' )->isDisabled() ) {
141
			$content .= $this->msg( 'pageinfo-footer' )->parse();
142
		}
143
144
		return $content;
145
	}
146
147
	/**
148
	 * Creates a header that can be added to the output.
149
	 *
150
	 * @param string $header The header text.
151
	 * @return string The HTML.
152
	 */
153
	protected function makeHeader( $header ) {
154
		$spanAttribs = [ 'class' => 'mw-headline', 'id' => Sanitizer::escapeId( $header ) ];
155
156
		return Html::rawElement( 'h2', [], Html::element( 'span', $spanAttribs, $header ) );
157
	}
158
159
	/**
160
	 * Adds a row to a table that will be added to the content.
161
	 *
162
	 * @param string $table The table that will be added to the content
163
	 * @param string $name The name of the row
164
	 * @param string $value The value of the row
165
	 * @param string $id The ID to use for the 'tr' element
166
	 * @return string The table with the row added
167
	 */
168
	protected function addRow( $table, $name, $value, $id ) {
169
		return $table .
170
			Html::rawElement(
171
				'tr',
172
				$id === null ? [] : [ 'id' => 'mw-' . $id ],
173
				Html::rawElement( 'td', [ 'style' => 'vertical-align: top;' ], $name ) .
174
					Html::rawElement( 'td', [], $value )
175
			);
176
	}
177
178
	/**
179
	 * Adds a table to the content that will be added to the output.
180
	 *
181
	 * @param string $content The content that will be added to the output
182
	 * @param string $table The table
183
	 * @return string The content with the table added
184
	 */
185
	protected function addTable( $content, $table ) {
186
		return $content . Html::rawElement( 'table', [ 'class' => 'wikitable mw-page-info' ],
187
			$table );
188
	}
189
190
	/**
191
	 * Returns page information in an easily-manipulated format. Array keys are used so extensions
192
	 * may add additional information in arbitrary positions. Array values are arrays with one
193
	 * element to be rendered as a header, arrays with two elements to be rendered as a table row.
194
	 *
195
	 * @return array
196
	 */
197
	protected function pageInfo() {
198
		global $wgContLang;
199
200
		$user = $this->getUser();
201
		$lang = $this->getLanguage();
202
		$title = $this->getTitle();
203
		$id = $title->getArticleID();
204
		$config = $this->context->getConfig();
205
		$linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
206
207
		$pageCounts = $this->pageCounts( $this->page );
208
209
		$pageProperties = [];
210
		$props = PageProps::getInstance()->getAllProperties( $title );
211
		if ( isset( $props[$id] ) ) {
212
			$pageProperties = $props[$id];
213
		}
214
215
		// Basic information
216
		$pageInfo = [];
217
		$pageInfo['header-basic'] = [];
218
219
		// Display title
220
		$displayTitle = $title->getPrefixedText();
221
		if ( isset( $pageProperties['displaytitle'] ) ) {
222
			$displayTitle = $pageProperties['displaytitle'];
223
		}
224
225
		$pageInfo['header-basic'][] = [
226
			$this->msg( 'pageinfo-display-title' ), $displayTitle
227
		];
228
229
		// Is it a redirect? If so, where to?
230
		if ( $title->isRedirect() ) {
231
			$pageInfo['header-basic'][] = [
232
				$this->msg( 'pageinfo-redirectsto' ),
233
				Linker::link( $this->page->getRedirectTarget() ) .
0 ignored issues
show
The method getRedirectTarget 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...
234
				$this->msg( 'word-separator' )->escaped() .
235
				$this->msg( 'parentheses' )->rawParams( Linker::link(
236
					$this->page->getRedirectTarget(),
237
					$this->msg( 'pageinfo-redirectsto-info' )->escaped(),
238
					[],
239
					[ 'action' => 'info' ]
240
				) )->escaped()
241
			];
242
		}
243
244
		// Default sort key
245
		$sortKey = $title->getCategorySortkey();
246
		if ( isset( $pageProperties['defaultsort'] ) ) {
247
			$sortKey = $pageProperties['defaultsort'];
248
		}
249
250
		$sortKey = htmlspecialchars( $sortKey );
251
		$pageInfo['header-basic'][] = [ $this->msg( 'pageinfo-default-sort' ), $sortKey ];
252
253
		// Page length (in bytes)
254
		$pageInfo['header-basic'][] = [
255
			$this->msg( 'pageinfo-length' ), $lang->formatNum( $title->getLength() )
256
		];
257
258
		// Page ID (number not localised, as it's a database ID)
259
		$pageInfo['header-basic'][] = [ $this->msg( 'pageinfo-article-id' ), $id ];
260
261
		// Language in which the page content is (supposed to be) written
262
		$pageLang = $title->getPageLanguage()->getCode();
263
264
		if ( $config->get( 'PageLanguageUseDB' )
265
			&& $this->getTitle()->userCan( 'pagelang', $this->getUser() )
266
		) {
267
			// Link to Special:PageLanguage with pre-filled page title if user has permissions
268
			$titleObj = SpecialPage::getTitleFor( 'PageLanguage', $title->getPrefixedText() );
269
			$langDisp = Linker::link(
270
				$titleObj,
271
				$this->msg( 'pageinfo-language' )->escaped()
272
			);
273
		} else {
274
			// Display just the message
275
			$langDisp = $this->msg( 'pageinfo-language' )->escaped();
276
		}
277
278
		$pageInfo['header-basic'][] = [ $langDisp,
279
			Language::fetchLanguageName( $pageLang, $lang->getCode() )
280
			. ' ' . $this->msg( 'parentheses', $pageLang )->escaped() ];
281
282
		// Content model of the page
283
		$modelHtml = htmlspecialchars( ContentHandler::getLocalizedName( $title->getContentModel() ) );
284
		// If the user can change it, add a link to Special:ChangeContentModel
285
		if ( $title->quickUserCan( 'editcontentmodel' ) ) {
286
			$modelHtml .= ' ' . $this->msg( 'parentheses' )->rawParams( $linkRenderer->makeLink(
287
				SpecialPage::getTitleValueFor( 'ChangeContentModel', $title->getPrefixedText() ),
288
				$this->msg( 'pageinfo-content-model-change' )->text()
289
			) )->escaped();
290
		}
291
292
		$pageInfo['header-basic'][] = [
293
			$this->msg( 'pageinfo-content-model' ),
294
			$modelHtml
295
		];
296
297
		if ( $title->inNamespace( NS_USER ) ) {
298
			$pageUser = User::newFromName( $title->getRootText() );
299
			if ( $pageUser && $pageUser->getId() && !$pageUser->isHidden() ) {
300
				$pageInfo['header-basic'][] = [
301
					$this->msg( 'pageinfo-user-id' ),
302
					$pageUser->getId()
303
				];
304
			}
305
		}
306
307
		// Search engine status
308
		$pOutput = new ParserOutput();
309
		if ( isset( $pageProperties['noindex'] ) ) {
310
			$pOutput->setIndexPolicy( 'noindex' );
311
		}
312
		if ( isset( $pageProperties['index'] ) ) {
313
			$pOutput->setIndexPolicy( 'index' );
314
		}
315
316
		// Use robot policy logic
317
		$policy = $this->page->getRobotPolicy( 'view', $pOutput );
0 ignored issues
show
The method getRobotPolicy does only exist in Article and CategoryPage and ImagePage, but not in Page and WikiPage.

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...
318
		$pageInfo['header-basic'][] = [
319
			// Messages: pageinfo-robot-index, pageinfo-robot-noindex
320
			$this->msg( 'pageinfo-robot-policy' ),
321
			$this->msg( "pageinfo-robot-${policy['index']}" )
322
		];
323
324
		$unwatchedPageThreshold = $config->get( 'UnwatchedPageThreshold' );
325
		if (
326
			$user->isAllowed( 'unwatchedpages' ) ||
327
			( $unwatchedPageThreshold !== false &&
328
				$pageCounts['watchers'] >= $unwatchedPageThreshold )
329
		) {
330
			// Number of page watchers
331
			$pageInfo['header-basic'][] = [
332
				$this->msg( 'pageinfo-watchers' ),
333
				$lang->formatNum( $pageCounts['watchers'] )
334
			];
335
			if (
336
				$config->get( 'ShowUpdatedMarker' ) &&
337
				isset( $pageCounts['visitingWatchers'] )
338
			) {
339
				$minToDisclose = $config->get( 'UnwatchedPageSecret' );
340
				if ( $pageCounts['visitingWatchers'] > $minToDisclose ||
341
					$user->isAllowed( 'unwatchedpages' ) ) {
342
					$pageInfo['header-basic'][] = [
343
						$this->msg( 'pageinfo-visiting-watchers' ),
344
						$lang->formatNum( $pageCounts['visitingWatchers'] )
345
					];
346
				} else {
347
					$pageInfo['header-basic'][] = [
348
						$this->msg( 'pageinfo-visiting-watchers' ),
349
						$this->msg( 'pageinfo-few-visiting-watchers' )
350
					];
351
				}
352
			}
353
		} elseif ( $unwatchedPageThreshold !== false ) {
354
			$pageInfo['header-basic'][] = [
355
				$this->msg( 'pageinfo-watchers' ),
356
				$this->msg( 'pageinfo-few-watchers' )->numParams( $unwatchedPageThreshold )
357
			];
358
		}
359
360
		// Redirects to this page
361
		$whatLinksHere = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedText() );
362
		$pageInfo['header-basic'][] = [
363
			Linker::link(
364
				$whatLinksHere,
365
				$this->msg( 'pageinfo-redirects-name' )->escaped(),
366
				[],
367
				[
368
					'hidelinks' => 1,
369
					'hidetrans' => 1,
370
					'hideimages' => $title->getNamespace() == NS_FILE
371
				]
372
			),
373
			$this->msg( 'pageinfo-redirects-value' )
374
				->numParams( count( $title->getRedirectsHere() ) )
375
		];
376
377
		// Is it counted as a content page?
378
		if ( $this->page->isCountable() ) {
0 ignored issues
show
The method isCountable 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...
379
			$pageInfo['header-basic'][] = [
380
				$this->msg( 'pageinfo-contentpage' ),
381
				$this->msg( 'pageinfo-contentpage-yes' )
382
			];
383
		}
384
385
		// Subpages of this page, if subpages are enabled for the current NS
386
		if ( MWNamespace::hasSubpages( $title->getNamespace() ) ) {
387
			$prefixIndex = SpecialPage::getTitleFor(
388
				'Prefixindex', $title->getPrefixedText() . '/' );
389
			$pageInfo['header-basic'][] = [
390
				Linker::link( $prefixIndex, $this->msg( 'pageinfo-subpages-name' )->escaped() ),
391
				$this->msg( 'pageinfo-subpages-value' )
392
					->numParams(
393
						$pageCounts['subpages']['total'],
394
						$pageCounts['subpages']['redirects'],
395
						$pageCounts['subpages']['nonredirects'] )
396
			];
397
		}
398
399
		if ( $title->inNamespace( NS_CATEGORY ) ) {
400
			$category = Category::newFromTitle( $title );
401
402
			// $allCount is the total number of cat members,
403
			// not the count of how many members are normal pages.
404
			$allCount = (int)$category->getPageCount();
405
			$subcatCount = (int)$category->getSubcatCount();
406
			$fileCount = (int)$category->getFileCount();
407
			$pagesCount = $allCount - $subcatCount - $fileCount;
408
409
			$pageInfo['category-info'] = [
410
				[
411
					$this->msg( 'pageinfo-category-total' ),
412
					$lang->formatNum( $allCount )
413
				],
414
				[
415
					$this->msg( 'pageinfo-category-pages' ),
416
					$lang->formatNum( $pagesCount )
417
				],
418
				[
419
					$this->msg( 'pageinfo-category-subcats' ),
420
					$lang->formatNum( $subcatCount )
421
				],
422
				[
423
					$this->msg( 'pageinfo-category-files' ),
424
					$lang->formatNum( $fileCount )
425
				]
426
			];
427
		}
428
429
		// Page protection
430
		$pageInfo['header-restrictions'] = [];
431
432
		// Is this page affected by the cascading protection of something which includes it?
433
		if ( $title->isCascadeProtected() ) {
434
			$cascadingFrom = '';
435
			$sources = $title->getCascadeProtectionSources()[0];
436
437
			foreach ( $sources as $sourceTitle ) {
0 ignored issues
show
The expression $sources of type array|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
438
				$cascadingFrom .= Html::rawElement(
439
					'li', [], Linker::linkKnown( $sourceTitle ) );
440
			}
441
442
			$cascadingFrom = Html::rawElement( 'ul', [], $cascadingFrom );
443
			$pageInfo['header-restrictions'][] = [
444
				$this->msg( 'pageinfo-protect-cascading-from' ),
445
				$cascadingFrom
446
			];
447
		}
448
449
		// Is out protection set to cascade to other pages?
450
		if ( $title->areRestrictionsCascading() ) {
451
			$pageInfo['header-restrictions'][] = [
452
				$this->msg( 'pageinfo-protect-cascading' ),
453
				$this->msg( 'pageinfo-protect-cascading-yes' )
454
			];
455
		}
456
457
		// Page protection
458
		foreach ( $title->getRestrictionTypes() as $restrictionType ) {
459
			$protectionLevel = implode( ', ', $title->getRestrictions( $restrictionType ) );
460
461
			if ( $protectionLevel == '' ) {
462
				// Allow all users
463
				$message = $this->msg( 'protect-default' )->escaped();
464
			} else {
465
				// Administrators only
466
				// Messages: protect-level-autoconfirmed, protect-level-sysop
467
				$message = $this->msg( "protect-level-$protectionLevel" );
468
				if ( $message->isDisabled() ) {
469
					// Require "$1" permission
470
					$message = $this->msg( "protect-fallback", $protectionLevel )->parse();
471
				} else {
472
					$message = $message->escaped();
473
				}
474
			}
475
			$expiry = $title->getRestrictionExpiry( $restrictionType );
476
			$formattedexpiry = $this->msg( 'parentheses',
477
				$this->getLanguage()->formatExpiry( $expiry ) )->escaped();
0 ignored issues
show
It seems like $expiry defined by $title->getRestrictionExpiry($restrictionType) on line 475 can also be of type boolean; however, Language::formatExpiry() 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...
478
			$message .= $this->msg( 'word-separator' )->escaped() . $formattedexpiry;
479
480
			// Messages: restriction-edit, restriction-move, restriction-create,
481
			// restriction-upload
482
			$pageInfo['header-restrictions'][] = [
483
				$this->msg( "restriction-$restrictionType" ), $message
484
			];
485
		}
486
487
		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...
488
			return $pageInfo;
489
		}
490
491
		// Edit history
492
		$pageInfo['header-edits'] = [];
493
494
		$firstRev = $this->page->getOldestRevision();
0 ignored issues
show
The method getOldestRevision 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...
495
		$lastRev = $this->page->getRevision();
0 ignored issues
show
The method getRevision 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...
496
		$batch = new LinkBatch;
497
498 View Code Duplication
		if ( $firstRev ) {
499
			$firstRevUser = $firstRev->getUserText( Revision::FOR_THIS_USER );
500
			if ( $firstRevUser !== '' ) {
501
				$firstRevUserTitle = Title::makeTitle( NS_USER, $firstRevUser );
502
				$batch->addObj( $firstRevUserTitle );
503
				$batch->addObj( $firstRevUserTitle->getTalkPage() );
504
			}
505
		}
506
507 View Code Duplication
		if ( $lastRev ) {
508
			$lastRevUser = $lastRev->getUserText( Revision::FOR_THIS_USER );
509
			if ( $lastRevUser !== '' ) {
510
				$lastRevUserTitle = Title::makeTitle( NS_USER, $lastRevUser );
511
				$batch->addObj( $lastRevUserTitle );
512
				$batch->addObj( $lastRevUserTitle->getTalkPage() );
513
			}
514
		}
515
516
		$batch->execute();
517
518
		if ( $firstRev ) {
519
			// Page creator
520
			$pageInfo['header-edits'][] = [
521
				$this->msg( 'pageinfo-firstuser' ),
522
				Linker::revUserTools( $firstRev )
523
			];
524
525
			// Date of page creation
526
			$pageInfo['header-edits'][] = [
527
				$this->msg( 'pageinfo-firsttime' ),
528
				Linker::linkKnown(
529
					$title,
530
					htmlspecialchars( $lang->userTimeAndDate( $firstRev->getTimestamp(), $user ) ),
531
					[],
532
					[ 'oldid' => $firstRev->getId() ]
533
				)
534
			];
535
		}
536
537
		if ( $lastRev ) {
538
			// Latest editor
539
			$pageInfo['header-edits'][] = [
540
				$this->msg( 'pageinfo-lastuser' ),
541
				Linker::revUserTools( $lastRev )
542
			];
543
544
			// Date of latest edit
545
			$pageInfo['header-edits'][] = [
546
				$this->msg( 'pageinfo-lasttime' ),
547
				Linker::linkKnown(
548
					$title,
549
					htmlspecialchars(
550
						$lang->userTimeAndDate( $this->page->getTimestamp(), $user )
0 ignored issues
show
The method getTimestamp 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...
551
					),
552
					[],
553
					[ 'oldid' => $this->page->getLatest() ]
0 ignored issues
show
The method getLatest 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...
554
				)
555
			];
556
		}
557
558
		// Total number of edits
559
		$pageInfo['header-edits'][] = [
560
			$this->msg( 'pageinfo-edits' ), $lang->formatNum( $pageCounts['edits'] )
561
		];
562
563
		// Total number of distinct authors
564
		if ( $pageCounts['authors'] > 0 ) {
565
			$pageInfo['header-edits'][] = [
566
				$this->msg( 'pageinfo-authors' ), $lang->formatNum( $pageCounts['authors'] )
567
			];
568
		}
569
570
		// Recent number of edits (within past 30 days)
571
		$pageInfo['header-edits'][] = [
572
			$this->msg( 'pageinfo-recent-edits',
573
				$lang->formatDuration( $config->get( 'RCMaxAge' ) ) ),
574
			$lang->formatNum( $pageCounts['recent_edits'] )
575
		];
576
577
		// Recent number of distinct authors
578
		$pageInfo['header-edits'][] = [
579
			$this->msg( 'pageinfo-recent-authors' ),
580
			$lang->formatNum( $pageCounts['recent_authors'] )
581
		];
582
583
		// Array of MagicWord objects
584
		$magicWords = MagicWord::getDoubleUnderscoreArray();
585
586
		// Array of magic word IDs
587
		$wordIDs = $magicWords->names;
588
589
		// Array of IDs => localized magic words
590
		$localizedWords = $wgContLang->getMagicWords();
591
592
		$listItems = [];
593
		foreach ( $pageProperties as $property => $value ) {
594
			if ( in_array( $property, $wordIDs ) ) {
595
				$listItems[] = Html::element( 'li', [], $localizedWords[$property][1] );
596
			}
597
		}
598
599
		$localizedList = Html::rawElement( 'ul', [], implode( '', $listItems ) );
600
		$hiddenCategories = $this->page->getHiddenCategories();
0 ignored issues
show
The method getHiddenCategories 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...
601
602
		if (
603
			count( $listItems ) > 0 ||
604
			count( $hiddenCategories ) > 0 ||
605
			$pageCounts['transclusion']['from'] > 0 ||
606
			$pageCounts['transclusion']['to'] > 0
607
		) {
608
			$options = [ 'LIMIT' => $config->get( 'PageInfoTransclusionLimit' ) ];
609
			$transcludedTemplates = $title->getTemplateLinksFrom( $options );
610
			if ( $config->get( 'MiserMode' ) ) {
611
				$transcludedTargets = [];
612
			} else {
613
				$transcludedTargets = $title->getTemplateLinksTo( $options );
614
			}
615
616
			// Page properties
617
			$pageInfo['header-properties'] = [];
618
619
			// Magic words
620
			if ( count( $listItems ) > 0 ) {
621
				$pageInfo['header-properties'][] = [
622
					$this->msg( 'pageinfo-magic-words' )->numParams( count( $listItems ) ),
623
					$localizedList
624
				];
625
			}
626
627
			// Hidden categories
628
			if ( count( $hiddenCategories ) > 0 ) {
629
				$pageInfo['header-properties'][] = [
630
					$this->msg( 'pageinfo-hidden-categories' )
631
						->numParams( count( $hiddenCategories ) ),
632
					Linker::formatHiddenCategories( $hiddenCategories )
633
				];
634
			}
635
636
			// Transcluded templates
637
			if ( $pageCounts['transclusion']['from'] > 0 ) {
638
				if ( $pageCounts['transclusion']['from'] > count( $transcludedTemplates ) ) {
639
					$more = $this->msg( 'morenotlisted' )->escaped();
640
				} else {
641
					$more = null;
642
				}
643
644
				$templateListFormatter = new TemplatesOnThisPageFormatter(
645
					$this->getContext(),
646
					$linkRenderer
647
				);
648
649
				$pageInfo['header-properties'][] = [
650
					$this->msg( 'pageinfo-templates' )
651
						->numParams( $pageCounts['transclusion']['from'] ),
652
					$templateListFormatter->format( $transcludedTemplates, false, $more )
653
				];
654
			}
655
656
			if ( !$config->get( 'MiserMode' ) && $pageCounts['transclusion']['to'] > 0 ) {
657
				if ( $pageCounts['transclusion']['to'] > count( $transcludedTargets ) ) {
658
					$more = Linker::link(
659
						$whatLinksHere,
660
						$this->msg( 'moredotdotdot' )->escaped(),
661
						[],
662
						[ 'hidelinks' => 1, 'hideredirs' => 1 ]
663
					);
664
				} else {
665
					$more = null;
666
				}
667
668
				$templateListFormatter = new TemplatesOnThisPageFormatter(
669
					$this->getContext(),
670
					$linkRenderer
671
				);
672
673
				$pageInfo['header-properties'][] = [
674
					$this->msg( 'pageinfo-transclusions' )
675
						->numParams( $pageCounts['transclusion']['to'] ),
676
					$templateListFormatter->format( $transcludedTargets, false, $more )
677
				];
678
			}
679
		}
680
681
		return $pageInfo;
682
	}
683
684
	/**
685
	 * Returns page counts that would be too "expensive" to retrieve by normal means.
686
	 *
687
	 * @param WikiPage|Article|Page $page
688
	 * @return array
689
	 */
690
	protected function pageCounts( Page $page ) {
691
		$fname = __METHOD__;
692
		$config = $this->context->getConfig();
693
694
		return ObjectCache::getMainWANInstance()->getWithSetCallback(
695
			self::getCacheKey( $page->getTitle(), $page->getLatest() ),
696
			WANObjectCache::TTL_WEEK,
697
			function ( $oldValue, &$ttl, &$setOpts ) use ( $page, $config, $fname ) {
698
				$title = $page->getTitle();
699
				$id = $title->getArticleID();
700
701
				$dbr = wfGetDB( DB_REPLICA );
702
				$dbrWatchlist = wfGetDB( DB_REPLICA, 'watchlist' );
703
				$setOpts += Database::getCacheSetOptions( $dbr, $dbrWatchlist );
704
705
				$watchedItemStore = MediaWikiServices::getInstance()->getWatchedItemStore();
706
707
				$result = [];
708
				$result['watchers'] = $watchedItemStore->countWatchers( $title );
709
710
				if ( $config->get( 'ShowUpdatedMarker' ) ) {
711
					$updated = wfTimestamp( TS_UNIX, $page->getTimestamp() );
712
					$result['visitingWatchers'] = $watchedItemStore->countVisitingWatchers(
713
						$title,
714
						$updated - $config->get( 'WatchersMaxAge' )
715
					);
716
				}
717
718
				// Total number of edits
719
				$edits = (int)$dbr->selectField(
720
					'revision',
721
					'COUNT(*)',
722
					[ 'rev_page' => $id ],
723
					$fname
724
				);
725
				$result['edits'] = $edits;
726
727
				// Total number of distinct authors
728
				if ( $config->get( 'MiserMode' ) ) {
729
					$result['authors'] = 0;
730
				} else {
731
					$result['authors'] = (int)$dbr->selectField(
732
						'revision',
733
						'COUNT(DISTINCT rev_user_text)',
734
						[ 'rev_page' => $id ],
735
						$fname
736
					);
737
				}
738
739
				// "Recent" threshold defined by RCMaxAge setting
740
				$threshold = $dbr->timestamp( time() - $config->get( 'RCMaxAge' ) );
741
742
				// Recent number of edits
743
				$edits = (int)$dbr->selectField(
744
					'revision',
745
					'COUNT(rev_page)',
746
					[
747
						'rev_page' => $id,
748
						"rev_timestamp >= " . $dbr->addQuotes( $threshold )
749
					],
750
					$fname
751
				);
752
				$result['recent_edits'] = $edits;
753
754
				// Recent number of distinct authors
755
				$result['recent_authors'] = (int)$dbr->selectField(
756
					'revision',
757
					'COUNT(DISTINCT rev_user_text)',
758
					[
759
						'rev_page' => $id,
760
						"rev_timestamp >= " . $dbr->addQuotes( $threshold )
761
					],
762
					$fname
763
				);
764
765
				// Subpages (if enabled)
766
				if ( MWNamespace::hasSubpages( $title->getNamespace() ) ) {
767
					$conds = [ 'page_namespace' => $title->getNamespace() ];
768
					$conds[] = 'page_title ' .
769
						$dbr->buildLike( $title->getDBkey() . '/', $dbr->anyString() );
770
771
					// Subpages of this page (redirects)
772
					$conds['page_is_redirect'] = 1;
773
					$result['subpages']['redirects'] = (int)$dbr->selectField(
774
						'page',
775
						'COUNT(page_id)',
776
						$conds,
777
						$fname
778
					);
779
780
					// Subpages of this page (non-redirects)
781
					$conds['page_is_redirect'] = 0;
782
					$result['subpages']['nonredirects'] = (int)$dbr->selectField(
783
						'page',
784
						'COUNT(page_id)',
785
						$conds,
786
						$fname
787
					);
788
789
					// Subpages of this page (total)
790
					$result['subpages']['total'] = $result['subpages']['redirects']
791
						+ $result['subpages']['nonredirects'];
792
				}
793
794
				// Counts for the number of transclusion links (to/from)
795
				if ( $config->get( 'MiserMode' ) ) {
796
					$result['transclusion']['to'] = 0;
797
				} else {
798
					$result['transclusion']['to'] = (int)$dbr->selectField(
799
						'templatelinks',
800
						'COUNT(tl_from)',
801
						[
802
							'tl_namespace' => $title->getNamespace(),
803
							'tl_title' => $title->getDBkey()
804
						],
805
						$fname
806
					);
807
				}
808
809
				$result['transclusion']['from'] = (int)$dbr->selectField(
810
					'templatelinks',
811
					'COUNT(*)',
812
					[ 'tl_from' => $title->getArticleID() ],
813
					$fname
814
				);
815
816
				return $result;
817
			}
818
		);
819
	}
820
821
	/**
822
	 * Returns the name that goes in the "<h1>" page title.
823
	 *
824
	 * @return string
825
	 */
826
	protected function getPageTitle() {
827
		return $this->msg( 'pageinfo-title', $this->getTitle()->getPrefixedText() )->text();
828
	}
829
830
	/**
831
	 * Get a list of contributors of $article
832
	 * @return string Html
833
	 */
834
	protected function getContributors() {
835
		$contributors = $this->page->getContributors();
0 ignored issues
show
The method getContributors 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...
836
		$real_names = [];
837
		$user_names = [];
838
		$anon_ips = [];
839
840
		# Sift for real versus user names
841
		/** @var $user User */
842
		foreach ( $contributors as $user ) {
843
			$page = $user->isAnon()
844
				? SpecialPage::getTitleFor( 'Contributions', $user->getName() )
845
				: $user->getUserPage();
846
847
			$hiddenPrefs = $this->context->getConfig()->get( 'HiddenPrefs' );
848
			if ( $user->getId() == 0 ) {
849
				$anon_ips[] = Linker::link( $page, htmlspecialchars( $user->getName() ) );
850
			} elseif ( !in_array( 'realname', $hiddenPrefs ) && $user->getRealName() ) {
851
				$real_names[] = Linker::link( $page, htmlspecialchars( $user->getRealName() ) );
852
			} else {
853
				$user_names[] = Linker::link( $page, htmlspecialchars( $user->getName() ) );
854
			}
855
		}
856
857
		$lang = $this->getLanguage();
858
859
		$real = $lang->listToText( $real_names );
860
861
		# "ThisSite user(s) A, B and C"
862 View Code Duplication
		if ( count( $user_names ) ) {
863
			$user = $this->msg( 'siteusers' )
864
				->rawParams( $lang->listToText( $user_names ) )
865
				->params( count( $user_names ) )->escaped();
866
		} else {
867
			$user = false;
868
		}
869
870 View Code Duplication
		if ( count( $anon_ips ) ) {
871
			$anon = $this->msg( 'anonusers' )
872
				->rawParams( $lang->listToText( $anon_ips ) )
873
				->params( count( $anon_ips ) )->escaped();
874
		} else {
875
			$anon = false;
876
		}
877
878
		# This is the big list, all mooshed together. We sift for blank strings
879
		$fulllist = [];
880 View Code Duplication
		foreach ( [ $real, $user, $anon ] as $s ) {
881
			if ( $s !== '' ) {
882
				array_push( $fulllist, $s );
883
			}
884
		}
885
886
		$count = count( $fulllist );
887
888
		# "Based on work by ..."
889
		return $count
890
			? $this->msg( 'othercontribs' )->rawParams(
891
				$lang->listToText( $fulllist ) )->params( $count )->escaped()
892
			: '';
893
	}
894
895
	/**
896
	 * Returns the description that goes below the "<h1>" tag.
897
	 *
898
	 * @return string
899
	 */
900
	protected function getDescription() {
901
		return '';
902
	}
903
904
	/**
905
	 * @param Title $title
906
	 * @param int $revId
907
	 * @return string
908
	 */
909
	protected static function getCacheKey( Title $title, $revId ) {
910
		return wfMemcKey( 'infoaction', md5( $title->getPrefixedText() ), $revId, self::VERSION );
911
	}
912
}
913