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