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