MediaWiki master
ApiEditPage.php
Go to the documentation of this file.
1<?php
23namespace MediaWiki\Api;
24
25use Article;
26use ChangeTags;
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 'bot' => $params['bot'],
380 'wpUnicodeCheck' => EditPage::UNICODE_CHECK,
381 ];
382
383 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
384 if ( $params['summary'] !== null ) {
385 $requestArray['wpSummary'] = $params['summary'];
386 }
387
388 if ( $params['sectiontitle'] !== null ) {
389 $requestArray['wpSectionTitle'] = $params['sectiontitle'];
390 }
391
392 if ( $params['undo'] > 0 ) {
393 $requestArray['wpUndidRevision'] = $params['undo'];
394 }
395 if ( $params['undoafter'] > 0 ) {
396 $requestArray['wpUndoAfter'] = $params['undoafter'];
397 }
398
399 // Skip for baserevid == null or '' or '0' or 0
400 if ( !empty( $params['baserevid'] ) ) {
401 $requestArray['editRevId'] = $params['baserevid'];
402 }
403
404 // Watch out for basetimestamp == '' or '0'
405 // It gets treated as NOW, almost certainly causing an edit conflict
406 if ( $params['basetimestamp'] !== null && (bool)$this->getMain()->getVal( 'basetimestamp' ) ) {
407 $requestArray['wpEdittime'] = $params['basetimestamp'];
408 } elseif ( empty( $params['baserevid'] ) ) {
409 // Only set if baserevid is not set. Otherwise, conflicts would be ignored,
410 // due to the way userWasLastToEdit() works.
411 $requestArray['wpEdittime'] = $pageObj->getTimestamp();
412 }
413
414 if ( $params['starttimestamp'] !== null ) {
415 $requestArray['wpStarttime'] = $params['starttimestamp'];
416 } else {
417 $requestArray['wpStarttime'] = wfTimestampNow(); // Fake wpStartime
418 }
419
420 if ( $params['minor'] || ( !$params['notminor'] &&
421 $this->userOptionsLookup->getOption( $user, 'minordefault' ) )
422 ) {
423 $requestArray['wpMinoredit'] = '';
424 }
425
426 if ( $params['recreate'] ) {
427 $requestArray['wpRecreate'] = '';
428 }
429
430 if ( $params['section'] !== null ) {
431 $section = $params['section'];
432 if ( !preg_match( '/^((T-)?\d+|new)$/', $section ) ) {
433 $this->dieWithError( 'apierror-invalidsection' );
434 }
435 $content = $pageObj->getContent();
436 if ( $section !== '0'
437 && $section != 'new'
438 && ( !$content || !$content->getSection( $section ) )
439 ) {
440 $this->dieWithError( [ 'apierror-nosuchsection', $section ] );
441 }
442 $requestArray['wpSection'] = $params['section'];
443 } else {
444 $requestArray['wpSection'] = '';
445 }
446
447 $watch = $this->getWatchlistValue( $params['watchlist'], $titleObj, $user );
448
449 // Deprecated parameters
450 if ( $params['watch'] ) {
451 $watch = true;
452 } elseif ( $params['unwatch'] ) {
453 $watch = false;
454 }
455
456 if ( $watch ) {
457 $requestArray['wpWatchthis'] = true;
458 $watchlistExpiry = $this->getExpiryFromParams( $params );
459
460 if ( $watchlistExpiry ) {
461 $requestArray['wpWatchlistExpiry'] = $watchlistExpiry;
462 }
463 }
464
465 // Apply change tags
466 if ( $params['tags'] ) {
467 $tagStatus = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $this->getAuthority() );
468 if ( $tagStatus->isOK() ) {
469 $requestArray['wpChangeTags'] = implode( ',', $params['tags'] );
470 } else {
471 $this->dieStatus( $tagStatus );
472 }
473 }
474
475 // Pass through anything else we might have been given, to support extensions
476 // This is kind of a hack but it's the best we can do to make extensions work
477 $requestArray += $this->getRequest()->getValues();
478
479 // phpcs:ignore MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage,MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgTitle
480 global $wgTitle, $wgRequest;
481
482 $req = new DerivativeRequest( $this->getRequest(), $requestArray, true );
483
484 // Some functions depend on $wgTitle == $ep->mTitle
485 // TODO: Make them not or check if they still do
486 $wgTitle = $titleObj;
487
488 $articleContext = new RequestContext;
489 $articleContext->setRequest( $req );
490 $articleContext->setWikiPage( $pageObj );
491 $articleContext->setUser( $this->getUser() );
492
494 $articleObject = Article::newFromWikiPage( $pageObj, $articleContext );
495
496 $ep = new EditPage( $articleObject );
497
498 $ep->setApiEditOverride( true );
499 $ep->setContextTitle( $titleObj );
500 $ep->importFormData( $req );
501 $tempUserCreateStatus = $ep->maybeActivateTempUserCreate( true );
502 if ( !$tempUserCreateStatus->isOK() ) {
503 $this->dieWithError( 'apierror-tempuseracquirefailed', 'tempuseracquirefailed' );
504 }
505
506 // T255700: Ensure content models of the base content
507 // and fetched revision remain the same before attempting to save.
508 $editRevId = $requestArray['editRevId'] ?? false;
509 $baseRev = $this->revisionLookup->getRevisionByTitle( $titleObj, $editRevId );
510 $baseContentModel = null;
511
512 if ( $baseRev ) {
513 $baseContent = $baseRev->getContent( SlotRecord::MAIN );
514 $baseContentModel = $baseContent ? $baseContent->getModel() : null;
515 }
516
517 $baseContentModel ??= $pageObj->getContentModel();
518
519 // However, allow the content models to possibly differ if we are intentionally
520 // changing them or we are doing an undo edit that is reverting content model change.
521 $contentModelsCanDiffer = $params['contentmodel'] || isset( $undoContentModel );
522
523 if ( !$contentModelsCanDiffer && $contentModel !== $baseContentModel ) {
524 $this->dieWithError( [ 'apierror-contentmodel-mismatch', $contentModel, $baseContentModel ] );
525 }
526
527 // Do the actual save
528 $oldRevId = $articleObject->getRevIdFetched();
529 $result = null;
530
531 // Fake $wgRequest for some hooks inside EditPage
532 // @todo FIXME: This interface SUCKS
533 // phpcs:disable MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage
534 $oldRequest = $wgRequest;
535 $wgRequest = $req;
536
537 $status = $ep->attemptSave( $result );
538 $statusValue = is_int( $status->value ) ? $status->value : 0;
539 $wgRequest = $oldRequest;
540 // phpcs:enable MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage
541
542 $r = [];
543 switch ( $statusValue ) {
544 case EditPage::AS_HOOK_ERROR:
545 case EditPage::AS_HOOK_ERROR_EXPECTED:
546 if ( $status->statusData !== null ) {
547 $r = $status->statusData;
548 $r['result'] = 'Failure';
549 $apiResult->addValue( null, $this->getModuleName(), $r );
550 return;
551 }
552 if ( !$status->getMessages() ) {
553 // This appears to be unreachable right now, because all
554 // code paths will set an error. Could change, though.
555 $status->fatal( 'hookaborted' ); // @codeCoverageIgnore
556 }
557 $this->dieStatus( $status );
558
559 // These two cases will normally have been caught earlier, and will
560 // only occur if something blocks the user between the earlier
561 // check and the check in EditPage (presumably a hook). It's not
562 // obvious that this is even possible.
563 // @codeCoverageIgnoreStart
564 case EditPage::AS_BLOCKED_PAGE_FOR_USER:
565 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
566 $this->dieBlocked( $user->getBlock() );
567 // dieBlocked prevents continuation
568
569 case EditPage::AS_READ_ONLY_PAGE:
570 $this->dieReadOnly();
571 // @codeCoverageIgnoreEnd
572
573 case EditPage::AS_SUCCESS_NEW_ARTICLE:
574 $r['new'] = true;
575 // fall-through
576
577 case EditPage::AS_SUCCESS_UPDATE:
578 $r['result'] = 'Success';
579 $r['pageid'] = (int)$titleObj->getArticleID();
580 $r['title'] = $titleObj->getPrefixedText();
581 $r['contentmodel'] = $articleObject->getPage()->getContentModel();
582 $newRevId = $articleObject->getPage()->getLatest();
583 if ( $newRevId == $oldRevId ) {
584 $r['nochange'] = true;
585 } else {
586 $r['oldrevid'] = (int)$oldRevId;
587 $r['newrevid'] = (int)$newRevId;
588 $r['newtimestamp'] = wfTimestamp( TS_ISO_8601,
589 $pageObj->getTimestamp() );
590 }
591
592 if ( $watch ) {
593 $r['watched'] = true;
594
595 $watchlistExpiry = $this->getWatchlistExpiry(
596 $this->watchedItemStore,
597 $titleObj,
598 $user
599 );
600
601 if ( $watchlistExpiry ) {
602 $r['watchlistexpiry'] = $watchlistExpiry;
603 }
604 }
605 $this->persistGlobalSession();
606
607 // If the temporary account was created in this request,
608 // or if the temporary account has zero edits (implying
609 // that the account was created during a failed edit
610 // attempt in a previous request), perform the top-level
611 // redirect to ensure the account is attached.
612 // Note that the temp user could already have performed
613 // the top-level redirect if this a first edit on
614 // a wiki that is not the user's home wiki.
615 $shouldRedirectForTempUser = isset( $result['savedTempUser'] ) ||
616 ( $user->isTemp() && ( $user->getEditCount() === 0 ) );
617 if ( $shouldRedirectForTempUser ) {
618 $r['tempusercreated'] = true;
619 $params['returnto'] ??= $titleObj->getPrefixedDBkey();
620 $redirectUrl = $this->getTempUserRedirectUrl(
621 $params,
622 $result['savedTempUser'] ?? $user
623 );
624 if ( $redirectUrl ) {
625 $r['tempusercreatedredirect'] = $redirectUrl;
626 }
627 }
628
629 break;
630
631 default:
632 if ( !$status->getMessages() ) {
633 // EditPage sometimes only sets the status code without setting
634 // any actual error messages. Supply defaults for those cases.
635 switch ( $statusValue ) {
636 // Currently needed
637 case EditPage::AS_IMAGE_REDIRECT_ANON:
638 $status->fatal( 'apierror-noimageredirect-anon' );
639 break;
640 case EditPage::AS_IMAGE_REDIRECT_LOGGED:
641 $status->fatal( 'apierror-noimageredirect' );
642 break;
643 case EditPage::AS_CONTENT_TOO_BIG:
644 case EditPage::AS_MAX_ARTICLE_SIZE_EXCEEDED:
645 $status->fatal( 'apierror-contenttoobig',
647 break;
648 case EditPage::AS_READ_ONLY_PAGE_ANON:
649 $status->fatal( 'apierror-noedit-anon' );
650 break;
651 case EditPage::AS_NO_CHANGE_CONTENT_MODEL:
652 $status->fatal( 'apierror-cantchangecontentmodel' );
653 break;
654 case EditPage::AS_ARTICLE_WAS_DELETED:
655 $status->fatal( 'apierror-pagedeleted' );
656 break;
657 case EditPage::AS_CONFLICT_DETECTED:
658 $status->fatal( 'edit-conflict' );
659 break;
660
661 // Currently shouldn't be needed, but here in case
662 // hooks use them without setting appropriate
663 // errors on the status.
664 // @codeCoverageIgnoreStart
665 case EditPage::AS_SPAM_ERROR:
666 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
667 $status->fatal( 'apierror-spamdetected', $result['spam'] );
668 break;
669 case EditPage::AS_READ_ONLY_PAGE_LOGGED:
670 $status->fatal( 'apierror-noedit' );
671 break;
672 case EditPage::AS_RATE_LIMITED:
673 $status->fatal( 'apierror-ratelimited' );
674 break;
675 case EditPage::AS_NO_CREATE_PERMISSION:
676 $status->fatal( 'nocreate-loggedin' );
677 break;
678 case EditPage::AS_BLANK_ARTICLE:
679 $status->fatal( 'apierror-emptypage' );
680 break;
681 case EditPage::AS_TEXTBOX_EMPTY:
682 $status->fatal( 'apierror-emptynewsection' );
683 break;
684 case EditPage::AS_SUMMARY_NEEDED:
685 $status->fatal( 'apierror-summaryrequired' );
686 break;
687 default:
688 wfWarn( __METHOD__ . ": Unknown EditPage code $statusValue with no message" );
689 $status->fatal( 'apierror-unknownerror-editpage', $statusValue );
690 break;
691 // @codeCoverageIgnoreEnd
692 }
693 }
694 $this->dieStatus( $status );
695 }
696 $apiResult->addValue( null, $this->getModuleName(), $r );
697 }
698
699 public function mustBePosted() {
700 return true;
701 }
702
703 public function isWriteMode() {
704 return true;
705 }
706
707 public function getAllowedParams() {
708 $params = [
709 'title' => [
710 ParamValidator::PARAM_TYPE => 'string',
711 ],
712 'pageid' => [
713 ParamValidator::PARAM_TYPE => 'integer',
714 ],
715 'section' => null,
716 'sectiontitle' => [
717 ParamValidator::PARAM_TYPE => 'string',
718 ],
719 'text' => [
720 ParamValidator::PARAM_TYPE => 'text',
721 ],
722 'summary' => null,
723 'tags' => [
724 ParamValidator::PARAM_TYPE => 'tags',
725 ParamValidator::PARAM_ISMULTI => true,
726 ],
727 'minor' => false,
728 'notminor' => false,
729 'bot' => false,
730 'baserevid' => [
731 ParamValidator::PARAM_TYPE => 'integer',
732 ],
733 'basetimestamp' => [
734 ParamValidator::PARAM_TYPE => 'timestamp',
735 ],
736 'starttimestamp' => [
737 ParamValidator::PARAM_TYPE => 'timestamp',
738 ],
739 'recreate' => false,
740 'createonly' => false,
741 'nocreate' => false,
742 'watch' => [
743 ParamValidator::PARAM_DEFAULT => false,
744 ParamValidator::PARAM_DEPRECATED => true,
745 ],
746 'unwatch' => [
747 ParamValidator::PARAM_DEFAULT => false,
748 ParamValidator::PARAM_DEPRECATED => true,
749 ],
750 ];
751
752 // Params appear in the docs in the order they are defined,
753 // which is why this is here and not at the bottom.
754 $params += $this->getWatchlistParams();
755
756 $params += [
757 'md5' => null,
758 'prependtext' => [
759 ParamValidator::PARAM_TYPE => 'text',
760 ],
761 'appendtext' => [
762 ParamValidator::PARAM_TYPE => 'text',
763 ],
764 'undo' => [
765 ParamValidator::PARAM_TYPE => 'integer',
766 IntegerDef::PARAM_MIN => 0,
768 ],
769 'undoafter' => [
770 ParamValidator::PARAM_TYPE => 'integer',
771 IntegerDef::PARAM_MIN => 0,
773 ],
774 'redirect' => [
775 ParamValidator::PARAM_TYPE => 'boolean',
776 ParamValidator::PARAM_DEFAULT => false,
777 ],
778 'contentformat' => [
779 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
780 ],
781 'contentmodel' => [
782 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
783 ],
784 'token' => [
785 // Standard definition automatically inserted
786 ApiBase::PARAM_HELP_MSG_APPEND => [ 'apihelp-edit-param-token' ],
787 ],
788 ];
789
790 $params += $this->getCreateTempUserParams();
791
792 return $params;
793 }
794
795 public function needsToken() {
796 return 'csrf';
797 }
798
799 protected function getExamplesMessages() {
800 return [
801 'action=edit&title=Test&summary=test%20summary&' .
802 'text=article%20content&baserevid=1234567&token=123ABC'
803 => 'apihelp-edit-example-edit',
804 'action=edit&title=Test&summary=NOTOC&minor=&' .
805 'prependtext=__NOTOC__%0A&basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
806 => 'apihelp-edit-example-prepend',
807 'action=edit&title=Test&undo=13585&undoafter=13579&' .
808 'basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
809 => 'apihelp-edit-example-undo',
810 ];
811 }
812
813 public function getHelpUrls() {
814 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Edit';
815 }
816}
817
819class_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:442
if(!defined( 'MW_NO_SESSION') &&MW_ENTRY_POINT !=='cli' $wgTitle
Definition Setup.php:563
array $params
The job parameters.
Legacy class representing an editable page and handling UI for some page actions.
Definition Article.php:71
Recent changes tagging.
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.
This abstract class implements many basic API functions, and is the base of all API classes.
Definition ApiBase.php:76
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1565
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:571
requireAtLeastOneParameter( $params,... $required)
Die if 0 of a certain set of parameters is set and not false.
Definition ApiBase.php:1050
getMain()
Get the main module.
Definition ApiBase.php:589
useTransactionalTimeLimit()
Call wfTransactionalTimeLimit() if this request was POSTed.
Definition ApiBase.php:1413
getResult()
Get the result object.
Definition ApiBase.php:710
const PARAM_RANGE_ENFORCE
(boolean) Inverse of IntegerDef::PARAM_IGNORE_RANGE
Definition ApiBase.php:173
const PARAM_HELP_MSG_APPEND
((string|array|Message)[]) Specify additional i18n messages to append to the normal message for this ...
Definition ApiBase.php:192
dieWithException(Throwable $exception, array $options=[])
Abort execution with an error derived from a throwable.
Definition ApiBase.php:1578
dieBlocked(Block $block)
Throw an ApiUsageException, which will (if uncaught) call the main module's error handler and die wit...
Definition ApiBase.php:1593
dieStatus(StatusValue $status)
Throw an ApiUsageException based on the Status object.
Definition ApiBase.php:1620
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:851
dieReadOnly()
Helper function for readonly errors.
Definition ApiBase.php:1664
checkTitleUserPermissions(PageIdentity $pageIdentity, $actions, array $options=[])
Helper function for permission-denied errors.
Definition ApiBase.php:1703
getTitleOrPageId( $params, $load=false)
Attempts to load a WikiPage object from a title or pageid parameter, if possible.
Definition ApiBase.php:1172
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:78
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.
A content handler knows how do deal with a specific type of content on a wiki page.
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:149
A class containing constants representing the names of configuration variables.
const MaxArticleSize
Name constant for the MaxArticleSize setting, for use with Config::get()
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:155
static plaintextParam( $plaintext)
Definition Message.php:1310
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 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.