MediaWiki master
ApiEditPage.php
Go to the documentation of this file.
1<?php
9namespace MediaWiki\Api;
10
37use Wikimedia\Timestamp\TimestampFormat as TS;
38
55class ApiEditPage extends ApiBase {
58
59 private IContentHandlerFactory $contentHandlerFactory;
60 private RevisionLookup $revisionLookup;
61 private WikiPageFactory $wikiPageFactory;
62 private RedirectLookup $redirectLookup;
63 private TempUserCreator $tempUserCreator;
64 private UserFactory $userFactory;
65
69 private function persistGlobalSession() {
70 $this->getRequest()->getSession()->persist();
71 }
72
73 public function __construct(
74 ApiMain $mainModule,
75 string $moduleName,
76 ?IContentHandlerFactory $contentHandlerFactory = null,
77 ?RevisionLookup $revisionLookup = null,
78 ?WatchedItemStoreInterface $watchedItemStore = null,
79 ?WikiPageFactory $wikiPageFactory = null,
80 ?WatchlistManager $watchlistManager = null,
81 ?UserOptionsLookup $userOptionsLookup = null,
82 ?RedirectLookup $redirectLookup = null,
83 ?TempUserCreator $tempUserCreator = null,
84 ?UserFactory $userFactory = null
85 ) {
86 parent::__construct( $mainModule, $moduleName );
87
88 // This class is extended and therefore fallback to global state - T264213
90 $this->contentHandlerFactory = $contentHandlerFactory ?? $services->getContentHandlerFactory();
91 $this->revisionLookup = $revisionLookup ?? $services->getRevisionLookup();
92 $this->watchedItemStore = $watchedItemStore ?? $services->getWatchedItemStore();
93 $this->wikiPageFactory = $wikiPageFactory ?? $services->getWikiPageFactory();
94
95 // Variables needed in ApiWatchlistTrait trait
96 $this->watchlistExpiryEnabled = $this->getConfig()->get( MainConfigNames::WatchlistExpiry );
97 $this->watchlistMaxDuration =
99 $this->watchlistManager = $watchlistManager ?? $services->getWatchlistManager();
100 $this->userOptionsLookup = $userOptionsLookup ?? $services->getUserOptionsLookup();
101 $this->redirectLookup = $redirectLookup ?? $services->getRedirectLookup();
102 $this->tempUserCreator = $tempUserCreator ?? $services->getTempUserCreator();
103 $this->userFactory = $userFactory ?? $services->getUserFactory();
104 }
105
110 private function getUserForPermissions() {
111 $user = $this->getUser();
112 if ( $this->tempUserCreator->shouldAutoCreate( $user, 'edit' ) ) {
113 return $this->userFactory->newUnsavedTempUser(
114 $this->tempUserCreator->getStashedName( $this->getRequest()->getSession() )
115 );
116 }
117 return $user;
118 }
119
120 public function execute() {
122
123 $user = $this->getUser();
124 $params = $this->extractRequestParams();
125
126 $this->requireAtLeastOneParameter( $params, 'text', 'appendtext', 'prependtext', 'undo' );
127
128 $pageObj = $this->getTitleOrPageId( $params );
129 $titleObj = $pageObj->getTitle();
130 $this->getErrorFormatter()->setContextTitle( $titleObj );
131 $apiResult = $this->getResult();
132
133 if ( $params['redirect'] ) {
134 if ( $params['prependtext'] === null
135 && $params['appendtext'] === null
136 && $params['section'] !== 'new'
137 ) {
138 $this->dieWithError( 'apierror-redirect-appendonly' );
139 }
140 if ( $titleObj->isRedirect() ) {
141 $oldTarget = $titleObj;
142 $redirTarget = $this->redirectLookup->getRedirectTarget( $oldTarget );
143 $redirTarget = Title::castFromLinkTarget( $redirTarget );
144
145 $redirValues = [
146 'from' => $titleObj->getPrefixedText(),
147 'to' => $redirTarget->getPrefixedText()
148 ];
149
150 // T239428: Check whether the new title is valid
151 if ( $redirTarget->isExternal() || !$redirTarget->canExist() ) {
152 $redirValues['to'] = $redirTarget->getFullText();
153 $this->dieWithError(
154 [
155 'apierror-edit-invalidredirect',
156 Message::plaintextParam( $oldTarget->getPrefixedText() ),
157 Message::plaintextParam( $redirTarget->getFullText() ),
158 ],
159 'edit-invalidredirect',
160 [ 'redirects' => $redirValues ]
161 );
162 }
163
164 ApiResult::setIndexedTagName( $redirValues, 'r' );
165 $apiResult->addValue( null, 'redirects', $redirValues );
166
167 // Since the page changed, update $pageObj and $titleObj
168 $pageObj = $this->wikiPageFactory->newFromTitle( $redirTarget );
169 $titleObj = $pageObj->getTitle();
170
171 $this->getErrorFormatter()->setContextTitle( $redirTarget );
172 }
173 }
174
175 if ( $params['contentmodel'] ) {
176 $contentHandler = $this->contentHandlerFactory->getContentHandler( $params['contentmodel'] );
177 } else {
178 $contentHandler = $pageObj->getContentHandler();
179 }
180 $contentModel = $contentHandler->getModelID();
181
182 $name = $titleObj->getPrefixedDBkey();
183
184 if ( $params['undo'] > 0 ) {
185 // allow undo via api
186 } elseif ( $contentHandler->supportsDirectApiEditing() === false ) {
187 $this->dieWithError( [ 'apierror-no-direct-editing', $contentModel, $name ] );
188 }
189
190 $contentFormat = $params['contentformat'] ?: $contentHandler->getDefaultFormat();
191
192 if ( !$contentHandler->isSupportedFormat( $contentFormat ) ) {
193 $this->dieWithError( [ 'apierror-badformat', $contentFormat, $contentModel, $name ] );
194 }
195
196 if ( $params['createonly'] && $titleObj->exists() ) {
197 $this->dieWithError( 'apierror-articleexists' );
198 }
199 if ( $params['nocreate'] && !$titleObj->exists() ) {
200 $this->dieWithError( 'apierror-missingtitle' );
201 }
202
203 // Now let's check whether we're even allowed to do this
205 $titleObj,
206 'edit',
207 [ 'autoblock' => true, 'user' => $this->getUserForPermissions() ]
208 );
209
210 $toMD5 = $params['text'];
211 if ( $params['appendtext'] !== null || $params['prependtext'] !== null ) {
212 $content = $pageObj->getContent();
213
214 if ( !$content ) {
215 if ( $titleObj->getNamespace() === NS_MEDIAWIKI ) {
216 # If this is a MediaWiki:x message, then load the messages
217 # and return the message value for x.
218 $text = $titleObj->getDefaultMessageText();
219 if ( $text === false ) {
220 $text = '';
221 }
222
223 try {
224 $content = ContentHandler::makeContent( $text, $titleObj );
225 } catch ( ContentSerializationException $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 'wpTextbox1' => $params['text'],
357 'format' => $contentFormat,
358 'model' => $contentModel,
359 'wpEditToken' => $params['token'],
360 'wpIgnoreBlankSummary' => true,
361 'wpIgnoreBlankArticle' => true,
362 'wpIgnoreProblematicRedirects' => true,
363 'bot' => $params['bot'],
364 'wpUnicodeCheck' => EditPage::UNICODE_CHECK,
365 ];
366
367 if ( $params['summary'] !== null ) {
368 $requestArray['wpSummary'] = $params['summary'];
369 }
370
371 if ( $params['sectiontitle'] !== null ) {
372 $requestArray['wpSectionTitle'] = $params['sectiontitle'];
373 }
374
375 if ( $params['undo'] > 0 ) {
376 $requestArray['wpUndidRevision'] = $params['undo'];
377 }
378 if ( $params['undoafter'] > 0 ) {
379 $requestArray['wpUndoAfter'] = $params['undoafter'];
380 }
381
382 // Skip for baserevid == null or '' or '0' or 0
383 if ( !empty( $params['baserevid'] ) ) {
384 $requestArray['editRevId'] = $params['baserevid'];
385 }
386
387 // Watch out for basetimestamp == '' or '0'
388 // It gets treated as NOW, almost certainly causing an edit conflict
389 if ( $params['basetimestamp'] !== null && (bool)$this->getMain()->getVal( 'basetimestamp' ) ) {
390 $requestArray['wpEdittime'] = $params['basetimestamp'];
391 } elseif ( empty( $params['baserevid'] ) ) {
392 // Only set if baserevid is not set. Otherwise, conflicts would be ignored,
393 // due to the way userWasLastToEdit() works.
394 $requestArray['wpEdittime'] = $pageObj->getTimestamp();
395 }
396
397 if ( $params['starttimestamp'] !== null ) {
398 $requestArray['wpStarttime'] = $params['starttimestamp'];
399 } else {
400 $requestArray['wpStarttime'] = wfTimestampNow(); // Fake wpStartime
401 }
402
403 if ( $params['minor'] || ( !$params['notminor'] &&
404 $this->userOptionsLookup->getOption( $user, 'minordefault' ) )
405 ) {
406 $requestArray['wpMinoredit'] = '';
407 }
408
409 if ( $params['recreate'] ) {
410 $requestArray['wpRecreate'] = '';
411 }
412
413 if ( $params['section'] !== null ) {
414 $section = $params['section'];
415 if ( !preg_match( '/^((T-)?\d+|new)$/', $section ) ) {
416 $this->dieWithError( 'apierror-invalidsection' );
417 }
418 $content = $pageObj->getContent();
419 if ( $section !== '0'
420 && $section != 'new'
421 && ( !$content || !$content->getSection( $section ) )
422 ) {
423 $this->dieWithError( [ 'apierror-nosuchsection', $section ] );
424 }
425 $requestArray['wpSection'] = $params['section'];
426 } else {
427 $requestArray['wpSection'] = '';
428 }
429
430 $watch = $this->getWatchlistValue( $params['watchlist'], $titleObj, $user );
431
432 // Deprecated parameters
433 if ( $params['watch'] ) {
434 $watch = true;
435 } elseif ( $params['unwatch'] ) {
436 $watch = false;
437 }
438
439 if ( $watch ) {
440 $requestArray['wpWatchthis'] = true;
441 $prefName = 'watchdefault-expiry';
442 if ( !$pageObj->exists() ) {
443 $prefName = 'watchcreations-expiry';
444 }
445 $watchlistExpiry = $this->getExpiryFromParams( $params, $titleObj, $user, $prefName );
446
447 if ( $watchlistExpiry ) {
448 $requestArray['wpWatchlistExpiry'] = $watchlistExpiry;
449 }
450 }
451
452 // Apply change tags
453 if ( $params['tags'] ) {
454 $tagStatus = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $this->getAuthority() );
455 if ( $tagStatus->isOK() ) {
456 $requestArray['wpChangeTags'] = implode( ',', $params['tags'] );
457 } else {
458 $this->dieStatus( $tagStatus );
459 }
460 }
461
462 // Pass through anything else we might have been given, to support extensions
463 // This is kind of a hack but it's the best we can do to make extensions work
464 $requestArray += $this->getRequest()->getValues();
465
466 // phpcs:ignore MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage,MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgTitle
467 global $wgTitle, $wgRequest;
468
469 $req = new DerivativeRequest( $this->getRequest(), $requestArray, true );
470
471 // Some functions depend on $wgTitle == $ep->getTitle()
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 $tempUserCreateStatus = $ep->maybeActivateTempUserCreate( true );
489 if ( !$tempUserCreateStatus->isOK() ) {
490 $this->dieWithError( 'apierror-tempuseracquirefailed', 'tempuseracquirefailed' );
491 }
492
493 // T255700: Ensure content models of the base content
494 // and fetched revision remain the same before attempting to save.
495 $editRevId = $requestArray['editRevId'] ?? false;
496 $baseRev = $this->revisionLookup->getRevisionByTitle( $titleObj, $editRevId );
497 $baseContentModel = null;
498
499 if ( $baseRev ) {
500 $baseContent = $baseRev->getContent( SlotRecord::MAIN );
501 $baseContentModel = $baseContent ? $baseContent->getModel() : null;
502 }
503
504 $baseContentModel ??= $pageObj->getContentModel();
505
506 // However, allow the content models to possibly differ if we are intentionally
507 // changing them or we are doing an undo edit that is reverting content model change.
508 $contentModelsCanDiffer = $params['contentmodel'] || isset( $undoContentModel );
509
510 if ( !$contentModelsCanDiffer && $contentModel !== $baseContentModel ) {
511 $this->dieWithError( [ 'apierror-contentmodel-mismatch', $contentModel, $baseContentModel ] );
512 }
513
514 // Do the actual save
515 $oldRevId = $articleObject->getRevIdFetched();
516 $result = null;
517
518 // Fake $wgRequest for some hooks inside EditPage
519 // @todo FIXME: This interface SUCKS
520 // phpcs:disable MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage
521 $oldRequest = $wgRequest;
522 $wgRequest = $req;
523
524 $status = $ep->attemptSave( $result );
525 $statusValue = is_int( $status->value ) ? $status->value : 0;
526 $wgRequest = $oldRequest;
527 // phpcs:enable
528
529 $r = [];
530 switch ( $statusValue ) {
531 case EditPage::AS_HOOK_ERROR:
532 case EditPage::AS_HOOK_ERROR_EXPECTED:
533 if ( $status->statusData !== null ) {
534 $r = $status->statusData;
535 $r['result'] = 'Failure';
536 $apiResult->addValue( null, $this->getModuleName(), $r );
537 return;
538 }
539 if ( !$status->getMessages() ) {
540 // This appears to be unreachable right now, because all
541 // code paths will set an error. Could change, though.
542 $status->fatal( 'hookaborted' ); // @codeCoverageIgnore
543 }
544 $this->dieStatus( $status );
545
546 // These two cases will normally have been caught earlier, and will
547 // only occur if something blocks the user between the earlier
548 // check and the check in EditPage (presumably a hook). It's not
549 // obvious that this is even possible.
550 // @codeCoverageIgnoreStart
551 case EditPage::AS_BLOCKED_PAGE_FOR_USER:
552 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
553 $this->dieBlocked( $user->getBlock() );
554 // dieBlocked prevents continuation
555
556 case EditPage::AS_READ_ONLY_PAGE:
557 $this->dieReadOnly();
558 // @codeCoverageIgnoreEnd
559
560 case EditPage::AS_SUCCESS_NEW_ARTICLE:
561 $r['new'] = true;
562 // fall-through
563
564 case EditPage::AS_SUCCESS_UPDATE:
565 $r['result'] = 'Success';
566 $r['pageid'] = (int)$titleObj->getArticleID();
567 $r['title'] = $titleObj->getPrefixedText();
568 $r['contentmodel'] = $articleObject->getPage()->getContentModel();
569 $newRevId = $articleObject->getPage()->getLatest();
570 if ( $newRevId == $oldRevId ) {
571 $r['nochange'] = true;
572 } else {
573 $r['oldrevid'] = (int)$oldRevId;
574 $r['newrevid'] = (int)$newRevId;
575 $r['newtimestamp'] = wfTimestamp( TS::ISO_8601,
576 $pageObj->getTimestamp() );
577 }
578
579 if ( $watch ) {
580 $r['watched'] = true;
581
582 $watchlistExpiry = $this->getWatchlistExpiry(
583 $this->watchedItemStore,
584 $titleObj,
585 $user
586 );
587
588 if ( $watchlistExpiry ) {
589 $r['watchlistexpiry'] = $watchlistExpiry;
590 }
591 }
592 $this->persistGlobalSession();
593
594 // If the temporary account was created in this request,
595 // or if the temporary account has zero edits (implying
596 // that the account was created during a failed edit
597 // attempt in a previous request), perform the top-level
598 // redirect to ensure the account is attached.
599 // Note that the temp user could already have performed
600 // the top-level redirect if this a first edit on
601 // a wiki that is not the user's home wiki.
602 $shouldRedirectForTempUser = isset( $result['savedTempUser'] ) ||
603 ( $user->isTemp() && ( $user->getEditCount() === 0 ) );
604 if ( $shouldRedirectForTempUser ) {
605 $r['tempusercreated'] = true;
606 $params['returnto'] ??= $titleObj->getPrefixedDBkey();
607 $redirectUrl = $this->getTempUserRedirectUrl(
608 $params,
609 $result['savedTempUser'] ?? $user
610 );
611 if ( $redirectUrl ) {
612 $r['tempusercreatedredirect'] = $redirectUrl;
613 }
614 }
615
616 break;
617
618 default:
619 if ( !$status->getMessages() ) {
620 // EditPage sometimes only sets the status code without setting
621 // any actual error messages. Supply defaults for those cases.
622 switch ( $statusValue ) {
623 // Currently needed
624 case EditPage::AS_IMAGE_REDIRECT_ANON:
625 $status->fatal( 'apierror-noimageredirect-anon' );
626 break;
627 case EditPage::AS_IMAGE_REDIRECT_LOGGED:
628 $status->fatal( 'apierror-noimageredirect' );
629 break;
630 case EditPage::AS_READ_ONLY_PAGE_ANON:
631 $status->fatal( 'apierror-noedit-anon' );
632 break;
633 case EditPage::AS_NO_CHANGE_CONTENT_MODEL:
634 $status->fatal( 'apierror-cantchangecontentmodel' );
635 break;
636 case EditPage::AS_CONFLICT_DETECTED:
637 $status->fatal( 'edit-conflict' );
638 break;
639
640 // Currently shouldn't be needed, but here in case
641 // hooks use them without setting appropriate
642 // errors on the status.
643 // @codeCoverageIgnoreStart
644 case EditPage::AS_SPAM_ERROR:
645 $status->fatal( 'apierror-spamdetected', $result['spam'] );
646 break;
647 case EditPage::AS_READ_ONLY_PAGE_LOGGED:
648 $status->fatal( 'apierror-noedit' );
649 break;
650 case EditPage::AS_NO_CREATE_PERMISSION:
651 $status->fatal( 'nocreate-loggedin' );
652 break;
653 case EditPage::AS_BLANK_ARTICLE:
654 $status->fatal( 'apierror-emptypage' );
655 break;
656 case EditPage::AS_TEXTBOX_EMPTY:
657 $status->fatal( 'apierror-emptynewsection' );
658 break;
659 case EditPage::AS_SUMMARY_NEEDED:
660 $status->fatal( 'apierror-summaryrequired' );
661 break;
662 default:
663 wfWarn( __METHOD__ . ": Unknown EditPage code $statusValue with no message" );
664 $status->fatal( 'apierror-unknownerror-editpage', $statusValue );
665 break;
666 // @codeCoverageIgnoreEnd
667 }
668 }
669 $this->dieStatus( $status );
670 }
671 $apiResult->addValue( null, $this->getModuleName(), $r );
672 }
673
675 public function mustBePosted() {
676 return true;
677 }
678
680 public function isWriteMode() {
681 return true;
682 }
683
685 public function getAllowedParams() {
686 $params = [
687 'title' => [
688 ParamValidator::PARAM_TYPE => 'string',
689 ],
690 'pageid' => [
691 ParamValidator::PARAM_TYPE => 'integer',
692 ],
693 'section' => null,
694 'sectiontitle' => [
695 ParamValidator::PARAM_TYPE => 'string',
696 ],
697 'text' => [
698 ParamValidator::PARAM_TYPE => 'text',
699 ],
700 'summary' => null,
701 'tags' => [
702 ParamValidator::PARAM_TYPE => 'tags',
703 ParamValidator::PARAM_ISMULTI => true,
704 ],
705 'minor' => false,
706 'notminor' => false,
707 'bot' => false,
708 'baserevid' => [
709 ParamValidator::PARAM_TYPE => 'integer',
710 ],
711 'basetimestamp' => [
712 ParamValidator::PARAM_TYPE => 'timestamp',
713 ],
714 'starttimestamp' => [
715 ParamValidator::PARAM_TYPE => 'timestamp',
716 ],
717 'recreate' => false,
718 'createonly' => false,
719 'nocreate' => false,
720 'watch' => [
721 ParamValidator::PARAM_DEFAULT => false,
722 ParamValidator::PARAM_DEPRECATED => true,
723 ],
724 'unwatch' => [
725 ParamValidator::PARAM_DEFAULT => false,
726 ParamValidator::PARAM_DEPRECATED => true,
727 ],
728 ];
729
730 // Params appear in the docs in the order they are defined,
731 // which is why this is here and not at the bottom.
732 $params += $this->getWatchlistParams();
733
734 $params += [
735 'md5' => null,
736 'prependtext' => [
737 ParamValidator::PARAM_TYPE => 'text',
738 ],
739 'appendtext' => [
740 ParamValidator::PARAM_TYPE => 'text',
741 ],
742 'undo' => [
743 ParamValidator::PARAM_TYPE => 'integer',
744 IntegerDef::PARAM_MIN => 0,
746 ],
747 'undoafter' => [
748 ParamValidator::PARAM_TYPE => 'integer',
749 IntegerDef::PARAM_MIN => 0,
751 ],
752 'redirect' => [
753 ParamValidator::PARAM_TYPE => 'boolean',
754 ParamValidator::PARAM_DEFAULT => false,
755 ],
756 'contentformat' => [
757 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
758 ],
759 'contentmodel' => [
760 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
761 ],
762 'token' => [
763 // Standard definition automatically inserted
764 ApiBase::PARAM_HELP_MSG_APPEND => [ 'apihelp-edit-param-token' ],
765 ],
766 ];
767
768 $params += $this->getCreateTempUserParams();
769
770 return $params;
771 }
772
774 public function needsToken() {
775 return 'csrf';
776 }
777
779 protected function getExamplesMessages() {
780 return [
781 'action=edit&title=Test&summary=test%20summary&' .
782 'text=article%20content&baserevid=1234567&token=123ABC'
783 => 'apihelp-edit-example-edit',
784 'action=edit&title=Test&summary=NOTOC&minor=&' .
785 'prependtext=__NOTOC__%0A&basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
786 => 'apihelp-edit-example-prepend',
787 'action=edit&title=Test&undo=13585&undoafter=13579&' .
788 'basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
789 => 'apihelp-edit-example-undo',
790 ];
791 }
792
794 public function getHelpUrls() {
795 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Edit';
796 }
797}
798
800class_alias( ApiEditPage::class, 'ApiEditPage' );
const NS_MEDIAWIKI
Definition Defines.php:59
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
wfEscapeWikiText( $input)
Escapes the given text so that it may be output using addWikiText() without any linking,...
wfTimestamp( $outputtype=TS::UNIX, $ts=0)
Get a timestamp string in one of various formats.
global $wgRequest
Definition Setup.php:432
if(MW_ENTRY_POINT==='index') if(!defined( 'MW_NO_SESSION') &&MW_ENTRY_POINT !=='cli' $wgTitle
Definition Setup.php:549
This abstract class implements many basic API functions, and is the base of all API classes.
Definition ApiBase.php:60
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1506
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:542
requireAtLeastOneParameter( $params,... $required)
Die if 0 of a certain set of parameters is set and not false.
Definition ApiBase.php:1024
getMain()
Get the main module.
Definition ApiBase.php:560
useTransactionalTimeLimit()
Call wfTransactionalTimeLimit() if this request was POSTed.
Definition ApiBase.php:1354
getResult()
Get the result object.
Definition ApiBase.php:681
const PARAM_RANGE_ENFORCE
(boolean) Inverse of IntegerDef::PARAM_IGNORE_RANGE
Definition ApiBase.php:155
const PARAM_HELP_MSG_APPEND
((string|array|Message)[]) Specify additional i18n messages to append to the normal message for this ...
Definition ApiBase.php:174
dieWithException(Throwable $exception, array $options=[])
Abort execution with an error derived from a throwable.
Definition ApiBase.php:1519
dieBlocked(Block $block)
Throw an ApiUsageException, which will (if uncaught) call the main module's error handler and die wit...
Definition ApiBase.php:1534
dieStatus(StatusValue $status)
Throw an ApiUsageException based on the Status object.
Definition ApiBase.php:1557
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:822
dieReadOnly()
Helper function for readonly errors.
Definition ApiBase.php:1599
checkTitleUserPermissions(PageIdentity $pageIdentity, $actions, array $options=[])
Helper function for permission-denied errors.
Definition ApiBase.php:1638
getTitleOrPageId( $params, $load=false)
Attempts to load a WikiPage object from a title or pageid parameter, if possible.
Definition ApiBase.php:1146
A module that allows for editing and creating pages.
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
isWriteMode()
Indicates whether this module requires write access to the wiki.API modules must override this method...
getHelpUrls()
Return links to more detailed help pages about the module.1.25, returning boolean false is deprecated...
__construct(ApiMain $mainModule, string $moduleName, ?IContentHandlerFactory $contentHandlerFactory=null, ?RevisionLookup $revisionLookup=null, ?WatchedItemStoreInterface $watchedItemStore=null, ?WikiPageFactory $wikiPageFactory=null, ?WatchlistManager $watchlistManager=null, ?UserOptionsLookup $userOptionsLookup=null, ?RedirectLookup $redirectLookup=null, ?TempUserCreator $tempUserCreator=null, ?UserFactory $userFactory=null)
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
mustBePosted()
Indicates whether this module must be called with a POST request.Implementations of this method must ...
getExamplesMessages()
Returns usage examples for this module.Return value has query strings as keys, with values being eith...
needsToken()
Returns the token type this module requires in order to execute.Modules are strongly encouraged to us...
This is the main API class, used for both external and internal processing.
Definition ApiMain.php:66
static create( $msg, $code=null, ?array $data=null)
Create an IApiMessage for the message.
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
Recent changes tagging.
Base class for content handling.
Exception representing a failure to serialize or unserialize a content object.
Content object implementation for representing flat text.
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Group all the pieces relevant to the context of a request into one instance.
The HTML user interface for page editing.
Definition EditPage.php:139
A class containing constants representing the names of configuration variables.
const WatchlistExpiry
Name constant for the WatchlistExpiry setting, for use with Config::get()
const WatchlistExpiryMaxDuration
Name constant for the WatchlistExpiryMaxDuration setting, for use with Config::get()
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:144
static plaintextParam( $plaintext)
Definition Message.php:1344
Legacy class representing an editable page and handling UI for some page actions.
Definition Article.php:65
Service for creating WikiPage objects.
Similar to MediaWiki\Request\FauxRequest, but only fakes URL parameters and method (POST or GET) and ...
Page revision base class.
Value object representing a content slot associated with a page revision.
Represents a title within MediaWiki.
Definition Title.php:69
Provides access to user options.
Service for temporary user creation.
Create User objects.
User class for the MediaWiki software.
Definition User.php:130
Service for formatting and validating API parameters.
Type definition for integer types.
trait ApiCreateTempUserTrait
Methods needed by APIs that create a temporary user.
trait ApiWatchlistTrait
An ApiWatchlistTrait adds class properties and convenience methods for APIs that allow you to watch a...
Service for resolving a wiki page redirect.
Service for looking up page revisions.
getWatchlistValue(string $watchlist, PageIdentity $page, User $user, ?string $userOption=null)
Return true if we're to watch the page, false if not.
getWatchlistParams(array $watchOptions=[])
Get additional allow params specific to watchlisting.
getExpiryFromParams(array $params, ?PageIdentity $page=null, ?UserIdentity $user=null, string $userOption='watchdefault-expiry')
Get formatted expiry from the given parameters.
getWatchlistExpiry(WatchedItemStoreInterface $store, PageIdentity $page, UserIdentity $user)
Get existing expiry from the database.