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 ( isset( $result['savedTempUser'] ) ) {
611 $r['tempusercreated'] = true;
612 $params['returnto'] ??= $titleObj->getPrefixedDBkey();
613 $redirectUrl = $this->getTempUserRedirectUrl(
614 $params,
615 $result['savedTempUser']
616 );
617 if ( $redirectUrl ) {
618 $r['tempusercreatedredirect'] = $redirectUrl;
619 }
620 }
621
622 break;
623
624 default:
625 if ( !$status->getErrors() ) {
626 // EditPage sometimes only sets the status code without setting
627 // any actual error messages. Supply defaults for those cases.
628 switch ( $statusValue ) {
629 // Currently needed
630 case EditPage::AS_IMAGE_REDIRECT_ANON:
631 $status->fatal( 'apierror-noimageredirect-anon' );
632 break;
633 case EditPage::AS_IMAGE_REDIRECT_LOGGED:
634 $status->fatal( 'apierror-noimageredirect' );
635 break;
636 case EditPage::AS_CONTENT_TOO_BIG:
637 case EditPage::AS_MAX_ARTICLE_SIZE_EXCEEDED:
638 $status->fatal( 'apierror-contenttoobig',
639 $this->getConfig()->get( MainConfigNames::MaxArticleSize ) );
640 break;
641 case EditPage::AS_READ_ONLY_PAGE_ANON:
642 $status->fatal( 'apierror-noedit-anon' );
643 break;
644 case EditPage::AS_NO_CHANGE_CONTENT_MODEL:
645 $status->fatal( 'apierror-cantchangecontentmodel' );
646 break;
647 case EditPage::AS_ARTICLE_WAS_DELETED:
648 $status->fatal( 'apierror-pagedeleted' );
649 break;
650 case EditPage::AS_CONFLICT_DETECTED:
651 $status->fatal( 'edit-conflict' );
652 break;
653
654 // Currently shouldn't be needed, but here in case
655 // hooks use them without setting appropriate
656 // errors on the status.
657 // @codeCoverageIgnoreStart
658 case EditPage::AS_SPAM_ERROR:
659 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
660 $status->fatal( 'apierror-spamdetected', $result['spam'] );
661 break;
662 case EditPage::AS_READ_ONLY_PAGE_LOGGED:
663 $status->fatal( 'apierror-noedit' );
664 break;
665 case EditPage::AS_RATE_LIMITED:
666 $status->fatal( 'apierror-ratelimited' );
667 break;
668 case EditPage::AS_NO_CREATE_PERMISSION:
669 $status->fatal( 'nocreate-loggedin' );
670 break;
671 case EditPage::AS_BLANK_ARTICLE:
672 $status->fatal( 'apierror-emptypage' );
673 break;
674 case EditPage::AS_TEXTBOX_EMPTY:
675 $status->fatal( 'apierror-emptynewsection' );
676 break;
677 case EditPage::AS_SUMMARY_NEEDED:
678 $status->fatal( 'apierror-summaryrequired' );
679 break;
680 default:
681 wfWarn( __METHOD__ . ": Unknown EditPage code $statusValue with no message" );
682 $status->fatal( 'apierror-unknownerror-editpage', $statusValue );
683 break;
684 // @codeCoverageIgnoreEnd
685 }
686 }
687 $this->dieStatus( $status );
688 }
689 $apiResult->addValue( null, $this->getModuleName(), $r );
690 }
691
692 public function mustBePosted() {
693 return true;
694 }
695
696 public function isWriteMode() {
697 return true;
698 }
699
700 public function getAllowedParams() {
701 $params = [
702 'title' => [
703 ParamValidator::PARAM_TYPE => 'string',
704 ],
705 'pageid' => [
706 ParamValidator::PARAM_TYPE => 'integer',
707 ],
708 'section' => null,
709 'sectiontitle' => [
710 ParamValidator::PARAM_TYPE => 'string',
711 ],
712 'text' => [
713 ParamValidator::PARAM_TYPE => 'text',
714 ],
715 'summary' => null,
716 'tags' => [
717 ParamValidator::PARAM_TYPE => 'tags',
718 ParamValidator::PARAM_ISMULTI => true,
719 ],
720 'minor' => false,
721 'notminor' => false,
722 'bot' => false,
723 'baserevid' => [
724 ParamValidator::PARAM_TYPE => 'integer',
725 ],
726 'basetimestamp' => [
727 ParamValidator::PARAM_TYPE => 'timestamp',
728 ],
729 'starttimestamp' => [
730 ParamValidator::PARAM_TYPE => 'timestamp',
731 ],
732 'recreate' => false,
733 'createonly' => false,
734 'nocreate' => false,
735 'watch' => [
736 ParamValidator::PARAM_DEFAULT => false,
737 ParamValidator::PARAM_DEPRECATED => true,
738 ],
739 'unwatch' => [
740 ParamValidator::PARAM_DEFAULT => false,
741 ParamValidator::PARAM_DEPRECATED => true,
742 ],
743 ];
744
745 // Params appear in the docs in the order they are defined,
746 // which is why this is here and not at the bottom.
747 $params += $this->getWatchlistParams();
748
749 $params += [
750 'md5' => null,
751 'prependtext' => [
752 ParamValidator::PARAM_TYPE => 'text',
753 ],
754 'appendtext' => [
755 ParamValidator::PARAM_TYPE => 'text',
756 ],
757 'undo' => [
758 ParamValidator::PARAM_TYPE => 'integer',
759 IntegerDef::PARAM_MIN => 0,
761 ],
762 'undoafter' => [
763 ParamValidator::PARAM_TYPE => 'integer',
764 IntegerDef::PARAM_MIN => 0,
766 ],
767 'redirect' => [
768 ParamValidator::PARAM_TYPE => 'boolean',
769 ParamValidator::PARAM_DEFAULT => false,
770 ],
771 'contentformat' => [
772 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
773 ],
774 'contentmodel' => [
775 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
776 ],
777 'token' => [
778 // Standard definition automatically inserted
779 ApiBase::PARAM_HELP_MSG_APPEND => [ 'apihelp-edit-param-token' ],
780 ],
781 ];
782
783 $params += $this->getCreateTempUserParams();
784
785 return $params;
786 }
787
788 public function needsToken() {
789 return 'csrf';
790 }
791
792 protected function getExamplesMessages() {
793 return [
794 'action=edit&title=Test&summary=test%20summary&' .
795 'text=article%20content&baserevid=1234567&token=123ABC'
796 => 'apihelp-edit-example-edit',
797 'action=edit&title=Test&summary=NOTOC&minor=&' .
798 'prependtext=__NOTOC__%0A&basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
799 => 'apihelp-edit-example-prepend',
800 'action=edit&title=Test&undo=13585&undoafter=13579&' .
801 'basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
802 => 'apihelp-edit-example-undo',
803 ];
804 }
805
806 public function getHelpUrls() {
807 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Edit';
808 }
809}
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:415
if(!defined( 'MW_NO_SESSION') &&MW_ENTRY_POINT !=='cli' $wgTitle
Definition Setup.php:536
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:65
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:145
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.