]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/CategoryPage.php
MediaWiki 1.15.0
[autoinstallsdev/mediawiki.git] / includes / CategoryPage.php
1 <?php
2 /**
3  * Special handling for category description pages
4  * Modelled after ImagePage.php
5  *
6  */
7
8 if( !defined( 'MEDIAWIKI' ) )
9         die( 1 );
10
11 /**
12  */
13 class CategoryPage extends Article {
14         function view() {
15                 global $wgRequest, $wgUser;
16
17                 $diff = $wgRequest->getVal( 'diff' );
18                 $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) );
19
20                 if ( isset( $diff ) && $diffOnly )
21                         return Article::view();
22
23                 if( !wfRunHooks( 'CategoryPageView', array( &$this ) ) )
24                         return;
25
26                 if ( NS_CATEGORY == $this->mTitle->getNamespace() ) {
27                         $this->openShowCategory();
28                 }
29
30                 Article::view();
31
32                 if ( NS_CATEGORY == $this->mTitle->getNamespace() ) {
33                         $this->closeShowCategory();
34                 }
35         }
36         
37         /**
38          * Don't return a 404 for categories in use.
39          */
40         function hasViewableContent() {
41                 if( parent::hasViewableContent() ) {
42                         return true;
43                 } else {
44                         $cat = Category::newFromTitle( $this->mTitle );
45                         return $cat->getId() != 0;
46                 }
47                         
48         }
49
50         function openShowCategory() {
51                 # For overloading
52         }
53
54         function closeShowCategory() {
55                 global $wgOut, $wgRequest;
56                 $from = $wgRequest->getVal( 'from' );
57                 $until = $wgRequest->getVal( 'until' );
58
59                 $viewer = new CategoryViewer( $this->mTitle, $from, $until );
60                 $wgOut->addHTML( $viewer->getHTML() );
61         }
62 }
63
64 class CategoryViewer {
65         var $title, $limit, $from, $until,
66                 $articles, $articles_start_char,
67                 $children, $children_start_char,
68                 $showGallery, $gallery,
69                 $skin;
70         /** Category object for this page */
71         private $cat;
72
73         function __construct( $title, $from = '', $until = '' ) {
74                 global $wgCategoryPagingLimit;
75                 $this->title = $title;
76                 $this->from = $from;
77                 $this->until = $until;
78                 $this->limit = $wgCategoryPagingLimit;
79                 $this->cat = Category::newFromTitle( $title );
80         }
81
82         /**
83          * Format the category data list.
84          *
85          * @return string HTML output
86          * @private
87          */
88         function getHTML() {
89                 global $wgOut, $wgCategoryMagicGallery, $wgCategoryPagingLimit;
90                 wfProfileIn( __METHOD__ );
91
92                 $this->showGallery = $wgCategoryMagicGallery && !$wgOut->mNoGallery;
93
94                 $this->clearCategoryState();
95                 $this->doCategoryQuery();
96                 $this->finaliseCategoryState();
97
98                 $r = $this->getCategoryTop() .
99                         $this->getSubcategorySection() .
100                         $this->getPagesSection() .
101                         $this->getImageSection() .
102                         $this->getCategoryBottom();
103
104                 // Give a proper message if category is empty
105                 if ( $r == '' ) {
106                         $r = wfMsgExt( 'category-empty', array( 'parse' ) );
107                 }
108
109                 wfProfileOut( __METHOD__ );
110                 return $r;
111         }
112
113         function clearCategoryState() {
114                 $this->articles = array();
115                 $this->articles_start_char = array();
116                 $this->children = array();
117                 $this->children_start_char = array();
118                 if( $this->showGallery ) {
119                         $this->gallery = new ImageGallery();
120                         $this->gallery->setHideBadImages();
121                 }
122         }
123
124         function getSkin() {
125                 if ( !$this->skin ) {
126                         global $wgUser;
127                         $this->skin = $wgUser->getSkin();
128                 }
129                 return $this->skin;
130         }
131
132         /**
133          * Add a subcategory to the internal lists, using a Category object
134          */
135         function addSubcategoryObject( $cat, $sortkey, $pageLength ) {
136                 $title = $cat->getTitle();
137                 $this->addSubcategory( $title, $sortkey, $pageLength );
138         }
139
140         /**
141          * Add a subcategory to the internal lists, using a title object 
142          * @deprecated kept for compatibility, please use addSubcategoryObject instead
143          */
144         function addSubcategory( $title, $sortkey, $pageLength ) {
145                 global $wgContLang;
146                 // Subcategory; strip the 'Category' namespace from the link text.
147                 $this->children[] = $this->getSkin()->makeKnownLinkObj(
148                         $title, $wgContLang->convertHtml( $title->getText() ) );
149
150                 $this->children_start_char[] = $this->getSubcategorySortChar( $title, $sortkey );
151         }
152
153         /**
154         * Get the character to be used for sorting subcategories.
155         * If there's a link from Category:A to Category:B, the sortkey of the resulting
156         * entry in the categorylinks table is Category:A, not A, which it SHOULD be.
157         * Workaround: If sortkey == "Category:".$title, than use $title for sorting,
158         * else use sortkey...
159         */
160         function getSubcategorySortChar( $title, $sortkey ) {
161                 global $wgContLang;
162
163                 if( $title->getPrefixedText() == $sortkey ) {
164                         $firstChar = $wgContLang->firstChar( $title->getDBkey() );
165                 } else {
166                         $firstChar = $wgContLang->firstChar( $sortkey );
167                 }
168
169                 return $wgContLang->convert( $firstChar );
170         }
171
172         /**
173          * Add a page in the image namespace
174          */
175         function addImage( Title $title, $sortkey, $pageLength, $isRedirect = false ) {
176                 if ( $this->showGallery ) {
177                         if( $this->flip ) {
178                                 $this->gallery->insert( $title );
179                         } else {
180                                 $this->gallery->add( $title );
181                         }
182                 } else {
183                         $this->addPage( $title, $sortkey, $pageLength, $isRedirect );
184                 }
185         }
186
187         /**
188          * Add a miscellaneous page
189          */
190         function addPage( $title, $sortkey, $pageLength, $isRedirect = false ) {
191                 global $wgContLang;
192                 $titletext = $wgContLang->convert( $title->getPrefixedText() );
193                 $this->articles[] = $isRedirect
194                         ? '<span class="redirect-in-category">' . $this->getSkin()->makeKnownLinkObj( $title, $titletext ) . '</span>'
195                         : $this->getSkin()->makeSizeLinkObj( $pageLength, $title, $titletext );
196                 $this->articles_start_char[] = $wgContLang->convert( $wgContLang->firstChar( $sortkey ) );
197         }
198
199         function finaliseCategoryState() {
200                 if( $this->flip ) {
201                         $this->children            = array_reverse( $this->children );
202                         $this->children_start_char = array_reverse( $this->children_start_char );
203                         $this->articles            = array_reverse( $this->articles );
204                         $this->articles_start_char = array_reverse( $this->articles_start_char );
205                 }
206         }
207
208         function doCategoryQuery() {
209                 $dbr = wfGetDB( DB_SLAVE, 'category' );
210                 if( $this->from != '' ) {
211                         $pageCondition = 'cl_sortkey >= ' . $dbr->addQuotes( $this->from );
212                         $this->flip = false;
213                 } elseif( $this->until != '' ) {
214                         $pageCondition = 'cl_sortkey < ' . $dbr->addQuotes( $this->until );
215                         $this->flip = true;
216                 } else {
217                         $pageCondition = '1 = 1';
218                         $this->flip = false;
219                 }
220                 $res = $dbr->select(
221                         array( 'page', 'categorylinks', 'category' ),
222                         array( 'page_title', 'page_namespace', 'page_len', 'page_is_redirect', 'cl_sortkey',
223                                 'cat_id', 'cat_title', 'cat_subcats', 'cat_pages', 'cat_files' ),
224                         array( $pageCondition, 'cl_to' => $this->title->getDBkey() ),
225                         __METHOD__,
226                         array( 'ORDER BY' => $this->flip ? 'cl_sortkey DESC' : 'cl_sortkey',
227                                 'USE INDEX' => array( 'categorylinks' => 'cl_sortkey' ),
228                                 'LIMIT'    => $this->limit + 1 ),
229                         array( 'categorylinks'  => array( 'INNER JOIN', 'cl_from = page_id' ),
230                                 'category' => array( 'LEFT JOIN', 'cat_title = page_title AND page_namespace = ' . NS_CATEGORY ) )
231                 );
232
233                 $count = 0;
234                 $this->nextPage = null;
235                 while( $x = $dbr->fetchObject ( $res ) ) {
236                         if( ++$count > $this->limit ) {
237                                 // We've reached the one extra which shows that there are
238                                 // additional pages to be had. Stop here...
239                                 $this->nextPage = $x->cl_sortkey;
240                                 break;
241                         }
242
243                         $title = Title::makeTitle( $x->page_namespace, $x->page_title );
244
245                         if( $title->getNamespace() == NS_CATEGORY ) {
246                                 $cat = Category::newFromRow( $x, $title );
247                                 $this->addSubcategoryObject( $cat, $x->cl_sortkey, $x->page_len );
248                         } elseif( $this->showGallery && $title->getNamespace() == NS_FILE ) {
249                                 $this->addImage( $title, $x->cl_sortkey, $x->page_len, $x->page_is_redirect );
250                         } else {
251                                 $this->addPage( $title, $x->cl_sortkey, $x->page_len, $x->page_is_redirect );
252                         }
253                 }
254                 $dbr->freeResult( $res );
255         }
256
257         function getCategoryTop() {
258                 $r = '';
259                 if( $this->until != '' ) {
260                         $r .= $this->pagingLinks( $this->title, $this->nextPage, $this->until, $this->limit );
261                 } elseif( $this->nextPage != '' || $this->from != '' ) {
262                         $r .= $this->pagingLinks( $this->title, $this->from, $this->nextPage, $this->limit );
263                 }
264                 return $r == ''
265                         ? $r
266                         : "<br style=\"clear:both;\"/>\n" . $r;
267         }
268
269         function getSubcategorySection() {
270                 # Don't show subcategories section if there are none.
271                 $r = '';
272                 $rescnt = count( $this->children );
273                 $dbcnt = $this->cat->getSubcatCount();
274                 $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'subcat' );
275                 if( $rescnt > 0 ) {
276                         # Showing subcategories
277                         $r .= "<div id=\"mw-subcategories\">\n";
278                         $r .= '<h2>' . wfMsg( 'subcategories' ) . "</h2>\n";
279                         $r .= $countmsg;
280                         $r .= $this->formatList( $this->children, $this->children_start_char );
281                         $r .= "\n</div>";
282                 }
283                 return $r;
284         }
285
286         function getPagesSection() {
287                 $ti = htmlspecialchars( $this->title->getText() );
288                 # Don't show articles section if there are none.
289                 $r = '';
290
291                 # FIXME, here and in the other two sections: we don't need to bother
292                 # with this rigamarole if the entire category contents fit on one page
293                 # and have already been retrieved.  We can just use $rescnt in that
294                 # case and save a query and some logic.
295                 $dbcnt = $this->cat->getPageCount() - $this->cat->getSubcatCount()
296                         - $this->cat->getFileCount();
297                 $rescnt = count( $this->articles );
298                 $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'article' );
299
300                 if( $rescnt > 0 ) {
301                         $r = "<div id=\"mw-pages\">\n";
302                         $r .= '<h2>' . wfMsg( 'category_header', $ti ) . "</h2>\n";
303                         $r .= $countmsg;
304                         $r .= $this->formatList( $this->articles, $this->articles_start_char );
305                         $r .= "\n</div>";
306                 }
307                 return $r;
308         }
309
310         function getImageSection() {
311                 if( $this->showGallery && ! $this->gallery->isEmpty() ) {
312                         $dbcnt = $this->cat->getFileCount();
313                         $rescnt = $this->gallery->count();
314                         $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'file' );
315
316                         return "<div id=\"mw-category-media\">\n" .
317                         '<h2>' . wfMsg( 'category-media-header', htmlspecialchars( $this->title->getText() ) ) . "</h2>\n" .
318                         $countmsg . $this->gallery->toHTML() . "\n</div>";
319                 } else {
320                         return '';
321                 }
322         }
323
324         function getCategoryBottom() {
325                 if( $this->until != '' ) {
326                         return $this->pagingLinks( $this->title, $this->nextPage, $this->until, $this->limit );
327                 } elseif( $this->nextPage != '' || $this->from != '' ) {
328                         return $this->pagingLinks( $this->title, $this->from, $this->nextPage, $this->limit );
329                 } else {
330                         return '';
331                 }
332         }
333
334         /**
335          * Format a list of articles chunked by letter, either as a
336          * bullet list or a columnar format, depending on the length.
337          *
338          * @param $articles Array
339          * @param $articles_start_char Array
340          * @param $cutoff Int
341          * @return String
342          * @private
343          */
344         function formatList( $articles, $articles_start_char, $cutoff = 6 ) {
345                 if ( count ( $articles ) > $cutoff ) {
346                         return $this->columnList( $articles, $articles_start_char );
347                 } elseif ( count($articles) > 0) {
348                         // for short lists of articles in categories.
349                         return $this->shortList( $articles, $articles_start_char );
350                 }
351                 return '';
352         }
353
354         /**
355          * Format a list of articles chunked by letter in a three-column
356          * list, ordered vertically.
357          *
358          * @param $articles Array
359          * @param $articles_start_char Array
360          * @return String
361          * @private
362          */
363         function columnList( $articles, $articles_start_char ) {
364                 // divide list into three equal chunks
365                 $chunk = (int) (count ( $articles ) / 3);
366
367                 // get and display header
368                 $r = '<table width="100%"><tr valign="top">';
369
370                 $prev_start_char = 'none';
371
372                 // loop through the chunks
373                 for($startChunk = 0, $endChunk = $chunk, $chunkIndex = 0;
374                         $chunkIndex < 3;
375                         $chunkIndex++, $startChunk = $endChunk, $endChunk += $chunk + 1)
376                 {
377                         $r .= "<td>\n";
378                         $atColumnTop = true;
379
380                         // output all articles in category
381                         for ($index = $startChunk ;
382                                 $index < $endChunk && $index < count($articles);
383                                 $index++ )
384                         {
385                                 // check for change of starting letter or begining of chunk
386                                 if ( ($index == $startChunk) ||
387                                          ($articles_start_char[$index] != $articles_start_char[$index - 1]) )
388
389                                 {
390                                         if( $atColumnTop ) {
391                                                 $atColumnTop = false;
392                                         } else {
393                                                 $r .= "</ul>\n";
394                                         }
395                                         $cont_msg = "";
396                                         if ( $articles_start_char[$index] == $prev_start_char )
397                                                 $cont_msg = ' ' . wfMsgHtml( 'listingcontinuesabbrev' );
398                                         $r .= "<h3>" . htmlspecialchars( $articles_start_char[$index] ) . "$cont_msg</h3>\n<ul>";
399                                         $prev_start_char = $articles_start_char[$index];
400                                 }
401
402                                 $r .= "<li>{$articles[$index]}</li>";
403                         }
404                         if( !$atColumnTop ) {
405                                 $r .= "</ul>\n";
406                         }
407                         $r .= "</td>\n";
408
409
410                 }
411                 $r .= '</tr></table>';
412                 return $r;
413         }
414
415         /**
416          * Format a list of articles chunked by letter in a bullet list.
417          * @param $articles Array
418          * @param $articles_start_char Array
419          * @return String
420          * @private
421          */
422         function shortList( $articles, $articles_start_char ) {
423                 $r = '<h3>' . htmlspecialchars( $articles_start_char[0] ) . "</h3>\n";
424                 $r .= '<ul><li>'.$articles[0].'</li>';
425                 for ($index = 1; $index < count($articles); $index++ )
426                 {
427                         if ($articles_start_char[$index] != $articles_start_char[$index - 1])
428                         {
429                                 $r .= "</ul><h3>" . htmlspecialchars( $articles_start_char[$index] ) . "</h3>\n<ul>";
430                         }
431
432                         $r .= "<li>{$articles[$index]}</li>";
433                 }
434                 $r .= '</ul>';
435                 return $r;
436         }
437
438         /**
439          * @param $title Title object
440          * @param $first String
441          * @param $last String
442          * @param $limit Int
443          * @param $query Array: additional query options to pass
444          * @return String
445          * @private
446          */
447         function pagingLinks( $title, $first, $last, $limit, $query = array() ) {
448                 global $wgLang;
449                 $sk = $this->getSkin();
450                 $limitText = $wgLang->formatNum( $limit );
451
452                 $prevLink = wfMsgExt( 'prevn', array( 'escape', 'parsemag' ), $limitText );
453                 if( $first != '' ) {
454                         $prevLink = $sk->makeLinkObj( $title, $prevLink,
455                                 wfArrayToCGI( $query + array( 'until' => $first ) ) );
456                 }
457                 $nextLink = wfMsgExt( 'nextn', array( 'escape', 'parsemag' ), $limitText );
458                 if( $last != '' ) {
459                         $nextLink = $sk->makeLinkObj( $title, $nextLink,
460                                 wfArrayToCGI( $query + array( 'from' => $last ) ) );
461                 }
462
463                 return "($prevLink) ($nextLink)";
464         }
465
466         /**
467          * What to do if the category table conflicts with the number of results
468          * returned?  This function says what.  It works the same whether the
469          * things being counted are articles, subcategories, or files.
470          *
471          * Note for grepping: uses the messages category-article-count,
472          * category-article-count-limited, category-subcat-count,
473          * category-subcat-count-limited, category-file-count,
474          * category-file-count-limited.
475          *
476          * @param $rescnt Int: The number of items returned by our database query.
477          * @param $dbcnt Int: The number of items according to the category table.
478          * @param $type String: 'subcat', 'article', or 'file'
479          * @return String: A message giving the number of items, to output to HTML.
480          */
481         private function getCountMessage( $rescnt, $dbcnt, $type ) {
482                 global $wgLang;
483                 # There are three cases:
484                 #   1) The category table figure seems sane.  It might be wrong, but
485                 #      we can't do anything about it if we don't recalculate it on ev-
486                 #      ery category view.
487                 #   2) The category table figure isn't sane, like it's smaller than the
488                 #      number of actual results, *but* the number of results is less
489                 #      than $this->limit and there's no offset.  In this case we still
490                 #      know the right figure.
491                 #   3) We have no idea.
492                 $totalrescnt = count( $this->articles ) + count( $this->children ) +
493                         ($this->showGallery ? $this->gallery->count() : 0);
494                 if($dbcnt == $rescnt || (($totalrescnt == $this->limit || $this->from
495                 || $this->until) && $dbcnt > $rescnt)){
496                         # Case 1: seems sane.
497                         $totalcnt = $dbcnt;
498                 } elseif($totalrescnt < $this->limit && !$this->from && !$this->until){
499                         # Case 2: not sane, but salvageable.  Use the number of results.
500                         # Since there are fewer than 200, we can also take this opportunity
501                         # to refresh the incorrect category table entry -- which should be
502                         # quick due to the small number of entries.
503                         $totalcnt = $rescnt;
504                         $this->cat->refreshCounts();
505                 } else {
506                         # Case 3: hopeless.  Don't give a total count at all.
507                         return wfMsgExt("category-$type-count-limited", 'parse',
508                                 $wgLang->formatNum( $rescnt ) );
509                 }
510                 return wfMsgExt( "category-$type-count", 'parse', $wgLang->formatNum( $rescnt ),
511                         $wgLang->formatNum( $totalcnt ) );
512         }
513 }