CategoriesPage no more
[lhc/web/wiklou.git] / includes / QueryPage.php
1 <?php
2 /**
3 * Contain a class for special pages
4 */
5
6 /**
7 * List of query page classes and their associated special pages, for periodic update purposes
8 */
9 global $wgQueryPages; // not redundant
10 $wgQueryPages = array(
11 // QueryPage subclass Special page name Limit (false for none, none for the default)
12 //----------------------------------------------------------------------------
13 array( 'AncientPagesPage', 'Ancientpages' ),
14 array( 'BrokenRedirectsPage', 'BrokenRedirects' ),
15 array( 'DeadendPagesPage', 'Deadendpages' ),
16 array( 'DisambiguationsPage', 'Disambiguations' ),
17 array( 'DoubleRedirectsPage', 'DoubleRedirects' ),
18 array( 'ListUsersPage', 'Listusers' ),
19 array( 'ListredirectsPage', 'Listredirects' ),
20 array( 'LonelyPagesPage', 'Lonelypages' ),
21 array( 'LongPagesPage', 'Longpages' ),
22 array( 'MostcategoriesPage', 'Mostcategories' ),
23 array( 'MostimagesPage', 'Mostimages' ),
24 array( 'MostlinkedCategoriesPage', 'Mostlinkedcategories' ),
25 array( 'MostlinkedPage', 'Mostlinked' ),
26 array( 'MostrevisionsPage', 'Mostrevisions' ),
27 array( 'NewPagesPage', 'Newpages' ),
28 array( 'ShortPagesPage', 'Shortpages' ),
29 array( 'UncategorizedCategoriesPage', 'Uncategorizedcategories' ),
30 array( 'UncategorizedPagesPage', 'Uncategorizedpages' ),
31 array( 'UncategorizedImagesPage', 'Uncategorizedimages' ),
32 array( 'UnusedCategoriesPage', 'Unusedcategories' ),
33 array( 'UnusedimagesPage', 'Unusedimages' ),
34 array( 'WantedCategoriesPage', 'Wantedcategories' ),
35 array( 'WantedPagesPage', 'Wantedpages' ),
36 array( 'UnwatchedPagesPage', 'Unwatchedpages' ),
37 array( 'UnusedtemplatesPage', 'Unusedtemplates' ),
38 );
39 wfRunHooks( 'wgQueryPages', array( &$wgQueryPages ) );
40
41 global $wgDisableCounters;
42 if ( !$wgDisableCounters )
43 $wgQueryPages[] = array( 'PopularPagesPage', 'Popularpages' );
44
45
46 /**
47 * This is a class for doing query pages; since they're almost all the same,
48 * we factor out some of the functionality into a superclass, and let
49 * subclasses derive from it.
50 *
51 */
52 class QueryPage {
53 /**
54 * Whether or not we want plain listoutput rather than an ordered list
55 *
56 * @var bool
57 */
58 var $listoutput = false;
59
60 /**
61 * The offset and limit in use, as passed to the query() function
62 *
63 * @var integer
64 */
65 var $offset = 0;
66 var $limit = 0;
67
68 /**
69 * A mutator for $this->listoutput;
70 *
71 * @param bool $bool
72 */
73 function setListoutput( $bool ) {
74 $this->listoutput = $bool;
75 }
76
77 /**
78 * Subclasses return their name here. Make sure the name is also
79 * specified in SpecialPage.php and in Language.php as a language message
80 * param.
81 */
82 function getName() {
83 return '';
84 }
85
86 /**
87 * Return title object representing this page
88 *
89 * @return Title
90 */
91 function getTitle() {
92 return SpecialPage::getTitleFor( $this->getName() );
93 }
94
95 /**
96 * Subclasses return an SQL query here.
97 *
98 * Note that the query itself should return the following four columns:
99 * 'type' (your special page's name), 'namespace', 'title', and 'value'
100 * *in that order*. 'value' is used for sorting.
101 *
102 * These may be stored in the querycache table for expensive queries,
103 * and that cached data will be returned sometimes, so the presence of
104 * extra fields can't be relied upon. The cached 'value' column will be
105 * an integer; non-numeric values are useful only for sorting the initial
106 * query.
107 *
108 * Don't include an ORDER or LIMIT clause, this will be added.
109 */
110 function getSQL() {
111 return "SELECT 'sample' as type, 0 as namespace, 'Sample result' as title, 42 as value";
112 }
113
114 /**
115 * Override to sort by increasing values
116 */
117 function sortDescending() {
118 return true;
119 }
120
121 function getOrder() {
122 return ' ORDER BY value ' .
123 ($this->sortDescending() ? 'DESC' : '');
124 }
125
126 /**
127 * Is this query expensive (for some definition of expensive)? Then we
128 * don't let it run in miser mode. $wgDisableQueryPages causes all query
129 * pages to be declared expensive. Some query pages are always expensive.
130 */
131 function isExpensive( ) {
132 global $wgDisableQueryPages;
133 return $wgDisableQueryPages;
134 }
135
136 /**
137 * Whether or not the output of the page in question is retrived from
138 * the database cache.
139 *
140 * @return bool
141 */
142 function isCached() {
143 global $wgMiserMode;
144
145 return $this->isExpensive() && $wgMiserMode;
146 }
147
148 /**
149 * Sometime we dont want to build rss / atom feeds.
150 */
151 function isSyndicated() {
152 return true;
153 }
154
155 /**
156 * Formats the results of the query for display. The skin is the current
157 * skin; you can use it for making links. The result is a single row of
158 * result data. You should be able to grab SQL results off of it.
159 * If the function return "false", the line output will be skipped.
160 */
161 function formatResult( $skin, $result ) {
162 return '';
163 }
164
165 /**
166 * The content returned by this function will be output before any result
167 */
168 function getPageHeader( ) {
169 return '';
170 }
171
172 /**
173 * If using extra form wheely-dealies, return a set of parameters here
174 * as an associative array. They will be encoded and added to the paging
175 * links (prev/next/lengths).
176 * @return array
177 */
178 function linkParameters() {
179 return array();
180 }
181
182 /**
183 * Some special pages (for example SpecialListusers) might not return the
184 * current object formatted, but return the previous one instead.
185 * Setting this to return true, will call one more time wfFormatResult to
186 * be sure that the very last result is formatted and shown.
187 */
188 function tryLastResult( ) {
189 return false;
190 }
191
192 /**
193 * Clear the cache and save new results
194 */
195 function recache( $limit, $ignoreErrors = true ) {
196 $fname = get_class($this) . '::recache';
197 $dbw = wfGetDB( DB_MASTER );
198 $dbr = wfGetDB( DB_SLAVE, array( $this->getName(), 'QueryPage::recache', 'vslow' ) );
199 if ( !$dbw || !$dbr ) {
200 return false;
201 }
202
203 $querycache = $dbr->tableName( 'querycache' );
204
205 if ( $ignoreErrors ) {
206 $ignoreW = $dbw->ignoreErrors( true );
207 $ignoreR = $dbr->ignoreErrors( true );
208 }
209
210 # Clear out any old cached data
211 $dbw->delete( 'querycache', array( 'qc_type' => $this->getName() ), $fname );
212 # Do query
213 $sql = $this->getSQL() . $this->getOrder();
214 if ($limit !== false)
215 $sql = $dbr->limitResult($sql, $limit, 0);
216 $res = $dbr->query($sql, $fname);
217 $num = false;
218 if ( $res ) {
219 $num = $dbr->numRows( $res );
220 # Fetch results
221 $insertSql = "INSERT INTO $querycache (qc_type,qc_namespace,qc_title,qc_value) VALUES ";
222 $first = true;
223 while ( $res && $row = $dbr->fetchObject( $res ) ) {
224 if ( $first ) {
225 $first = false;
226 } else {
227 $insertSql .= ',';
228 }
229 if ( isset( $row->value ) ) {
230 $value = $row->value;
231 } else {
232 $value = '';
233 }
234
235 $insertSql .= '(' .
236 $dbw->addQuotes( $row->type ) . ',' .
237 $dbw->addQuotes( $row->namespace ) . ',' .
238 $dbw->addQuotes( $row->title ) . ',' .
239 $dbw->addQuotes( $value ) . ')';
240 }
241
242 # Save results into the querycache table on the master
243 if ( !$first ) {
244 if ( !$dbw->query( $insertSql, $fname ) ) {
245 // Set result to false to indicate error
246 $dbr->freeResult( $res );
247 $res = false;
248 }
249 }
250 if ( $res ) {
251 $dbr->freeResult( $res );
252 }
253 if ( $ignoreErrors ) {
254 $dbw->ignoreErrors( $ignoreW );
255 $dbr->ignoreErrors( $ignoreR );
256 }
257
258 # Update the querycache_info record for the page
259 $dbw->delete( 'querycache_info', array( 'qci_type' => $this->getName() ), $fname );
260 $dbw->insert( 'querycache_info', array( 'qci_type' => $this->getName(), 'qci_timestamp' => $dbw->timestamp() ), $fname );
261
262 }
263 return $num;
264 }
265
266 /**
267 * This is the actual workhorse. It does everything needed to make a
268 * real, honest-to-gosh query page.
269 *
270 * @param $offset database query offset
271 * @param $limit database query limit
272 * @param $shownavigation show navigation like "next 200"?
273 */
274 function doQuery( $offset, $limit, $shownavigation=true ) {
275 global $wgUser, $wgOut, $wgLang, $wgContLang;
276
277 $this->offset = $offset;
278 $this->limit = $limit;
279
280 $sname = $this->getName();
281 $fname = get_class($this) . '::doQuery';
282 $dbr = wfGetDB( DB_SLAVE );
283
284 $wgOut->setSyndicated( $this->isSyndicated() );
285
286 if ( !$this->isCached() ) {
287 $sql = $this->getSQL();
288 } else {
289 # Get the cached result
290 $querycache = $dbr->tableName( 'querycache' );
291 $type = $dbr->strencode( $sname );
292 $sql =
293 "SELECT qc_type as type, qc_namespace as namespace,qc_title as title, qc_value as value
294 FROM $querycache WHERE qc_type='$type'";
295
296 if( !$this->listoutput ) {
297
298 # Fetch the timestamp of this update
299 $tRes = $dbr->select( 'querycache_info', array( 'qci_timestamp' ), array( 'qci_type' => $type ), $fname );
300 $tRow = $dbr->fetchObject( $tRes );
301
302 if( $tRow ) {
303 $updated = $wgLang->timeAndDate( $tRow->qci_timestamp, true, true );
304 $cacheNotice = wfMsg( 'perfcachedts', $updated );
305 $wgOut->addMeta( 'Data-Cache-Time', $tRow->qci_timestamp );
306 $wgOut->addScript( '<script language="JavaScript">var dataCacheTime = \'' . $tRow->qci_timestamp . '\';</script>' );
307 } else {
308 $cacheNotice = wfMsg( 'perfcached' );
309 }
310
311 $wgOut->addWikiText( $cacheNotice );
312
313 # If updates on this page have been disabled, let the user know
314 # that the data set won't be refreshed for now
315 global $wgDisableQueryPageUpdate;
316 if( is_array( $wgDisableQueryPageUpdate ) && in_array( $this->getName(), $wgDisableQueryPageUpdate ) ) {
317 $wgOut->addWikiText( wfMsg( 'querypage-no-updates' ) );
318 }
319
320 }
321
322 }
323
324 $sql .= $this->getOrder();
325 $sql = $dbr->limitResult($sql, $limit, $offset);
326 $res = $dbr->query( $sql );
327 $num = $dbr->numRows($res);
328
329 $this->preprocessResults( $dbr, $res );
330
331 $sk = $wgUser->getSkin( );
332
333 if($shownavigation) {
334 $wgOut->addHTML( $this->getPageHeader() );
335 $top = wfShowingResults( $offset, $num);
336 $wgOut->addHTML( "<p>{$top}\n" );
337
338 # often disable 'next' link when we reach the end
339 $atend = $num < $limit;
340
341 $sl = wfViewPrevNext( $offset, $limit ,
342 $wgContLang->specialPage( $sname ),
343 wfArrayToCGI( $this->linkParameters() ), $atend );
344 $wgOut->addHTML( "<br />{$sl}</p>\n" );
345 }
346 if ( $num > 0 ) {
347 $s = array();
348 if ( ! $this->listoutput )
349 $s[] = $this->openList( $offset );
350
351 # Only read at most $num rows, because $res may contain the whole 1000
352 for ( $i = 0; $i < $num && $obj = $dbr->fetchObject( $res ); $i++ ) {
353 $format = $this->formatResult( $sk, $obj );
354 if ( $format ) {
355 $attr = ( isset ( $obj->usepatrol ) && $obj->usepatrol &&
356 $obj->patrolled == 0 ) ? ' class="not-patrolled"' : '';
357 $s[] = $this->listoutput ? $format : "<li{$attr}>{$format}</li>\n";
358 }
359 }
360
361 if($this->tryLastResult()) {
362 // flush the very last result
363 $obj = null;
364 $format = $this->formatResult( $sk, $obj );
365 if( $format ) {
366 $attr = ( isset ( $obj->usepatrol ) && $obj->usepatrol &&
367 $obj->patrolled == 0 ) ? ' class="not-patrolled"' : '';
368 $s[] = "<li{$attr}>{$format}</li>\n";
369 }
370 }
371
372 $dbr->freeResult( $res );
373 if ( ! $this->listoutput )
374 $s[] = $this->closeList();
375 $str = $this->listoutput ? $wgContLang->listToText( $s ) : implode( '', $s );
376 $wgOut->addHTML( $str );
377 }
378 if($shownavigation) {
379 $wgOut->addHTML( "<p>{$sl}</p>\n" );
380 }
381 return $num;
382 }
383
384 function openList( $offset ) {
385 return "<ol start='" . ( $offset + 1 ) . "' class='special'>";
386 }
387
388 function closeList() {
389 return '</ol>';
390 }
391
392 /**
393 * Do any necessary preprocessing of the result object.
394 * You should pass this by reference: &$db , &$res [although probably no longer necessary in PHP5]
395 */
396 function preprocessResults( &$db, &$res ) {}
397
398 /**
399 * Similar to above, but packaging in a syndicated feed instead of a web page
400 */
401 function doFeed( $class = '', $limit = 50 ) {
402 global $wgFeedClasses;
403
404 if( isset($wgFeedClasses[$class]) ) {
405 $feed = new $wgFeedClasses[$class](
406 $this->feedTitle(),
407 $this->feedDesc(),
408 $this->feedUrl() );
409 $feed->outHeader();
410
411 $dbr = wfGetDB( DB_SLAVE );
412 $sql = $this->getSQL() . $this->getOrder();
413 $sql = $dbr->limitResult( $sql, $limit, 0 );
414 $res = $dbr->query( $sql, 'QueryPage::doFeed' );
415 while( $obj = $dbr->fetchObject( $res ) ) {
416 $item = $this->feedResult( $obj );
417 if( $item ) $feed->outItem( $item );
418 }
419 $dbr->freeResult( $res );
420
421 $feed->outFooter();
422 return true;
423 } else {
424 return false;
425 }
426 }
427
428 /**
429 * Override for custom handling. If the titles/links are ok, just do
430 * feedItemDesc()
431 */
432 function feedResult( $row ) {
433 if( !isset( $row->title ) ) {
434 return NULL;
435 }
436 $title = Title::MakeTitle( intval( $row->namespace ), $row->title );
437 if( $title ) {
438 $date = isset( $row->timestamp ) ? $row->timestamp : '';
439 $comments = '';
440 if( $title ) {
441 $talkpage = $title->getTalkPage();
442 $comments = $talkpage->getFullURL();
443 }
444
445 return new FeedItem(
446 $title->getPrefixedText(),
447 $this->feedItemDesc( $row ),
448 $title->getFullURL(),
449 $date,
450 $this->feedItemAuthor( $row ),
451 $comments);
452 } else {
453 return NULL;
454 }
455 }
456
457 function feedItemDesc( $row ) {
458 return isset( $row->comment ) ? htmlspecialchars( $row->comment ) : '';
459 }
460
461 function feedItemAuthor( $row ) {
462 return isset( $row->user_text ) ? $row->user_text : '';
463 }
464
465 function feedTitle() {
466 global $wgContLanguageCode, $wgSitename;
467 $page = SpecialPage::getPage( $this->getName() );
468 $desc = $page->getDescription();
469 return "$wgSitename - $desc [$wgContLanguageCode]";
470 }
471
472 function feedDesc() {
473 return wfMsg( 'tagline' );
474 }
475
476 function feedUrl() {
477 $title = SpecialPage::getTitleFor( $this->getName() );
478 return $title->getFullURL();
479 }
480 }
481
482 /**
483 * This is a subclass for very simple queries that are just looking for page
484 * titles that match some criteria. It formats each result item as a link to
485 * that page.
486 *
487 */
488 class PageQueryPage extends QueryPage {
489
490 function formatResult( $skin, $result ) {
491 global $wgContLang;
492 $nt = Title::makeTitle( $result->namespace, $result->title );
493 return $skin->makeKnownLinkObj( $nt, htmlspecialchars( $wgContLang->convert( $nt->getPrefixedText() ) ) );
494 }
495 }
496
497 ?>