Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
85.00% covered (warning)
85.00%
68 / 80
57.14% covered (warning)
57.14%
4 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ContribsPager
86.08% covered (warning)
86.08%
68 / 79
57.14% covered (warning)
57.14%
4 / 7
24.43
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 getTargetTable
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getRevisionQuery
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
3
 getIpRangeConds
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getIndexField
55.56% covered (warning)
55.56%
5 / 9
0.00% covered (danger)
0.00%
0 / 1
5.40
 getExtraSortFields
55.56% covered (warning)
55.56%
5 / 9
0.00% covered (danger)
0.00%
0 / 1
5.40
 processDateFilter
82.35% covered (warning)
82.35%
14 / 17
0.00% covered (danger)
0.00%
0 / 1
6.20
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 * @ingroup Pager
20 */
21
22namespace MediaWiki\Pager;
23
24use DateTime;
25use MediaWiki\Cache\LinkBatchFactory;
26use MediaWiki\CommentFormatter\CommentFormatter;
27use MediaWiki\Context\IContextSource;
28use MediaWiki\HookContainer\HookContainer;
29use MediaWiki\Linker\LinkRenderer;
30use MediaWiki\MediaWikiServices;
31use MediaWiki\Revision\RevisionStore;
32use MediaWiki\SpecialPage\ContributionsRangeTrait;
33use MediaWiki\Title\NamespaceInfo;
34use MediaWiki\User\UserIdentity;
35use Wikimedia\IPUtils;
36use Wikimedia\Rdbms\IConnectionProvider;
37use Wikimedia\Rdbms\IExpression;
38use Wikimedia\Rdbms\IReadableDatabase;
39
40/**
41 * Pager for Special:Contributions
42 *
43 * Most of the work is done by the parent class. This class:
44 * - handles using the ip_changes table in case of an IP range target
45 * - provides static utility functions (kept here for backwards compatibility)
46 *
47 * @ingroup Pager
48 */
49class ContribsPager extends ContributionsPager {
50
51    use ContributionsRangeTrait;
52
53    /**
54     * FIXME List services first T266484 / T290405
55     * @param IContextSource $context
56     * @param array $options
57     * @param LinkRenderer|null $linkRenderer
58     * @param LinkBatchFactory|null $linkBatchFactory
59     * @param HookContainer|null $hookContainer
60     * @param IConnectionProvider|null $dbProvider
61     * @param RevisionStore|null $revisionStore
62     * @param NamespaceInfo|null $namespaceInfo
63     * @param UserIdentity|null $targetUser
64     * @param CommentFormatter|null $commentFormatter
65     */
66    public function __construct(
67        IContextSource $context,
68        array $options,
69        ?LinkRenderer $linkRenderer = null,
70        ?LinkBatchFactory $linkBatchFactory = null,
71        ?HookContainer $hookContainer = null,
72        ?IConnectionProvider $dbProvider = null,
73        ?RevisionStore $revisionStore = null,
74        ?NamespaceInfo $namespaceInfo = null,
75        ?UserIdentity $targetUser = null,
76        ?CommentFormatter $commentFormatter = null
77    ) {
78        // Class is used directly in extensions - T266484
79        $services = MediaWikiServices::getInstance();
80        $dbProvider ??= $services->getConnectionProvider();
81
82        parent::__construct(
83            $linkRenderer ?? $services->getLinkRenderer(),
84            $linkBatchFactory ?? $services->getLinkBatchFactory(),
85            $hookContainer ?? $services->getHookContainer(),
86            $revisionStore ?? $services->getRevisionStore(),
87            $namespaceInfo ?? $services->getNamespaceInfo(),
88            $commentFormatter ?? $services->getCommentFormatter(),
89            $services->getUserFactory(),
90            $context,
91            $options,
92            $targetUser
93        );
94    }
95
96    /**
97     * Return the table targeted for ordering and continuation
98     *
99     * See T200259 and T221380.
100     *
101     * @warning Keep this in sync with self::getQueryInfo()!
102     *
103     * @return string
104     */
105    private function getTargetTable() {
106        $dbr = $this->getDatabase();
107        $ipRangeConds = $this->targetUser->isRegistered()
108            ? null : $this->getIpRangeConds( $dbr, $this->target );
109        if ( $ipRangeConds ) {
110            return 'ip_changes';
111        }
112
113        return 'revision';
114    }
115
116    protected function getRevisionQuery() {
117        $revQuery = $this->revisionStore->getQueryInfo( [ 'page', 'user' ] );
118        $queryInfo = [
119            'tables' => $revQuery['tables'],
120            'fields' => array_merge( $revQuery['fields'], [ 'page_is_new' ] ),
121            'conds' => [],
122            'options' => [],
123            'join_conds' => $revQuery['joins'],
124        ];
125
126        // WARNING: Keep this in sync with getTargetTable()!
127        $ipRangeConds = !$this->targetUser->isRegistered() ?
128            $this->getIpRangeConds( $this->getDatabase(), $this->target ) :
129            null;
130        if ( $ipRangeConds ) {
131            // Put ip_changes first (T284419)
132            array_unshift( $queryInfo['tables'], 'ip_changes' );
133            $queryInfo['join_conds']['revision'] = [
134                'JOIN', [ 'rev_id = ipc_rev_id' ]
135            ];
136            $queryInfo['conds'][] = $ipRangeConds;
137        } else {
138            $queryInfo['conds']['actor_name'] = $this->targetUser->getName();
139            // Force the appropriate index to avoid bad query plans (T307295)
140            $queryInfo['options']['USE INDEX']['revision'] = 'rev_actor_timestamp';
141        }
142
143        return $queryInfo;
144    }
145
146    /**
147     * Get SQL conditions for an IP range, if applicable
148     * @param IReadableDatabase $db
149     * @param string $ip The IP address or CIDR
150     * @return IExpression|false SQL for valid IP ranges, false if invalid
151     */
152    private function getIpRangeConds( $db, $ip ) {
153        // First make sure it is a valid range and they are not outside the CIDR limit
154        if ( !$this->isQueryableRange( $ip, $this->getConfig() ) ) {
155            return false;
156        }
157
158        [ $start, $end ] = IPUtils::parseRange( $ip );
159
160        return $db->expr( 'ipc_hex', '>=', $start )->and( 'ipc_hex', '<=', $end );
161    }
162
163    /**
164     * @return string
165     */
166    public function getIndexField() {
167        // The returned column is used for sorting and continuation, so we need to
168        // make sure to use the right denormalized column depending on which table is
169        // being targeted by the query to avoid bad query plans.
170        // See T200259, T204669, T220991, and T221380.
171        $target = $this->getTargetTable();
172        switch ( $target ) {
173            case 'revision':
174                return 'rev_timestamp';
175            case 'ip_changes':
176                return 'ipc_rev_timestamp';
177            default:
178                wfWarn(
179                    __METHOD__ . ": Unknown value '$target' from " . static::class . '::getTargetTable()', 0
180                );
181                return 'rev_timestamp';
182        }
183    }
184
185    /**
186     * @return string[]
187     */
188    protected function getExtraSortFields() {
189        // The returned columns are used for sorting, so we need to make sure
190        // to use the right denormalized column depending on which table is
191        // being targeted by the query to avoid bad query plans.
192        // See T200259, T204669, T220991, and T221380.
193        $target = $this->getTargetTable();
194        switch ( $target ) {
195            case 'revision':
196                return [ 'rev_id' ];
197            case 'ip_changes':
198                return [ 'ipc_rev_id' ];
199            default:
200                wfWarn(
201                    __METHOD__ . ": Unknown value '$target' from " . static::class . '::getTargetTable()', 0
202                );
203                return [ 'rev_id' ];
204        }
205    }
206
207    /**
208     * Set up date filter options, given request data.
209     *
210     * @param array $opts Options array
211     * @return array Options array with processed start and end date filter options
212     */
213    public static function processDateFilter( array $opts ) {
214        $start = $opts['start'] ?? '';
215        $end = $opts['end'] ?? '';
216        $year = $opts['year'] ?? '';
217        $month = $opts['month'] ?? '';
218
219        if ( $start !== '' && $end !== '' && $start > $end ) {
220            $temp = $start;
221            $start = $end;
222            $end = $temp;
223        }
224
225        // If year/month legacy filtering options are set, convert them to display the new stamp
226        if ( $year !== '' || $month !== '' ) {
227            // Reuse getDateCond logic, but subtract a day because
228            // the endpoints of our date range appear inclusive
229            // but the internal end offsets are always exclusive
230            $legacyTimestamp = ReverseChronologicalPager::getOffsetDate( $year, $month );
231            $legacyDateTime = new DateTime( $legacyTimestamp->getTimestamp( TS_ISO_8601 ) );
232            $legacyDateTime = $legacyDateTime->modify( '-1 day' );
233
234            // Clear the new timestamp range options if used and
235            // replace with the converted legacy timestamp
236            $start = '';
237            $end = $legacyDateTime->format( 'Y-m-d' );
238        }
239
240        $opts['start'] = $start;
241        $opts['end'] = $end;
242
243        return $opts;
244    }
245}
246
247/**
248 * Retain the old class name for backwards compatibility.
249 * @deprecated since 1.41
250 */
251class_alias( ContribsPager::class, 'ContribsPager' );