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