SpecialWhatlinkshere.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. <?php
  2. /**
  3. * @todo Use some variant of Pager or something; the pagination here is lousy.
  4. *
  5. * @file
  6. * @ingroup SpecialPage
  7. */
  8. /**
  9. * Entry point
  10. * @param $par String: An article name ??
  11. */
  12. function wfSpecialWhatlinkshere($par = NULL) {
  13. global $wgRequest;
  14. $page = new WhatLinksHerePage( $wgRequest, $par );
  15. $page->execute();
  16. }
  17. /**
  18. * implements Special:Whatlinkshere
  19. * @ingroup SpecialPage
  20. */
  21. class WhatLinksHerePage {
  22. // Stored data
  23. protected $par;
  24. // Stored objects
  25. protected $opts, $target, $selfTitle;
  26. // Stored globals
  27. protected $skin, $request;
  28. protected $limits = array( 20, 50, 100, 250, 500 );
  29. function WhatLinksHerePage( $request, $par = null ) {
  30. global $wgUser;
  31. $this->request = $request;
  32. $this->skin = $wgUser->getSkin();
  33. $this->par = $par;
  34. }
  35. function execute() {
  36. global $wgOut;
  37. $opts = new FormOptions();
  38. $opts->add( 'target', '' );
  39. $opts->add( 'namespace', '', FormOptions::INTNULL );
  40. $opts->add( 'limit', 50 );
  41. $opts->add( 'from', 0 );
  42. $opts->add( 'back', 0 );
  43. $opts->add( 'hideredirs', false );
  44. $opts->add( 'hidetrans', false );
  45. $opts->add( 'hidelinks', false );
  46. $opts->add( 'hideimages', false );
  47. $opts->fetchValuesFromRequest( $this->request );
  48. $opts->validateIntBounds( 'limit', 0, 5000 );
  49. // Give precedence to subpage syntax
  50. if ( isset($this->par) ) {
  51. $opts->setValue( 'target', $this->par );
  52. }
  53. // Bind to member variable
  54. $this->opts = $opts;
  55. $this->target = Title::newFromURL( $opts->getValue( 'target' ) );
  56. if( !$this->target ) {
  57. $wgOut->addHTML( $this->whatlinkshereForm() );
  58. return;
  59. }
  60. $this->selfTitle = SpecialPage::getTitleFor( 'Whatlinkshere', $this->target->getPrefixedDBkey() );
  61. $wgOut->setPageTitle( wfMsg( 'whatlinkshere-title', $this->target->getPrefixedText() ) );
  62. $wgOut->setSubtitle( wfMsg( 'whatlinkshere-backlink', $this->skin->link( $this->target, $this->target->getPrefixedText(), array(), array( 'redirect' => 'no' ) ) ) );
  63. $this->showIndirectLinks( 0, $this->target, $opts->getValue( 'limit' ),
  64. $opts->getValue( 'from' ), $opts->getValue( 'back' ) );
  65. }
  66. /**
  67. * @param $level int Recursion level
  68. * @param $target Title Target title
  69. * @param $limit int Number of entries to display
  70. * @param $from Title Display from this article ID
  71. * @param $back Title Display from this article ID at backwards scrolling
  72. * @private
  73. */
  74. function showIndirectLinks( $level, $target, $limit, $from = 0, $back = 0 ) {
  75. global $wgOut, $wgMaxRedirectLinksRetrieved;
  76. $dbr = wfGetDB( DB_SLAVE );
  77. $options = array();
  78. $hidelinks = $this->opts->getValue( 'hidelinks' );
  79. $hideredirs = $this->opts->getValue( 'hideredirs' );
  80. $hidetrans = $this->opts->getValue( 'hidetrans' );
  81. $hideimages = $target->getNamespace() != NS_FILE || $this->opts->getValue( 'hideimages' );
  82. $fetchlinks = (!$hidelinks || !$hideredirs);
  83. // Make the query
  84. $plConds = array(
  85. 'page_id=pl_from',
  86. 'pl_namespace' => $target->getNamespace(),
  87. 'pl_title' => $target->getDBkey(),
  88. );
  89. if( $hideredirs ) {
  90. $plConds['page_is_redirect'] = 0;
  91. } elseif( $hidelinks ) {
  92. $plConds['page_is_redirect'] = 1;
  93. }
  94. $tlConds = array(
  95. 'page_id=tl_from',
  96. 'tl_namespace' => $target->getNamespace(),
  97. 'tl_title' => $target->getDBkey(),
  98. );
  99. $ilConds = array(
  100. 'page_id=il_from',
  101. 'il_to' => $target->getDBkey(),
  102. );
  103. $namespace = $this->opts->getValue( 'namespace' );
  104. if ( is_int($namespace) ) {
  105. $plConds['page_namespace'] = $namespace;
  106. $tlConds['page_namespace'] = $namespace;
  107. $ilConds['page_namespace'] = $namespace;
  108. }
  109. if ( $from ) {
  110. $tlConds[] = "tl_from >= $from";
  111. $plConds[] = "pl_from >= $from";
  112. $ilConds[] = "il_from >= $from";
  113. }
  114. // Read an extra row as an at-end check
  115. $queryLimit = $limit + 1;
  116. // Enforce join order, sometimes namespace selector may
  117. // trigger filesorts which are far less efficient than scanning many entries
  118. $options[] = 'STRAIGHT_JOIN';
  119. $options['LIMIT'] = $queryLimit;
  120. $fields = array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' );
  121. if( $fetchlinks ) {
  122. $options['ORDER BY'] = 'pl_from';
  123. $plRes = $dbr->select( array( 'pagelinks', 'page' ), $fields,
  124. $plConds, __METHOD__, $options );
  125. }
  126. if( !$hidetrans ) {
  127. $options['ORDER BY'] = 'tl_from';
  128. $tlRes = $dbr->select( array( 'templatelinks', 'page' ), $fields,
  129. $tlConds, __METHOD__, $options );
  130. }
  131. if( !$hideimages ) {
  132. $options['ORDER BY'] = 'il_from';
  133. $ilRes = $dbr->select( array( 'imagelinks', 'page' ), $fields,
  134. $ilConds, __METHOD__, $options );
  135. }
  136. if( ( !$fetchlinks || !$dbr->numRows($plRes) ) && ( $hidetrans || !$dbr->numRows($tlRes) ) && ( $hideimages || !$dbr->numRows($ilRes) ) ) {
  137. if ( 0 == $level ) {
  138. $wgOut->addHTML( $this->whatlinkshereForm() );
  139. // Show filters only if there are links
  140. if( $hidelinks || $hidetrans || $hideredirs || $hideimages )
  141. $wgOut->addHTML( $this->getFilterPanel() );
  142. $errMsg = is_int($namespace) ? 'nolinkshere-ns' : 'nolinkshere';
  143. $wgOut->addWikiMsg( $errMsg, $this->target->getPrefixedText() );
  144. }
  145. return;
  146. }
  147. // Read the rows into an array and remove duplicates
  148. // templatelinks comes second so that the templatelinks row overwrites the
  149. // pagelinks row, so we get (inclusion) rather than nothing
  150. if( $fetchlinks ) {
  151. while ( $row = $dbr->fetchObject( $plRes ) ) {
  152. $row->is_template = 0;
  153. $row->is_image = 0;
  154. $rows[$row->page_id] = $row;
  155. }
  156. $dbr->freeResult( $plRes );
  157. }
  158. if( !$hidetrans ) {
  159. while ( $row = $dbr->fetchObject( $tlRes ) ) {
  160. $row->is_template = 1;
  161. $row->is_image = 0;
  162. $rows[$row->page_id] = $row;
  163. }
  164. $dbr->freeResult( $tlRes );
  165. }
  166. if( !$hideimages ) {
  167. while ( $row = $dbr->fetchObject( $ilRes ) ) {
  168. $row->is_template = 0;
  169. $row->is_image = 1;
  170. $rows[$row->page_id] = $row;
  171. }
  172. $dbr->freeResult( $ilRes );
  173. }
  174. // Sort by key and then change the keys to 0-based indices
  175. ksort( $rows );
  176. $rows = array_values( $rows );
  177. $numRows = count( $rows );
  178. // Work out the start and end IDs, for prev/next links
  179. if ( $numRows > $limit ) {
  180. // More rows available after these ones
  181. // Get the ID from the last row in the result set
  182. $nextId = $rows[$limit]->page_id;
  183. // Remove undisplayed rows
  184. $rows = array_slice( $rows, 0, $limit );
  185. } else {
  186. // No more rows after
  187. $nextId = false;
  188. }
  189. $prevId = $from;
  190. if ( $level == 0 ) {
  191. $wgOut->addHTML( $this->whatlinkshereForm() );
  192. $wgOut->addHTML( $this->getFilterPanel() );
  193. $wgOut->addWikiMsg( 'linkshere', $this->target->getPrefixedText() );
  194. $prevnext = $this->getPrevNext( $prevId, $nextId );
  195. $wgOut->addHTML( $prevnext );
  196. }
  197. $wgOut->addHTML( $this->listStart() );
  198. foreach ( $rows as $row ) {
  199. $nt = Title::makeTitle( $row->page_namespace, $row->page_title );
  200. if ( $row->page_is_redirect && $level < 2 ) {
  201. $wgOut->addHTML( $this->listItem( $row, $nt, true ) );
  202. $this->showIndirectLinks( $level + 1, $nt, $wgMaxRedirectLinksRetrieved );
  203. $wgOut->addHTML( Xml::closeElement( 'li' ) );
  204. } else {
  205. $wgOut->addHTML( $this->listItem( $row, $nt ) );
  206. }
  207. }
  208. $wgOut->addHTML( $this->listEnd() );
  209. if( $level == 0 ) {
  210. $wgOut->addHTML( $prevnext );
  211. }
  212. }
  213. protected function listStart() {
  214. return Xml::openElement( 'ul', array ( 'id' => 'mw-whatlinkshere-list' ) );
  215. }
  216. protected function listItem( $row, $nt, $notClose = false ) {
  217. # local message cache
  218. static $msgcache = null;
  219. if ( $msgcache === null ) {
  220. static $msgs = array( 'isredirect', 'istemplate', 'semicolon-separator',
  221. 'whatlinkshere-links', 'isimage' );
  222. $msgcache = array();
  223. foreach ( $msgs as $msg ) {
  224. $msgcache[$msg] = wfMsgExt( $msg, array( 'escapenoentities' ) );
  225. }
  226. }
  227. $suppressRedirect = $row->page_is_redirect ? 'redirect=no' : '';
  228. $link = $this->skin->makeKnownLinkObj( $nt, '', $suppressRedirect );
  229. // Display properties (redirect or template)
  230. $propsText = '';
  231. $props = array();
  232. if ( $row->page_is_redirect )
  233. $props[] = $msgcache['isredirect'];
  234. if ( $row->is_template )
  235. $props[] = $msgcache['istemplate'];
  236. if( $row->is_image )
  237. $props[] = $msgcache['isimage'];
  238. if ( count( $props ) ) {
  239. $propsText = '(' . implode( $msgcache['semicolon-separator'], $props ) . ')';
  240. }
  241. # Space for utilities links, with a what-links-here link provided
  242. $wlhLink = $this->wlhLink( $nt, $msgcache['whatlinkshere-links'] );
  243. $wlh = Xml::wrapClass( "($wlhLink)", 'mw-whatlinkshere-tools' );
  244. return $notClose ?
  245. Xml::openElement( 'li' ) . "$link $propsText $wlh\n" :
  246. Xml::tags( 'li', null, "$link $propsText $wlh" ) . "\n";
  247. }
  248. protected function listEnd() {
  249. return Xml::closeElement( 'ul' );
  250. }
  251. protected function wlhLink( Title $target, $text ) {
  252. static $title = null;
  253. if ( $title === null )
  254. $title = SpecialPage::getTitleFor( 'Whatlinkshere' );
  255. $targetText = $target->getPrefixedUrl();
  256. return $this->skin->makeKnownLinkObj( $title, $text, 'target=' . $targetText );
  257. }
  258. function makeSelfLink( $text, $query ) {
  259. return $this->skin->makeKnownLinkObj( $this->selfTitle, $text, $query );
  260. }
  261. function getPrevNext( $prevId, $nextId ) {
  262. global $wgLang;
  263. $currentLimit = $this->opts->getValue( 'limit' );
  264. $fmtLimit = $wgLang->formatNum( $currentLimit );
  265. $prev = wfMsgExt( 'whatlinkshere-prev', array( 'parsemag', 'escape' ), $fmtLimit );
  266. $next = wfMsgExt( 'whatlinkshere-next', array( 'parsemag', 'escape' ), $fmtLimit );
  267. $changed = $this->opts->getChangedValues();
  268. unset($changed['target']); // Already in the request title
  269. if ( 0 != $prevId ) {
  270. $overrides = array( 'from' => $this->opts->getValue( 'back' ) );
  271. $prev = $this->makeSelfLink( $prev, wfArrayToCGI( $overrides, $changed ) );
  272. }
  273. if ( 0 != $nextId ) {
  274. $overrides = array( 'from' => $nextId, 'back' => $prevId );
  275. $next = $this->makeSelfLink( $next, wfArrayToCGI( $overrides, $changed ) );
  276. }
  277. $limitLinks = array();
  278. foreach ( $this->limits as $limit ) {
  279. $prettyLimit = $wgLang->formatNum( $limit );
  280. $overrides = array( 'limit' => $limit );
  281. $limitLinks[] = $this->makeSelfLink( $prettyLimit, wfArrayToCGI( $overrides, $changed ) );
  282. }
  283. $nums = $wgLang->pipeList( $limitLinks );
  284. return wfMsgHtml( 'viewprevnext', $prev, $next, $nums );
  285. }
  286. function whatlinkshereForm() {
  287. global $wgScript, $wgTitle;
  288. // We get nicer value from the title object
  289. $this->opts->consumeValue( 'target' );
  290. // Reset these for new requests
  291. $this->opts->consumeValues( array( 'back', 'from' ) );
  292. $target = $this->target ? $this->target->getPrefixedText() : '';
  293. $namespace = $this->opts->consumeValue( 'namespace' );
  294. # Build up the form
  295. $f = Xml::openElement( 'form', array( 'action' => $wgScript ) );
  296. # Values that should not be forgotten
  297. $f .= Xml::hidden( 'title', $wgTitle->getPrefixedText() );
  298. foreach ( $this->opts->getUnconsumedValues() as $name => $value ) {
  299. $f .= Xml::hidden( $name, $value );
  300. }
  301. $f .= Xml::fieldset( wfMsg( 'whatlinkshere' ) );
  302. # Target input
  303. $f .= Xml::inputLabel( wfMsg( 'whatlinkshere-page' ), 'target',
  304. 'mw-whatlinkshere-target', 40, $target );
  305. $f .= ' ';
  306. # Namespace selector
  307. $f .= Xml::label( wfMsg( 'namespace' ), 'namespace' ) . '&nbsp;' .
  308. Xml::namespaceSelector( $namespace, '' );
  309. $f .= ' ';
  310. # Submit
  311. $f .= Xml::submitButton( wfMsg( 'allpagessubmit' ) );
  312. # Close
  313. $f .= Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' ) . "\n";
  314. return $f;
  315. }
  316. function getFilterPanel() {
  317. global $wgLang;
  318. $show = wfMsgHtml( 'show' );
  319. $hide = wfMsgHtml( 'hide' );
  320. $changed = $this->opts->getChangedValues();
  321. unset($changed['target']); // Already in the request title
  322. $links = array();
  323. $types = array( 'hidetrans', 'hidelinks', 'hideredirs' );
  324. if( $this->target->getNamespace() == NS_FILE )
  325. $types[] = 'hideimages';
  326. foreach( $types as $type ) {
  327. $chosen = $this->opts->getValue( $type );
  328. $msg = wfMsgHtml( "whatlinkshere-{$type}", $chosen ? $show : $hide );
  329. $overrides = array( $type => !$chosen );
  330. $links[] = $this->makeSelfLink( $msg, wfArrayToCGI( $overrides, $changed ) );
  331. }
  332. return Xml::fieldset( wfMsg( 'whatlinkshere-filters' ), $wgLang->pipeList( $links ) );
  333. }
  334. }