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