Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/api/ApiQueryUserContributions.php (10 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 *
4
 *
5
 * Created on Oct 16, 2006
6
 *
7
 * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
8
 *
9
 * This program is free software; you can redistribute it and/or modify
10
 * it under the terms of the GNU General Public License as published by
11
 * the Free Software Foundation; either version 2 of the License, or
12
 * (at your option) any later version.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU General Public License along
20
 * with this program; if not, write to the Free Software Foundation, Inc.,
21
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22
 * http://www.gnu.org/copyleft/gpl.html
23
 *
24
 * @file
25
 */
26
27
/**
28
 * This query action adds a list of a specified user's contributions to the output.
29
 *
30
 * @ingroup API
31
 */
32
class ApiQueryContributions extends ApiQueryBase {
33
34
	public function __construct( ApiQuery $query, $moduleName ) {
35
		parent::__construct( $query, $moduleName, 'uc' );
36
	}
37
38
	private $params, $prefixMode, $userprefix, $multiUserMode, $idMode, $usernames, $userids,
0 ignored issues
show
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
39
		$parentLens;
40
	private $fld_ids = false, $fld_title = false, $fld_timestamp = false,
0 ignored issues
show
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
41
		$fld_comment = false, $fld_parsedcomment = false, $fld_flags = false,
42
		$fld_patrolled = false, $fld_tags = false, $fld_size = false, $fld_sizediff = false;
43
44
	public function execute() {
45
		// Parse some parameters
46
		$this->params = $this->extractRequestParams();
47
48
		$prop = array_flip( $this->params['prop'] );
49
		$this->fld_ids = isset( $prop['ids'] );
50
		$this->fld_title = isset( $prop['title'] );
51
		$this->fld_comment = isset( $prop['comment'] );
52
		$this->fld_parsedcomment = isset( $prop['parsedcomment'] );
53
		$this->fld_size = isset( $prop['size'] );
54
		$this->fld_sizediff = isset( $prop['sizediff'] );
55
		$this->fld_flags = isset( $prop['flags'] );
56
		$this->fld_timestamp = isset( $prop['timestamp'] );
57
		$this->fld_patrolled = isset( $prop['patrolled'] );
58
		$this->fld_tags = isset( $prop['tags'] );
59
60
		// Most of this code will use the 'contributions' group DB, which can map to replica DBs
61
		// with extra user based indexes or partioning by user. The additional metadata
62
		// queries should use a regular replica DB since the lookup pattern is not all by user.
63
		$dbSecondary = $this->getDB(); // any random replica DB
64
65
		// TODO: if the query is going only against the revision table, should this be done?
66
		$this->selectNamedDB( 'contributions', DB_REPLICA, 'contributions' );
67
68
		$this->idMode = false;
69
		if ( isset( $this->params['userprefix'] ) ) {
70
			$this->prefixMode = true;
71
			$this->multiUserMode = true;
72
			$this->userprefix = $this->params['userprefix'];
73
		} else {
74
			$anyIPs = false;
75
			$this->userids = [];
76
			$this->usernames = [];
77
			if ( !is_array( $this->params['user'] ) ) {
78
				$this->params['user'] = [ $this->params['user'] ];
79
			}
80
			if ( !count( $this->params['user'] ) ) {
81
				$this->dieUsage( 'User parameter may not be empty.', 'param_user' );
82
			}
83
			foreach ( $this->params['user'] as $u ) {
0 ignored issues
show
The expression $this->params['user'] of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
84
				if ( is_null( $u ) || $u === '' ) {
85
					$this->dieUsage( 'User parameter may not be empty', 'param_user' );
86
				}
87
88
				if ( User::isIP( $u ) ) {
89
					$anyIPs = true;
90
					$this->usernames[] = $u;
91
				} else {
92
					$name = User::getCanonicalName( $u, 'valid' );
93
					if ( $name === false ) {
94
						$this->dieUsage( "User name {$u} is not valid", 'param_user' );
95
					}
96
					$this->usernames[] = $name;
97
				}
98
			}
99
			$this->prefixMode = false;
100
			$this->multiUserMode = ( count( $this->params['user'] ) > 1 );
101
102
			if ( !$anyIPs ) {
103
				$dbr = $this->getDB();
104
				$res = $dbr->select( 'user', 'user_id', [ 'user_name' => $this->usernames ], __METHOD__ );
105
				foreach ( $res as $row ) {
0 ignored issues
show
The expression $res of type boolean|object<ResultWrapper> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
106
					$this->userids[] = $row->user_id;
107
				}
108
				$this->idMode = count( $this->userids ) === count( $this->usernames );
109
			}
110
		}
111
112
		$this->prepareQuery();
113
114
		$hookData = [];
115
		// Do the actual query.
116
		$res = $this->select( __METHOD__, [], $hookData );
117
118
		if ( $this->fld_sizediff ) {
119
			$revIds = [];
120
			foreach ( $res as $row ) {
121
				if ( $row->rev_parent_id ) {
122
					$revIds[] = $row->rev_parent_id;
123
				}
124
			}
125
			$this->parentLens = Revision::getParentLengths( $dbSecondary, $revIds );
126
			$res->rewind(); // reset
127
		}
128
129
		// Initialise some variables
130
		$count = 0;
131
		$limit = $this->params['limit'];
132
133
		// Fetch each row
134
		foreach ( $res as $row ) {
135
			if ( ++$count > $limit ) {
136
				// We've reached the one extra which shows that there are
137
				// additional pages to be had. Stop here...
138
				$this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) );
139
				break;
140
			}
141
142
			$vals = $this->extractRowInfo( $row );
0 ignored issues
show
It seems like $row defined by $row on line 134 can be null; however, ApiQueryContributions::extractRowInfo() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
143
			$fit = $this->processRow( $row, $vals, $hookData ) &&
0 ignored issues
show
It seems like $row defined by $row on line 134 can be null; however, ApiQueryBase::processRow() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
It seems like $hookData can also be of type null; however, ApiQueryBase::processRow() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
144
				$this->getResult()->addValue( [ 'query', $this->getModuleName() ], null, $vals );
145
			if ( !$fit ) {
146
				$this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) );
147
				break;
148
			}
149
		}
150
151
		$this->getResult()->addIndexedTagName(
152
			[ 'query', $this->getModuleName() ],
153
			'item'
154
		);
155
	}
156
157
	/**
158
	 * Prepares the query and returns the limit of rows requested
159
	 */
160
	private function prepareQuery() {
161
		// We're after the revision table, and the corresponding page
162
		// row for anything we retrieve. We may also need the
163
		// recentchanges row and/or tag summary row.
164
		$user = $this->getUser();
165
		$tables = [ 'page', 'revision' ]; // Order may change
166
		$this->addWhere( 'page_id=rev_page' );
167
168
		// Handle continue parameter
169
		if ( !is_null( $this->params['continue'] ) ) {
170
			$continue = explode( '|', $this->params['continue'] );
171
			$db = $this->getDB();
172
			if ( $this->multiUserMode ) {
173
				$this->dieContinueUsageIf( count( $continue ) != 4 );
174
				$modeFlag = array_shift( $continue );
175
				$this->dieContinueUsageIf( !in_array( $modeFlag, [ 'id', 'name' ] ) );
176
				if ( $this->idMode && $modeFlag === 'name' ) {
177
					// The users were created since this query started, but we
178
					// can't go back and change modes now. So just keep on with
179
					// name mode.
180
					$this->idMode = false;
181
				}
182
				$this->dieContinueUsageIf( ( $modeFlag === 'id' ) !== $this->idMode );
183
				$userField = $this->idMode ? 'rev_user' : 'rev_user_text';
184
				$encUser = $db->addQuotes( array_shift( $continue ) );
185
			} else {
186
				$this->dieContinueUsageIf( count( $continue ) != 2 );
187
			}
188
			$encTS = $db->addQuotes( $db->timestamp( $continue[0] ) );
189
			$encId = (int)$continue[1];
190
			$this->dieContinueUsageIf( $encId != $continue[1] );
191
			$op = ( $this->params['dir'] == 'older' ? '<' : '>' );
192
			if ( $this->multiUserMode ) {
193
				$this->addWhere(
194
					"$userField $op $encUser OR " .
0 ignored issues
show
The variable $userField does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
The variable $encUser does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
195
					"($userField = $encUser AND " .
196
					"(rev_timestamp $op $encTS OR " .
197
					"(rev_timestamp = $encTS AND " .
198
					"rev_id $op= $encId)))"
199
				);
200
			} else {
201
				$this->addWhere(
202
					"rev_timestamp $op $encTS OR " .
203
					"(rev_timestamp = $encTS AND " .
204
					"rev_id $op= $encId)"
205
				);
206
			}
207
		}
208
209
		// Don't include any revisions where we're not supposed to be able to
210
		// see the username.
211
		if ( !$user->isAllowed( 'deletedhistory' ) ) {
212
			$bitmask = Revision::DELETED_USER;
213
		} elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
214
			$bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED;
215
		} else {
216
			$bitmask = 0;
217
		}
218
		if ( $bitmask ) {
219
			$this->addWhere( $this->getDB()->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask" );
220
		}
221
222
		// We only want pages by the specified users.
223
		if ( $this->prefixMode ) {
224
			$this->addWhere( 'rev_user_text' .
225
				$this->getDB()->buildLike( $this->userprefix, $this->getDB()->anyString() ) );
226
		} elseif ( $this->idMode ) {
227
			$this->addWhereFld( 'rev_user', $this->userids );
228
		} else {
229
			$this->addWhereFld( 'rev_user_text', $this->usernames );
230
		}
231
		// ... and in the specified timeframe.
232
		// Ensure the same sort order for rev_user/rev_user_text and rev_timestamp
233
		// so our query is indexed
234
		if ( $this->multiUserMode ) {
235
			$this->addWhereRange( $this->idMode ? 'rev_user' : 'rev_user_text',
236
				$this->params['dir'], null, null );
237
		}
238
		$this->addTimestampWhereRange( 'rev_timestamp',
239
			$this->params['dir'], $this->params['start'], $this->params['end'] );
240
		// Include in ORDER BY for uniqueness
241
		$this->addWhereRange( 'rev_id', $this->params['dir'], null, null );
242
243
		$this->addWhereFld( 'page_namespace', $this->params['namespace'] );
244
245
		$show = $this->params['show'];
246
		if ( $this->params['toponly'] ) { // deprecated/old param
247
			$show[] = 'top';
248
		}
249
		if ( !is_null( $show ) ) {
250
			$show = array_flip( $show );
251
252 View Code Duplication
			if ( ( isset( $show['minor'] ) && isset( $show['!minor'] ) )
253
				|| ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) )
254
				|| ( isset( $show['top'] ) && isset( $show['!top'] ) )
255
				|| ( isset( $show['new'] ) && isset( $show['!new'] ) )
256
			) {
257
				$this->dieUsageMsg( 'show' );
258
			}
259
260
			$this->addWhereIf( 'rev_minor_edit = 0', isset( $show['!minor'] ) );
261
			$this->addWhereIf( 'rev_minor_edit != 0', isset( $show['minor'] ) );
262
			$this->addWhereIf( 'rc_patrolled = 0', isset( $show['!patrolled'] ) );
263
			$this->addWhereIf( 'rc_patrolled != 0', isset( $show['patrolled'] ) );
264
			$this->addWhereIf( 'rev_id != page_latest', isset( $show['!top'] ) );
265
			$this->addWhereIf( 'rev_id = page_latest', isset( $show['top'] ) );
266
			$this->addWhereIf( 'rev_parent_id != 0', isset( $show['!new'] ) );
267
			$this->addWhereIf( 'rev_parent_id = 0', isset( $show['new'] ) );
268
		}
269
		$this->addOption( 'LIMIT', $this->params['limit'] + 1 );
270
271
		// Mandatory fields: timestamp allows request continuation
272
		// ns+title checks if the user has access rights for this page
273
		// user_text is necessary if multiple users were specified
274
		$this->addFields( [
275
			'rev_id',
276
			'rev_timestamp',
277
			'page_namespace',
278
			'page_title',
279
			'rev_user',
280
			'rev_user_text',
281
			'rev_deleted'
282
		] );
283
284
		if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ||
285
			$this->fld_patrolled
286
		) {
287
			if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
288
				$this->dieUsage(
289
					'You need the patrol right to request the patrolled flag',
290
					'permissiondenied'
291
				);
292
			}
293
294
			// Use a redundant join condition on both
295
			// timestamp and ID so we can use the timestamp
296
			// index
297
			$index['recentchanges'] = 'rc_user_text';
0 ignored issues
show
Coding Style Comprehensibility introduced by
$index was never initialized. Although not strictly required by PHP, it is generally a good practice to add $index = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
298
			if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ) {
299
				// Put the tables in the right order for
300
				// STRAIGHT_JOIN
301
				$tables = [ 'revision', 'recentchanges', 'page' ];
302
				$this->addOption( 'STRAIGHT_JOIN' );
303
				$this->addWhere( 'rc_user_text=rev_user_text' );
304
				$this->addWhere( 'rc_timestamp=rev_timestamp' );
305
				$this->addWhere( 'rc_this_oldid=rev_id' );
306
			} else {
307
				$tables[] = 'recentchanges';
308
				$this->addJoinConds( [ 'recentchanges' => [
309
					'LEFT JOIN', [
310
						'rc_user_text=rev_user_text',
311
						'rc_timestamp=rev_timestamp',
312
						'rc_this_oldid=rev_id' ] ] ] );
313
			}
314
		}
315
316
		$this->addTables( $tables );
317
		$this->addFieldsIf( 'rev_page', $this->fld_ids );
318
		$this->addFieldsIf( 'page_latest', $this->fld_flags );
319
		// $this->addFieldsIf( 'rev_text_id', $this->fld_ids ); // Should this field be exposed?
320
		$this->addFieldsIf( 'rev_comment', $this->fld_comment || $this->fld_parsedcomment );
321
		$this->addFieldsIf( 'rev_len', $this->fld_size || $this->fld_sizediff );
322
		$this->addFieldsIf( 'rev_minor_edit', $this->fld_flags );
323
		$this->addFieldsIf( 'rev_parent_id', $this->fld_flags || $this->fld_sizediff || $this->fld_ids );
324
		$this->addFieldsIf( 'rc_patrolled', $this->fld_patrolled );
325
326 View Code Duplication
		if ( $this->fld_tags ) {
327
			$this->addTables( 'tag_summary' );
328
			$this->addJoinConds(
329
				[ 'tag_summary' => [ 'LEFT JOIN', [ 'rev_id=ts_rev_id' ] ] ]
330
			);
331
			$this->addFields( 'ts_tags' );
332
		}
333
334
		if ( isset( $this->params['tag'] ) ) {
335
			$this->addTables( 'change_tag' );
336
			$this->addJoinConds(
337
				[ 'change_tag' => [ 'INNER JOIN', [ 'rev_id=ct_rev_id' ] ] ]
338
			);
339
			$this->addWhereFld( 'ct_tag', $this->params['tag'] );
340
		}
341
342
		if ( isset( $index ) ) {
343
			$this->addOption( 'USE INDEX', $index );
344
		}
345
	}
346
347
	/**
348
	 * Extract fields from the database row and append them to a result array
349
	 *
350
	 * @param stdClass $row
351
	 * @return array
352
	 */
353
	private function extractRowInfo( $row ) {
354
		$vals = [];
355
		$anyHidden = false;
356
357
		if ( $row->rev_deleted & Revision::DELETED_TEXT ) {
358
			$vals['texthidden'] = true;
359
			$anyHidden = true;
360
		}
361
362
		// Any rows where we can't view the user were filtered out in the query.
363
		$vals['userid'] = (int)$row->rev_user;
364
		$vals['user'] = $row->rev_user_text;
365
		if ( $row->rev_deleted & Revision::DELETED_USER ) {
366
			$vals['userhidden'] = true;
367
			$anyHidden = true;
368
		}
369
		if ( $this->fld_ids ) {
370
			$vals['pageid'] = intval( $row->rev_page );
371
			$vals['revid'] = intval( $row->rev_id );
372
			// $vals['textid'] = intval( $row->rev_text_id ); // todo: Should this field be exposed?
373
374
			if ( !is_null( $row->rev_parent_id ) ) {
375
				$vals['parentid'] = intval( $row->rev_parent_id );
376
			}
377
		}
378
379
		$title = Title::makeTitle( $row->page_namespace, $row->page_title );
380
381
		if ( $this->fld_title ) {
382
			ApiQueryBase::addTitleInfo( $vals, $title );
383
		}
384
385
		if ( $this->fld_timestamp ) {
386
			$vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->rev_timestamp );
387
		}
388
389
		if ( $this->fld_flags ) {
390
			$vals['new'] = $row->rev_parent_id == 0 && !is_null( $row->rev_parent_id );
391
			$vals['minor'] = (bool)$row->rev_minor_edit;
392
			$vals['top'] = $row->page_latest == $row->rev_id;
393
		}
394
395 View Code Duplication
		if ( ( $this->fld_comment || $this->fld_parsedcomment ) && isset( $row->rev_comment ) ) {
396
			if ( $row->rev_deleted & Revision::DELETED_COMMENT ) {
397
				$vals['commenthidden'] = true;
398
				$anyHidden = true;
399
			}
400
401
			$userCanView = Revision::userCanBitfield(
402
				$row->rev_deleted,
403
				Revision::DELETED_COMMENT, $this->getUser()
404
			);
405
406
			if ( $userCanView ) {
407
				if ( $this->fld_comment ) {
408
					$vals['comment'] = $row->rev_comment;
409
				}
410
411
				if ( $this->fld_parsedcomment ) {
412
					$vals['parsedcomment'] = Linker::formatComment( $row->rev_comment, $title );
413
				}
414
			}
415
		}
416
417
		if ( $this->fld_patrolled ) {
418
			$vals['patrolled'] = (bool)$row->rc_patrolled;
419
		}
420
421
		if ( $this->fld_size && !is_null( $row->rev_len ) ) {
422
			$vals['size'] = intval( $row->rev_len );
423
		}
424
425
		if ( $this->fld_sizediff
426
			&& !is_null( $row->rev_len )
427
			&& !is_null( $row->rev_parent_id )
428
		) {
429
			$parentLen = isset( $this->parentLens[$row->rev_parent_id] )
430
				? $this->parentLens[$row->rev_parent_id]
431
				: 0;
432
			$vals['sizediff'] = intval( $row->rev_len - $parentLen );
433
		}
434
435 View Code Duplication
		if ( $this->fld_tags ) {
436
			if ( $row->ts_tags ) {
437
				$tags = explode( ',', $row->ts_tags );
438
				ApiResult::setIndexedTagName( $tags, 'tag' );
439
				$vals['tags'] = $tags;
440
			} else {
441
				$vals['tags'] = [];
442
			}
443
		}
444
445
		if ( $anyHidden && $row->rev_deleted & Revision::DELETED_RESTRICTED ) {
446
			$vals['suppressed'] = true;
447
		}
448
449
		return $vals;
450
	}
451
452
	private function continueStr( $row ) {
453
		if ( $this->multiUserMode ) {
454
			if ( $this->idMode ) {
455
				return "id|$row->rev_user|$row->rev_timestamp|$row->rev_id";
456
			} else {
457
				return "name|$row->rev_user_text|$row->rev_timestamp|$row->rev_id";
458
			}
459
		} else {
460
			return "$row->rev_timestamp|$row->rev_id";
461
		}
462
	}
463
464
	public function getCacheMode( $params ) {
465
		// This module provides access to deleted revisions and patrol flags if
466
		// the requester is logged in
467
		return 'anon-public-user-private';
468
	}
469
470
	public function getAllowedParams() {
471
		return [
472
			'limit' => [
473
				ApiBase::PARAM_DFLT => 10,
474
				ApiBase::PARAM_TYPE => 'limit',
475
				ApiBase::PARAM_MIN => 1,
476
				ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
477
				ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2
478
			],
479
			'start' => [
480
				ApiBase::PARAM_TYPE => 'timestamp'
481
			],
482
			'end' => [
483
				ApiBase::PARAM_TYPE => 'timestamp'
484
			],
485
			'continue' => [
486
				ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
487
			],
488
			'user' => [
489
				ApiBase::PARAM_TYPE => 'user',
490
				ApiBase::PARAM_ISMULTI => true
491
			],
492
			'userprefix' => null,
493
			'dir' => [
494
				ApiBase::PARAM_DFLT => 'older',
495
				ApiBase::PARAM_TYPE => [
496
					'newer',
497
					'older'
498
				],
499
				ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
500
			],
501
			'namespace' => [
502
				ApiBase::PARAM_ISMULTI => true,
503
				ApiBase::PARAM_TYPE => 'namespace'
504
			],
505
			'prop' => [
506
				ApiBase::PARAM_ISMULTI => true,
507
				ApiBase::PARAM_DFLT => 'ids|title|timestamp|comment|size|flags',
508
				ApiBase::PARAM_TYPE => [
509
					'ids',
510
					'title',
511
					'timestamp',
512
					'comment',
513
					'parsedcomment',
514
					'size',
515
					'sizediff',
516
					'flags',
517
					'patrolled',
518
					'tags'
519
				],
520
				ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
521
			],
522
			'show' => [
523
				ApiBase::PARAM_ISMULTI => true,
524
				ApiBase::PARAM_TYPE => [
525
					'minor',
526
					'!minor',
527
					'patrolled',
528
					'!patrolled',
529
					'top',
530
					'!top',
531
					'new',
532
					'!new',
533
				],
534
				ApiBase::PARAM_HELP_MSG => [
535
					'apihelp-query+usercontribs-param-show',
536
					$this->getConfig()->get( 'RCMaxAge' )
537
				],
538
			],
539
			'tag' => null,
540
			'toponly' => [
541
				ApiBase::PARAM_DFLT => false,
542
				ApiBase::PARAM_DEPRECATED => true,
543
			],
544
		];
545
	}
546
547
	protected function getExamplesMessages() {
548
		return [
549
			'action=query&list=usercontribs&ucuser=Example'
550
				=> 'apihelp-query+usercontribs-example-user',
551
			'action=query&list=usercontribs&ucuserprefix=192.0.2.'
552
				=> 'apihelp-query+usercontribs-example-ipprefix',
553
		];
554
	}
555
556
	public function getHelpUrls() {
557
		return 'https://www.mediawiki.org/wiki/API:Usercontribs';
558
	}
559
}
560