Completed
Branch master (939199)
by
unknown
39:35
created

includes/api/ApiQueryUserContributions.php (1 issue)

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,
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 ) {
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 ) {
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 );
143
			$fit = $this->processRow( $row, $vals, $hookData ) &&
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 " .
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';
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