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.
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.
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
22 namespace MediaWiki\Pager
;
25 use MediaWiki\Cache\LinkBatchFactory
;
26 use MediaWiki\CommentFormatter\CommentFormatter
;
27 use MediaWiki\Context\IContextSource
;
28 use MediaWiki\HookContainer\HookContainer
;
29 use MediaWiki\Linker\LinkRenderer
;
30 use MediaWiki\MediaWikiServices
;
31 use MediaWiki\Revision\RevisionStore
;
32 use MediaWiki\SpecialPage\ContributionsRangeTrait
;
33 use MediaWiki\Title\NamespaceInfo
;
34 use MediaWiki\User\UserIdentity
;
35 use Wikimedia\IPUtils
;
36 use Wikimedia\Rdbms\IConnectionProvider
;
37 use Wikimedia\Rdbms\IExpression
;
38 use Wikimedia\Rdbms\IReadableDatabase
;
41 * Pager for Special:Contributions
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)
49 class ContribsPager
extends ContributionsPager
{
51 use ContributionsRangeTrait
;
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
66 public function __construct(
67 IContextSource
$context,
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
78 // Class is used directly in extensions - T266484
79 $services = MediaWikiServices
::getInstance();
80 $dbProvider ??
= $services->getConnectionProvider();
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(),
97 * Return the table targeted for ordering and continuation
99 * See T200259 and T221380.
101 * @warning Keep this in sync with self::getQueryInfo()!
105 private function getTargetTable() {
106 $dbr = $this->getDatabase();
107 $ipRangeConds = $this->targetUser
->isRegistered()
108 ?
null : $this->getIpRangeConds( $dbr, $this->target
);
109 if ( $ipRangeConds ) {
116 protected function getRevisionQuery() {
117 $revQuery = $this->revisionStore
->getQueryInfo( [ 'page', 'user' ] );
119 'tables' => $revQuery['tables'],
120 'fields' => array_merge( $revQuery['fields'], [ 'page_is_new' ] ),
123 'join_conds' => $revQuery['joins'],
126 // WARNING: Keep this in sync with getTargetTable()!
127 $ipRangeConds = !$this->targetUser
->isRegistered() ?
128 $this->getIpRangeConds( $this->getDatabase(), $this->target
) :
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' ]
136 $queryInfo['conds'][] = $ipRangeConds;
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';
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
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() ) ) {
158 [ $start, $end ] = IPUtils
::parseRange( $ip );
160 return $db->expr( 'ipc_hex', '>=', $start )->and( 'ipc_hex', '<=', $end );
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();
174 return 'rev_timestamp';
176 return 'ipc_rev_timestamp';
179 __METHOD__
. ": Unknown value '$target' from " . static::class . '::getTargetTable()', 0
181 return 'rev_timestamp';
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();
198 return [ 'ipc_rev_id' ];
201 __METHOD__
. ": Unknown value '$target' from " . static::class . '::getTargetTable()', 0
208 * Set up date filter options, given request data.
210 * @param array $opts Options array
211 * @return array Options array with processed start and end date filter options
213 public static function processDateFilter( array $opts ) {
214 $start = $opts['start'] ??
'';
215 $end = $opts['end'] ??
'';
216 $year = $opts['year'] ??
'';
217 $month = $opts['month'] ??
'';
219 if ( $start !== '' && $end !== '' && $start > $end ) {
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' );
234 // Clear the new timestamp range options if used and
235 // replace with the converted legacy timestamp
237 $end = $legacyDateTime->format( 'Y-m-d' );
240 $opts['start'] = $start;
248 * Retain the old class name for backwards compatibility.
249 * @deprecated since 1.41
251 class_alias( ContribsPager
::class, 'ContribsPager' );