MediaWiki
REL1_24
|
00001 <?php 00066 abstract class IndexPager extends ContextSource implements Pager { 00072 const DIR_ASCENDING = false; 00073 const DIR_DESCENDING = true; 00074 00075 public $mRequest; 00076 public $mLimitsShown = array( 20, 50, 100, 250, 500 ); 00077 public $mDefaultLimit = 50; 00078 public $mOffset, $mLimit; 00079 public $mQueryDone = false; 00080 public $mDb; 00081 public $mPastTheEndRow; 00082 00087 protected $mIndexField; 00092 protected $mExtraSortFields; 00095 protected $mOrderType; 00107 public $mDefaultDirection; 00108 public $mIsBackwards; 00109 00111 public $mIsFirst; 00112 public $mIsLast; 00113 00114 protected $mLastShown, $mFirstShown, $mPastTheEndIndex, $mDefaultQuery, $mNavigationBar; 00115 00119 protected $mIncludeOffset = false; 00120 00126 public $mResult; 00127 00128 public function __construct( IContextSource $context = null ) { 00129 if ( $context ) { 00130 $this->setContext( $context ); 00131 } 00132 00133 $this->mRequest = $this->getRequest(); 00134 00135 # NB: the offset is quoted, not validated. It is treated as an 00136 # arbitrary string to support the widest variety of index types. Be 00137 # careful outputting it into HTML! 00138 $this->mOffset = $this->mRequest->getText( 'offset' ); 00139 00140 # Use consistent behavior for the limit options 00141 $this->mDefaultLimit = $this->getUser()->getIntOption( 'rclimit' ); 00142 if ( !$this->mLimit ) { 00143 // Don't override if a subclass calls $this->setLimit() in its constructor. 00144 list( $this->mLimit, /* $offset */ ) = $this->mRequest->getLimitOffset(); 00145 } 00146 00147 $this->mIsBackwards = ( $this->mRequest->getVal( 'dir' ) == 'prev' ); 00148 # Let the subclass set the DB here; otherwise use a slave DB for the current wiki 00149 $this->mDb = $this->mDb ?: wfGetDB( DB_SLAVE ); 00150 00151 $index = $this->getIndexField(); // column to sort on 00152 $extraSort = $this->getExtraSortFields(); // extra columns to sort on for query planning 00153 $order = $this->mRequest->getVal( 'order' ); 00154 if ( is_array( $index ) && isset( $index[$order] ) ) { 00155 $this->mOrderType = $order; 00156 $this->mIndexField = $index[$order]; 00157 $this->mExtraSortFields = isset( $extraSort[$order] ) 00158 ? (array)$extraSort[$order] 00159 : array(); 00160 } elseif ( is_array( $index ) ) { 00161 # First element is the default 00162 reset( $index ); 00163 list( $this->mOrderType, $this->mIndexField ) = each( $index ); 00164 $this->mExtraSortFields = isset( $extraSort[$this->mOrderType] ) 00165 ? (array)$extraSort[$this->mOrderType] 00166 : array(); 00167 } else { 00168 # $index is not an array 00169 $this->mOrderType = null; 00170 $this->mIndexField = $index; 00171 $this->mExtraSortFields = (array)$extraSort; 00172 } 00173 00174 if ( !isset( $this->mDefaultDirection ) ) { 00175 $dir = $this->getDefaultDirections(); 00176 $this->mDefaultDirection = is_array( $dir ) 00177 ? $dir[$this->mOrderType] 00178 : $dir; 00179 } 00180 } 00181 00187 public function getDatabase() { 00188 return $this->mDb; 00189 } 00190 00196 public function doQuery() { 00197 # Use the child class name for profiling 00198 $fname = __METHOD__ . ' (' . get_class( $this ) . ')'; 00199 wfProfileIn( $fname ); 00200 00201 // @todo This should probably compare to DIR_DESCENDING and DIR_ASCENDING constants 00202 $descending = ( $this->mIsBackwards == $this->mDefaultDirection ); 00203 # Plus an extra row so that we can tell the "next" link should be shown 00204 $queryLimit = $this->mLimit + 1; 00205 00206 if ( $this->mOffset == '' ) { 00207 $isFirst = true; 00208 } else { 00209 // If there's an offset, we may or may not be at the first entry. 00210 // The only way to tell is to run the query in the opposite 00211 // direction see if we get a row. 00212 $oldIncludeOffset = $this->mIncludeOffset; 00213 $this->mIncludeOffset = !$this->mIncludeOffset; 00214 $isFirst = !$this->reallyDoQuery( $this->mOffset, 1, !$descending )->numRows(); 00215 $this->mIncludeOffset = $oldIncludeOffset; 00216 } 00217 00218 $this->mResult = $this->reallyDoQuery( 00219 $this->mOffset, 00220 $queryLimit, 00221 $descending 00222 ); 00223 00224 $this->extractResultInfo( $isFirst, $queryLimit, $this->mResult ); 00225 $this->mQueryDone = true; 00226 00227 $this->preprocessResults( $this->mResult ); 00228 $this->mResult->rewind(); // Paranoia 00229 00230 wfProfileOut( $fname ); 00231 } 00232 00236 function getResult() { 00237 return $this->mResult; 00238 } 00239 00245 function setOffset( $offset ) { 00246 $this->mOffset = $offset; 00247 } 00248 00256 function setLimit( $limit ) { 00257 $limit = (int)$limit; 00258 // WebRequest::getLimitOffset() puts a cap of 5000, so do same here. 00259 if ( $limit > 5000 ) { 00260 $limit = 5000; 00261 } 00262 if ( $limit > 0 ) { 00263 $this->mLimit = $limit; 00264 } 00265 } 00266 00272 function getLimit() { 00273 return $this->mLimit; 00274 } 00275 00283 public function setIncludeOffset( $include ) { 00284 $this->mIncludeOffset = $include; 00285 } 00286 00296 function extractResultInfo( $isFirst, $limit, ResultWrapper $res ) { 00297 $numRows = $res->numRows(); 00298 if ( $numRows ) { 00299 # Remove any table prefix from index field 00300 $parts = explode( '.', $this->mIndexField ); 00301 $indexColumn = end( $parts ); 00302 00303 $row = $res->fetchRow(); 00304 $firstIndex = $row[$indexColumn]; 00305 00306 # Discard the extra result row if there is one 00307 if ( $numRows > $this->mLimit && $numRows > 1 ) { 00308 $res->seek( $numRows - 1 ); 00309 $this->mPastTheEndRow = $res->fetchObject(); 00310 $this->mPastTheEndIndex = $this->mPastTheEndRow->$indexColumn; 00311 $res->seek( $numRows - 2 ); 00312 $row = $res->fetchRow(); 00313 $lastIndex = $row[$indexColumn]; 00314 } else { 00315 $this->mPastTheEndRow = null; 00316 # Setting indexes to an empty string means that they will be 00317 # omitted if they would otherwise appear in URLs. It just so 00318 # happens that this is the right thing to do in the standard 00319 # UI, in all the relevant cases. 00320 $this->mPastTheEndIndex = ''; 00321 $res->seek( $numRows - 1 ); 00322 $row = $res->fetchRow(); 00323 $lastIndex = $row[$indexColumn]; 00324 } 00325 } else { 00326 $firstIndex = ''; 00327 $lastIndex = ''; 00328 $this->mPastTheEndRow = null; 00329 $this->mPastTheEndIndex = ''; 00330 } 00331 00332 if ( $this->mIsBackwards ) { 00333 $this->mIsFirst = ( $numRows < $limit ); 00334 $this->mIsLast = $isFirst; 00335 $this->mLastShown = $firstIndex; 00336 $this->mFirstShown = $lastIndex; 00337 } else { 00338 $this->mIsFirst = $isFirst; 00339 $this->mIsLast = ( $numRows < $limit ); 00340 $this->mLastShown = $lastIndex; 00341 $this->mFirstShown = $firstIndex; 00342 } 00343 } 00344 00350 function getSqlComment() { 00351 return get_class( $this ); 00352 } 00353 00363 public function reallyDoQuery( $offset, $limit, $descending ) { 00364 list( $tables, $fields, $conds, $fname, $options, $join_conds ) = 00365 $this->buildQueryInfo( $offset, $limit, $descending ); 00366 00367 return $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds ); 00368 } 00369 00378 protected function buildQueryInfo( $offset, $limit, $descending ) { 00379 $fname = __METHOD__ . ' (' . $this->getSqlComment() . ')'; 00380 $info = $this->getQueryInfo(); 00381 $tables = $info['tables']; 00382 $fields = $info['fields']; 00383 $conds = isset( $info['conds'] ) ? $info['conds'] : array(); 00384 $options = isset( $info['options'] ) ? $info['options'] : array(); 00385 $join_conds = isset( $info['join_conds'] ) ? $info['join_conds'] : array(); 00386 $sortColumns = array_merge( array( $this->mIndexField ), $this->mExtraSortFields ); 00387 if ( $descending ) { 00388 $options['ORDER BY'] = $sortColumns; 00389 $operator = $this->mIncludeOffset ? '>=' : '>'; 00390 } else { 00391 $orderBy = array(); 00392 foreach ( $sortColumns as $col ) { 00393 $orderBy[] = $col . ' DESC'; 00394 } 00395 $options['ORDER BY'] = $orderBy; 00396 $operator = $this->mIncludeOffset ? '<=' : '<'; 00397 } 00398 if ( $offset != '' ) { 00399 $conds[] = $this->mIndexField . $operator . $this->mDb->addQuotes( $offset ); 00400 } 00401 $options['LIMIT'] = intval( $limit ); 00402 return array( $tables, $fields, $conds, $fname, $options, $join_conds ); 00403 } 00404 00410 protected function preprocessResults( $result ) { 00411 } 00412 00419 public function getBody() { 00420 if ( !$this->mQueryDone ) { 00421 $this->doQuery(); 00422 } 00423 00424 if ( $this->mResult->numRows() ) { 00425 # Do any special query batches before display 00426 $this->doBatchLookups(); 00427 } 00428 00429 # Don't use any extra rows returned by the query 00430 $numRows = min( $this->mResult->numRows(), $this->mLimit ); 00431 00432 $s = $this->getStartBody(); 00433 if ( $numRows ) { 00434 if ( $this->mIsBackwards ) { 00435 for ( $i = $numRows - 1; $i >= 0; $i-- ) { 00436 $this->mResult->seek( $i ); 00437 $row = $this->mResult->fetchObject(); 00438 $s .= $this->formatRow( $row ); 00439 } 00440 } else { 00441 $this->mResult->seek( 0 ); 00442 for ( $i = 0; $i < $numRows; $i++ ) { 00443 $row = $this->mResult->fetchObject(); 00444 $s .= $this->formatRow( $row ); 00445 } 00446 } 00447 } else { 00448 $s .= $this->getEmptyBody(); 00449 } 00450 $s .= $this->getEndBody(); 00451 return $s; 00452 } 00453 00463 function makeLink( $text, array $query = null, $type = null ) { 00464 if ( $query === null ) { 00465 return $text; 00466 } 00467 00468 $attrs = array(); 00469 if ( in_array( $type, array( 'prev', 'next' ) ) ) { 00470 $attrs['rel'] = $type; 00471 } 00472 00473 if ( in_array( $type, array( 'asc', 'desc' ) ) ) { 00474 $attrs['title'] = wfMessage( $type == 'asc' ? 'sort-ascending' : 'sort-descending' )->text(); 00475 } 00476 00477 if ( $type ) { 00478 $attrs['class'] = "mw-{$type}link"; 00479 } 00480 00481 00482 return Linker::linkKnown( 00483 $this->getTitle(), 00484 $text, 00485 $attrs, 00486 $query + $this->getDefaultQuery() 00487 ); 00488 } 00489 00497 protected function doBatchLookups() { 00498 } 00499 00506 protected function getStartBody() { 00507 return ''; 00508 } 00509 00515 protected function getEndBody() { 00516 return ''; 00517 } 00518 00525 protected function getEmptyBody() { 00526 return ''; 00527 } 00528 00536 function getDefaultQuery() { 00537 if ( !isset( $this->mDefaultQuery ) ) { 00538 $this->mDefaultQuery = $this->getRequest()->getQueryValues(); 00539 unset( $this->mDefaultQuery['title'] ); 00540 unset( $this->mDefaultQuery['dir'] ); 00541 unset( $this->mDefaultQuery['offset'] ); 00542 unset( $this->mDefaultQuery['limit'] ); 00543 unset( $this->mDefaultQuery['order'] ); 00544 unset( $this->mDefaultQuery['month'] ); 00545 unset( $this->mDefaultQuery['year'] ); 00546 } 00547 return $this->mDefaultQuery; 00548 } 00549 00555 function getNumRows() { 00556 if ( !$this->mQueryDone ) { 00557 $this->doQuery(); 00558 } 00559 return $this->mResult->numRows(); 00560 } 00561 00567 function getPagingQueries() { 00568 if ( !$this->mQueryDone ) { 00569 $this->doQuery(); 00570 } 00571 00572 # Don't announce the limit everywhere if it's the default 00573 $urlLimit = $this->mLimit == $this->mDefaultLimit ? null : $this->mLimit; 00574 00575 if ( $this->mIsFirst ) { 00576 $prev = false; 00577 $first = false; 00578 } else { 00579 $prev = array( 00580 'dir' => 'prev', 00581 'offset' => $this->mFirstShown, 00582 'limit' => $urlLimit 00583 ); 00584 $first = array( 'limit' => $urlLimit ); 00585 } 00586 if ( $this->mIsLast ) { 00587 $next = false; 00588 $last = false; 00589 } else { 00590 $next = array( 'offset' => $this->mLastShown, 'limit' => $urlLimit ); 00591 $last = array( 'dir' => 'prev', 'limit' => $urlLimit ); 00592 } 00593 return array( 00594 'prev' => $prev, 00595 'next' => $next, 00596 'first' => $first, 00597 'last' => $last 00598 ); 00599 } 00600 00606 function isNavigationBarShown() { 00607 if ( !$this->mQueryDone ) { 00608 $this->doQuery(); 00609 } 00610 // Hide navigation by default if there is nothing to page 00611 return !( $this->mIsFirst && $this->mIsLast ); 00612 } 00613 00624 function getPagingLinks( $linkTexts, $disabledTexts = array() ) { 00625 $queries = $this->getPagingQueries(); 00626 $links = array(); 00627 00628 foreach ( $queries as $type => $query ) { 00629 if ( $query !== false ) { 00630 $links[$type] = $this->makeLink( 00631 $linkTexts[$type], 00632 $queries[$type], 00633 $type 00634 ); 00635 } elseif ( isset( $disabledTexts[$type] ) ) { 00636 $links[$type] = $disabledTexts[$type]; 00637 } else { 00638 $links[$type] = $linkTexts[$type]; 00639 } 00640 } 00641 00642 return $links; 00643 } 00644 00645 function getLimitLinks() { 00646 $links = array(); 00647 if ( $this->mIsBackwards ) { 00648 $offset = $this->mPastTheEndIndex; 00649 } else { 00650 $offset = $this->mOffset; 00651 } 00652 foreach ( $this->mLimitsShown as $limit ) { 00653 $links[] = $this->makeLink( 00654 $this->getLanguage()->formatNum( $limit ), 00655 array( 'offset' => $offset, 'limit' => $limit ), 00656 'num' 00657 ); 00658 } 00659 return $links; 00660 } 00661 00670 abstract function formatRow( $row ); 00671 00684 abstract function getQueryInfo(); 00685 00698 abstract function getIndexField(); 00699 00716 protected function getExtraSortFields() { 00717 return array(); 00718 } 00719 00739 protected function getDefaultDirections() { 00740 return IndexPager::DIR_ASCENDING; 00741 } 00742 }