Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
13.19% covered (danger)
13.19%
12 / 91
0.00% covered (danger)
0.00%
0 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
ReverseChronologicalPager
13.33% covered (danger)
13.33%
12 / 90
0.00% covered (danger)
0.00%
0 / 14
1135.27
0.00% covered (danger)
0.00%
0 / 1
 getHeaderRow
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 isHeaderRowNeeded
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 isFirstHeaderRow
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTimestampField
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 getDateFromTimestamp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRow
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 getStartGroup
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getEndGroup
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFooter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getNavigationBar
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 getDateCond
80.00% covered (warning)
80.00%
12 / 15
0.00% covered (danger)
0.00%
0 / 1
5.20
 getOffsetDate
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
182
 getEndOffset
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildQueryInfo
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
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 DateTime;
24use MediaWiki\Html\Html;
25use MediaWiki\Utils\MWTimestamp;
26use Wikimedia\Timestamp\TimestampException;
27
28/**
29 * IndexPager with a formatted navigation bar.
30 *
31 * @stable to extend
32 * @ingroup Pager
33 */
34abstract class ReverseChronologicalPager extends IndexPager {
35    /** @var bool */
36    public $mDefaultDirection = IndexPager::DIR_DESCENDING;
37    /** @var bool Whether to group items by date */
38    public $mGroupByDate = false;
39    /** @var int */
40    public $mYear;
41    /** @var int */
42    public $mMonth;
43    /** @var int */
44    public $mDay;
45    /** @var string */
46    private $lastHeaderDate;
47    /** @var string */
48    protected $endOffset;
49
50    /**
51     * @param string $date
52     * @return string
53     */
54    protected function getHeaderRow( string $date ): string {
55        $headingClass = $this->isFirstHeaderRow() ?
56            // We use mw-index-pager- prefix here on the anticipation that this method will
57            // eventually be upstreamed to apply to other pagers. For now we constrain the
58            // change to ReverseChronologicalPager to reduce the risk of pages this touches
59            // in case there are any bugs.
60            'mw-index-pager-list-header-first mw-index-pager-list-header' :
61            'mw-index-pager-list-header';
62
63        $s = $this->isFirstHeaderRow() ? '' : $this->getEndGroup();
64        $s .= Html::element( 'h4', [
65                'class' => $headingClass,
66            ],
67            $date
68        );
69        $s .= $this->getStartGroup();
70        return $s;
71    }
72
73    /**
74     * Determines if a header row is needed based on the current state of the IndexPager.
75     *
76     * @since 1.38
77     * @param string $date Formatted date header
78     * @return bool
79     */
80    protected function isHeaderRowNeeded( string $date ): bool {
81        if ( !$this->mGroupByDate ) {
82            return false;
83        }
84        return $date && $this->lastHeaderDate !== $date;
85    }
86
87    /**
88     * Determines whether the header row is the first that will be outputted to the page.
89     *
90     * @since 1.38
91     * @return bool
92     */
93    final protected function isFirstHeaderRow(): bool {
94        return $this->lastHeaderDate === null;
95    }
96
97    /**
98     * Returns the name of the timestamp field. Subclass can override this to provide the
99     * timestamp field if they are using a aliased field for getIndexField()
100     *
101     * @since 1.40
102     * @return string
103     */
104    public function getTimestampField() {
105        // This is a chronological pager, so the first column should be some kind of timestamp
106        return is_array( $this->mIndexField ) ? $this->mIndexField[0] : $this->mIndexField;
107    }
108
109    /**
110     * Get date from the timestamp
111     *
112     * @since 1.38
113     * @param string $timestamp
114     * @return string Formatted date header
115     */
116    final protected function getDateFromTimestamp( string $timestamp ) {
117        return $this->getLanguage()->userDate( $timestamp, $this->getUser() );
118    }
119
120    /**
121     * @inheritDoc
122     */
123    protected function getRow( $row ): string {
124        $s = '';
125
126        $timestampField = $this->getTimestampField();
127        $timestamp = $row->$timestampField ?? null;
128        $date = $timestamp ? $this->getDateFromTimestamp( $timestamp ) : null;
129        if ( $date && $this->isHeaderRowNeeded( $date ) ) {
130            $s .= $this->getHeaderRow( $date );
131            $this->lastHeaderDate = $date;
132        }
133
134        $s .= $this->formatRow( $row );
135        return $s;
136    }
137
138    /**
139     * Start a new group of page rows.
140     *
141     * @stable to override
142     * @since 1.38
143     * @return string
144     */
145    protected function getStartGroup(): string {
146        return "<ul class=\"mw-contributions-list\">\n";
147    }
148
149    /**
150     * End an existing group of page rows.
151     *
152     * @stable to override
153     * @since 1.38
154     * @return string
155     */
156    protected function getEndGroup(): string {
157        return '</ul>';
158    }
159
160    /**
161     * @inheritDoc
162     */
163    protected function getFooter(): string {
164        return $this->getEndGroup();
165    }
166
167    /**
168     * @stable to override
169     * @return string HTML
170     */
171    public function getNavigationBar() {
172        if ( !$this->isNavigationBarShown() ) {
173            return '';
174        }
175
176        if ( isset( $this->mNavigationBar ) ) {
177            return $this->mNavigationBar;
178        }
179
180        $navBuilder = $this->getNavigationBuilder()
181            ->setPrevMsg( 'pager-newer-n' )
182            ->setNextMsg( 'pager-older-n' )
183            ->setFirstMsg( 'histlast' )
184            ->setLastMsg( 'histfirst' );
185
186        $this->mNavigationBar = $navBuilder->getHtml();
187
188        return $this->mNavigationBar;
189    }
190
191    /**
192     * Set and return the offset timestamp such that we can get all revisions with
193     * a timestamp up to the specified parameters.
194     *
195     * @stable to override
196     *
197     * @param int $year Year up to which we want revisions
198     * @param int $month Month up to which we want revisions
199     * @param int $day [optional] Day up to which we want revisions. Default is end of month.
200     * @return string|null Timestamp or null if year and month are false/invalid
201     */
202    public function getDateCond( $year, $month, $day = -1 ) {
203        $year = (int)$year;
204        $month = (int)$month;
205        $day = (int)$day;
206
207        // Basic validity checks for year and month
208        // If year and month are invalid, don't update the offset
209        if ( $year <= 0 && ( $month <= 0 || $month >= 13 ) ) {
210            return null;
211        }
212
213        $timestamp = self::getOffsetDate( $year, $month, $day );
214
215        try {
216            // The timestamp used for DB queries is at midnight of the *next* day after the selected date.
217            $selectedDate = new DateTime( $timestamp->getTimestamp( TS_ISO_8601 ) );
218            $selectedDate = $selectedDate->modify( '-1 day' );
219
220            $this->mYear = (int)$selectedDate->format( 'Y' );
221            $this->mMonth = (int)$selectedDate->format( 'm' );
222            $this->mDay = (int)$selectedDate->format( 'd' );
223            // Don't mess with mOffset which IndexPager uses
224            $this->endOffset = $this->mDb->timestamp( $timestamp->getTimestamp() );
225        } catch ( TimestampException $e ) {
226            // Invalid user provided timestamp (T149257)
227            return null;
228        }
229
230        return $this->endOffset;
231    }
232
233    /**
234     * Core logic of determining the offset timestamp such that we can get all items with
235     * a timestamp up to the specified parameters. Given parameters for a day up to which to get
236     * items, this function finds the timestamp of the day just after the end of the range for use
237     * in a database strict inequality filter.
238     *
239     * This is separate from getDateCond so we can use this logic in other places, such as in
240     * RangeChronologicalPager, where this function is used to convert year/month/day filter options
241     * into a timestamp.
242     *
243     * @param int $year Year up to which we want revisions
244     * @param int $month Month up to which we want revisions
245     * @param int $day [optional] Day up to which we want revisions. Default is end of month.
246     * @return MWTimestamp Timestamp or null if year and month are false/invalid
247     */
248    public static function getOffsetDate( $year, $month, $day = -1 ) {
249        // Given an optional year, month, and day, we need to generate a timestamp
250        // to use as "WHERE rev_timestamp <= result"
251        // Examples: year = 2006      equals < 20070101 (+000000)
252        // year=2005, month=1         equals < 20050201
253        // year=2005, month=12        equals < 20060101
254        // year=2005, month=12, day=5 equals < 20051206
255        if ( $year <= 0 ) {
256            // If no year given, assume the current one
257            $timestamp = MWTimestamp::getInstance();
258            $year = $timestamp->format( 'Y' );
259            // If this month hasn't happened yet this year, go back to last year's month
260            if ( $month > $timestamp->format( 'n' ) ) {
261                $year--;
262            }
263        }
264
265        if ( $month && $month > 0 && $month < 13 ) {
266            // Day validity check after we have month and year checked
267            $day = checkdate( $month, $day, $year ) ? $day : false;
268
269            if ( $day && $day > 0 ) {
270                // If we have a day, we want up to the day immediately afterward
271                $day++;
272
273                // Did we overflow the current month?
274                if ( !checkdate( $month, $day, $year ) ) {
275                    $day = 1;
276                    $month++;
277                }
278            } else {
279                // If no day, assume beginning of next month
280                $day = 1;
281                $month++;
282            }
283
284            // Did we overflow the current year?
285            if ( $month > 12 ) {
286                $month = 1;
287                $year++;
288            }
289
290        } else {
291            // No month implies we want up to the end of the year in question
292            $month = 1;
293            $day = 1;
294            $year++;
295        }
296
297        // Y2K38 bug
298        if ( $year > 2032 ) {
299            $year = 2032;
300        }
301
302        $ymd = sprintf( "%04d%02d%02d", $year, $month, $day );
303
304        if ( $ymd > '20320101' ) {
305            $ymd = '20320101';
306        }
307
308        return MWTimestamp::getInstance( "{$ymd}000000" );
309    }
310
311    /**
312     * Return the end offset, extensions can use this if they are not in the context of subclass.
313     *
314     * @since 1.40
315     * @return string
316     */
317    public function getEndOffset() {
318        return $this->endOffset;
319    }
320
321    /**
322     * @inheritDoc
323     */
324    protected function buildQueryInfo( $offset, $limit, $order ) {
325        [ $tables, $fields, $conds, $fname, $options, $join_conds ] = parent::buildQueryInfo(
326            $offset,
327            $limit,
328            $order
329        );
330        if ( $this->endOffset ) {
331            $conds[] = $this->mDb->expr( $this->getTimestampField(), '<', $this->endOffset );
332        }
333
334        return [ $tables, $fields, $conds, $fname, $options, $join_conds ];
335    }
336}
337
338/** @deprecated class alias since 1.41 */
339class_alias( ReverseChronologicalPager::class, 'ReverseChronologicalPager' );