Completed
Branch master (1655eb)
by
unknown
22:24
created

CategoryViewer::getPrettyPageNameHtml()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 8
rs 9.4285
1
<?php
2
/**
3
 * List and paging of category members.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 */
22
use MediaWiki\MediaWikiServices;
23
24
class CategoryViewer extends ContextSource {
25
	/** @var int */
26
	public $limit;
27
28
	/** @var array */
29
	public $from;
30
31
	/** @var array */
32
	public $until;
33
34
	/** @var string[] */
35
	public $articles;
36
37
	/** @var array */
38
	public $articles_start_char;
39
40
	/** @var array */
41
	public $children;
42
43
	/** @var array */
44
	public $children_start_char;
45
46
	/** @var bool */
47
	public $showGallery;
48
49
	/** @var array */
50
	public $imgsNoGallery_start_char;
51
52
	/** @var array */
53
	public $imgsNoGallery;
54
55
	/** @var array */
56
	public $nextPage;
57
58
	/** @var array */
59
	protected $prevPage;
60
61
	/** @var array */
62
	public $flip;
63
64
	/** @var Title */
65
	public $title;
66
67
	/** @var Collation */
68
	public $collation;
69
70
	/** @var ImageGallery */
71
	public $gallery;
72
73
	/** @var Category Category object for this page. */
74
	private $cat;
75
76
	/** @var array The original query array, to be used in generating paging links. */
77
	private $query;
78
79
	/**
80
	 * @since 1.19 $context is a second, required parameter
81
	 * @param Title $title
82
	 * @param IContextSource $context
83
	 * @param array $from An array with keys page, subcat,
84
	 *        and file for offset of results of each section (since 1.17)
85
	 * @param array $until An array with 3 keys for until of each section (since 1.17)
86
	 * @param array $query
87
	 */
88
	function __construct( $title, IContextSource $context, $from = [],
89
		$until = [], $query = []
90
	) {
91
		$this->title = $title;
92
		$this->setContext( $context );
93
		$this->getOutput()->addModuleStyles( [
94
			'mediawiki.action.view.categoryPage.styles'
95
		] );
96
		$this->from = $from;
97
		$this->until = $until;
98
		$this->limit = $context->getConfig()->get( 'CategoryPagingLimit' );
99
		$this->cat = Category::newFromTitle( $title );
0 ignored issues
show
Documentation Bug introduced by
It seems like \Category::newFromTitle($title) can also be of type boolean. However, the property $cat is declared as type object<Category>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
100
		$this->query = $query;
101
		$this->collation = Collation::singleton();
102
		unset( $this->query['title'] );
103
	}
104
105
	/**
106
	 * Format the category data list.
107
	 *
108
	 * @return string HTML output
109
	 */
110
	public function getHTML() {
111
112
		$this->showGallery = $this->getConfig()->get( 'CategoryMagicGallery' )
113
			&& !$this->getOutput()->mNoGallery;
114
115
		$this->clearCategoryState();
116
		$this->doCategoryQuery();
117
		$this->finaliseCategoryState();
118
119
		$r = $this->getSubcategorySection() .
120
			$this->getPagesSection() .
121
			$this->getImageSection();
122
123
		if ( $r == '' ) {
124
			// If there is no category content to display, only
125
			// show the top part of the navigation links.
126
			// @todo FIXME: Cannot be completely suppressed because it
127
			//        is unknown if 'until' or 'from' makes this
128
			//        give 0 results.
129
			$r = $r . $this->getCategoryTop();
130
		} else {
131
			$r = $this->getCategoryTop() .
132
				$r .
133
				$this->getCategoryBottom();
134
		}
135
136
		// Give a proper message if category is empty
137
		if ( $r == '' ) {
138
			$r = $this->msg( 'category-empty' )->parseAsBlock();
139
		}
140
141
		$lang = $this->getLanguage();
142
		$attribs = [
143
			'class' => 'mw-category-generated',
144
			'lang' => $lang->getHtmlCode(),
145
			'dir' => $lang->getDir()
146
		];
147
		# put a div around the headings which are in the user language
148
		$r = Html::openElement( 'div', $attribs ) . $r . '</div>';
149
150
		return $r;
151
	}
152
153
	function clearCategoryState() {
154
		$this->articles = [];
155
		$this->articles_start_char = [];
156
		$this->children = [];
157
		$this->children_start_char = [];
158
		if ( $this->showGallery ) {
159
			// Note that null for mode is taken to mean use default.
160
			$mode = $this->getRequest()->getVal( 'gallerymode', null );
161
			try {
162
				$this->gallery = ImageGalleryBase::factory( $mode, $this->getContext() );
163
			} catch ( Exception $e ) {
164
				// User specified something invalid, fallback to default.
165
				$this->gallery = ImageGalleryBase::factory( false, $this->getContext() );
166
			}
167
168
			$this->gallery->setHideBadImages();
169
		} else {
170
			$this->imgsNoGallery = [];
171
			$this->imgsNoGallery_start_char = [];
172
		}
173
	}
174
175
	/**
176
	 * Add a subcategory to the internal lists, using a Category object
177
	 * @param Category $cat
178
	 * @param string $sortkey
179
	 * @param int $pageLength
180
	 */
181
	function addSubcategoryObject( Category $cat, $sortkey, $pageLength ) {
182
		// Subcategory; strip the 'Category' namespace from the link text.
183
		$title = $cat->getTitle();
184
185
		$this->children[] = $this->generateLink(
186
			'subcat',
187
			$title,
0 ignored issues
show
Bug introduced by
It seems like $title defined by $cat->getTitle() on line 183 can also be of type false or null; however, CategoryViewer::generateLink() does only seem to accept object<Title>, 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...
188
			$title->isRedirect(),
189
			htmlspecialchars( $title->getText() )
190
		);
191
192
		$this->children_start_char[] =
193
			$this->getSubcategorySortChar( $cat->getTitle(), $sortkey );
0 ignored issues
show
Bug introduced by
It seems like $cat->getTitle() targeting Category::getTitle() can also be of type false or null; however, CategoryViewer::getSubcategorySortChar() does only seem to accept object<Title>, 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...
194
	}
195
196
	function generateLink( $type, Title $title, $isRedirect, $html = null ) {
197
		$link = null;
198
		Hooks::run( 'CategoryViewer::generateLink', [ $type, $title, $html, &$link ] );
199
		if ( $link === null ) {
200
			$link = Linker::link( $title, $html );
201
		}
202
		if ( $isRedirect ) {
203
			$link = '<span class="redirect-in-category">' . $link . '</span>';
204
		}
205
206
		return $link;
207
	}
208
209
	/**
210
	 * Get the character to be used for sorting subcategories.
211
	 * If there's a link from Category:A to Category:B, the sortkey of the resulting
212
	 * entry in the categorylinks table is Category:A, not A, which it SHOULD be.
213
	 * Workaround: If sortkey == "Category:".$title, than use $title for sorting,
214
	 * else use sortkey...
215
	 *
216
	 * @param Title $title
217
	 * @param string $sortkey The human-readable sortkey (before transforming to icu or whatever).
218
	 * @return string
219
	 */
220
	function getSubcategorySortChar( $title, $sortkey ) {
221
		global $wgContLang;
222
223
		if ( $title->getPrefixedText() == $sortkey ) {
224
			$word = $title->getDBkey();
225
		} else {
226
			$word = $sortkey;
227
		}
228
229
		$firstChar = $this->collation->getFirstLetter( $word );
230
231
		return $wgContLang->convert( $firstChar );
232
	}
233
234
	/**
235
	 * Add a page in the image namespace
236
	 * @param Title $title
237
	 * @param string $sortkey
238
	 * @param int $pageLength
239
	 * @param bool $isRedirect
240
	 */
241
	function addImage( Title $title, $sortkey, $pageLength, $isRedirect = false ) {
242
		global $wgContLang;
243
		if ( $this->showGallery ) {
244
			$flip = $this->flip['file'];
245
			if ( $flip ) {
246
				$this->gallery->insert( $title );
247
			} else {
248
				$this->gallery->add( $title );
249
			}
250
		} else {
251
			$this->imgsNoGallery[] = $this->generateLink( 'image', $title, $isRedirect );
252
253
			$this->imgsNoGallery_start_char[] = $wgContLang->convert(
254
				$this->collation->getFirstLetter( $sortkey ) );
255
		}
256
	}
257
258
	/**
259
	 * Add a miscellaneous page
260
	 * @param Title $title
261
	 * @param string $sortkey
262
	 * @param int $pageLength
263
	 * @param bool $isRedirect
264
	 */
265
	function addPage( $title, $sortkey, $pageLength, $isRedirect = false ) {
266
		global $wgContLang;
267
268
		$this->articles[] = $this->generateLink( 'page', $title, $isRedirect );
269
270
		$this->articles_start_char[] = $wgContLang->convert(
271
			$this->collation->getFirstLetter( $sortkey ) );
272
	}
273
274
	function finaliseCategoryState() {
275
		if ( $this->flip['subcat'] ) {
276
			$this->children = array_reverse( $this->children );
277
			$this->children_start_char = array_reverse( $this->children_start_char );
278
		}
279
		if ( $this->flip['page'] ) {
280
			$this->articles = array_reverse( $this->articles );
281
			$this->articles_start_char = array_reverse( $this->articles_start_char );
282
		}
283
		if ( !$this->showGallery && $this->flip['file'] ) {
284
			$this->imgsNoGallery = array_reverse( $this->imgsNoGallery );
285
			$this->imgsNoGallery_start_char = array_reverse( $this->imgsNoGallery_start_char );
286
		}
287
	}
288
289
	function doCategoryQuery() {
290
		$dbr = wfGetDB( DB_REPLICA, 'category' );
291
292
		$this->nextPage = [
293
			'page' => null,
294
			'subcat' => null,
295
			'file' => null,
296
		];
297
		$this->prevPage = [
298
			'page' => null,
299
			'subcat' => null,
300
			'file' => null,
301
		];
302
303
		$this->flip = [ 'page' => false, 'subcat' => false, 'file' => false ];
304
305
		foreach ( [ 'page', 'subcat', 'file' ] as $type ) {
306
			# Get the sortkeys for start/end, if applicable.  Note that if
307
			# the collation in the database differs from the one
308
			# set in $wgCategoryCollation, pagination might go totally haywire.
309
			$extraConds = [ 'cl_type' => $type ];
310
			if ( isset( $this->from[$type] ) && $this->from[$type] !== null ) {
311
				$extraConds[] = 'cl_sortkey >= '
312
					. $dbr->addQuotes( $this->collation->getSortKey( $this->from[$type] ) );
313
			} elseif ( isset( $this->until[$type] ) && $this->until[$type] !== null ) {
314
				$extraConds[] = 'cl_sortkey < '
315
					. $dbr->addQuotes( $this->collation->getSortKey( $this->until[$type] ) );
316
				$this->flip[$type] = true;
317
			}
318
319
			$res = $dbr->select(
320
				[ 'page', 'categorylinks', 'category' ],
321
				array_merge(
322
					LinkCache::getSelectFields(),
323
					[
324
						'page_namespace',
325
						'page_title',
326
						'cl_sortkey',
327
						'cat_id',
328
						'cat_title',
329
						'cat_subcats',
330
						'cat_pages',
331
						'cat_files',
332
						'cl_sortkey_prefix',
333
						'cl_collation'
334
					]
335
				),
336
				array_merge( [ 'cl_to' => $this->title->getDBkey() ], $extraConds ),
337
				__METHOD__,
338
				[
339
					'USE INDEX' => [ 'categorylinks' => 'cl_sortkey' ],
340
					'LIMIT' => $this->limit + 1,
341
					'ORDER BY' => $this->flip[$type] ? 'cl_sortkey DESC' : 'cl_sortkey',
342
				],
343
				[
344
					'categorylinks' => [ 'INNER JOIN', 'cl_from = page_id' ],
345
					'category' => [ 'LEFT JOIN', [
346
						'cat_title = page_title',
347
						'page_namespace' => NS_CATEGORY
348
					] ]
349
				]
350
			);
351
352
			Hooks::run( 'CategoryViewer::doCategoryQuery', [ $type, $res ] );
353
			$linkCache = MediaWikiServices::getInstance()->getLinkCache();
354
355
			$count = 0;
356
			foreach ( $res as $row ) {
357
				$title = Title::newFromRow( $row );
358
				$linkCache->addGoodLinkObjFromRow( $title, $row );
359
360
				if ( $row->cl_collation === '' ) {
361
					// Hack to make sure that while updating from 1.16 schema
362
					// and db is inconsistent, that the sky doesn't fall.
363
					// See r83544. Could perhaps be removed in a couple decades...
364
					$humanSortkey = $row->cl_sortkey;
365
				} else {
366
					$humanSortkey = $title->getCategorySortkey( $row->cl_sortkey_prefix );
367
				}
368
369
				if ( ++$count > $this->limit ) {
370
					# We've reached the one extra which shows that there
371
					# are additional pages to be had. Stop here...
372
					$this->nextPage[$type] = $humanSortkey;
373
					break;
374
				}
375
				if ( $count == $this->limit ) {
376
					$this->prevPage[$type] = $humanSortkey;
377
				}
378
379
				if ( $title->getNamespace() == NS_CATEGORY ) {
380
					$cat = Category::newFromRow( $row, $title );
381
					$this->addSubcategoryObject( $cat, $humanSortkey, $row->page_len );
382
				} elseif ( $title->getNamespace() == NS_FILE ) {
383
					$this->addImage( $title, $humanSortkey, $row->page_len, $row->page_is_redirect );
384
				} else {
385
					$this->addPage( $title, $humanSortkey, $row->page_len, $row->page_is_redirect );
386
				}
387
			}
388
		}
389
	}
390
391
	/**
392
	 * @return string
393
	 */
394
	function getCategoryTop() {
395
		$r = $this->getCategoryBottom();
396
		return $r === ''
397
			? $r
398
			: "<br style=\"clear:both;\"/>\n" . $r;
399
	}
400
401
	/**
402
	 * @return string
403
	 */
404
	function getSubcategorySection() {
405
		# Don't show subcategories section if there are none.
406
		$r = '';
407
		$rescnt = count( $this->children );
408
		$dbcnt = $this->cat->getSubcatCount();
409
		// This function should be called even if the result isn't used, it has side-effects
410
		$countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'subcat' );
411
412 View Code Duplication
		if ( $rescnt > 0 ) {
413
			# Showing subcategories
414
			$r .= "<div id=\"mw-subcategories\">\n";
415
			$r .= '<h2>' . $this->msg( 'subcategories' )->parse() . "</h2>\n";
416
			$r .= $countmsg;
417
			$r .= $this->getSectionPagingLinks( 'subcat' );
418
			$r .= $this->formatList( $this->children, $this->children_start_char );
419
			$r .= $this->getSectionPagingLinks( 'subcat' );
420
			$r .= "\n</div>";
421
		}
422
		return $r;
423
	}
424
425
	/**
426
	 * @return string
427
	 */
428
	function getPagesSection() {
429
		$ti = wfEscapeWikiText( $this->title->getText() );
430
		# Don't show articles section if there are none.
431
		$r = '';
432
433
		# @todo FIXME: Here and in the other two sections: we don't need to bother
434
		# with this rigmarole if the entire category contents fit on one page
435
		# and have already been retrieved.  We can just use $rescnt in that
436
		# case and save a query and some logic.
437
		$dbcnt = $this->cat->getPageCount() - $this->cat->getSubcatCount()
438
			- $this->cat->getFileCount();
439
		$rescnt = count( $this->articles );
440
		// This function should be called even if the result isn't used, it has side-effects
441
		$countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'article' );
442
443 View Code Duplication
		if ( $rescnt > 0 ) {
444
			$r = "<div id=\"mw-pages\">\n";
445
			$r .= '<h2>' . $this->msg( 'category_header', $ti )->parse() . "</h2>\n";
446
			$r .= $countmsg;
447
			$r .= $this->getSectionPagingLinks( 'page' );
448
			$r .= $this->formatList( $this->articles, $this->articles_start_char );
449
			$r .= $this->getSectionPagingLinks( 'page' );
450
			$r .= "\n</div>";
451
		}
452
		return $r;
453
	}
454
455
	/**
456
	 * @return string
457
	 */
458
	function getImageSection() {
459
		$r = '';
460
		$rescnt = $this->showGallery ? $this->gallery->count() : count( $this->imgsNoGallery );
461
		$dbcnt = $this->cat->getFileCount();
462
		// This function should be called even if the result isn't used, it has side-effects
463
		$countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'file' );
464
465
		if ( $rescnt > 0 ) {
466
			$r .= "<div id=\"mw-category-media\">\n";
467
			$r .= '<h2>' .
468
				$this->msg(
469
					'category-media-header',
470
					wfEscapeWikiText( $this->title->getText() )
471
				)->text() .
472
				"</h2>\n";
473
			$r .= $countmsg;
474
			$r .= $this->getSectionPagingLinks( 'file' );
475
			if ( $this->showGallery ) {
476
				$r .= $this->gallery->toHTML();
477
			} else {
478
				$r .= $this->formatList( $this->imgsNoGallery, $this->imgsNoGallery_start_char );
479
			}
480
			$r .= $this->getSectionPagingLinks( 'file' );
481
			$r .= "\n</div>";
482
		}
483
		return $r;
484
	}
485
486
	/**
487
	 * Get the paging links for a section (subcats/pages/files), to go at the top and bottom
488
	 * of the output.
489
	 *
490
	 * @param string $type 'page', 'subcat', or 'file'
491
	 * @return string HTML output, possibly empty if there are no other pages
492
	 */
493
	private function getSectionPagingLinks( $type ) {
494
		if ( isset( $this->until[$type] ) && $this->until[$type] !== null ) {
495
			// The new value for the until parameter should be pointing to the first
496
			// result displayed on the page which is the second last result retrieved
497
			// from the database.The next link should have a from parameter pointing
498
			// to the until parameter of the current page.
499
			if ( $this->nextPage[$type] !== null ) {
500
				return $this->pagingLinks( $this->prevPage[$type], $this->until[$type], $type );
501
			} else {
502
				// If the nextPage variable is null, it means that we have reached the first page
503
				// and therefore the previous link should be disabled.
504
				return $this->pagingLinks( null, $this->until[$type], $type );
505
			}
506
		} elseif ( $this->nextPage[$type] !== null
507
			|| ( isset( $this->from[$type] ) && $this->from[$type] !== null )
508
		) {
509
			return $this->pagingLinks( $this->from[$type], $this->nextPage[$type], $type );
510
		} else {
511
			return '';
512
		}
513
	}
514
515
	/**
516
	 * @return string
517
	 */
518
	function getCategoryBottom() {
519
		return '';
520
	}
521
522
	/**
523
	 * Format a list of articles chunked by letter, either as a
524
	 * bullet list or a columnar format, depending on the length.
525
	 *
526
	 * @param array $articles
527
	 * @param array $articles_start_char
528
	 * @param int $cutoff
529
	 * @return string
530
	 * @private
531
	 */
532
	function formatList( $articles, $articles_start_char, $cutoff = 6 ) {
533
		$list = '';
534
		if ( count( $articles ) > $cutoff ) {
535
			$list = self::columnList( $articles, $articles_start_char );
536
		} elseif ( count( $articles ) > 0 ) {
537
			// for short lists of articles in categories.
538
			$list = self::shortList( $articles, $articles_start_char );
539
		}
540
541
		$pageLang = $this->title->getPageLanguage();
542
		$attribs = [ 'lang' => $pageLang->getHtmlCode(), 'dir' => $pageLang->getDir(),
543
			'class' => 'mw-content-' . $pageLang->getDir() ];
544
		$list = Html::rawElement( 'div', $attribs, $list );
545
546
		return $list;
547
	}
548
549
	/**
550
	 * Format a list of articles chunked by letter in a three-column list, ordered
551
	 * vertically. This is used for categories with a significant number of pages.
552
	 *
553
	 * TODO: Take the headers into account when creating columns, so they're
554
	 * more visually equal.
555
	 *
556
	 * TODO: shortList and columnList are similar, need merging
557
	 *
558
	 * @param string[] $articles HTML links to each article
559
	 * @param string[] $articles_start_char The header characters for each article
560
	 * @return string HTML to output
561
	 * @private
562
	 */
563
	static function columnList( $articles, $articles_start_char ) {
564
		$columns = array_combine( $articles, $articles_start_char );
565
566
		$ret = Html::openElement( 'div', [ 'class' => 'mw-category' ] );
567
568
		$colContents = [];
569
570
		# Kind of like array_flip() here, but we keep duplicates in an
571
		# array instead of dropping them.
572
		foreach ( $columns as $article => $char ) {
573
			if ( !isset( $colContents[$char] ) ) {
574
				$colContents[$char] = [];
575
			}
576
			$colContents[$char][] = $article;
577
		}
578
579
		foreach ( $colContents as $char => $articles ) {
580
			# Change space to non-breaking space to keep headers aligned
581
			$h3char = $char === ' ' ? '&#160;' : htmlspecialchars( $char );
582
583
			$ret .= '<div class="mw-category-group"><h3>' . $h3char;
584
			$ret .= "</h3>\n";
585
586
			$ret .= '<ul><li>';
587
			$ret .= implode( "</li>\n<li>", $articles );
588
			$ret .= '</li></ul></div>';
589
590
		}
591
592
		$ret .= Html::closeElement( 'div' );
593
		return $ret;
594
	}
595
596
	/**
597
	 * Format a list of articles chunked by letter in a bullet list. This is used
598
	 * for categories with a small number of pages (when columns aren't needed).
599
	 * @param string[] $articles HTML links to each article
600
	 * @param string[] $articles_start_char The header characters for each article
601
	 * @return string HTML to output
602
	 * @private
603
	 */
604
	static function shortList( $articles, $articles_start_char ) {
605
		$r = '<h3>' . htmlspecialchars( $articles_start_char[0] ) . "</h3>\n";
606
		$r .= '<ul><li>' . $articles[0] . '</li>';
607
		$articleCount = count( $articles );
608
		for ( $index = 1; $index < $articleCount; $index++ ) {
609
			if ( $articles_start_char[$index] != $articles_start_char[$index - 1] ) {
610
				$r .= "</ul><h3>" . htmlspecialchars( $articles_start_char[$index] ) . "</h3>\n<ul>";
611
			}
612
613
			$r .= "<li>{$articles[$index]}</li>";
614
		}
615
		$r .= '</ul>';
616
		return $r;
617
	}
618
619
	/**
620
	 * Create paging links, as a helper method to getSectionPagingLinks().
621
	 *
622
	 * @param string $first The 'until' parameter for the generated URL
623
	 * @param string $last The 'from' parameter for the generated URL
624
	 * @param string $type A prefix for parameters, 'page' or 'subcat' or
625
	 *     'file'
626
	 * @return string HTML
627
	 */
628
	private function pagingLinks( $first, $last, $type = '' ) {
629
		$prevLink = $this->msg( 'prev-page' )->text();
630
631 View Code Duplication
		if ( $first != '' ) {
632
			$prevQuery = $this->query;
633
			$prevQuery["{$type}until"] = $first;
634
			unset( $prevQuery["{$type}from"] );
635
			$prevLink = Linker::linkKnown(
636
				$this->addFragmentToTitle( $this->title, $type ),
637
				$prevLink,
638
				[],
639
				$prevQuery
640
			);
641
		}
642
643
		$nextLink = $this->msg( 'next-page' )->text();
644
645 View Code Duplication
		if ( $last != '' ) {
646
			$lastQuery = $this->query;
647
			$lastQuery["{$type}from"] = $last;
648
			unset( $lastQuery["{$type}until"] );
649
			$nextLink = Linker::linkKnown(
650
				$this->addFragmentToTitle( $this->title, $type ),
651
				$nextLink,
652
				[],
653
				$lastQuery
654
			);
655
		}
656
657
		return $this->msg( 'categoryviewer-pagedlinks' )->rawParams( $prevLink, $nextLink )->escaped();
658
	}
659
660
	/**
661
	 * Takes a title, and adds the fragment identifier that
662
	 * corresponds to the correct segment of the category.
663
	 *
664
	 * @param Title $title The title (usually $this->title)
665
	 * @param string $section Which section
666
	 * @throws MWException
667
	 * @return Title
668
	 */
669
	private function addFragmentToTitle( $title, $section ) {
670
		switch ( $section ) {
671
			case 'page':
672
				$fragment = 'mw-pages';
673
				break;
674
			case 'subcat':
675
				$fragment = 'mw-subcategories';
676
				break;
677
			case 'file':
678
				$fragment = 'mw-category-media';
679
				break;
680
			default:
681
				throw new MWException( __METHOD__ .
682
					" Invalid section $section." );
683
		}
684
685
		return Title::makeTitle( $title->getNamespace(),
686
			$title->getDBkey(), $fragment );
687
	}
688
689
	/**
690
	 * What to do if the category table conflicts with the number of results
691
	 * returned?  This function says what. Each type is considered independently
692
	 * of the other types.
693
	 *
694
	 * @param int $rescnt The number of items returned by our database query.
695
	 * @param int $dbcnt The number of items according to the category table.
696
	 * @param string $type 'subcat', 'article', or 'file'
697
	 * @return string A message giving the number of items, to output to HTML.
698
	 */
699
	private function getCountMessage( $rescnt, $dbcnt, $type ) {
700
		// There are three cases:
701
		//   1) The category table figure seems sane.  It might be wrong, but
702
		//      we can't do anything about it if we don't recalculate it on ev-
703
		//      ery category view.
704
		//   2) The category table figure isn't sane, like it's smaller than the
705
		//      number of actual results, *but* the number of results is less
706
		//      than $this->limit and there's no offset.  In this case we still
707
		//      know the right figure.
708
		//   3) We have no idea.
709
710
		// Check if there's a "from" or "until" for anything
711
712
		// This is a little ugly, but we seem to use different names
713
		// for the paging types then for the messages.
714
		if ( $type === 'article' ) {
715
			$pagingType = 'page';
716
		} else {
717
			$pagingType = $type;
718
		}
719
720
		$fromOrUntil = false;
721
		if ( ( isset( $this->from[$pagingType] ) && $this->from[$pagingType] !== null ) ||
722
			( isset( $this->until[$pagingType] ) && $this->until[$pagingType] !== null )
723
		) {
724
			$fromOrUntil = true;
725
		}
726
727
		if ( $dbcnt == $rescnt ||
728
			( ( $rescnt == $this->limit || $fromOrUntil ) && $dbcnt > $rescnt )
729
		) {
730
			// Case 1: seems sane.
731
			$totalcnt = $dbcnt;
732
		} elseif ( $rescnt < $this->limit && !$fromOrUntil ) {
733
			// Case 2: not sane, but salvageable.  Use the number of results.
734
			// Since there are fewer than 200, we can also take this opportunity
735
			// to refresh the incorrect category table entry -- which should be
736
			// quick due to the small number of entries.
737
			$totalcnt = $rescnt;
738
			$category = $this->cat;
739
			DeferredUpdates::addCallableUpdate( function () use ( $category ) {
740
				$category->refreshCounts();
741
			} );
742
		} else {
743
			// Case 3: hopeless.  Don't give a total count at all.
744
			// Messages: category-subcat-count-limited, category-article-count-limited,
745
			// category-file-count-limited
746
			return $this->msg( "category-$type-count-limited" )->numParams( $rescnt )->parseAsBlock();
747
		}
748
		// Messages: category-subcat-count, category-article-count, category-file-count
749
		return $this->msg( "category-$type-count" )->numParams( $rescnt, $totalcnt )->parseAsBlock();
750
	}
751
}
752