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