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