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 private ShadowPageLoader $shadowPageLoader;
66
70 private function persistGlobalSession() {
71 $this->getRequest()->getSession()->persist();
72 }
73
74 public function __construct(
75 ApiMain $mainModule,
76 string $moduleName,
77 ?IContentHandlerFactory $contentHandlerFactory = null,
78 ?RevisionLookup $revisionLookup = null,
79 ?WatchedItemStoreInterface $watchedItemStore = null,
80 ?WikiPageFactory $wikiPageFactory = null,
81 ?WatchlistManager $watchlistManager = null,
82 ?UserOptionsLookup $userOptionsLookup = null,
83 ?RedirectLookup $redirectLookup = null,
84 ?TempUserCreator $tempUserCreator = null,
85 ?UserFactory $userFactory = null,
86 ?ShadowPageLoader $shadowPageLoader = null,
87 ) {
88 parent::__construct( $mainModule, $moduleName );
89
90 // This class is extended and therefore fallback to global state - T264213
92 $this->contentHandlerFactory = $contentHandlerFactory ?? $services->getContentHandlerFactory();
93 $this->revisionLookup = $revisionLookup ?? $services->getRevisionLookup();
94 $this->watchedItemStore = $watchedItemStore ?? $services->getWatchedItemStore();
95 $this->wikiPageFactory = $wikiPageFactory ?? $services->getWikiPageFactory();
96
97 // Variables needed in ApiWatchlistTrait trait
98 $this->watchlistExpiryEnabled = $this->getConfig()->get( MainConfigNames::WatchlistExpiry );
99 $this->watchlistMaxDuration =
101 $this->watchlistManager = $watchlistManager ?? $services->getWatchlistManager();
102 $this->userOptionsLookup = $userOptionsLookup ?? $services->getUserOptionsLookup();
103 $this->redirectLookup = $redirectLookup ?? $services->getRedirectLookup();
104 $this->tempUserCreator = $tempUserCreator ?? $services->getTempUserCreator();
105 $this->userFactory = $userFactory ?? $services->getUserFactory();
106 $this->shadowPageLoader = $shadowPageLoader ?? $services->getShadowPageLoader();
107 }
108
113 private function getUserForPermissions() {
114 $user = $this->getUser();
115 if ( $this->tempUserCreator->shouldAutoCreate( $user, 'edit' ) ) {
116 return $this->userFactory->newUnsavedTempUser(
117 $this->tempUserCreator->getStashedName( $this->getRequest()->getSession() )
118 );
119 }
120 return $user;
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, 'user' => $this->getUserForPermissions() ]
211 );
212
213 $toMD5 = $params['text'];
214 if ( $params['appendtext'] !== null || $params['prependtext'] !== null ) {
215 try {
216 $content = $pageObj->getContent()
217 ?? $this->shadowPageLoader->get( $titleObj )?->getPreloadContent()
218 ?? $contentHandler->makeEmptyContent();
219 } catch ( ContentSerializationException $ex ) {
220 $this->dieWithException( $ex, [
221 'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' )
222 ] );
223 }
224
225 // @todo Add support for appending/prepending to the Content interface
226
227 if ( !( $content instanceof TextContent ) ) {
228 $this->dieWithError( [ 'apierror-appendnotsupported', $contentModel ] );
229 }
230
231 if ( $params['section'] !== null ) {
232 if ( !$contentHandler->supportsSections() ) {
233 $this->dieWithError( [ 'apierror-sectionsnotsupported', $contentModel ] );
234 }
235
236 if ( $params['section'] == 'new' ) {
237 // DWIM if they're trying to prepend/append to a new section.
238 $content = null;
239 } else {
240 // Process the content for section edits
241 $section = $params['section'];
242 $content = $content->getSection( $section );
243
244 if ( !$content ) {
245 $this->dieWithError( [ 'apierror-nosuchsection', wfEscapeWikiText( $section ) ] );
246 }
247 }
248 }
249
250 if ( !$content ) {
251 $text = '';
252 } else {
253 $text = $content->serialize( $contentFormat );
254 }
255
256 $params['text'] = $params['prependtext'] . $text . $params['appendtext'];
257 $toMD5 = $params['prependtext'] . $params['appendtext'];
258 }
259
260 if ( $params['undo'] > 0 ) {
261 $undoRev = $this->revisionLookup->getRevisionById( $params['undo'] );
262 if ( $undoRev === null || $undoRev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
263 $this->dieWithError( [ 'apierror-nosuchrevid', $params['undo'] ] );
264 }
265
266 if ( $params['undoafter'] > 0 ) {
267 $undoafterRev = $this->revisionLookup->getRevisionById( $params['undoafter'] );
268 } else {
269 // undoafter=0 or null
270 $undoafterRev = $this->revisionLookup->getPreviousRevision( $undoRev );
271 }
272 if ( $undoafterRev === null || $undoafterRev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
273 $this->dieWithError( [ 'apierror-nosuchrevid', $params['undoafter'] ] );
274 }
275
276 if ( $undoRev->getPageId() != $pageObj->getId() ) {
277 $this->dieWithError( [ 'apierror-revwrongpage', $undoRev->getId(),
278 $titleObj->getPrefixedText() ] );
279 }
280 if ( $undoafterRev->getPageId() != $pageObj->getId() ) {
281 $this->dieWithError( [ 'apierror-revwrongpage', $undoafterRev->getId(),
282 $titleObj->getPrefixedText() ] );
283 }
284
285 $newContent = $contentHandler->getUndoContent(
286 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Content is for public use here
287 $pageObj->getRevisionRecord()->getContent( SlotRecord::MAIN ),
288 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Content is for public use here
289 $undoRev->getContent( SlotRecord::MAIN ),
290 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Content is for public use here
291 $undoafterRev->getContent( SlotRecord::MAIN ),
292 $pageObj->getRevisionRecord()->getId() === $undoRev->getId()
293 );
294
295 if ( !$newContent ) {
296 $this->dieWithError( 'undo-failure', 'undofailure' );
297 }
298 if ( !$params['contentmodel'] && !$params['contentformat'] ) {
299 // If we are reverting content model, the new content model
300 // might not support the current serialization format, in
301 // which case go back to the old serialization format,
302 // but only if the user hasn't specified a format/model
303 // parameter.
304 if ( !$newContent->isSupportedFormat( $contentFormat ) ) {
305 $undoafterRevMainSlot = $undoafterRev->getSlot(
306 SlotRecord::MAIN,
307 RevisionRecord::RAW
308 );
309 $contentFormat = $undoafterRevMainSlot->getFormat();
310 if ( !$contentFormat ) {
311 // fall back to default content format for the model
312 // of $undoafterRev
313 $contentFormat = $this->contentHandlerFactory
314 ->getContentHandler( $undoafterRevMainSlot->getModel() )
315 ->getDefaultFormat();
316 }
317 }
318 // Override content model with model of undid revision.
319 $contentModel = $newContent->getModel();
320 $undoContentModel = true;
321 }
322 $params['text'] = $newContent->serialize( $contentFormat );
323 // If no summary was given and we only undid one rev,
324 // use an autosummary
325
326 if ( $params['summary'] === null ) {
327 $nextRev = $this->revisionLookup->getNextRevision( $undoafterRev );
328 if ( $nextRev && $nextRev->getId() == $params['undo'] ) {
329 $undoRevUser = $undoRev->getUser();
330 $params['summary'] = $this->msg( 'undo-summary' )
331 ->params( $params['undo'], $undoRevUser ? $undoRevUser->getName() : '' )
332 ->inContentLanguage()->text();
333 }
334 }
335 }
336
337 // See if the MD5 hash checks out
338 if ( $params['md5'] !== null && md5( $toMD5 ) !== $params['md5'] ) {
339 $this->dieWithError( 'apierror-badmd5' );
340 }
341
342 // EditPage wants to parse its stuff from a WebRequest
343 // That interface kind of sucks, but it's workable
344 $requestArray = [
345 'wpTextbox1' => $params['text'],
346 'format' => $contentFormat,
347 'model' => $contentModel,
348 'wpEditToken' => $params['token'],
349 'wpIgnoreBlankSummary' => true,
350 'wpIgnoreBlankArticle' => true,
351 'wpIgnoreProblematicRedirects' => true,
352 'bot' => $params['bot'],
353 'wpUnicodeCheck' => EditPage::UNICODE_CHECK,
354 ];
355
356 if ( $params['summary'] !== null ) {
357 $requestArray['wpSummary'] = $params['summary'];
358 }
359
360 if ( $params['sectiontitle'] !== null ) {
361 $requestArray['wpSectionTitle'] = $params['sectiontitle'];
362 }
363
364 if ( $params['undo'] > 0 ) {
365 $requestArray['wpUndidRevision'] = $params['undo'];
366 }
367 if ( $params['undoafter'] > 0 ) {
368 $requestArray['wpUndoAfter'] = $params['undoafter'];
369 }
370
371 // Skip for baserevid == null or '' or '0' or 0
372 if ( !empty( $params['baserevid'] ) ) {
373 $requestArray['editRevId'] = $params['baserevid'];
374 }
375
376 // Watch out for basetimestamp == '' or '0'
377 // It gets treated as NOW, almost certainly causing an edit conflict
378 if ( $params['basetimestamp'] !== null && (bool)$this->getMain()->getVal( 'basetimestamp' ) ) {
379 $requestArray['wpEdittime'] = $params['basetimestamp'];
380 } elseif ( empty( $params['baserevid'] ) ) {
381 // Only set if baserevid is not set. Otherwise, conflicts would be ignored,
382 // due to the way userWasLastToEdit() works.
383 $requestArray['wpEdittime'] = $pageObj->getTimestamp();
384 }
385
386 if ( $params['starttimestamp'] !== null ) {
387 $requestArray['wpStarttime'] = $params['starttimestamp'];
388 } else {
389 $requestArray['wpStarttime'] = wfTimestampNow(); // Fake wpStartime
390 }
391
392 if ( $params['minor'] || ( !$params['notminor'] &&
393 $this->userOptionsLookup->getOption( $user, 'minordefault' ) )
394 ) {
395 $requestArray['wpMinoredit'] = '';
396 }
397
398 if ( $params['recreate'] ) {
399 $requestArray['wpRecreate'] = '';
400 }
401
402 if ( $params['section'] !== null ) {
403 $section = $params['section'];
404 if ( !preg_match( '/^((T-)?\d+|new)$/', $section ) ) {
405 $this->dieWithError( 'apierror-invalidsection' );
406 }
407 $content = $pageObj->getContent();
408 if ( $section !== '0'
409 && $section != 'new'
410 && ( !$content || !$content->getSection( $section ) )
411 ) {
412 $this->dieWithError( [ 'apierror-nosuchsection', $section ] );
413 }
414 $requestArray['wpSection'] = $params['section'];
415 } else {
416 $requestArray['wpSection'] = '';
417 }
418
419 $watch = $this->getWatchlistValue( $params['watchlist'], $titleObj, $user );
420
421 // Deprecated parameters
422 if ( $params['watch'] ) {
423 $watch = true;
424 } elseif ( $params['unwatch'] ) {
425 $watch = false;
426 }
427
428 if ( $watch ) {
429 $requestArray['wpWatchthis'] = true;
430 $prefName = 'watchdefault-expiry';
431 if ( !$pageObj->exists() ) {
432 $prefName = 'watchcreations-expiry';
433 }
434 $watchlistExpiry = $this->getExpiryFromParams( $params, $titleObj, $user, $prefName );
435
436 if ( $watchlistExpiry ) {
437 $requestArray['wpWatchlistExpiry'] = $watchlistExpiry;
438 }
439 }
440
441 // Apply change tags
442 if ( $params['tags'] ) {
443 $tagStatus = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $this->getAuthority() );
444 if ( $tagStatus->isOK() ) {
445 $requestArray['wpChangeTags'] = implode( ',', $params['tags'] );
446 } else {
447 $this->dieStatus( $tagStatus );
448 }
449 }
450
451 // Pass through anything else we might have been given, to support extensions
452 // This is kind of a hack but it's the best we can do to make extensions work
453 $requestArray += $this->getRequest()->getValues();
454
455 // phpcs:ignore MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage,MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgTitle
456 global $wgTitle, $wgRequest;
457
458 $req = new DerivativeRequest( $this->getRequest(), $requestArray, true );
459
460 // Some functions depend on $wgTitle == $ep->getTitle()
461 // TODO: Make them not or check if they still do
462 $wgTitle = $titleObj;
463
464 $articleContext = new RequestContext;
465 $articleContext->setRequest( $req );
466 $articleContext->setWikiPage( $pageObj );
467 $articleContext->setUser( $this->getUser() );
468
470 $articleObject = Article::newFromWikiPage( $pageObj, $articleContext );
471
472 $ep = new EditPage( $articleObject );
473
474 $ep->setApiEditOverride( true );
475 $ep->setContextTitle( $titleObj );
476 $ep->importFormData();
477 $tempUserCreateStatus = $ep->maybeActivateTempUserCreate( true );
478 if ( !$tempUserCreateStatus->isOK() ) {
479 $this->dieWithError( 'apierror-tempuseracquirefailed', 'tempuseracquirefailed' );
480 }
481
482 // T255700: Ensure content models of the base content
483 // and fetched revision remain the same before attempting to save.
484 $editRevId = $requestArray['editRevId'] ?? false;
485 $baseRev = $this->revisionLookup->getRevisionByTitle( $titleObj, $editRevId );
486 $baseContentModel = null;
487
488 if ( $baseRev ) {
489 $baseContent = $baseRev->getContent( SlotRecord::MAIN );
490 $baseContentModel = $baseContent ? $baseContent->getModel() : null;
491 }
492
493 $baseContentModel ??= $pageObj->getContentModel();
494
495 // However, allow the content models to possibly differ if we are intentionally
496 // changing them or we are doing an undo edit that is reverting content model change.
497 $contentModelsCanDiffer = $params['contentmodel'] || isset( $undoContentModel );
498
499 if ( !$contentModelsCanDiffer && $contentModel !== $baseContentModel ) {
500 $this->dieWithError( [ 'apierror-contentmodel-mismatch', $contentModel, $baseContentModel ] );
501 }
502
503 // Do the actual save
504 $oldRevId = $articleObject->getRevIdFetched();
505 $result = null;
506
507 // Fake $wgRequest for some hooks inside EditPage
508 // @todo FIXME: This interface SUCKS
509 // phpcs:disable MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage
510 $oldRequest = $wgRequest;
511 $wgRequest = $req;
512
513 $status = $ep->attemptSave( $result );
514 $statusValue = is_int( $status->value ) ? $status->value : 0;
515 $wgRequest = $oldRequest;
516 // phpcs:enable
517
518 $r = [];
519 switch ( $statusValue ) {
520 case EditPage::AS_HOOK_ERROR:
521 case EditPage::AS_HOOK_ERROR_EXPECTED:
522 if ( $status->statusData !== null ) {
523 $r = $status->statusData;
524 $r['result'] = 'Failure';
525 $apiResult->addValue( null, $this->getModuleName(), $r );
526 return;
527 }
528 if ( !$status->getMessages() ) {
529 // This appears to be unreachable right now, because all
530 // code paths will set an error. Could change, though.
531 $status->fatal( 'hookaborted' ); // @codeCoverageIgnore
532 }
533 $this->dieStatus( $status );
534
535 // These two cases will normally have been caught earlier, and will
536 // only occur if something blocks the user between the earlier
537 // check and the check in EditPage (presumably a hook). It's not
538 // obvious that this is even possible.
539 // @codeCoverageIgnoreStart
540 case EditPage::AS_BLOCKED_PAGE_FOR_USER:
541 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
542 $this->dieBlocked( $user->getBlock() );
543 // dieBlocked prevents continuation
544
545 case EditPage::AS_READ_ONLY_PAGE:
546 $this->dieReadOnly();
547 // @codeCoverageIgnoreEnd
548
549 case EditPage::AS_SUCCESS_NEW_ARTICLE:
550 $r['new'] = true;
551 // fall-through
552
553 case EditPage::AS_SUCCESS_UPDATE:
554 $r['result'] = 'Success';
555 $r['pageid'] = (int)$titleObj->getArticleID();
556 $r['title'] = $titleObj->getPrefixedText();
557 $r['contentmodel'] = $articleObject->getPage()->getContentModel();
558 $newRevId = $articleObject->getPage()->getLatest();
559 if ( $newRevId == $oldRevId ) {
560 $r['nochange'] = true;
561 } else {
562 $r['oldrevid'] = (int)$oldRevId;
563 $r['newrevid'] = (int)$newRevId;
564 $r['newtimestamp'] = wfTimestamp( TS::ISO_8601,
565 $pageObj->getTimestamp() );
566 }
567
568 if ( $watch ) {
569 $r['watched'] = true;
570
571 $watchlistExpiry = $this->getWatchlistExpiry(
572 $this->watchedItemStore,
573 $titleObj,
574 $user
575 );
576
577 if ( $watchlistExpiry ) {
578 $r['watchlistexpiry'] = $watchlistExpiry;
579 }
580 }
581 $this->persistGlobalSession();
582
583 // If the temporary account was created in this request,
584 // or if the temporary account has zero edits (implying
585 // that the account was created during a failed edit
586 // attempt in a previous request), perform the top-level
587 // redirect to ensure the account is attached.
588 // Note that the temp user could already have performed
589 // the top-level redirect if this a first edit on
590 // a wiki that is not the user's home wiki.
591 $shouldRedirectForTempUser = isset( $result['savedTempUser'] ) ||
592 ( $user->isTemp() && ( $user->getEditCount() === 0 ) );
593 if ( $shouldRedirectForTempUser ) {
594 $r['tempusercreated'] = true;
595 $params['returnto'] ??= $titleObj->getPrefixedDBkey();
596 $redirectUrl = $this->getTempUserRedirectUrl(
597 $params,
598 $result['savedTempUser'] ?? $user
599 );
600 if ( $redirectUrl ) {
601 $r['tempusercreatedredirect'] = $redirectUrl;
602 }
603 }
604
605 break;
606
607 default:
608 if ( !$status->getMessages() ) {
609 // EditPage sometimes only sets the status code without setting
610 // any actual error messages. Supply defaults for those cases.
611 switch ( $statusValue ) {
612 // Currently needed
613 case EditPage::AS_IMAGE_REDIRECT_ANON:
614 $status->fatal( 'apierror-noimageredirect-anon' );
615 break;
616 case EditPage::AS_IMAGE_REDIRECT_LOGGED:
617 $status->fatal( 'apierror-noimageredirect' );
618 break;
619 case EditPage::AS_READ_ONLY_PAGE_ANON:
620 $status->fatal( 'apierror-noedit-anon' );
621 break;
622 case EditPage::AS_NO_CHANGE_CONTENT_MODEL:
623 $status->fatal( 'apierror-cantchangecontentmodel' );
624 break;
625 case EditPage::AS_CONFLICT_DETECTED:
626 $status->fatal( 'edit-conflict' );
627 break;
628
629 // Currently shouldn't be needed, but here in case
630 // hooks use them without setting appropriate
631 // errors on the status.
632 // @codeCoverageIgnoreStart
633 case EditPage::AS_SPAM_ERROR:
634 $status->fatal( 'apierror-spamdetected', $result['spam'] ?? '' );
635 break;
636 case EditPage::AS_READ_ONLY_PAGE_LOGGED:
637 $status->fatal( 'apierror-noedit' );
638 break;
639 case EditPage::AS_NO_CREATE_PERMISSION:
640 $status->fatal( 'nocreate-loggedin' );
641 break;
642 case EditPage::AS_BLANK_ARTICLE:
643 $status->fatal( 'apierror-emptypage' );
644 break;
645 case EditPage::AS_TEXTBOX_EMPTY:
646 $status->fatal( 'apierror-emptynewsection' );
647 break;
648 case EditPage::AS_SUMMARY_NEEDED:
649 $status->fatal( 'apierror-summaryrequired' );
650 break;
651 default:
652 wfWarn( __METHOD__ . ": Unknown EditPage code $statusValue with no message" );
653 $status->fatal( 'apierror-unknownerror-editpage', $statusValue );
654 break;
655 // @codeCoverageIgnoreEnd
656 }
657 }
658 $this->dieStatus( $status );
659 }
660 $apiResult->addValue( null, $this->getModuleName(), $r );
661 }
662
664 public function mustBePosted() {
665 return true;
666 }
667
669 public function isWriteMode() {
670 return true;
671 }
672
674 public function getAllowedParams() {
675 $params = [
676 'title' => [
677 ParamValidator::PARAM_TYPE => 'string',
678 ],
679 'pageid' => [
680 ParamValidator::PARAM_TYPE => 'integer',
681 ],
682 'section' => null,
683 'sectiontitle' => [
684 ParamValidator::PARAM_TYPE => 'string',
685 ],
686 'text' => [
687 ParamValidator::PARAM_TYPE => 'text',
688 ],
689 'summary' => null,
690 'tags' => [
691 ParamValidator::PARAM_TYPE => 'tags',
692 ParamValidator::PARAM_ISMULTI => true,
693 ],
694 'minor' => false,
695 'notminor' => false,
696 'bot' => false,
697 'baserevid' => [
698 ParamValidator::PARAM_TYPE => 'integer',
699 ],
700 'basetimestamp' => [
701 ParamValidator::PARAM_TYPE => 'timestamp',
702 ],
703 'starttimestamp' => [
704 ParamValidator::PARAM_TYPE => 'timestamp',
705 ],
706 'recreate' => false,
707 'createonly' => false,
708 'nocreate' => false,
709 'watch' => [
710 ParamValidator::PARAM_DEFAULT => false,
711 ParamValidator::PARAM_DEPRECATED => true,
712 ],
713 'unwatch' => [
714 ParamValidator::PARAM_DEFAULT => false,
715 ParamValidator::PARAM_DEPRECATED => true,
716 ],
717 ];
718
719 // Params appear in the docs in the order they are defined,
720 // which is why this is here and not at the bottom.
721 $params += $this->getWatchlistParams();
722
723 $params += [
724 'md5' => null,
725 'prependtext' => [
726 ParamValidator::PARAM_TYPE => 'text',
727 ],
728 'appendtext' => [
729 ParamValidator::PARAM_TYPE => 'text',
730 ],
731 'undo' => [
732 ParamValidator::PARAM_TYPE => 'integer',
733 IntegerDef::PARAM_MIN => 0,
735 ],
736 'undoafter' => [
737 ParamValidator::PARAM_TYPE => 'integer',
738 IntegerDef::PARAM_MIN => 0,
740 ],
741 'redirect' => [
742 ParamValidator::PARAM_TYPE => 'boolean',
743 ParamValidator::PARAM_DEFAULT => false,
744 ],
745 'contentformat' => [
746 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
747 ],
748 'contentmodel' => [
749 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
750 ],
751 'token' => [
752 // Standard definition automatically inserted
753 ApiBase::PARAM_HELP_MSG_APPEND => [ 'apihelp-edit-param-token' ],
754 ],
755 ];
756
757 $params += $this->getCreateTempUserParams();
758
759 return $params;
760 }
761
763 public function needsToken() {
764 return 'csrf';
765 }
766
768 protected function getExamplesMessages() {
769 return [
770 'action=edit&title=Test&summary=test%20summary&' .
771 'text=article%20content&baserevid=1234567&token=123ABC'
772 => 'apihelp-edit-example-edit',
773 'action=edit&title=Test&summary=NOTOC&minor=&' .
774 'prependtext=__NOTOC__%0A&basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
775 => 'apihelp-edit-example-prepend',
776 'action=edit&title=Test&undo=13585&undoafter=13579&' .
777 'basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
778 => 'apihelp-edit-example-undo',
779 ];
780 }
781
783 public function getHelpUrls() {
784 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Edit';
785 }
786}
787
789class_alias( ApiEditPage::class, 'ApiEditPage' );
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:439
if(MW_ENTRY_POINT==='index') if(!defined( 'MW_NO_SESSION') &&MW_ENTRY_POINT !=='cli') global $wgTitle
Definition Setup.php:527
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:1522
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:557
requireAtLeastOneParameter( $params,... $required)
Die if 0 of a certain set of parameters is set and not false.
Definition ApiBase.php:1039
getMain()
Get the main module.
Definition ApiBase.php:575
useTransactionalTimeLimit()
Call wfTransactionalTimeLimit() if this request was POSTed.
Definition ApiBase.php:1369
getResult()
Get the result object.
Definition ApiBase.php:696
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:1535
dieBlocked(Block $block)
Throw an ApiUsageException, which will (if uncaught) call the main module's error handler and die wit...
Definition ApiBase.php:1550
dieStatus(StatusValue $status)
Throw an ApiUsageException based on the Status object.
Definition ApiBase.php:1573
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:837
dieReadOnly()
Helper function for readonly errors.
Definition ApiBase.php:1615
checkTitleUserPermissions(PageIdentity $pageIdentity, $actions, array $options=[])
Helper function for permission-denied errors.
Definition ApiBase.php:1654
getTitleOrPageId( $params, $load=false)
Attempts to load a WikiPage object from a title or pageid parameter, if possible.
Definition ApiBase.php:1161
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...
__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, ?ShadowPageLoader $shadowPageLoader=null,)
getHelpUrls()
Return links to more detailed help pages about the module.1.25, returning boolean false is deprecated...
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.
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:131
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:66
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.
A service which loads shadow content, which is content that is displayed on a nonexistent page with a...
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:129
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.