MediaWiki  REL1_24
SpecialWhatlinkshere.php
Go to the documentation of this file.
00001 <?php
00029 class SpecialWhatLinksHere extends IncludableSpecialPage {
00031     protected $opts;
00032 
00033     protected $selfTitle;
00034 
00036     protected $target;
00037 
00038     protected $limits = array( 20, 50, 100, 250, 500 );
00039 
00040     public function __construct() {
00041         parent::__construct( 'Whatlinkshere' );
00042     }
00043 
00044     function execute( $par ) {
00045         $out = $this->getOutput();
00046 
00047         $this->setHeaders();
00048         $this->outputHeader();
00049 
00050         $opts = new FormOptions();
00051 
00052         $opts->add( 'target', '' );
00053         $opts->add( 'namespace', '', FormOptions::INTNULL );
00054         $opts->add( 'limit', $this->getConfig()->get( 'QueryPageDefaultLimit' ) );
00055         $opts->add( 'from', 0 );
00056         $opts->add( 'back', 0 );
00057         $opts->add( 'hideredirs', false );
00058         $opts->add( 'hidetrans', false );
00059         $opts->add( 'hidelinks', false );
00060         $opts->add( 'hideimages', false );
00061 
00062         $opts->fetchValuesFromRequest( $this->getRequest() );
00063         $opts->validateIntBounds( 'limit', 0, 5000 );
00064 
00065         // Give precedence to subpage syntax
00066         if ( $par !== null ) {
00067             $opts->setValue( 'target', $par );
00068         }
00069 
00070         // Bind to member variable
00071         $this->opts = $opts;
00072 
00073         $this->target = Title::newFromURL( $opts->getValue( 'target' ) );
00074         if ( !$this->target ) {
00075             $out->addHTML( $this->whatlinkshereForm() );
00076 
00077             return;
00078         }
00079 
00080         $this->getSkin()->setRelevantTitle( $this->target );
00081 
00082         $this->selfTitle = $this->getPageTitle( $this->target->getPrefixedDBkey() );
00083 
00084         $out->setPageTitle( $this->msg( 'whatlinkshere-title', $this->target->getPrefixedText() ) );
00085         $out->addBacklinkSubtitle( $this->target );
00086         $this->showIndirectLinks(
00087             0,
00088             $this->target,
00089             $opts->getValue( 'limit' ),
00090             $opts->getValue( 'from' ),
00091             $opts->getValue( 'back' )
00092         );
00093     }
00094 
00102     function showIndirectLinks( $level, $target, $limit, $from = 0, $back = 0 ) {
00103         $out = $this->getOutput();
00104         $dbr = wfGetDB( DB_SLAVE );
00105 
00106         $hidelinks = $this->opts->getValue( 'hidelinks' );
00107         $hideredirs = $this->opts->getValue( 'hideredirs' );
00108         $hidetrans = $this->opts->getValue( 'hidetrans' );
00109         $hideimages = $target->getNamespace() != NS_FILE || $this->opts->getValue( 'hideimages' );
00110 
00111         $fetchlinks = ( !$hidelinks || !$hideredirs );
00112 
00113         // Build query conds in concert for all three tables...
00114         $conds['pagelinks'] = array(
00115             'pl_namespace' => $target->getNamespace(),
00116             'pl_title' => $target->getDBkey(),
00117         );
00118         $conds['templatelinks'] = array(
00119             'tl_namespace' => $target->getNamespace(),
00120             'tl_title' => $target->getDBkey(),
00121         );
00122         $conds['imagelinks'] = array(
00123             'il_to' => $target->getDBkey(),
00124         );
00125 
00126         $useLinkNamespaceDBFields = $this->getConfig()->get( 'UseLinkNamespaceDBFields' );
00127         $namespace = $this->opts->getValue( 'namespace' );
00128         if ( is_int( $namespace ) ) {
00129             if ( $useLinkNamespaceDBFields ) {
00130                 $conds['pagelinks']['pl_from_namespace'] = $namespace;
00131                 $conds['templatelinks']['tl_from_namespace'] = $namespace;
00132                 $conds['imagelinks']['il_from_namespace'] = $namespace;
00133             } else {
00134                 $conds['pagelinks']['page_namespace'] = $namespace;
00135                 $conds['templatelinks']['page_namespace'] = $namespace;
00136                 $conds['imagelinks']['page_namespace'] = $namespace;
00137             }
00138         }
00139 
00140         if ( $from ) {
00141             $conds['templatelinks'][] = "tl_from >= $from";
00142             $conds['pagelinks'][] = "pl_from >= $from";
00143             $conds['imagelinks'][] = "il_from >= $from";
00144         }
00145 
00146         if ( $hideredirs ) {
00147             $conds['pagelinks']['rd_from'] = null;
00148         } elseif ( $hidelinks ) {
00149             $conds['pagelinks'][] = 'rd_from is NOT NULL';
00150         }
00151 
00152         $queryFunc = function ( $dbr, $table, $fromCol ) use ( $conds, $target, $limit, $useLinkNamespaceDBFields ) {
00153             // Read an extra row as an at-end check
00154             $queryLimit = $limit + 1;
00155             $on = array(
00156                 "rd_from = $fromCol",
00157                 'rd_title' => $target->getDBkey(),
00158                 'rd_interwiki = ' . $dbr->addQuotes( '' ) . ' OR rd_interwiki IS NULL'
00159             );
00160             if ( $useLinkNamespaceDBFields ) { // migration check
00161                 $on['rd_namespace'] = $target->getNamespace();
00162             }
00163             // Inner LIMIT is 2X in case of stale backlinks with wrong namespaces
00164             $subQuery = $dbr->selectSqlText(
00165                 array( $table, 'page', 'redirect' ),
00166                 array( $fromCol, 'rd_from' ),
00167                 $conds[$table],
00168                 __CLASS__ . '::showIndirectLinks',
00169                 array( 'ORDER BY' => $fromCol, 'LIMIT' => 2 * $queryLimit ),
00170                 array(
00171                     'page' => array( 'INNER JOIN', "$fromCol = page_id" ),
00172                     'redirect' => array( 'LEFT JOIN', $on )
00173                 )
00174             );
00175             return $dbr->select(
00176                 array( 'page', 'temp_backlink_range' => "($subQuery)" ),
00177                 array( 'page_id', 'page_namespace', 'page_title', 'rd_from' ),
00178                 array(),
00179                 __CLASS__ . '::showIndirectLinks',
00180                 array( 'ORDER BY' => 'page_id', 'LIMIT' => $queryLimit ),
00181                 array( 'page' => array( 'INNER JOIN', "$fromCol = page_id" ) )
00182             );
00183         };
00184 
00185         if ( $fetchlinks ) {
00186             $plRes = $queryFunc( $dbr, 'pagelinks', 'pl_from' );
00187         }
00188 
00189         if ( !$hidetrans ) {
00190             $tlRes = $queryFunc( $dbr, 'templatelinks', 'tl_from' );
00191         }
00192 
00193         if ( !$hideimages ) {
00194             $ilRes = $queryFunc( $dbr, 'imagelinks', 'il_from' );
00195         }
00196 
00197         if ( ( !$fetchlinks || !$plRes->numRows() )
00198             && ( $hidetrans || !$tlRes->numRows() )
00199             && ( $hideimages || !$ilRes->numRows() )
00200         ) {
00201             if ( 0 == $level ) {
00202                 if ( !$this->including() ) {
00203                     $out->addHTML( $this->whatlinkshereForm() );
00204 
00205                     // Show filters only if there are links
00206                     if ( $hidelinks || $hidetrans || $hideredirs || $hideimages ) {
00207                         $out->addHTML( $this->getFilterPanel() );
00208                     }
00209                     $errMsg = is_int( $namespace ) ? 'nolinkshere-ns' : 'nolinkshere';
00210                     $out->addWikiMsg( $errMsg, $this->target->getPrefixedText() );
00211                     $out->setStatusCode( 404 );
00212                 }
00213             }
00214 
00215             return;
00216         }
00217 
00218         // Read the rows into an array and remove duplicates
00219         // templatelinks comes second so that the templatelinks row overwrites the
00220         // pagelinks row, so we get (inclusion) rather than nothing
00221         if ( $fetchlinks ) {
00222             foreach ( $plRes as $row ) {
00223                 $row->is_template = 0;
00224                 $row->is_image = 0;
00225                 $rows[$row->page_id] = $row;
00226             }
00227         }
00228         if ( !$hidetrans ) {
00229             foreach ( $tlRes as $row ) {
00230                 $row->is_template = 1;
00231                 $row->is_image = 0;
00232                 $rows[$row->page_id] = $row;
00233             }
00234         }
00235         if ( !$hideimages ) {
00236             foreach ( $ilRes as $row ) {
00237                 $row->is_template = 0;
00238                 $row->is_image = 1;
00239                 $rows[$row->page_id] = $row;
00240             }
00241         }
00242 
00243         // Sort by key and then change the keys to 0-based indices
00244         ksort( $rows );
00245         $rows = array_values( $rows );
00246 
00247         $numRows = count( $rows );
00248 
00249         // Work out the start and end IDs, for prev/next links
00250         if ( $numRows > $limit ) {
00251             // More rows available after these ones
00252             // Get the ID from the last row in the result set
00253             $nextId = $rows[$limit]->page_id;
00254             // Remove undisplayed rows
00255             $rows = array_slice( $rows, 0, $limit );
00256         } else {
00257             // No more rows after
00258             $nextId = false;
00259         }
00260         $prevId = $from;
00261 
00262         if ( $level == 0 ) {
00263             if ( !$this->including() ) {
00264                 $out->addHTML( $this->whatlinkshereForm() );
00265                 $out->addHTML( $this->getFilterPanel() );
00266                 $out->addWikiMsg( 'linkshere', $this->target->getPrefixedText() );
00267 
00268                 $prevnext = $this->getPrevNext( $prevId, $nextId );
00269                 $out->addHTML( $prevnext );
00270             }
00271         }
00272         $out->addHTML( $this->listStart( $level ) );
00273         foreach ( $rows as $row ) {
00274             $nt = Title::makeTitle( $row->page_namespace, $row->page_title );
00275 
00276             if ( $row->rd_from && $level < 2 ) {
00277                 $out->addHTML( $this->listItem( $row, $nt, $target, true ) );
00278                 $this->showIndirectLinks( $level + 1, $nt, $this->getConfig()->get( 'MaxRedirectLinksRetrieved' ) );
00279                 $out->addHTML( Xml::closeElement( 'li' ) );
00280             } else {
00281                 $out->addHTML( $this->listItem( $row, $nt, $target ) );
00282             }
00283         }
00284 
00285         $out->addHTML( $this->listEnd() );
00286 
00287         if ( $level == 0 ) {
00288             if ( !$this->including() ) {
00289                 $out->addHTML( $prevnext );
00290             }
00291         }
00292     }
00293 
00294     protected function listStart( $level ) {
00295         return Xml::openElement( 'ul', ( $level ? array() : array( 'id' => 'mw-whatlinkshere-list' ) ) );
00296     }
00297 
00298     protected function listItem( $row, $nt, $target, $notClose = false ) {
00299         $dirmark = $this->getLanguage()->getDirMark();
00300 
00301         # local message cache
00302         static $msgcache = null;
00303         if ( $msgcache === null ) {
00304             static $msgs = array( 'isredirect', 'istemplate', 'semicolon-separator',
00305                 'whatlinkshere-links', 'isimage' );
00306             $msgcache = array();
00307             foreach ( $msgs as $msg ) {
00308                 $msgcache[$msg] = $this->msg( $msg )->escaped();
00309             }
00310         }
00311 
00312         if ( $row->rd_from ) {
00313             $query = array( 'redirect' => 'no' );
00314         } else {
00315             $query = array();
00316         }
00317 
00318         $link = Linker::linkKnown(
00319             $nt,
00320             null,
00321             array(),
00322             $query
00323         );
00324 
00325         // Display properties (redirect or template)
00326         $propsText = '';
00327         $props = array();
00328         if ( $row->rd_from ) {
00329             $props[] = $msgcache['isredirect'];
00330         }
00331         if ( $row->is_template ) {
00332             $props[] = $msgcache['istemplate'];
00333         }
00334         if ( $row->is_image ) {
00335             $props[] = $msgcache['isimage'];
00336         }
00337 
00338         wfRunHooks( 'WhatLinksHereProps', array( $row, $nt, $target, &$props ) );
00339 
00340         if ( count( $props ) ) {
00341             $propsText = $this->msg( 'parentheses' )
00342                 ->rawParams( implode( $msgcache['semicolon-separator'], $props ) )->escaped();
00343         }
00344 
00345         # Space for utilities links, with a what-links-here link provided
00346         $wlhLink = $this->wlhLink( $nt, $msgcache['whatlinkshere-links'] );
00347         $wlh = Xml::wrapClass(
00348             $this->msg( 'parentheses' )->rawParams( $wlhLink )->escaped(),
00349             'mw-whatlinkshere-tools'
00350         );
00351 
00352         return $notClose ?
00353             Xml::openElement( 'li' ) . "$link $propsText $dirmark $wlh\n" :
00354             Xml::tags( 'li', null, "$link $propsText $dirmark $wlh" ) . "\n";
00355     }
00356 
00357     protected function listEnd() {
00358         return Xml::closeElement( 'ul' );
00359     }
00360 
00361     protected function wlhLink( Title $target, $text ) {
00362         static $title = null;
00363         if ( $title === null ) {
00364             $title = $this->getPageTitle();
00365         }
00366 
00367         return Linker::linkKnown(
00368             $title,
00369             $text,
00370             array(),
00371             array( 'target' => $target->getPrefixedText() )
00372         );
00373     }
00374 
00375     function makeSelfLink( $text, $query ) {
00376         return Linker::linkKnown(
00377             $this->selfTitle,
00378             $text,
00379             array(),
00380             $query
00381         );
00382     }
00383 
00384     function getPrevNext( $prevId, $nextId ) {
00385         $currentLimit = $this->opts->getValue( 'limit' );
00386         $prev = $this->msg( 'whatlinkshere-prev' )->numParams( $currentLimit )->escaped();
00387         $next = $this->msg( 'whatlinkshere-next' )->numParams( $currentLimit )->escaped();
00388 
00389         $changed = $this->opts->getChangedValues();
00390         unset( $changed['target'] ); // Already in the request title
00391 
00392         if ( 0 != $prevId ) {
00393             $overrides = array( 'from' => $this->opts->getValue( 'back' ) );
00394             $prev = $this->makeSelfLink( $prev, array_merge( $changed, $overrides ) );
00395         }
00396         if ( 0 != $nextId ) {
00397             $overrides = array( 'from' => $nextId, 'back' => $prevId );
00398             $next = $this->makeSelfLink( $next, array_merge( $changed, $overrides ) );
00399         }
00400 
00401         $limitLinks = array();
00402         $lang = $this->getLanguage();
00403         foreach ( $this->limits as $limit ) {
00404             $prettyLimit = htmlspecialchars( $lang->formatNum( $limit ) );
00405             $overrides = array( 'limit' => $limit );
00406             $limitLinks[] = $this->makeSelfLink( $prettyLimit, array_merge( $changed, $overrides ) );
00407         }
00408 
00409         $nums = $lang->pipeList( $limitLinks );
00410 
00411         return $this->msg( 'viewprevnext' )->rawParams( $prev, $next, $nums )->escaped();
00412     }
00413 
00414     function whatlinkshereForm() {
00415         // We get nicer value from the title object
00416         $this->opts->consumeValue( 'target' );
00417         // Reset these for new requests
00418         $this->opts->consumeValues( array( 'back', 'from' ) );
00419 
00420         $target = $this->target ? $this->target->getPrefixedText() : '';
00421         $namespace = $this->opts->consumeValue( 'namespace' );
00422 
00423         # Build up the form
00424         $f = Xml::openElement( 'form', array( 'action' => wfScript() ) );
00425 
00426         # Values that should not be forgotten
00427         $f .= Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() );
00428         foreach ( $this->opts->getUnconsumedValues() as $name => $value ) {
00429             $f .= Html::hidden( $name, $value );
00430         }
00431 
00432         $f .= Xml::fieldset( $this->msg( 'whatlinkshere' )->text() );
00433 
00434         # Target input
00435         $f .= Xml::inputLabel( $this->msg( 'whatlinkshere-page' )->text(), 'target',
00436             'mw-whatlinkshere-target', 40, $target );
00437 
00438         $f .= ' ';
00439 
00440         # Namespace selector
00441         $f .= Html::namespaceSelector(
00442             array(
00443                 'selected' => $namespace,
00444                 'all' => '',
00445                 'label' => $this->msg( 'namespace' )->text()
00446             ), array(
00447                 'name' => 'namespace',
00448                 'id' => 'namespace',
00449                 'class' => 'namespaceselector',
00450             )
00451         );
00452 
00453         $f .= ' ';
00454 
00455         # Submit
00456         $f .= Xml::submitButton( $this->msg( 'allpagessubmit' )->text() );
00457 
00458         # Close
00459         $f .= Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' ) . "\n";
00460 
00461         return $f;
00462     }
00463 
00469     function getFilterPanel() {
00470         $show = $this->msg( 'show' )->escaped();
00471         $hide = $this->msg( 'hide' )->escaped();
00472 
00473         $changed = $this->opts->getChangedValues();
00474         unset( $changed['target'] ); // Already in the request title
00475 
00476         $links = array();
00477         $types = array( 'hidetrans', 'hidelinks', 'hideredirs' );
00478         if ( $this->target->getNamespace() == NS_FILE ) {
00479             $types[] = 'hideimages';
00480         }
00481 
00482         // Combined message keys: 'whatlinkshere-hideredirs', 'whatlinkshere-hidetrans',
00483         // 'whatlinkshere-hidelinks', 'whatlinkshere-hideimages'
00484         // To be sure they will be found by grep
00485         foreach ( $types as $type ) {
00486             $chosen = $this->opts->getValue( $type );
00487             $msg = $chosen ? $show : $hide;
00488             $overrides = array( $type => !$chosen );
00489             $links[] = $this->msg( "whatlinkshere-{$type}" )->rawParams(
00490                 $this->makeSelfLink( $msg, array_merge( $changed, $overrides ) ) )->escaped();
00491         }
00492 
00493         return Xml::fieldset(
00494             $this->msg( 'whatlinkshere-filters' )->text(),
00495             $this->getLanguage()->pipeList( $links )
00496         );
00497     }
00498 
00499     protected function getGroupName() {
00500         return 'pagetools';
00501     }
00502 }