MediaWiki REL1_39
ApiEditPage.php
Go to the documentation of this file.
1<?php
35
52class ApiEditPage extends ApiBase {
54
56 private $contentHandlerFactory;
57
59 private $revisionLookup;
60
62 private $watchedItemStore;
63
65 private $wikiPageFactory;
66
68 private $userOptionsLookup;
69
71 private $redirectLookup;
72
76 private function persistGlobalSession() {
77 MediaWiki\Session\SessionManager::getGlobalSession()->persist();
78 }
79
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 ) {
102 parent::__construct( $mainModule, $moduleName );
103
104 // This class is extended and therefor fallback to global state - T264213
105 $services = MediaWikiServices::getInstance();
106 $this->contentHandlerFactory = $contentHandlerFactory ?? $services->getContentHandlerFactory();
107 $this->revisionLookup = $revisionLookup ?? $services->getRevisionLookup();
108 $this->watchedItemStore = $watchedItemStore ?? $services->getWatchedItemStore();
109 $this->wikiPageFactory = $wikiPageFactory ?? $services->getWikiPageFactory();
110
111 // Variables needed in ApiWatchlistTrait trait
112 $this->watchlistExpiryEnabled = $this->getConfig()->get( MainConfigNames::WatchlistExpiry );
113 $this->watchlistMaxDuration =
114 $this->getConfig()->get( MainConfigNames::WatchlistExpiryMaxDuration );
115 $this->watchlistManager = $watchlistManager ?? $services->getWatchlistManager();
116 $this->userOptionsLookup = $userOptionsLookup ?? $services->getUserOptionsLookup();
117 $this->redirectLookup = $redirectLookup ?? $services->getRedirectLookup();
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 ]
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 'wpIgnoreSelfRedirect' => 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 $watchlistExpiry = $this->getExpiryFromParams( $params );
444
445 if ( $watchlistExpiry ) {
446 $requestArray['wpWatchlistExpiry'] = $watchlistExpiry;
447 }
448 }
449
450 // Apply change tags
451 if ( $params['tags'] ) {
452 $tagStatus = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $this->getAuthority() );
453 if ( $tagStatus->isOK() ) {
454 $requestArray['wpChangeTags'] = implode( ',', $params['tags'] );
455 } else {
456 $this->dieStatus( $tagStatus );
457 }
458 }
459
460 // Pass through anything else we might have been given, to support extensions
461 // This is kind of a hack but it's the best we can do to make extensions work
462 $requestArray += $this->getRequest()->getValues();
463
464 global $wgTitle, $wgRequest;
465
466 $req = new DerivativeRequest( $this->getRequest(), $requestArray, true );
467
468 // Some functions depend on $wgTitle == $ep->mTitle
469 // TODO: Make them not or check if they still do
470 $wgTitle = $titleObj;
471
472 $articleContext = new RequestContext;
473 $articleContext->setRequest( $req );
474 $articleContext->setWikiPage( $pageObj );
475 $articleContext->setUser( $this->getUser() );
476
478 $articleObject = Article::newFromWikiPage( $pageObj, $articleContext );
479
480 $ep = new EditPage( $articleObject );
481
482 $ep->setApiEditOverride( true );
483 $ep->setContextTitle( $titleObj );
484 $ep->importFormData( $req );
485
486 // T255700: Ensure content models of the base content
487 // and fetched revision remain the same before attempting to save.
488 $editRevId = $requestArray['editRevId'] ?? false;
489 $baseRev = $this->revisionLookup->getRevisionByTitle( $titleObj, $editRevId );
490 $baseContentModel = null;
491
492 if ( $baseRev ) {
493 $baseContent = $baseRev->getContent( SlotRecord::MAIN );
494 $baseContentModel = $baseContent ? $baseContent->getModel() : null;
495 }
496
497 if ( $baseContentModel === null ) {
498 $baseContentModel = $pageObj->getContentModel();
499 }
500
501 // However, allow the content models to possibly differ if we are intentionally
502 // changing them or we are doing an undo edit that is reverting content model change.
503 $contentModelsCanDiffer = $params['contentmodel'] || isset( $undoContentModel );
504
505 if ( !$contentModelsCanDiffer && $contentModel !== $baseContentModel ) {
506 $this->dieWithError( [ 'apierror-contentmodel-mismatch', $contentModel, $baseContentModel ] );
507 }
508
509 // Do the actual save
510 $oldRevId = $articleObject->getRevIdFetched();
511 $result = null;
512
513 // Fake $wgRequest for some hooks inside EditPage
514 // @todo FIXME: This interface SUCKS
515 $oldRequest = $wgRequest;
516 $wgRequest = $req;
517
518 $status = $ep->attemptSave( $result );
519 $statusValue = is_int( $status->value ) ? $status->value : 0;
520 $wgRequest = $oldRequest;
521
522 $r = [];
523 switch ( $statusValue ) {
526 if ( $status->statusData !== null ) {
527 $r = $status->statusData;
528 $r['result'] = 'Failure';
529 $apiResult->addValue( null, $this->getModuleName(), $r );
530 return;
531 }
532 if ( !$status->getErrors() ) {
533 // This appears to be unreachable right now, because all
534 // code paths will set an error. Could change, though.
535 $status->fatal( 'hookaborted' ); // @codeCoverageIgnore
536 }
537 $this->dieStatus( $status );
538
539 // These two cases will normally have been caught earlier, and will
540 // only occur if something blocks the user between the earlier
541 // check and the check in EditPage (presumably a hook). It's not
542 // obvious that this is even possible.
543 // @codeCoverageIgnoreStart
545 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
546 $this->dieBlocked( $user->getBlock() );
547 // dieBlocked prevents continuation
548
550 $this->dieReadOnly();
551 // @codeCoverageIgnoreEnd
552
554 $r['new'] = true;
555 // fall-through
556
558 $r['result'] = 'Success';
559 $r['pageid'] = (int)$titleObj->getArticleID();
560 $r['title'] = $titleObj->getPrefixedText();
561 $r['contentmodel'] = $articleObject->getPage()->getContentModel();
562 $newRevId = $articleObject->getPage()->getLatest();
563 if ( $newRevId == $oldRevId ) {
564 $r['nochange'] = true;
565 } else {
566 $r['oldrevid'] = (int)$oldRevId;
567 $r['newrevid'] = (int)$newRevId;
568 $r['newtimestamp'] = wfTimestamp( TS_ISO_8601,
569 $pageObj->getTimestamp() );
570 }
571
572 if ( $watch ) {
573 $r['watched'] = true;
574
575 $watchlistExpiry = $this->getWatchlistExpiry(
576 $this->watchedItemStore,
577 $titleObj,
578 $user
579 );
580
581 if ( $watchlistExpiry ) {
582 $r['watchlistexpiry'] = $watchlistExpiry;
583 }
584 }
585 $this->persistGlobalSession();
586 break;
587
588 default:
589 if ( !$status->getErrors() ) {
590 // EditPage sometimes only sets the status code without setting
591 // any actual error messages. Supply defaults for those cases.
592 switch ( $statusValue ) {
593 // Currently needed
595 $status->fatal( 'apierror-noimageredirect-anon' );
596 break;
598 $status->fatal( 'apierror-noimageredirect' );
599 break;
602 $status->fatal( 'apierror-contenttoobig',
603 $this->getConfig()->get( MainConfigNames::MaxArticleSize ) );
604 break;
606 $status->fatal( 'apierror-noedit-anon' );
607 break;
609 $status->fatal( 'apierror-cantchangecontentmodel' );
610 break;
612 $status->fatal( 'apierror-pagedeleted' );
613 break;
615 $status->fatal( 'edit-conflict' );
616 break;
617
618 // Currently shouldn't be needed, but here in case
619 // hooks use them without setting appropriate
620 // errors on the status.
621 // @codeCoverageIgnoreStart
623 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
624 $status->fatal( 'apierror-spamdetected', $result['spam'] );
625 break;
627 $status->fatal( 'apierror-noedit' );
628 break;
630 $status->fatal( 'apierror-ratelimited' );
631 break;
633 $status->fatal( 'nocreate-loggedin' );
634 break;
636 $status->fatal( 'apierror-emptypage' );
637 break;
639 $status->fatal( 'apierror-emptynewsection' );
640 break;
642 $status->fatal( 'apierror-summaryrequired' );
643 break;
644 default:
645 wfWarn( __METHOD__ . ": Unknown EditPage code $statusValue with no message" );
646 $status->fatal( 'apierror-unknownerror-editpage', $statusValue );
647 break;
648 // @codeCoverageIgnoreEnd
649 }
650 }
651 $this->dieStatus( $status );
652 }
653 $apiResult->addValue( null, $this->getModuleName(), $r );
654 }
655
656 public function mustBePosted() {
657 return true;
658 }
659
660 public function isWriteMode() {
661 return true;
662 }
663
664 public function getAllowedParams() {
665 $params = [
666 'title' => [
667 ParamValidator::PARAM_TYPE => 'string',
668 ],
669 'pageid' => [
670 ParamValidator::PARAM_TYPE => 'integer',
671 ],
672 'section' => null,
673 'sectiontitle' => [
674 ParamValidator::PARAM_TYPE => 'string',
675 ],
676 'text' => [
677 ParamValidator::PARAM_TYPE => 'text',
678 ],
679 'summary' => null,
680 'tags' => [
681 ParamValidator::PARAM_TYPE => 'tags',
682 ParamValidator::PARAM_ISMULTI => true,
683 ],
684 'minor' => false,
685 'notminor' => false,
686 'bot' => false,
687 'baserevid' => [
688 ParamValidator::PARAM_TYPE => 'integer',
689 ],
690 'basetimestamp' => [
691 ParamValidator::PARAM_TYPE => 'timestamp',
692 ],
693 'starttimestamp' => [
694 ParamValidator::PARAM_TYPE => 'timestamp',
695 ],
696 'recreate' => false,
697 'createonly' => false,
698 'nocreate' => false,
699 'watch' => [
700 ParamValidator::PARAM_DEFAULT => false,
701 ParamValidator::PARAM_DEPRECATED => true,
702 ],
703 'unwatch' => [
704 ParamValidator::PARAM_DEFAULT => false,
705 ParamValidator::PARAM_DEPRECATED => true,
706 ],
707 ];
708
709 // Params appear in the docs in the order they are defined,
710 // which is why this is here and not at the bottom.
711 $params += $this->getWatchlistParams();
712
713 return $params + [
714 'md5' => null,
715 'prependtext' => [
716 ParamValidator::PARAM_TYPE => 'text',
717 ],
718 'appendtext' => [
719 ParamValidator::PARAM_TYPE => 'text',
720 ],
721 'undo' => [
722 ParamValidator::PARAM_TYPE => 'integer',
723 IntegerDef::PARAM_MIN => 0,
725 ],
726 'undoafter' => [
727 ParamValidator::PARAM_TYPE => 'integer',
728 IntegerDef::PARAM_MIN => 0,
730 ],
731 'redirect' => [
732 ParamValidator::PARAM_TYPE => 'boolean',
733 ParamValidator::PARAM_DEFAULT => false,
734 ],
735 'contentformat' => [
736 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
737 ],
738 'contentmodel' => [
739 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
740 ],
741 'token' => [
742 // Standard definition automatically inserted
743 ApiBase::PARAM_HELP_MSG_APPEND => [ 'apihelp-edit-param-token' ],
744 ],
745 ];
746 }
747
748 public function needsToken() {
749 return 'csrf';
750 }
751
752 protected function getExamplesMessages() {
753 return [
754 'action=edit&title=Test&summary=test%20summary&' .
755 'text=article%20content&baserevid=1234567&token=123ABC'
756 => 'apihelp-edit-example-edit',
757 'action=edit&title=Test&summary=NOTOC&minor=&' .
758 'prependtext=__NOTOC__%0A&basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
759 => 'apihelp-edit-example-prepend',
760 'action=edit&title=Test&undo=13585&undoafter=13579&' .
761 'basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
762 => 'apihelp-edit-example-undo',
763 ];
764 }
765
766 public function getHelpUrls() {
767 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Edit';
768 }
769}
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:377
if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode $wgTitle
Definition Setup.php:497
This abstract class implements many basic API functions, and is the base of all API classes.
Definition ApiBase.php:56
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1454
getMain()
Get the main module.
Definition ApiBase.php:514
getErrorFormatter()
Definition ApiBase.php:640
const PARAM_HELP_MSG_APPEND
((string|array|Message)[]) Specify additional i18n messages to append to the normal message for this ...
Definition ApiBase.php:170
dieReadOnly()
Helper function for readonly errors.
Definition ApiBase.php:1544
requireAtLeastOneParameter( $params,... $required)
Die if none of a certain set of parameters is set and not false.
Definition ApiBase.php:963
getResult()
Get the result object.
Definition ApiBase.php:629
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:765
const PARAM_RANGE_ENFORCE
(boolean) Inverse of IntegerDef::PARAM_IGNORE_RANGE
Definition ApiBase.php:153
checkTitleUserPermissions( $pageIdentity, $actions, array $options=[])
Helper function for permission-denied errors.
Definition ApiBase.php:1586
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:498
getTitleOrPageId( $params, $load=false)
Get a WikiPage object from a title or pageid param, if possible.
Definition ApiBase.php:1036
dieStatus(StatusValue $status)
Throw an ApiUsageException based on the Status object.
Definition ApiBase.php:1515
useTransactionalTimeLimit()
Call wfTransactionalTimeLimit() if this request was POSTed.
Definition ApiBase.php:1299
dieBlocked(Block $block)
Throw an ApiUsageException, which will (if uncaught) call the main module's error handler and die wit...
Definition ApiBase.php:1483
dieWithException(Throwable $exception, array $options=[])
Abort execution with an error derived from a throwable.
Definition ApiBase.php:1467
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:52
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()
Similar to FauxRequest, but only fakes URL parameters and method (POST or GET) and use the base reque...
The edit page/HTML interface (split from Article) The actual database and text munging is still in Ar...
Definition EditPage.php:103
const UNICODE_CHECK
Used for Unicode support checks.
Definition EditPage.php:110
Exception representing a failure to serialize or unserialize a content object.
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Service for creating WikiPage objects.
Page revision base class.
Value object representing a content slot associated with a page revision.
Provides access to user options.
static plaintextParam( $plaintext)
Definition Message.php:1266
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...
const AS_RATE_LIMITED
Status: rate limiter for action 'edit' was tripped.
const AS_NO_CHANGE_CONTENT_MODEL
Status: user tried to modify the content model, but is not allowed to do that ( User::isAllowed('edit...
const AS_READ_ONLY_PAGE_LOGGED
Status: this logged in user is not allowed to edit this page.
const AS_READ_ONLY_PAGE_ANON
Status: this anonymous user is not allowed to edit this page.
const AS_CONFLICT_DETECTED
Status: (non-resolvable) edit conflict.
const AS_ARTICLE_WAS_DELETED
Status: article was deleted while editing and wpRecreate == false or form was not posted.
const AS_TEXTBOX_EMPTY
Status: user tried to create a new section without content.
const AS_HOOK_ERROR_EXPECTED
Status: A hook function returned an error.
const AS_CONTENT_TOO_BIG
Status: Content too big (> $wgMaxArticleSize)
const AS_SPAM_ERROR
Status: summary contained spam according to one of the regexes in $wgSummarySpamRegex.
const AS_SUCCESS_UPDATE
Status: Article successfully updated.
const AS_IMAGE_REDIRECT_ANON
Status: anonymous user is not allowed to upload (User::isAllowed('upload') == false)
const AS_SUCCESS_NEW_ARTICLE
Status: Article successfully created.
const AS_NO_CREATE_PERMISSION
Status: user tried to create this page, but is not allowed to do that.
const AS_IMAGE_REDIRECT_LOGGED
Status: logged in user is not allowed to upload (User::isAllowed('upload') == false)
const AS_MAX_ARTICLE_SIZE_EXCEEDED
Status: article is too big (> $wgMaxArticleSize), after merging in the new section.
const AS_BLOCKED_PAGE_FOR_USER
Status: User is blocked from editing this page.
const AS_SUMMARY_NEEDED
Status: no edit summary given and the user has forceeditsummary set and the user is not editing in hi...
const AS_HOOK_ERROR
Status: Article update aborted by a hook function.
const AS_BLANK_ARTICLE
Status: user tried to create a blank page and wpIgnoreBlankArticle == false.
const AS_READ_ONLY_PAGE
Status: wiki is in readonly mode (ReadOnlyMode::isReadOnly() == true)
Service for resolving a wiki page redirect.
Service for looking up page revisions.
$content
Definition router.php:76