ApiQueryRevisions::run()   F
last analyzed

Complexity

Conditions 64
Paths > 20000

Size

Total Lines 289
Code Lines 180

Duplication

Lines 62
Ratio 21.45 %

Importance

Changes 0
Metric Value
cc 64
eloc 180
nc 145367056
nop 1
dl 62
loc 289
rs 2
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 *
4
 *
5
 * Created on Sep 7, 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
 * A query action to enumerate revisions of a given page, or show top revisions
29
 * of multiple pages. Various pieces of information may be shown - flags,
30
 * comments, and the actual wiki markup of the rev. In the enumeration mode,
31
 * ranges of revisions may be requested and filtered.
32
 *
33
 * @ingroup API
34
 */
35
class ApiQueryRevisions extends ApiQueryRevisionsBase {
36
37
	private $token = null;
38
39
	public function __construct( ApiQuery $query, $moduleName ) {
40
		parent::__construct( $query, $moduleName, 'rv' );
41
	}
42
43
	private $tokenFunctions;
44
45
	/** @deprecated since 1.24 */
46 View Code Duplication
	protected function getTokenFunctions() {
47
		// tokenname => function
48
		// function prototype is func($pageid, $title, $rev)
49
		// should return token or false
50
51
		// Don't call the hooks twice
52
		if ( isset( $this->tokenFunctions ) ) {
53
			return $this->tokenFunctions;
54
		}
55
56
		// If we're in a mode that breaks the same-origin policy, no tokens can
57
		// be obtained
58
		if ( $this->lacksSameOriginSecurity() ) {
59
			return [];
60
		}
61
62
		$this->tokenFunctions = [
63
			'rollback' => [ 'ApiQueryRevisions', 'getRollbackToken' ]
64
		];
65
		Hooks::run( 'APIQueryRevisionsTokens', [ &$this->tokenFunctions ] );
66
67
		return $this->tokenFunctions;
68
	}
69
70
	/**
71
	 * @deprecated since 1.24
72
	 * @param int $pageid
73
	 * @param Title $title
74
	 * @param Revision $rev
75
	 * @return bool|string
76
	 */
77
	public static function getRollbackToken( $pageid, $title, $rev ) {
78
		global $wgUser;
79
		if ( !$wgUser->isAllowed( 'rollback' ) ) {
80
			return false;
81
		}
82
83
		return $wgUser->getEditToken( 'rollback' );
84
	}
85
86
	protected function run( ApiPageSet $resultPageSet = null ) {
87
		$params = $this->extractRequestParams( false );
88
89
		// If any of those parameters are used, work in 'enumeration' mode.
90
		// Enum mode can only be used when exactly one page is provided.
91
		// Enumerating revisions on multiple pages make it extremely
92
		// difficult to manage continuations and require additional SQL indexes
93
		$enumRevMode = ( $params['user'] !== null || $params['excludeuser'] !== null ||
94
			$params['limit'] !== null || $params['startid'] !== null ||
95
			$params['endid'] !== null || $params['dir'] === 'newer' ||
96
			$params['start'] !== null || $params['end'] !== null );
97
98
		$pageSet = $this->getPageSet();
99
		$pageCount = $pageSet->getGoodTitleCount();
100
		$revCount = $pageSet->getRevisionCount();
101
102
		// Optimization -- nothing to do
103
		if ( $revCount === 0 && $pageCount === 0 ) {
104
			// Nothing to do
105
			return;
106
		}
107
		if ( $revCount > 0 && count( $pageSet->getLiveRevisionIDs() ) === 0 ) {
108
			// We're in revisions mode but all given revisions are deleted
109
			return;
110
		}
111
112
		if ( $revCount > 0 && $enumRevMode ) {
113
			$this->dieUsage(
114
				'The revids= parameter may not be used with the list options ' .
115
					'(limit, startid, endid, dirNewer, start, end).',
116
				'revids'
117
			);
118
		}
119
120
		if ( $pageCount > 1 && $enumRevMode ) {
121
			$this->dieUsage(
122
				'titles, pageids or a generator was used to supply multiple pages, ' .
123
					'but the limit, startid, endid, dirNewer, user, excludeuser, start ' .
124
					'and end parameters may only be used on a single page.',
125
				'multpages'
126
			);
127
		}
128
129
		// In non-enum mode, rvlimit can't be directly used. Use the maximum
130
		// allowed value.
131
		if ( !$enumRevMode ) {
132
			$this->setParsedLimit = false;
133
			$params['limit'] = 'max';
134
		}
135
136
		$db = $this->getDB();
137
		$this->addTables( [ 'revision', 'page' ] );
138
		$this->addJoinConds(
139
			[ 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ] ]
140
		);
141
142
		if ( $resultPageSet === null ) {
143
			$this->parseParameters( $params );
144
			$this->token = $params['token'];
145
			$this->addFields( Revision::selectFields() );
146
			if ( $this->token !== null || $pageCount > 0 ) {
147
				$this->addFields( Revision::selectPageFields() );
148
			}
149
		} else {
150
			$this->limit = $this->getParameter( 'limit' ) ?: 10;
151
			$this->addFields( [ 'rev_id', 'rev_timestamp', 'rev_page' ] );
152
		}
153
154 View Code Duplication
		if ( $this->fld_tags ) {
155
			$this->addTables( 'tag_summary' );
156
			$this->addJoinConds(
157
				[ 'tag_summary' => [ 'LEFT JOIN', [ 'rev_id=ts_rev_id' ] ] ]
158
			);
159
			$this->addFields( 'ts_tags' );
160
		}
161
162 View Code Duplication
		if ( $params['tag'] !== null ) {
163
			$this->addTables( 'change_tag' );
164
			$this->addJoinConds(
165
				[ 'change_tag' => [ 'INNER JOIN', [ 'rev_id=ct_rev_id' ] ] ]
166
			);
167
			$this->addWhereFld( 'ct_tag', $params['tag'] );
168
		}
169
170
		if ( $this->fetchContent ) {
171
			// For each page we will request, the user must have read rights for that page
172
			$user = $this->getUser();
173
			/** @var $title Title */
174
			foreach ( $pageSet->getGoodTitles() as $title ) {
175
				if ( !$title->userCan( 'read', $user ) ) {
176
					$this->dieUsage(
177
						'The current user is not allowed to read ' . $title->getPrefixedText(),
178
						'accessdenied' );
179
				}
180
			}
181
182
			$this->addTables( 'text' );
183
			$this->addJoinConds(
184
				[ 'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ] ]
185
			);
186
			$this->addFields( 'old_id' );
187
			$this->addFields( Revision::selectTextFields() );
188
		}
189
190
		// add user name, if needed
191
		if ( $this->fld_user ) {
192
			$this->addTables( 'user' );
193
			$this->addJoinConds( [ 'user' => Revision::userJoinCond() ] );
194
			$this->addFields( Revision::selectUserFields() );
195
		}
196
197
		if ( $enumRevMode ) {
198
			// Indexes targeted:
199
			//  page_timestamp if we don't have rvuser
200
			//  page_user_timestamp if we have a logged-in rvuser
201
			//  page_timestamp or usertext_timestamp if we have an IP rvuser
202
203
			// This is mostly to prevent parameter errors (and optimize SQL?)
204
			if ( $params['startid'] !== null && $params['start'] !== null ) {
205
				$this->dieUsage( 'start and startid cannot be used together', 'badparams' );
206
			}
207
208
			if ( $params['endid'] !== null && $params['end'] !== null ) {
209
				$this->dieUsage( 'end and endid cannot be used together', 'badparams' );
210
			}
211
212
			if ( $params['user'] !== null && $params['excludeuser'] !== null ) {
213
				$this->dieUsage( 'user and excludeuser cannot be used together', 'badparams' );
214
			}
215
216
			if ( $params['continue'] !== null ) {
217
				$cont = explode( '|', $params['continue'] );
218
				$this->dieContinueUsageIf( count( $cont ) != 2 );
219
				$op = ( $params['dir'] === 'newer' ? '>' : '<' );
220
				$continueTimestamp = $db->addQuotes( $db->timestamp( $cont[0] ) );
221
				$continueId = (int)$cont[1];
222
				$this->dieContinueUsageIf( $continueId != $cont[1] );
223
				$this->addWhere( "rev_timestamp $op $continueTimestamp OR " .
224
					"(rev_timestamp = $continueTimestamp AND " .
225
					"rev_id $op= $continueId)"
226
				);
227
			}
228
229
			$this->addTimestampWhereRange( 'rev_timestamp', $params['dir'],
230
				$params['start'], $params['end'] );
231
			$this->addWhereRange( 'rev_id', $params['dir'],
232
				$params['startid'], $params['endid'] );
233
234
			// There is only one ID, use it
235
			$ids = array_keys( $pageSet->getGoodTitles() );
236
			$this->addWhereFld( 'rev_page', reset( $ids ) );
237
238
			if ( $params['user'] !== null ) {
239
				$user = User::newFromName( $params['user'] );
240
				if ( $user && $user->getId() > 0 ) {
241
					$this->addWhereFld( 'rev_user', $user->getId() );
242
				} else {
243
					$this->addWhereFld( 'rev_user_text', $params['user'] );
244
				}
245 View Code Duplication
			} elseif ( $params['excludeuser'] !== null ) {
246
				$user = User::newFromName( $params['excludeuser'] );
247
				if ( $user && $user->getId() > 0 ) {
248
					$this->addWhere( 'rev_user != ' . $user->getId() );
249
				} else {
250
					$this->addWhere( 'rev_user_text != ' .
251
						$db->addQuotes( $params['excludeuser'] ) );
252
				}
253
			}
254 View Code Duplication
			if ( $params['user'] !== null || $params['excludeuser'] !== null ) {
255
				// Paranoia: avoid brute force searches (bug 17342)
256
				if ( !$this->getUser()->isAllowed( 'deletedhistory' ) ) {
257
					$bitmask = Revision::DELETED_USER;
258
				} elseif ( !$this->getUser()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
259
					$bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED;
260
				} else {
261
					$bitmask = 0;
262
				}
263
				if ( $bitmask ) {
264
					$this->addWhere( $db->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask" );
265
				}
266
			}
267
		} elseif ( $revCount > 0 ) {
268
			// Always targets the PRIMARY index
269
270
			$revs = $pageSet->getLiveRevisionIDs();
271
272
			// Get all revision IDs
273
			$this->addWhereFld( 'rev_id', array_keys( $revs ) );
274
275
			if ( $params['continue'] !== null ) {
276
				$this->addWhere( 'rev_id >= ' . intval( $params['continue'] ) );
277
			}
278
			$this->addOption( 'ORDER BY', 'rev_id' );
279
		} elseif ( $pageCount > 0 ) {
280
			// Always targets the rev_page_id index
281
282
			$titles = $pageSet->getGoodTitles();
283
284
			// When working in multi-page non-enumeration mode,
285
			// limit to the latest revision only
286
			$this->addWhere( 'page_latest=rev_id' );
287
288
			// Get all page IDs
289
			$this->addWhereFld( 'page_id', array_keys( $titles ) );
290
			// Every time someone relies on equality propagation, god kills a kitten :)
291
			$this->addWhereFld( 'rev_page', array_keys( $titles ) );
292
293
			if ( $params['continue'] !== null ) {
294
				$cont = explode( '|', $params['continue'] );
295
				$this->dieContinueUsageIf( count( $cont ) != 2 );
296
				$pageid = intval( $cont[0] );
297
				$revid = intval( $cont[1] );
298
				$this->addWhere(
299
					"rev_page > $pageid OR " .
300
					"(rev_page = $pageid AND " .
301
					"rev_id >= $revid)"
302
				);
303
			}
304
			$this->addOption( 'ORDER BY', [
305
				'rev_page',
306
				'rev_id'
307
			] );
308
		} else {
309
			ApiBase::dieDebug( __METHOD__, 'param validation?' );
310
		}
311
312
		$this->addOption( 'LIMIT', $this->limit + 1 );
313
314
		$count = 0;
315
		$generated = [];
316
		$hookData = [];
317
		$res = $this->select( __METHOD__, [], $hookData );
318
319
		foreach ( $res as $row ) {
320 View Code Duplication
			if ( ++$count > $this->limit ) {
321
				// We've reached the one extra which shows that there are
322
				// additional pages to be had. Stop here...
323
				if ( $enumRevMode ) {
324
					$this->setContinueEnumParameter( 'continue',
325
						$row->rev_timestamp . '|' . intval( $row->rev_id ) );
326
				} elseif ( $revCount > 0 ) {
327
					$this->setContinueEnumParameter( 'continue', intval( $row->rev_id ) );
328
				} else {
329
					$this->setContinueEnumParameter( 'continue', intval( $row->rev_page ) .
330
						'|' . intval( $row->rev_id ) );
331
				}
332
				break;
333
			}
334
335
			if ( $resultPageSet !== null ) {
336
				$generated[] = $row->rev_id;
337
			} else {
338
				$revision = new Revision( $row );
0 ignored issues
show
Bug introduced by
It seems like $row defined by $row on line 319 can be null; however, Revision::__construct() 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...
339
				$rev = $this->extractRevisionInfo( $revision, $row );
0 ignored issues
show
Bug introduced by
It seems like $row defined by $row on line 319 can be null; however, ApiQueryRevisionsBase::extractRevisionInfo() 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...
340
341
				if ( $this->token !== null ) {
342
					$title = $revision->getTitle();
343
					$tokenFunctions = $this->getTokenFunctions();
344
					foreach ( $this->token as $t ) {
345
						$val = call_user_func( $tokenFunctions[$t], $title->getArticleID(), $title, $revision );
346
						if ( $val === false ) {
347
							$this->setWarning( "Action '$t' is not allowed for the current user" );
348
						} else {
349
							$rev[$t . 'token'] = $val;
350
						}
351
					}
352
				}
353
354
				$fit = $this->processRow( $row, $rev, $hookData ) &&
0 ignored issues
show
Bug introduced by
It seems like $row defined by $row on line 319 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...
Bug introduced by
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...
355
					$this->addPageSubItem( $row->rev_page, $rev, 'rev' );
356 View Code Duplication
				if ( !$fit ) {
357
					if ( $enumRevMode ) {
358
						$this->setContinueEnumParameter( 'continue',
359
							$row->rev_timestamp . '|' . intval( $row->rev_id ) );
360
					} elseif ( $revCount > 0 ) {
361
						$this->setContinueEnumParameter( 'continue', intval( $row->rev_id ) );
362
					} else {
363
						$this->setContinueEnumParameter( 'continue', intval( $row->rev_page ) .
364
							'|' . intval( $row->rev_id ) );
365
					}
366
					break;
367
				}
368
			}
369
		}
370
371
		if ( $resultPageSet !== null ) {
372
			$resultPageSet->populateFromRevisionIDs( $generated );
373
		}
374
	}
375
376
	public function getCacheMode( $params ) {
377
		if ( isset( $params['token'] ) ) {
378
			return 'private';
379
		}
380
		return parent::getCacheMode( $params );
381
	}
382
383
	public function getAllowedParams() {
384
		$ret = parent::getAllowedParams() + [
385
			'startid' => [
386
				ApiBase::PARAM_TYPE => 'integer',
387
				ApiBase::PARAM_HELP_MSG_INFO => [ [ 'singlepageonly' ] ],
388
			],
389
			'endid' => [
390
				ApiBase::PARAM_TYPE => 'integer',
391
				ApiBase::PARAM_HELP_MSG_INFO => [ [ 'singlepageonly' ] ],
392
			],
393
			'start' => [
394
				ApiBase::PARAM_TYPE => 'timestamp',
395
				ApiBase::PARAM_HELP_MSG_INFO => [ [ 'singlepageonly' ] ],
396
			],
397
			'end' => [
398
				ApiBase::PARAM_TYPE => 'timestamp',
399
				ApiBase::PARAM_HELP_MSG_INFO => [ [ 'singlepageonly' ] ],
400
			],
401
			'dir' => [
402
				ApiBase::PARAM_DFLT => 'older',
403
				ApiBase::PARAM_TYPE => [
404
					'newer',
405
					'older'
406
				],
407
				ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
408
				ApiBase::PARAM_HELP_MSG_INFO => [ [ 'singlepageonly' ] ],
409
			],
410
			'user' => [
411
				ApiBase::PARAM_TYPE => 'user',
412
				ApiBase::PARAM_HELP_MSG_INFO => [ [ 'singlepageonly' ] ],
413
			],
414
			'excludeuser' => [
415
				ApiBase::PARAM_TYPE => 'user',
416
				ApiBase::PARAM_HELP_MSG_INFO => [ [ 'singlepageonly' ] ],
417
			],
418
			'tag' => null,
419
			'token' => [
420
				ApiBase::PARAM_DEPRECATED => true,
421
				ApiBase::PARAM_TYPE => array_keys( $this->getTokenFunctions() ),
422
				ApiBase::PARAM_ISMULTI => true
423
			],
424
			'continue' => [
425
				ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
426
			],
427
		];
428
429
		$ret['limit'][ApiBase::PARAM_HELP_MSG_INFO] = [ [ 'singlepageonly' ] ];
430
431
		return $ret;
432
	}
433
434
	protected function getExamplesMessages() {
435
		return [
436
			'action=query&prop=revisions&titles=API|Main%20Page&' .
437
				'rvprop=timestamp|user|comment|content'
438
				=> 'apihelp-query+revisions-example-content',
439
			'action=query&prop=revisions&titles=Main%20Page&rvlimit=5&' .
440
				'rvprop=timestamp|user|comment'
441
				=> 'apihelp-query+revisions-example-last5',
442
			'action=query&prop=revisions&titles=Main%20Page&rvlimit=5&' .
443
				'rvprop=timestamp|user|comment&rvdir=newer'
444
				=> 'apihelp-query+revisions-example-first5',
445
			'action=query&prop=revisions&titles=Main%20Page&rvlimit=5&' .
446
				'rvprop=timestamp|user|comment&rvdir=newer&rvstart=2006-05-01T00:00:00Z'
447
				=> 'apihelp-query+revisions-example-first5-after',
448
			'action=query&prop=revisions&titles=Main%20Page&rvlimit=5&' .
449
				'rvprop=timestamp|user|comment&rvexcludeuser=127.0.0.1'
450
				=> 'apihelp-query+revisions-example-first5-not-localhost',
451
			'action=query&prop=revisions&titles=Main%20Page&rvlimit=5&' .
452
				'rvprop=timestamp|user|comment&rvuser=MediaWiki%20default'
453
				=> 'apihelp-query+revisions-example-first5-user',
454
		];
455
	}
456
457
	public function getHelpUrls() {
458
		return 'https://www.mediawiki.org/wiki/API:Revisions';
459
	}
460
}
461