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

includes/api/ApiEditPage.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 August 16, 2007
6
 *
7
 * Copyright © 2007 Iker Labarga "<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 module that allows for editing and creating pages.
29
 *
30
 * Currently, this wraps around the EditPage class in an ugly way,
31
 * EditPage.php should be rewritten to provide a cleaner interface,
32
 * see T20654 if you're inspired to fix this.
33
 *
34
 * @ingroup API
35
 */
36
class ApiEditPage extends ApiBase {
37
	public function execute() {
38
		$this->useTransactionalTimeLimit();
39
40
		$user = $this->getUser();
41
		$params = $this->extractRequestParams();
42
43
		if ( is_null( $params['text'] ) && is_null( $params['appendtext'] ) &&
44
			is_null( $params['prependtext'] ) &&
45
			$params['undo'] == 0
46
		) {
47
			$this->dieUsageMsg( 'missingtext' );
48
		}
49
50
		$pageObj = $this->getTitleOrPageId( $params );
51
		$titleObj = $pageObj->getTitle();
52
		$apiResult = $this->getResult();
53
54
		if ( $params['redirect'] ) {
55
			if ( $params['prependtext'] === null && $params['appendtext'] === null
56
				&& $params['section'] !== 'new'
57
			) {
58
				$this->dieUsage( 'You have attempted to edit using the "redirect"-following'
59
					. ' mode, which must be used in conjuction with section=new, prependtext'
60
					. ', or appendtext.', 'redirect-appendonly' );
61
			}
62
			if ( $titleObj->isRedirect() ) {
63
				$oldTitle = $titleObj;
64
65
				$titles = Revision::newFromTitle( $oldTitle, false, Revision::READ_LATEST )
66
					->getContent( Revision::FOR_THIS_USER, $user )
67
					->getRedirectChain();
68
				// array_shift( $titles );
69
70
				$redirValues = [];
71
72
				/** @var $newTitle Title */
73
				foreach ( $titles as $id => $newTitle ) {
74
75
					if ( !isset( $titles[$id - 1] ) ) {
76
						$titles[$id - 1] = $oldTitle;
77
					}
78
79
					$redirValues[] = [
80
						'from' => $titles[$id - 1]->getPrefixedText(),
81
						'to' => $newTitle->getPrefixedText()
82
					];
83
84
					$titleObj = $newTitle;
85
				}
86
87
				ApiResult::setIndexedTagName( $redirValues, 'r' );
88
				$apiResult->addValue( null, 'redirects', $redirValues );
89
90
				// Since the page changed, update $pageObj
91
				$pageObj = WikiPage::factory( $titleObj );
92
			}
93
		}
94
95
		if ( !isset( $params['contentmodel'] ) || $params['contentmodel'] == '' ) {
96
			$contentHandler = $pageObj->getContentHandler();
97
		} else {
98
			$contentHandler = ContentHandler::getForModelID( $params['contentmodel'] );
99
		}
100
		$contentModel = $contentHandler->getModelID();
101
102
		$name = $titleObj->getPrefixedDBkey();
103
		$model = $contentHandler->getModelID();
104
105
		if ( $params['undo'] > 0 ) {
106
			// allow undo via api
107
		} elseif ( $contentHandler->supportsDirectApiEditing() === false ) {
108
			$this->dieUsage(
109
				"Direct editing via API is not supported for content model $model used by $name",
110
				'no-direct-editing'
111
			);
112
		}
113
114
		if ( !isset( $params['contentformat'] ) || $params['contentformat'] == '' ) {
115
			$contentFormat = $contentHandler->getDefaultFormat();
116
		} else {
117
			$contentFormat = $params['contentformat'];
118
		}
119
120
		if ( !$contentHandler->isSupportedFormat( $contentFormat ) ) {
121
122
			$this->dieUsage( "The requested format $contentFormat is not supported for content model " .
123
				" $model used by $name", 'badformat' );
124
		}
125
126
		if ( $params['createonly'] && $titleObj->exists() ) {
127
			$this->dieUsageMsg( 'createonly-exists' );
128
		}
129
		if ( $params['nocreate'] && !$titleObj->exists() ) {
130
			$this->dieUsageMsg( 'nocreate-missing' );
131
		}
132
133
		// Now let's check whether we're even allowed to do this
134
		$errors = $titleObj->getUserPermissionsErrors( 'edit', $user );
135
		if ( !$titleObj->exists() ) {
136
			$errors = array_merge( $errors, $titleObj->getUserPermissionsErrors( 'create', $user ) );
137
		}
138
		if ( count( $errors ) ) {
139
			if ( is_array( $errors[0] ) ) {
140
				switch ( $errors[0][0] ) {
141
					case 'blockedtext':
142
						$this->dieUsage(
143
							'You have been blocked from editing',
144
							'blocked',
145
							0,
146
							[ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ]
147
						);
148
						break;
149
					case 'autoblockedtext':
150
						$this->dieUsage(
151
							'Your IP address has been blocked automatically, because it was used by a blocked user',
152
							'autoblocked',
153
							0,
154
							[ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ]
155
						);
156
						break;
157
					default:
158
						$this->dieUsageMsg( $errors[0] );
159
				}
160
			} else {
161
				$this->dieUsageMsg( $errors[0] );
162
			}
163
		}
164
165
		$toMD5 = $params['text'];
166
		if ( !is_null( $params['appendtext'] ) || !is_null( $params['prependtext'] ) ) {
167
			$content = $pageObj->getContent();
168
169
			if ( !$content ) {
170
				if ( $titleObj->getNamespace() == NS_MEDIAWIKI ) {
171
					# If this is a MediaWiki:x message, then load the messages
172
					# and return the message value for x.
173
					$text = $titleObj->getDefaultMessageText();
174
					if ( $text === false ) {
175
						$text = '';
176
					}
177
178
					try {
179
						$content = ContentHandler::makeContent( $text, $this->getTitle() );
180
					} catch ( MWContentSerializationException $ex ) {
181
						$this->dieUsage( $ex->getMessage(), 'parseerror' );
182
183
						return;
184
					}
185
				} else {
186
					# Otherwise, make a new empty content.
187
					$content = $contentHandler->makeEmptyContent();
188
				}
189
			}
190
191
			// @todo Add support for appending/prepending to the Content interface
192
193
			if ( !( $content instanceof TextContent ) ) {
194
				$mode = $contentHandler->getModelID();
195
				$this->dieUsage( "Can't append to pages using content model $mode", 'appendnotsupported' );
196
			}
197
198
			if ( !is_null( $params['section'] ) ) {
199
				if ( !$contentHandler->supportsSections() ) {
200
					$modelName = $contentHandler->getModelID();
201
					$this->dieUsage(
202
						"Sections are not supported for this content model: $modelName.",
203
						'sectionsnotsupported'
204
					);
205
				}
206
207
				if ( $params['section'] == 'new' ) {
208
					// DWIM if they're trying to prepend/append to a new section.
209
					$content = null;
210
				} else {
211
					// Process the content for section edits
212
					$section = $params['section'];
213
					$content = $content->getSection( $section );
214
215
					if ( !$content ) {
216
						$this->dieUsage( "There is no section {$section}.", 'nosuchsection' );
217
					}
218
				}
219
			}
220
221
			if ( !$content ) {
222
				$text = '';
223
			} else {
224
				$text = $content->serialize( $contentFormat );
225
			}
226
227
			$params['text'] = $params['prependtext'] . $text . $params['appendtext'];
228
			$toMD5 = $params['prependtext'] . $params['appendtext'];
229
		}
230
231
		if ( $params['undo'] > 0 ) {
232
			if ( $params['undoafter'] > 0 ) {
233
				if ( $params['undo'] < $params['undoafter'] ) {
234
					list( $params['undo'], $params['undoafter'] ) =
235
						[ $params['undoafter'], $params['undo'] ];
236
				}
237
				$undoafterRev = Revision::newFromId( $params['undoafter'] );
238
			}
239
			$undoRev = Revision::newFromId( $params['undo'] );
240 View Code Duplication
			if ( is_null( $undoRev ) || $undoRev->isDeleted( Revision::DELETED_TEXT ) ) {
241
				$this->dieUsageMsg( [ 'nosuchrevid', $params['undo'] ] );
242
			}
243
244
			if ( $params['undoafter'] == 0 ) {
245
				$undoafterRev = $undoRev->getPrevious();
246
			}
247 View Code Duplication
			if ( is_null( $undoafterRev ) || $undoafterRev->isDeleted( Revision::DELETED_TEXT ) ) {
248
				$this->dieUsageMsg( [ 'nosuchrevid', $params['undoafter'] ] );
249
			}
250
251 View Code Duplication
			if ( $undoRev->getPage() != $pageObj->getId() ) {
252
				$this->dieUsageMsg( [ 'revwrongpage', $undoRev->getId(),
253
					$titleObj->getPrefixedText() ] );
254
			}
255 View Code Duplication
			if ( $undoafterRev->getPage() != $pageObj->getId() ) {
256
				$this->dieUsageMsg( [ 'revwrongpage', $undoafterRev->getId(),
257
					$titleObj->getPrefixedText() ] );
258
			}
259
260
			$newContent = $contentHandler->getUndoContent(
261
				$pageObj->getRevision(),
262
				$undoRev,
263
				$undoafterRev
264
			);
265
266
			if ( !$newContent ) {
267
				$this->dieUsageMsg( 'undo-failure' );
268
			}
269
			if ( empty( $params['contentmodel'] )
270
				&& empty( $params['contentformat'] )
271
			) {
272
				// If we are reverting content model, the new content model
273
				// might not support the current serialization format, in
274
				// which case go back to the old serialization format,
275
				// but only if the user hasn't specified a format/model
276
				// parameter.
277
				if ( !$newContent->isSupportedFormat( $contentFormat ) ) {
278
					$contentFormat = $undoafterRev->getContentFormat();
279
				}
280
				// Override content model with model of undid revision.
281
				$contentModel = $newContent->getModel();
282
			}
283
			$params['text'] = $newContent->serialize( $contentFormat );
284
			// If no summary was given and we only undid one rev,
285
			// use an autosummary
286
			if ( is_null( $params['summary'] ) &&
287
				$titleObj->getNextRevisionID( $undoafterRev->getId() ) == $params['undo']
288
			) {
289
				$params['summary'] = wfMessage( 'undo-summary' )
290
					->params( $params['undo'], $undoRev->getUserText() )->inContentLanguage()->text();
291
			}
292
		}
293
294
		// See if the MD5 hash checks out
295
		if ( !is_null( $params['md5'] ) && md5( $toMD5 ) !== $params['md5'] ) {
296
			$this->dieUsageMsg( 'hashcheckfailed' );
297
		}
298
299
		// EditPage wants to parse its stuff from a WebRequest
300
		// That interface kind of sucks, but it's workable
301
		$requestArray = [
302
			'wpTextbox1' => $params['text'],
303
			'format' => $contentFormat,
304
			'model' => $contentModel,
305
			'wpEditToken' => $params['token'],
306
			'wpIgnoreBlankSummary' => true,
307
			'wpIgnoreBlankArticle' => true,
308
			'wpIgnoreSelfRedirect' => true,
309
			'bot' => $params['bot'],
310
		];
311
312
		if ( !is_null( $params['summary'] ) ) {
313
			$requestArray['wpSummary'] = $params['summary'];
314
		}
315
316
		if ( !is_null( $params['sectiontitle'] ) ) {
317
			$requestArray['wpSectionTitle'] = $params['sectiontitle'];
318
		}
319
320
		// TODO: Pass along information from 'undoafter' as well
321
		if ( $params['undo'] > 0 ) {
322
			$requestArray['wpUndidRevision'] = $params['undo'];
323
		}
324
325
		// Watch out for basetimestamp == '' or '0'
326
		// It gets treated as NOW, almost certainly causing an edit conflict
327
		if ( $params['basetimestamp'] !== null && (bool)$this->getMain()->getVal( 'basetimestamp' ) ) {
328
			$requestArray['wpEdittime'] = $params['basetimestamp'];
329
		} else {
330
			$requestArray['wpEdittime'] = $pageObj->getTimestamp();
331
		}
332
333
		if ( $params['starttimestamp'] !== null ) {
334
			$requestArray['wpStarttime'] = $params['starttimestamp'];
335
		} else {
336
			$requestArray['wpStarttime'] = wfTimestampNow(); // Fake wpStartime
337
		}
338
339
		if ( $params['minor'] || ( !$params['notminor'] && $user->getOption( 'minordefault' ) ) ) {
340
			$requestArray['wpMinoredit'] = '';
341
		}
342
343
		if ( $params['recreate'] ) {
344
			$requestArray['wpRecreate'] = '';
345
		}
346
347
		if ( !is_null( $params['section'] ) ) {
348
			$section = $params['section'];
349
			if ( !preg_match( '/^((T-)?\d+|new)$/', $section ) ) {
350
				$this->dieUsage( "The section parameter must be a valid section id or 'new'",
351
					'invalidsection' );
352
			}
353
			$content = $pageObj->getContent();
354
			if ( $section !== '0' && $section != 'new'
355
				&& ( !$content || !$content->getSection( $section ) )
356
			) {
357
				$this->dieUsage( "There is no section {$section}.", 'nosuchsection' );
358
			}
359
			$requestArray['wpSection'] = $params['section'];
360
		} else {
361
			$requestArray['wpSection'] = '';
362
		}
363
364
		$watch = $this->getWatchlistValue( $params['watchlist'], $titleObj );
365
366
		// Deprecated parameters
367
		if ( $params['watch'] ) {
368
			$watch = true;
369
		} elseif ( $params['unwatch'] ) {
370
			$watch = false;
371
		}
372
373
		if ( $watch ) {
374
			$requestArray['wpWatchthis'] = '';
375
		}
376
377
		// Apply change tags
378
		if ( count( $params['tags'] ) ) {
379
			$tagStatus = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $user );
380
			if ( $tagStatus->isOK() ) {
381
				$requestArray['wpChangeTags'] = implode( ',', $params['tags'] );
382
			} else {
383
				$this->dieStatus( $tagStatus );
384
			}
385
		}
386
387
		// Pass through anything else we might have been given, to support extensions
388
		// This is kind of a hack but it's the best we can do to make extensions work
389
		$requestArray += $this->getRequest()->getValues();
390
391
		global $wgTitle, $wgRequest;
392
393
		$req = new DerivativeRequest( $this->getRequest(), $requestArray, true );
394
395
		// Some functions depend on $wgTitle == $ep->mTitle
396
		// TODO: Make them not or check if they still do
397
		$wgTitle = $titleObj;
398
399
		$articleContext = new RequestContext;
400
		$articleContext->setRequest( $req );
401
		$articleContext->setWikiPage( $pageObj );
402
		$articleContext->setUser( $this->getUser() );
403
404
		/** @var $articleObject Article */
405
		$articleObject = Article::newFromWikiPage( $pageObj, $articleContext );
406
407
		$ep = new EditPage( $articleObject );
408
409
		$ep->setApiEditOverride( true );
410
		$ep->setContextTitle( $titleObj );
411
		$ep->importFormData( $req );
412
		$content = $ep->textbox1;
413
414
		// Run hooks
415
		// Handle APIEditBeforeSave parameters
416
		$r = [];
417
		// Deprecated in favour of EditFilterMergedContent
418
		if ( !Hooks::run( 'APIEditBeforeSave', [ $ep, $content, &$r ], '1.28' ) ) {
419
			if ( count( $r ) ) {
420
				$r['result'] = 'Failure';
421
				$apiResult->addValue( null, $this->getModuleName(), $r );
422
423
				return;
424
			}
425
426
			$this->dieUsageMsg( 'hookaborted' );
427
		}
428
429
		// Do the actual save
430
		$oldRevId = $articleObject->getRevIdFetched();
431
		$result = null;
432
		// Fake $wgRequest for some hooks inside EditPage
433
		// @todo FIXME: This interface SUCKS
434
		$oldRequest = $wgRequest;
435
		$wgRequest = $req;
436
437
		$status = $ep->attemptSave( $result );
438
		$wgRequest = $oldRequest;
439
440
		switch ( $status->value ) {
441
			case EditPage::AS_HOOK_ERROR:
442
			case EditPage::AS_HOOK_ERROR_EXPECTED:
443
				if ( isset( $status->apiHookResult ) ) {
444
					$r = $status->apiHookResult;
445
					$r['result'] = 'Failure';
446
					$apiResult->addValue( null, $this->getModuleName(), $r );
447
					return;
448
				} else {
449
					$this->dieUsageMsg( 'hookaborted' );
450
				}
451
452
			case EditPage::AS_PARSE_ERROR:
453
				$this->dieUsage( $status->getMessage(), 'parseerror' );
454
455
			case EditPage::AS_IMAGE_REDIRECT_ANON:
456
				$this->dieUsageMsg( 'noimageredirect-anon' );
457
458
			case EditPage::AS_IMAGE_REDIRECT_LOGGED:
459
				$this->dieUsageMsg( 'noimageredirect-logged' );
460
461
			case EditPage::AS_SPAM_ERROR:
462
				$this->dieUsageMsg( [ 'spamdetected', $result['spam'] ] );
463
464
			case EditPage::AS_BLOCKED_PAGE_FOR_USER:
465
				$this->dieUsage(
466
					'You have been blocked from editing',
467
					'blocked',
468
					0,
469
					[ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ]
470
				);
471
472
			case EditPage::AS_MAX_ARTICLE_SIZE_EXCEEDED:
473
			case EditPage::AS_CONTENT_TOO_BIG:
474
				$this->dieUsageMsg( [ 'contenttoobig', $this->getConfig()->get( 'MaxArticleSize' ) ] );
475
476
			case EditPage::AS_READ_ONLY_PAGE_ANON:
477
				$this->dieUsageMsg( 'noedit-anon' );
478
479
			case EditPage::AS_READ_ONLY_PAGE_LOGGED:
480
				$this->dieUsageMsg( 'noedit' );
481
482
			case EditPage::AS_READ_ONLY_PAGE:
483
				$this->dieReadOnly();
484
485
			case EditPage::AS_RATE_LIMITED:
486
				$this->dieUsageMsg( 'actionthrottledtext' );
487
488
			case EditPage::AS_ARTICLE_WAS_DELETED:
489
				$this->dieUsageMsg( 'wasdeleted' );
490
491
			case EditPage::AS_NO_CREATE_PERMISSION:
492
				$this->dieUsageMsg( 'nocreate-loggedin' );
493
494
			case EditPage::AS_NO_CHANGE_CONTENT_MODEL:
495
				$this->dieUsageMsg( 'cantchangecontentmodel' );
496
497
			case EditPage::AS_BLANK_ARTICLE:
498
				$this->dieUsageMsg( 'blankpage' );
499
500
			case EditPage::AS_CONFLICT_DETECTED:
501
				$this->dieUsageMsg( 'editconflict' );
502
503
			case EditPage::AS_TEXTBOX_EMPTY:
504
				$this->dieUsageMsg( 'emptynewsection' );
505
506
			case EditPage::AS_CHANGE_TAG_ERROR:
0 ignored issues
show
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
507
				$this->dieStatus( $status );
508
509
			case EditPage::AS_SUCCESS_NEW_ARTICLE:
510
				$r['new'] = true;
511
				// fall-through
512
513
			case EditPage::AS_SUCCESS_UPDATE:
514
				$r['result'] = 'Success';
515
				$r['pageid'] = intval( $titleObj->getArticleID() );
516
				$r['title'] = $titleObj->getPrefixedText();
517
				$r['contentmodel'] = $articleObject->getContentModel();
518
				$newRevId = $articleObject->getLatest();
519
				if ( $newRevId == $oldRevId ) {
520
					$r['nochange'] = true;
521
				} else {
522
					$r['oldrevid'] = intval( $oldRevId );
523
					$r['newrevid'] = intval( $newRevId );
524
					$r['newtimestamp'] = wfTimestamp( TS_ISO_8601,
525
						$pageObj->getTimestamp() );
526
				}
527
				break;
528
529
			case EditPage::AS_SUMMARY_NEEDED:
530
				// Shouldn't happen since we set wpIgnoreBlankSummary, but just in case
531
				$this->dieUsageMsg( 'summaryrequired' );
532
533
			case EditPage::AS_END:
534
			default:
535
				// $status came from WikiPage::doEditContent()
536
				$errors = $status->getErrorsArray();
537
				$this->dieUsageMsg( $errors[0] ); // TODO: Add new errors to message map
538
				break;
539
		}
540
		$apiResult->addValue( null, $this->getModuleName(), $r );
541
	}
542
543
	public function mustBePosted() {
544
		return true;
545
	}
546
547
	public function isWriteMode() {
548
		return true;
549
	}
550
551
	public function getAllowedParams() {
552
		return [
553
			'title' => [
554
				ApiBase::PARAM_TYPE => 'string',
555
			],
556
			'pageid' => [
557
				ApiBase::PARAM_TYPE => 'integer',
558
			],
559
			'section' => null,
560
			'sectiontitle' => [
561
				ApiBase::PARAM_TYPE => 'string',
562
			],
563
			'text' => [
564
				ApiBase::PARAM_TYPE => 'text',
565
			],
566
			'summary' => null,
567
			'tags' => [
568
				ApiBase::PARAM_TYPE => 'tags',
569
				ApiBase::PARAM_ISMULTI => true,
570
			],
571
			'minor' => false,
572
			'notminor' => false,
573
			'bot' => false,
574
			'basetimestamp' => [
575
				ApiBase::PARAM_TYPE => 'timestamp',
576
			],
577
			'starttimestamp' => [
578
				ApiBase::PARAM_TYPE => 'timestamp',
579
			],
580
			'recreate' => false,
581
			'createonly' => false,
582
			'nocreate' => false,
583
			'watch' => [
584
				ApiBase::PARAM_DFLT => false,
585
				ApiBase::PARAM_DEPRECATED => true,
586
			],
587
			'unwatch' => [
588
				ApiBase::PARAM_DFLT => false,
589
				ApiBase::PARAM_DEPRECATED => true,
590
			],
591
			'watchlist' => [
592
				ApiBase::PARAM_DFLT => 'preferences',
593
				ApiBase::PARAM_TYPE => [
594
					'watch',
595
					'unwatch',
596
					'preferences',
597
					'nochange'
598
				],
599
			],
600
			'md5' => null,
601
			'prependtext' => [
602
				ApiBase::PARAM_TYPE => 'text',
603
			],
604
			'appendtext' => [
605
				ApiBase::PARAM_TYPE => 'text',
606
			],
607
			'undo' => [
608
				ApiBase::PARAM_TYPE => 'integer'
609
			],
610
			'undoafter' => [
611
				ApiBase::PARAM_TYPE => 'integer'
612
			],
613
			'redirect' => [
614
				ApiBase::PARAM_TYPE => 'boolean',
615
				ApiBase::PARAM_DFLT => false,
616
			],
617
			'contentformat' => [
618
				ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
619
			],
620
			'contentmodel' => [
621
				ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
622
			],
623
			'token' => [
624
				// Standard definition automatically inserted
625
				ApiBase::PARAM_HELP_MSG_APPEND => [ 'apihelp-edit-param-token' ],
626
			],
627
		];
628
	}
629
630
	public function needsToken() {
631
		return 'csrf';
632
	}
633
634
	protected function getExamplesMessages() {
635
		return [
636
			'action=edit&title=Test&summary=test%20summary&' .
637
				'text=article%20content&basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
638
				=> 'apihelp-edit-example-edit',
639
			'action=edit&title=Test&summary=NOTOC&minor=&' .
640
				'prependtext=__NOTOC__%0A&basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
641
				=> 'apihelp-edit-example-prepend',
642
			'action=edit&title=Test&undo=13585&undoafter=13579&' .
643
				'basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
644
				=> 'apihelp-edit-example-undo',
645
		];
646
	}
647
648
	public function getHelpUrls() {
649
		return 'https://www.mediawiki.org/wiki/API:Edit';
650
	}
651
}
652