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

InfoAction   F

Complexity

Total Complexity 89

Size/Duplication

Total Lines 858
Duplicated Lines 3.85 %

Coupling/Cohesion

Components 1
Dependencies 27

Importance

Changes 0
Metric Value
dl 33
loc 858
rs 1.0434
c 0
b 0
f 0
wmc 89
lcom 1
cbo 27

14 Methods

Rating   Name   Duplication   Size   Complexity  
A getName() 0 3 1
A requiresUnblock() 0 3 1
A requiresWrite() 0 3 1
A invalidateCache() 0 10 4
C onView() 0 61 11
A makeHeader() 0 5 1
A addRow() 0 9 2
A addTable() 0 4 1
F pageInfo() 14 462 48
B pageCounts() 0 131 5
A getPageTitle() 0 3 1
C getContributors() 19 60 11
A getDescription() 0 3 1
A getCacheKey() 0 3 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

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

Common duplication problems, and corresponding solutions are:

Complex Class

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

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

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

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

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
Bug introduced by
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
Bug introduced by
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
206
		$pageCounts = $this->pageCounts( $this->page );
207
208
		$pageProperties = [];
209
		$props = PageProps::getInstance()->getAllProperties( $title );
210
		if ( isset( $props[$id] ) ) {
211
			$pageProperties = $props[$id];
212
		}
213
214
		// Basic information
215
		$pageInfo = [];
216
		$pageInfo['header-basic'] = [];
217
218
		// Display title
219
		$displayTitle = $title->getPrefixedText();
220
		if ( isset( $pageProperties['displaytitle'] ) ) {
221
			$displayTitle = $pageProperties['displaytitle'];
222
		}
223
224
		$pageInfo['header-basic'][] = [
225
			$this->msg( 'pageinfo-display-title' ), $displayTitle
226
		];
227
228
		// Is it a redirect? If so, where to?
229
		if ( $title->isRedirect() ) {
230
			$pageInfo['header-basic'][] = [
231
				$this->msg( 'pageinfo-redirectsto' ),
232
				Linker::link( $this->page->getRedirectTarget() ) .
0 ignored issues
show
Bug introduced by
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...
233
				$this->msg( 'word-separator' )->escaped() .
234
				$this->msg( 'parentheses' )->rawParams( Linker::link(
235
					$this->page->getRedirectTarget(),
236
					$this->msg( 'pageinfo-redirectsto-info' )->escaped(),
237
					[],
238
					[ 'action' => 'info' ]
239
				) )->escaped()
240
			];
241
		}
242
243
		// Default sort key
244
		$sortKey = $title->getCategorySortkey();
245
		if ( isset( $pageProperties['defaultsort'] ) ) {
246
			$sortKey = $pageProperties['defaultsort'];
247
		}
248
249
		$sortKey = htmlspecialchars( $sortKey );
250
		$pageInfo['header-basic'][] = [ $this->msg( 'pageinfo-default-sort' ), $sortKey ];
251
252
		// Page length (in bytes)
253
		$pageInfo['header-basic'][] = [
254
			$this->msg( 'pageinfo-length' ), $lang->formatNum( $title->getLength() )
255
		];
256
257
		// Page ID (number not localised, as it's a database ID)
258
		$pageInfo['header-basic'][] = [ $this->msg( 'pageinfo-article-id' ), $id ];
259
260
		// Language in which the page content is (supposed to be) written
261
		$pageLang = $title->getPageLanguage()->getCode();
262
263
		if ( $config->get( 'PageLanguageUseDB' )
264
			&& $this->getTitle()->userCan( 'pagelang', $this->getUser() )
265
		) {
266
			// Link to Special:PageLanguage with pre-filled page title if user has permissions
267
			$titleObj = SpecialPage::getTitleFor( 'PageLanguage', $title->getPrefixedText() );
268
			$langDisp = Linker::link(
269
				$titleObj,
270
				$this->msg( 'pageinfo-language' )->escaped()
271
			);
272
		} else {
273
			// Display just the message
274
			$langDisp = $this->msg( 'pageinfo-language' )->escaped();
275
		}
276
277
		$pageInfo['header-basic'][] = [ $langDisp,
278
			Language::fetchLanguageName( $pageLang, $lang->getCode() )
279
			. ' ' . $this->msg( 'parentheses', $pageLang )->escaped() ];
280
281
		// Content model of the page
282
		$pageInfo['header-basic'][] = [
283
			$this->msg( 'pageinfo-content-model' ),
284
			htmlspecialchars( ContentHandler::getLocalizedName( $title->getContentModel() ) )
285
		];
286
287
		// Search engine status
288
		$pOutput = new ParserOutput();
289
		if ( isset( $pageProperties['noindex'] ) ) {
290
			$pOutput->setIndexPolicy( 'noindex' );
291
		}
292
		if ( isset( $pageProperties['index'] ) ) {
293
			$pOutput->setIndexPolicy( 'index' );
294
		}
295
296
		// Use robot policy logic
297
		$policy = $this->page->getRobotPolicy( 'view', $pOutput );
0 ignored issues
show
Bug introduced by
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...
298
		$pageInfo['header-basic'][] = [
299
			// Messages: pageinfo-robot-index, pageinfo-robot-noindex
300
			$this->msg( 'pageinfo-robot-policy' ),
301
			$this->msg( "pageinfo-robot-${policy['index']}" )
302
		];
303
304
		$unwatchedPageThreshold = $config->get( 'UnwatchedPageThreshold' );
305
		if (
306
			$user->isAllowed( 'unwatchedpages' ) ||
307
			( $unwatchedPageThreshold !== false &&
308
				$pageCounts['watchers'] >= $unwatchedPageThreshold )
309
		) {
310
			// Number of page watchers
311
			$pageInfo['header-basic'][] = [
312
				$this->msg( 'pageinfo-watchers' ),
313
				$lang->formatNum( $pageCounts['watchers'] )
314
			];
315
			if (
316
				$config->get( 'ShowUpdatedMarker' ) &&
317
				isset( $pageCounts['visitingWatchers'] )
318
			) {
319
				$minToDisclose = $config->get( 'UnwatchedPageSecret' );
320
				if ( $pageCounts['visitingWatchers'] > $minToDisclose ||
321
					$user->isAllowed( 'unwatchedpages' ) ) {
322
					$pageInfo['header-basic'][] = [
323
						$this->msg( 'pageinfo-visiting-watchers' ),
324
						$lang->formatNum( $pageCounts['visitingWatchers'] )
325
					];
326
				} else {
327
					$pageInfo['header-basic'][] = [
328
						$this->msg( 'pageinfo-visiting-watchers' ),
329
						$this->msg( 'pageinfo-few-visiting-watchers' )
330
					];
331
				}
332
			}
333
		} elseif ( $unwatchedPageThreshold !== false ) {
334
			$pageInfo['header-basic'][] = [
335
				$this->msg( 'pageinfo-watchers' ),
336
				$this->msg( 'pageinfo-few-watchers' )->numParams( $unwatchedPageThreshold )
337
			];
338
		}
339
340
		// Redirects to this page
341
		$whatLinksHere = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedText() );
342
		$pageInfo['header-basic'][] = [
343
			Linker::link(
344
				$whatLinksHere,
345
				$this->msg( 'pageinfo-redirects-name' )->escaped(),
346
				[],
347
				[
348
					'hidelinks' => 1,
349
					'hidetrans' => 1,
350
					'hideimages' => $title->getNamespace() == NS_FILE
351
				]
352
			),
353
			$this->msg( 'pageinfo-redirects-value' )
354
				->numParams( count( $title->getRedirectsHere() ) )
355
		];
356
357
		// Is it counted as a content page?
358
		if ( $this->page->isCountable() ) {
0 ignored issues
show
Bug introduced by
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...
359
			$pageInfo['header-basic'][] = [
360
				$this->msg( 'pageinfo-contentpage' ),
361
				$this->msg( 'pageinfo-contentpage-yes' )
362
			];
363
		}
364
365
		// Subpages of this page, if subpages are enabled for the current NS
366
		if ( MWNamespace::hasSubpages( $title->getNamespace() ) ) {
367
			$prefixIndex = SpecialPage::getTitleFor(
368
				'Prefixindex', $title->getPrefixedText() . '/' );
369
			$pageInfo['header-basic'][] = [
370
				Linker::link( $prefixIndex, $this->msg( 'pageinfo-subpages-name' )->escaped() ),
371
				$this->msg( 'pageinfo-subpages-value' )
372
					->numParams(
373
						$pageCounts['subpages']['total'],
374
						$pageCounts['subpages']['redirects'],
375
						$pageCounts['subpages']['nonredirects'] )
376
			];
377
		}
378
379
		if ( $title->inNamespace( NS_CATEGORY ) ) {
380
			$category = Category::newFromTitle( $title );
381
382
			// $allCount is the total number of cat members,
383
			// not the count of how many members are normal pages.
384
			$allCount = (int)$category->getPageCount();
385
			$subcatCount = (int)$category->getSubcatCount();
386
			$fileCount = (int)$category->getFileCount();
387
			$pagesCount = $allCount - $subcatCount - $fileCount;
388
389
			$pageInfo['category-info'] = [
390
				[
391
					$this->msg( 'pageinfo-category-total' ),
392
					$lang->formatNum( $allCount )
393
				],
394
				[
395
					$this->msg( 'pageinfo-category-pages' ),
396
					$lang->formatNum( $pagesCount )
397
				],
398
				[
399
					$this->msg( 'pageinfo-category-subcats' ),
400
					$lang->formatNum( $subcatCount )
401
				],
402
				[
403
					$this->msg( 'pageinfo-category-files' ),
404
					$lang->formatNum( $fileCount )
405
				]
406
			];
407
		}
408
409
		// Page protection
410
		$pageInfo['header-restrictions'] = [];
411
412
		// Is this page affected by the cascading protection of something which includes it?
413
		if ( $title->isCascadeProtected() ) {
414
			$cascadingFrom = '';
415
			$sources = $title->getCascadeProtectionSources()[0];
416
417
			foreach ( $sources as $sourceTitle ) {
0 ignored issues
show
Bug introduced by
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...
418
				$cascadingFrom .= Html::rawElement(
419
					'li', [], Linker::linkKnown( $sourceTitle ) );
420
			}
421
422
			$cascadingFrom = Html::rawElement( 'ul', [], $cascadingFrom );
423
			$pageInfo['header-restrictions'][] = [
424
				$this->msg( 'pageinfo-protect-cascading-from' ),
425
				$cascadingFrom
426
			];
427
		}
428
429
		// Is out protection set to cascade to other pages?
430
		if ( $title->areRestrictionsCascading() ) {
431
			$pageInfo['header-restrictions'][] = [
432
				$this->msg( 'pageinfo-protect-cascading' ),
433
				$this->msg( 'pageinfo-protect-cascading-yes' )
434
			];
435
		}
436
437
		// Page protection
438
		foreach ( $title->getRestrictionTypes() as $restrictionType ) {
439
			$protectionLevel = implode( ', ', $title->getRestrictions( $restrictionType ) );
440
441
			if ( $protectionLevel == '' ) {
442
				// Allow all users
443
				$message = $this->msg( 'protect-default' )->escaped();
444
			} else {
445
				// Administrators only
446
				// Messages: protect-level-autoconfirmed, protect-level-sysop
447
				$message = $this->msg( "protect-level-$protectionLevel" );
448
				if ( $message->isDisabled() ) {
449
					// Require "$1" permission
450
					$message = $this->msg( "protect-fallback", $protectionLevel )->parse();
451
				} else {
452
					$message = $message->escaped();
453
				}
454
			}
455
			$expiry = $title->getRestrictionExpiry( $restrictionType );
456
			$formattedexpiry = $this->msg( 'parentheses',
457
				$this->getLanguage()->formatExpiry( $expiry ) )->escaped();
0 ignored issues
show
Bug introduced by
It seems like $expiry defined by $title->getRestrictionExpiry($restrictionType) on line 455 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...
458
			$message .= $this->msg( 'word-separator' )->escaped() . $formattedexpiry;
459
460
			// Messages: restriction-edit, restriction-move, restriction-create,
461
			// restriction-upload
462
			$pageInfo['header-restrictions'][] = [
463
				$this->msg( "restriction-$restrictionType" ), $message
464
			];
465
		}
466
467
		if ( !$this->page->exists() ) {
0 ignored issues
show
Bug introduced by
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...
468
			return $pageInfo;
469
		}
470
471
		// Edit history
472
		$pageInfo['header-edits'] = [];
473
474
		$firstRev = $this->page->getOldestRevision();
0 ignored issues
show
Bug introduced by
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...
475
		$lastRev = $this->page->getRevision();
0 ignored issues
show
Bug introduced by
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...
476
		$batch = new LinkBatch;
477
478 View Code Duplication
		if ( $firstRev ) {
479
			$firstRevUser = $firstRev->getUserText( Revision::FOR_THIS_USER );
480
			if ( $firstRevUser !== '' ) {
481
				$batch->add( NS_USER, $firstRevUser );
482
				$batch->add( NS_USER_TALK, $firstRevUser );
483
			}
484
		}
485
486 View Code Duplication
		if ( $lastRev ) {
487
			$lastRevUser = $lastRev->getUserText( Revision::FOR_THIS_USER );
488
			if ( $lastRevUser !== '' ) {
489
				$batch->add( NS_USER, $lastRevUser );
490
				$batch->add( NS_USER_TALK, $lastRevUser );
491
			}
492
		}
493
494
		$batch->execute();
495
496
		if ( $firstRev ) {
497
			// Page creator
498
			$pageInfo['header-edits'][] = [
499
				$this->msg( 'pageinfo-firstuser' ),
500
				Linker::revUserTools( $firstRev )
501
			];
502
503
			// Date of page creation
504
			$pageInfo['header-edits'][] = [
505
				$this->msg( 'pageinfo-firsttime' ),
506
				Linker::linkKnown(
507
					$title,
508
					htmlspecialchars( $lang->userTimeAndDate( $firstRev->getTimestamp(), $user ) ),
509
					[],
510
					[ 'oldid' => $firstRev->getId() ]
511
				)
512
			];
513
		}
514
515
		if ( $lastRev ) {
516
			// Latest editor
517
			$pageInfo['header-edits'][] = [
518
				$this->msg( 'pageinfo-lastuser' ),
519
				Linker::revUserTools( $lastRev )
520
			];
521
522
			// Date of latest edit
523
			$pageInfo['header-edits'][] = [
524
				$this->msg( 'pageinfo-lasttime' ),
525
				Linker::linkKnown(
526
					$title,
527
					htmlspecialchars(
528
						$lang->userTimeAndDate( $this->page->getTimestamp(), $user )
0 ignored issues
show
Bug introduced by
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...
529
					),
530
					[],
531
					[ 'oldid' => $this->page->getLatest() ]
0 ignored issues
show
Bug introduced by
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...
532
				)
533
			];
534
		}
535
536
		// Total number of edits
537
		$pageInfo['header-edits'][] = [
538
			$this->msg( 'pageinfo-edits' ), $lang->formatNum( $pageCounts['edits'] )
539
		];
540
541
		// Total number of distinct authors
542
		if ( $pageCounts['authors'] > 0 ) {
543
			$pageInfo['header-edits'][] = [
544
				$this->msg( 'pageinfo-authors' ), $lang->formatNum( $pageCounts['authors'] )
545
			];
546
		}
547
548
		// Recent number of edits (within past 30 days)
549
		$pageInfo['header-edits'][] = [
550
			$this->msg( 'pageinfo-recent-edits',
551
				$lang->formatDuration( $config->get( 'RCMaxAge' ) ) ),
552
			$lang->formatNum( $pageCounts['recent_edits'] )
553
		];
554
555
		// Recent number of distinct authors
556
		$pageInfo['header-edits'][] = [
557
			$this->msg( 'pageinfo-recent-authors' ),
558
			$lang->formatNum( $pageCounts['recent_authors'] )
559
		];
560
561
		// Array of MagicWord objects
562
		$magicWords = MagicWord::getDoubleUnderscoreArray();
563
564
		// Array of magic word IDs
565
		$wordIDs = $magicWords->names;
566
567
		// Array of IDs => localized magic words
568
		$localizedWords = $wgContLang->getMagicWords();
569
570
		$listItems = [];
571
		foreach ( $pageProperties as $property => $value ) {
572
			if ( in_array( $property, $wordIDs ) ) {
573
				$listItems[] = Html::element( 'li', [], $localizedWords[$property][1] );
574
			}
575
		}
576
577
		$localizedList = Html::rawElement( 'ul', [], implode( '', $listItems ) );
578
		$hiddenCategories = $this->page->getHiddenCategories();
0 ignored issues
show
Bug introduced by
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...
579
580
		if (
581
			count( $listItems ) > 0 ||
582
			count( $hiddenCategories ) > 0 ||
583
			$pageCounts['transclusion']['from'] > 0 ||
584
			$pageCounts['transclusion']['to'] > 0
585
		) {
586
			$options = [ 'LIMIT' => $config->get( 'PageInfoTransclusionLimit' ) ];
587
			$transcludedTemplates = $title->getTemplateLinksFrom( $options );
588
			if ( $config->get( 'MiserMode' ) ) {
589
				$transcludedTargets = [];
590
			} else {
591
				$transcludedTargets = $title->getTemplateLinksTo( $options );
592
			}
593
594
			// Page properties
595
			$pageInfo['header-properties'] = [];
596
597
			// Magic words
598
			if ( count( $listItems ) > 0 ) {
599
				$pageInfo['header-properties'][] = [
600
					$this->msg( 'pageinfo-magic-words' )->numParams( count( $listItems ) ),
601
					$localizedList
602
				];
603
			}
604
605
			// Hidden categories
606
			if ( count( $hiddenCategories ) > 0 ) {
607
				$pageInfo['header-properties'][] = [
608
					$this->msg( 'pageinfo-hidden-categories' )
609
						->numParams( count( $hiddenCategories ) ),
610
					Linker::formatHiddenCategories( $hiddenCategories )
611
				];
612
			}
613
614
			// Transcluded templates
615
			if ( $pageCounts['transclusion']['from'] > 0 ) {
616
				if ( $pageCounts['transclusion']['from'] > count( $transcludedTemplates ) ) {
617
					$more = $this->msg( 'morenotlisted' )->escaped();
618
				} else {
619
					$more = null;
620
				}
621
622
				$pageInfo['header-properties'][] = [
623
					$this->msg( 'pageinfo-templates' )
624
						->numParams( $pageCounts['transclusion']['from'] ),
625
					Linker::formatTemplates(
626
						$transcludedTemplates,
627
						false,
628
						false,
629
						$more )
630
				];
631
			}
632
633
			if ( !$config->get( 'MiserMode' ) && $pageCounts['transclusion']['to'] > 0 ) {
634
				if ( $pageCounts['transclusion']['to'] > count( $transcludedTargets ) ) {
635
					$more = Linker::link(
636
						$whatLinksHere,
637
						$this->msg( 'moredotdotdot' )->escaped(),
638
						[],
639
						[ 'hidelinks' => 1, 'hideredirs' => 1 ]
640
					);
641
				} else {
642
					$more = null;
643
				}
644
645
				$pageInfo['header-properties'][] = [
646
					$this->msg( 'pageinfo-transclusions' )
647
						->numParams( $pageCounts['transclusion']['to'] ),
648
					Linker::formatTemplates(
649
						$transcludedTargets,
650
						false,
651
						false,
652
						$more )
653
				];
654
			}
655
		}
656
657
		return $pageInfo;
658
	}
659
660
	/**
661
	 * Returns page counts that would be too "expensive" to retrieve by normal means.
662
	 *
663
	 * @param WikiPage|Article|Page $page
664
	 * @return array
665
	 */
666
	protected function pageCounts( Page $page ) {
667
		$fname = __METHOD__;
668
		$config = $this->context->getConfig();
669
670
		return ObjectCache::getMainWANInstance()->getWithSetCallback(
671
			self::getCacheKey( $page->getTitle(), $page->getLatest() ),
672
			86400 * 7,
673
			function ( $oldValue, &$ttl, &$setOpts ) use ( $page, $config, $fname ) {
674
				$title = $page->getTitle();
675
				$id = $title->getArticleID();
676
677
				$dbr = wfGetDB( DB_SLAVE );
678
				$dbrWatchlist = wfGetDB( DB_SLAVE, 'watchlist' );
679
680
				$setOpts += Database::getCacheSetOptions( $dbr, $dbrWatchlist );
681
682
				$watchedItemStore = MediaWikiServices::getInstance()->getWatchedItemStore();
683
684
				$result = [];
685
				$result['watchers'] = $watchedItemStore->countWatchers( $title );
686
687
				if ( $config->get( 'ShowUpdatedMarker' ) ) {
688
					$updated = wfTimestamp( TS_UNIX, $page->getTimestamp() );
689
					$result['visitingWatchers'] = $watchedItemStore->countVisitingWatchers(
690
						$title,
691
						$updated - $config->get( 'WatchersMaxAge' )
692
					);
693
				}
694
695
				// Total number of edits
696
				$edits = (int)$dbr->selectField(
697
					'revision',
698
					'COUNT(*)',
699
					[ 'rev_page' => $id ],
700
					$fname
701
				);
702
				$result['edits'] = $edits;
703
704
				// Total number of distinct authors
705
				if ( $config->get( 'MiserMode' ) ) {
706
					$result['authors'] = 0;
707
				} else {
708
					$result['authors'] = (int)$dbr->selectField(
709
						'revision',
710
						'COUNT(DISTINCT rev_user_text)',
711
						[ 'rev_page' => $id ],
712
						$fname
713
					);
714
				}
715
716
				// "Recent" threshold defined by RCMaxAge setting
717
				$threshold = $dbr->timestamp( time() - $config->get( 'RCMaxAge' ) );
718
719
				// Recent number of edits
720
				$edits = (int)$dbr->selectField(
721
					'revision',
722
					'COUNT(rev_page)',
723
					[
724
						'rev_page' => $id,
725
						"rev_timestamp >= " . $dbr->addQuotes( $threshold )
0 ignored issues
show
Security Bug introduced by
It seems like $threshold defined by $dbr->timestamp(time() -...onfig->get('RCMaxAge')) on line 717 can also be of type false; however, DatabaseBase::addQuotes() does only seem to accept string|object<Blob>, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
726
					],
727
					$fname
728
				);
729
				$result['recent_edits'] = $edits;
730
731
				// Recent number of distinct authors
732
				$result['recent_authors'] = (int)$dbr->selectField(
733
					'revision',
734
					'COUNT(DISTINCT rev_user_text)',
735
					[
736
						'rev_page' => $id,
737
						"rev_timestamp >= " . $dbr->addQuotes( $threshold )
0 ignored issues
show
Security Bug introduced by
It seems like $threshold defined by $dbr->timestamp(time() -...onfig->get('RCMaxAge')) on line 717 can also be of type false; however, DatabaseBase::addQuotes() does only seem to accept string|object<Blob>, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
738
					],
739
					$fname
740
				);
741
742
				// Subpages (if enabled)
743
				if ( MWNamespace::hasSubpages( $title->getNamespace() ) ) {
744
					$conds = [ 'page_namespace' => $title->getNamespace() ];
745
					$conds[] = 'page_title ' .
746
						$dbr->buildLike( $title->getDBkey() . '/', $dbr->anyString() );
747
748
					// Subpages of this page (redirects)
749
					$conds['page_is_redirect'] = 1;
750
					$result['subpages']['redirects'] = (int)$dbr->selectField(
751
						'page',
752
						'COUNT(page_id)',
753
						$conds,
754
						$fname
755
					);
756
757
					// Subpages of this page (non-redirects)
758
					$conds['page_is_redirect'] = 0;
759
					$result['subpages']['nonredirects'] = (int)$dbr->selectField(
760
						'page',
761
						'COUNT(page_id)',
762
						$conds,
763
						$fname
764
					);
765
766
					// Subpages of this page (total)
767
					$result['subpages']['total'] = $result['subpages']['redirects']
768
						+ $result['subpages']['nonredirects'];
769
				}
770
771
				// Counts for the number of transclusion links (to/from)
772
				if ( $config->get( 'MiserMode' ) ) {
773
					$result['transclusion']['to'] = 0;
774
				} else {
775
					$result['transclusion']['to'] = (int)$dbr->selectField(
776
						'templatelinks',
777
						'COUNT(tl_from)',
778
						[
779
							'tl_namespace' => $title->getNamespace(),
780
							'tl_title' => $title->getDBkey()
781
						],
782
						$fname
783
					);
784
				}
785
786
				$result['transclusion']['from'] = (int)$dbr->selectField(
787
					'templatelinks',
788
					'COUNT(*)',
789
					[ 'tl_from' => $title->getArticleID() ],
790
					$fname
791
				);
792
793
				return $result;
794
			}
795
		);
796
	}
797
798
	/**
799
	 * Returns the name that goes in the "<h1>" page title.
800
	 *
801
	 * @return string
802
	 */
803
	protected function getPageTitle() {
804
		return $this->msg( 'pageinfo-title', $this->getTitle()->getPrefixedText() )->text();
805
	}
806
807
	/**
808
	 * Get a list of contributors of $article
809
	 * @return string Html
810
	 */
811
	protected function getContributors() {
812
		$contributors = $this->page->getContributors();
0 ignored issues
show
Bug introduced by
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...
813
		$real_names = [];
814
		$user_names = [];
815
		$anon_ips = [];
816
817
		# Sift for real versus user names
818
		/** @var $user User */
819
		foreach ( $contributors as $user ) {
820
			$page = $user->isAnon()
821
				? SpecialPage::getTitleFor( 'Contributions', $user->getName() )
822
				: $user->getUserPage();
823
824
			$hiddenPrefs = $this->context->getConfig()->get( 'HiddenPrefs' );
825
			if ( $user->getId() == 0 ) {
826
				$anon_ips[] = Linker::link( $page, htmlspecialchars( $user->getName() ) );
827
			} elseif ( !in_array( 'realname', $hiddenPrefs ) && $user->getRealName() ) {
828
				$real_names[] = Linker::link( $page, htmlspecialchars( $user->getRealName() ) );
829
			} else {
830
				$user_names[] = Linker::link( $page, htmlspecialchars( $user->getName() ) );
831
			}
832
		}
833
834
		$lang = $this->getLanguage();
835
836
		$real = $lang->listToText( $real_names );
837
838
		# "ThisSite user(s) A, B and C"
839 View Code Duplication
		if ( count( $user_names ) ) {
840
			$user = $this->msg( 'siteusers' )
841
				->rawParams( $lang->listToText( $user_names ) )
842
				->params( count( $user_names ) )->escaped();
843
		} else {
844
			$user = false;
845
		}
846
847 View Code Duplication
		if ( count( $anon_ips ) ) {
848
			$anon = $this->msg( 'anonusers' )
849
				->rawParams( $lang->listToText( $anon_ips ) )
850
				->params( count( $anon_ips ) )->escaped();
851
		} else {
852
			$anon = false;
853
		}
854
855
		# This is the big list, all mooshed together. We sift for blank strings
856
		$fulllist = [];
857 View Code Duplication
		foreach ( [ $real, $user, $anon ] as $s ) {
858
			if ( $s !== '' ) {
859
				array_push( $fulllist, $s );
860
			}
861
		}
862
863
		$count = count( $fulllist );
864
865
		# "Based on work by ..."
866
		return $count
867
			? $this->msg( 'othercontribs' )->rawParams(
868
				$lang->listToText( $fulllist ) )->params( $count )->escaped()
869
			: '';
870
	}
871
872
	/**
873
	 * Returns the description that goes below the "<h1>" tag.
874
	 *
875
	 * @return string
876
	 */
877
	protected function getDescription() {
878
		return '';
879
	}
880
881
	/**
882
	 * @param Title $title
883
	 * @param int $revId
884
	 * @return string
885
	 */
886
	protected static function getCacheKey( Title $title, $revId ) {
887
		return wfMemcKey( 'infoaction', md5( $title->getPrefixedText() ), $revId, self::VERSION );
888
	}
889
}
890