QueryPage.php 16 KB

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