Completed
Branch master (227f0c)
by
unknown
30:54
created

ApiStashEdit::execute()   F

Complexity

Conditions 13
Paths 400

Size

Total Lines 89
Code Lines 55

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 13
eloc 55
nc 400
nop 0
dl 0
loc 89
rs 3.7737
c 2
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
 * 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
 * @author Aaron Schulz
20
 */
21
22
use MediaWiki\Logger\LoggerFactory;
23
24
/**
25
 * Prepare an edit in shared cache so that it can be reused on edit
26
 *
27
 * This endpoint can be called via AJAX as the user focuses on the edit
28
 * summary box. By the time of submission, the parse may have already
29
 * finished, and can be immediately used on page save. Certain parser
30
 * functions like {{REVISIONID}} or {{CURRENTTIME}} may cause the cache
31
 * to not be used on edit. Template and files used are check for changes
32
 * since the output was generated. The cache TTL is also kept low for sanity.
33
 *
34
 * @ingroup API
35
 * @since 1.25
36
 */
37
class ApiStashEdit extends ApiBase {
38
	const ERROR_NONE = 'stashed';
39
	const ERROR_PARSE = 'error_parse';
40
	const ERROR_CACHE = 'error_cache';
41
	const ERROR_UNCACHEABLE = 'uncacheable';
42
43
	const PRESUME_FRESH_TTL_SEC = 30;
44
45
	public function execute() {
46
		$user = $this->getUser();
47
		$params = $this->extractRequestParams();
48
49
		if ( $user->isBot() ) { // sanity
50
			$this->dieUsage( 'This interface is not supported for bots', 'botsnotsupported' );
51
		}
52
53
		$page = $this->getTitleOrPageId( $params );
54
		$title = $page->getTitle();
55
56
		if ( !ContentHandler::getForModelID( $params['contentmodel'] )
57
			->isSupportedFormat( $params['contentformat'] )
58
		) {
59
			$this->dieUsage( 'Unsupported content model/format', 'badmodelformat' );
60
		}
61
62
		// Trim and fix newlines so the key SHA1's match (see RequestContext::getText())
63
		$text = rtrim( str_replace( "\r\n", "\n", $params['text'] ) );
64
		$textContent = ContentHandler::makeContent(
65
			$text, $title, $params['contentmodel'], $params['contentformat'] );
66
67
		$page = WikiPage::factory( $title );
68
		if ( $page->exists() ) {
69
			// Page exists: get the merged content with the proposed change
70
			$baseRev = Revision::newFromPageId( $page->getId(), $params['baserevid'] );
71
			if ( !$baseRev ) {
72
				$this->dieUsage( "No revision ID {$params['baserevid']}", 'missingrev' );
73
			}
74
			$currentRev = $page->getRevision();
75
			if ( !$currentRev ) {
76
				$this->dieUsage( "No current revision of page ID {$page->getId()}", 'missingrev' );
77
			}
78
			// Merge in the new version of the section to get the proposed version
79
			$editContent = $page->replaceSectionAtRev(
80
				$params['section'],
81
				$textContent,
82
				$params['sectiontitle'],
83
				$baseRev->getId()
84
			);
85
			if ( !$editContent ) {
86
				$this->dieUsage( 'Could not merge updated section.', 'replacefailed' );
87
			}
88
			if ( $currentRev->getId() == $baseRev->getId() ) {
89
				// Base revision was still the latest; nothing to merge
90
				$content = $editContent;
91
			} else {
92
				// Merge the edit into the current version
93
				$baseContent = $baseRev->getContent();
94
				$currentContent = $currentRev->getContent();
95
				if ( !$baseContent || !$currentContent ) {
96
					$this->dieUsage( "Missing content for page ID {$page->getId()}", 'missingrev' );
97
				}
98
				$handler = ContentHandler::getForModelID( $baseContent->getModel() );
99
				$content = $handler->merge3( $baseContent, $editContent, $currentContent );
0 ignored issues
show
Bug introduced by
It seems like $editContent defined by $page->replaceSectionAtR...e'], $baseRev->getId()) on line 79 can also be of type null or string; however, ContentHandler::merge3() does only seem to accept object<Content>, 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...
100
			}
101
		} else {
102
			// New pages: use the user-provided content model
103
			$content = $textContent;
104
		}
105
106
		if ( !$content ) { // merge3() failed
107
			$this->getResult()->addValue( null,
108
				$this->getModuleName(), [ 'status' => 'editconflict' ] );
109
			return;
110
		}
111
112
		// The user will abort the AJAX request by pressing "save", so ignore that
113
		ignore_user_abort( true );
114
115
		// Use the master DB for fast blocking locks
116
		$dbw = wfGetDB( DB_MASTER );
117
118
		// Get a key based on the source text, format, and user preferences
119
		$key = self::getStashKey( $title, $content, $user );
120
		// De-duplicate requests on the same key
121
		if ( $user->pingLimiter( 'stashedit' ) ) {
122
			$status = 'ratelimited';
123
		} elseif ( $dbw->lock( $key, __METHOD__, 1 ) ) {
124
			$status = self::parseAndStash( $page, $content, $user );
125
			$dbw->unlock( $key, __METHOD__ );
126
		} else {
127
			$status = 'busy';
128
		}
129
130
		$this->getStats()->increment( "editstash.cache_stores.$status" );
0 ignored issues
show
Deprecated Code introduced by
The method ContextSource::getStats() has been deprecated with message: since 1.27 use a StatsdDataFactory from MediaWikiServices (preferably injected)

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
131
132
		$this->getResult()->addValue( null, $this->getModuleName(), [ 'status' => $status ] );
133
	}
134
135
	/**
136
	 * @param WikiPage $page
137
	 * @param Content $content
138
	 * @param User $user
139
	 * @return integer ApiStashEdit::ERROR_* constant
140
	 * @since 1.25
141
	 */
142
	public static function parseAndStash( WikiPage $page, Content $content, User $user ) {
143
		$cache = ObjectCache::getLocalClusterInstance();
144
		$logger = LoggerFactory::getInstance( 'StashEdit' );
145
146
		$format = $content->getDefaultFormat();
147
		$editInfo = $page->prepareContentForEdit( $content, null, $user, $format, false );
148
149
		if ( $editInfo && $editInfo->output ) {
150
			$key = self::getStashKey( $page->getTitle(), $content, $user );
151
152
			// Let extensions add ParserOutput metadata or warm other caches
153
			Hooks::run( 'ParserOutputStashForEdit', [ $page, $content, $editInfo->output ] );
154
155
			list( $stashInfo, $ttl ) = self::buildStashValue(
156
				$editInfo->pstContent,
157
				$editInfo->output,
158
				$editInfo->timestamp,
159
				$user
160
			);
161
162
			if ( $stashInfo ) {
163
				$ok = $cache->set( $key, $stashInfo, $ttl );
164
				if ( $ok ) {
165
					$logger->debug( "Cached parser output for key '$key'." );
166
					return self::ERROR_NONE;
167
				} else {
168
					$logger->error( "Failed to cache parser output for key '$key'." );
169
					return self::ERROR_CACHE;
170
				}
171
			} else {
172
				$logger->info( "Uncacheable parser output for key '$key'." );
173
				return self::ERROR_UNCACHEABLE;
174
			}
175
		}
176
177
		return self::ERROR_PARSE;
178
	}
179
180
	/**
181
	 * Attempt to cache PST content and corresponding parser output in passing
182
	 *
183
	 * This method can be called when the output was already generated for other
184
	 * reasons. Parsing should not be done just to call this method, however.
185
	 * $pstOpts must be that of the user doing the edit preview. If $pOpts does
186
	 * not match the options of WikiPage::makeParserOptions( 'canonical' ), this
187
	 * will do nothing. Provided the values are cacheable, they will be stored
188
	 * in memcached so that final edit submission might make use of them.
189
	 *
190
	 * @param Page|Article|WikiPage $page Page title
191
	 * @param Content $content Proposed page content
192
	 * @param Content $pstContent The result of preSaveTransform() on $content
193
	 * @param ParserOutput $pOut The result of getParserOutput() on $pstContent
194
	 * @param ParserOptions $pstOpts Options for $pstContent (MUST be for prospective author)
195
	 * @param ParserOptions $pOpts Options for $pOut
196
	 * @param string $timestamp TS_MW timestamp of parser output generation
197
	 * @return bool Success
198
	 */
199
	public static function stashEditFromPreview(
200
		Page $page, Content $content, Content $pstContent, ParserOutput $pOut,
201
		ParserOptions $pstOpts, ParserOptions $pOpts, $timestamp
202
	) {
203
		$cache = ObjectCache::getLocalClusterInstance();
204
		$logger = LoggerFactory::getInstance( 'StashEdit' );
205
206
		// getIsPreview() controls parser function behavior that references things
207
		// like user/revision that don't exists yet. The user/text should already
208
		// be set correctly by callers, just double check the preview flag.
209
		if ( !$pOpts->getIsPreview() ) {
210
			return false; // sanity
211
		} elseif ( $pOpts->getIsSectionPreview() ) {
212
			return false; // short-circuit (need the full content)
213
		}
214
215
		// PST parser options are for the user (handles signatures, etc...)
216
		$user = $pstOpts->getUser();
217
		// Get a key based on the source text, format, and user preferences
218
		$key = self::getStashKey( $page->getTitle(), $content, $user );
219
220
		// Parser output options must match cannonical options.
221
		// Treat some options as matching that are different but don't matter.
222
		$canonicalPOpts = $page->makeParserOptions( 'canonical' );
223
		$canonicalPOpts->setIsPreview( true ); // force match
224
		$canonicalPOpts->setTimestamp( $pOpts->getTimestamp() ); // force match
225
		if ( !$pOpts->matches( $canonicalPOpts ) ) {
226
			$logger->info( "Uncacheable preview output for key '$key' (options)." );
227
			return false;
228
		}
229
230
		// Set the time the output was generated
231
		$pOut->setCacheTime( wfTimestampNow() );
0 ignored issues
show
Security Bug introduced by
It seems like wfTimestampNow() can also be of type false; however, CacheTime::setCacheTime() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
232
233
		// Build a value to cache with a proper TTL
234
		list( $stashInfo, $ttl ) = self::buildStashValue( $pstContent, $pOut, $timestamp, $user );
235
		if ( !$stashInfo ) {
236
			$logger->info( "Uncacheable parser output for key '$key' (rev/TTL)." );
237
			return false;
238
		}
239
240
		$ok = $cache->set( $key, $stashInfo, $ttl );
241
		if ( !$ok ) {
242
			$logger->error( "Failed to cache preview parser output for key '$key'." );
243
		} else {
244
			$logger->debug( "Cached preview output for key '$key'." );
245
		}
246
247
		return $ok;
248
	}
249
250
	/**
251
	 * Check that a prepared edit is in cache and still up-to-date
252
	 *
253
	 * This method blocks if the prepared edit is already being rendered,
254
	 * waiting until rendering finishes before doing final validity checks.
255
	 *
256
	 * The cache is rejected if template or file changes are detected.
257
	 * Note that foreign template or file transclusions are not checked.
258
	 *
259
	 * The result is a map (pstContent,output,timestamp) with fields
260
	 * extracted directly from WikiPage::prepareContentForEdit().
261
	 *
262
	 * @param Title $title
263
	 * @param Content $content
264
	 * @param User $user User to get parser options from
265
	 * @return stdClass|bool Returns false on cache miss
266
	 */
267
	public static function checkCache( Title $title, Content $content, User $user ) {
268
		if ( $user->isBot() ) {
269
			return false; // bots never stash - don't pollute stats
270
		}
271
272
		$cache = ObjectCache::getLocalClusterInstance();
273
		$logger = LoggerFactory::getInstance( 'StashEdit' );
274
		$stats = RequestContext::getMain()->getStats();
0 ignored issues
show
Deprecated Code introduced by
The method RequestContext::getStats() has been deprecated with message: since 1.27 use a StatsdDataFactory from MediaWikiServices (preferably injected)

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
275
276
		$key = self::getStashKey( $title, $content, $user );
277
		$editInfo = $cache->get( $key );
278
		if ( !is_object( $editInfo ) ) {
279
			$start = microtime( true );
280
			// We ignore user aborts and keep parsing. Block on any prior parsing
281
			// so as to use its results and make use of the time spent parsing.
282
			// Skip this logic if there no master connection in case this method
283
			// is called on an HTTP GET request for some reason.
284
			$lb = wfGetLB();
0 ignored issues
show
Deprecated Code introduced by
The function wfGetLB() has been deprecated with message: since 1.27, use MediaWikiServices::getDBLoadBalancer() or MediaWikiServices::getDBLoadBalancerFactory() instead.

This function has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed from the class and what other function to use instead.

Loading history...
285
			$dbw = $lb->getAnyOpenConnection( $lb->getWriterIndex() );
286
			if ( $dbw && $dbw->lock( $key, __METHOD__, 30 ) ) {
287
				$editInfo = $cache->get( $key );
288
				$dbw->unlock( $key, __METHOD__ );
289
			}
290
291
			$timeMs = 1000 * max( 0, microtime( true ) - $start );
292
			$stats->timing( 'editstash.lock_wait_time', $timeMs );
293
		}
294
295
		if ( !is_object( $editInfo ) || !$editInfo->output ) {
296
			$stats->increment( 'editstash.cache_misses.no_stash' );
297
			$logger->debug( "No cache value for key '$key'." );
298
			return false;
299
		}
300
301
		$age = time() - wfTimestamp( TS_UNIX, $editInfo->output->getCacheTime() );
302
		if ( $age <= self::PRESUME_FRESH_TTL_SEC ) {
303
			$stats->increment( 'editstash.cache_hits.presumed_fresh' );
304
			$logger->debug( "Timestamp-based cache hit for key '$key' (age: $age sec)." );
305
			return $editInfo; // assume nothing changed
306
		} elseif ( isset( $editInfo->edits ) && $editInfo->edits === $user->getEditCount() ) {
307
			// Logged-in user made no local upload/template edits in the meantime
308
			$stats->increment( 'editstash.cache_hits.presumed_fresh' );
309
			$logger->debug( "Edit count based cache hit for key '$key' (age: $age sec)." );
310
			return $editInfo;
311
		} elseif ( $user->isAnon()
312
			&& self::lastEditTime( $user ) < $editInfo->output->getCacheTime()
313
		) {
314
			// Logged-out user made no local upload/template edits in the meantime
315
			$stats->increment( 'editstash.cache_hits.presumed_fresh' );
316
			$logger->debug( "Edit check based cache hit for key '$key' (age: $age sec)." );
317
			return $editInfo;
318
		}
319
320
		$dbr = wfGetDB( DB_SLAVE );
321
322
		$templates = []; // conditions to find changes/creations
323
		$templateUses = 0; // expected existing templates
324
		foreach ( $editInfo->output->getTemplateIds() as $ns => $stuff ) {
325
			foreach ( $stuff as $dbkey => $revId ) {
326
				$templates[(string)$ns][$dbkey] = (int)$revId;
327
				++$templateUses;
328
			}
329
		}
330
		// Check that no templates used in the output changed...
331
		if ( count( $templates ) ) {
332
			$res = $dbr->select(
333
				'page',
334
				[ 'ns' => 'page_namespace', 'dbk' => 'page_title', 'page_latest' ],
335
				$dbr->makeWhereFrom2d( $templates, 'page_namespace', 'page_title' ),
0 ignored issues
show
Security Bug introduced by
It seems like $dbr->makeWhereFrom2d($t...mespace', 'page_title') targeting DatabaseBase::makeWhereFrom2d() can also be of type false; however, DatabaseBase::select() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
336
				__METHOD__
337
			);
338
			$changed = false;
339
			foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean 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...
340
				$changed = $changed || ( $row->page_latest != $templates[$row->ns][$row->dbk] );
341
			}
342
343 View Code Duplication
			if ( $changed || $res->numRows() != $templateUses ) {
344
				$stats->increment( 'editstash.cache_misses.proven_stale' );
345
				$logger->info( "Stale cache for key '$key'; template changed. (age: $age sec)" );
346
				return false;
347
			}
348
		}
349
350
		$files = []; // conditions to find changes/creations
351
		foreach ( $editInfo->output->getFileSearchOptions() as $name => $options ) {
352
			$files[$name] = (string)$options['sha1'];
353
		}
354
		// Check that no files used in the output changed...
355
		if ( count( $files ) ) {
356
			$res = $dbr->select(
357
				'image',
358
				[ 'name' => 'img_name', 'img_sha1' ],
359
				[ 'img_name' => array_keys( $files ) ],
360
				__METHOD__
361
			);
362
			$changed = false;
363
			foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean 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...
364
				$changed = $changed || ( $row->img_sha1 != $files[$row->name] );
365
			}
366
367 View Code Duplication
			if ( $changed || $res->numRows() != count( $files ) ) {
368
				$stats->increment( 'editstash.cache_misses.proven_stale' );
369
				$logger->info( "Stale cache for key '$key'; file changed. (age: $age sec)" );
370
				return false;
371
			}
372
		}
373
374
		$stats->increment( 'editstash.cache_hits.proven_fresh' );
375
		$logger->debug( "Verified cache hit for key '$key' (age: $age sec)." );
376
377
		return $editInfo;
378
	}
379
380
	/**
381
	 * @param User $user
382
	 * @return string|null TS_MW timestamp or null
383
	 */
384
	private static function lastEditTime( User $user ) {
385
		$time = wfGetDB( DB_SLAVE )->selectField(
386
			'recentchanges',
387
			'MAX(rc_timestamp)',
388
			[ 'rc_user_text' => $user->getName() ],
389
			__METHOD__
390
		);
391
392
		return wfTimestampOrNull( TS_MW, $time );
393
	}
394
395
	/**
396
	 * Get the temporary prepared edit stash key for a user
397
	 *
398
	 * This key can be used for caching prepared edits provided:
399
	 *   - a) The $user was used for PST options
400
	 *   - b) The parser output was made from the PST using cannonical matching options
401
	 *
402
	 * @param Title $title
403
	 * @param Content $content
404
	 * @param User $user User to get parser options from
405
	 * @return string
406
	 */
407
	private static function getStashKey( Title $title, Content $content, User $user ) {
408
		$hash = sha1( implode( ':', [
409
			$content->getModel(),
410
			$content->getDefaultFormat(),
411
			sha1( $content->serialize( $content->getDefaultFormat() ) ),
412
			$user->getId() ?: md5( $user->getName() ), // account for user parser options
413
			$user->getId() ? $user->getDBTouched() : '-' // handle preference change races
414
		] ) );
415
416
		return wfMemcKey( 'prepared-edit', md5( $title->getPrefixedDBkey() ), $hash );
417
	}
418
419
	/**
420
	 * Build a value to store in memcached based on the PST content and parser output
421
	 *
422
	 * This makes a simple version of WikiPage::prepareContentForEdit() as stash info
423
	 *
424
	 * @param Content $pstContent
425
	 * @param ParserOutput $parserOutput
426
	 * @param string $timestamp TS_MW
427
	 * @param User $user
428
	 * @return array (stash info array, TTL in seconds) or (null, 0)
429
	 */
430
	private static function buildStashValue(
431
		Content $pstContent, ParserOutput $parserOutput, $timestamp, User $user
432
	) {
433
		// If an item is renewed, mind the cache TTL determined by config and parser functions.
434
		// Put an upper limit on the TTL for sanity to avoid extreme template/file staleness.
435
		$since = time() - wfTimestamp( TS_UNIX, $parserOutput->getTimestamp() );
436
		$ttl = min( $parserOutput->getCacheExpiry() - $since, 5 * 60 );
437
438
		if ( $ttl > 0 && !$parserOutput->getFlag( 'vary-revision' ) ) {
439
			// Only store what is actually needed
440
			$stashInfo = (object)[
441
				'pstContent' => $pstContent,
442
				'output'     => $parserOutput,
443
				'timestamp'  => $timestamp,
444
				'edits'      => $user->getEditCount()
445
			];
446
			return [ $stashInfo, $ttl ];
447
		}
448
449
		return [ null, 0 ];
450
	}
451
452
	public function getAllowedParams() {
453
		return [
454
			'title' => [
455
				ApiBase::PARAM_TYPE => 'string',
456
				ApiBase::PARAM_REQUIRED => true
457
			],
458
			'section' => [
459
				ApiBase::PARAM_TYPE => 'string',
460
			],
461
			'sectiontitle' => [
462
				ApiBase::PARAM_TYPE => 'string'
463
			],
464
			'text' => [
465
				ApiBase::PARAM_TYPE => 'text',
466
				ApiBase::PARAM_REQUIRED => true
467
			],
468
			'contentmodel' => [
469
				ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
470
				ApiBase::PARAM_REQUIRED => true
471
			],
472
			'contentformat' => [
473
				ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
474
				ApiBase::PARAM_REQUIRED => true
475
			],
476
			'baserevid' => [
477
				ApiBase::PARAM_TYPE => 'integer',
478
				ApiBase::PARAM_REQUIRED => true
479
			]
480
		];
481
	}
482
483
	public function needsToken() {
484
		return 'csrf';
485
	}
486
487
	public function mustBePosted() {
488
		return true;
489
	}
490
491
	public function isWriteMode() {
492
		return true;
493
	}
494
495
	public function isInternal() {
496
		return true;
497
	}
498
}
499