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