Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
34.29% covered (danger)
34.29%
48 / 140
40.91% covered (danger)
40.91%
9 / 22
CRAP
0.00% covered (danger)
0.00%
0 / 1
TablePager
34.53% covered (danger)
34.53%
48 / 139
40.91% covered (danger)
40.91%
9 / 22
613.20
0.00% covered (danger)
0.00%
0 / 1
 __construct
72.73% covered (warning)
72.73%
8 / 11
0.00% covered (danger)
0.00%
0 / 1
6.73
 getBodyOutput
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getFullOutput
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getStartBody
44.83% covered (danger)
44.83%
13 / 29
0.00% covered (danger)
0.00%
0 / 1
12.05
 getEndBody
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getEmptyBody
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 formatRow
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
3.01
 getRowClass
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRowAttrs
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCurrentRow
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCellAttrs
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIndexField
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTableClass
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNavClass
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSortHeaderClass
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNavigationBar
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
30
 getModuleStyles
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getLimitSelect
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getLimitSelectList
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 getHiddenFields
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 getLimitForm
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 getLimitDropdown
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 isFieldSortable
n/a
0 / 0
n/a
0 / 0
0
 formatValue
n/a
0 / 0
n/a
0 / 0
0
 getDefaultSort
n/a
0 / 0
n/a
0 / 0
0
 getFieldNames
n/a
0 / 0
n/a
0 / 0
0
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21namespace MediaWiki\Pager;
22
23use MediaWiki\Context\IContextSource;
24use MediaWiki\Html\Html;
25use MediaWiki\Linker\LinkRenderer;
26use MediaWiki\Parser\ParserOutput;
27use MediaWiki\Xml\XmlSelect;
28use OOUI\ButtonGroupWidget;
29use OOUI\ButtonWidget;
30use stdClass;
31
32/**
33 * Table-based display with a user-selectable sort order
34 *
35 * @stable to extend
36 * @ingroup Pager
37 */
38abstract class TablePager extends IndexPager {
39    /** @var string */
40    protected $mSort;
41
42    /** @var stdClass */
43    protected $mCurrentRow;
44
45    /**
46     * @stable to call
47     *
48     * @param IContextSource|null $context
49     * @param LinkRenderer|null $linkRenderer
50     */
51    public function __construct( ?IContextSource $context = null, ?LinkRenderer $linkRenderer = null ) {
52        if ( $context ) {
53            $this->setContext( $context );
54        }
55
56        $this->mSort = $this->getRequest()->getText( 'sort' );
57        if ( !array_key_exists( $this->mSort, $this->getFieldNames() )
58            || !$this->isFieldSortable( $this->mSort )
59        ) {
60            $this->mSort = $this->getDefaultSort();
61        }
62        if ( $this->getRequest()->getBool( 'asc' ) ) {
63            $this->mDefaultDirection = IndexPager::DIR_ASCENDING;
64        } elseif ( $this->getRequest()->getBool( 'desc' ) ) {
65            $this->mDefaultDirection = IndexPager::DIR_DESCENDING;
66        } /* Else leave it at whatever the class default is */
67
68        // Parent constructor needs mSort set, so we call it last
69        parent::__construct( null, $linkRenderer );
70    }
71
72    /**
73     * Get the formatted result list.
74     *
75     * Calls getBody() and getModuleStyles() and builds a ParserOutput object. (This is a bit hacky
76     * but works well.)
77     *
78     * @since 1.24
79     * @return ParserOutput
80     */
81    public function getBodyOutput() {
82        $body = parent::getBody();
83
84        $pout = new ParserOutput;
85        $pout->setRawText( $body );
86        return $pout;
87    }
88
89    /**
90     * Get the formatted result list, with navigation bars.
91     *
92     * Calls getBody(), getNavigationBar() and getModuleStyles() and
93     * builds a ParserOutput object. (This is a bit hacky but works well.)
94     *
95     * @since 1.24
96     * @return ParserOutput
97     */
98    public function getFullOutput() {
99        $navigation = $this->getNavigationBar();
100        $body = parent::getBody();
101
102        $pout = new ParserOutput;
103        $pout->setRawText( $navigation . $body . $navigation );
104        $pout->addModuleStyles( $this->getModuleStyles() );
105        return $pout;
106    }
107
108    /**
109     * @stable to override
110     * @return string
111     */
112    protected function getStartBody() {
113        $sortClass = $this->getSortHeaderClass();
114
115        $s = '';
116        $fields = $this->getFieldNames();
117
118        // Make table header
119        foreach ( $fields as $field => $name ) {
120            if ( strval( $name ) == '' ) {
121                $s .= Html::rawElement( 'th', [], "\u{00A0}" ) . "\n";
122            } elseif ( $this->isFieldSortable( $field ) ) {
123                $query = [ 'sort' => $field, 'limit' => $this->mLimit ];
124                $linkType = null;
125                $class = null;
126
127                if ( $this->mSort == $field ) {
128                    // The table is sorted by this field already, make a link to sort in the other direction
129                    // We don't actually know in which direction other fields will be sorted by default…
130                    if ( $this->mDefaultDirection == IndexPager::DIR_DESCENDING ) {
131                        $linkType = 'asc';
132                        $class = "$sortClass mw-datatable-is-sorted mw-datatable-is-descending";
133                        $query['asc'] = '1';
134                        $query['desc'] = '';
135                    } else {
136                        $linkType = 'desc';
137                        $class = "$sortClass mw-datatable-is-sorted mw-datatable-is-ascending";
138                        $query['asc'] = '';
139                        $query['desc'] = '1';
140                    }
141                }
142
143                $link = $this->makeLink( htmlspecialchars( $name ), $query, $linkType );
144                $s .= Html::rawElement( 'th', [ 'class' => $class ], $link ) . "\n";
145            } else {
146                $s .= Html::element( 'th', [], $name ) . "\n";
147            }
148        }
149
150        $ret = Html::openElement( 'table', [
151            'class' => $this->getTableClass() ]
152        );
153        $ret .= Html::rawElement( 'thead', [], Html::rawElement( 'tr', [], "\n" . $s . "\n" ) );
154        $ret .= Html::openElement( 'tbody' ) . "\n";
155
156        return $ret;
157    }
158
159    /**
160     * @stable to override
161     * @return string
162     */
163    protected function getEndBody() {
164        return "</tbody></table>\n";
165    }
166
167    /**
168     * @return string
169     */
170    protected function getEmptyBody() {
171        $colspan = count( $this->getFieldNames() );
172        $msgEmpty = $this->msg( 'table_pager_empty' )->text();
173        return Html::rawElement( 'tr', [],
174            Html::element( 'td', [ 'colspan' => $colspan ], $msgEmpty ) );
175    }
176
177    /**
178     * @stable to override
179     * @param stdClass $row
180     * @return string HTML
181     */
182    public function formatRow( $row ) {
183        $this->mCurrentRow = $row; // In case formatValue etc need to know
184        $s = Html::openElement( 'tr', $this->getRowAttrs( $row ) ) . "\n";
185        $fieldNames = $this->getFieldNames();
186
187        foreach ( $fieldNames as $field => $name ) {
188            $value = $row->$field ?? null;
189            $formatted = strval( $this->formatValue( $field, $value ) );
190
191            if ( $formatted == '' ) {
192                $formatted = "\u{00A0}";
193            }
194
195            $s .= Html::rawElement( 'td', $this->getCellAttrs( $field, $value ), $formatted ) . "\n";
196        }
197
198        $s .= Html::closeElement( 'tr' ) . "\n";
199
200        return $s;
201    }
202
203    /**
204     * Get a class name to be applied to the given row.
205     *
206     * @stable to override
207     *
208     * @param stdClass $row The database result row
209     * @return string
210     */
211    protected function getRowClass( $row ) {
212        return '';
213    }
214
215    /**
216     * Get attributes to be applied to the given row.
217     *
218     * @stable to override
219     *
220     * @param stdClass $row The database result row
221     * @return array Array of attribute => value
222     */
223    protected function getRowAttrs( $row ) {
224        return [ 'class' => $this->getRowClass( $row ) ];
225    }
226
227    /**
228     * @return stdClass
229     */
230    protected function getCurrentRow() {
231        return $this->mCurrentRow;
232    }
233
234    /**
235     * Get any extra attributes to be applied to the given cell. Don't
236     * take this as an excuse to hardcode styles; use classes and
237     * CSS instead.  Row context is available in $this->mCurrentRow
238     *
239     * @stable to override
240     *
241     * @param string $field The column
242     * @param string $value The cell contents
243     * @return array Array of attr => value
244     */
245    protected function getCellAttrs( $field, $value ) {
246        return [ 'class' => 'TablePager_col_' . $field ];
247    }
248
249    /**
250     * @inheritDoc
251     * @stable to override
252     */
253    public function getIndexField() {
254        return $this->mSort;
255    }
256
257    /**
258     * TablePager relies on `mw-datatable` for styling, see T214208
259     *
260     * @stable to override
261     * @return string
262     */
263    protected function getTableClass() {
264        return 'mw-datatable';
265    }
266
267    /**
268     * @stable to override
269     * @return string
270     */
271    protected function getNavClass() {
272        return 'TablePager_nav';
273    }
274
275    /**
276     * @stable to override
277     * @return string
278     */
279    protected function getSortHeaderClass() {
280        return 'TablePager_sort';
281    }
282
283    /**
284     * A navigation bar with images
285     *
286     * @stable to override
287     * @return string HTML
288     */
289    public function getNavigationBar() {
290        if ( !$this->isNavigationBarShown() ) {
291            return '';
292        }
293
294        $this->getOutput()->enableOOUI();
295
296        $types = [ 'first', 'prev', 'next', 'last' ];
297
298        $queries = $this->getPagingQueries();
299
300        $buttons = [];
301
302        $title = $this->getTitle();
303
304        foreach ( $types as $type ) {
305            $buttons[] = new ButtonWidget( [
306                // Messages used here:
307                // * table_pager_first
308                // * table_pager_prev
309                // * table_pager_next
310                // * table_pager_last
311                'classes' => [ 'TablePager-button-' . $type ],
312                'flags' => [ 'progressive' ],
313                'framed' => false,
314                'label' => $this->msg( 'table_pager_' . $type )->text(),
315                'href' => $queries[ $type ] ?
316                    $title->getLinkURL( $queries[ $type ] + $this->getDefaultQuery() ) :
317                    null,
318                'icon' => $type === 'prev' ? 'previous' : $type,
319                'disabled' => $queries[ $type ] === false
320            ] );
321        }
322        return new ButtonGroupWidget( [
323            'classes' => [ $this->getNavClass() ],
324            'items' => $buttons,
325        ] );
326    }
327
328    /**
329     * @inheritDoc
330     */
331    public function getModuleStyles() {
332        return array_merge(
333            parent::getModuleStyles(), [ 'oojs-ui.styles.icons-movement' ]
334        );
335    }
336
337    /**
338     * Get a "<select>" element which has options for each of the allowed limits
339     *
340     * @param string[] $attribs Extra attributes to set
341     * @return string HTML fragment
342     */
343    public function getLimitSelect( array $attribs = [] ): string {
344        $select = new XmlSelect( 'limit', false, $this->mLimit );
345        $select->addOptions( $this->getLimitSelectList() );
346        foreach ( $attribs as $name => $value ) {
347            $select->setAttribute( $name, $value );
348        }
349        return $select->getHTML();
350    }
351
352    /**
353     * Get a list of items to show in a "<select>" element of limits.
354     * This can be passed directly to XmlSelect::addOptions().
355     *
356     * @since 1.22
357     * @return array
358     */
359    public function getLimitSelectList() {
360        # Add the current limit from the query string
361        # to avoid that the limit is lost after clicking Go next time
362        if ( !in_array( $this->mLimit, $this->mLimitsShown ) ) {
363            $this->mLimitsShown[] = $this->mLimit;
364            sort( $this->mLimitsShown );
365        }
366        $ret = [];
367        foreach ( $this->mLimitsShown as $key => $value ) {
368            # The pair is either $index => $limit, in which case the $value
369            # will be numeric, or $limit => $text, in which case the $value
370            # will be a string.
371            if ( is_int( $value ) ) {
372                $limit = $value;
373                $text = $this->getLanguage()->formatNum( $limit );
374            } else {
375                $limit = $key;
376                $text = $value;
377            }
378            $ret[$text] = $limit;
379        }
380        return $ret;
381    }
382
383    /**
384     * Get \<input type="hidden"\> elements for use in a method="get" form.
385     * Resubmits all defined elements of the query string, except for a
386     * exclusion list, passed in the $noResubmit parameter.
387     * Also array values are discarded for security reasons (per WebRequest::getVal)
388     *
389     * @param array $noResubmit Parameters from the request query which should not be resubmitted
390     * @return string HTML fragment
391     */
392    public function getHiddenFields( $noResubmit = [] ) {
393        $noResubmit = (array)$noResubmit;
394        $query = $this->getRequest()->getQueryValues();
395        foreach ( $noResubmit as $name ) {
396            unset( $query[$name] );
397        }
398        $s = '';
399        foreach ( $query as $name => $value ) {
400            if ( is_array( $value ) ) {
401                // Per WebRequest::getVal: Array values are discarded for security reasons.
402                continue;
403            }
404            $s .= Html::hidden( $name, $value ) . "\n";
405        }
406        return $s;
407    }
408
409    /**
410     * Get a form containing a limit selection dropdown
411     *
412     * @return string HTML fragment
413     */
414    public function getLimitForm() {
415        return Html::rawElement(
416            'form',
417            [
418                'method' => 'get',
419                'action' => wfScript(),
420            ],
421            "\n" . $this->getLimitDropdown()
422        ) . "\n";
423    }
424
425    /**
426     * Gets a limit selection dropdown
427     *
428     * @return string
429     */
430    private function getLimitDropdown() {
431        # Make the select with some explanatory text
432        $msgSubmit = $this->msg( 'table_pager_limit_submit' )->escaped();
433
434        return $this->msg( 'table_pager_limit' )
435            ->rawParams( $this->getLimitSelect() )->escaped() .
436            "\n<input type=\"submit\" value=\"$msgSubmit\"/>\n" .
437            $this->getHiddenFields( [ 'limit' ] );
438    }
439
440    /**
441     * Return true if the named field should be sortable by the UI, false
442     * otherwise
443     *
444     * @param string $field
445     * @return bool
446     */
447    abstract protected function isFieldSortable( $field );
448
449    /**
450     * Format a table cell. The return value should be HTML, but use an empty
451     * string not &#160; for empty cells. Do not include the <td> and </td>.
452     *
453     * The current result row is available as $this->mCurrentRow, in case you
454     * need more context.
455     *
456     * @param string $name The database field name
457     * @param string|null $value The value retrieved from the database, or null if
458     *   the row doesn't contain this field
459     */
460    abstract public function formatValue( $name, $value );
461
462    /**
463     * The database field name used as a default sort order.
464     *
465     * Note that this field will only be sorted on if isFieldSortable returns
466     * true for this field. If not (e.g. paginating on multiple columns), this
467     * should return empty string, and getIndexField should be overridden.
468     *
469     * @return string
470     */
471    abstract public function getDefaultSort();
472
473    /**
474     * An array mapping database field names to a textual description of the
475     * field name, for use in the table header. The description should be plain
476     * text, it will be HTML-escaped later.
477     *
478     * @return string[]
479     */
480    abstract protected function getFieldNames();
481}
482
483/** @deprecated class alias since 1.41 */
484class_alias( TablePager::class, 'TablePager' );