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