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