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