IndexPager.php 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820
  1. <?php
  2. /**
  3. * Efficient paging for SQL queries.
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. * @ingroup Pager
  22. */
  23. use MediaWiki\Linker\LinkRenderer;
  24. use MediaWiki\MediaWikiServices;
  25. use MediaWiki\Navigation\PrevNextNavigationRenderer;
  26. use Wikimedia\Rdbms\IDatabase;
  27. use Wikimedia\Rdbms\IResultWrapper;
  28. /**
  29. * IndexPager is an efficient pager which uses a (roughly unique) index in the
  30. * data set to implement paging, rather than a "LIMIT offset,limit" clause.
  31. * In MySQL, such a limit/offset clause requires counting through the
  32. * specified number of offset rows to find the desired data, which can be
  33. * expensive for large offsets.
  34. *
  35. * ReverseChronologicalPager is a child class of the abstract IndexPager, and
  36. * contains some formatting and display code which is specific to the use of
  37. * timestamps as indexes. Here is a synopsis of its operation:
  38. *
  39. * * The query is specified by the offset, limit and direction (dir)
  40. * parameters, in addition to any subclass-specific parameters.
  41. * * The offset is the non-inclusive start of the DB query. A row with an
  42. * index value equal to the offset will never be shown.
  43. * * The query may either be done backwards, where the rows are returned by
  44. * the database in the opposite order to which they are displayed to the
  45. * user, or forwards. This is specified by the "dir" parameter, dir=prev
  46. * means backwards, anything else means forwards. The offset value
  47. * specifies the start of the database result set, which may be either
  48. * the start or end of the displayed data set. This allows "previous"
  49. * links to be implemented without knowledge of the index value at the
  50. * start of the previous page.
  51. * * An additional row beyond the user-specified limit is always requested.
  52. * This allows us to tell whether we should display a "next" link in the
  53. * case of forwards mode, or a "previous" link in the case of backwards
  54. * mode. Determining whether to display the other link (the one for the
  55. * page before the start of the database result set) can be done
  56. * heuristically by examining the offset.
  57. *
  58. * * An empty offset indicates that the offset condition should be omitted
  59. * from the query. This naturally produces either the first page or the
  60. * last page depending on the dir parameter.
  61. *
  62. * Subclassing the pager to implement concrete functionality should be fairly
  63. * simple, please see the examples in HistoryAction.php and
  64. * SpecialBlockList.php. You just need to override formatRow(),
  65. * getQueryInfo() and getIndexField(). Don't forget to call the parent
  66. * constructor if you override it.
  67. *
  68. * @ingroup Pager
  69. */
  70. abstract class IndexPager extends ContextSource implements Pager {
  71. /** Backwards-compatible constant for $mDefaultDirection field (do not change) */
  72. const DIR_ASCENDING = false;
  73. /** Backwards-compatible constant for $mDefaultDirection field (do not change) */
  74. const DIR_DESCENDING = true;
  75. /** Backwards-compatible constant for reallyDoQuery() (do not change) */
  76. const QUERY_ASCENDING = true;
  77. /** Backwards-compatible constant for reallyDoQuery() (do not change) */
  78. const QUERY_DESCENDING = false;
  79. /** @var WebRequest */
  80. public $mRequest;
  81. /** @var int[] List of default entry limit options to be presented to clients */
  82. public $mLimitsShown = [ 20, 50, 100, 250, 500 ];
  83. /** @var int The default entry limit choosen for clients */
  84. public $mDefaultLimit = 50;
  85. /** @var mixed The starting point to enumerate entries */
  86. public $mOffset;
  87. /** @var int The maximum number of entries to show */
  88. public $mLimit;
  89. /** @var bool Whether the listing query completed */
  90. public $mQueryDone = false;
  91. /** @var IDatabase */
  92. public $mDb;
  93. /** @var stdClass|bool|null Extra row fetched at the end to see if the end was reached */
  94. public $mPastTheEndRow;
  95. /**
  96. * The index to actually be used for ordering. This is a single column,
  97. * for one ordering, even if multiple orderings are supported.
  98. * @var string
  99. */
  100. protected $mIndexField;
  101. /**
  102. * An array of secondary columns to order by. These fields are not part of the offset.
  103. * This is a column list for one ordering, even if multiple orderings are supported.
  104. * @var string[]
  105. */
  106. protected $mExtraSortFields;
  107. /** For pages that support multiple types of ordering, which one to use.
  108. * @var string|null
  109. */
  110. protected $mOrderType;
  111. /**
  112. * $mDefaultDirection gives the direction to use when sorting results:
  113. * DIR_ASCENDING or DIR_DESCENDING. If $mIsBackwards is set, we start from
  114. * the opposite end, but we still sort the page itself according to
  115. * $mDefaultDirection. For example, if $mDefaultDirection is DIR_ASCENDING
  116. * but we're going backwards, we'll display the last page of results, but
  117. * the last result will be at the bottom, not the top.
  118. *
  119. * Like $mIndexField, $mDefaultDirection will be a single value even if the
  120. * class supports multiple default directions for different order types.
  121. * @var bool
  122. */
  123. public $mDefaultDirection;
  124. /** @var bool */
  125. public $mIsBackwards;
  126. /** @var bool True if the current result set is the first one */
  127. public $mIsFirst;
  128. /** @var bool */
  129. public $mIsLast;
  130. /** @var mixed */
  131. protected $mLastShown;
  132. /** @var mixed */
  133. protected $mFirstShown;
  134. /** @var mixed */
  135. protected $mPastTheEndIndex;
  136. /** @var array */
  137. protected $mDefaultQuery;
  138. /** @var string */
  139. protected $mNavigationBar;
  140. /**
  141. * Whether to include the offset in the query
  142. * @var bool
  143. */
  144. protected $mIncludeOffset = false;
  145. /**
  146. * Result object for the query. Warning: seek before use.
  147. *
  148. * @var IResultWrapper
  149. */
  150. public $mResult;
  151. /** @var LinkRenderer */
  152. private $linkRenderer;
  153. public function __construct( IContextSource $context = null, LinkRenderer $linkRenderer = null ) {
  154. if ( $context ) {
  155. $this->setContext( $context );
  156. }
  157. $this->mRequest = $this->getRequest();
  158. # NB: the offset is quoted, not validated. It is treated as an
  159. # arbitrary string to support the widest variety of index types. Be
  160. # careful outputting it into HTML!
  161. $this->mOffset = $this->mRequest->getText( 'offset' );
  162. # Use consistent behavior for the limit options
  163. $this->mDefaultLimit = $this->getUser()->getIntOption( 'rclimit' );
  164. if ( !$this->mLimit ) {
  165. // Don't override if a subclass calls $this->setLimit() in its constructor.
  166. list( $this->mLimit, /* $offset */ ) = $this->mRequest->getLimitOffset();
  167. }
  168. $this->mIsBackwards = ( $this->mRequest->getVal( 'dir' ) == 'prev' );
  169. # Let the subclass set the DB here; otherwise use a replica DB for the current wiki
  170. $this->mDb = $this->mDb ?: wfGetDB( DB_REPLICA );
  171. $index = $this->getIndexField(); // column to sort on
  172. $extraSort = $this->getExtraSortFields(); // extra columns to sort on for query planning
  173. $order = $this->mRequest->getVal( 'order' );
  174. if ( is_array( $index ) && isset( $index[$order] ) ) {
  175. $this->mOrderType = $order;
  176. $this->mIndexField = $index[$order];
  177. $this->mExtraSortFields = isset( $extraSort[$order] )
  178. ? (array)$extraSort[$order]
  179. : [];
  180. } elseif ( is_array( $index ) ) {
  181. # First element is the default
  182. $this->mIndexField = reset( $index );
  183. $this->mOrderType = key( $index );
  184. $this->mExtraSortFields = isset( $extraSort[$this->mOrderType] )
  185. ? (array)$extraSort[$this->mOrderType]
  186. : [];
  187. } else {
  188. # $index is not an array
  189. $this->mOrderType = null;
  190. $this->mIndexField = $index;
  191. $this->mExtraSortFields = (array)$extraSort;
  192. }
  193. if ( !isset( $this->mDefaultDirection ) ) {
  194. $dir = $this->getDefaultDirections();
  195. $this->mDefaultDirection = is_array( $dir )
  196. ? $dir[$this->mOrderType]
  197. : $dir;
  198. }
  199. $this->linkRenderer = $linkRenderer;
  200. }
  201. /**
  202. * Get the Database object in use
  203. *
  204. * @return IDatabase
  205. */
  206. public function getDatabase() {
  207. return $this->mDb;
  208. }
  209. /**
  210. * Do the query, using information from the object context. This function
  211. * has been kept minimal to make it overridable if necessary, to allow for
  212. * result sets formed from multiple DB queries.
  213. */
  214. public function doQuery() {
  215. # Use the child class name for profiling
  216. $fname = __METHOD__ . ' (' . static::class . ')';
  217. /** @noinspection PhpUnusedLocalVariableInspection */
  218. $section = Profiler::instance()->scopedProfileIn( $fname );
  219. $defaultOrder = ( $this->mDefaultDirection === self::DIR_ASCENDING )
  220. ? self::QUERY_ASCENDING
  221. : self::QUERY_DESCENDING;
  222. $order = $this->mIsBackwards ? self::oppositeOrder( $defaultOrder ) : $defaultOrder;
  223. # Plus an extra row so that we can tell the "next" link should be shown
  224. $queryLimit = $this->mLimit + 1;
  225. if ( $this->mOffset == '' ) {
  226. $isFirst = true;
  227. } else {
  228. // If there's an offset, we may or may not be at the first entry.
  229. // The only way to tell is to run the query in the opposite
  230. // direction see if we get a row.
  231. $oldIncludeOffset = $this->mIncludeOffset;
  232. $this->mIncludeOffset = !$this->mIncludeOffset;
  233. $oppositeOrder = self::oppositeOrder( $order );
  234. $isFirst = !$this->reallyDoQuery( $this->mOffset, 1, $oppositeOrder )->numRows();
  235. $this->mIncludeOffset = $oldIncludeOffset;
  236. }
  237. $this->mResult = $this->reallyDoQuery(
  238. $this->mOffset,
  239. $queryLimit,
  240. $order
  241. );
  242. $this->extractResultInfo( $isFirst, $queryLimit, $this->mResult );
  243. $this->mQueryDone = true;
  244. $this->preprocessResults( $this->mResult );
  245. $this->mResult->rewind(); // Paranoia
  246. }
  247. /**
  248. * @param bool $order One of the IndexPager::QUERY_* class constants
  249. * @return bool The opposite query order as an IndexPager::QUERY_ constant
  250. */
  251. final protected static function oppositeOrder( $order ) {
  252. return ( $order === self::QUERY_ASCENDING )
  253. ? self::QUERY_DESCENDING
  254. : self::QUERY_ASCENDING;
  255. }
  256. /**
  257. * @return IResultWrapper The result wrapper.
  258. */
  259. function getResult() {
  260. return $this->mResult;
  261. }
  262. /**
  263. * Set the offset from an other source than the request
  264. *
  265. * @param int|string $offset
  266. */
  267. function setOffset( $offset ) {
  268. $this->mOffset = $offset;
  269. }
  270. /**
  271. * Set the limit from an other source than the request
  272. *
  273. * Verifies limit is between 1 and 5000
  274. *
  275. * @param int|string $limit
  276. */
  277. function setLimit( $limit ) {
  278. $limit = (int)$limit;
  279. // WebRequest::getLimitOffset() puts a cap of 5000, so do same here.
  280. if ( $limit > 5000 ) {
  281. $limit = 5000;
  282. }
  283. if ( $limit > 0 ) {
  284. $this->mLimit = $limit;
  285. }
  286. }
  287. /**
  288. * Get the current limit
  289. *
  290. * @return int
  291. */
  292. function getLimit() {
  293. return $this->mLimit;
  294. }
  295. /**
  296. * Set whether a row matching exactly the offset should be also included
  297. * in the result or not. By default this is not the case, but when the
  298. * offset is user-supplied this might be wanted.
  299. *
  300. * @param bool $include
  301. */
  302. public function setIncludeOffset( $include ) {
  303. $this->mIncludeOffset = $include;
  304. }
  305. /**
  306. * Extract some useful data from the result object for use by
  307. * the navigation bar, put it into $this
  308. *
  309. * @param bool $isFirst False if there are rows before those fetched (i.e.
  310. * if a "previous" link would make sense)
  311. * @param int $limit Exact query limit
  312. * @param IResultWrapper $res
  313. */
  314. function extractResultInfo( $isFirst, $limit, IResultWrapper $res ) {
  315. $numRows = $res->numRows();
  316. if ( $numRows ) {
  317. # Remove any table prefix from index field
  318. $parts = explode( '.', $this->mIndexField );
  319. $indexColumn = end( $parts );
  320. $row = $res->fetchRow();
  321. $firstIndex = $row[$indexColumn];
  322. # Discard the extra result row if there is one
  323. if ( $numRows > $this->mLimit && $numRows > 1 ) {
  324. $res->seek( $numRows - 1 );
  325. $this->mPastTheEndRow = $res->fetchObject();
  326. $this->mPastTheEndIndex = $this->mPastTheEndRow->$indexColumn;
  327. $res->seek( $numRows - 2 );
  328. $row = $res->fetchRow();
  329. $lastIndex = $row[$indexColumn];
  330. } else {
  331. $this->mPastTheEndRow = null;
  332. # Setting indexes to an empty string means that they will be
  333. # omitted if they would otherwise appear in URLs. It just so
  334. # happens that this is the right thing to do in the standard
  335. # UI, in all the relevant cases.
  336. $this->mPastTheEndIndex = '';
  337. $res->seek( $numRows - 1 );
  338. $row = $res->fetchRow();
  339. $lastIndex = $row[$indexColumn];
  340. }
  341. } else {
  342. $firstIndex = '';
  343. $lastIndex = '';
  344. $this->mPastTheEndRow = null;
  345. $this->mPastTheEndIndex = '';
  346. }
  347. if ( $this->mIsBackwards ) {
  348. $this->mIsFirst = ( $numRows < $limit );
  349. $this->mIsLast = $isFirst;
  350. $this->mLastShown = $firstIndex;
  351. $this->mFirstShown = $lastIndex;
  352. } else {
  353. $this->mIsFirst = $isFirst;
  354. $this->mIsLast = ( $numRows < $limit );
  355. $this->mLastShown = $lastIndex;
  356. $this->mFirstShown = $firstIndex;
  357. }
  358. }
  359. /**
  360. * Get some text to go in brackets in the "function name" part of the SQL comment
  361. *
  362. * @return string
  363. */
  364. function getSqlComment() {
  365. return static::class;
  366. }
  367. /**
  368. * Do a query with specified parameters, rather than using the object context
  369. *
  370. * @note For b/c, query direction is true for ascending and false for descending
  371. *
  372. * @param string $offset Index offset, inclusive
  373. * @param int $limit Exact query limit
  374. * @param bool $order IndexPager::QUERY_ASCENDING or IndexPager::QUERY_DESCENDING
  375. * @return IResultWrapper
  376. */
  377. public function reallyDoQuery( $offset, $limit, $order ) {
  378. list( $tables, $fields, $conds, $fname, $options, $join_conds ) =
  379. $this->buildQueryInfo( $offset, $limit, $order );
  380. return $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds );
  381. }
  382. /**
  383. * Build variables to use by the database wrapper.
  384. *
  385. * @note For b/c, query direction is true for ascending and false for descending
  386. *
  387. * @param string $offset Index offset, inclusive
  388. * @param int $limit Exact query limit
  389. * @param bool $order IndexPager::QUERY_ASCENDING or IndexPager::QUERY_DESCENDING
  390. * @return array
  391. */
  392. protected function buildQueryInfo( $offset, $limit, $order ) {
  393. $fname = __METHOD__ . ' (' . $this->getSqlComment() . ')';
  394. $info = $this->getQueryInfo();
  395. $tables = $info['tables'];
  396. $fields = $info['fields'];
  397. $conds = $info['conds'] ?? [];
  398. $options = $info['options'] ?? [];
  399. $join_conds = $info['join_conds'] ?? [];
  400. $sortColumns = array_merge( [ $this->mIndexField ], $this->mExtraSortFields );
  401. if ( $order === self::QUERY_ASCENDING ) {
  402. $options['ORDER BY'] = $sortColumns;
  403. $operator = $this->mIncludeOffset ? '>=' : '>';
  404. } else {
  405. $orderBy = [];
  406. foreach ( $sortColumns as $col ) {
  407. $orderBy[] = $col . ' DESC';
  408. }
  409. $options['ORDER BY'] = $orderBy;
  410. $operator = $this->mIncludeOffset ? '<=' : '<';
  411. }
  412. if ( $offset != '' ) {
  413. $conds[] = $this->mIndexField . $operator . $this->mDb->addQuotes( $offset );
  414. }
  415. $options['LIMIT'] = intval( $limit );
  416. return [ $tables, $fields, $conds, $fname, $options, $join_conds ];
  417. }
  418. /**
  419. * Pre-process results; useful for performing batch existence checks, etc.
  420. *
  421. * @param IResultWrapper $result
  422. */
  423. protected function preprocessResults( $result ) {
  424. }
  425. /**
  426. * Get the formatted result list. Calls getStartBody(), formatRow() and
  427. * getEndBody(), concatenates the results and returns them.
  428. *
  429. * @return string
  430. */
  431. public function getBody() {
  432. if ( !$this->mQueryDone ) {
  433. $this->doQuery();
  434. }
  435. if ( $this->mResult->numRows() ) {
  436. # Do any special query batches before display
  437. $this->doBatchLookups();
  438. }
  439. # Don't use any extra rows returned by the query
  440. $numRows = min( $this->mResult->numRows(), $this->mLimit );
  441. $s = $this->getStartBody();
  442. if ( $numRows ) {
  443. if ( $this->mIsBackwards ) {
  444. for ( $i = $numRows - 1; $i >= 0; $i-- ) {
  445. $this->mResult->seek( $i );
  446. $row = $this->mResult->fetchObject();
  447. $s .= $this->formatRow( $row );
  448. }
  449. } else {
  450. $this->mResult->seek( 0 );
  451. for ( $i = 0; $i < $numRows; $i++ ) {
  452. $row = $this->mResult->fetchObject();
  453. $s .= $this->formatRow( $row );
  454. }
  455. }
  456. } else {
  457. $s .= $this->getEmptyBody();
  458. }
  459. $s .= $this->getEndBody();
  460. return $s;
  461. }
  462. /**
  463. * Make a self-link
  464. *
  465. * @param string $text Text displayed on the link
  466. * @param array|null $query Associative array of parameter to be in the query string
  467. * @param string|null $type Link type used to create additional attributes, like "rel", "class" or
  468. * "title". Valid values (non-exhaustive list): 'first', 'last', 'prev', 'next', 'asc', 'desc'.
  469. * @return string HTML fragment
  470. */
  471. function makeLink( $text, array $query = null, $type = null ) {
  472. if ( $query === null ) {
  473. return $text;
  474. }
  475. $attrs = [];
  476. if ( in_array( $type, [ 'prev', 'next' ] ) ) {
  477. $attrs['rel'] = $type;
  478. }
  479. if ( in_array( $type, [ 'asc', 'desc' ] ) ) {
  480. $attrs['title'] = $this->msg( $type == 'asc' ? 'sort-ascending' : 'sort-descending' )->text();
  481. }
  482. if ( $type ) {
  483. $attrs['class'] = "mw-{$type}link";
  484. }
  485. return $this->getLinkRenderer()->makeKnownLink(
  486. $this->getTitle(),
  487. new HtmlArmor( $text ),
  488. $attrs,
  489. $query + $this->getDefaultQuery()
  490. );
  491. }
  492. /**
  493. * Called from getBody(), before getStartBody() is called and
  494. * after doQuery() was called. This will be called only if there
  495. * are rows in the result set.
  496. *
  497. * @return void
  498. */
  499. protected function doBatchLookups() {
  500. }
  501. /**
  502. * Hook into getBody(), allows text to be inserted at the start. This
  503. * will be called even if there are no rows in the result set.
  504. *
  505. * @return string
  506. */
  507. protected function getStartBody() {
  508. return '';
  509. }
  510. /**
  511. * Hook into getBody() for the end of the list
  512. *
  513. * @return string
  514. */
  515. protected function getEndBody() {
  516. return '';
  517. }
  518. /**
  519. * Hook into getBody(), for the bit between the start and the
  520. * end when there are no rows
  521. *
  522. * @return string
  523. */
  524. protected function getEmptyBody() {
  525. return '';
  526. }
  527. /**
  528. * Get an array of query parameters that should be put into self-links.
  529. * By default, all parameters passed in the URL are used, except for a
  530. * short blacklist.
  531. *
  532. * @return array Associative array
  533. */
  534. function getDefaultQuery() {
  535. if ( !isset( $this->mDefaultQuery ) ) {
  536. $this->mDefaultQuery = $this->getRequest()->getQueryValues();
  537. unset( $this->mDefaultQuery['title'] );
  538. unset( $this->mDefaultQuery['dir'] );
  539. unset( $this->mDefaultQuery['offset'] );
  540. unset( $this->mDefaultQuery['limit'] );
  541. unset( $this->mDefaultQuery['order'] );
  542. unset( $this->mDefaultQuery['month'] );
  543. unset( $this->mDefaultQuery['year'] );
  544. }
  545. return $this->mDefaultQuery;
  546. }
  547. /**
  548. * Get the number of rows in the result set
  549. *
  550. * @return int
  551. */
  552. function getNumRows() {
  553. if ( !$this->mQueryDone ) {
  554. $this->doQuery();
  555. }
  556. return $this->mResult->numRows();
  557. }
  558. /**
  559. * Get a URL query array for the prev, next, first and last links.
  560. *
  561. * @return array
  562. */
  563. function getPagingQueries() {
  564. if ( !$this->mQueryDone ) {
  565. $this->doQuery();
  566. }
  567. # Don't announce the limit everywhere if it's the default
  568. $urlLimit = $this->mLimit == $this->mDefaultLimit ? null : $this->mLimit;
  569. if ( $this->mIsFirst ) {
  570. $prev = false;
  571. $first = false;
  572. } else {
  573. $prev = [
  574. 'dir' => 'prev',
  575. 'offset' => $this->mFirstShown,
  576. 'limit' => $urlLimit
  577. ];
  578. $first = [ 'limit' => $urlLimit ];
  579. }
  580. if ( $this->mIsLast ) {
  581. $next = false;
  582. $last = false;
  583. } else {
  584. $next = [ 'offset' => $this->mLastShown, 'limit' => $urlLimit ];
  585. $last = [ 'dir' => 'prev', 'limit' => $urlLimit ];
  586. }
  587. return [
  588. 'prev' => $prev,
  589. 'next' => $next,
  590. 'first' => $first,
  591. 'last' => $last
  592. ];
  593. }
  594. /**
  595. * Returns whether to show the "navigation bar"
  596. *
  597. * @return bool
  598. */
  599. function isNavigationBarShown() {
  600. if ( !$this->mQueryDone ) {
  601. $this->doQuery();
  602. }
  603. // Hide navigation by default if there is nothing to page
  604. return !( $this->mIsFirst && $this->mIsLast );
  605. }
  606. /**
  607. * Get paging links. If a link is disabled, the item from $disabledTexts
  608. * will be used. If there is no such item, the unlinked text from
  609. * $linkTexts will be used. Both $linkTexts and $disabledTexts are arrays
  610. * of HTML.
  611. *
  612. * @param array $linkTexts
  613. * @param array $disabledTexts
  614. * @return array
  615. */
  616. function getPagingLinks( $linkTexts, $disabledTexts = [] ) {
  617. $queries = $this->getPagingQueries();
  618. $links = [];
  619. foreach ( $queries as $type => $query ) {
  620. if ( $query !== false ) {
  621. $links[$type] = $this->makeLink(
  622. $linkTexts[$type],
  623. $queries[$type],
  624. $type
  625. );
  626. } elseif ( isset( $disabledTexts[$type] ) ) {
  627. $links[$type] = $disabledTexts[$type];
  628. } else {
  629. $links[$type] = $linkTexts[$type];
  630. }
  631. }
  632. return $links;
  633. }
  634. function getLimitLinks() {
  635. $links = [];
  636. if ( $this->mIsBackwards ) {
  637. $offset = $this->mPastTheEndIndex;
  638. } else {
  639. $offset = $this->mOffset;
  640. }
  641. foreach ( $this->mLimitsShown as $limit ) {
  642. $links[] = $this->makeLink(
  643. $this->getLanguage()->formatNum( $limit ),
  644. [ 'offset' => $offset, 'limit' => $limit ],
  645. 'num'
  646. );
  647. }
  648. return $links;
  649. }
  650. /**
  651. * Abstract formatting function. This should return an HTML string
  652. * representing the result row $row. Rows will be concatenated and
  653. * returned by getBody()
  654. *
  655. * @param array|stdClass $row Database row
  656. * @return string
  657. */
  658. abstract function formatRow( $row );
  659. /**
  660. * This function should be overridden to provide all parameters
  661. * needed for the main paged query. It returns an associative
  662. * array with the following elements:
  663. * tables => Table(s) for passing to Database::select()
  664. * fields => Field(s) for passing to Database::select(), may be *
  665. * conds => WHERE conditions
  666. * options => option array
  667. * join_conds => JOIN conditions
  668. *
  669. * @return array
  670. */
  671. abstract function getQueryInfo();
  672. /**
  673. * This function should be overridden to return the name of the index fi-
  674. * eld. If the pager supports multiple orders, it may return an array of
  675. * 'querykey' => 'indexfield' pairs, so that a request with &count=querykey
  676. * will use indexfield to sort. In this case, the first returned key is
  677. * the default.
  678. *
  679. * Needless to say, it's really not a good idea to use a non-unique index
  680. * for this! That won't page right.
  681. *
  682. * @return string|string[]
  683. */
  684. abstract function getIndexField();
  685. /**
  686. * This function should be overridden to return the names of secondary columns
  687. * to order by in addition to the column in getIndexField(). These fields will
  688. * not be used in the pager offset or in any links for users.
  689. *
  690. * If getIndexField() returns an array of 'querykey' => 'indexfield' pairs then
  691. * this must return a corresponding array of 'querykey' => [ fields... ] pairs
  692. * in order for a request with &count=querykey to use [ fields... ] to sort.
  693. *
  694. * This is useful for pagers that GROUP BY a unique column (say page_id)
  695. * and ORDER BY another (say page_len). Using GROUP BY and ORDER BY both on
  696. * page_len,page_id avoids temp tables (given a page_len index). This would
  697. * also work if page_id was non-unique but we had a page_len,page_id index.
  698. *
  699. * @return string[]|array[]
  700. */
  701. protected function getExtraSortFields() {
  702. return [];
  703. }
  704. /**
  705. * Return the default sorting direction: DIR_ASCENDING or DIR_DESCENDING.
  706. * You can also have an associative array of ordertype => dir,
  707. * if multiple order types are supported. In this case getIndexField()
  708. * must return an array, and the keys of that must exactly match the keys
  709. * of this.
  710. *
  711. * For backward compatibility, this method's return value will be ignored
  712. * if $this->mDefaultDirection is already set when the constructor is
  713. * called, for instance if it's statically initialized. In that case the
  714. * value of that variable (which must be a boolean) will be used.
  715. *
  716. * Note that despite its name, this does not return the value of the
  717. * $this->mDefaultDirection member variable. That's the default for this
  718. * particular instantiation, which is a single value. This is the set of
  719. * all defaults for the class.
  720. *
  721. * @return bool
  722. */
  723. protected function getDefaultDirections() {
  724. return self::DIR_ASCENDING;
  725. }
  726. /**
  727. * Generate (prev x| next x) (20|50|100...) type links for paging
  728. *
  729. * @param Title $title
  730. * @param int $offset
  731. * @param int $limit
  732. * @param array $query Optional URL query parameter string
  733. * @param bool $atend Optional param for specified if this is the last page
  734. * @return string
  735. */
  736. protected function buildPrevNextNavigation( Title $title, $offset, $limit,
  737. array $query = [], $atend = false
  738. ) {
  739. $prevNext = new PrevNextNavigationRenderer( $this );
  740. return $prevNext->buildPrevNextNavigation( $title, $offset, $limit, $query, $atend );
  741. }
  742. protected function getLinkRenderer() {
  743. if ( $this->linkRenderer === null ) {
  744. $this->linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
  745. }
  746. return $this->linkRenderer;
  747. }
  748. }