MediaWiki master
ApiEditPage.php
Go to the documentation of this file.
1<?php
41
58class ApiEditPage extends ApiBase {
61
62 private IContentHandlerFactory $contentHandlerFactory;
63 private RevisionLookup $revisionLookup;
64 private WatchedItemStoreInterface $watchedItemStore;
65 private WikiPageFactory $wikiPageFactory;
66 private RedirectLookup $redirectLookup;
67 private TempUserCreator $tempUserCreator;
68 private UserFactory $userFactory;
69
73 private function persistGlobalSession() {
74 MediaWiki\Session\SessionManager::getGlobalSession()->persist();
75 }
76
90 public function __construct(
91 ApiMain $mainModule,
92 $moduleName,
93 IContentHandlerFactory $contentHandlerFactory = null,
94 RevisionLookup $revisionLookup = null,
95 WatchedItemStoreInterface $watchedItemStore = null,
96 WikiPageFactory $wikiPageFactory = null,
97 WatchlistManager $watchlistManager = null,
98 UserOptionsLookup $userOptionsLookup = null,
99 RedirectLookup $redirectLookup = null,
100 TempUserCreator $tempUserCreator = null,
101 UserFactory $userFactory = null
102 ) {
103 parent::__construct( $mainModule, $moduleName );
104
105 // This class is extended and therefor fallback to global state - T264213
106 $services = MediaWikiServices::getInstance();
107 $this->contentHandlerFactory = $contentHandlerFactory ?? $services->getContentHandlerFactory();
108 $this->revisionLookup = $revisionLookup ?? $services->getRevisionLookup();
109 $this->watchedItemStore = $watchedItemStore ?? $services->getWatchedItemStore();
110 $this->wikiPageFactory = $wikiPageFactory ?? $services->getWikiPageFactory();
111
112 // Variables needed in ApiWatchlistTrait trait
113 $this->watchlistExpiryEnabled = $this->getConfig()->get( MainConfigNames::WatchlistExpiry );
114 $this->watchlistMaxDuration =
115 $this->getConfig()->get( MainConfigNames::WatchlistExpiryMaxDuration );
116 $this->watchlistManager = $watchlistManager ?? $services->getWatchlistManager();
117 $this->userOptionsLookup = $userOptionsLookup ?? $services->getUserOptionsLookup();
118 $this->redirectLookup = $redirectLookup ?? $services->getRedirectLookup();
119 $this->tempUserCreator = $tempUserCreator ?? $services->getTempUserCreator();
120 $this->userFactory = $userFactory ?? $services->getUserFactory();
121 }
122
127 private function getUserForPermissions() {
128 $user = $this->getUser();
129 if ( $this->tempUserCreator->shouldAutoCreate( $user, 'edit' ) ) {
130 return $this->userFactory->newUnsavedTempUser(
131 $this->tempUserCreator->getStashedName( $this->getRequest()->getSession() )
132 );
133 }
134 return $user;
135 }
136
137 public function execute() {
139
140 $user = $this->getUser();
141 $params = $this->extractRequestParams();
142
143 $this->requireAtLeastOneParameter( $params, 'text', 'appendtext', 'prependtext', 'undo' );
144
145 $pageObj = $this->getTitleOrPageId( $params );
146 $titleObj = $pageObj->getTitle();
147 $this->getErrorFormatter()->setContextTitle( $titleObj );
148 $apiResult = $this->getResult();
149
150 if ( $params['redirect'] ) {
151 if ( $params['prependtext'] === null
152 && $params['appendtext'] === null
153 && $params['section'] !== 'new'
154 ) {
155 $this->dieWithError( 'apierror-redirect-appendonly' );
156 }
157 if ( $titleObj->isRedirect() ) {
158 $oldTarget = $titleObj;
159 $redirTarget = $this->redirectLookup->getRedirectTarget( $oldTarget );
160 $redirTarget = Title::castFromLinkTarget( $redirTarget );
161
162 $redirValues = [
163 'from' => $titleObj->getPrefixedText(),
164 'to' => $redirTarget->getPrefixedText()
165 ];
166
167 // T239428: Check whether the new title is valid
168 if ( $redirTarget->isExternal() || !$redirTarget->canExist() ) {
169 $redirValues['to'] = $redirTarget->getFullText();
170 $this->dieWithError(
171 [
172 'apierror-edit-invalidredirect',
173 Message::plaintextParam( $oldTarget->getPrefixedText() ),
174 Message::plaintextParam( $redirTarget->getFullText() ),
175 ],
176 'edit-invalidredirect',
177 [ 'redirects' => $redirValues ]
178 );
179 }
180
181 ApiResult::setIndexedTagName( $redirValues, 'r' );
182 $apiResult->addValue( null, 'redirects', $redirValues );
183
184 // Since the page changed, update $pageObj and $titleObj
185 $pageObj = $this->wikiPageFactory->newFromTitle( $redirTarget );
186 $titleObj = $pageObj->getTitle();
187
188 $this->getErrorFormatter()->setContextTitle( $redirTarget );
189 }
190 }
191
192 if ( $params['contentmodel'] ) {
193 $contentHandler = $this->contentHandlerFactory->getContentHandler( $params['contentmodel'] );
194 } else {
195 $contentHandler = $pageObj->getContentHandler();
196 }
197 $contentModel = $contentHandler->getModelID();
198
199 $name = $titleObj->getPrefixedDBkey();
200
201 if ( $params['undo'] > 0 ) {
202 // allow undo via api
203 } elseif ( $contentHandler->supportsDirectApiEditing() === false ) {
204 $this->dieWithError( [ 'apierror-no-direct-editing', $contentModel, $name ] );
205 }
206
207 $contentFormat = $params['contentformat'] ?: $contentHandler->getDefaultFormat();
208
209 if ( !$contentHandler->isSupportedFormat( $contentFormat ) ) {
210 $this->dieWithError( [ 'apierror-badformat', $contentFormat, $contentModel, $name ] );
211 }
212
213 if ( $params['createonly'] && $titleObj->exists() ) {
214 $this->dieWithError( 'apierror-articleexists' );
215 }
216 if ( $params['nocreate'] && !$titleObj->exists() ) {
217 $this->dieWithError( 'apierror-missingtitle' );
218 }
219
220 // Now let's check whether we're even allowed to do this
222 $titleObj,
223 'edit',
224 [ 'autoblock' => true, 'user' => $this->getUserForPermissions() ]
225 );
226
227 $toMD5 = $params['text'];
228 if ( $params['appendtext'] !== null || $params['prependtext'] !== null ) {
229 $content = $pageObj->getContent();
230
231 if ( !$content ) {
232 if ( $titleObj->getNamespace() === NS_MEDIAWIKI ) {
233 # If this is a MediaWiki:x message, then load the messages
234 # and return the message value for x.
235 $text = $titleObj->getDefaultMessageText();
236 if ( $text === false ) {
237 $text = '';
238 }
239
240 try {
241 $content = ContentHandler::makeContent( $text, $titleObj );
242 } catch ( MWContentSerializationException $ex ) {
243 $this->dieWithException( $ex, [
244 'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' )
245 ] );
246 }
247 } else {
248 # Otherwise, make a new empty content.
249 $content = $contentHandler->makeEmptyContent();
250 }
251 }
252
253 // @todo Add support for appending/prepending to the Content interface
254
255 if ( !( $content instanceof TextContent ) ) {
256 $this->dieWithError( [ 'apierror-appendnotsupported', $contentModel ] );
257 }
258
259 if ( $params['section'] !== null ) {
260 if ( !$contentHandler->supportsSections() ) {
261 $this->dieWithError( [ 'apierror-sectionsnotsupported', $contentModel ] );
262 }
263
264 if ( $params['section'] == 'new' ) {
265 // DWIM if they're trying to prepend/append to a new section.
266 $content = null;
267 } else {
268 // Process the content for section edits
269 $section = $params['section'];
270 $content = $content->getSection( $section );
271
272 if ( !$content ) {
273 $this->dieWithError( [ 'apierror-nosuchsection', wfEscapeWikiText( $section ) ] );
274 }
275 }
276 }
277
278 if ( !$content ) {
279 $text = '';
280 } else {
281 $text = $content->serialize( $contentFormat );
282 }
283
284 $params['text'] = $params['prependtext'] . $text . $params['appendtext'];
285 $toMD5 = $params['prependtext'] . $params['appendtext'];
286 }
287
288 if ( $params['undo'] > 0 ) {
289 $undoRev = $this->revisionLookup->getRevisionById( $params['undo'] );
290 if ( $undoRev === null || $undoRev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
291 $this->dieWithError( [ 'apierror-nosuchrevid', $params['undo'] ] );
292 }
293
294 if ( $params['undoafter'] > 0 ) {
295 $undoafterRev = $this->revisionLookup->getRevisionById( $params['undoafter'] );
296 } else {
297 // undoafter=0 or null
298 $undoafterRev = $this->revisionLookup->getPreviousRevision( $undoRev );
299 }
300 if ( $undoafterRev === null || $undoafterRev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
301 $this->dieWithError( [ 'apierror-nosuchrevid', $params['undoafter'] ] );
302 }
303
304 if ( $undoRev->getPageId() != $pageObj->getId() ) {
305 $this->dieWithError( [ 'apierror-revwrongpage', $undoRev->getId(),
306 $titleObj->getPrefixedText() ] );
307 }
308 if ( $undoafterRev->getPageId() != $pageObj->getId() ) {
309 $this->dieWithError( [ 'apierror-revwrongpage', $undoafterRev->getId(),
310 $titleObj->getPrefixedText() ] );
311 }
312
313 $newContent = $contentHandler->getUndoContent(
314 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Content is for public use here
315 $pageObj->getRevisionRecord()->getContent( SlotRecord::MAIN ),
316 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Content is for public use here
317 $undoRev->getContent( SlotRecord::MAIN ),
318 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Content is for public use here
319 $undoafterRev->getContent( SlotRecord::MAIN ),
320 $pageObj->getRevisionRecord()->getId() === $undoRev->getId()
321 );
322
323 if ( !$newContent ) {
324 $this->dieWithError( 'undo-failure', 'undofailure' );
325 }
326 if ( !$params['contentmodel'] && !$params['contentformat'] ) {
327 // If we are reverting content model, the new content model
328 // might not support the current serialization format, in
329 // which case go back to the old serialization format,
330 // but only if the user hasn't specified a format/model
331 // parameter.
332 if ( !$newContent->isSupportedFormat( $contentFormat ) ) {
333 $undoafterRevMainSlot = $undoafterRev->getSlot(
334 SlotRecord::MAIN,
335 RevisionRecord::RAW
336 );
337 $contentFormat = $undoafterRevMainSlot->getFormat();
338 if ( !$contentFormat ) {
339 // fall back to default content format for the model
340 // of $undoafterRev
341 $contentFormat = $this->contentHandlerFactory
342 ->getContentHandler( $undoafterRevMainSlot->getModel() )
343 ->getDefaultFormat();
344 }
345 }
346 // Override content model with model of undid revision.
347 $contentModel = $newContent->getModel();
348 $undoContentModel = true;
349 }
350 $params['text'] = $newContent->serialize( $contentFormat );
351 // If no summary was given and we only undid one rev,
352 // use an autosummary
353
354 if ( $params['summary'] === null ) {
355 $nextRev = $this->revisionLookup->getNextRevision( $undoafterRev );
356 if ( $nextRev && $nextRev->getId() == $params['undo'] ) {
357 $undoRevUser = $undoRev->getUser();
358 $params['summary'] = $this->msg( 'undo-summary' )
359 ->params( $params['undo'], $undoRevUser ? $undoRevUser->getName() : '' )
360 ->inContentLanguage()->text();
361 }
362 }
363 }
364
365 // See if the MD5 hash checks out
366 if ( $params['md5'] !== null && md5( $toMD5 ) !== $params['md5'] ) {
367 $this->dieWithError( 'apierror-badmd5' );
368 }
369
370 // EditPage wants to parse its stuff from a WebRequest
371 // That interface kind of sucks, but it's workable
372 $requestArray = [
373 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
374 'wpTextbox1' => $params['text'],
375 'format' => $contentFormat,
376 'model' => $contentModel,
377 'wpEditToken' => $params['token'],
378 'wpIgnoreBlankSummary' => true,
379 'wpIgnoreBlankArticle' => true,
380 'wpIgnoreSelfRedirect' => true,
381 'bot' => $params['bot'],
382 'wpUnicodeCheck' => EditPage::UNICODE_CHECK,
383 ];
384
385 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
386 if ( $params['summary'] !== null ) {
387 $requestArray['wpSummary'] = $params['summary'];
388 }
389
390 if ( $params['sectiontitle'] !== null ) {
391 $requestArray['wpSectionTitle'] = $params['sectiontitle'];
392 }
393
394 if ( $params['undo'] > 0 ) {
395 $requestArray['wpUndidRevision'] = $params['undo'];
396 }
397 if ( $params['undoafter'] > 0 ) {
398 $requestArray['wpUndoAfter'] = $params['undoafter'];
399 }
400
401 // Skip for baserevid == null or '' or '0' or 0
402 if ( !empty( $params['baserevid'] ) ) {
403 $requestArray['editRevId'] = $params['baserevid'];
404 }
405
406 // Watch out for basetimestamp == '' or '0'
407 // It gets treated as NOW, almost certainly causing an edit conflict
408 if ( $params['basetimestamp'] !== null && (bool)$this->getMain()->getVal( 'basetimestamp' ) ) {
409 $requestArray['wpEdittime'] = $params['basetimestamp'];
410 } elseif ( empty( $params['baserevid'] ) ) {
411 // Only set if baserevid is not set. Otherwise, conflicts would be ignored,
412 // due to the way userWasLastToEdit() works.
413 $requestArray['wpEdittime'] = $pageObj->getTimestamp();
414 }
415
416 if ( $params['starttimestamp'] !== null ) {
417 $requestArray['wpStarttime'] = $params['starttimestamp'];
418 } else {
419 $requestArray['wpStarttime'] = wfTimestampNow(); // Fake wpStartime
420 }
421
422 if ( $params['minor'] || ( !$params['notminor'] &&
423 $this->userOptionsLookup->getOption( $user, 'minordefault' ) )
424 ) {
425 $requestArray['wpMinoredit'] = '';
426 }
427
428 if ( $params['recreate'] ) {
429 $requestArray['wpRecreate'] = '';
430 }
431
432 if ( $params['section'] !== null ) {
433 $section = $params['section'];
434 if ( !preg_match( '/^((T-)?\d+|new)$/', $section ) ) {
435 $this->dieWithError( 'apierror-invalidsection' );
436 }
437 $content = $pageObj->getContent();
438 if ( $section !== '0'
439 && $section != 'new'
440 && ( !$content || !$content->getSection( $section ) )
441 ) {
442 $this->dieWithError( [ 'apierror-nosuchsection', $section ] );
443 }
444 $requestArray['wpSection'] = $params['section'];
445 } else {
446 $requestArray['wpSection'] = '';
447 }
448
449 $watch = $this->getWatchlistValue( $params['watchlist'], $titleObj, $user );
450
451 // Deprecated parameters
452 if ( $params['watch'] ) {
453 $watch = true;
454 } elseif ( $params['unwatch'] ) {
455 $watch = false;
456 }
457
458 if ( $watch ) {
459 $requestArray['wpWatchthis'] = true;
460 $watchlistExpiry = $this->getExpiryFromParams( $params );
461
462 if ( $watchlistExpiry ) {
463 $requestArray['wpWatchlistExpiry'] = $watchlistExpiry;
464 }
465 }
466
467 // Apply change tags
468 if ( $params['tags'] ) {
469 $tagStatus = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $this->getAuthority() );
470 if ( $tagStatus->isOK() ) {
471 $requestArray['wpChangeTags'] = implode( ',', $params['tags'] );
472 } else {
473 $this->dieStatus( $tagStatus );
474 }
475 }
476
477 // Pass through anything else we might have been given, to support extensions
478 // This is kind of a hack but it's the best we can do to make extensions work
479 $requestArray += $this->getRequest()->getValues();
480
481 global $wgTitle, $wgRequest;
482
483 $req = new DerivativeRequest( $this->getRequest(), $requestArray, true );
484
485 // Some functions depend on $wgTitle == $ep->mTitle
486 // TODO: Make them not or check if they still do
487 $wgTitle = $titleObj;
488
489 $articleContext = new RequestContext;
490 $articleContext->setRequest( $req );
491 $articleContext->setWikiPage( $pageObj );
492 $articleContext->setUser( $this->getUser() );
493
495 $articleObject = Article::newFromWikiPage( $pageObj, $articleContext );
496
497 $ep = new EditPage( $articleObject );
498
499 $ep->setApiEditOverride( true );
500 $ep->setContextTitle( $titleObj );
501 $ep->importFormData( $req );
502 $ep->maybeActivateTempUserCreate( true );
503
504 // T255700: Ensure content models of the base content
505 // and fetched revision remain the same before attempting to save.
506 $editRevId = $requestArray['editRevId'] ?? false;
507 $baseRev = $this->revisionLookup->getRevisionByTitle( $titleObj, $editRevId );
508 $baseContentModel = null;
509
510 if ( $baseRev ) {
511 $baseContent = $baseRev->getContent( SlotRecord::MAIN );
512 $baseContentModel = $baseContent ? $baseContent->getModel() : null;
513 }
514
515 $baseContentModel ??= $pageObj->getContentModel();
516
517 // However, allow the content models to possibly differ if we are intentionally
518 // changing them or we are doing an undo edit that is reverting content model change.
519 $contentModelsCanDiffer = $params['contentmodel'] || isset( $undoContentModel );
520
521 if ( !$contentModelsCanDiffer && $contentModel !== $baseContentModel ) {
522 $this->dieWithError( [ 'apierror-contentmodel-mismatch', $contentModel, $baseContentModel ] );
523 }
524
525 // Do the actual save
526 $oldRevId = $articleObject->getRevIdFetched();
527 $result = null;
528
529 // Fake $wgRequest for some hooks inside EditPage
530 // @todo FIXME: This interface SUCKS
531 $oldRequest = $wgRequest;
532 $wgRequest = $req;
533
534 $status = $ep->attemptSave( $result );
535 $statusValue = is_int( $status->value ) ? $status->value : 0;
536 $wgRequest = $oldRequest;
537
538 $r = [];
539 switch ( $statusValue ) {
540 case EditPage::AS_HOOK_ERROR:
541 case EditPage::AS_HOOK_ERROR_EXPECTED:
542 if ( $status->statusData !== null ) {
543 $r = $status->statusData;
544 $r['result'] = 'Failure';
545 $apiResult->addValue( null, $this->getModuleName(), $r );
546 return;
547 }
548 if ( !$status->getErrors() ) {
549 // This appears to be unreachable right now, because all
550 // code paths will set an error. Could change, though.
551 $status->fatal( 'hookaborted' ); // @codeCoverageIgnore
552 }
553 $this->dieStatus( $status );
554
555 // These two cases will normally have been caught earlier, and will
556 // only occur if something blocks the user between the earlier
557 // check and the check in EditPage (presumably a hook). It's not
558 // obvious that this is even possible.
559 // @codeCoverageIgnoreStart
560 case EditPage::AS_BLOCKED_PAGE_FOR_USER:
561 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
562 $this->dieBlocked( $user->getBlock() );
563 // dieBlocked prevents continuation
564
565 case EditPage::AS_READ_ONLY_PAGE:
566 $this->dieReadOnly();
567 // @codeCoverageIgnoreEnd
568
569 case EditPage::AS_SUCCESS_NEW_ARTICLE:
570 $r['new'] = true;
571 // fall-through
572
573 case EditPage::AS_SUCCESS_UPDATE:
574 $r['result'] = 'Success';
575 $r['pageid'] = (int)$titleObj->getArticleID();
576 $r['title'] = $titleObj->getPrefixedText();
577 $r['contentmodel'] = $articleObject->getPage()->getContentModel();
578 $newRevId = $articleObject->getPage()->getLatest();
579 if ( $newRevId == $oldRevId ) {
580 $r['nochange'] = true;
581 } else {
582 $r['oldrevid'] = (int)$oldRevId;
583 $r['newrevid'] = (int)$newRevId;
584 $r['newtimestamp'] = wfTimestamp( TS_ISO_8601,
585 $pageObj->getTimestamp() );
586 }
587
588 if ( $watch ) {
589 $r['watched'] = true;
590
591 $watchlistExpiry = $this->getWatchlistExpiry(
592 $this->watchedItemStore,
593 $titleObj,
594 $user
595 );
596
597 if ( $watchlistExpiry ) {
598 $r['watchlistexpiry'] = $watchlistExpiry;
599 }
600 }
601 $this->persistGlobalSession();
602
603 if ( isset( $result['savedTempUser'] ) ) {
604 $r['tempusercreated'] = true;
605 $params['returnto'] ??= $titleObj->getPrefixedDBkey();
606 $redirectUrl = $this->getTempUserRedirectUrl(
607 $params,
608 $result['savedTempUser']
609 );
610 if ( $redirectUrl ) {
611 $r['tempusercreatedredirect'] = $redirectUrl;
612 }
613 }
614
615 break;
616
617 default:
618 if ( !$status->getErrors() ) {
619 // EditPage sometimes only sets the status code without setting
620 // any actual error messages. Supply defaults for those cases.
621 switch ( $statusValue ) {
622 // Currently needed
623 case EditPage::AS_IMAGE_REDIRECT_ANON:
624 $status->fatal( 'apierror-noimageredirect-anon' );
625 break;
626 case EditPage::AS_IMAGE_REDIRECT_LOGGED:
627 $status->fatal( 'apierror-noimageredirect' );
628 break;
629 case EditPage::AS_CONTENT_TOO_BIG:
630 case EditPage::AS_MAX_ARTICLE_SIZE_EXCEEDED:
631 $status->fatal( 'apierror-contenttoobig',
632 $this->getConfig()->get( MainConfigNames::MaxArticleSize ) );
633 break;
634 case EditPage::AS_READ_ONLY_PAGE_ANON:
635 $status->fatal( 'apierror-noedit-anon' );
636 break;
637 case EditPage::AS_NO_CHANGE_CONTENT_MODEL:
638 $status->fatal( 'apierror-cantchangecontentmodel' );
639 break;
640 case EditPage::AS_ARTICLE_WAS_DELETED:
641 $status->fatal( 'apierror-pagedeleted' );
642 break;
643 case EditPage::AS_CONFLICT_DETECTED:
644 $status->fatal( 'edit-conflict' );
645 break;
646
647 // Currently shouldn't be needed, but here in case
648 // hooks use them without setting appropriate
649 // errors on the status.
650 // @codeCoverageIgnoreStart
651 case EditPage::AS_SPAM_ERROR:
652 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
653 $status->fatal( 'apierror-spamdetected', $result['spam'] );
654 break;
655 case EditPage::AS_READ_ONLY_PAGE_LOGGED:
656 $status->fatal( 'apierror-noedit' );
657 break;
658 case EditPage::AS_RATE_LIMITED:
659 $status->fatal( 'apierror-ratelimited' );
660 break;
661 case EditPage::AS_NO_CREATE_PERMISSION:
662 $status->fatal( 'nocreate-loggedin' );
663 break;
664 case EditPage::AS_BLANK_ARTICLE:
665 $status->fatal( 'apierror-emptypage' );
666 break;
667 case EditPage::AS_TEXTBOX_EMPTY:
668 $status->fatal( 'apierror-emptynewsection' );
669 break;
670 case EditPage::AS_SUMMARY_NEEDED:
671 $status->fatal( 'apierror-summaryrequired' );
672 break;
673 default:
674 wfWarn( __METHOD__ . ": Unknown EditPage code $statusValue with no message" );
675 $status->fatal( 'apierror-unknownerror-editpage', $statusValue );
676 break;
677 // @codeCoverageIgnoreEnd
678 }
679 }
680 $this->dieStatus( $status );
681 }
682 $apiResult->addValue( null, $this->getModuleName(), $r );
683 }
684
685 public function mustBePosted() {
686 return true;
687 }
688
689 public function isWriteMode() {
690 return true;
691 }
692
693 public function getAllowedParams() {
694 $params = [
695 'title' => [
696 ParamValidator::PARAM_TYPE => 'string',
697 ],
698 'pageid' => [
699 ParamValidator::PARAM_TYPE => 'integer',
700 ],
701 'section' => null,
702 'sectiontitle' => [
703 ParamValidator::PARAM_TYPE => 'string',
704 ],
705 'text' => [
706 ParamValidator::PARAM_TYPE => 'text',
707 ],
708 'summary' => null,
709 'tags' => [
710 ParamValidator::PARAM_TYPE => 'tags',
711 ParamValidator::PARAM_ISMULTI => true,
712 ],
713 'minor' => false,
714 'notminor' => false,
715 'bot' => false,
716 'baserevid' => [
717 ParamValidator::PARAM_TYPE => 'integer',
718 ],
719 'basetimestamp' => [
720 ParamValidator::PARAM_TYPE => 'timestamp',
721 ],
722 'starttimestamp' => [
723 ParamValidator::PARAM_TYPE => 'timestamp',
724 ],
725 'recreate' => false,
726 'createonly' => false,
727 'nocreate' => false,
728 'watch' => [
729 ParamValidator::PARAM_DEFAULT => false,
730 ParamValidator::PARAM_DEPRECATED => true,
731 ],
732 'unwatch' => [
733 ParamValidator::PARAM_DEFAULT => false,
734 ParamValidator::PARAM_DEPRECATED => true,
735 ],
736 ];
737
738 // Params appear in the docs in the order they are defined,
739 // which is why this is here and not at the bottom.
740 $params += $this->getWatchlistParams();
741
742 $params += [
743 'md5' => null,
744 'prependtext' => [
745 ParamValidator::PARAM_TYPE => 'text',
746 ],
747 'appendtext' => [
748 ParamValidator::PARAM_TYPE => 'text',
749 ],
750 'undo' => [
751 ParamValidator::PARAM_TYPE => 'integer',
752 IntegerDef::PARAM_MIN => 0,
754 ],
755 'undoafter' => [
756 ParamValidator::PARAM_TYPE => 'integer',
757 IntegerDef::PARAM_MIN => 0,
759 ],
760 'redirect' => [
761 ParamValidator::PARAM_TYPE => 'boolean',
762 ParamValidator::PARAM_DEFAULT => false,
763 ],
764 'contentformat' => [
765 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
766 ],
767 'contentmodel' => [
768 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
769 ],
770 'token' => [
771 // Standard definition automatically inserted
772 ApiBase::PARAM_HELP_MSG_APPEND => [ 'apihelp-edit-param-token' ],
773 ],
774 ];
775
776 $params += $this->getCreateTempUserParams();
777
778 return $params;
779 }
780
781 public function needsToken() {
782 return 'csrf';
783 }
784
785 protected function getExamplesMessages() {
786 return [
787 'action=edit&title=Test&summary=test%20summary&' .
788 'text=article%20content&baserevid=1234567&token=123ABC'
789 => 'apihelp-edit-example-edit',
790 'action=edit&title=Test&summary=NOTOC&minor=&' .
791 'prependtext=__NOTOC__%0A&basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
792 => 'apihelp-edit-example-prepend',
793 'action=edit&title=Test&undo=13585&undoafter=13579&' .
794 'basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
795 => 'apihelp-edit-example-undo',
796 ];
797 }
798
799 public function getHelpUrls() {
800 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Edit';
801 }
802}
getWatchlistValue(string $watchlist, Title $title, User $user, ?string $userOption=null)
Return true if we're to watch the page, false if not.
getExpiryFromParams(array $params)
Get formatted expiry from the given parameters, or null if no expiry was provided.
getWatchlistExpiry(WatchedItemStoreInterface $store, Title $title, UserIdentity $user)
Get existing expiry from the database.
getWatchlistParams(array $watchOptions=[])
Get additional allow params specific to watchlisting.
const NS_MEDIAWIKI
Definition Defines.php:72
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
global $wgRequest
Definition Setup.php:415
if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode) $wgTitle
Definition Setup.php:536
This abstract class implements many basic API functions, and is the base of all API classes.
Definition ApiBase.php:62
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1529
checkTitleUserPermissions(PageIdentity $pageIdentity, $actions, array $options=[])
Helper function for permission-denied errors.
Definition ApiBase.php:1667
getMain()
Get the main module.
Definition ApiBase.php:546
getErrorFormatter()
Definition ApiBase.php:678
const PARAM_HELP_MSG_APPEND
((string|array|Message)[]) Specify additional i18n messages to append to the normal message for this ...
Definition ApiBase.php:176
dieReadOnly()
Helper function for readonly errors.
Definition ApiBase.php:1628
requireAtLeastOneParameter( $params,... $required)
Die if 0 of a certain set of parameters is set and not false.
Definition ApiBase.php:1006
getResult()
Get the result object.
Definition ApiBase.php:667
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:807
const PARAM_RANGE_ENFORCE
(boolean) Inverse of IntegerDef::PARAM_IGNORE_RANGE
Definition ApiBase.php:159
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:528
getTitleOrPageId( $params, $load=false)
Attempts to load a WikiPage object from a title or pageid parameter, if possible.
Definition ApiBase.php:1094
dieStatus(StatusValue $status)
Throw an ApiUsageException based on the Status object.
Definition ApiBase.php:1584
useTransactionalTimeLimit()
Call wfTransactionalTimeLimit() if this request was POSTed.
Definition ApiBase.php:1377
dieBlocked(Block $block)
Throw an ApiUsageException, which will (if uncaught) call the main module's error handler and die wit...
Definition ApiBase.php:1558
dieWithException(Throwable $exception, array $options=[])
Abort execution with an error derived from a throwable.
Definition ApiBase.php:1542
A module that allows for editing and creating pages.
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
needsToken()
Returns the token type this module requires in order to execute.
__construct(ApiMain $mainModule, $moduleName, IContentHandlerFactory $contentHandlerFactory=null, RevisionLookup $revisionLookup=null, WatchedItemStoreInterface $watchedItemStore=null, WikiPageFactory $wikiPageFactory=null, WatchlistManager $watchlistManager=null, UserOptionsLookup $userOptionsLookup=null, RedirectLookup $redirectLookup=null, TempUserCreator $tempUserCreator=null, UserFactory $userFactory=null)
isWriteMode()
Indicates whether this module requires write mode.
mustBePosted()
Indicates whether this module must be called with a POST request.
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
getExamplesMessages()
Returns usage examples for this module.
getHelpUrls()
Return links to more detailed help pages about the module.
This is the main API class, used for both external and internal processing.
Definition ApiMain.php:64
static canAddTagsAccompanyingChange(array $tags, Authority $performer=null, $checkBlock=true)
Is it OK to allow the user to apply all the specified tags at the same time as they edit/make the cha...
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Exception representing a failure to serialize or unserialize a content object.
The HTML user interface for page editing.
Definition EditPage.php:143
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Service for creating WikiPage objects.
Similar to MediaWiki\Request\FauxRequest, but only fakes URL parameters and method (POST or GET) and ...
Page revision base class.
Value object representing a content slot associated with a page revision.
Represents a title within MediaWiki.
Definition Title.php:79
Provides access to user options.
Service for temporary user creation.
Creates User objects.
internal since 1.36
Definition User.php:96
static plaintextParam( $plaintext)
Definition Message.php:1275
Group all the pieces relevant to the context of a request into one instance.
setRequest(WebRequest $request)
Content object implementation for representing flat text.
Service for formatting and validating API parameters.
Type definition for integer types.
trait ApiWatchlistTrait
An ApiWatchlistTrait adds class properties and convenience methods for APIs that allow you to watch a...
trait ApiCreateTempUserTrait
Methods needed by APIs that create a temporary user.
Service for resolving a wiki page redirect.
Service for looking up page revisions.
$content
Definition router.php:76