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