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