MediaWiki 1.42.0
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 global $wgTitle, $wgRequest;
483
484 $req = new DerivativeRequest( $this->getRequest(), $requestArray, true );
485
486 // Some functions depend on $wgTitle == $ep->mTitle
487 // TODO: Make them not or check if they still do
488 $wgTitle = $titleObj;
489
490 $articleContext = new RequestContext;
491 $articleContext->setRequest( $req );
492 $articleContext->setWikiPage( $pageObj );
493 $articleContext->setUser( $this->getUser() );
494
496 $articleObject = Article::newFromWikiPage( $pageObj, $articleContext );
497
498 $ep = new EditPage( $articleObject );
499
500 $ep->setApiEditOverride( true );
501 $ep->setContextTitle( $titleObj );
502 $ep->importFormData( $req );
503 $tempUserCreateStatus = $ep->maybeActivateTempUserCreate( true );
504 if ( !$tempUserCreateStatus->isOK() ) {
505 $this->dieWithError( 'apierror-tempuseracquirefailed', 'tempuseracquirefailed' );
506 }
507
508 // T255700: Ensure content models of the base content
509 // and fetched revision remain the same before attempting to save.
510 $editRevId = $requestArray['editRevId'] ?? false;
511 $baseRev = $this->revisionLookup->getRevisionByTitle( $titleObj, $editRevId );
512 $baseContentModel = null;
513
514 if ( $baseRev ) {
515 $baseContent = $baseRev->getContent( SlotRecord::MAIN );
516 $baseContentModel = $baseContent ? $baseContent->getModel() : null;
517 }
518
519 $baseContentModel ??= $pageObj->getContentModel();
520
521 // However, allow the content models to possibly differ if we are intentionally
522 // changing them or we are doing an undo edit that is reverting content model change.
523 $contentModelsCanDiffer = $params['contentmodel'] || isset( $undoContentModel );
524
525 if ( !$contentModelsCanDiffer && $contentModel !== $baseContentModel ) {
526 $this->dieWithError( [ 'apierror-contentmodel-mismatch', $contentModel, $baseContentModel ] );
527 }
528
529 // Do the actual save
530 $oldRevId = $articleObject->getRevIdFetched();
531 $result = null;
532
533 // Fake $wgRequest for some hooks inside EditPage
534 // @todo FIXME: This interface SUCKS
535 $oldRequest = $wgRequest;
536 $wgRequest = $req;
537
538 $status = $ep->attemptSave( $result );
539 $statusValue = is_int( $status->value ) ? $status->value : 0;
540 $wgRequest = $oldRequest;
541
542 $r = [];
543 switch ( $statusValue ) {
544 case EditPage::AS_HOOK_ERROR:
545 case EditPage::AS_HOOK_ERROR_EXPECTED:
546 if ( $status->statusData !== null ) {
547 $r = $status->statusData;
548 $r['result'] = 'Failure';
549 $apiResult->addValue( null, $this->getModuleName(), $r );
550 return;
551 }
552 if ( !$status->getErrors() ) {
553 // This appears to be unreachable right now, because all
554 // code paths will set an error. Could change, though.
555 $status->fatal( 'hookaborted' ); // @codeCoverageIgnore
556 }
557 $this->dieStatus( $status );
558
559 // These two cases will normally have been caught earlier, and will
560 // only occur if something blocks the user between the earlier
561 // check and the check in EditPage (presumably a hook). It's not
562 // obvious that this is even possible.
563 // @codeCoverageIgnoreStart
564 case EditPage::AS_BLOCKED_PAGE_FOR_USER:
565 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
566 $this->dieBlocked( $user->getBlock() );
567 // dieBlocked prevents continuation
568
569 case EditPage::AS_READ_ONLY_PAGE:
570 $this->dieReadOnly();
571 // @codeCoverageIgnoreEnd
572
573 case EditPage::AS_SUCCESS_NEW_ARTICLE:
574 $r['new'] = true;
575 // fall-through
576
577 case EditPage::AS_SUCCESS_UPDATE:
578 $r['result'] = 'Success';
579 $r['pageid'] = (int)$titleObj->getArticleID();
580 $r['title'] = $titleObj->getPrefixedText();
581 $r['contentmodel'] = $articleObject->getPage()->getContentModel();
582 $newRevId = $articleObject->getPage()->getLatest();
583 if ( $newRevId == $oldRevId ) {
584 $r['nochange'] = true;
585 } else {
586 $r['oldrevid'] = (int)$oldRevId;
587 $r['newrevid'] = (int)$newRevId;
588 $r['newtimestamp'] = wfTimestamp( TS_ISO_8601,
589 $pageObj->getTimestamp() );
590 }
591
592 if ( $watch ) {
593 $r['watched'] = true;
594
595 $watchlistExpiry = $this->getWatchlistExpiry(
596 $this->watchedItemStore,
597 $titleObj,
598 $user
599 );
600
601 if ( $watchlistExpiry ) {
602 $r['watchlistexpiry'] = $watchlistExpiry;
603 }
604 }
605 $this->persistGlobalSession();
606
607 if ( isset( $result['savedTempUser'] ) ) {
608 $r['tempusercreated'] = true;
609 $params['returnto'] ??= $titleObj->getPrefixedDBkey();
610 $redirectUrl = $this->getTempUserRedirectUrl(
611 $params,
612 $result['savedTempUser']
613 );
614 if ( $redirectUrl ) {
615 $r['tempusercreatedredirect'] = $redirectUrl;
616 }
617 }
618
619 break;
620
621 default:
622 if ( !$status->getErrors() ) {
623 // EditPage sometimes only sets the status code without setting
624 // any actual error messages. Supply defaults for those cases.
625 switch ( $statusValue ) {
626 // Currently needed
627 case EditPage::AS_IMAGE_REDIRECT_ANON:
628 $status->fatal( 'apierror-noimageredirect-anon' );
629 break;
630 case EditPage::AS_IMAGE_REDIRECT_LOGGED:
631 $status->fatal( 'apierror-noimageredirect' );
632 break;
633 case EditPage::AS_CONTENT_TOO_BIG:
634 case EditPage::AS_MAX_ARTICLE_SIZE_EXCEEDED:
635 $status->fatal( 'apierror-contenttoobig',
636 $this->getConfig()->get( MainConfigNames::MaxArticleSize ) );
637 break;
638 case EditPage::AS_READ_ONLY_PAGE_ANON:
639 $status->fatal( 'apierror-noedit-anon' );
640 break;
641 case EditPage::AS_NO_CHANGE_CONTENT_MODEL:
642 $status->fatal( 'apierror-cantchangecontentmodel' );
643 break;
644 case EditPage::AS_ARTICLE_WAS_DELETED:
645 $status->fatal( 'apierror-pagedeleted' );
646 break;
647 case EditPage::AS_CONFLICT_DETECTED:
648 $status->fatal( 'edit-conflict' );
649 break;
650
651 // Currently shouldn't be needed, but here in case
652 // hooks use them without setting appropriate
653 // errors on the status.
654 // @codeCoverageIgnoreStart
655 case EditPage::AS_SPAM_ERROR:
656 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
657 $status->fatal( 'apierror-spamdetected', $result['spam'] );
658 break;
659 case EditPage::AS_READ_ONLY_PAGE_LOGGED:
660 $status->fatal( 'apierror-noedit' );
661 break;
662 case EditPage::AS_RATE_LIMITED:
663 $status->fatal( 'apierror-ratelimited' );
664 break;
665 case EditPage::AS_NO_CREATE_PERMISSION:
666 $status->fatal( 'nocreate-loggedin' );
667 break;
668 case EditPage::AS_BLANK_ARTICLE:
669 $status->fatal( 'apierror-emptypage' );
670 break;
671 case EditPage::AS_TEXTBOX_EMPTY:
672 $status->fatal( 'apierror-emptynewsection' );
673 break;
674 case EditPage::AS_SUMMARY_NEEDED:
675 $status->fatal( 'apierror-summaryrequired' );
676 break;
677 default:
678 wfWarn( __METHOD__ . ": Unknown EditPage code $statusValue with no message" );
679 $status->fatal( 'apierror-unknownerror-editpage', $statusValue );
680 break;
681 // @codeCoverageIgnoreEnd
682 }
683 }
684 $this->dieStatus( $status );
685 }
686 $apiResult->addValue( null, $this->getModuleName(), $r );
687 }
688
689 public function mustBePosted() {
690 return true;
691 }
692
693 public function isWriteMode() {
694 return true;
695 }
696
697 public function getAllowedParams() {
698 $params = [
699 'title' => [
700 ParamValidator::PARAM_TYPE => 'string',
701 ],
702 'pageid' => [
703 ParamValidator::PARAM_TYPE => 'integer',
704 ],
705 'section' => null,
706 'sectiontitle' => [
707 ParamValidator::PARAM_TYPE => 'string',
708 ],
709 'text' => [
710 ParamValidator::PARAM_TYPE => 'text',
711 ],
712 'summary' => null,
713 'tags' => [
714 ParamValidator::PARAM_TYPE => 'tags',
715 ParamValidator::PARAM_ISMULTI => true,
716 ],
717 'minor' => false,
718 'notminor' => false,
719 'bot' => false,
720 'baserevid' => [
721 ParamValidator::PARAM_TYPE => 'integer',
722 ],
723 'basetimestamp' => [
724 ParamValidator::PARAM_TYPE => 'timestamp',
725 ],
726 'starttimestamp' => [
727 ParamValidator::PARAM_TYPE => 'timestamp',
728 ],
729 'recreate' => false,
730 'createonly' => false,
731 'nocreate' => false,
732 'watch' => [
733 ParamValidator::PARAM_DEFAULT => false,
734 ParamValidator::PARAM_DEPRECATED => true,
735 ],
736 'unwatch' => [
737 ParamValidator::PARAM_DEFAULT => false,
738 ParamValidator::PARAM_DEPRECATED => true,
739 ],
740 ];
741
742 // Params appear in the docs in the order they are defined,
743 // which is why this is here and not at the bottom.
744 $params += $this->getWatchlistParams();
745
746 $params += [
747 'md5' => null,
748 'prependtext' => [
749 ParamValidator::PARAM_TYPE => 'text',
750 ],
751 'appendtext' => [
752 ParamValidator::PARAM_TYPE => 'text',
753 ],
754 'undo' => [
755 ParamValidator::PARAM_TYPE => 'integer',
756 IntegerDef::PARAM_MIN => 0,
758 ],
759 'undoafter' => [
760 ParamValidator::PARAM_TYPE => 'integer',
761 IntegerDef::PARAM_MIN => 0,
763 ],
764 'redirect' => [
765 ParamValidator::PARAM_TYPE => 'boolean',
766 ParamValidator::PARAM_DEFAULT => false,
767 ],
768 'contentformat' => [
769 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
770 ],
771 'contentmodel' => [
772 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
773 ],
774 'token' => [
775 // Standard definition automatically inserted
776 ApiBase::PARAM_HELP_MSG_APPEND => [ 'apihelp-edit-param-token' ],
777 ],
778 ];
779
780 $params += $this->getCreateTempUserParams();
781
782 return $params;
783 }
784
785 public function needsToken() {
786 return 'csrf';
787 }
788
789 protected function getExamplesMessages() {
790 return [
791 'action=edit&title=Test&summary=test%20summary&' .
792 'text=article%20content&baserevid=1234567&token=123ABC'
793 => 'apihelp-edit-example-edit',
794 'action=edit&title=Test&summary=NOTOC&minor=&' .
795 'prependtext=__NOTOC__%0A&basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
796 => 'apihelp-edit-example-prepend',
797 'action=edit&title=Test&undo=13585&undoafter=13579&' .
798 'basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
799 => 'apihelp-edit-example-undo',
800 ];
801 }
802
803 public function getHelpUrls() {
804 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Edit';
805 }
806}
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:1533
checkTitleUserPermissions(PageIdentity $pageIdentity, $actions, array $options=[])
Helper function for permission-denied errors.
Definition ApiBase.php:1672
getMain()
Get the main module.
Definition ApiBase.php:550
getErrorFormatter()
Definition ApiBase.php:682
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:1633
requireAtLeastOneParameter( $params,... $required)
Die if 0 of a certain set of parameters is set and not false.
Definition ApiBase.php:1010
getResult()
Get the result object.
Definition ApiBase.php:671
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:811
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:532
getTitleOrPageId( $params, $load=false)
Attempts to load a WikiPage object from a title or pageid parameter, if possible.
Definition ApiBase.php:1098
dieStatus(StatusValue $status)
Throw an ApiUsageException based on the Status object.
Definition ApiBase.php:1589
useTransactionalTimeLimit()
Call wfTransactionalTimeLimit() if this request was POSTed.
Definition ApiBase.php:1381
dieBlocked(Block $block)
Throw an ApiUsageException, which will (if uncaught) call the main module's error handler and die wit...
Definition ApiBase.php:1562
dieWithException(Throwable $exception, array $options=[])
Abort execution with an error derived from a throwable.
Definition ApiBase.php:1546
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.