]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/CategoryPage.php
MediaWiki 1.17.1
[autoinstallsdev/mediawiki.git] / includes / CategoryPage.php
1 <?php
2 /**
3  * Special handling for category description pages.
4  * Modelled after ImagePage.php.
5  *
6  * @file
7  */
8
9 if ( !defined( 'MEDIAWIKI' ) )
10         die( 1 );
11
12 /**
13  * Special handling for category description pages, showing pages,
14  * subcategories and file that belong to the category
15  */
16 class CategoryPage extends Article {
17         # Subclasses can change this to override the viewer class.
18         protected $mCategoryViewerClass = 'CategoryViewer';
19
20         function view() {
21                 global $wgRequest, $wgUser;
22
23                 $diff = $wgRequest->getVal( 'diff' );
24                 $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) );
25
26                 if ( isset( $diff ) && $diffOnly ) {
27                         return parent::view();
28                 }
29
30                 if ( !wfRunHooks( 'CategoryPageView', array( &$this ) ) ) {
31                         return;
32                 }
33
34                 if ( NS_CATEGORY == $this->mTitle->getNamespace() ) {
35                         $this->openShowCategory();
36                 }
37
38                 parent::view();
39
40                 if ( NS_CATEGORY == $this->mTitle->getNamespace() ) {
41                         $this->closeShowCategory();
42                 }
43         }
44
45         /**
46          * Don't return a 404 for categories in use.
47          * In use defined as: either the actual page exists
48          * or the category currently has members.
49          */
50         function hasViewableContent() {
51                 if ( parent::hasViewableContent() ) {
52                         return true;
53                 } else {
54                         $cat = Category::newFromTitle( $this->mTitle );
55                         // If any of these are not 0, then has members
56                         if ( $cat->getPageCount()
57                                 || $cat->getSubcatCount()
58                                 || $cat->getFileCount()
59                         ) {
60                                 return true;
61                         }
62                 }
63                 return false;
64         }
65
66         function openShowCategory() {
67                 # For overloading
68         }
69
70         function closeShowCategory() {
71                 global $wgOut, $wgRequest;
72
73                 $from = $until = array();
74                 foreach ( array( 'page', 'subcat', 'file' ) as $type ) {
75                         $from[$type] = $wgRequest->getVal( "{$type}from" );
76                         $until[$type] = $wgRequest->getVal( "{$type}until" );
77                 }
78
79                 $viewer = new $this->mCategoryViewerClass( $this->mTitle, $from, $until, $wgRequest->getValues() );
80                 $wgOut->addHTML( $viewer->getHTML() );
81         }
82 }
83
84 class CategoryViewer {
85         var $title, $limit, $from, $until,
86                 $articles, $articles_start_char,
87                 $children, $children_start_char,
88                 $showGallery, $gallery,
89                 $imgsNoGalley, $imgsNoGallery_start_char,
90                 $skin, $collation;
91         # Category object for this page
92         private $cat;
93         # The original query array, to be used in generating paging links.
94         private $query;
95
96         function __construct( $title, $from = '', $until = '', $query = array() ) {
97                 global $wgCategoryPagingLimit;
98                 $this->title = $title;
99                 $this->from = $from;
100                 $this->until = $until;
101                 $this->limit = $wgCategoryPagingLimit;
102                 $this->cat = Category::newFromTitle( $title );
103                 $this->query = $query;
104                 $this->collation = Collation::singleton();
105                 unset( $this->query['title'] );
106         }
107
108         /**
109          * Format the category data list.
110          *
111          * @return string HTML output
112          */
113         public function getHTML() {
114                 global $wgOut, $wgCategoryMagicGallery, $wgContLang;
115                 wfProfileIn( __METHOD__ );
116
117                 $this->showGallery = $wgCategoryMagicGallery && !$wgOut->mNoGallery;
118
119                 $this->clearCategoryState();
120                 $this->doCategoryQuery();
121                 $this->finaliseCategoryState();
122
123                 $r = $this->getSubcategorySection() .
124                         $this->getPagesSection() .
125                         $this->getImageSection();
126
127                 if ( $r == '' ) {
128                         // If there is no category content to display, only
129                         // show the top part of the navigation links.
130                         // FIXME: cannot be completely suppressed because it
131                         //        is unknown if 'until' or 'from' makes this
132                         //        give 0 results.
133                         $r = $r . $this->getCategoryTop();
134                 } else {
135                         $r = $this->getCategoryTop() .
136                                 $r .
137                                 $this->getCategoryBottom();
138                 }
139
140                 // Give a proper message if category is empty
141                 if ( $r == '' ) {
142                         $r = wfMsgExt( 'category-empty', array( 'parse' ) );
143                 }
144
145                 wfProfileOut( __METHOD__ );
146                 return $wgContLang->convert( $r );
147         }
148
149         function clearCategoryState() {
150                 $this->articles = array();
151                 $this->articles_start_char = array();
152                 $this->children = array();
153                 $this->children_start_char = array();
154                 if ( $this->showGallery ) {
155                         $this->gallery = new ImageGallery();
156                         $this->gallery->setHideBadImages();
157                 } else {
158                         $this->imgsNoGallery = array();
159                         $this->imgsNoGallery_start_char = array();
160                 }
161         }
162
163         function getSkin() {
164                 if ( !$this->skin ) {
165                         global $wgUser;
166                         $this->skin = $wgUser->getSkin();
167                 }
168                 return $this->skin;
169         }
170
171         /**
172          * Add a subcategory to the internal lists, using a Category object
173          */
174         function addSubcategoryObject( Category $cat, $sortkey, $pageLength ) {
175                 // Subcategory; strip the 'Category' namespace from the link text.
176                 $title = $cat->getTitle();
177
178                 $link = $this->getSkin()->link( $title, $title->getText() );
179                 if ( $title->isRedirect() ) {
180                         // This didn't used to add redirect-in-category, but might
181                         // as well be consistent with the rest of the sections
182                         // on a category page.
183                         $link = '<span class="redirect-in-category">' . $link . '</span>';
184                 }
185                 $this->children[] = $link;
186
187                 $this->children_start_char[] = 
188                         $this->getSubcategorySortChar( $cat->getTitle(), $sortkey );
189         }
190
191         /**
192          * Add a subcategory to the internal lists, using a title object
193          * @deprecated kept for compatibility, please use addSubcategoryObject instead
194          */
195         function addSubcategory( Title $title, $sortkey, $pageLength ) {
196                 $this->addSubcategoryObject( Category::newFromTitle( $title ), $sortkey, $pageLength );
197         }
198
199         /**
200         * Get the character to be used for sorting subcategories.
201         * If there's a link from Category:A to Category:B, the sortkey of the resulting
202         * entry in the categorylinks table is Category:A, not A, which it SHOULD be.
203         * Workaround: If sortkey == "Category:".$title, than use $title for sorting,
204         * else use sortkey...
205         *
206         * @param Title $title
207         * @param string $sortkey The human-readable sortkey (before transforming to icu or whatever).
208         */
209         function getSubcategorySortChar( $title, $sortkey ) {
210                 global $wgContLang;
211
212                 if ( $title->getPrefixedText() == $sortkey ) {
213                         $word = $title->getDBkey();
214                 } else {
215                         $word = $sortkey;
216                 }
217
218                 $firstChar = $this->collation->getFirstLetter( $word );
219
220                 return $wgContLang->convert( $firstChar );
221         }
222
223         /**
224          * Add a page in the image namespace
225          */
226         function addImage( Title $title, $sortkey, $pageLength, $isRedirect = false ) {
227                 global $wgContLang;
228                 if ( $this->showGallery ) {
229                         $flip = $this->flip['file'];
230                         if ( $flip ) {
231                                 $this->gallery->insert( $title );
232                         } else {
233                                 $this->gallery->add( $title );
234                         }
235                 } else {
236                         $link = $this->getSkin()->link( $title );
237                         if ( $isRedirect ) {
238                                 // This seems kind of pointless given 'mw-redirect' class,
239                                 // but keeping for back-compatibility with user css.
240                                 $link = '<span class="redirect-in-category">' . $link . '</span>';
241                         }
242                         $this->imgsNoGallery[] = $link;
243
244                         $this->imgsNoGallery_start_char[] = $wgContLang->convert( 
245                                 $this->collation->getFirstLetter( $sortkey ) );
246                 }
247         }
248
249         /**
250          * Add a miscellaneous page
251          */
252         function addPage( $title, $sortkey, $pageLength, $isRedirect = false ) {
253                 global $wgContLang;
254
255                 $link = $this->getSkin()->link( $title );
256                 if ( $isRedirect ) {
257                         // This seems kind of pointless given 'mw-redirect' class,
258                         // but keeping for back-compatiability with user css.
259                         $link = '<span class="redirect-in-category">' . $link . '</span>';
260                 }
261                 $this->articles[] = $link;
262
263                 $this->articles_start_char[] = $wgContLang->convert( 
264                         $this->collation->getFirstLetter( $sortkey ) );
265         }
266
267         function finaliseCategoryState() {
268                 if ( $this->flip['subcat'] ) {
269                         $this->children            = array_reverse( $this->children );
270                         $this->children_start_char = array_reverse( $this->children_start_char );
271                 }
272                 if ( $this->flip['page'] ) {
273                         $this->articles            = array_reverse( $this->articles );
274                         $this->articles_start_char = array_reverse( $this->articles_start_char );
275                 }
276                 if ( !$this->showGallery && $this->flip['file'] ) {
277                         $this->imgsNoGallery            = array_reverse( $this->imgsNoGallery );
278                         $this->imgsNoGallery_start_char = array_reverse( $this->imgsNoGallery_start_char );
279                 }
280         }
281
282         function doCategoryQuery() {
283                 $dbr = wfGetDB( DB_SLAVE, 'category' );
284
285                 $this->nextPage = array(
286                         'page' => null,
287                         'subcat' => null,
288                         'file' => null,
289                 );
290                 $this->flip = array( 'page' => false, 'subcat' => false, 'file' => false );
291
292                 foreach ( array( 'page', 'subcat', 'file' ) as $type ) {
293                         # Get the sortkeys for start/end, if applicable.  Note that if
294                         # the collation in the database differs from the one
295                         # set in $wgCategoryCollation, pagination might go totally haywire.
296                         $extraConds = array( 'cl_type' => $type );
297                         if ( $this->from[$type] !== null ) {
298                                 $extraConds[] = 'cl_sortkey >= '
299                                         . $dbr->addQuotes( $this->collation->getSortKey( $this->from[$type] ) );
300                         } elseif ( $this->until[$type] !== null ) {
301                                 $extraConds[] = 'cl_sortkey < '
302                                         . $dbr->addQuotes( $this->collation->getSortKey( $this->until[$type] ) );
303                                 $this->flip[$type] = true;
304                         }
305
306                         $res = $dbr->select(
307                                 array( 'page', 'categorylinks', 'category' ),
308                                 array( 'page_id', 'page_title', 'page_namespace', 'page_len',
309                                         'page_is_redirect', 'cl_sortkey', 'cat_id', 'cat_title',
310                                         'cat_subcats', 'cat_pages', 'cat_files',
311                                         'cl_sortkey_prefix', 'cl_collation' ),
312                                 array( 'cl_to' => $this->title->getDBkey() ) + $extraConds,
313                                 __METHOD__,
314                                 array(
315                                         'USE INDEX' => array( 'categorylinks' => 'cl_sortkey' ),
316                                         'LIMIT' => $this->limit + 1,
317                                         'ORDER BY' => $this->flip[$type] ? 'cl_sortkey DESC' : 'cl_sortkey',
318                                 ),
319                                 array(
320                                         'categorylinks'  => array( 'INNER JOIN', 'cl_from = page_id' ),
321                                         'category' => array( 'LEFT JOIN', 'cat_title = page_title AND page_namespace = ' . NS_CATEGORY )
322                                 )
323                         );
324
325                         $count = 0;
326                         foreach ( $res as $row ) {
327                                 $title = Title::newFromRow( $row );
328                                 if ( $row->cl_collation === '' ) {
329                                         // Hack to make sure that while updating from 1.16 schema
330                                         // and db is inconsistent, that the sky doesn't fall.
331                                         // See r83544. Could perhaps be removed in a couple decades...
332                                         $humanSortkey = $row->cl_sortkey;
333                                 } else {
334                                         $humanSortkey = $title->getCategorySortkey( $row->cl_sortkey_prefix );
335                                 }
336
337                                 if ( ++$count > $this->limit ) {
338                                         # We've reached the one extra which shows that there
339                                         # are additional pages to be had. Stop here...
340                                         $this->nextPage[$type] = $humanSortkey;
341                                         break;
342                                 }
343
344                                 if ( $title->getNamespace() == NS_CATEGORY ) {
345                                         $cat = Category::newFromRow( $row, $title );
346                                         $this->addSubcategoryObject( $cat, $humanSortkey, $row->page_len );
347                                 } elseif ( $title->getNamespace() == NS_FILE ) {
348                                         $this->addImage( $title, $humanSortkey, $row->page_len, $row->page_is_redirect );
349                                 } else {
350                                         $this->addPage( $title, $humanSortkey, $row->page_len, $row->page_is_redirect );
351                                 }
352                         }
353                 }
354         }
355
356         function getCategoryTop() {
357                 $r = $this->getCategoryBottom();
358                 return $r === ''
359                         ? $r
360                         : "<br style=\"clear:both;\"/>\n" . $r;
361         }
362
363         function getSubcategorySection() {
364                 # Don't show subcategories section if there are none.
365                 $r = '';
366                 $rescnt = count( $this->children );
367                 $dbcnt = $this->cat->getSubcatCount();
368                 $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'subcat' );
369
370                 if ( $rescnt > 0 ) {
371                         # Showing subcategories
372                         $r .= "<div id=\"mw-subcategories\">\n";
373                         $r .= '<h2>' . wfMsg( 'subcategories' ) . "</h2>\n";
374                         $r .= $countmsg;
375                         $r .= $this->getSectionPagingLinks( 'subcat' );
376                         $r .= $this->formatList( $this->children, $this->children_start_char );
377                         $r .= $this->getSectionPagingLinks( 'subcat' );
378                         $r .= "\n</div>";
379                 }
380                 return $r;
381         }
382
383         function getPagesSection() {
384                 $ti = htmlspecialchars( $this->title->getText() );
385                 # Don't show articles section if there are none.
386                 $r = '';
387
388                 # FIXME, here and in the other two sections: we don't need to bother
389                 # with this rigamarole if the entire category contents fit on one page
390                 # and have already been retrieved.  We can just use $rescnt in that
391                 # case and save a query and some logic.
392                 $dbcnt = $this->cat->getPageCount() - $this->cat->getSubcatCount()
393                         - $this->cat->getFileCount();
394                 $rescnt = count( $this->articles );
395                 $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'article' );
396
397                 if ( $rescnt > 0 ) {
398                         $r = "<div id=\"mw-pages\">\n";
399                         $r .= '<h2>' . wfMsg( 'category_header', $ti ) . "</h2>\n";
400                         $r .= $countmsg;
401                         $r .= $this->getSectionPagingLinks( 'page' );
402                         $r .= $this->formatList( $this->articles, $this->articles_start_char );
403                         $r .= $this->getSectionPagingLinks( 'page' );
404                         $r .= "\n</div>";
405                 }
406                 return $r;
407         }
408
409         function getImageSection() {
410                 $r = '';
411                 $rescnt = $this->showGallery ? $this->gallery->count() : count( $this->imgsNoGallery );
412                 if ( $rescnt > 0 ) {
413                         $dbcnt = $this->cat->getFileCount();
414                         $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'file' );
415
416                         $r .= "<div id=\"mw-category-media\">\n";
417                         $r .= '<h2>' . wfMsg( 'category-media-header', htmlspecialchars( $this->title->getText() ) ) . "</h2>\n";
418                         $r .= $countmsg;
419                         $r .= $this->getSectionPagingLinks( 'file' );
420                         if ( $this->showGallery ) {
421                                 $r .= $this->gallery->toHTML();
422                         } else {
423                                 $r .= $this->formatList( $this->imgsNoGallery, $this->imgsNoGallery_start_char );
424                         }
425                         $r .= $this->getSectionPagingLinks( 'file' );
426                         $r .= "\n</div>";
427                 }
428                 return $r;
429         }
430
431         /**
432          * Get the paging links for a section (subcats/pages/files), to go at the top and bottom
433          * of the output.
434          *
435          * @param $type String: 'page', 'subcat', or 'file'
436          * @return String: HTML output, possibly empty if there are no other pages
437          */
438         private function getSectionPagingLinks( $type ) {
439                 if ( $this->until[$type] !== null ) {
440                         return $this->pagingLinks( $this->nextPage[$type], $this->until[$type], $type );
441                 } elseif ( $this->nextPage[$type] !== null || $this->from[$type] !== null ) {
442                         return $this->pagingLinks( $this->from[$type], $this->nextPage[$type], $type );
443                 } else {
444                         return '';
445                 }
446         }
447
448         function getCategoryBottom() {
449                 return '';
450         }
451
452         /**
453          * Format a list of articles chunked by letter, either as a
454          * bullet list or a columnar format, depending on the length.
455          *
456          * @param $articles Array
457          * @param $articles_start_char Array
458          * @param $cutoff Int
459          * @return String
460          * @private
461          */
462         function formatList( $articles, $articles_start_char, $cutoff = 6 ) {
463                 if ( count ( $articles ) > $cutoff ) {
464                         return self::columnList( $articles, $articles_start_char );
465                 } elseif ( count( $articles ) > 0 ) {
466                         // for short lists of articles in categories.
467                         return self::shortList( $articles, $articles_start_char );
468                 }
469                 return '';
470         }
471
472         /**
473          * Format a list of articles chunked by letter in a three-column
474          * list, ordered vertically.
475          *
476          * TODO: Take the headers into account when creating columns, so they're
477          * more visually equal.
478          *
479          * More distant TODO: Scrap this and use CSS columns, whenever IE finally
480          * supports those.
481          *
482          * @param $articles Array
483          * @param $articles_start_char Array
484          * @return String
485          * @private
486          */
487         static function columnList( $articles, $articles_start_char ) {
488                 $columns = array_combine( $articles, $articles_start_char );
489                 # Split into three columns
490                 $columns = array_chunk( $columns, ceil( count( $columns ) / 3 ), true /* preserve keys */ );
491
492                 $ret = '<table width="100%"><tr valign="top"><td>';
493                 $prevchar = null;
494
495                 foreach ( $columns as $column ) {
496                         $colContents = array();
497
498                         # Kind of like array_flip() here, but we keep duplicates in an
499                         # array instead of dropping them.
500                         foreach ( $column as $article => $char ) {
501                                 if ( !isset( $colContents[$char] ) ) {
502                                         $colContents[$char] = array();
503                                 }
504                                 $colContents[$char][] = $article;
505                         }
506
507                         $first = true;
508                         foreach ( $colContents as $char => $articles ) {
509                                 $ret .= '<h3>' . htmlspecialchars( $char );
510                                 if ( $first && $char === $prevchar ) {
511                                         # We're continuing a previous chunk at the top of a new
512                                         # column, so add " cont." after the letter.
513                                         $ret .= ' ' . wfMsgHtml( 'listingcontinuesabbrev' );
514                                 }
515                                 $ret .= "</h3>\n";
516
517                                 $ret .= '<ul><li>';
518                                 $ret .= implode( "</li>\n<li>", $articles );
519                                 $ret .= '</li></ul>';
520
521                                 $first = false;
522                                 $prevchar = $char;
523                         }
524
525                         $ret .= "</td>\n<td>";
526                 }
527
528                 $ret .= '</td></tr></table>';
529                 return $ret;
530         }
531
532         /**
533          * Format a list of articles chunked by letter in a bullet list.
534          * @param $articles Array
535          * @param $articles_start_char Array
536          * @return String
537          * @private
538          */
539         static function shortList( $articles, $articles_start_char ) {
540                 $r = '<h3>' . htmlspecialchars( $articles_start_char[0] ) . "</h3>\n";
541                 $r .= '<ul><li>' . $articles[0] . '</li>';
542                 for ( $index = 1; $index < count( $articles ); $index++ )
543                 {
544                         if ( $articles_start_char[$index] != $articles_start_char[$index - 1] )
545                         {
546                                 $r .= "</ul><h3>" . htmlspecialchars( $articles_start_char[$index] ) . "</h3>\n<ul>";
547                         }
548
549                         $r .= "<li>{$articles[$index]}</li>";
550                 }
551                 $r .= '</ul>';
552                 return $r;
553         }
554
555         /**
556          * Create paging links, as a helper method to getSectionPagingLinks().
557          *
558          * @param $first String The 'until' parameter for the generated URL
559          * @param $last String The 'from' parameter for the genererated URL
560          * @param $type String A prefix for parameters, 'page' or 'subcat' or
561          *     'file'
562          * @return String HTML
563          */
564         private function pagingLinks( $first, $last, $type = '' ) {
565                 global $wgLang;
566                 $sk = $this->getSkin();
567                 $limitText = $wgLang->formatNum( $this->limit );
568
569                 $prevLink = wfMsgExt( 'prevn', array( 'escape', 'parsemag' ), $limitText );
570
571                 if ( $first != '' ) {
572                         $prevQuery = $this->query;
573                         $prevQuery["{$type}until"] = $first;
574                         unset( $prevQuery["{$type}from"] );
575                         $prevLink = $sk->linkKnown(
576                                 $this->title,
577                                 $prevLink,
578                                 array(),
579                                 $prevQuery
580                         );
581                 }
582
583                 $nextLink = wfMsgExt( 'nextn', array( 'escape', 'parsemag' ), $limitText );
584
585                 if ( $last != '' ) {
586                         $lastQuery = $this->query;
587                         $lastQuery["{$type}from"] = $last;
588                         unset( $lastQuery["{$type}until"] );
589                         $nextLink = $sk->linkKnown(
590                                 $this->title,
591                                 $nextLink,
592                                 array(),
593                                 $lastQuery
594                         );
595                 }
596
597                 return "($prevLink) ($nextLink)";
598         }
599
600         /**
601          * What to do if the category table conflicts with the number of results
602          * returned?  This function says what. Each type is considered independantly
603          * of the other types.
604          *
605          * Note for grepping: uses the messages category-article-count,
606          * category-article-count-limited, category-subcat-count,
607          * category-subcat-count-limited, category-file-count,
608          * category-file-count-limited.
609          *
610          * @param $rescnt Int: The number of items returned by our database query.
611          * @param $dbcnt Int: The number of items according to the category table.
612          * @param $type String: 'subcat', 'article', or 'file'
613          * @return String: A message giving the number of items, to output to HTML.
614          */
615         private function getCountMessage( $rescnt, $dbcnt, $type ) {
616                 global $wgLang;
617                 # There are three cases:
618                 #   1) The category table figure seems sane.  It might be wrong, but
619                 #      we can't do anything about it if we don't recalculate it on ev-
620                 #      ery category view.
621                 #   2) The category table figure isn't sane, like it's smaller than the
622                 #      number of actual results, *but* the number of results is less
623                 #      than $this->limit and there's no offset.  In this case we still
624                 #      know the right figure.
625                 #   3) We have no idea.
626
627                 # Check if there's a "from" or "until" for anything
628
629                 // This is a little ugly, but we seem to use different names
630                 // for the paging types then for the messages.
631                 if ( $type === 'article' ) {
632                         $pagingType = 'page';
633                 } else {
634                         $pagingType = $type;
635                 }
636
637                 $fromOrUntil = false;
638                 if ( $this->from[$pagingType] !== null || $this->until[$pagingType] !== null ) {
639                         $fromOrUntil = true;
640                 }
641
642                 if ( $dbcnt == $rescnt || ( ( $rescnt == $this->limit || $fromOrUntil )
643                         && $dbcnt > $rescnt ) )
644                 {
645                         # Case 1: seems sane.
646                         $totalcnt = $dbcnt;
647                 } elseif ( $rescnt < $this->limit && !$fromOrUntil ) {
648                         # Case 2: not sane, but salvageable.  Use the number of results.
649                         # Since there are fewer than 200, we can also take this opportunity
650                         # to refresh the incorrect category table entry -- which should be
651                         # quick due to the small number of entries.
652                         $totalcnt = $rescnt;
653                         $this->cat->refreshCounts();
654                 } else {
655                         # Case 3: hopeless.  Don't give a total count at all.
656                         return wfMsgExt( "category-$type-count-limited", 'parse',
657                                 $wgLang->formatNum( $rescnt ) );
658                 }
659                 return wfMsgExt(
660                         "category-$type-count",
661                         'parse',
662                         $wgLang->formatNum( $rescnt ),
663                         $wgLang->formatNum( $totalcnt )
664                 );
665         }
666 }