From 78accabd9fad2a7b22879ce595a24dc208a8e02b Mon Sep 17 00:00:00 2001 From: Aaron Schulz Date: Mon, 26 May 2008 21:32:53 +0000 Subject: [PATCH] * Convert to using Pager and remove a bunch of ugly code. The order and distinctness is now all done by the DB * Use linkbatch to avoid some O(n) query spam * Break some long lines --- includes/SpecialWhatlinkshere.php | 405 ++++++++++++++++-------------- 1 file changed, 216 insertions(+), 189 deletions(-) diff --git a/includes/SpecialWhatlinkshere.php b/includes/SpecialWhatlinkshere.php index 2c520366e6..346a83b6e4 100644 --- a/includes/SpecialWhatlinkshere.php +++ b/includes/SpecialWhatlinkshere.php @@ -46,16 +46,12 @@ class WhatLinksHerePage { $opts->add( 'target', '' ); $opts->add( 'namespace', '', FormOptions::INTNULL ); - $opts->add( 'limit', 50 ); - $opts->add( 'from', 0 ); - $opts->add( 'back', 0 ); $opts->add( 'hideredirs', false ); $opts->add( 'hidetrans', false ); $opts->add( 'hidelinks', false ); $opts->add( 'hideimages', false ); $opts->fetchValuesFromRequest( $this->request ); - $opts->validateIntBounds( 'limit', 0, 5000 ); // Give precedence to subpage syntax if ( isset($this->par) ) { @@ -76,22 +72,22 @@ class WhatLinksHerePage { $wgOut->setPageTitle( wfMsgExt( 'whatlinkshere-title', 'escape', $this->target->getPrefixedText() ) ); $wgOut->setSubtitle( wfMsgHtml( 'linklistsub' ) ); - $wgOut->addHTML( wfMsgExt( 'whatlinkshere-barrow', array( 'escapenoentities') ) . ' ' .$this->skin->makeLinkObj($this->target, '', 'redirect=no' )."
\n"); + $wgOut->addHTML( wfMsgExt( 'whatlinkshere-barrow', array( 'escapenoentities') ) . ' ' . + $this->skin->makeLinkObj($this->target, '', 'redirect=no' )."
\n"); - $this->showIndirectLinks( 0, $this->target, $opts->getValue( 'limit' ), - $opts->getValue( 'from' ), $opts->getValue( 'back' ) ); + $this->showIndirectLinks( 0, $this->target ); } /** * @param int $level Recursion level * @param Title $target Target title - * @param int $limit Number of entries to display - * @param Title $from Display from this article ID - * @param Title $back Display from this article ID at backwards scrolling - * @private + * @param int $limit Result limit (used for redirects) + * For the top level, this outputs the link list to $wgOut. + * For sublevels, it returns the string of the HTML. + * @returns mixed (true/string) */ - function showIndirectLinks( $level, $target, $limit, $from = 0, $back = 0 ) { - global $wgOut, $wgMaxRedirectLinksRetrieved; + function showIndirectLinks( $level, $target, $limit=false ) { + global $wgOut; $dbr = wfGetDB( DB_SLAVE ); $options = array(); @@ -101,72 +97,13 @@ class WhatLinksHerePage { $hideimages = $target->getNamespace() != NS_IMAGE || $this->opts->getValue( 'hideimages' ); $fetchlinks = (!$hidelinks || !$hideredirs); - - // Make the query - $plConds = array( - 'page_id=pl_from', - 'pl_namespace' => $target->getNamespace(), - 'pl_title' => $target->getDBkey(), - ); - if( $hideredirs ) { - $plConds['page_is_redirect'] = 0; - } elseif( $hidelinks ) { - $plConds['page_is_redirect'] = 1; - } - - $tlConds = array( - 'page_id=tl_from', - 'tl_namespace' => $target->getNamespace(), - 'tl_title' => $target->getDBkey(), - ); - - $ilConds = array( - 'page_id=il_from', - 'il_to' => $target->getDBkey(), - ); - + $namespace = $this->opts->getValue( 'namespace' ); - if ( is_int($namespace) ) { - $plConds['page_namespace'] = $namespace; - $tlConds['page_namespace'] = $namespace; - $ilConds['page_namespace'] = $namespace; - } - - if ( $from ) { - $tlConds[] = "tl_from >= $from"; - $plConds[] = "pl_from >= $from"; - $ilConds[] = "il_from >= $from"; - } - - // Read an extra row as an at-end check - $queryLimit = $limit + 1; - - // Enforce join order, sometimes namespace selector may - // trigger filesorts which are far less efficient than scanning many entries - $options[] = 'STRAIGHT_JOIN'; - - $options['LIMIT'] = $queryLimit; - $fields = array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ); - - if( $fetchlinks ) { - $options['ORDER BY'] = 'pl_from'; - $plRes = $dbr->select( array( 'pagelinks', 'page' ), $fields, - $plConds, __METHOD__, $options ); - } - - if( !$hidetrans ) { - $options['ORDER BY'] = 'tl_from'; - $tlRes = $dbr->select( array( 'templatelinks', 'page' ), $fields, - $tlConds, __METHOD__, $options ); - } - - if( !$hideimages ) { - $options['ORDER BY'] = 'il_from'; - $ilRes = $dbr->select( array( 'imagelinks', 'page' ), $fields, - $ilConds, __METHOD__, $options ); - } + + $pager = new WhatLinksHerePager( $this, $target, $level, $limit, $namespace, + $hidelinks, $hideredirs, $hidetrans, $hideimages ); - if( ( !$fetchlinks || !$dbr->numRows($plRes) ) && ( $hidetrans || !$dbr->numRows($tlRes) ) && ( $hideimages || !$dbr->numRows($ilRes) ) ) { + if( !$pager->getNumRows() ) { if ( 0 == $level ) { $wgOut->addHTML( $this->whatlinkshereForm() ); $errMsg = is_int($namespace) ? 'nolinkshere-ns' : 'nolinkshere'; @@ -177,89 +114,31 @@ class WhatLinksHerePage { } return; } - - // Read the rows into an array and remove duplicates - // templatelinks comes second so that the templatelinks row overwrites the - // pagelinks row, so we get (inclusion) rather than nothing - if( $fetchlinks ) { - while ( $row = $dbr->fetchObject( $plRes ) ) { - $row->is_template = 0; - $row->is_image = 0; - $rows[$row->page_id] = $row; - } - $dbr->freeResult( $plRes ); - - } - if( !$hidetrans ) { - while ( $row = $dbr->fetchObject( $tlRes ) ) { - $row->is_template = 1; - $row->is_image = 0; - $rows[$row->page_id] = $row; - } - $dbr->freeResult( $tlRes ); - } - if( !$hideimages ) { - while ( $row = $dbr->fetchObject( $ilRes ) ) { - $row->is_template = 0; - $row->is_image = 1; - $rows[$row->page_id] = $row; - } - $dbr->freeResult( $ilRes ); - } - - // Sort by key and then change the keys to 0-based indices - ksort( $rows ); - $rows = array_values( $rows ); - - $numRows = count( $rows ); - - // Work out the start and end IDs, for prev/next links - if ( $numRows > $limit ) { - // More rows available after these ones - // Get the ID from the last row in the result set - $nextId = $rows[$limit]->page_id; - // Remove undisplayed rows - $rows = array_slice( $rows, 0, $limit ); - } else { - // No more rows after - $nextId = false; + + // If this is a sub-level, return out a string + if( $level > 0 ) { + return $pager->getBody(); } - $prevId = $from; - if ( $level == 0 ) { + if( $level == 0 ) { $wgOut->addHTML( $this->whatlinkshereForm() ); $wgOut->addHTML( $this->getFilterPanel() ); $wgOut->addWikiMsg( 'linkshere', $this->target->getPrefixedText() ); - - $prevnext = $this->getPrevNext( $prevId, $nextId ); - $wgOut->addHTML( $prevnext ); + $wgOut->addHTML( '
' ); + // Add nav links only to top list + $wgOut->addHTML( $pager->getNavigationBar() ); } - - $wgOut->addHTML( $this->listStart() ); - foreach ( $rows as $row ) { - $nt = Title::makeTitle( $row->page_namespace, $row->page_title ); - - if ( $row->page_is_redirect && $level < 2 ) { - $wgOut->addHTML( $this->listItem( $row, $nt, true ) ); - $this->showIndirectLinks( $level + 1, $nt, $wgMaxRedirectLinksRetrieved ); - $wgOut->addHTML( Xml::closeElement( 'li' ) ); - } else { - $wgOut->addHTML( $this->listItem( $row, $nt ) ); - } - } - - $wgOut->addHTML( $this->listEnd() ); - - if( $level == 0 ) { - $wgOut->addHTML( $prevnext ); + + $wgOut->addHTML( $pager->getBody() ); + + if ( $level == 0 ) { + // Add nav links only to top list + $wgOut->addHTML( $pager->getNavigationBar() ); } + return true; } - protected function listStart() { - return Xml::openElement( 'ul' ); - } - - protected function listItem( $row, $nt, $notClose = false ) { + public function listItem( $row, $nt, $notClose = false ) { # local message cache static $msgcache = null; if ( $msgcache === null ) { @@ -281,7 +160,7 @@ class WhatLinksHerePage { $props[] = $msgcache['isredirect']; if ( $row->is_template ) $props[] = $msgcache['istemplate']; - if( $row->is_image ) + if ( $row->is_image ) $props[] = $msgcache['isimage']; if ( count( $props ) ) { @@ -297,10 +176,6 @@ class WhatLinksHerePage { Xml::tags( 'li', null, "$link $propsText $wlh" ) . "\n"; } - protected function listEnd() { - return Xml::closeElement( 'ul' ); - } - protected function wlhLink( Title $target, $text ) { static $title = null; if ( $title === null ) @@ -314,44 +189,11 @@ class WhatLinksHerePage { return $this->skin->makeKnownLinkObj( $this->selfTitle, $text, $query ); } - function getPrevNext( $prevId, $nextId ) { - global $wgLang; - $currentLimit = $this->opts->getValue( 'limit' ); - $fmtLimit = $wgLang->formatNum( $currentLimit ); - $prev = wfMsgExt( 'whatlinkshere-prev', array( 'parsemag', 'escape' ), $fmtLimit ); - $next = wfMsgExt( 'whatlinkshere-next', array( 'parsemag', 'escape' ), $fmtLimit ); - - $changed = $this->opts->getChangedValues(); - unset($changed['target']); // Already in the request title - - if ( 0 != $prevId ) { - $overrides = array( 'from' => $this->opts->getValue( 'back' ) ); - $prev = $this->makeSelfLink( $prev, wfArrayToCGI( $overrides, $changed ) ); - } - if ( 0 != $nextId ) { - $overrides = array( 'from' => $nextId, 'back' => $prevId ); - $next = $this->makeSelfLink( $next, wfArrayToCGI( $overrides, $changed ) ); - } - - $limitLinks = array(); - foreach ( $this->limits as $limit ) { - $prettyLimit = $wgLang->formatNum( $limit ); - $overrides = array( 'limit' => $limit ); - $limitLinks[] = $this->makeSelfLink( $prettyLimit, wfArrayToCGI( $overrides, $changed ) ); - } - - $nums = implode ( ' | ', $limitLinks ); - - return wfMsgHtml( 'viewprevnext', $prev, $next, $nums ); - } - function whatlinkshereForm() { global $wgScript, $wgTitle; // We get nicer value from the title object $this->opts->consumeValue( 'target' ); - // Reset these for new requests - $this->opts->consumeValues( array( 'back', 'from' ) ); $target = $this->target ? $this->target->getPrefixedText() : ''; $namespace = $this->opts->consumeValue( 'namespace' ); @@ -406,3 +248,188 @@ class WhatLinksHerePage { return Xml::fieldset( wfMsg( 'whatlinkshere-filters' ), implode( ' | ', $links ) ); } } + +/** + * Pager for Special:WhatLinksHere + * @ingroup SpecialPage Pager + */ +class WhatLinksHerePager extends AlphabeticPager { + protected $form, $target, $level, $limit, $namespace, $hidelinks, $hideredirs, $hidetrans, $hideimages; + + function __construct( $form, $target, $level, $limit, $namespace, $hidelinks, $hideredirs, $hidetrans, $hideimages ) { + parent::__construct(); + $this->mForm = $form; + + $this->target = $target; + $this->namespace = $namespace; + $this->hidelinks = (bool)$hidelinks; + $this->hideredirs = (bool)$hideredirs; + $this->hidetrans = (bool)$hidetrans; + $this->hideimages = (bool)$hideimages; + + $this->upperLimit = $limit; // limit cannot pass this + $this->level = intval($level); // recursion depth + } + + function getQueryInfo() { + $queryInfo = array(); + $options = array(); + + $fields = array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ); + + $plConds = array( + 'page_id=pl_from', + 'pl_namespace' => $this->target->getNamespace(), + 'pl_title' => $this->target->getDBkey(), + ); + if( $this->hideredirs ) { + $plConds['page_is_redirect'] = 0; + } else if( $this->hidelinks ) { + $plConds['page_is_redirect'] = 1; + } + + $tlConds = array( + 'page_id=tl_from', + 'tl_namespace' => $this->target->getNamespace(), + 'tl_title' => $this->target->getDBkey(), + ); + + $ilConds = array( + 'page_id=il_from', + 'il_to' => $this->target->getDBkey(), + ); + + if( is_int($this->namespace) ) { + $plConds['page_namespace'] = $this->namespace; + $tlConds['page_namespace'] = $this->namespace; + $ilConds['page_namespace'] = $this->namespace; + } + + $plOptions['ORDER BY'] = 'pl_from'; + $tlOptions['ORDER BY'] = 'tl_from'; + $ilOptions['ORDER BY'] = 'il_from'; + + // Enforce join order, sometimes namespace selector may + // trigger filesorts which are far less efficient than scanning many entries + $plOptions['USE INDEX'] = array('pagelinks' => 'pl_namespace'); + $tlOptions['USE INDEX'] = array('templatelinks' => 'tl_namespace'); + $ilOptions['USE INDEX'] = array('imagelinks' => 'il_to'); + + $fetchlinks = (!$this->hidelinks || !$this->hideredirs); + if( $fetchlinks ) { + $plQueryInfo = array( + 'tables' => array( 'pagelinks', 'page' ), + 'fields' => array_merge($fields,array('0 AS is_template','0 AS is_image')), + 'conds' => $plConds, + 'options' => array_merge($options,$plOptions) + ); + $queryInfo[] = $plQueryInfo; + } + + if( !$this->hidetrans ) { + $tlQueryInfo = array( + 'tables' => array( 'templatelinks', 'page' ), + 'fields' => array_merge($fields,array('1 AS is_template','0 AS is_image')), + 'conds' => $tlConds, + 'options' => array_merge($options,$tlOptions) + ); + $queryInfo[] = $tlQueryInfo; + } + + if( !$this->hideimages ) { + $ilQueryInfo = array( + 'tables' => array( 'imagelinks', 'page' ), + 'fields' => array_merge($fields,array('0 AS is_template','1 AS is_image')), + 'conds' => $ilConds, + 'options' => array_merge($options,$ilOptions) + ); + $queryInfo[] = $ilQueryInfo; + } + return $queryInfo; + } + + /** + * Do a query with specified parameters, rather than using the object + * context + * + * @param string $offset Index offset, inclusive + * @param integer $limit Exact query limit + * @param boolean $descending Query direction, false for ascending, true for descending + * @return ResultWrapper + */ + function reallyDoQuery( $offset, $limit, $descending ) { + $fname = __METHOD__ . ' (' . get_class( $this ) . ')'; + $queryInfo = $this->getQueryInfo(); + # Make sure a valid wrapper is always returned! + if( empty($queryInfo) ) { + return new ResultWrapper( $this->mDb, false ); + } + $SQLqueries = array(); + // Redirect subqueries have an upper limit for perfomance. + // If one is given, override the requested limit with this if smaller. + $limit = $this->upperLimit ? min( $limit, $this->upperLimit ) : $limit; + // Some code here lifted from Pager.php + foreach( $queryInfo as $info ) { + $tables = $info['tables']; + $fields = $info['fields']; + $conds = isset( $info['conds'] ) ? $info['conds'] : array(); + $options = isset( $info['options'] ) ? $info['options'] : array(); + $join_conds = isset( $info['join_conds'] ) ? $info['join_conds'] : array(); + if ( $descending ) { + $operator = '>'; + } else { + $options['ORDER BY'] .= ' DESC'; + $operator = '<'; + } + if ( $offset != '' ) { + $conds[] = $this->mIndexField . $operator . $this->mDb->addQuotes( $offset ); + } + $options['LIMIT'] = intval( $limit ); + $SQLqueries[] = '('.$this->mDb->selectSQLText( $tables, $fields, $conds, $fname, $options, $join_conds ).')'; + } + // Contruct the final query. UNION the mini-queries and merge the results. + // Remove duplicates within the result set. + $SQL = implode(' UNION DISTINCT ',$SQLqueries); + // Use proper order of result set + $SQL .= $descending ? " ORDER BY {$this->mIndexField} DESC" : " ORDER BY {$this->mIndexField}"; + // Cut off at the specified limit + $SQL .= ' LIMIT ' . intval( $limit ); + # Run the query! + $res = $this->mDb->query( $SQL, __METHOD__ ); + return new ResultWrapper( $this->mDb, $res ); + } + + function getIndexField() { + return 'page_id'; + } + + function getStartBody() { + wfProfileIn( __METHOD__ ); + # Do a link batch query + $lb = new LinkBatch; + while( $row = $this->mResult->fetchObject() ) { + $lb->add( $row->page_namespace, $row->page_title ); + } + $lb->execute(); + wfProfileOut( __METHOD__ ); + return "\n"; + } + + function formatRow( $row ) { + global $wgMaxRedirectLinksRetrieved; + $nt = Title::makeTitle( $row->page_namespace, $row->page_title ); + # For redirects, recursively list out the links there...unless + # we hit the maxixum recurision depth. + if( $row->page_is_redirect && $this->level < 2 ) { + return $this->mForm->listItem( $row, $nt, true ) . + $this->mForm->showIndirectLinks( $this->level + 1, $nt, $wgMaxRedirectLinksRetrieved ) . + Xml::closeElement( 'li' ); + } else { + return $this->mForm->listItem( $row, $nt ); + } + } +} -- 2.20.1