MediaWiki master
EditPage.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\EditPage;
8
9use BadMethodCallException;
38use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
92use OOUI;
93use OOUI\ButtonWidget;
94use OOUI\CheckboxInputWidget;
95use OOUI\CheckboxMultiselectInputWidget;
96use OOUI\DropdownInputWidget;
97use OOUI\FieldLayout;
98use RuntimeException;
99use StatusValue;
100use Wikimedia\Assert\Assert;
105use Wikimedia\Parsoid\Parsoid;
107use Wikimedia\Timestamp\ConvertibleTimestamp;
108use Wikimedia\Timestamp\TimestampFormat as TS;
109
132#[\AllowDynamicProperties]
133class EditPage implements IEditObject {
134 use ProtectedHookAccessorTrait;
135 use DataStashTrait;
136
140 public const UNICODE_CHECK = 'ℳ𝒲β™₯π“Šπ“ƒπ’Ύπ’Έβ„΄π’Ήβ„―';
141
145 public const EDITFORM_ID = 'editform';
146
151 public const POST_EDIT_COOKIE_KEY_PREFIX = 'PostEditRevision';
152
169 public const POST_EDIT_COOKIE_DURATION = 1200;
170
174 private $mArticle;
175
177 private $page;
178
180 private $mContextTitle = null;
181
186 public bool $isConflict = false;
187
189 private bool $isNew = false;
190
192 public $formtype;
193
199
201 private bool $stashedDataRetrieved = false;
202
203 private bool $mTokenOk = false;
204
205 private bool $mTriedSave = false;
206
207 private bool $incompleteForm = false;
208
209 private bool $missingSummary = false;
210
211 private bool $allowBlankSummary = false;
212
213 private bool $blankArticle = false;
214
215 private bool $allowBlankArticle = false;
216
218 private $problematicRedirectTarget = null;
219
221 private $allowedProblematicRedirectTarget = null;
222
223 private bool $ignoreProblematicRedirects = false;
224
225 private string $autoSumm = '';
226
227 private ?ParserOutput $mParserOutput = null;
228
229 public bool $mShowSummaryField = true;
230
231 # Form values
232
233 public bool $save = false;
234
235 public bool $preview = false;
236
237 private bool $diff = false;
238
239 private bool $minoredit = false;
240
241 private bool $watchthis = false;
242
244 private bool $watchlistExpiryEnabled;
245
247 private ?string $watchlistExpiry = null;
248
250 private bool $watchlistLabelsEnabled;
251
253 private ?array $watchlistLabels = null;
254
255 private bool $recreate = false;
256
257 private bool $ignoreRevisionDeletedWarning = false;
258
262 public string $textbox1 = '';
263
264 public string $summary = '';
265
267 private bool $nosummary = false;
268
273 public ?string $edittime = '';
274
287 private ?int $editRevId = null;
288
289 public string $section = '';
290
291 public ?string $sectiontitle = null;
292
296 public ?string $starttime = '';
297
304 public int $oldid = 0;
305
312 private int $parentRevId = 0;
313
314 private ?int $scrolltop = null;
315
316 private bool $markAsBot = true;
317
318 public string $contentModel;
319
320 public ?string $contentFormat = null;
321
323 private array $changeTags = [];
324
325 # Placeholders for text injection by hooks (must be HTML)
326 # extensions should take care to _append_ to the present value
327
329 public $editFormPageTop = '';
331 public $editFormTextTop = '';
344
346 public bool $didSave = false;
347 public int $undidRev = 0;
348 private int $undoAfter = 0;
349
350 public bool $suppressIntro = false;
351
352 private int|false $contentLength = false;
353
357 private bool $enableApiEditOverride = false;
358
360
362 private bool $isOldRev = false;
363
365 private string $unicodeCheck = '';
366
368 private $editConflictHelperFactory = null;
369 private ?TextConflictHelper $editConflictHelper = null;
370
371 private AuthManager $authManager;
372 private EditConstraintFactory $constraintFactory;
373 private IContentHandlerFactory $contentHandlerFactory;
374 private IConnectionProvider $dbProvider;
375 private LinkBatchFactory $linkBatchFactory;
376 private LinkRenderer $linkRenderer;
377 private PageEditFactory $pageEditFactory;
378 private PageEditingHelper $pageEditingHelper;
379 private PermissionManager $permManager;
380 private RestrictionStore $restrictionStore;
381 private RevisionStore $revisionStore;
382 private SessionManager $sessionManager;
383 private ShadowPageLoader $shadowPageLoader;
384 private TempUserCreator $tempUserCreator;
385 private TextboxBuilder $textboxBuilder;
386 private UserEditTracker $userEditTracker;
387 private UserFactory $userFactory;
388 private UserIdentityUtils $userIdentityUtils;
389 private UserOptionsLookup $userOptionsLookup;
390 private UserRegistrationLookup $userRegistrationLookup;
391 private WatchedItemStoreInterface $watchedItemStore;
392 private WatchlistLabelStore $watchlistLabelStore;
393 private WatchlistManager $watchlistManager;
394
396 private $placeholderTempUser;
397
399 private $unsavedTempUser;
400
402 private $savedTempUser;
403
405 private bool $tempUserCreateActive = false;
406
408 private ?string $tempUserName = null;
409
411 private bool $tempUserCreateDone = false;
412
414 private bool $unableToAcquireTempName = false;
415
417 private ?string $reauthRequired = null;
418
423 public function __construct( Article $article ) {
424 $this->mArticle = $article;
425 $this->page = $article->getPage(); // model object
426
427 // Make sure the local context is in sync with other member variables.
428 // Particularly make sure everything is using the same WikiPage instance.
429 // This should probably be the case in Article as well, but it's
430 // particularly important for EditPage, to make use of the in-place caching
431 // facility in WikiPage::prepareContentForEdit.
432 $this->context = new DerivativeContext( $article->getContext() );
433 $this->context->setWikiPage( $this->page );
434
435 $services = MediaWikiServices::getInstance();
436 $this->authManager = $services->getAuthManager();
437 $this->constraintFactory = $services->getService( '_EditConstraintFactory' );
438 $this->contentHandlerFactory = $services->getContentHandlerFactory();
439 $this->dbProvider = $services->getConnectionProvider();
440 $this->linkBatchFactory = $services->getLinkBatchFactory();
441 $this->linkRenderer = $services->getLinkRenderer();
442 $this->pageEditFactory = $services->getService( '_PageEditFactory' );
443 $this->pageEditingHelper = $services->getService( '_PageEditingHelper' );
444 $this->permManager = $services->getPermissionManager();
445 $this->restrictionStore = $services->getRestrictionStore();
446 $this->revisionStore = $services->getRevisionStore();
447 $this->sessionManager = $services->getSessionManager();
448 $this->shadowPageLoader = $services->getShadowPageLoader();
449 $this->tempUserCreator = $services->getTempUserCreator();
450 $this->textboxBuilder = $services->getTextboxBuilder();
451 $this->userEditTracker = $services->getUserEditTracker();
452 $this->userFactory = $services->getUserFactory();
453 $this->userIdentityUtils = $services->getUserIdentityUtils();
454 $this->userOptionsLookup = $services->getUserOptionsLookup();
455 $this->userRegistrationLookup = $services->getUserRegistrationLookup();
456 $this->watchedItemStore = $services->getWatchedItemStore();
457 $this->watchlistLabelStore = $services->getWatchlistLabelStore();
458 $this->watchlistManager = $services->getWatchlistManager();
459
460 $this->watchlistExpiryEnabled = $this->getContext()->getConfig() instanceof Config
461 && $this->getContext()->getConfig()->get( MainConfigNames::WatchlistExpiry );
462 $this->watchlistLabelsEnabled = $this->getContext()->getConfig() instanceof Config
463 && $this->getContext()->getConfig()->get( MainConfigNames::EnableWatchlistLabels );
464
465 $this->contentModel = $this->getTitle()->getContentModel();
466 $this->contentFormat = $this->contentHandlerFactory
467 ->getContentHandler( $this->contentModel )
468 ->getDefaultFormat();
469 }
470
474 public function getArticle() {
475 return $this->mArticle;
476 }
477
482 public function getContext() {
483 return $this->context;
484 }
485
490 public function getTitle() {
491 return $this->page->getTitle();
492 }
493
497 public function setContextTitle( $title ) {
498 $this->mContextTitle = $title;
499 }
500
505 public function getContextTitle() {
506 if ( $this->mContextTitle === null ) {
507 throw new RuntimeException( "EditPage does not have a context title set" );
508 } else {
509 return $this->mContextTitle;
510 }
511 }
512
518 public function setApiEditOverride( bool $enableOverride ): void {
519 $this->enableApiEditOverride = $enableOverride;
520 }
521
525 protected function handleRetrievedData( array $data ): void {
526 $data = array_diff_key(
527 $data,
528 // Unset wpEditToken and wpSave, as we only want to show a diff for this request.
529 [ 'wpEditToken' => true, 'wpSave' => true ]
530 );
531 $data['wpDiff'] = '1';
532 $request = new DerivativeRequest( $this->context->getRequest(), $data );
533 $this->context->setRequest( $request );
534 }
535
547 public function edit() {
548 // Allow extensions to modify/prevent this form or submission
549 if ( !$this->getHookRunner()->onAlternateEdit( $this ) ) {
550 return;
551 }
552
553 wfDebug( __METHOD__ . ": enter" );
554
555 // If they used redlink=1 and the page exists, redirect to the main article
556 if ( $this->context->getRequest()->getBool( 'redlink' ) && $this->page->exists() ) {
557 $this->context->getOutput()->redirect( $this->getTitle()->getFullURL() );
558 return;
559 }
560
561 // check for stashed form data for post-submit reauths, set
562 // EditPage request context if data exists, prior to importFormData
563 $this->setStashKey( self::EDITFORM_ID . ':' . $this->getTitle()->getPrefixedDBkey() );
564 $this->stashedDataRetrieved = $this->retrieveStashedData();
565 $this->importFormData();
566
567 $this->firsttime = false;
568
569 $readOnlyMode = MediaWikiServices::getInstance()->getReadOnlyMode();
570 if ( $this->save && $readOnlyMode->isReadOnly() ) {
571 // Force preview
572 $this->save = false;
573 $this->preview = true;
574 }
575
576 if ( $this->save ) {
577 $this->formtype = 'save';
578 } elseif ( $this->preview ) {
579 $this->formtype = 'preview';
580 } elseif ( $this->diff ) {
581 $this->formtype = 'diff';
582 } else { # First time through
583 $this->firsttime = true;
584 if ( $this->previewOnOpen() ) {
585 $this->formtype = 'preview';
586 } else {
587 $this->formtype = 'initial';
588 }
589 }
590
591 // Check permissions after possibly creating a placeholder temp user.
592 // This allows anonymous users to edit via a temporary account, if the site is
593 // configured to (1) disallow anonymous editing and (2) autocreate temporary
594 // accounts on edit.
595 $this->unableToAcquireTempName = !$this->maybeActivateTempUserCreate( !$this->firsttime )->isOK();
596
597 $status = $this->getEditPermissionStatus(
598 $this->save ? PermissionManager::RIGOR_SECURE : PermissionManager::RIGOR_FULL
599 );
600 if ( !$status->isGood() && $this->formtype !== 'save' ) {
601 wfDebug( __METHOD__ . ": User can't edit" );
602
603 $user = $this->context->getUser();
604 if ( $user->getBlock() && !$readOnlyMode->isReadOnly() ) {
605 // Auto-block user's IP if the account was "hard" blocked
606 $user->scheduleSpreadBlock();
607 }
608 $this->displayPermissionStatus( $status );
609
610 return;
611 }
612
613 $this->reauthRequired = $status->getReauthOperation();
614 $checkedReauthOps = $status->getCheckedReauthOperations();
615
616 $revRecord = $this->mArticle->fetchRevisionRecord();
617 // Disallow editing revisions with content models different from the current one
618 // Undo edits being an exception in order to allow reverting content model changes.
619 $revContentModel = $revRecord ?
620 $revRecord->getMainContentModel() :
621 false;
622 if ( $revContentModel && $revContentModel !== $this->contentModel ) {
623 $prevRevRecord = null;
624 $prevContentModel = false;
625 if ( $this->undidRev ) {
626 $undidRevRecord = $this->revisionStore
627 ->getRevisionById( $this->undidRev );
628 $prevRevRecord = $undidRevRecord ?
629 $this->revisionStore->getPreviousRevision( $undidRevRecord ) :
630 null;
631
632 $prevContentModel = $prevRevRecord ?
633 $prevRevRecord->getMainContentModel() :
634 '';
635 }
636
637 if ( !$this->undidRev
638 || !$prevRevRecord
639 || $prevContentModel !== $this->contentModel
640 ) {
641 $this->displayViewSourcePage(
642 $this->getContentObject(),
643 $this->context->msg(
644 'contentmodelediterror',
645 $revContentModel,
646 $this->contentModel
647 )->plain()
648 );
649 return;
650 }
651 }
652
653 $this->isConflict = false;
654
655 # Attempt submission here. This will check for edit conflicts,
656 # and redundantly check for locked database, blocked IPs, etc.
657 # that edit() already checked just in case someone tries to sneak
658 # in the back door with a hand-edited submission URL.
659
660 if ( $this->formtype === 'save' ) {
661 $this->setStashKey( self::EDITFORM_ID . ':' . $this->getTitle()->getPrefixedDBkey() );
662
663 if ( $this->reauthRequired !== null ) {
664 // reauth at the attempted post/form save, stash user-submitted data
665 $queryParams = $this->stashDataOnPost();
666 $this->doReauthRedirect( $status, $queryParams );
667 }
668
669 $this->destroyStashedData();
670
671 // we need to check this again here, for other potential issues, since
672 // we're not checking formtype == save for the same validation above
673 if ( !$status->isGood() ) {
674 wfDebug( __METHOD__ . ": User can't edit" );
675 $user = $this->context->getUser();
676 if ( $user->getBlock() && !$readOnlyMode->isReadOnly() ) {
677 // Auto-block user's IP if the account was "hard" blocked
678 $user->scheduleSpreadBlock();
679 }
680 $this->displayPermissionStatus( $status );
681 return;
682 }
683
684 $resultDetails = null;
685 $status = $this->attemptSave( $resultDetails );
686 if ( !( $status instanceof PageEditStatus ) ) {
687 // Hooks and subclasses can cause attemptSave to return a normal Status, so cast it if necessary
688 $status = PageEditStatus::cast( $status );
689 }
690 if ( !$this->handleStatus( $status, $resultDetails ) ) {
691 return;
692 }
693 }
694
695 # First time through: get contents, set time for conflict
696 # checking, etc.
697 if ( $this->formtype === 'initial' || $this->firsttime ) {
698 if ( !$this->initialiseForm() ) {
699 return;
700 }
701
702 if ( $this->page->getId() ) {
703 $this->getHookRunner()->onEditFormInitialText( $this );
704 }
705 }
706
707 // If we're displaying an old revision, and there are differences between it and the
708 // latest revision outside the main slot, then we can't allow the old revision to be
709 // editable, as what would happen to the non-main-slot data if someone saves the old
710 // revision is undefined.
711 // When this is the case, display a read-only version of the page instead, with a link
712 // to a diff page from which the old revision can be restored
713 $curRevisionRecord = $this->page->getRevisionRecord();
714 if ( $curRevisionRecord
715 && $revRecord
716 && $curRevisionRecord->getId() !== $revRecord->getId()
717 && ( WikiPage::hasDifferencesOutsideMainSlot(
718 $revRecord,
719 $curRevisionRecord
720 ) || !$this->pageEditingHelper->isSupportedContentModel(
721 $revRecord->getSlot(
722 SlotRecord::MAIN,
723 RevisionRecord::RAW
724 )->getModel(),
725 $this->enableApiEditOverride
726 ) )
727 ) {
728 $restoreLink = $this->getTitle()->getFullURL(
729 [
730 'action' => 'mcrrestore',
731 'restore' => $revRecord->getId(),
732 ]
733 );
734 $this->displayViewSourcePage(
735 $this->getContentObject(),
736 $this->context->msg(
737 'nonmain-slot-differences-therefore-readonly',
738 $restoreLink
739 )->plain()
740 );
741 return;
742 }
743
744 // Attach AuthPopup whenever the edit action is reauth-gated on this
745 // wiki, even if the user's reauthentication is currently valid β€” it
746 // may expire while they sit on the form (T430197). The client uses
747 // wgReauthCurrentlyRequired to decide whether to open the popup
748 // immediately on submit or to verify with the server first.
749 $reauthOperation = $this->reauthRequired ?? ( $checkedReauthOps[0] ?? null );
750 $this->enableReauthPopup(
751 'mediawiki.editPage.reauthPopup',
752 $reauthOperation,
753 $this->reauthRequired !== null
754 );
755
756 $this->showEditForm();
757 }
758
768 public function maybeActivateTempUserCreate( $doAcquire ): Status {
769 if ( $this->tempUserCreateActive ) {
770 // Already done
771 return Status::newGood();
772 }
773 $user = $this->context->getUser();
774
775 // Log out any user using an expired temporary account, so that we can give them a new temporary account.
776 // As described in T389485, we need to do this because the maintenance script to expire temporary accounts
777 // may fail to run or not be configured to run.
778 if ( $user->isTemp() ) {
779 $expireAfterDays = $this->tempUserCreator->getExpireAfterDays();
780 if ( $expireAfterDays ) {
781 $expirationCutoff = (int)ConvertibleTimestamp::now( TS::UNIX ) - ( 86_400 * $expireAfterDays );
782
783 // If the user was created before the expiration cutoff, then log them out, expire any other existing
784 // sessions, and revoke any access to the account that may exist.
785 // If no registration is set then do nothing, as if registration date system is broken it would
786 // cause a new temporary account for each edit.
787 $firstUserRegistration = $this->userRegistrationLookup->getFirstRegistration( $user );
788 if (
789 $firstUserRegistration &&
790 ConvertibleTimestamp::convert( TS::UNIX, $firstUserRegistration ) < $expirationCutoff
791 ) {
792 // Log the user out of the expired temporary account.
793 $user->logout();
794
795 // Clear any stashed temporary account name (if any is set), as we want a new name for the user.
796 $session = $this->context->getRequest()->getSession();
797 $session->set( 'TempUser:name', null );
798 $session->save();
799
800 // Invalidate any sessions for the expired temporary account
801 $this->sessionManager->invalidateSessionsForUser(
802 $this->userFactory->newFromUserIdentity( $user )
803 );
804 }
805 }
806 }
807
808 if ( $this->tempUserCreator->shouldAutoCreate( $user, 'edit' ) ) {
809 if ( $doAcquire ) {
810 $name = $this->tempUserCreator->acquireAndStashName(
811 $this->context->getRequest()->getSession() );
812 if ( $name === null ) {
813 $status = Status::newFatal( 'temp-user-unable-to-acquire' );
814 $status->value = self::AS_UNABLE_TO_ACQUIRE_TEMP_ACCOUNT;
815 return $status;
816 }
817 $this->unsavedTempUser = $this->userFactory->newUnsavedTempUser( $name );
818 $this->tempUserName = $name;
819 } else {
820 $this->placeholderTempUser = $this->userFactory->newTempPlaceholder();
821 }
822 $this->tempUserCreateActive = true;
823 }
824 return Status::newGood();
825 }
826
834 private function createTempUser(): Status {
835 if ( !$this->tempUserCreateActive ) {
836 return Status::newGood();
837 }
838 $request = $this->context->getRequest();
839 $status = $this->tempUserCreator->create(
840 $this->tempUserName,
842 );
843 if ( $status->isOK() ) {
844 $this->placeholderTempUser = null;
845 $this->unsavedTempUser = null;
846 $this->savedTempUser = $status->getUser();
847 $this->authManager->setRequestContextUserFromSessionUser();
848 $this->tempUserCreateDone = true;
849 }
850 LoggerFactory::getInstance( 'authevents' )->info(
851 'Temporary account creation attempt: {user}',
852 [
853 'user' => $this->tempUserName,
854 'success' => $status->isOK(),
855 ] + $request->getSecurityLogContext( $status->isOK() ? $status->getUser() : null )
856 );
857 return $status;
858 }
859
869 private function getAuthority(): Authority {
870 return $this->getUserForPermissions();
871 }
872
879 private function getUserForPermissions() {
880 if ( $this->savedTempUser ) {
881 return $this->savedTempUser;
882 } elseif ( $this->unsavedTempUser ) {
883 return $this->unsavedTempUser;
884 } elseif ( $this->placeholderTempUser ) {
885 return $this->placeholderTempUser;
886 } else {
887 return $this->context->getUser();
888 }
889 }
890
895 private function getUserForPreview(): UserIdentity {
896 if ( $this->savedTempUser ) {
897 return $this->savedTempUser;
898 } elseif ( $this->unsavedTempUser ) {
899 return $this->unsavedTempUser;
900 } elseif ( $this->firsttime && $this->placeholderTempUser ) {
901 // Mostly a GET request and no temp user was acquired,
902 // but needed for pst or content transform for preview,
903 // fallback to a placeholder for this situation (T330943)
904 return $this->placeholderTempUser;
905 } elseif ( $this->tempUserCreateActive ) {
906 throw new BadMethodCallException(
907 "Can't use the request user for preview with IP masking enabled" );
908 } else {
909 return $this->context->getUser();
910 }
911 }
912
917 private function getUserForSave(): UserIdentity {
918 if ( $this->savedTempUser ) {
919 return $this->savedTempUser;
920 } elseif ( $this->tempUserCreateActive ) {
921 throw new BadMethodCallException(
922 "Can't use the request user for storage with IP masking enabled" );
923 } else {
924 return $this->context->getUser();
925 }
926 }
927
932 private function getEditPermissionStatus( string $rigor = PermissionManager::RIGOR_SECURE ): PermissionStatus {
933 $user = $this->getUserForPermissions();
934 return $this->permManager->getPermissionStatus(
935 'edit',
936 $user,
937 $this->getTitle(),
938 $rigor
939 );
940 }
941
953 private function displayPermissionStatus( PermissionStatus $status ): void {
954 $out = $this->context->getOutput();
955 if ( $this->context->getRequest()->getBool( 'redlink' ) ) {
956 // The edit page was reached via a red link.
957 // Redirect to the article page and let them click the edit tab if
958 // they really want a permission error.
959 $out->redirect( $this->getTitle()->getFullURL() );
960 return;
961 }
962
963 $content = $this->getContentObject();
964
965 // Use the normal message if there's nothing to display:
966 // page or section does not exist (T249978), and the user isn't in the middle of an edit
967 if ( !$content || ( $this->firsttime && !$this->page->exists() && $content->isEmpty() ) ) {
968 $action = $this->page->exists() ? 'edit' :
969 ( $this->getTitle()->isTalkPage() ? 'createtalk' : 'createpage' );
970 throw new PermissionsError( $action, $status );
971 }
972
973 $this->displayViewSourcePage(
974 $content,
975 $out->formatPermissionStatus( $status, 'edit' )
976 );
977 }
978
984 private function displayViewSourcePage( Content $content, string $errorMessage ): void {
985 $out = $this->context->getOutput();
986 $this->getHookRunner()->onEditPage__showReadOnlyForm_initial( $this, $out );
987
988 $out->setRobotPolicy( 'noindex,nofollow' );
989 $out->setPageTitleMsg( $this->context->msg(
990 'viewsource-title'
991 )->plaintextParams(
992 $this->getContextTitle()->getPrefixedText()
993 ) );
994 $out->addBacklinkSubtitle( $this->getContextTitle() );
995 $out->addHTML( $this->editFormPageTop );
996 $out->addHTML( $this->editFormTextTop );
997
998 if ( $errorMessage !== '' ) {
999 $out->addWikiTextAsInterface( $errorMessage );
1000 $out->addHTML( "<hr />\n" );
1001 }
1002
1003 # If the user made changes, preserve them when showing the markup
1004 # (This happens when a user is blocked during edit, for instance)
1005 if ( !$this->firsttime ) {
1006 $text = $this->textbox1;
1007 $out->addWikiMsg( 'viewyourtext' );
1008 } else {
1009 // Convert the content to editable text, or serialize using the default format if the content model is not
1010 // supported (e.g. for an old revision with a different model)
1011 try {
1012 $text = $this->pageEditingHelper->toEditText(
1013 $content, $this->contentFormat, $this->enableApiEditOverride
1014 ) ?? $content->serialize();
1015 } catch ( UnsupportedContentFormatException ) {
1016 // T419883: If the content format isn't supported, Content::serialize throws an exception
1017 $text = $content->serialize();
1018 }
1019 $out->addWikiMsg( 'viewsourcetext' );
1020 }
1021
1022 $out->addHTML( $this->editFormTextBeforeContent );
1023 $this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] );
1024 $out->addHTML( $this->editFormTextAfterContent );
1025
1026 $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
1027
1028 $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
1029
1030 $out->addHTML( $this->editFormTextBottom );
1031 if ( $this->page->exists() ) {
1032 $out->returnToMain( null, $this->page );
1033 }
1034 }
1035
1041 protected function previewOnOpen() {
1042 $config = $this->context->getConfig();
1043 $previewOnOpenNamespaces = $config->get( MainConfigNames::PreviewOnOpenNamespaces );
1044 $request = $this->context->getRequest();
1045 if ( $config->get( MainConfigNames::RawHtml ) ) {
1046 // If raw HTML is enabled, disable preview on open
1047 // since it has to be posted with a token for
1048 // security reasons
1049 return false;
1050 }
1051 $preview = $request->getRawVal( 'preview' );
1052 if ( $preview === 'yes' ) {
1053 // Explicit override from request
1054 return true;
1055 } elseif ( $preview === 'no' ) {
1056 // Explicit override from request
1057 return false;
1058 } elseif ( $this->section === 'new' ) {
1059 // Nothing *to* preview for new sections
1060 return false;
1061 } elseif ( ( $request->getCheck( 'preload' ) || $this->page->exists() )
1062 && $this->userOptionsLookup->getOption( $this->context->getUser(), 'previewonfirst' )
1063 ) {
1064 // Standard preference behavior
1065 return true;
1066 } elseif ( !$this->page->exists()
1067 && isset( $previewOnOpenNamespaces[$this->page->getNamespace()] )
1068 && $previewOnOpenNamespaces[$this->page->getNamespace()]
1069 ) {
1070 // Categories are special
1071 return true;
1072 } else {
1073 return false;
1074 }
1075 }
1076
1083 private function isSectionEditSupported(): bool {
1084 $currentRev = $this->page->getRevisionRecord();
1085
1086 // $currentRev is null for non-existing pages, use the page default content model.
1087 $revContentModel = $currentRev
1088 ? $currentRev->getMainContentModel()
1089 : $this->page->getContentModel();
1090
1091 return (
1092 ( $this->mArticle->getRevIdFetched() === $this->page->getLatest() ) &&
1093 $this->contentHandlerFactory->getContentHandler( $revContentModel )->supportsSections()
1094 );
1095 }
1096
1102 public function importFormData( $request = null ) {
1103 if ( $request !== null ) {
1104 wfDeprecated( __METHOD__ . ' with non-null $request', '1.47' );
1105 }
1106 $request ??= $this->getContext()->getRequest();
1107
1108 # Section edit can come from either the form or a link
1109 $this->section = $request->getVal( 'wpSection', $request->getVal( 'section', '' ) );
1110
1111 if ( $this->section !== '' && !$this->isSectionEditSupported() ) {
1112 throw new ErrorPageError( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
1113 }
1114
1115 $this->isNew = !$this->page->exists() || $this->section === 'new';
1116
1117 if ( $request->wasPosted() || $this->stashedDataRetrieved ) {
1118 $this->importFormDataPosted( $request );
1119 } else {
1120 # Not a posted form? Start with nothing.
1121 wfDebug( __METHOD__ . ": Not a posted form." );
1122 $this->textbox1 = '';
1123 $this->summary = '';
1124 $this->sectiontitle = null;
1125 $this->edittime = '';
1126 $this->editRevId = null;
1127 $this->starttime = wfTimestampNow();
1128 $this->preview = false;
1129 $this->save = false;
1130 $this->diff = false;
1131 $this->minoredit = false;
1132 // Watch may be overridden by request parameters
1133 $this->watchthis = $request->getBool( 'watchthis', false );
1134 if ( $this->watchlistExpiryEnabled ) {
1135 $this->watchlistExpiry = null;
1136 }
1137 $this->recreate = false;
1138
1139 // When creating a new section, we can preload a section title by passing it as the
1140 // preloadtitle parameter in the URL (T15100)
1141 if ( $this->section === 'new' && $request->getCheck( 'preloadtitle' ) ) {
1142 $this->sectiontitle = $request->getVal( 'preloadtitle' );
1143 $this->setNewSectionSummary();
1144 } elseif ( $this->section !== 'new' && $request->getRawVal( 'summary' ) !== '' ) {
1145 $this->summary = $request->getText( 'summary' );
1146 if ( $this->summary !== '' ) {
1147 // If a summary has been preset using &summary= we don't want to prompt for
1148 // a different summary. Only prompt for a summary if the summary is blanked.
1149 // (T19416)
1150 $this->autoSumm = md5( '' );
1151 }
1152 }
1153
1154 if ( $request->getVal( 'minor' ) ) {
1155 $this->minoredit = true;
1156 }
1157 }
1158
1159 if ( $request !== $this->getContext()->getRequest() ) {
1160 // TODO Remove this once the $request parameter has been removed from this method
1161 $this->oldid = $request->getInt( 'oldid' );
1162 } else {
1163 // Article::getOldID falls back to the request if no oldid is set
1164 $this->oldid = $this->mArticle->getOldID();
1165 }
1166 $this->parentRevId = $request->getInt( 'parentRevId' );
1167
1168 $this->markAsBot = $request->getBool( 'bot', true );
1169 $this->nosummary = $request->getBool( 'nosummary' );
1170
1171 // May be overridden by revision.
1172 $this->contentModel = $request->getText( 'model', $this->contentModel );
1173 // May be overridden by revision.
1174 $this->contentFormat = $request->getText( 'format', $this->contentFormat );
1175
1176 try {
1177 $handler = $this->contentHandlerFactory->getContentHandler( $this->contentModel );
1178 } catch ( UnknownContentModelException ) {
1179 throw new ErrorPageError(
1180 'editpage-invalidcontentmodel-title',
1181 'editpage-invalidcontentmodel-text',
1182 [ wfEscapeWikiText( $this->contentModel ) ]
1183 );
1184 }
1185
1186 if ( !$handler->isSupportedFormat( $this->contentFormat ) ) {
1187 throw new ErrorPageError(
1188 'editpage-notsupportedcontentformat-title',
1189 'editpage-notsupportedcontentformat-text',
1190 [
1191 wfEscapeWikiText( $this->contentFormat ),
1192 wfEscapeWikiText( ContentHandler::getLocalizedName( $this->contentModel ) )
1193 ]
1194 );
1195 }
1196
1197 // Allow extensions to modify form data
1198 $this->getHookRunner()->onEditPage__importFormData( $this, $request );
1199 }
1200
1201 private function importFormDataPosted( WebRequest $request ): void {
1202 # These fields need to be checked for encoding.
1203 # Also remove trailing whitespace, but don't remove _initial_
1204 # whitespace from the text boxes. This may be significant formatting.
1205 $this->textbox1 = rtrim( $request->getText( 'wpTextbox1' ) );
1206 if ( !$request->getCheck( 'wpTextbox2' ) ) {
1207 // Skip this if wpTextbox2 has input, it indicates that we came
1208 // from a conflict page with raw page text, not a custom form
1209 // modified by subclasses
1210 $textbox1 = $this->importContentFormData( $request );
1211 if ( $textbox1 !== null ) {
1212 $this->textbox1 = $textbox1;
1213 }
1214 }
1215
1216 $this->unicodeCheck = $request->getText( 'wpUnicodeCheck' );
1217
1218 if ( $this->section === 'new' ) {
1219 # Allow setting sectiontitle different from the edit summary.
1220 # Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is
1221 # currently doing double duty as both edit summary and section title. Right now this
1222 # is just to allow API edits to work around this limitation, but this should be
1223 # incorporated into the actual edit form when EditPage is rewritten (T20654, T28312).
1224 if ( $request->getCheck( 'wpSectionTitle' ) ) {
1225 $this->sectiontitle = $request->getText( 'wpSectionTitle' );
1226 if ( $request->getCheck( 'wpSummary' ) ) {
1227 $this->summary = $request->getText( 'wpSummary' );
1228 }
1229 } else {
1230 $this->sectiontitle = $request->getText( 'wpSummary' );
1231 }
1232 } else {
1233 $this->sectiontitle = null;
1234 $this->summary = $request->getText( 'wpSummary' );
1235 }
1236
1237 # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the
1238 # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for
1239 # section titles. (T3600)
1240 # It is weird to modify 'sectiontitle', even when it is provided when using the API, but API
1241 # users have come to rely on it: https://github.com/wikimedia-gadgets/twinkle/issues/1625
1242 $this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary );
1243 if ( $this->sectiontitle !== null ) {
1244 $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
1245 }
1246
1247 if ( $this->section === 'new' ) {
1248 $this->setNewSectionSummary();
1249 }
1250
1251 $this->edittime = $request->getVal( 'wpEdittime' );
1252 $this->editRevId = $request->getIntOrNull( 'editRevId' );
1253 $this->starttime = $request->getVal( 'wpStarttime' );
1254
1255 $undidRev = $request->getInt( 'wpUndidRevision' );
1256 if ( $undidRev ) {
1257 $this->undidRev = $undidRev;
1258 }
1259 $undoAfter = $request->getInt( 'wpUndoAfter' );
1260 if ( $undoAfter ) {
1261 $this->undoAfter = $undoAfter;
1262 }
1263
1264 $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' );
1265
1266 if ( $this->textbox1 === '' && !$request->getCheck( 'wpTextbox1' ) ) {
1267 // wpTextbox1 field is missing, possibly due to being "too big"
1268 // according to some filter rules that may have been configured
1269 // for security reasons.
1270 $this->incompleteForm = true;
1271 } else {
1272 // If we receive the last parameter of the request, we can fairly
1273 // claim the POST request has not been truncated.
1274 $this->incompleteForm = !$request->getVal( 'wpUltimateParam' );
1275 }
1276 if ( $this->incompleteForm ) {
1277 # If the form is incomplete, force to preview.
1278 wfDebug( __METHOD__ . ": Form data appears to be incomplete" );
1279 wfDebug( "POST DATA: " . var_export( $request->getPostValues(), true ) );
1280 $this->preview = true;
1281 } else {
1282 $this->preview = $request->getCheck( 'wpPreview' );
1283 $this->diff = $request->getCheck( 'wpDiff' );
1284
1285 // Remember whether a save was requested, so we can indicate
1286 // if we forced preview due to session failure.
1287 $this->mTriedSave = !$this->preview;
1288
1289 if ( $this->tokenOk( $request ) ) {
1290 # Some browsers will not report any submit button
1291 # if the user hits enter in the comment box.
1292 # The unmarked state will be assumed to be a save,
1293 # if the form seems otherwise complete.
1294 wfDebug( __METHOD__ . ": Passed token check." );
1295 } elseif ( $this->diff ) {
1296 # Failed token check, but only requested "Show Changes".
1297 wfDebug( __METHOD__ . ": Failed token check; Show Changes requested." );
1298 } else {
1299 # Page might be a hack attempt posted from
1300 # an external site. Preview instead of saving.
1301 wfDebug( __METHOD__ . ": Failed token check; forcing preview" );
1302 $this->preview = true;
1303 }
1304 }
1305 $this->save = !$this->preview && !$this->diff;
1306 if ( !$this->edittime || !preg_match( '/^\d{14}$/', $this->edittime ) ) {
1307 $this->edittime = null;
1308 }
1309
1310 if ( !$this->starttime || !preg_match( '/^\d{14}$/', $this->starttime ) ) {
1311 $this->starttime = null;
1312 }
1313
1314 $this->recreate = $request->getCheck( 'wpRecreate' );
1315
1316 $this->ignoreRevisionDeletedWarning = $request->getCheck( 'wpIgnoreRevisionDeleted' );
1317
1318 $this->minoredit = $request->getCheck( 'wpMinoredit' );
1319 $this->watchthis = $request->getCheck( 'wpWatchthis' );
1320 $submittedExpiry = $request->getText( 'wpWatchlistExpiry' );
1321 if ( $this->watchlistExpiryEnabled && $submittedExpiry !== '' ) {
1322 // This parsing of the user-posted expiry is done for both preview and saving. This
1323 // is necessary because ApiEditPage uses preview when it saves (yuck!). Note that it
1324 // only works because the unnormalized value is retrieved again below in
1325 // getCheckboxesDefinitionForWatchlist().
1326 $submittedExpiry = ExpiryDef::normalizeExpiry( $submittedExpiry, TS::ISO_8601 );
1327 if ( $submittedExpiry !== false ) {
1328 $this->watchlistExpiry = $submittedExpiry;
1329 }
1330 }
1331 if ( $this->watchlistLabelsEnabled ) {
1332 $watchlistLabelsSubmitted = $request->getCheck( 'wpWatchlistLabelsSubmitted' );
1333 $this->watchlistLabels = $watchlistLabelsSubmitted
1334 ? $request->getIntArray( 'wpWatchlistLabels', [] )
1335 : null;
1336 }
1337
1338 $this->autoSumm = $request->getText( 'wpAutoSummary' );
1339
1340 $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' );
1341 $this->allowBlankArticle = $request->getBool( 'wpIgnoreBlankArticle' );
1342 $allowedProblematicRedirectTargetText = $request->getText( 'wpAllowedProblematicRedirectTarget' );
1343 $this->allowedProblematicRedirectTarget = $allowedProblematicRedirectTargetText === ''
1344 ? null : Title::newFromText( $allowedProblematicRedirectTargetText );
1345 $this->ignoreProblematicRedirects = $request->getBool( 'wpIgnoreProblematicRedirects' );
1346
1347 $changeTags = $request->getVal( 'wpChangeTags' );
1348 $changeTagsAfterPreview = $request->getVal( 'wpChangeTagsAfterPreview' );
1349 if ( $changeTags === null || $changeTags === '' ) {
1350 $this->changeTags = [];
1351 } else {
1352 $this->changeTags = array_filter(
1353 array_map(
1354 'trim',
1355 explode( ',', $changeTags )
1356 )
1357 );
1358 }
1359 if ( $changeTagsAfterPreview !== null && $changeTagsAfterPreview !== '' ) {
1360 $this->changeTags = array_merge( $this->changeTags, array_filter(
1361 array_map(
1362 'trim',
1363 explode( ',', $changeTagsAfterPreview )
1364 )
1365 ) );
1366 }
1367 }
1368
1378 protected function importContentFormData( &$request ) {
1379 return null; // Don't do anything, EditPage already extracted wpTextbox1
1380 }
1381
1387 private function initialiseForm(): bool {
1388 $this->edittime = $this->page->getTimestamp();
1389 $this->editRevId = $this->page->getLatest();
1390
1391 $dummy = $this->contentHandlerFactory
1392 ->getContentHandler( $this->contentModel )
1393 ->makeEmptyContent();
1394 $content = $this->getContentObject( $dummy ); # TODO: track content object?!
1395 if ( $content === $dummy ) { // Invalid section
1396 $this->noSuchSectionPage();
1397 return false;
1398 }
1399
1400 if ( !$content ) {
1401 $this->editFormPageTop .= Html::errorBox( $this->context->msg(
1402 'missing-revision-content',
1403 $this->oldid,
1404 wfEscapeWikiText( $this->getTitle()->getPrefixedText() )
1405 )->parse() );
1406 } elseif ( !$this->pageEditingHelper->isSupportedContentModel(
1407 $content->getModel(), $this->enableApiEditOverride,
1408 ) ) {
1409 $modelMsg = $this->getContext()->msg( 'content-model-' . $content->getModel() );
1410 $modelName = $modelMsg->exists() ? $modelMsg->text() : $content->getModel();
1411
1412 $out = $this->context->getOutput();
1413 $out->showErrorPage(
1414 'modeleditnotsupported-title',
1415 'modeleditnotsupported-text',
1416 [ $modelName ]
1417 );
1418 return false;
1419 }
1420
1421 try {
1422 $this->textbox1 = $this->pageEditingHelper->toEditText(
1423 $content, $this->contentFormat, $this->enableApiEditOverride
1424 ) ?? $content->serialize();
1425 } catch ( UnsupportedContentFormatException ) {
1426 // T391524: If the content format isn't supported, Content::serialize throws an exception
1427 $this->textbox1 = $content->serialize();
1428 }
1429
1430 $user = $this->context->getUser();
1431 // activate checkboxes if user wants them to be always active
1432 # Sort out the "watch" checkbox
1433 if ( $this->userOptionsLookup->getOption( $user, 'watchdefault' ) ) {
1434 # Watch all edits
1435 $this->watchthis = true;
1436 } elseif ( $this->userOptionsLookup->getOption( $user, 'watchcreations' ) && !$this->page->exists() ) {
1437 # Watch creations
1438 $this->watchthis = true;
1439 } elseif ( $this->watchlistManager->isWatched( $user, $this->page ) ) {
1440 # Already watched
1441 $this->watchthis = true;
1442 }
1443 if ( $this->watchthis && $this->watchlistExpiryEnabled ) {
1444 $watchedItem = $this->watchedItemStore->getWatchedItem( $user, $this->getTitle() );
1445 $this->watchlistExpiry = $watchedItem ? $watchedItem->getExpiry() : null;
1446 }
1447 if ( !$this->isNew && $this->userOptionsLookup->getOption( $user, 'minordefault' ) ) {
1448 $this->minoredit = true;
1449 }
1450 return true;
1451 }
1452
1458 protected function getContentObject( $defaultContent = null ) {
1459 $services = MediaWikiServices::getInstance();
1460 $request = $this->context->getRequest();
1461
1462 $content = false;
1463
1464 // For non-existent articles and new sections, use preload text if any.
1465 if ( !$this->page->exists() || $this->section === 'new' ) {
1466 $content = $services->getPreloadedContentBuilder()->getPreloadedContent(
1467 $this->page,
1468 $this->context->getUser(),
1469 $request->getVal( 'preload' ),
1470 array_filter( $request->getArray( 'preloadparams', [] ), is_string( ... ) ),
1471 $request->getVal( 'section' )
1472 );
1473 // For existing pages, get text based on "undo" or section parameters.
1474 } elseif ( $this->section !== '' ) {
1475 // Get section edit text (returns $def_text for invalid sections)
1476 $orig = $this->pageEditingHelper->getOriginalContent(
1477 $this->getAuthority(),
1478 $this->page,
1479 $this->mArticle->fetchRevisionRecord(),
1480 $this->contentModel,
1481 $this->section,
1482 );
1483 $content = $orig ? $orig->getSection( $this->section ) : null;
1484
1485 if ( !$content ) {
1486 $content = $defaultContent;
1487 }
1488 } else {
1489 $undoafter = $request->getInt( 'undoafter' );
1490 $undo = $request->getInt( 'undo' );
1491
1492 if ( $undo > 0 && $undoafter > 0 ) {
1493 // The use of getRevisionByTitle() is intentional, as allowing access to
1494 // arbitrary revisions on arbitrary pages bypass partial visibility restrictions (T297322).
1495 $undorev = $this->revisionStore->getRevisionByTitle( $this->page, $undo );
1496 $oldrev = $this->revisionStore->getRevisionByTitle( $this->page, $undoafter );
1497 $undoMsg = null;
1498
1499 # Make sure it's the right page,
1500 # the revisions exist and they were not deleted.
1501 # Otherwise, $content will be left as-is.
1502 if ( $undorev !== null && $oldrev !== null &&
1503 !$undorev->isDeleted( RevisionRecord::DELETED_TEXT ) &&
1504 !$oldrev->isDeleted( RevisionRecord::DELETED_TEXT )
1505 ) {
1506 if ( WikiPage::hasDifferencesOutsideMainSlot( $undorev, $oldrev )
1507 || !$this->pageEditingHelper->isSupportedContentModel(
1508 $oldrev->getMainContentModel(), $this->enableApiEditOverride
1509 )
1510 ) {
1511 // Hack for undo while EditPage can't handle multi-slot editing
1512 $this->context->getOutput()->redirect( $this->getTitle()->getFullURL( [
1513 'action' => 'mcrundo',
1514 'undo' => $undo,
1515 'undoafter' => $undoafter,
1516 ] ) );
1517 return false;
1518 } else {
1519 $content = $this->pageEditingHelper->getUndoContent( $this->page, $undorev, $oldrev, $undoMsg );
1520 }
1521
1522 if ( $undoMsg === null ) {
1523 $oldContent = $this->page->getContent( RevisionRecord::RAW );
1524 $parserOptions = ParserOptions::newFromUserAndLang(
1525 $this->getUserForPreview(),
1526 $services->getContentLanguage()
1527 );
1528 $contentTransformer = $services->getContentTransformer();
1529 $newContent = $contentTransformer->preSaveTransform(
1530 $content, $this->page, $this->getUserForPreview(), $parserOptions
1531 );
1532
1533 if ( $newContent->getModel() !== $oldContent->getModel() ) {
1534 // The undo may change content
1535 // model if its reverting the top
1536 // edit. This can result in
1537 // mismatched content model/format.
1538 $this->contentModel = $newContent->getModel();
1539 $oldMainSlot = $oldrev->getSlot(
1540 SlotRecord::MAIN,
1541 RevisionRecord::RAW
1542 );
1543 $this->contentFormat = $oldMainSlot->getFormat();
1544 if ( $this->contentFormat === null ) {
1545 $this->contentFormat = $this->contentHandlerFactory
1546 ->getContentHandler( $oldMainSlot->getModel() )
1547 ->getDefaultFormat();
1548 }
1549 }
1550
1551 if ( $newContent->equals( $oldContent ) ) {
1552 # Tell the user that the undo results in no change,
1553 # i.e. the revisions were already undone.
1554 $undoMsg = 'nochange';
1555 $content = false;
1556 } else {
1557 # Inform the user of our success and set an automatic edit summary
1558 $undoMsg = 'success';
1559 $this->generateUndoEditSummary( $oldrev, $undo, $undorev );
1560 $this->undidRev = $undo;
1561 $this->undoAfter = $undoafter;
1562 $this->formtype = 'diff';
1563 }
1564 }
1565 } else {
1566 // Failed basic checks.
1567 // Older revisions may have been removed since the link
1568 // was created, or we may simply have got bogus input.
1569 $undoMsg = 'norev';
1570 }
1571
1572 $out = $this->context->getOutput();
1573 // Messages: undo-success, undo-failure, undo-main-slot-only, undo-norev,
1574 // undo-nochange.
1575 $class = "mw-undo-{$undoMsg}";
1576 $html = $this->context->msg( 'undo-' . $undoMsg )->parse();
1577 if ( $undoMsg !== 'success' ) {
1578 $html = Html::errorBox( $html );
1579 }
1580 $this->editFormPageTop .= Html::rawElement(
1581 'div',
1582 [ 'class' => $class ],
1583 $html
1584 );
1585 }
1586
1587 if ( $content === false ) {
1588 $content = $this->pageEditingHelper->getOriginalContent(
1589 $this->getAuthority(),
1590 $this->page,
1591 $this->mArticle->fetchRevisionRecord(),
1592 $this->contentModel,
1593 $this->section,
1594 );
1595 }
1596 }
1597
1598 return $content;
1599 }
1600
1609 private function generateUndoEditSummary(
1610 ?RevisionRecord $oldRev,
1611 int $undo,
1612 ?RevisionRecord $undoRev,
1613 ): void {
1614 // Generate an autosummary
1615 $firstRev = $this->revisionStore->getNextRevision( $oldRev );
1616 if ( $firstRev && $firstRev->getId() == $undo ) {
1617 // Undid just one revision
1618 $userText = $undoRev->getUser()?->getName();
1619 if ( $userText === null ) {
1620 $undoSummary = $this->context->msg(
1621 'undo-summary-username-hidden',
1622 $undo
1623 )->inContentLanguage()->text();
1624 } elseif ( ExternalUserNames::isExternal( $userText ) ) {
1625 // Handle external users (imported revisions)
1626 $userLinkTitle = ExternalUserNames::getUserLinkTitle( $userText );
1627 if ( $userLinkTitle ) {
1628 $userLink = $userLinkTitle->getPrefixedText();
1629 $undoSummary = $this->context->msg(
1630 'undo-summary-import',
1631 $undo,
1632 $userLink,
1633 $userText
1634 )->inContentLanguage()->text();
1635 } else {
1636 $undoSummary = $this->context->msg(
1637 'undo-summary-import2',
1638 $undo,
1639 $userText
1640 )->inContentLanguage()->text();
1641 }
1642 } else {
1643 $undoIsAnon =
1644 !$undoRev->getUser() ||
1645 !$undoRev->getUser()->isRegistered();
1646 $disableAnonTalk = $this->context->getConfig()->get( MainConfigNames::DisableAnonTalk );
1647 $undoMessage = ( $undoIsAnon && $disableAnonTalk ) ?
1648 'undo-summary-anon' :
1649 'undo-summary';
1650 $undoSummary = $this->context->msg(
1651 $undoMessage,
1652 $undo,
1653 $userText
1654 )->inContentLanguage()->text();
1655 }
1656 } else {
1657 // Undid multiple revisions
1658 $firstRevisionId = $firstRev->getId();
1659 $lastRevisionId = $undoRev->getId();
1660 $revisionCount = $this->revisionStore->countRevisionsBetween(
1661 $firstRev->getPageId(),
1662 $firstRev,
1663 $undoRev,
1664 null,
1665 [ RevisionStore::INCLUDE_BOTH, RevisionStore::INCLUDE_DELETED_REVISIONS ]
1666 );
1667 $undoSummary = $this->context->msg( 'undo-summary-multiple' )
1668 ->numParams( $revisionCount )
1669 ->params( $firstRevisionId, $lastRevisionId )
1670 ->inContentLanguage()
1671 ->text();
1672 }
1673 if ( $this->summary === '' ) {
1674 $this->summary = $undoSummary;
1675 } else {
1676 $this->summary = $undoSummary . $this->context->msg( 'colon-separator' )
1677 ->inContentLanguage()->text() . $this->summary;
1678 }
1679 }
1680
1692 private function getParentRevId() {
1693 if ( $this->parentRevId ) {
1694 return $this->parentRevId;
1695 } else {
1696 return $this->mArticle->getRevIdFetched();
1697 }
1698 }
1699
1708 protected function getCurrentContent() {
1709 return $this->pageEditingHelper->getCurrentContent( $this->contentModel, $this->page );
1710 }
1711
1718 private function tokenOk( WebRequest $request ): bool {
1719 $token = $request->getVal( 'wpEditToken' );
1720 $user = $this->context->getUser();
1721 $this->mTokenOk = $user->matchEditToken( $token );
1722 return $this->mTokenOk;
1723 }
1724
1739 private function setPostEditCookie( int $statusValue ): void {
1740 $revisionId = $this->page->getLatest();
1741 $postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
1742
1743 $val = 'saved';
1744 if ( $statusValue === self::AS_SUCCESS_NEW_ARTICLE ) {
1745 $val = 'created';
1746 } elseif ( $this->oldid ) {
1747 $val = 'restored';
1748 }
1749 if ( $this->tempUserCreateDone ) {
1750 $val .= '+tempuser';
1751 }
1752
1753 $response = $this->context->getRequest()->response();
1754 $response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION );
1755 }
1756
1763 public function attemptSave( &$resultDetails = false ) {
1764 $status = $this->internalAttemptSave( $resultDetails );
1765 if ( !$status->isOK() ) {
1766 $this->handleFailedConstraint( $status );
1767 }
1768
1769 // Status::wrap() takes references to all internal variables, allowing hook handlers to modify
1770 // the $status, without changing the hook interface to use the PageEditStatus type.
1771 $this->getHookRunner()->onEditPage__attemptSave_after( $this, Status::wrap( $status ), $resultDetails );
1772
1773 return $status;
1774 }
1775
1779 private function incrementResolvedConflicts(): void {
1780 if ( $this->context->getRequest()->getText( 'mode' ) !== 'conflict' ) {
1781 return;
1782 }
1783
1784 $this->getEditConflictHelper()->incrementResolvedStats( $this->context->getUser() );
1785 }
1786
1796 private function handleStatus( PageEditStatus $status, $resultDetails ): bool {
1797 $statusValue = is_int( $status->value ) ? $status->value : 0;
1798
1803 if ( $statusValue === self::AS_SUCCESS_UPDATE
1804 || $statusValue === self::AS_SUCCESS_NEW_ARTICLE
1805 ) {
1806 $this->incrementResolvedConflicts();
1807
1808 $this->didSave = true;
1809 if ( !$resultDetails['nullEdit'] ) {
1810 $this->setPostEditCookie( $statusValue );
1811 }
1812 }
1813
1814 $out = $this->context->getOutput();
1815
1816 // "wpExtraQueryRedirect" is a hidden input to modify
1817 // after save URL and is not used by actual edit form
1818 $request = $this->context->getRequest();
1819 $extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
1820
1821 $status->throwErrorIfSet();
1822
1823 switch ( $statusValue ) {
1824 // For status codes that provide their own error/warning messages, we display the errors and warnings in
1825 // message boxes.
1826 default:
1827 $out->addHTML( $this->formatConstraintStatus( $status ) );
1828 return true;
1829
1830 // There is a UI for edit conflicts, so we don't need to show a message box.
1831 case self::AS_CONFLICT_DETECTED:
1832 return true;
1833
1834 case self::AS_HOOK_ERROR:
1835 return false;
1836
1837 case self::AS_SUCCESS_NEW_ARTICLE:
1838 $queryParts = [];
1839 if ( $resultDetails['redirect'] ) {
1840 $queryParts[] = 'redirect=no';
1841 }
1842 if ( $extraQueryRedirect ) {
1843 $queryParts[] = $extraQueryRedirect;
1844 }
1845 $anchor = $resultDetails['sectionanchor'];
1846 $this->doPostEditRedirect( implode( '&', $queryParts ), $anchor );
1847 return false;
1848
1849 case self::AS_SUCCESS_UPDATE:
1850 $extraQuery = '';
1851 $sectionanchor = $resultDetails['sectionanchor'];
1852 // Give extensions a chance to modify URL query on update
1853 $this->getHookRunner()->onArticleUpdateBeforeRedirect( $this->mArticle,
1854 $sectionanchor, $extraQuery );
1855
1856 $queryParts = [];
1857 if ( $resultDetails['redirect'] ) {
1858 $queryParts[] = 'redirect=no';
1859 }
1860 if ( $extraQuery ) {
1861 $queryParts[] = $extraQuery;
1862 }
1863 if ( $extraQueryRedirect ) {
1864 $queryParts[] = $extraQueryRedirect;
1865 }
1866 $this->doPostEditRedirect( implode( '&', $queryParts ), $sectionanchor );
1867 return false;
1868
1869 case self::AS_SPAM_ERROR:
1870 $this->spamPageWithContent( $resultDetails['spam'] ?? false );
1871 return false;
1872 }
1873 }
1874
1879 private function formatConstraintStatus( StatusValue $status ): string {
1880 return $this->createMessageBox( $status->getMessages( 'error' ), 'error' ) .
1881 $this->createMessageBox( $status->getMessages( 'warning' ), 'warning' );
1882 }
1883
1889 private function createMessageBox( array $messages, string $type ): string {
1890 if ( !$messages ) {
1891 // Don't create a box if there are no messages
1892 return '';
1893 }
1894
1895 $html = implode(
1896 Html::openElement( 'br' ),
1897 array_map( fn ( $msg ) => $this->context->msg( $msg )->parse(), $messages )
1898 );
1899
1900 if ( $type === 'warning' ) {
1901 return Html::warningBox( $html );
1902 }
1903 return Html::errorBox( $html );
1904 }
1905
1909 private function doPostEditRedirect( string $query, string $anchor ): void {
1910 $out = $this->context->getOutput();
1911 $url = $this->getTitle()->getFullURL( $query ) . $anchor;
1912 $user = $this->getUserForSave();
1913 // If the temporary account was created in this request,
1914 // or if the temporary account has zero edits (implying
1915 // that the account was created during a failed edit
1916 // attempt in a previous request), perform the top-level
1917 // redirect to ensure the account is attached.
1918 // Note that the temp user could already have performed
1919 // the top-level redirect if this a first edit on
1920 // a wiki that is not the user's home wiki.
1921 $shouldRedirectForTempUser = $this->tempUserCreateDone ||
1922 ( $this->userIdentityUtils->isTemp( $user ) && $this->userEditTracker->getUserEditCount( $user ) === 0 );
1923 if ( $shouldRedirectForTempUser ) {
1924 $this->getHookRunner()->onTempUserCreatedRedirect(
1925 $this->context->getRequest()->getSession(),
1926 $user,
1927 $this->getTitle()->getPrefixedDBkey(),
1928 $query,
1929 $anchor,
1930 $url
1931 );
1932 }
1933 $out->redirect( $url );
1934 }
1935
1939 private function setNewSectionSummary(): void {
1940 Assert::precondition( $this->section === 'new', 'This method can only be called for new sections' );
1941 Assert::precondition( $this->sectiontitle !== null, 'This method can only be called for new sections' );
1942
1943 $services = MediaWikiServices::getInstance();
1944 $parser = $services->getParser();
1945 $textFormatter = $services->getMessageFormatterFactory()->getTextFormatter(
1946 $services->getContentLanguageCode()->toString()
1947 );
1948
1949 // If no edit summary was specified and the section title is not empty, create
1950 // a summary automatically from the section title and have it link to the new section.
1951 // Otherwise, respect the summary as passed.
1952 if ( $this->sectiontitle !== '' && $this->summary === '' ) {
1953 $messageValue = MessageValue::new( 'newsectionsummary' )
1954 ->plaintextParams( $parser->stripSectionName( $this->sectiontitle ) );
1955 $this->summary = $textFormatter->format( $messageValue );
1956 }
1957 }
1958
1982 private function internalAttemptSave( &$result ) {
1983 // If an attempt to acquire a temporary name failed, don't attempt to do anything else.
1984 if ( $this->unableToAcquireTempName ) {
1985 return PageEditStatus::newFatal( 'temp-user-unable-to-acquire' )
1986 ->setValue( self::AS_UNABLE_TO_ACQUIRE_TEMP_ACCOUNT );
1987 }
1988 // Auto-create the temporary account user, if the feature is enabled.
1989 // We create the account before any constraint checks or edit hooks fire, to ensure
1990 // that we have an actor and user account that can be used for any logs generated
1991 // by the edit attempt, and to ensure continuity in the user experience (if a constraint
1992 // denies an edit to a logged-out user, that history should be associated with the
1993 // eventually successful account creation)
1994 $tempAccountStatus = $this->createTempUser();
1995 if ( !$tempAccountStatus->isOK() ) {
1996 return PageEditStatus::cast( $tempAccountStatus );
1997 }
1998 if ( $tempAccountStatus instanceof CreateStatus ) {
1999 $result['savedTempUser'] = $tempAccountStatus->getUser();
2000 }
2001
2002 if ( !$this->getHookRunner()->onEditPage__attemptSave( $this ) ) {
2003 wfDebug( "Hook 'EditPage::attemptSave' aborted article saving" );
2004 return PageEditStatus::newFatal( 'hookaborted' )
2005 ->setValue( self::AS_HOOK_ERROR );
2006 }
2007
2008 if ( $this->unicodeCheck !== self::UNICODE_CHECK ) {
2009 return PageEditStatus::newFatal( 'unicode-support-fail' )
2010 ->setValue( self::AS_UNICODE_NOT_SUPPORTED );
2011 }
2012
2013 $antispam = $this->getContext()->getRequest()->getText( 'wpAntispam' );
2014 if ( $antispam !== '' ) {
2015 LoggerFactory::getInstance( 'SimpleAntiSpam' )->debug(
2016 '{name} editing "{title}" submitted bogus field "{input}"',
2017 [
2018 // Use the context user since there is no permissions aspect
2019 'name' => $this->context->getUser()->getName(),
2020 'title' => $this->page,
2021 'input' => $antispam,
2022 ]
2023 );
2024 return PageEditStatus::newFatal( 'spamprotectionmatch', '' )
2025 ->setValue( self::AS_SPAM_ERROR );
2026 }
2027
2028 $allowBlankSummary = $this->allowBlankSummary || ( $this->nosummary && $this->section === 'new' );
2029
2030 $inputs = ( new PageEditInputs(
2031 authority: $this->getAuthority(),
2032 contentModel: $this->contentModel,
2033 context: $this->context,
2034 page: $this->page,
2035 summary: $this->summary,
2036 textbox1: $this->textbox1,
2037 ) )->setAllowBlankArticle( $this->allowBlankArticle )
2038 ->setAllowBlankSummary( $allowBlankSummary )
2039 ->setAllowedProblematicRedirectTarget( $this->allowedProblematicRedirectTarget )
2040 ->setAutoSumm( $this->autoSumm )
2041 ->setChangeTags( $this->changeTags )
2042 ->setContentFormat( $this->contentFormat )
2043 ->setContextPage( $this->mContextTitle )
2044 ->setEdittime( $this->edittime )
2045 ->setEditRevId( $this->editRevId )
2046 ->setEnableApiEditOverride( $this->enableApiEditOverride )
2047 ->setIgnoreProblematicRedirects( $this->ignoreProblematicRedirects )
2048 ->setIgnoreRevisionDeletedWarning( $this->ignoreRevisionDeletedWarning )
2049 ->setMarkAsBot( $this->markAsBot )
2050 ->setMarkAsMinor( $this->minoredit )
2051 ->setOldid( $this->oldid )
2052 ->setParentRevId( $this->parentRevId )
2053 ->setRecreate( $this->recreate )
2054 ->setSection( $this->section )
2055 ->setSectiontitle( $this->sectiontitle )
2056 ->setStarttime( $this->starttime )
2057 ->setUndidRev( $this->undidRev )
2058 ->setUndoAfter( $this->undoAfter )
2059 ->setUserForPreview( $this->getUserForPreview() )
2060 ->setUserForSave( $this->getUserForSave() )
2061 ->setWatchlistExpiry( $this->watchlistExpiry )
2062 ->setWatchlistLabels( $this->watchlistLabels )
2063 ->setWatchthis( $this->watchthis );
2064
2065 $pageEdit = $this->pageEditFactory->newPageEdit( $inputs );
2066 $pageEditResult = $pageEdit->edit();
2067
2068 // PageEdit writes to those properties, so update them for backwards compatibility
2069 $this->contentLength = $pageEditResult->getContentLength();
2070 $this->isConflict = $pageEditResult->isConflict();
2071 $this->parentRevId = $pageEditResult->getParentRevId();
2072 $this->section = $pageEditResult->getSection();
2073 $this->textbox1 = $pageEditResult->getTextbox1();
2074
2075 if ( !$pageEditResult->getStatus()->isOK() ) {
2076 $failed = $pageEditResult->getStatus()->getFailedConstraint();
2077
2078 // Need to check SpamRegexConstraint here, to avoid needing to pass
2079 // $result by reference again
2080 if ( $failed instanceof SpamRegexConstraint ) {
2081 $result['spam'] = $failed->getMatch();
2082 } else {
2083 $this->handleFailedConstraint( $pageEditResult->getStatus() );
2084 }
2085 }
2086
2087 $result['sectionanchor'] = $pageEditResult->getSectionanchor();
2088 if ( $pageEditResult->isNullEdit() !== null ) {
2089 $result['nullEdit'] = $pageEditResult->isNullEdit();
2090 }
2091 if ( $pageEditResult->isRedirect() !== null ) {
2092 $result['redirect'] = $pageEditResult->isRedirect();
2093 }
2094
2095 return $pageEditResult->getStatus();
2096 }
2097
2104 private function handleFailedConstraint( PageEditStatus $status ): void {
2105 $failed = $status->getFailedConstraint();
2106 if ( $failed instanceof AuthorizationConstraint ) {
2107 // Auto-block user's IP if the account was "hard" blocked
2108 if (
2109 !MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly()
2110 && $status->value === self::AS_BLOCKED_PAGE_FOR_USER
2111 ) {
2112 $this->context->getUser()->spreadAnyEditBlock();
2113 }
2114 } elseif ( $failed instanceof DefaultTextConstraint ) {
2115 $this->blankArticle = true;
2116 } elseif (
2117 $failed instanceof ExistingSectionEditConstraint ||
2118 $failed instanceof NewSectionMissingSubjectConstraint
2119 ) {
2120 $this->missingSummary = true;
2121 $this->allowBlankSummary = true;
2122 } elseif ( $failed instanceof RedirectConstraint ) {
2123 $this->problematicRedirectTarget = $failed->problematicTarget;
2124 } elseif ( $failed instanceof AccidentalRecreationConstraint ) {
2125 $this->recreate = true;
2126 } elseif ( $failed instanceof RevisionDeletedConstraint ) {
2127 $this->ignoreRevisionDeletedWarning = true;
2128 }
2129 }
2130
2139 return $this->pageEditingHelper->getExpectedParentRevision(
2140 $this->editRevId,
2141 $this->edittime,
2142 $this->page,
2143 );
2144 }
2145
2146 public function setHeaders() {
2147 $out = $this->context->getOutput();
2148
2149 # Indicate to client-side JS (Visual Editor in particular) whether
2150 # Parsoid would be used to render this article, so previews of the
2151 # edit can match. Mirrors Article::showMissingArticle().
2152 if ( $this->getArticle()->getParserOptions()->getUseParsoid() ) {
2153 $out->addJsConfigVars(
2154 'wgParsoidHtmlVersion',
2155 Parsoid::defaultHTMLVersion()
2156 );
2157 }
2158
2159 $out->addModules( 'mediawiki.action.edit' );
2160 $out->addModuleStyles( [
2161 'mediawiki.action.edit.styles',
2162 'mediawiki.codex.messagebox.styles',
2163 'mediawiki.editfont.styles',
2164 'mediawiki.interface.helpers.styles',
2165 ] );
2166
2167 $user = $this->context->getUser();
2168
2169 if ( $this->userOptionsLookup->getOption( $user, 'uselivepreview' ) ) {
2170 $out->addModules( 'mediawiki.action.edit.preview' );
2171 }
2172
2173 if ( $this->userOptionsLookup->getOption( $user, 'useeditwarning' ) ) {
2174 $out->addModules( 'mediawiki.action.edit.editWarning' );
2175 }
2176
2177 if ( $this->context->getConfig()->get( MainConfigNames::EnableEditRecovery )
2178 && $this->userOptionsLookup->getOption( $user, 'editrecovery' )
2179 ) {
2180 $wasPosted = $this->getContext()->getRequest()->getMethod() === 'POST';
2181 $out->addJsConfigVars( 'wgEditRecoveryWasPosted', $wasPosted );
2182 $out->addModules( 'mediawiki.editRecovery.edit' );
2183 }
2184
2185 # Enabled article-related sidebar, toplinks, etc.
2186 $out->setArticleRelated( true );
2187
2188 $contextTitle = $this->getContextTitle();
2189 if ( $this->isConflict ) {
2190 $msg = 'editconflict';
2191 } elseif ( $contextTitle->exists() && $this->section != '' ) {
2192 $msg = $this->section === 'new' ? 'editingcomment' : 'editingsection';
2193 } else {
2194 $msg = $contextTitle->exists()
2195 || $this->shadowPageLoader->get( $contextTitle )?->existsForEdit()
2196 ? 'editing'
2197 : 'creating';
2198 }
2199
2200 # Use the title defined by DISPLAYTITLE magic word when present
2201 $displayTitleParts = $this->mParserOutput?->getDisplayTitleParts();
2202 $displayTitle = Parser::formatPageTitle(
2203 # Title parts
2204 $displayTitleParts[0] ??
2205 str_replace( '_', ' ', $contextTitle->getNsText() ),
2206 $displayTitleParts[1] ?? ':',
2207 $displayTitleParts[2] ?? $contextTitle->getText(),
2208 # We're using the page language, but it might be more accurate
2209 # to use the variant in the case of a displaytitle.
2210 $contextTitle->getPageLanguage(),
2211 );
2212 if ( $displayTitleParts !== null ) {
2213 $out->setDisplayTitleParts( ...[
2214 ...$displayTitleParts,
2215 new HtmlArmor( $displayTitle ),
2216 ] );
2217 }
2218
2219 // Enclose the title with an element. This is used on live preview to update the
2220 // preview of the display title.
2221 $displayTitle = Html::rawElement( 'span', [ 'id' => 'firstHeadingTitle' ], $displayTitle );
2222
2223 $out->setPageTitleMsg( $this->context->msg( $msg )->rawParams( $displayTitle ) );
2224
2225 $config = $this->context->getConfig();
2226
2227 # Transmit the name of the message to JavaScript. This was added for live preview.
2228 # Live preview doesn't use this anymore. The variable is still transmitted because
2229 # Edit Recovery and user scripts use it.
2230 $out->addJsConfigVars( [
2231 'wgEditMessage' => $msg,
2232 ] );
2233
2234 // Add whether to use 'save' or 'publish' messages to JavaScript for post-edit, other
2235 // editors, etc.
2236 $out->addJsConfigVars(
2237 'wgEditSubmitButtonLabelPublish',
2238 $config->get( MainConfigNames::EditSubmitButtonLabelPublish )
2239 );
2240 }
2241
2245 private function showIntro(): void {
2246 $services = MediaWikiServices::getInstance();
2247
2248 // Hardcoded list of notices that are suppressable for historical reasons.
2249 // This feature was originally added for LiquidThreads, to avoid showing non-essential messages
2250 // when commenting in a thread, but some messages were included (or excluded) by mistake before
2251 // its implementation was moved to one place, and this list doesn't make a lot of sense.
2252 // TODO: Remove the suppressIntro feature from EditPage, and invent a better way for extensions
2253 // to skip individual intro messages.
2254 $skip = $this->suppressIntro ? [
2255 'editintro',
2256 'code-editing-intro',
2257 'sharedupload-desc-create',
2258 'sharedupload-desc-edit',
2259 'userpage-userdoesnotexist',
2260 'blocked-notice-logextract',
2261 'newarticletext',
2262 'newarticletextanon',
2263 'recreate-moveddeleted-warn',
2264 ] : [];
2265
2266 $messages = $services->getIntroMessageBuilder()->getIntroMessages(
2267 IntroMessageBuilder::MORE_FRAMES,
2268 $skip,
2269 $this->context,
2270 $this->page,
2271 $this->mArticle->fetchRevisionRecord(),
2272 $this->context->getUser(),
2273 $this->context->getRequest()->getVal( 'editintro' ),
2275 array_diff_key(
2276 $this->context->getRequest()->getQueryValues(),
2277 [ 'title' => true, 'returnto' => true, 'returntoquery' => true ]
2278 )
2279 ),
2280 !$this->firsttime,
2281 $this->section !== '' ? $this->section : null
2282 );
2283
2284 foreach ( $messages as $message ) {
2285 $this->context->getOutput()->addHTML( $message );
2286 }
2287 }
2288
2305 protected function toEditContent( $text ) {
2306 if ( $text === false || $text === null ) {
2307 return $text;
2308 }
2309
2310 $content = ContentHandler::makeContent( $text, $this->getTitle(),
2311 $this->contentModel, $this->contentFormat );
2312 $this->assertSupportedContent( $content );
2313 return $content;
2314 }
2315
2321 private function assertSupportedContent( Content $content ): void {
2322 if ( !$this->pageEditingHelper->isSupportedContentModel(
2323 $content->getModel(), $this->enableApiEditOverride
2324 ) ) {
2325 throw new NotDirectlyEditableException( 'This content model is not supported: ' . $content->getModel() );
2326 }
2327 }
2328
2332 public function showEditForm() {
2333 # need to parse the preview early so that we know which templates are used,
2334 # otherwise users with "show preview after edit box" will get a blank list
2335 # we parse this near the beginning so that setHeaders can do the title
2336 # setting work instead of leaving it in getPreviewText
2337 $previewOutput = '';
2338 if ( $this->formtype === 'preview' ) {
2339 $previewOutput = $this->getPreviewText();
2340 }
2341
2342 $out = $this->context->getOutput();
2343
2344 // FlaggedRevs depends on running this hook before adding edit notices in showIntro() (T337637)
2345 $this->getHookRunner()->onEditPage__showEditForm_initial( $this, $out );
2346
2347 $this->setHeaders();
2348
2349 // Show applicable editing introductions
2350 $this->showIntro();
2351
2352 if ( !$this->isConflict &&
2353 $this->section !== '' &&
2354 !$this->isSectionEditSupported()
2355 ) {
2356 // We use $this->section to much before this and getVal('wgSection') directly in other places
2357 // at this point we can't reset $this->section to '' to fallback to non-section editing.
2358 // Someone is welcome to try refactoring though
2359 $out->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
2360 return;
2361 }
2362
2363 $this->showHeader();
2364
2365 $out->addHTML( $this->editFormPageTop );
2366
2367 $user = $this->context->getUser();
2368 if ( $this->userOptionsLookup->getOption( $user, 'previewontop' ) ) {
2369 $this->displayPreviewArea( $previewOutput, true );
2370 }
2371
2372 $out->addHTML( $this->editFormTextTop );
2373
2374 // @todo add EditForm plugin interface and use it here!
2375 // search for textarea1 and textarea2, and allow EditForm to override all uses.
2376 $out->addHTML( Html::openElement(
2377 'form',
2378 [
2379 'class' => 'mw-editform',
2380 'id' => self::EDITFORM_ID,
2381 'name' => self::EDITFORM_ID,
2382 'method' => 'post',
2383 'action' => $this->getActionURL( $this->getContextTitle() ),
2384 'enctype' => 'multipart/form-data',
2385 'data-mw-editform-type' => $this->formtype
2386 ]
2387 ) );
2388
2389 // Add a check for Unicode support
2390 $out->addHTML( Html::hidden( 'wpUnicodeCheck', self::UNICODE_CHECK ) );
2391
2392 // Add an empty field to trip up spambots
2393 $out->addHTML(
2394 Html::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
2395 . Html::rawElement(
2396 'label',
2397 [ 'for' => 'wpAntispam' ],
2398 $this->context->msg( 'simpleantispam-label' )->parse()
2399 )
2400 . Html::element(
2401 'input',
2402 [
2403 'type' => 'text',
2404 'name' => 'wpAntispam',
2405 'id' => 'wpAntispam',
2406 'value' => ''
2407 ]
2408 )
2409 . Html::closeElement( 'div' )
2410 );
2411
2412 $this->getHookRunner()->onEditPage__showEditForm_fields( $this, $out );
2413
2414 // Put these up at the top to ensure they aren't lost on early form submission
2415 $this->showFormBeforeText();
2416
2417 # When the summary is hidden, also hide them on preview/show changes
2418 if ( $this->nosummary ) {
2419 $out->addHTML( Html::hidden( 'nosummary', true ) );
2420 }
2421
2422 # If a blank edit summary was previously provided and an edit constraint fails
2423 # because of it, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
2424 # user being bounced back more than once in the event that a summary
2425 # is not required.
2426 if ( $this->allowBlankSummary ) {
2427 $out->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
2428 }
2429
2430 if ( $this->undidRev ) {
2431 $out->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
2432 }
2433 if ( $this->undoAfter ) {
2434 $out->addHTML( Html::hidden( 'wpUndoAfter', $this->undoAfter ) );
2435 }
2436 if ( $this->recreate ) {
2437 $out->addHTML( Html::hidden( 'wpRecreate', $this->recreate ) );
2438 }
2439 if ( $this->ignoreRevisionDeletedWarning ) {
2440 $out->addHTML( Html::hidden( 'wpIgnoreRevisionDeleted', $this->ignoreRevisionDeletedWarning ) );
2441 }
2442
2443 if ( $this->problematicRedirectTarget !== null ) {
2444 // T395767, T395768: Save the target to a variable so the constraint can fail again if the redirect is
2445 // still problematic but has changed between two save attempts
2446 $out->addHTML( Html::hidden(
2447 'wpAllowedProblematicRedirectTarget',
2448 $this->problematicRedirectTarget->getFullText()
2449 ) );
2450 }
2451
2452 $autosumm = $this->autoSumm !== '' ? $this->autoSumm : md5( $this->summary );
2453 $out->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
2454
2455 $out->addHTML( Html::hidden( 'oldid', $this->oldid ) );
2456 $out->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
2457
2458 $out->addHTML( Html::hidden( 'format', $this->contentFormat ) );
2459 $out->addHTML( Html::hidden( 'model', $this->contentModel ) );
2460 if ( $this->changeTags ) {
2461 $out->addHTML( Html::hidden( 'wpChangeTagsAfterPreview', implode( ',', $this->changeTags ) ) );
2462 }
2463
2464 $out->enableOOUI();
2465
2466 if ( $this->section === 'new' ) {
2467 $this->showSummaryInput( true );
2468 $out->addHTML( $this->getSummaryPreview( true ) );
2469 }
2470
2471 $out->addHTML( $this->editFormTextBeforeContent );
2472 if ( $this->isConflict ) {
2473 $currentText = $this->pageEditingHelper->toEditText(
2474 $this->getCurrentContent(), $this->contentFormat, $this->enableApiEditOverride
2475 ) ?? '';
2476
2477 $editConflictHelper = $this->getEditConflictHelper();
2478 $editConflictHelper->setTextboxes( $this->textbox1, $currentText );
2479 $editConflictHelper->setContentModel( $this->contentModel );
2480 $editConflictHelper->setContentFormat( $this->contentFormat );
2481 $out->addHTML( $editConflictHelper->getEditFormHtmlBeforeContent() );
2482
2483 $this->textbox1 = $currentText;
2484 }
2485
2486 if ( !$this->getTitle()->isUserConfigPage() ) {
2487 $out->addHTML( self::getEditToolbar() ?? '' );
2488 }
2489
2490 if ( $this->blankArticle ) {
2491 $out->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
2492 }
2493
2494 if ( $this->isConflict ) {
2495 // In an edit conflict bypass the overridable content form method
2496 // and fallback to the raw wpTextbox1 since editconflicts can't be
2497 // resolved between page source edits and custom ui edits using the
2498 // custom edit ui.
2499 $conflictTextBoxAttribs = [];
2500 if ( $this->isOldRev ) {
2501 $conflictTextBoxAttribs['class'] = 'mw-textarea-oldrev';
2502 }
2503
2504 // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable
2505 // $editConflictHelper is declared, when isConflict is true
2506 $out->addHTML( $editConflictHelper->getEditConflictMainTextBox( $conflictTextBoxAttribs ) );
2507 // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable
2508 // $editConflictHelper is declared, when isConflict is true
2509 $out->addHTML( $editConflictHelper->getEditFormHtmlAfterContent() );
2510 } else {
2511 $this->showContentForm();
2512 }
2513
2514 $out->addHTML( $this->editFormTextAfterContent );
2515
2516 $this->showStandardInputs();
2517
2518 $this->showFormAfterText();
2519
2520 $this->showTosSummary();
2521
2522 $this->showEditTools();
2523
2524 $out->addHTML( $this->editFormTextAfterTools . "\n" );
2525
2526 $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
2527
2528 $out->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
2529 Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
2530
2531 $out->addHTML( Html::rawElement( 'div', [ 'class' => 'limitreport' ],
2532 self::getPreviewLimitReport( $this->mParserOutput ) ) );
2533
2534 $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
2535
2536 if ( $this->isConflict ) {
2537 try {
2538 $this->showConflict();
2539 } catch ( ContentSerializationException $ex ) {
2540 // this can't really happen, but be nice if it does.
2541 $out->addHTML( Html::errorBox(
2542 $this->context->msg(
2543 'content-failed-to-parse',
2544 $this->contentModel,
2545 $this->contentFormat,
2546 $ex->getMessage()
2547 )->parse()
2548 ) );
2549 }
2550 }
2551
2552 // Set a hidden field so JS knows what edit form mode we are in
2553 if ( $this->isConflict ) {
2554 $mode = 'conflict';
2555 } elseif ( $this->preview ) {
2556 $mode = 'preview';
2557 } elseif ( $this->diff ) {
2558 $mode = 'diff';
2559 } else {
2560 $mode = 'text';
2561 }
2562 $out->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
2563
2564 // Marker for detecting truncated form data. This must be the last
2565 // parameter sent in order to be of use, so do not move me.
2566 $out->addHTML( Html::hidden( 'wpUltimateParam', true ) );
2567 $out->addHTML( $this->editFormTextBottom . "\n</form>\n" );
2568
2569 if ( !$this->userOptionsLookup->getOption( $user, 'previewontop' ) ) {
2570 $this->displayPreviewArea( $previewOutput, false );
2571 }
2572 }
2573
2581 public function makeTemplatesOnThisPageList( array $templates ): string {
2582 $templateListFormatter = new TemplatesOnThisPageFormatter(
2583 $this->context,
2584 $this->linkRenderer,
2585 $this->linkBatchFactory,
2586 $this->restrictionStore
2587 );
2588
2589 // preview if preview, else section if section, else false
2590 $type = false;
2591 if ( $this->preview ) {
2592 $type = 'preview';
2593 } elseif ( $this->section !== '' ) {
2594 $type = 'section';
2595 }
2596
2597 return Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
2598 $templateListFormatter->format( $templates, $type )
2599 );
2600 }
2601
2605 private static function extractSectionTitle( string $text ): string|false {
2606 if ( preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches ) ) {
2607 return MediaWikiServices::getInstance()->getParser()
2608 ->stripSectionName( trim( $matches[2] ) );
2609 } else {
2610 return false;
2611 }
2612 }
2613
2614 private function showHeader(): void {
2615 $out = $this->context->getOutput();
2616 if ( $this->isConflict ) {
2617 $this->addExplainConflictHeader();
2618 $this->editRevId = $this->page->getLatest();
2619 } else {
2620 if ( $this->section !== '' && $this->section !== 'new' && $this->summary === '' &&
2621 !$this->preview && !$this->diff
2622 ) {
2623 // https://github.com/phan/phan/issues/5514
2624 // @phan-suppress-next-line PhanImpossibleTypeComparison
2625 if ( $this->section === '0' ) {
2626 $this->summary = "/* */ ";
2627 } else {
2628 $sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object
2629 if ( $sectionTitle !== false && $sectionTitle !== '' ) {
2630 $this->summary = "/* $sectionTitle */ ";
2631 }
2632 }
2633 }
2634
2635 if ( $this->section != 'new' ) {
2636 $revRecord = $this->mArticle->fetchRevisionRecord();
2637 if ( $revRecord instanceof RevisionStoreRecord && !$revRecord->isCurrent() ) {
2638 $this->mArticle->setOldSubtitle( $revRecord->getId() );
2639 $this->isOldRev = true;
2640 }
2641 }
2642 }
2643
2644 if ( $this->formtype !== 'save' ) {
2646 $constraintFactory = MediaWikiServices::getInstance()->getService( '_EditConstraintFactory' );
2647 $constraintRunner = new EditConstraintRunner(
2648 $constraintFactory->newAccidentalRecreationConstraint(
2649 $this->getTitle(),
2650 // Ignore wpRedirect so the warning is still shown after a save attempt
2651 false,
2652 $this->starttime,
2653 ),
2654 new RevisionDeletedConstraint(
2655 false,
2656 $this->oldid,
2657 $this->mArticle->fetchRevisionRecord(),
2658 $this->section,
2659 $this->getTitle(),
2660 $this->context->getUser(),
2661 ),
2662 );
2663
2664 // No call to $this->handleFailedConstraint() here to avoid setting wpRedirect
2665 $out->addHTML( $this->formatConstraintStatus( $constraintRunner->checkAllConstraints() ) );
2666 }
2667
2668 $this->addLongPageWarningHeader();
2669 }
2670
2675 private function getSummaryInputAttributes( array $inputAttrs ): array {
2676 // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
2677 // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
2678 // Unicode codepoints.
2679 return $inputAttrs + [
2680 'id' => 'wpSummary',
2681 'name' => 'wpSummary',
2682 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
2683 'tabindex' => 1,
2684 'size' => 60,
2685 'spellcheck' => 'true',
2686 ];
2687 }
2688
2698 private function getSummaryInputWidget( $summary, string $labelText, array $inputAttrs ): FieldLayout {
2699 $inputAttrs = OOUI\Element::configFromHtmlAttributes(
2700 $this->getSummaryInputAttributes( $inputAttrs )
2701 );
2702 $inputAttrs += [
2703 'title' => Linker::titleAttrib( 'summary' ),
2704 'accessKey' => Linker::accesskey( 'summary' ),
2705 ];
2706
2707 // For compatibility with old scripts and extensions, we want the legacy 'id' on the `<input>`
2708 $inputAttrs['inputId'] = $inputAttrs['id'];
2709 $inputAttrs['id'] = 'wpSummaryWidget';
2710
2711 return new OOUI\FieldLayout(
2712 new OOUI\TextInputWidget( [
2713 'value' => $summary,
2714 'infusable' => true,
2715 ] + $inputAttrs ),
2716 [
2717 'label' => new OOUI\HtmlSnippet( $labelText ),
2718 'align' => 'top',
2719 'id' => 'wpSummaryLabel',
2720 'classes' => [ $this->missingSummary ? 'mw-summarymissed' : 'mw-summary' ],
2721 ]
2722 );
2723 }
2724
2730 private function showSummaryInput( bool $isSubjectPreview ): void {
2731 # Add a class if 'missingsummary' is triggered to allow styling of the summary line
2732 $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
2733 if ( $isSubjectPreview ) {
2734 if ( $this->nosummary ) {
2735 return;
2736 }
2737 } elseif ( !$this->mShowSummaryField ) {
2738 return;
2739 }
2740
2741 $labelText = $this->context->msg( $isSubjectPreview ? 'subject' : 'summary' )->parse();
2742 $this->context->getOutput()->addHTML(
2743 $this->getSummaryInputWidget(
2744 $isSubjectPreview ? $this->sectiontitle : $this->summary,
2745 $labelText,
2746 [ 'class' => $summaryClass ]
2747 )->toString()
2748 );
2749 }
2750
2756 private function getSummaryPreview( bool $isSubjectPreview ): string {
2757 // avoid spaces in preview, gets always trimmed on save
2758 $summary = trim( $this->summary );
2759 if ( $summary === '' || ( !$this->preview && !$this->diff ) ) {
2760 return "";
2761 }
2762
2763 $commentFormatter = MediaWikiServices::getInstance()->getCommentFormatter();
2764 $summary = $this->context->msg( 'summary-preview' )->parse()
2765 . $commentFormatter->formatBlock( $summary, $this->getTitle(), $isSubjectPreview );
2766 return Html::rawElement( 'div', [ 'class' => 'mw-summary-preview' ], $summary );
2767 }
2768
2769 private function showFormBeforeText(): void {
2770 $out = $this->context->getOutput();
2771 $out->addHTML( Html::hidden( 'wpSection', $this->section ) );
2772 $out->addHTML( Html::hidden( 'wpStarttime', $this->starttime ) );
2773 $out->addHTML( Html::hidden( 'wpEdittime', $this->edittime ) );
2774 $out->addHTML( Html::hidden( 'editRevId', $this->editRevId ) );
2775 $out->addHTML( Html::hidden( 'wpScrolltop', $this->scrolltop, [ 'id' => 'wpScrolltop' ] ) );
2776 }
2777
2778 protected function showFormAfterText() {
2791 $this->context->getOutput()->addHTML(
2792 "\n" .
2793 Html::hidden( "wpEditToken", $this->context->getUser()->getEditToken() ) .
2794 "\n"
2795 );
2796 }
2797
2806 protected function showContentForm() {
2807 $this->showTextbox1();
2808 }
2809
2810 private function showTextbox1(): void {
2811 $classes = $this->textboxBuilder->getTextboxProtectionCSSClasses( $this->getTitle() );
2812
2813 # Is an old revision being edited?
2814 if ( $this->isOldRev ) {
2815 $classes[] = 'mw-textarea-oldrev';
2816 }
2817
2818 $attribs = [
2819 'aria-label' => $this->context->msg( 'edit-textarea-aria-label' )->text(),
2820 'tabindex' => 1,
2821 'class' => $classes,
2822 ];
2823
2824 $this->showTextbox(
2825 $this->textbox1,
2826 'wpTextbox1',
2827 $attribs
2828 );
2829 }
2830
2831 protected function showTextbox( string $text, string $name, array $customAttribs = [] ): void {
2832 $attribs = $this->textboxBuilder->buildTextboxAttribs(
2833 $name,
2834 $customAttribs,
2835 $this->context->getUser(),
2836 $this->page
2837 );
2838
2839 $this->context->getOutput()->addHTML(
2840 Html::textarea( $name, $this->textboxBuilder->addNewLineAtEnd( $text ), $attribs )
2841 );
2842 }
2843
2844 private function displayPreviewArea( string $previewOutput, bool $isOnTop ): void {
2845 $attribs = [ 'id' => 'wikiPreview' ];
2846 if ( $isOnTop ) {
2847 $attribs['class'] = 'ontop';
2848 }
2849 if ( $this->formtype !== 'preview' ) {
2850 $attribs['style'] = 'display: none;';
2851 }
2852
2853 $out = $this->context->getOutput();
2854 $out->addHTML( Html::openElement( 'div', $attribs ) );
2855
2856 if ( $this->formtype === 'preview' ) {
2857 $this->showPreview( $previewOutput );
2858 }
2859
2860 $out->addHTML( '</div>' );
2861
2862 if ( $this->formtype === 'diff' ) {
2863 try {
2864 $this->showDiff();
2865 } catch ( ContentSerializationException $ex ) {
2866 $out->addHTML( Html::errorBox(
2867 $this->context->msg(
2868 'content-failed-to-parse',
2869 $this->contentModel,
2870 $this->contentFormat,
2871 $ex->getMessage()
2872 )->parse()
2873 ) );
2874 }
2875 }
2876 }
2877
2884 private function showPreview( string $text ): void {
2885 if ( $this->mArticle instanceof CategoryPage ) {
2886 $this->mArticle->openShowCategory();
2887 }
2888 # This hook seems slightly odd here, but makes things more
2889 # consistent for extensions.
2890 $out = $this->context->getOutput();
2891 $this->getHookRunner()->onOutputPageBeforeHTML( $out, $text );
2892 $out->addHTML( $text );
2893 if ( $this->mArticle instanceof CategoryPage ) {
2894 $this->mArticle->closeShowCategory();
2895 }
2896 }
2897
2905 public function showDiff(): void {
2906 $oldtitlemsg = 'currentrev';
2907 # if page does not exist, show diff against the preloaded default
2908 if ( !$this->page->exists() ) {
2909 $oldShadow = $this->shadowPageLoader->get( $this->getTitle() );
2910 $oldContent = $oldShadow?->getPreloadContent();
2911 if ( $oldContent ) {
2912 $this->assertSupportedContent( $oldContent );
2913 $oldtitlemsg = $oldShadow->getDiffTitleMessage() ?? $oldtitlemsg;
2914 }
2915 } else {
2916 $oldContent = $this->getCurrentContent();
2917 }
2918
2919 $textboxContent = $this->toEditContent( $this->textbox1 );
2920 if ( $this->editRevId !== null ) {
2921 $newContent = $this->page->replaceSectionAtRev(
2922 $this->section, $textboxContent, $this->sectiontitle, $this->editRevId
2923 );
2924 } else {
2925 $newContent = $this->page->replaceSectionContent(
2926 $this->section, $textboxContent, $this->sectiontitle, $this->edittime
2927 );
2928 }
2929
2930 if ( $newContent ) {
2931 $this->getHookRunner()->onEditPageGetDiffContent( $this, $newContent );
2932
2933 $user = $this->getUserForPreview();
2934 $parserOptions = ParserOptions::newFromUserAndLang( $user,
2935 MediaWikiServices::getInstance()->getContentLanguage() );
2936 $services = MediaWikiServices::getInstance();
2937 $contentTransformer = $services->getContentTransformer();
2938 $newContent = $contentTransformer->preSaveTransform( $newContent, $this->page, $user, $parserOptions );
2939 }
2940
2941 if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
2942 $oldtitle = $this->context->msg( $oldtitlemsg )->parse();
2943 $newtitle = $this->context->msg( 'yourtext' )->parse();
2944
2945 if ( !$oldContent ) {
2946 $oldContent = $newContent->getContentHandler()->makeEmptyContent();
2947 }
2948
2949 if ( !$newContent ) {
2950 $newContent = $oldContent->getContentHandler()->makeEmptyContent();
2951 }
2952
2953 $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->context );
2954 $de->setContent( $oldContent, $newContent );
2955
2956 $difftext = $de->getDiff( $oldtitle, $newtitle );
2957 $de->showDiffStyle();
2958 } else {
2959 $difftext = '';
2960 }
2961
2962 $this->context->getOutput()->addHTML( Html::rawElement( 'div', [ 'id' => 'wikiDiff' ], $difftext ) );
2963 }
2964
2973 private function showTosSummary(): void {
2974 $msgKey = 'editpage-tos-summary';
2975 $this->getHookRunner()->onEditPageTosSummary( $this->getTitle(), $msgKey );
2976 $msg = $this->context->msg( $msgKey );
2977 if ( !$msg->isDisabled() ) {
2978 $this->context->getOutput()->addHTML( Html::rawElement(
2979 'div',
2980 [ 'class' => 'mw-tos-summary' ],
2981 $msg->parseAsBlock()
2982 ) );
2983 }
2984 }
2985
2990 private function showEditTools(): void {
2991 $this->context->getOutput()->addHTML( Html::rawElement(
2992 'div',
2993 [ 'class' => 'mw-editTools' ],
2994 $this->context->msg( 'edittools' )->inContentLanguage()->parse()
2995 ) );
2996 }
2997
3006 public static function getCopyrightWarning(
3007 PageReference $page,
3008 string $format,
3009 MessageLocalizer $localizer
3010 ): string {
3011 $services = MediaWikiServices::getInstance();
3012 $rightsText = $services->getMainConfig()->get( MainConfigNames::RightsText );
3013 if ( $rightsText ) {
3014 $copywarnMsg = [ 'copyrightwarning',
3015 '[[' . $localizer->msg( 'copyrightpage' )->inContentLanguage()->text() . ']]',
3016 $rightsText ];
3017 } else {
3018 $copywarnMsg = [ 'copyrightwarning2',
3019 '[[' . $localizer->msg( 'copyrightpage' )->inContentLanguage()->text() . ']]' ];
3020 }
3021 // Allow for site and per-namespace customization of contribution/copyright notice.
3022 $title = Title::newFromPageReference( $page );
3023 ( new HookRunner( $services->getHookContainer() ) )->onEditPageCopyrightWarning( $title, $copywarnMsg );
3024 if ( !$copywarnMsg ) {
3025 return '';
3026 }
3027
3028 $msg = $localizer->msg( ...$copywarnMsg )->page( $page );
3029 return Html::rawElement( 'div', [ 'id' => 'editpage-copywarn' ], $msg->$format() );
3030 }
3031
3039 public static function getPreviewLimitReport( ?ParserOutput $output = null ): string {
3040 if ( !$output || !$output->getLimitReportData() ) {
3041 return '';
3042 }
3043
3044 $limitReport = Html::rawElement( 'div', [ 'class' => 'mw-limitReportExplanation' ],
3045 wfMessage( 'limitreport-title' )->parseAsBlock()
3046 );
3047
3048 // Show/hide animation doesn't work correctly on a table, so wrap it in a div.
3049 $limitReport .= Html::openElement( 'div', [ 'class' => 'preview-limit-report-wrapper' ] );
3050
3051 $limitReport .= Html::openElement( 'table', [
3052 'class' => 'preview-limit-report wikitable'
3053 ] ) .
3054 Html::openElement( 'tbody' );
3055
3056 $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
3057 foreach ( $output->getLimitReportData() as $key => $value ) {
3058 if ( in_array( $key, [
3059 'cachereport-origin',
3060 'cachereport-timestamp',
3061 'cachereport-ttl',
3062 'cachereport-transientcontent',
3063 'limitreport-timingprofile',
3064 ] ) ) {
3065 // These entries have non-numeric parameters, and can't be displayed by this code.
3066 // They are used by the plaintext limit report (see RenderDebugInfo::debugInfo()).
3067 // TODO: Display this information in the table somehow.
3068 continue;
3069 }
3070
3071 if ( $hookRunner->onParserLimitReportFormat( $key, $value, $limitReport, true, true ) ) {
3072 $keyMsg = wfMessage( $key );
3073 $valueMsg = wfMessage( "$key-value" );
3074 if ( !$valueMsg->exists() ) {
3075 // This is formatted raw, not as localized number.
3076 // If you want the parameter formatted as a number,
3077 // define the `$key-value` message.
3078 $valueMsg = ( new RawMessage( '$1' ) )->params( $value );
3079 } else {
3080 // If you define the `$key-value` or `$key-value-html`
3081 // message then the argument *must* be numeric.
3082 $valueMsg = $valueMsg->numParams( $value );
3083 }
3084 if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
3085 $limitReport .= Html::rawElement( 'tr', [],
3086 Html::rawElement( 'th', [], $keyMsg->parse() ) .
3087 Html::rawElement( 'td', [], $valueMsg->parse() )
3088 );
3089 }
3090 }
3091 }
3092
3093 $limitReport .= Html::closeElement( 'tbody' ) .
3094 Html::closeElement( 'table' ) .
3095 Html::closeElement( 'div' );
3096
3097 return $limitReport;
3098 }
3099
3100 protected function showStandardInputs( int &$tabindex = 2 ): void {
3101 $out = $this->context->getOutput();
3102 $out->addHTML( "<div class='editOptions'>\n" );
3103
3104 if ( $this->section !== 'new' ) {
3105 $this->showSummaryInput( false );
3106 $out->addHTML( $this->getSummaryPreview( false ) );
3107 }
3108
3109 // When previewing, override the selected dropdown option to select whatever was posted
3110 // (if it's a valid option) rather than the current value for watchlistExpiry.
3111 // See also above in $this->importFormDataPosted().
3112 $expiryFromRequest = null;
3113 if ( $this->preview || $this->diff || $this->isConflict ) {
3114 $expiryFromRequest = $this->getContext()->getRequest()->getText( 'wpWatchlistExpiry' );
3115 }
3116
3117 $checkboxes = $this->getCheckboxesWidget(
3118 $tabindex,
3119 [ 'minor' => $this->minoredit, 'watch' => $this->watchthis, 'wpWatchlistExpiry' => $expiryFromRequest ]
3120 );
3121 $watchlistLabelsWidget = $this->getWatchlistLabelsWidget( $tabindex );
3122 $watchlistLabelClass = 'mw-watchlistlabels-component';
3123 // Add a message (inline with the checkboxes) when the user doesn't have any labels.
3124 if ( $this->context->getUser()->isNamed() && !$watchlistLabelsWidget && $this->watchlistLabelsEnabled ) {
3125 $checkboxes['watchlistlabels'] = new OOUI\LabelWidget( [
3126 'label' => new OOUI\HtmlSnippet( $this->context->msg( 'watchlistlabels-editpage-nolabels' )->parse() ),
3127 'classes' => [ $watchlistLabelClass ],
3128 ] );
3129 }
3130 $checkboxesHTML = new OOUI\HorizontalLayout( [ 'items' => array_values( $checkboxes ) ] );
3131
3132 $out->addHTML( Html::rawElement( 'div', [ 'class' => 'editCheckboxes' ], (string)$checkboxesHTML ) . "\n" );
3133 if ( $watchlistLabelsWidget ) {
3134 $out->addHTML( Html::rawElement(
3135 'div',
3136 [ 'class' => $watchlistLabelClass . ' mw-editpage-watchlistLabels' ],
3137 Html::hidden( 'wpWatchlistLabelsSubmitted', 1 ) . (string)$watchlistLabelsWidget
3138 ) . "\n" );
3139 }
3140
3141 // Show copyright warning.
3142 $out->addHTML( self::getCopyrightWarning( $this->page, 'parse', $this->context ) );
3143 $out->addHTML( $this->editFormTextAfterWarn );
3144
3145 $out->addHTML( "<div class='editButtons'>\n" );
3146 $out->addHTML( implode( "\n", $this->getEditButtons( $tabindex ) ) . "\n" );
3147
3148 $cancel = $this->getCancelLink( $tabindex++ );
3149
3150 $edithelp = $this->getHelpLink() .
3151 $this->context->msg( 'word-separator' )->escaped() .
3152 $this->context->msg( 'newwindow' )->parse();
3153
3154 $out->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" );
3155 $out->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" );
3156 $out->addHTML( "</div><!-- editButtons -->\n" );
3157
3158 $this->getHookRunner()->onEditPage__showStandardInputs_options( $this, $out, $tabindex );
3159
3160 $out->addHTML( "</div><!-- editOptions -->\n" );
3161 }
3162
3167 private function showConflict(): void {
3168 $out = $this->context->getOutput();
3169 if ( $this->getHookRunner()->onEditPageBeforeConflictDiff( $this, $out ) ) {
3170 $this->incrementConflictStats();
3171
3172 $this->getEditConflictHelper()->showEditFormTextAfterFooters();
3173 }
3174 }
3175
3176 private function incrementConflictStats(): void {
3177 $this->getEditConflictHelper()->incrementConflictStats( $this->context->getUser() );
3178 }
3179
3180 private function getHelpLink(): string {
3181 $message = $this->context->msg( 'edithelppage' )->inContentLanguage()->text();
3182 $editHelpUrl = Skin::makeInternalOrExternalUrl( $message );
3183 return Html::element( 'a', [
3184 'href' => $editHelpUrl,
3185 'target' => 'helpwindow'
3186 ], $this->context->msg( 'edithelp' )->text() );
3187 }
3188
3193 private function getCancelLink( int $tabindex ): ButtonWidget {
3194 $cancelParams = [];
3195 if ( !$this->isConflict && $this->oldid > 0 ) {
3196 $cancelParams['oldid'] = $this->oldid;
3197 } elseif ( $this->getContextTitle()->isRedirect() ) {
3198 $cancelParams['redirect'] = 'no';
3199 }
3200
3201 return new OOUI\ButtonWidget( [
3202 'id' => 'mw-editform-cancel',
3203 'tabIndex' => $tabindex,
3204 'href' => $this->getContextTitle()->getLinkURL( $cancelParams ),
3205 'label' => new OOUI\HtmlSnippet( $this->context->msg( 'cancel' )->parse() ),
3206 'framed' => false,
3207 'infusable' => true,
3208 'flags' => 'destructive',
3209 ] );
3210 }
3211
3221 protected function getActionURL( Title $title ) {
3222 $request = $this->context->getRequest();
3223 $params = $request->getQueryValuesOnly();
3224
3225 $allowedFormParams = [
3226 'section', 'oldid', 'preloadtitle', 'undo', 'undoafter',
3227 // Considered safe in all contexts
3228 'uselang', 'useskin', 'useformat', 'variant', 'debug', 'safemode'
3229 ];
3230 $formParams = [ 'action' => 'submit' ];
3231 foreach ( $params as $arg => $val ) {
3232 if ( in_array( $arg, $allowedFormParams, true ) ) {
3233 $formParams[$arg] = $val;
3234 }
3235 }
3236
3237 return $title->getLocalURL( $formParams );
3238 }
3239
3245 public function getPreviewText() {
3246 $out = $this->context->getOutput();
3247 $config = $this->context->getConfig();
3248
3249 if ( $config->get( MainConfigNames::RawHtml ) && !$this->mTokenOk ) {
3250 // Could be an offsite preview attempt. This is very unsafe if
3251 // HTML is enabled, as it could be an attack.
3252 $parsedNote = '';
3253 if ( $this->textbox1 !== '' ) {
3254 // Do not put big scary notice, if previewing the empty
3255 // string, which happens when you initially edit
3256 // a category page, due to automatic preview-on-open.
3257 $parsedNote = Html::rawElement( 'div', [ 'class' => 'previewnote' ],
3258 $out->parseAsInterface(
3259 $this->context->msg( 'session_fail_preview_html' )->plain()
3260 ) );
3261 }
3262 $this->incrementEditFailureStats( 'session_loss' );
3263 return $parsedNote;
3264 }
3265
3266 $previewStatus = StatusValue::newGood();
3267 $previewNoteHtml = '';
3268
3269 try {
3270 $content = $this->toEditContent( $this->textbox1 );
3271
3272 $previewHTML = '';
3273 if ( !$this->getHookRunner()->onAlternateEditPreview(
3274 $this, $content, $previewHTML, $this->mParserOutput )
3275 ) {
3276 return $previewHTML;
3277 }
3278
3279 $continueEditingHtml = Html::rawElement(
3280 'span',
3281 [ 'class' => 'mw-continue-editing' ],
3282 $this->linkRenderer->makePreloadedLink(
3283 new TitleValue( NS_MAIN, '', self::EDITFORM_ID ),
3284 $this->context->getLanguage()->getArrow() . ' ' . $this->context->msg( 'continue-editing' )->text()
3285 )
3286 );
3287
3288 if ( $this->mTriedSave && !$this->mTokenOk ) {
3289 $previewStatus->fatal( 'session_fail_preview' );
3290 $this->incrementEditFailureStats( 'session_loss' );
3291 } elseif ( $this->incompleteForm ) {
3292 $previewStatus->fatal( 'edit_form_incomplete' );
3293 if ( $this->mTriedSave ) {
3294 $this->incrementEditFailureStats( 'incomplete_form' );
3295 }
3296 } else {
3297 $previewNoteHtml = Html::noticeBox(
3298 $this->context->msg( 'previewnote' )->parse() . ' ' . $continueEditingHtml
3299 );
3300 }
3301
3302 # don't parse non-wikitext pages, show message about preview
3303 if ( $this->getTitle()->isUserConfigPage() || $this->getTitle()->isSiteConfigPage() ) {
3304 if ( $this->getTitle()->isUserConfigPage() ) {
3305 $level = 'user';
3306 } elseif ( $this->getTitle()->isSiteConfigPage() ) {
3307 $level = 'site';
3308 } else {
3309 $level = false;
3310 }
3311
3312 if ( $content->getModel() === CONTENT_MODEL_CSS ) {
3313 $format = 'css';
3314 if ( $level === 'user' && !$config->get( MainConfigNames::AllowUserCss ) ) {
3315 $format = false;
3316 }
3317 } elseif ( $content->getModel() === CONTENT_MODEL_JSON ) {
3318 $format = 'json';
3319 if ( $level === 'user' /* No comparable 'AllowUserJson' */ ) {
3320 $format = false;
3321 }
3322 } elseif ( $content->getModel() === CONTENT_MODEL_JAVASCRIPT ) {
3323 $format = 'js';
3324 if ( $level === 'user' && !$config->get( MainConfigNames::AllowUserJs ) ) {
3325 $format = false;
3326 }
3327 } elseif ( $content->getModel() === CONTENT_MODEL_VUE ) {
3328 $format = 'vue';
3329 if ( $level === 'user' && !$config->get( MainConfigNames::AllowUserJs ) ) {
3330 $format = false;
3331 }
3332 } else {
3333 $format = false;
3334 }
3335
3336 # Used messages to make sure grep find them:
3337 # Messages: usercsspreview, userjsonpreview, userjspreview, uservuepreview,
3338 # sitecsspreview, sitejsonpreview, sitejspreview, sitevuepreview
3339 if ( $level && $format ) {
3340 $previewNoteHtml = Html::noticeBox( Html::rawElement(
3341 'div',
3342 [ 'id' => "mw-{$level}{$format}preview" ],
3343 $this->context->msg( "{$level}{$format}preview" )->parse() . $continueEditingHtml
3344 ) );
3345 }
3346 }
3347
3348 if ( $this->section === "new" ) {
3349 $content = $content->addSectionHeader( $this->sectiontitle );
3350 }
3351
3352 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
3353 $this->getHookRunner()->onEditPageGetPreviewContent( $this, $content );
3354
3355 $parserResult = $this->doPreviewParse( $content );
3356 $parserOutput = $parserResult['parserOutput'];
3357 $previewHTML = $parserResult['html'];
3358 $this->mParserOutput = $parserOutput;
3359 $out->addParserOutputMetadata( $parserOutput );
3360 if ( $out->userCanPreview() ) {
3361 $out->addContentOverride( $this->getTitle(), $content );
3362 }
3363
3364 // T394016 - Run some edit constraints on page preview
3366 $constraintFactory = MediaWikiServices::getInstance()->getService( '_EditConstraintFactory' );
3367 $constraintRunner = new EditConstraintRunner();
3368
3369 $constraintRunner->addConstraint( $constraintFactory->newRedirectConstraint(
3370 null,
3371 $content,
3372 null,
3373 $this->getTitle(),
3374 MessageValue::new( 'edit-constraint-warning-wrapper' ),
3375 $this->contentFormat,
3376 ) );
3377
3378 $previewStatus->merge( $constraintRunner->checkAllConstraints() );
3379
3380 foreach ( $parserOutput->getWarningMsgs() as $warning ) {
3381 $previewStatus->warning( $warning );
3382 }
3383 } catch ( ContentSerializationException $ex ) {
3384 $previewStatus->fatal(
3385 'content-failed-to-parse',
3386 $this->contentModel,
3387 $this->contentFormat,
3388 $ex->getMessage()
3389 );
3390 $previewHTML = '';
3391 }
3392
3393 if ( $this->isConflict ) {
3394 $conflict = Html::warningBox(
3395 $this->context->msg( 'previewconflict' )->escaped(),
3396 'mw-previewconflict'
3397 );
3398 } else {
3399 $conflict = '';
3400 }
3401
3402 $previewhead = Html::rawElement(
3403 'div', [ 'class' => 'previewnote' ],
3404 Html::element(
3405 'h2', [ 'id' => 'mw-previewheader' ],
3406 $this->context->msg( 'preview' )->text()
3407 ) . $this->formatConstraintStatus( $previewStatus ) . $previewNoteHtml . $conflict
3408 );
3409
3410 return $previewhead . $previewHTML . $this->previewTextAfterContent;
3411 }
3412
3413 private function incrementEditFailureStats( string $failureType ): void {
3414 MediaWikiServices::getInstance()->getStatsFactory()
3415 ->getCounter( 'edit_failure_total' )
3416 ->setLabel( 'cause', $failureType )
3417 ->setLabel( 'namespace', 'n/a' )
3418 ->setLabel( 'user_bucket', 'n/a' )
3419 ->increment();
3420 }
3421
3426 $parserOptions = $this->mArticle->getParserOptions();
3427 $parserOptions->setRenderReason( 'page-preview' );
3428 $parserOptions->setIsPreview( true );
3429 $parserOptions->setIsSectionPreview( $this->section !== '' );
3430 $parserOptions->setSuppressSectionEditLinks();
3431
3432 // XXX: we could call $parserOptions->setCurrentRevisionRecordCallback here to force the
3433 // current revision to be null during PST, until setupFakeRevision is called on
3434 // the ParserOptions. Currently, we rely on Parser::getRevisionRecordObject() to ignore
3435 // existing revisions in preview mode.
3436
3437 return $parserOptions;
3438 }
3439
3449 protected function doPreviewParse( Content $content ) {
3450 $user = $this->getUserForPreview();
3451 $parserOptions = $this->getPreviewParserOptions();
3452
3453 // NOTE: preSaveTransform doesn't have a fake revision to operate on.
3454 // Parser::getRevisionRecordObject() will return null in preview mode,
3455 // causing the context user to be used for {{subst:REVISIONUSER}}.
3456 // XXX: Alternatively, we could also call setupFakeRevision()
3457 // before PST with $content.
3458 $services = MediaWikiServices::getInstance();
3459 $contentTransformer = $services->getContentTransformer();
3460 $contentRenderer = $services->getContentRenderer();
3461 $pstContent = $contentTransformer->preSaveTransform( $content, $this->page, $user, $parserOptions );
3462 $parserOutput = $contentRenderer->getParserOutput( $pstContent, $this->page, null, $parserOptions );
3463 $out = $this->context->getOutput();
3464 $skin = $out->getSkin();
3465 $skinOptions = $skin->getOptions();
3466 // TODO T371004 move runOutputPipeline out of $parserOutput
3467 // TODO T371022 ideally we clone here, but for now let's reproduce getText behaviour
3468 $oldHtml = $parserOutput->getContentHolderText();
3469 $html = $parserOutput->runOutputPipeline( $parserOptions, [
3470 'allowClone' => 'false',
3471 'userLang' => $skin->getLanguage(),
3472 'injectTOC' => $skinOptions['toc'],
3473 'includeDebugInfo' => true,
3474 ] )->getContentHolderText();
3475 $parserOutput->setContentHolderText( $oldHtml );
3476 return [
3477 'parserOutput' => $parserOutput,
3478 'html' => $html
3479 ];
3480 }
3481
3485 public function getTemplates() {
3486 if ( $this->preview || $this->section !== '' ) {
3487 $templates = [];
3488 if ( !$this->mParserOutput ) {
3489 return $templates;
3490 }
3491 foreach (
3492 $this->mParserOutput->getLinkList( ParserOutputLinkTypes::TEMPLATE )
3493 as [ 'link' => $link ]
3494 ) {
3495 $templates[] = Title::newFromLinkTarget( $link );
3496 }
3497 return $templates;
3498 } else {
3499 return $this->getTitle()->getTemplateLinksFrom();
3500 }
3501 }
3502
3506 public static function getEditToolbar(): ?string {
3507 $startingToolbar = '<div id="toolbar"></div>';
3508 $toolbar = $startingToolbar;
3509
3510 $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
3511 if ( !$hookRunner->onEditPageBeforeEditToolbar( $toolbar ) ) {
3512 return null;
3513 }
3514 // Don't add a pointless `<div>` to the page unless a hook caller populated it
3515 return ( $toolbar === $startingToolbar ) ? null : $toolbar;
3516 }
3517
3543 public function getCheckboxesDefinition( $values ): array {
3544 $checkboxes = [];
3545
3546 $user = $this->context->getUser();
3547 // don't show the minor edit checkbox if it's a new page or section
3548 if ( !$this->isNew && $this->permManager->userHasRight( $user, 'minoredit' ) ) {
3549 $checkboxes['wpMinoredit'] = [
3550 'id' => 'wpMinoredit',
3551 'label-message' => 'minoredit',
3552 // Uses messages: tooltip-minoredit, accesskey-minoredit
3553 'tooltip' => 'minoredit',
3554 'label-id' => 'mw-editpage-minoredit',
3555 'legacy-name' => 'minor',
3556 'default' => $values['minor'],
3557 ];
3558 }
3559
3560 if ( $user->isNamed() ) {
3561 $checkboxes = array_merge(
3562 $checkboxes,
3563 $this->getCheckboxesDefinitionForWatchlist( $values['watch'], $values['wpWatchlistExpiry'] ?? null )
3564 );
3565 }
3566
3567 $this->getHookRunner()->onEditPageGetCheckboxesDefinition( $this, $checkboxes );
3568
3569 return $checkboxes;
3570 }
3571
3579 private function getCheckboxesDefinitionForWatchlist( $watch, $watchexpiry ): array {
3580 $fieldDefs = [
3581 'wpWatchthis' => [
3582 'id' => 'wpWatchthis',
3583 'label-message' => 'watchthis',
3584 // Uses messages: tooltip-watch, accesskey-watch
3585 'tooltip' => 'watch',
3586 'label-id' => 'mw-editpage-watch',
3587 'legacy-name' => 'watch',
3588 'default' => $watch,
3589 ]
3590 ];
3591 if ( $this->watchlistExpiryEnabled ) {
3592 $watchedItem = $this->watchedItemStore->getWatchedItem( $this->getContext()->getUser(), $this->getTitle() );
3593 if ( $watchedItem instanceof WatchedItem && $watchedItem->getExpiry() === null ) {
3594 // Not temporarily watched, so we always default to infinite.
3595 $userPreferredExpiry = 'infinite';
3596 } else {
3597 $userPreferredExpiryOption = !$this->page->exists()
3598 ? 'watchcreations-expiry'
3599 : 'watchdefault-expiry';
3600 $userPreferredExpiry = $this->userOptionsLookup->getOption(
3601 $this->getContext()->getUser(),
3602 $userPreferredExpiryOption,
3603 'infinite'
3604 );
3605 }
3606
3607 $expiryOptions = WatchAction::getExpiryOptions(
3608 $this->getContext(),
3609 $watchedItem,
3610 $userPreferredExpiry
3611 );
3612
3613 if ( $watchexpiry && in_array( $watchexpiry, $expiryOptions['options'] ) ) {
3614 $expiryOptions['default'] = $watchexpiry;
3615 }
3616 // When previewing, override the selected dropdown option to select whatever was posted
3617 // (if it's a valid option) rather than the current value for watchlistExpiry.
3618 // See also above in $this->importFormDataPosted().
3619 $expiryFromRequest = $this->getContext()->getRequest()->getText( 'wpWatchlistExpiry' );
3620 if ( ( $this->preview || $this->diff ) && in_array( $expiryFromRequest, $expiryOptions['options'] ) ) {
3621 $expiryOptions['default'] = $expiryFromRequest;
3622 }
3623
3624 // Reformat the options to match what DropdownInputWidget wants.
3625 $options = [];
3626 foreach ( $expiryOptions['options'] as $label => $value ) {
3627 $options[] = [ 'data' => $value, 'label' => $label ];
3628 }
3629
3630 $fieldDefs['wpWatchlistExpiry'] = [
3631 'id' => 'wpWatchlistExpiry',
3632 'label-message' => 'confirm-watch-label',
3633 // Uses messages: tooltip-watchlist-expiry, accesskey-watchlist-expiry
3634 'tooltip' => 'watchlist-expiry',
3635 'label-id' => 'mw-editpage-watchlist-expiry',
3636 'default' => $expiryOptions['default'],
3637 'value-attr' => 'value',
3638 'class' => DropdownInputWidget::class,
3639 'options' => $options,
3640 'invisibleLabel' => true,
3641 ];
3642 }
3643 return $fieldDefs;
3644 }
3645
3656 public function getCheckboxesWidget( &$tabindex, $values ) {
3657 $checkboxes = [];
3658 $checkboxesDef = $this->getCheckboxesDefinition( $values );
3659
3660 foreach ( $checkboxesDef as $name => $options ) {
3661 $legacyName = $options['legacy-name'] ?? $name;
3662
3663 $title = null;
3664 $accesskey = null;
3665 if ( isset( $options['tooltip'] ) ) {
3666 $accesskey = $this->context->msg( "accesskey-{$options['tooltip']}" )->text();
3667 $title = Linker::titleAttrib( $options['tooltip'] );
3668 }
3669 if ( isset( $options['title-message'] ) ) {
3670 $title = $this->context->msg( $options['title-message'] )->text();
3671 }
3672 // Allow checkbox definitions to set their own class and value-attribute names.
3673 // See $this->getCheckboxesDefinition() for details.
3674 $className = $options['class'] ?? CheckboxInputWidget::class;
3675 $valueAttr = $options['value-attr'] ?? 'selected';
3676 $checkboxes[ $legacyName ] = new FieldLayout(
3677 new $className( [
3678 'tabIndex' => ++$tabindex,
3679 'accessKey' => $accesskey,
3680 'id' => $options['id'] . 'Widget',
3681 'inputId' => $options['id'],
3682 'name' => $name,
3683 $valueAttr => $options['default'],
3684 'infusable' => true,
3685 'options' => $options['options'] ?? null,
3686 ] ),
3687 [
3688 'align' => 'inline',
3689 'label' => new OOUI\HtmlSnippet( $this->context->msg( $options['label-message'] )->parse() ),
3690 'title' => $title,
3691 'id' => $options['label-id'] ?? null,
3692 'invisibleLabel' => $options['invisibleLabel'] ?? null,
3693 ]
3694 );
3695 }
3696
3697 return $checkboxes;
3698 }
3699
3706 private function getWatchlistLabelsWidget( int &$tabindex ): ?FieldLayout {
3707 if ( !$this->watchlistLabelsEnabled ) {
3708 return null;
3709 }
3710
3711 $user = $this->context->getUser();
3712 if ( !$user->isNamed() ) {
3713 return null;
3714 }
3715
3716 $userLabels = $this->watchlistLabelStore->loadAllForUser( $user );
3717 if ( !$userLabels ) {
3718 return null;
3719 }
3720
3721 $this->context->getOutput()->addModuleStyles( 'mediawiki.widgets.TagMultiselectWidget.styles' );
3722 $this->context->getOutput()->addModules( 'mediawiki.widgets.MenuTagMultiselectWidget' );
3723
3724 // Determine which labels are currently selected.
3725 // After the initial render, use submitted values to preserve state across retries.
3726 if ( !$this->firsttime ) {
3727 // getIntArray() should already return int[], but keep this non-null for static analysis.
3728 $selectedLabelIds = (array)$this->getContext()->getRequest()->getIntArray( 'wpWatchlistLabels', [] );
3729 } else {
3730 // Use the labels currently assigned to this watched item.
3731 $watchedItem = $this->watchedItemStore->getWatchedItem( $user, $this->page );
3732 $selectedLabelIds = [];
3733 // Keep this as int[] so array_map( strval(...) ) is always called with an array.
3734 if ( $watchedItem instanceof WatchedItem ) {
3735 foreach ( $watchedItem->getLabels() as $label ) {
3736 $labelId = $label->getId();
3737 if ( $labelId !== null ) {
3738 $selectedLabelIds[] = $labelId;
3739 }
3740 }
3741 }
3742 }
3743
3744 // Build options for MenuTagMultiselectWidget (flat list, no group header).
3745 $options = [];
3746 foreach ( $userLabels as $label ) {
3747 $options[] = [ 'data' => (string)$label->getId(), 'label' => $label->getName() ];
3748 }
3749
3750 $selectedStrings = array_map( strval( ... ), $selectedLabelIds );
3751
3752 // Build no-JS fallback: a CheckboxMultiselectInputWidget
3753 $noJsFallback = [ new CheckboxMultiselectInputWidget( [
3754 'name' => 'wpWatchlistLabels[]',
3755 'value' => $selectedStrings,
3756 'options' => $options,
3757 ] ) ];
3758
3759 $widget = new MenuTagMultiselectWidget( [
3760 'name' => 'wpWatchlistLabels',
3761 'options' => [ '' => $options ],
3762 'default' => $selectedStrings,
3763 'noJsFallback' => $noJsFallback,
3764 'infusable' => true,
3765 'id' => 'wpWatchlistLabelsWidget',
3766 'tabIndex' => ++$tabindex,
3767 'allowReordering' => false,
3768 'placeholder' => $this->context->msg( 'watchlistlabels-editpage-placeholder' )->text(),
3769 ] );
3770
3771 $helpMsg = $this->context->msg( 'watchlistlabels-editpage-help' );
3772 $help = null;
3773 if ( !$helpMsg->isDisabled() ) {
3774 $help = new OOUI\HtmlSnippet( $helpMsg->parse() );
3775 }
3776 return new FieldLayout(
3777 $widget,
3778 [
3779 'label' => new OOUI\HtmlSnippet(
3780 $this->context->msg( 'watchlistlabels-editpage-label' )->parse()
3781 ),
3782 'help' => $help,
3783 'helpInline' => true,
3784 'align' => 'top',
3785 'id' => 'mw-editpage-watchlist-labels',
3786 'classes' => [ 'mw-editpage-watchlistLabels' ],
3787 ]
3788 );
3789 }
3790
3801 public function getEditButtons( &$tabindex ) {
3802 $buttons = [];
3803
3804 $labelAsPublish =
3805 $this->context->getConfig()->get( MainConfigNames::EditSubmitButtonLabelPublish );
3806
3807 $buttonLabel = $this->context->msg( $this->pageEditingHelper->getSubmitButtonLabel( $this->page ) )->text();
3808 $buttonTooltip = $labelAsPublish ? 'publish' : 'save';
3809
3810 $buttons['save'] = new OOUI\ButtonInputWidget( [
3811 'name' => 'wpSave',
3812 'tabIndex' => ++$tabindex,
3813 'id' => 'wpSaveWidget',
3814 'inputId' => 'wpSave',
3815 'flags' => [ 'progressive', 'primary' ],
3816 'label' => $buttonLabel,
3817 'infusable' => true,
3818 'type' => 'submit',
3819 // Messages used: tooltip-save, tooltip-publish
3820 'title' => Linker::titleAttrib( $buttonTooltip ),
3821 // Messages used: accesskey-save, accesskey-publish
3822 'accessKey' => Linker::accesskey( $buttonTooltip ),
3823 'icon' => $this->reauthRequired ? 'lock' : null,
3824 ] );
3825
3826 // Make sure the lock icon is loaded if we need it
3827 if ( $this->reauthRequired ) {
3828 $this->context->getOutput()->addModuleStyles( 'oojs-ui.styles.icons-moderation' );
3829 }
3830
3831 $buttons['preview'] = new OOUI\ButtonInputWidget( [
3832 'name' => 'wpPreview',
3833 'tabIndex' => ++$tabindex,
3834 'id' => 'wpPreviewWidget',
3835 'inputId' => 'wpPreview',
3836 'label' => $this->context->msg( 'showpreview' )->text(),
3837 'infusable' => true,
3838 'type' => 'submit',
3839 // Allow previewing even when the form is in invalid state (T343585)
3840 'formNoValidate' => true,
3841 // Message used: tooltip-preview
3842 'title' => Linker::titleAttrib( 'preview' ),
3843 // Message used: accesskey-preview
3844 'accessKey' => Linker::accesskey( 'preview' ),
3845 ] );
3846
3847 $buttons['diff'] = new OOUI\ButtonInputWidget( [
3848 'name' => 'wpDiff',
3849 'tabIndex' => ++$tabindex,
3850 'id' => 'wpDiffWidget',
3851 'inputId' => 'wpDiff',
3852 'label' => $this->context->msg( 'showdiff' )->text(),
3853 'infusable' => true,
3854 'type' => 'submit',
3855 // Allow previewing even when the form is in invalid state (T343585)
3856 'formNoValidate' => true,
3857 // Message used: tooltip-diff
3858 'title' => Linker::titleAttrib( 'diff' ),
3859 // Message used: accesskey-diff
3860 'accessKey' => Linker::accesskey( 'diff' ),
3861 ] );
3862
3863 $this->getHookRunner()->onEditPageBeforeEditButtons( $this, $buttons, $tabindex );
3864
3865 return $buttons;
3866 }
3867
3872 private function noSuchSectionPage(): void {
3873 $out = $this->context->getOutput();
3874 $out->prepareErrorPage();
3875 $out->setPageTitleMsg( $this->context->msg( 'nosuchsectiontitle' ) );
3876
3877 $res = $this->context->msg( 'nosuchsectiontext', $this->section )->parseAsBlock();
3878
3879 $this->getHookRunner()->onEditPageNoSuchSection( $this, $res );
3880 $out->addHTML( $res );
3881
3882 $out->returnToMain( false, $this->page );
3883 }
3884
3890 public function spamPageWithContent( $match = false ): void {
3891 $out = $this->context->getOutput();
3892 $out->prepareErrorPage();
3893 $out->setPageTitleMsg( $this->context->msg( 'spamprotectiontitle' ) );
3894
3895 $spamText = $this->context->msg( 'spamprotectiontext' )->parseAsBlock();
3896
3897 if ( $match ) {
3898 if ( is_array( $match ) ) {
3899 $matchText = Message::listParam( array_map( wfEscapeWikiText( ... ), $match ) );
3900 } else {
3901 $matchText = wfEscapeWikiText( $match );
3902 }
3903
3904 $spamText .= $this->context->msg( 'spamprotectionmatch', $matchText )->parseAsBlock();
3905 }
3906 $out->addHTML( Html::rawElement(
3907 'div',
3908 [ 'id' => 'spamprotected' ],
3909 $spamText
3910 ) );
3911
3912 $out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
3913 $this->showDiff();
3914
3915 $out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
3916 $this->showTextbox( $this->textbox1, 'wpTextbox2', [ 'tabindex' => 6, 'readonly' ] );
3917
3918 $out->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
3919 }
3920
3921 private function addLongPageWarningHeader(): void {
3922 if ( $this->contentLength === false ) {
3923 $this->contentLength = strlen( $this->textbox1 );
3924 }
3925
3926 $out = $this->context->getOutput();
3927 $longPageHint = $this->context->msg( 'longpage-hint' );
3928 if ( !$longPageHint->isDisabled() ) {
3929 $msgText = trim( $longPageHint->sizeParams( $this->contentLength )
3930 ->params( $this->contentLength ) // Keep this unformatted for math inside message
3931 ->parse() );
3932 if ( $msgText !== '' && $msgText !== '-' ) {
3933 $out->addHTML( "<div id='mw-edit-longpage-hint'>\n$msgText\n</div>" );
3934 }
3935 }
3936 }
3937
3938 private function addExplainConflictHeader(): void {
3939 $this->context->getOutput()->addHTML(
3940 $this->getEditConflictHelper()->getExplainHeader()
3941 );
3942 }
3943
3948 public function setEditConflictHelperFactory( callable $factory ): void {
3949 Assert::precondition( !$this->editConflictHelperFactory,
3950 'Can only have one extension that resolves edit conflicts' );
3951 $this->editConflictHelperFactory = $factory;
3952 }
3953
3954 private function getEditConflictHelper(): TextConflictHelper {
3955 if ( !$this->editConflictHelper ) {
3956 $label = $this->pageEditingHelper->getSubmitButtonLabel( $this->page );
3957 if ( $this->editConflictHelperFactory ) {
3958 $this->editConflictHelper = ( $this->editConflictHelperFactory )( $label );
3959 } else {
3960 $this->editConflictHelper = new TextConflictHelper(
3961 $this->getTitle(),
3962 $this->getContext()->getOutput(),
3963 MediaWikiServices::getInstance()->getStatsFactory(),
3964 $label,
3965 MediaWikiServices::getInstance()->getContentHandlerFactory()
3966 );
3967 }
3968 }
3969 return $this->editConflictHelper;
3970 }
3971}
const CONTENT_MODEL_VUE
Definition Defines.php:241
const CONTENT_MODEL_CSS
Definition Defines.php:237
const NS_MAIN
Definition Defines.php:51
const CONTENT_MODEL_JSON
Definition Defines.php:239
const CONTENT_MODEL_JAVASCRIPT
Definition Defines.php:236
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
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,...
wfArrayToCgi( $array1, $array2=null, $prefix='')
This function takes one or two arrays as input, and returns a CGI-style string, e....
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:71
Page addition to a user's watchlist.
AuthManager is the authentication system in MediaWiki and serves entry point for authentication.
Handle database storage of comments such as edit summaries and log reasons.
Base class for content handling.
Exception representing a failure to serialize or unserialize a content object.
Exception thrown when an unregistered content model is requested.
An IContextSource implementation which will inherit context from another source but allow individual ...
Make sure user doesn't accidentally recreate a page deleted after they started editing.
Verify authorization to edit the page (user rights, rate limits, blocks).
Don't save a new page if it's blank or if it's a page with shadow content equal to the default,...
Constraints reflect possible errors that need to be checked.
newRedirectConstraint(?LinkTarget $allowedProblematicRedirectTarget, Content $newContent, ?Content $originalContent, PageReference $page, MessageSpecifier $errorMessageWrapper, ?string $contentFormat,)
newAccidentalRecreationConstraint(Title $title, bool $allowRecreation, ?string $startTime, ?MessageSpecifier $submitButtonLabel=null,)
Back end to process the edit constraints.
To simplify the logic in EditPage, this constraint may be created even if the section being edited do...
For a new section, do not allow the user to post with an empty subject (section title) unless they ch...
This constraint is used to display an error if the user loses access to the revision while editing it...
Verify summary and text do not match spam regexes.
The HTML user interface for page editing.
Definition EditPage.php:133
showEditForm()
Send the edit form and related headers to OutputPage.
importFormData( $request=null)
This function collects the form data and uses it to populate various member variables.
int $oldid
Revision ID the edit is based on, or 0 if it's the latest revision.
Definition EditPage.php:304
showTextbox(string $text, string $name, array $customAttribs=[])
setEditConflictHelperFactory(callable $factory)
showStandardInputs(int &$tabindex=2)
__construct(Article $article)
Definition EditPage.php:423
doPreviewParse(Content $content)
Parse the page for a preview.
static getPreviewLimitReport(?ParserOutput $output=null)
Get the Limit report for page previews.
string $starttime
Timestamp from the first time the edit form was rendered.
Definition EditPage.php:296
edit()
This is the function that gets called for "action=edit".
Definition EditPage.php:547
toEditContent( $text)
Turns the given text into a Content object by unserializing it.
makeTemplatesOnThisPageList(array $templates)
Wrapper around TemplatesOnThisPageFormatter to make a "templates on this page" list.
bool $didSave
Should be set to true whenever an article was successfully altered.
Definition EditPage.php:346
showDiff()
Get a diff between the current contents of the edit box and the version of the page we're editing fro...
getPreviewText()
Get the rendered text for previewing.
spamPageWithContent( $match=false)
Show "your edit contains spam" page with your diff and text.
string $textbox1
Page content input field.
Definition EditPage.php:262
getCheckboxesWidget(&$tabindex, $values)
Returns an array of fields for the edit form, including 'minor' and 'watch' checkboxes and any other ...
string $edittime
Timestamp of the latest revision of the page when editing was initiated on the client.
Definition EditPage.php:273
static getCopyrightWarning(PageReference $page, string $format, MessageLocalizer $localizer)
Get the copyright warning.
attemptSave(&$resultDetails=false)
Attempt submission.
DerivativeContext $context
Definition EditPage.php:359
static getEditToolbar()
Allow extensions to provide a toolbar.
string $editFormPageTop
Before even the preview.
Definition EditPage.php:329
getActionURL(Title $title)
Returns the URL to use in the form's action attribute.
const UNICODE_CHECK
Used for Unicode support checks.
Definition EditPage.php:140
getCheckboxesDefinition( $values)
Return an array of field definitions.
getEditButtons(&$tabindex)
Returns an array of html code of the following buttons: save, diff and preview.
const EDITFORM_ID
HTML id and name for the beginning of the edit form.
Definition EditPage.php:145
const POST_EDIT_COOKIE_DURATION
Duration of PostEdit cookie, in seconds.
Definition EditPage.php:169
bool $firsttime
True the first time the edit form is rendered, false after re-rendering with diff,...
Definition EditPage.php:198
showContentForm()
Subpage overridable method for printing the form for page content editing By default this simply outp...
previewOnOpen()
Should we show a preview when the edit form is first shown?
getCurrentContent()
Get the current content of the page.
getContentObject( $defaultContent=null)
const POST_EDIT_COOKIE_KEY_PREFIX
Prefix of key for cookie used to pass post-edit state.
Definition EditPage.php:151
getPreviewParserOptions()
Get parser options for a preview.
setApiEditOverride(bool $enableOverride)
Allow editing of content that supports API direct editing, but not general direct editing.
Definition EditPage.php:518
bool $isConflict
Whether an edit conflict needs to be resolved.
Definition EditPage.php:186
maybeActivateTempUserCreate( $doAcquire)
Check the configuration and current user and enable automatic temporary user creation if possible.
Definition EditPage.php:768
handleRetrievedData(array $data)
Overriden for DataStashTrait.
Definition EditPage.php:525
getExpectedParentRevision()
Returns the RevisionRecord corresponding to the revision that was current at the time editing was ini...
Provides helper methods used by EditPage and a future editing backend (T157658).
Handles formatting for the "templates used on this page" lists.
Helper for displaying edit conflicts in text content models to users.
setTextboxes(string $yourtext, string $storedversion)
getEditFormHtmlAfterContent()
Content to go in the edit form after textbox1.
getEditConflictMainTextBox(array $customAttribs=[])
HTML to build the textbox1 on edit conflicts.
getEditFormHtmlBeforeContent()
Content to go in the edit form before textbox1.
Helps EditPage build textboxes.
An error page which can definitely be safely rendered using the OutputPage.
Show an error when a user tries to do something they do not have the necessary permissions for.
Show an error when the wiki is locked/read-only and the user tries to do something that requires writ...
Show an error when the user hits a rate limit.
Show an error when the user tries to do something whilst blocked.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
This class is a collection of static functions that serve two purposes:
Definition Html.php:44
Variant of the Message class.
Class that generates HTML for internal links.
Some internal bits split of from Skin.php.
Definition Linker.php:48
Create PSR-3 logger objects.
A class containing constants representing the names of configuration variables.
const EnableWatchlistLabels
Name constant for the EnableWatchlistLabels setting, for use with Config::get()
const WatchlistExpiry
Name constant for the WatchlistExpiry setting, for use with Config::get()
Service locator for MediaWiki core services.
getMainConfig()
Returns the Config object that provides configuration for MediaWiki core.
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
Status returned by edit constraints and other page editing checks.
Legacy class representing an editable page and handling UI for some page actions.
Definition Article.php:65
getPage()
Get the WikiPage object of this instance.
Definition Article.php:258
getContext()
Gets the context this Article is executed in.
Definition Article.php:2137
Special handling for category description pages.
Factory for LinkBatch objects to batch query page metadata.
Base representation for an editable wiki page.
Definition WikiPage.php:83
Set options of the Parser.
setRenderReason(string $renderReason)
Sets reason for rendering the content.
ParserOutput is a rendering of a Content object or a message.
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:139
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
A StatusValue for permission errors.
Similar to MediaWiki\Request\FauxRequest, but only fakes URL parameters and method (POST or GET) and ...
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form,...
Page revision base class.
getUser(int $audience=self::FOR_PUBLIC, ?Authority $performer=null)
Fetch revision's author's user identity, if it's available to the specified audience.
getId( $wikiId=self::LOCAL)
Get revision ID.
A RevisionRecord representing an existing revision persisted in the revision table.
Service for looking up page revisions.
Value object representing a content slot associated with a page revision.
This serves as the entry point to the MediaWiki session handling system.
A service which loads shadow content, which is content that is displayed on a nonexistent page with a...
The base class for all skins.
Definition Skin.php:54
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
Represents the target of a wiki link.
Represents a title within MediaWiki.
Definition Title.php:69
getLocalURL( $query='')
Get a URL with no fragment or server name (relative URL) from a Title object.
Definition Title.php:2209
getText()
Get the text form (spaces not underscores) of the main part.
Definition Title.php:1007
Class to parse and build external user names.
Provides access to user options.
Status object with strongly typed value, for TempUserManager::createUser()
Service for temporary user creation.
Track info about user edit counts and timings.
Create User objects.
Convenience functions for interpreting UserIdentity objects using additional services or config.
User class for the MediaWiki software.
Definition User.php:129
Representation of a pair of user and title for watchlist entries.
Service class for storage of watchlist labels.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:18
Value object representing a message for i18n.
Type definition for expiry timestamps.
Definition ExpiryDef.php:18
return[ 'config-schema-inverse'=>['default'=>['ConfigRegistry'=>['main'=> 'MediaWiki\\Config\\GlobalVarConfig::newInstance',], 'Sitename'=> 'MediaWiki', 'Server'=> false, 'CanonicalServer'=> false, 'ServerName'=> false, 'AssumeProxiesUseDefaultProtocolPorts'=> true, 'HttpsPort'=> 443, 'ForceHTTPS'=> false, 'ScriptPath'=> '/wiki', 'UsePathInfo'=> null, 'Script'=> false, 'LoadScript'=> false, 'RestPath'=> false, 'StylePath'=> false, 'LocalStylePath'=> false, 'ExtensionAssetsPath'=> false, 'ExtensionDirectory'=> null, 'StyleDirectory'=> null, 'ArticlePath'=> false, 'UploadPath'=> false, 'ImgAuthPath'=> false, 'ThumbPath'=> false, 'UploadDirectory'=> false, 'FileCacheDirectory'=> false, 'Logo'=> false, 'Logos'=> false, 'Favicon'=> '/favicon.ico', 'AppleTouchIcon'=> false, 'ReferrerPolicy'=> false, 'TmpDirectory'=> false, 'UploadBaseUrl'=> '', 'UploadStashScalerBaseUrl'=> false, 'ActionPaths'=>[], 'MainPageIsDomainRoot'=> false, 'EnableUploads'=> false, 'UploadStashMaxAge'=> 21600, 'EnableAsyncUploads'=> false, 'EnableAsyncUploadsByURL'=> false, 'EnableChunkedUploads'=> false, 'UploadMaintenance'=> false, 'IllegalFileChars'=> ':\\/\\\\', 'DeletedDirectory'=> false, 'ImgAuthDetails'=> false, 'ImgAuthUrlPathMap'=>[], 'LocalFileRepo'=>['class'=> 'MediaWiki\\FileRepo\\LocalRepo', 'name'=> 'local', 'directory'=> null, 'scriptDirUrl'=> null, 'favicon'=> null, 'url'=> null, 'hashLevels'=> null, 'thumbScriptUrl'=> null, 'transformVia404'=> null, 'deletedDir'=> null, 'deletedHashLevels'=> null, 'updateCompatibleMetadata'=> null, 'reserializeMetadata'=> null,], 'ForeignFileRepos'=>[], 'UseInstantCommons'=> false, 'UseSharedUploads'=> false, 'SharedUploadDirectory'=> null, 'SharedUploadPath'=> null, 'HashedSharedUploadDirectory'=> true, 'RepositoryBaseUrl'=> 'https:'FetchCommonsDescriptions'=> false, 'SharedUploadDBname'=> false, 'SharedUploadDBprefix'=> '', 'SharedUploadDBschema'=> null, 'CacheSharedUploads'=> true, 'ForeignUploadTargets'=>['local',], 'UploadDialog'=>['fields'=>['description'=> true, 'date'=> false, 'categories'=> false,], 'licensemessages'=>['local'=> 'generic-local', 'foreign'=> 'generic-foreign',], 'comment'=>['local'=> '', 'foreign'=> '',], 'format'=>['filepage'=> ' $DESCRIPTION', 'description'=> ' $TEXT', 'ownwork'=> '', 'license'=> '', 'uncategorized'=> '',],], 'FileBackends'=>[], 'LockManagers'=>[], 'DefaultLockManager'=> null, 'ShowEXIF'=> null, 'UpdateCompatibleMetadata'=> false, 'AllowCopyUploads'=> false, 'CopyUploadsDomains'=>[], 'CopyUploadsFromSpecialUpload'=> false, 'CopyUploadProxy'=> false, 'CopyUploadTimeout'=> false, 'CopyUploadAllowOnWikiDomainConfig'=> false, 'MaxUploadSize'=> 104857600, 'MinUploadChunkSize'=> 1024, 'UploadNavigationUrl'=> false, 'UploadMissingFileUrl'=> false, 'ThumbnailScriptPath'=> false, 'SharedThumbnailScriptPath'=> false, 'HashedUploadDirectory'=> true, 'CSPUploadEntryPoint'=> true, 'FileExtensions'=>['png', 'gif', 'jpg', 'jpeg', 'webp',], 'ProhibitedFileExtensions'=>['html', 'htm', 'js', 'jsb', 'mhtml', 'mht', 'xhtml', 'xht', 'php', 'phtml', 'php3', 'php4', 'php5', 'phps', 'phar', 'shtml', 'jhtml', 'pl', 'py', 'cgi', 'exe', 'scr', 'dll', 'msi', 'vbs', 'bat', 'com', 'pif', 'cmd', 'vxd', 'cpl', 'xml',], 'MimeTypeExclusions'=>['text/html', 'application/javascript', 'text/javascript', 'text/x-javascript', 'application/x-shellscript', 'application/x-php', 'text/x-php', 'text/x-python', 'text/x-perl', 'text/x-bash', 'text/x-sh', 'text/x-csh', 'text/scriptlet', 'application/x-msdownload', 'application/x-msmetafile', 'application/java', 'application/xml', 'text/xml',], 'CheckFileExtensions'=> true, 'StrictFileExtensions'=> true, 'DisableUploadScriptChecks'=> false, 'UploadSizeWarning'=> false, 'TrustedMediaFormats'=>['BITMAP', 'AUDIO', 'VIDEO', 'image/svg+xml', 'application/pdf',], 'MediaHandlers'=>[], 'NativeImageLazyLoading'=> true, 'ParserTestMediaHandlers'=>['image/jpeg'=> 'MockBitmapHandler', 'image/png'=> 'MockBitmapHandler', 'image/gif'=> 'MockBitmapHandler', 'image/tiff'=> 'MockBitmapHandler', 'image/webp'=> 'MockBitmapHandler', 'image/x-ms-bmp'=> 'MockBitmapHandler', 'image/x-bmp'=> 'MockBitmapHandler', 'image/x-xcf'=> 'MockBitmapHandler', 'image/svg+xml'=> 'MockSvgHandler', 'image/vnd.djvu'=> 'MockDjVuHandler',], 'UseImageResize'=> true, 'UseImageMagick'=> false, 'ImageMagickConvertCommand'=> '/usr/bin/convert', 'MaxInterlacingAreas'=>[], 'SharpenParameter'=> '0x0.4', 'SharpenReductionThreshold'=> 0.85, 'ImageMagickTempDir'=> false, 'CustomConvertCommand'=> false, 'JpegTran'=> '/usr/bin/jpegtran', 'JpegPixelFormat'=> 'yuv420', 'JpegQuality'=> 80, 'Exiv2Command'=> '/usr/bin/exiv2', 'Exiftool'=> '/usr/bin/exiftool', 'SVGConverters'=>['ImageMagick'=> ' $path/convert -background "#ffffff00" -thumbnail $widthx$height\\! $input PNG:$output', 'inkscape'=> ' $path/inkscape -w $width -o $output $input', 'batik'=> 'java -Djava.awt.headless=true -jar $path/batik-rasterizer.jar -w $width -d $output $input', 'rsvg'=> ' $path/rsvg-convert -w $width -h $height -l $lang -o $output $input', 'ImagickExt'=>['SvgHandler::rasterizeImagickExt',],], 'SVGConverter'=> 'ImageMagick', 'SVGConverterPath'=> '', 'SVGMaxSize'=> 5120, 'SVGMetadataCutoff'=> 5242880, 'SVGNativeRendering'=> true, 'SVGNativeRenderingSizeLimit'=> 51200, 'MediaInTargetLanguage'=> true, 'MaxImageArea'=> 12500000, 'MaxAnimatedGifArea'=> 12500000, 'MaxAnimatedWebPArea'=> 12500000, 'WebPThumbnailType'=>['webp', 'image/webp',], 'TiffThumbnailType'=>[], 'ThumbnailEpoch'=> '20030516000000', 'AttemptFailureEpoch'=> 1, 'IgnoreImageErrors'=> false, 'GenerateThumbnailOnParse'=> true, 'ShowArchiveThumbnails'=> true, 'EnableAutoRotation'=> null, 'Antivirus'=> null, 'AntivirusSetup'=>['clamav'=>['command'=> 'clamscan --no-summary ', 'codemap'=>[0=> 0, 1=> 1, 52=> -1, ' *'=> false,], 'messagepattern'=> '/.*?:(.*)/sim',],], 'AntivirusRequired'=> true, 'VerifyMimeType'=> true, 'MimeTypeFile'=> 'internal', 'MimeInfoFile'=> 'internal', 'MimeDetectorCommand'=> null, 'TrivialMimeDetection'=> false, 'XMLMimeTypes'=>['http:'svg'=> 'image/svg+xml', 'http:'http:'html'=> 'text/html',], 'ImageLimits'=>[[320, 240,], [640, 480,], [800, 600,], [1024, 768,], [1280, 1024,], [2560, 2048,],], 'ThumbLimits'=>[120, 150, 180, 200, 220, 250, 300, 400,], 'ThumbnailNamespaces'=>[6,], 'ThumbnailSteps'=> null, 'ThumbnailBuckets'=> null, 'ThumbnailMinimumBucketDistance'=> 50, 'UploadThumbnailRenderMap'=>[], 'UploadThumbnailRenderMethod'=> 'jobqueue', 'UploadThumbnailRenderHttpCustomHost'=> false, 'UploadThumbnailRenderHttpCustomDomain'=> false, 'UseTinyRGBForJPGThumbnails'=> false, 'GalleryOptions'=>[], 'ThumbUpright'=> 0.75, 'DirectoryMode'=> 511, 'ResponsiveImages'=> true, 'ImagePreconnect'=> false, 'TrackMediaRequestProvenance'=> false, 'DjvuUseBoxedCommand'=> false, 'DjvuDump'=> null, 'DjvuRenderer'=> null, 'DjvuTxt'=> null, 'DjvuPostProcessor'=> 'pnmtojpeg', 'DjvuOutputExtension'=> 'jpg', 'EmergencyContact'=> false, 'PasswordSender'=> false, 'NoReplyAddress'=> false, 'EnableEmail'=> true, 'EnableUserEmail'=> true, 'UserEmailUseReplyTo'=> true, 'PasswordReminderResendTime'=> 24, 'NewPasswordExpiry'=> 604800, 'UserEmailConfirmationTokenExpiry'=> 604800, 'PasswordExpirationDays'=> false, 'PasswordExpireGrace'=> 604800, 'SMTP'=> false, 'AdditionalMailParams'=> null, 'AllowHTMLEmail'=> false, 'EnotifFromEditor'=> false, 'EmailAuthentication'=> true, 'EmailConfirmationBanner'=> false, 'EnotifWatchlist'=> false, 'EnotifUserTalk'=> false, 'EnotifRevealEditorAddress'=> false, 'EnotifMinorEdits'=> true, 'EnotifUseRealName'=> false, 'UsersNotifiedOnAllChanges'=>[], 'DBname'=> 'my_wiki', 'DBmwschema'=> null, 'DBprefix'=> '', 'DBserver'=> 'localhost', 'DBport'=> 5432, 'DBuser'=> 'wikiuser', 'DBpassword'=> '', 'DBtype'=> 'mysql', 'DBssl'=> false, 'DBcompress'=> false, 'DBStrictWarnings'=> false, 'DBadminuser'=> null, 'DBadminpassword'=> null, 'SearchType'=> null, 'SearchTypeAlternatives'=> null, 'DBTableOptions'=> 'ENGINE=InnoDB, DEFAULT CHARSET=binary', 'SQLMode'=> '', 'SQLiteDataDir'=> '', 'SharedDB'=> null, 'SharedPrefix'=> false, 'SharedTables'=>['user', 'user_properties', 'user_autocreate_serial',], 'SharedSchema'=> false, 'DBservers'=> false, 'LBFactoryConf'=>['class'=> 'Wikimedia\\Rdbms\\LBFactorySimple',], 'DataCenterUpdateStickTTL'=> 10, 'DBerrorLog'=> false, 'DBerrorLogTZ'=> false, 'LocalDatabases'=>[], 'DatabaseReplicaLagWarning'=> 10, 'DatabaseReplicaLagCritical'=> 30, 'MaxExecutionTimeForExpensiveQueries'=> 0, 'VirtualDomainsMapping'=>[], 'RemoteVirtualDomainsMapping'=>[], 'FileSchemaMigrationStage'=> 3, 'ExternalLinksDomainGaps'=>[], 'ContentHandlers'=>['wikitext'=>['class'=> 'MediaWiki\\Content\\WikitextContentHandler', 'services'=>['TitleFactory', 'ParserFactory', 'GlobalIdGenerator', 'LanguageNameUtils', 'LinkRenderer', 'MagicWordFactory', 'ParsoidParserFactory',],], 'javascript'=>['class'=> 'MediaWiki\\Content\\JavaScriptContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'UserOptionsLookup', 'CodeHighlighter',],], 'json'=>['class'=> 'MediaWiki\\Content\\JsonContentHandler', 'services'=>['ParsoidParserFactory', 'TitleFactory',],], 'css'=>['class'=> 'MediaWiki\\Content\\CssContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'UserOptionsLookup', 'CodeHighlighter',],], 'vue'=>['class'=> 'MediaWiki\\Content\\VueContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'CodeHighlighter',],], 'text'=> 'MediaWiki\\Content\\TextContentHandler', 'unknown'=> 'MediaWiki\\Content\\FallbackContentHandler',], 'NamespaceContentModels'=>[], 'TextModelsToParse'=>['wikitext', 'javascript', 'css',], 'CompressRevisions'=> false, 'ExternalStores'=>[], 'ExternalServers'=>[], 'DefaultExternalStore'=> false, 'RevisionCacheExpiry'=> 604800, 'PageLanguageUseDB'=> false, 'DiffEngine'=> null, 'ExternalDiffEngine'=> false, 'Wikidiff2Options'=>[], 'RequestTimeLimit'=> null, 'TransactionalTimeLimit'=> 120, 'CriticalSectionTimeLimit'=> 180.0, 'MiserMode'=> false, 'DisableQueryPages'=> false, 'QueryCacheLimit'=> 1000, 'WantedPagesThreshold'=> 1, 'AllowSlowParserFunctions'=> false, 'AllowSchemaUpdates'=> true, 'MaxArticleSize'=> 2048, 'MemoryLimit'=> '50M', 'PoolCounterConf'=> null, 'PoolCountClientConf'=>['servers'=>['127.0.0.1',], 'timeout'=> 0.1,], 'MaxUserDBWriteDuration'=> false, 'MaxJobDBWriteDuration'=> false, 'LinkHolderBatchSize'=> 1000, 'MaximumMovedPages'=> 100, 'ForceDeferredUpdatesPreSend'=> false, 'MultiShardSiteStats'=> false, 'CacheDirectory'=> false, 'MainCacheType'=> 0, 'MessageCacheType'=> -1, 'ParserCacheType'=> -1, 'SessionCacheType'=> -1, 'AnonSessionCacheType'=> false, 'LanguageConverterCacheType'=> -1, 'ObjectCaches'=>[0=>['class'=> 'Wikimedia\\ObjectCache\\EmptyBagOStuff', 'reportDupes'=> false,], 1=>['class'=> 'MediaWiki\\ObjectCache\\SqlBagOStuff', 'loggroup'=> 'SQLBagOStuff',], 'memcached-php'=>['class'=> 'Wikimedia\\ObjectCache\\MemcachedPhpBagOStuff', 'loggroup'=> 'memcached',], 'memcached-pecl'=>['class'=> 'Wikimedia\\ObjectCache\\MemcachedPeclBagOStuff', 'loggroup'=> 'memcached',], 'hash'=>['class'=> 'Wikimedia\\ObjectCache\\HashBagOStuff', 'reportDupes'=> false,], 'apc'=>['class'=> 'Wikimedia\\ObjectCache\\APCUBagOStuff', 'reportDupes'=> false,], 'apcu'=>['class'=> 'Wikimedia\\ObjectCache\\APCUBagOStuff', 'reportDupes'=> false,],], 'WANObjectCache'=>[], 'MicroStashType'=> -1, 'MainStash'=> 1, 'ParsoidCacheConfig'=>['StashType'=> null, 'StashDuration'=> 86400,], 'ParsoidSelectiveUpdateSampleRate'=> 0, 'SplitParsoidParserCache'=> false, 'ParserCacheFilterConfig'=>['pcache'=>['default'=>['minCpuTime'=> 0,],], 'postproc-pcache'=>['default'=>['minCpuTime'=> 9223372036854775807,],], 'parsoid-pcache'=>['default'=>['minCpuTime'=> 0,],], 'postproc-parsoid-pcache'=>['default'=>['minCpuTime'=> 0,],],], 'ChronologyProtectorSecret'=> '', 'ParserCacheExpireTime'=> 86400, 'ParserCacheAsyncExpireTime'=> 60, 'ParserCacheAsyncRefreshJobs'=> true, 'OldRevisionParserCacheExpireTime'=> 3600, 'ObjectCacheSessionExpiry'=> 3600, 'SuspiciousIpExpiry'=> false, 'SessionPbkdf2Iterations'=> 10001, 'UseSessionCookieJwt'=> false, 'JwtSessionCookieIssuer'=> null, 'MemCachedServers'=>['127.0.0.1:11211',], 'MemCachedPersistent'=> false, 'MemCachedTimeout'=> 500000, 'UseLocalMessageCache'=> false, 'AdaptiveMessageCache'=> false, 'LocalisationCacheConf'=>['class'=> 'MediaWiki\\Language\\LocalisationCache', 'store'=> 'detect', 'storeClass'=> false, 'storeDirectory'=> false, 'storeServer'=>[], 'forceRecache'=> false, 'manualRecache'=> false,], 'CachePages'=> true, 'CacheEpoch'=> '20030516000000', 'GitInfoCacheDirectory'=> false, 'UseFileCache'=> false, 'FileCacheDepth'=> 2, 'RenderHashAppend'=> '', 'EnableSidebarCache'=> false, 'SidebarCacheExpiry'=> 86400, 'UseGzip'=> false, 'InvalidateCacheOnLocalSettingsChange'=> true, 'ExtensionInfoMTime'=> false, 'EnableRemoteBagOStuffTests'=> false, 'UseCdn'=> false, 'VaryOnXFP'=> false, 'InternalServer'=> false, 'CdnMaxAge'=> 18000, 'CdnMaxageLagged'=> 30, 'CdnMaxageStale'=> 10, 'CdnReboundPurgeDelay'=> 0, 'CdnMaxageSubstitute'=> 60, 'ForcedRawSMaxage'=> 300, 'CdnServers'=>[], 'CdnServersNoPurge'=>[], 'HTCPRouting'=>[], 'HTCPMulticastTTL'=> 1, 'UsePrivateIPs'=> false, 'CdnMatchParameterOrder'=> true, 'LanguageCode'=> 'en', 'GrammarForms'=>[], 'InterwikiMagic'=> true, 'HideInterlanguageLinks'=> false, 'ExtraInterlanguageLinkPrefixes'=>[], 'InterlanguageLinkCodeMap'=>[], 'ExtraLanguageNames'=>[], 'ExtraLanguageCodes'=>['bh'=> 'bho', 'no'=> 'nb', 'simple'=> 'en',], 'DummyLanguageCodes'=>[], 'AllUnicodeFixes'=> false, 'LegacyEncoding'=> false, 'AmericanDates'=> false, 'TranslateNumerals'=> true, 'UseDatabaseMessages'=> true, 'MaxMsgCacheEntrySize'=> 10000, 'DisableLangConversion'=> false, 'DisableTitleConversion'=> false, 'DefaultLanguageVariant'=> false, 'UsePigLatinVariant'=> false, 'DisabledVariants'=>[], 'VariantArticlePath'=> false, 'UseXssLanguage'=> false, 'LoginLanguageSelector'=> false, 'ForceUIMsgAsContentMsg'=>[], 'RawHtmlMessages'=>[], 'Localtimezone'=> null, 'LocalTZoffset'=> null, 'OverrideUcfirstCharacters'=>[], 'MimeType'=> 'text/html', 'Html5Version'=> null, 'EditSubmitButtonLabelPublish'=> false, 'XhtmlNamespaces'=>[], 'SiteNotice'=> '', 'BrowserFormatDetection'=> 'telephone=no', 'SkinMetaTags'=>[], 'DefaultSkin'=> 'vector-2022', 'FallbackSkin'=> 'fallback', 'SkipSkins'=>[], 'DisableOutputCompression'=> false, 'FragmentMode'=>['html5', 'legacy',], 'ExternalInterwikiFragmentMode'=> 'legacy', 'FooterIcons'=>['copyright'=>['copyright'=>[],], 'poweredby'=>['mediawiki'=>['src'=> null, 'url'=> 'https:'alt'=> 'Powered by MediaWiki', 'lang'=> 'en',],],], 'EnableSectionShare'=> false, 'PageShareSkinsEnabled'=>[], 'UseCombinedLoginLink'=> false, 'Edititis'=> false, 'Send404Code'=> true, 'ShowRollbackEditCount'=> 10, 'EnableCanonicalServerLink'=> false, 'InterwikiLogoOverride'=>[], 'ResourceModules'=>[], 'ResourceModuleSkinStyles'=>[], 'ResourceLoaderSources'=>[], 'ResourceBasePath'=> null, 'ResourceLoaderMaxage'=>[], 'ResourceLoaderDebug'=> false, 'ResourceLoaderMaxQueryLength'=> false, 'ResourceLoaderValidateJS'=> true, 'ResourceLoaderEnableJSProfiler'=> false, 'ResourceLoaderStorageEnabled'=> true, 'ResourceLoaderStorageVersion'=> 1, 'ResourceLoaderEnableSourceMapLinks'=> true, 'AllowSiteCSSOnRestrictedPages'=> false, 'VueDevelopmentMode'=> false, 'CodexDevelopmentDir'=> null, 'MetaNamespace'=> false, 'MetaNamespaceTalk'=> false, 'CanonicalNamespaceNames'=>[-2=> 'Media', -1=> 'Special', 0=> '', 1=> 'Talk', 2=> 'User', 3=> 'User_talk', 4=> 'Project', 5=> 'Project_talk', 6=> 'File', 7=> 'File_talk', 8=> 'MediaWiki', 9=> 'MediaWiki_talk', 10=> 'Template', 11=> 'Template_talk', 12=> 'Help', 13=> 'Help_talk', 14=> 'Category', 15=> 'Category_talk',], 'ExtraNamespaces'=>[], 'ExtraGenderNamespaces'=>[], 'NamespaceAliases'=>[], 'LegalTitleChars'=> ' %!"$&\'()*,\\-.\\/0-9:;=?@A-Z\\\\^_`a-z~\\x80-\\xFF+', 'CapitalLinks' => true, 'CapitalLinkOverrides' => [ ], 'NamespacesWithSubpages' => [ 1 => true, 2 => true, 3 => true, 4 => true, 5 => true, 7 => true, 8 => true, 9 => true, 10 => true, 11 => true, 12 => true, 13 => true, 15 => true, ], 'NamespacesWithoutAutoSummaries' => [ ], 'ContentNamespaces' => [ 0, ], 'ShortPagesNamespaceExclusions' => [ ], 'ExtraSignatureNamespaces' => [ ], 'InvalidRedirectTargets' => [ 'Filepath', 'Mypage', 'Mytalk', 'Redirect', 'Mylog', ], 'DisableHardRedirects' => false, 'FixDoubleRedirects' => false, 'LocalInterwikis' => [ ], 'InterwikiExpiry' => 10800, 'InterwikiCache' => false, 'InterwikiScopes' => 3, 'InterwikiFallbackSite' => 'wiki', 'RedirectSources' => false, 'SiteTypes' => [ 'mediawiki' => 'MediaWiki\\Site\\MediaWikiSite', ], 'MaxTocLevel' => 999, 'MaxPPNodeCount' => 1000000, 'MaxTemplateDepth' => 100, 'MaxPPExpandDepth' => 100, 'UrlProtocols' => [ 'bitcoin:', 'ftp: 'ftps: 'geo:', 'git: 'gopher: 'http: 'https: 'irc: 'ircs: 'magnet:', 'mailto:', 'matrix:', 'mms: 'news:', 'nntp: 'redis: 'sftp: 'sip:', 'sips:', 'sms:', 'ssh: 'svn: 'tel:', 'telnet: 'urn:', 'wikipedia: 'worldwind: 'xmpp:', ' ], 'CleanSignatures' => true, 'AllowExternalImages' => false, 'AllowExternalImagesFrom' => '', 'EnableImageWhitelist' => false, 'TidyConfig' => [ ], 'ParsoidSettings' => [ 'useSelser' => true, ], 'ParsoidExperimentalParserFunctionOutput' => false, 'RawHtml' => false, 'ExternalLinkTarget' => false, 'NoFollowLinks' => true, 'NoFollowNsExceptions' => [ ], 'NoFollowDomainExceptions' => [ 'mediawiki.org', ], 'RegisterInternalExternals' => false, 'ExternalLinksIgnoreDomains' => [ ], 'AllowDisplayTitle' => true, 'RestrictDisplayTitle' => true, 'ExpensiveParserFunctionLimit' => 100, 'PreprocessorCacheThreshold' => 1000, 'EnableScaryTranscluding' => false, 'TranscludeCacheExpiry' => 3600, 'EnableMagicLinks' => [ 'ISBN' => false, 'PMID' => false, 'RFC' => false, ], 'ParserEnableUserLanguage' => false, 'ArticleCountMethod' => 'link', 'ActiveUserDays' => 30, 'LearnerEdits' => 10, 'LearnerMemberSince' => 4, 'ExperiencedUserEdits' => 500, 'ExperiencedUserMemberSince' => 30, 'ManualRevertSearchRadius' => 15, 'RevertedTagMaxDepth' => 15, 'CentralIdLookupProviders' => [ 'local' => [ 'class' => 'MediaWiki\\User\\CentralId\\LocalIdLookup', 'services' => [ 'MainConfig', 'DBLoadBalancerFactory', 'HideUserUtils', ], ], ], 'CentralIdLookupProvider' => 'local', 'UserRegistrationProviders' => [ 'local' => [ 'class' => 'MediaWiki\\User\\Registration\\LocalUserRegistrationProvider', 'services' => [ 'ConnectionProvider', ], ], ], 'PasswordPolicy' => [ 'policies' => [ 'bureaucrat' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'sysop' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'interface-admin' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'bot' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'default' => [ 'MinimalPasswordLength' => [ 'value' => 8, 'suggestChangeOnLogin' => true, ], 'PasswordCannotBeSubstringInUsername' => [ 'value' => true, 'suggestChangeOnLogin' => true, ], 'PasswordCannotMatchDefaults' => [ 'value' => true, 'suggestChangeOnLogin' => true, ], 'MaximalPasswordLength' => [ 'value' => 4096, 'suggestChangeOnLogin' => true, ], 'PasswordNotInCommonList' => [ 'value' => true, 'suggestChangeOnLogin' => true, ], ], ], 'checks' => [ 'MinimalPasswordLength' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkMinimalPasswordLength', ], 'MinimumPasswordLengthToLogin' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkMinimumPasswordLengthToLogin', ], 'PasswordCannotBeSubstringInUsername' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkPasswordCannotBeSubstringInUsername', ], 'PasswordCannotMatchDefaults' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkPasswordCannotMatchDefaults', ], 'MaximalPasswordLength' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkMaximalPasswordLength', ], 'PasswordNotInCommonList' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkPasswordNotInCommonList', ], ], ], 'AuthManagerConfig' => null, 'AuthManagerAutoConfig' => [ 'preauth' => [ 'MediaWiki\\Auth\\ThrottlePreAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\ThrottlePreAuthenticationProvider', 'sort' => 0, ], 'MediaWiki\\Auth\\PreviouslyRenamedAccountPreAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\PreviouslyRenamedAccountPreAuthenticationProvider', 'services' => [ 'ConnectionProvider', 'UserFactory', ], 'sort' => 0, ], ], 'primaryauth' => [ 'MediaWiki\\Auth\\TemporaryPasswordPrimaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\TemporaryPasswordPrimaryAuthenticationProvider', 'services' => [ 'DBLoadBalancerFactory', 'UserOptionsLookup', ], 'args' => [ [ 'authoritative' => false, ], ], 'sort' => 0, ], 'MediaWiki\\Auth\\LocalPasswordPrimaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\LocalPasswordPrimaryAuthenticationProvider', 'services' => [ 'DBLoadBalancerFactory', ], 'args' => [ [ 'authoritative' => true, ], ], 'sort' => 100, ], ], 'secondaryauth' => [ 'MediaWiki\\Auth\\CheckBlocksSecondaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\CheckBlocksSecondaryAuthenticationProvider', 'sort' => 0, ], 'MediaWiki\\Auth\\ResetPasswordSecondaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\ResetPasswordSecondaryAuthenticationProvider', 'sort' => 100, ], 'MediaWiki\\Auth\\EmailNotificationSecondaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\EmailNotificationSecondaryAuthenticationProvider', 'services' => [ 'DBLoadBalancerFactory', ], 'sort' => 200, ], ], ], 'RememberMe' => 'choose', 'ReauthenticateTime' => [ 'default' => 3600, ], 'ChangeCredentialsBlacklist' => [ 'MediaWiki\\Auth\\TemporaryPasswordAuthenticationRequest', ], 'RemoveCredentialsBlacklist' => [ 'MediaWiki\\Auth\\PasswordAuthenticationRequest', ], 'InvalidPasswordReset' => true, 'PasswordDefault' => 'pbkdf2', 'PasswordConfig' => [ 'A' => [ 'class' => 'MediaWiki\\Password\\MWOldPassword', ], 'B' => [ 'class' => 'MediaWiki\\Password\\MWSaltedPassword', ], 'pbkdf2-legacyA' => [ 'class' => 'MediaWiki\\Password\\LayeredParameterizedPassword', 'types' => [ 'A', 'pbkdf2', ], ], 'pbkdf2-legacyB' => [ 'class' => 'MediaWiki\\Password\\LayeredParameterizedPassword', 'types' => [ 'B', 'pbkdf2', ], ], 'bcrypt' => [ 'class' => 'MediaWiki\\Password\\BcryptPassword', 'cost' => 9, ], 'pbkdf2' => [ 'class' => 'MediaWiki\\Password\\Pbkdf2PasswordUsingOpenSSL', 'algo' => 'sha512', 'cost' => '30000', 'length' => '64', ], 'argon2' => [ 'class' => 'MediaWiki\\Password\\Argon2Password', 'algo' => 'auto', ], ], 'PasswordResetRoutes' => [ 'username' => true, 'email' => true, ], 'MaxSigChars' => 255, 'SignatureValidation' => 'warning', 'SignatureAllowedLintErrors' => [ 'obsolete-tag', ], 'MaxNameChars' => 255, 'ReservedUsernames' => [ 'MediaWiki default', 'Conversion script', 'Maintenance script', 'Template namespace initialisation script', 'ScriptImporter', 'Delete page script', 'Move page script', 'Command line script', 'Unknown user', 'msg:double-redirect-fixer', 'msg:usermessage-editor', 'msg:proxyblocker', 'msg:sorbs', 'msg:spambot_username', 'msg:autochange-username', ], 'DefaultUserOptions' => [ 'ccmeonemails' => 0, 'date' => 'default', 'diffonly' => 0, 'diff-type' => 'table', 'disablemail' => 0, 'editfont' => 'monospace', 'editondblclick' => 0, 'editrecovery' => 0, 'editsectiononrightclick' => 0, 'email-allow-new-users' => 1, 'enotifminoredits' => 0, 'enotifrevealaddr' => 0, 'enotifusertalkpages' => 1, 'enotifwatchlistpages' => 1, 'extendwatchlist' => 1, 'fancysig' => 0, 'forceeditsummary' => 0, 'forcesafemode' => 0, 'gender' => 'unknown', 'hidecategorization' => 1, 'hideminor' => 0, 'hidepatrolled' => 0, 'imagesize' => 2, 'minordefault' => 0, 'newpageshidepatrolled' => 0, 'nickname' => '', 'norollbackdiff' => 0, 'prefershttps' => 1, 'previewonfirst' => 0, 'previewontop' => 1, 'pst-cssjs' => 1, 'rcdays' => 7, 'rcenhancedfilters-disable' => 0, 'rclimit' => 50, 'requireemail' => 0, 'search-match-redirect' => true, 'search-special-page' => 'Search', 'search-thumbnail-extra-namespaces' => true, 'searchlimit' => 20, 'showhiddencats' => 0, 'shownumberswatching' => 1, 'showrollbackconfirmation' => 0, 'skin' => false, 'skin-responsive' => 1, 'thumbsize' => 5, 'underline' => 2, 'useeditwarning' => 1, 'uselivepreview' => 0, 'usenewrc' => 1, 'watchcreations' => 1, 'watchcreations-expiry' => 'infinite', 'watchdefault' => 1, 'watchdefault-expiry' => 'infinite', 'watchdeletion' => 0, 'watchlistdays' => 7, 'watchlisthideanons' => 0, 'watchlisthidebots' => 0, 'watchlisthidecategorization' => 1, 'watchlisthideliu' => 0, 'watchlisthideminor' => 0, 'watchlisthideown' => 0, 'watchlisthidepatrolled' => 0, 'watchlistreloadautomatically' => 0, 'watchlistunwatchlinks' => 0, 'watchmoves' => 0, 'watchrollback' => 0, 'watchuploads' => 1, 'watchrollback-expiry' => 'infinite', 'watchstar-expiry' => 'infinite', 'wlenhancedfilters-disable' => 0, 'wllimit' => 250, ], 'ConditionalUserOptions' => [ ], 'HiddenPrefs' => [ ], 'UserJsPrefLimit' => 100, 'InvalidUsernameCharacters' => '@:>=', 'UserrightsInterwikiDelimiter' => '@', 'SecureLogin' => false, 'AuthenticationTokenVersion' => null, 'SessionProviders' => [ 'MediaWiki\\Session\\CookieSessionProvider' => [ 'class' => 'MediaWiki\\Session\\CookieSessionProvider', 'args' => [ [ 'priority' => 30, ], ], 'services' => [ 'JwtCodec', 'UrlUtils', ], ], 'MediaWiki\\Session\\BotPasswordSessionProvider' => [ 'class' => 'MediaWiki\\Session\\BotPasswordSessionProvider', 'args' => [ [ 'priority' => 75, ], ], 'services' => [ 'GrantsInfo', ], ], ], 'AutoCreateTempUser' => [ 'known' => false, 'enabled' => false, 'actions' => [ 'edit', ], 'genPattern' => '~$1', 'matchPattern' => null, 'reservedPattern' => '~$1', 'serialProvider' => [ 'type' => 'local', 'useYear' => true, ], 'serialMapping' => [ 'type' => 'readable-numeric', ], 'expireAfterDays' => 90, 'notifyBeforeExpirationDays' => 10, ], 'AutoblockExemptions' => [ ], 'AutoblockExpiry' => 86400, 'BlockAllowsUTEdit' => true, 'BlockCIDRLimit' => [ 'IPv4' => 16, 'IPv6' => 19, ], 'BlockDisablesLogin' => false, 'EnableMultiBlocks' => false, 'WhitelistRead' => false, 'WhitelistReadRegexp' => false, 'EmailConfirmToEdit' => false, 'HideIdentifiableRedirects' => true, 'GroupPermissions' => [ '*' => [ 'createaccount' => true, 'autocreateaccount' => true, 'read' => true, 'edit' => true, 'createpage' => true, 'createtalk' => true, 'viewmyprivateinfo' => true, 'editmyprivateinfo' => true, 'editmyoptions' => true, ], 'user' => [ 'move' => true, 'move-subpages' => true, 'move-rootuserpages' => true, 'move-categorypages' => true, 'movefile' => true, 'read' => true, 'edit' => true, 'createpage' => true, 'createtalk' => true, 'upload' => true, 'reupload' => true, 'reupload-shared' => true, 'minoredit' => true, 'editmyusercss' => true, 'editmyuserjson' => true, 'editmyuserjs' => true, 'editmyuserjsredirect' => true, 'sendemail' => true, 'applychangetags' => true, 'changetags' => true, 'viewmywatchlist' => true, 'editmywatchlist' => true, 'createwithcontentmodel' => true, 'logout' => true, ], 'autoconfirmed' => [ 'autoconfirmed' => true, 'editsemiprotected' => true, ], 'bot' => [ 'bot' => true, 'autoconfirmed' => true, 'editsemiprotected' => true, 'nominornewtalk' => true, 'autopatrol' => true, 'suppressredirect' => true, 'apihighlimits' => true, ], 'sysop' => [ 'block' => true, 'createaccount' => true, 'createpreviouslyrenamedaccount' => true, 'delete' => true, 'bigdelete' => true, 'deletedhistory' => true, 'deletedtext' => true, 'undelete' => true, 'editcontentmodel' => true, 'editinterface' => true, 'editsitejson' => true, 'edituserjson' => true, 'import' => true, 'importupload' => true, 'move' => true, 'move-subpages' => true, 'move-rootuserpages' => true, 'move-categorypages' => true, 'patrol' => true, 'autopatrol' => true, 'protect' => true, 'editprotected' => true, 'rollback' => true, 'upload' => true, 'reupload' => true, 'reupload-shared' => true, 'unwatchedpages' => true, 'autoconfirmed' => true, 'editsemiprotected' => true, 'ipblock-exempt' => true, 'blockemail' => true, 'markbotedits' => true, 'apihighlimits' => true, 'browsearchive' => true, 'noratelimit' => true, 'movefile' => true, 'unblockself' => true, 'suppressredirect' => true, 'mergehistory' => true, 'managechangetags' => true, 'deletechangetags' => true, ], 'interface-admin' => [ 'editinterface' => true, 'editsitecss' => true, 'editsitejson' => true, 'editsitejs' => true, 'editusercss' => true, 'edituserjson' => true, 'edituserjs' => true, ], 'bureaucrat' => [ 'userrights' => true, 'noratelimit' => true, 'renameuser' => true, ], 'suppress' => [ 'hideuser' => true, 'suppressrevision' => true, 'viewsuppressed' => true, 'suppressionlog' => true, 'deleterevision' => true, 'deletelogentry' => true, ], ], 'PrivilegedGroups' => [ 'bureaucrat', 'interface-admin', 'suppress', 'sysop', ], 'RevokePermissions' => [ ], 'GroupInheritsPermissions' => [ ], 'ImplicitGroups' => [ '*', 'user', 'autoconfirmed', ], 'GroupsAddToSelf' => [ ], 'GroupsRemoveFromSelf' => [ ], 'RestrictedGroups' => [ ], 'UserRequirementsPrivateConditions' => [ ], 'RestrictionTypes' => [ 'create', 'edit', 'move', 'upload', ], 'RestrictionLevels' => [ '', 'autoconfirmed', 'sysop', ], 'CascadingRestrictionLevels' => [ 'sysop', ], 'SemiprotectedRestrictionLevels' => [ 'autoconfirmed', ], 'NamespaceProtection' => [ ], 'RestrictUserPageEditing' => false, 'NonincludableNamespaces' => [ ], 'AutoConfirmAge' => 0, 'AutoConfirmCount' => 0, 'Autopromote' => [ 'autoconfirmed' => [ '&', [ 1, null, ], [ 2, null, ], ], ], 'AutopromoteOnce' => [ 'onEdit' => [ ], ], 'AutopromoteOnceLogInRC' => true, 'AutopromoteOnceRCExcludedGroups' => [ ], 'AddGroups' => [ ], 'RemoveGroups' => [ ], 'AvailableRights' => [ ], 'ImplicitRights' => [ ], 'DeleteRevisionsLimit' => 0, 'DeleteRevisionsBatchSize' => 1000, 'HideUserContribLimit' => 1000, 'AccountCreationThrottle' => [ [ 'count' => 0, 'seconds' => 86400, ], ], 'TempAccountCreationThrottle' => [ [ 'count' => 1, 'seconds' => 600, ], [ 'count' => 6, 'seconds' => 86400, ], ], 'TempAccountNameAcquisitionThrottle' => [ [ 'count' => 60, 'seconds' => 86400, ], ], 'SpamRegex' => [ ], 'SummarySpamRegex' => [ ], 'EnableDnsBlacklist' => false, 'DnsBlacklistUrls' => [ ], 'ProxyList' => [ ], 'ProxyWhitelist' => [ ], 'SoftBlockRanges' => [ ], 'ApplyIpBlocksToXff' => false, 'RateLimits' => [ 'edit' => [ 'ip' => [ 8, 60, ], 'newbie' => [ 8, 60, ], 'user' => [ 90, 60, ], ], 'move' => [ 'newbie' => [ 2, 120, ], 'user' => [ 8, 60, ], ], 'upload' => [ 'ip' => [ 8, 60, ], 'newbie' => [ 8, 60, ], ], 'rollback' => [ 'user' => [ 10, 60, ], 'newbie' => [ 5, 120, ], ], 'mailpassword' => [ 'ip' => [ 5, 3600, ], ], 'sendemail' => [ 'ip' => [ 5, 86400, ], 'newbie' => [ 5, 86400, ], 'user' => [ 20, 86400, ], ], 'changeemail' => [ 'ip-all' => [ 10, 3600, ], 'user' => [ 4, 86400, ], ], 'confirmemail' => [ 'ip-all' => [ 10, 3600, ], 'user' => [ 4, 86400, ], ], 'purge' => [ 'ip' => [ 30, 60, ], 'user' => [ 30, 60, ], ], 'linkpurge' => [ 'ip' => [ 30, 60, ], 'user' => [ 30, 60, ], ], 'renderfile' => [ 'ip' => [ 700, 30, ], 'user' => [ 700, 30, ], ], 'renderfile-nonstandard' => [ 'ip' => [ 70, 30, ], 'user' => [ 70, 30, ], ], 'stashedit' => [ 'ip' => [ 30, 60, ], 'newbie' => [ 30, 60, ], ], 'stashbasehtml' => [ 'ip' => [ 5, 60, ], 'newbie' => [ 5, 60, ], ], 'changetags' => [ 'ip' => [ 8, 60, ], 'newbie' => [ 8, 60, ], ], 'editcontentmodel' => [ 'newbie' => [ 2, 120, ], 'user' => [ 8, 60, ], ], ], 'RateLimitsExcludedIPs' => [ ], 'PutIPinRC' => true, 'QueryPageDefaultLimit' => 50, 'ExternalQuerySources' => [ ], 'PasswordAttemptThrottle' => [ [ 'count' => 5, 'seconds' => 300, ], [ 'count' => 150, 'seconds' => 172800, ], ], 'GrantPermissions' => [ 'basic' => [ 'autocreateaccount' => true, 'autoconfirmed' => true, 'autopatrol' => true, 'editsemiprotected' => true, 'ipblock-exempt' => true, 'nominornewtalk' => true, 'patrolmarks' => true, 'read' => true, 'unwatchedpages' => true, ], 'highvolume' => [ 'bot' => true, 'apihighlimits' => true, 'noratelimit' => true, 'markbotedits' => true, ], 'import' => [ 'import' => true, 'importupload' => true, ], 'editpage' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'pagelang' => true, ], 'editprotected' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editprotected' => true, ], 'editmycssjs' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editmyusercss' => true, 'editmyuserjson' => true, 'editmyuserjs' => true, ], 'editmyoptions' => [ 'editmyoptions' => true, 'editmyuserjson' => true, ], 'editinterface' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editinterface' => true, 'edituserjson' => true, 'editsitejson' => true, ], 'editsiteconfig' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editinterface' => true, 'edituserjson' => true, 'editsitejson' => true, 'editusercss' => true, 'edituserjs' => true, 'editsitecss' => true, 'editsitejs' => true, ], 'createeditmovepage' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'createpage' => true, 'createtalk' => true, 'delete-redirect' => true, 'move' => true, 'move-rootuserpages' => true, 'move-subpages' => true, 'move-categorypages' => true, 'suppressredirect' => true, ], 'uploadfile' => [ 'upload' => true, 'reupload-own' => true, ], 'uploadeditmovefile' => [ 'upload' => true, 'reupload-own' => true, 'reupload' => true, 'reupload-shared' => true, 'upload_by_url' => true, 'movefile' => true, 'suppressredirect' => true, ], 'patrol' => [ 'patrol' => true, ], 'rollback' => [ 'rollback' => true, ], 'blockusers' => [ 'block' => true, 'blockemail' => true, ], 'viewdeleted' => [ 'browsearchive' => true, 'deletedhistory' => true, 'deletedtext' => true, ], 'viewrestrictedlogs' => [ 'suppressionlog' => true, ], 'delete' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'browsearchive' => true, 'deletedhistory' => true, 'deletedtext' => true, 'delete' => true, 'bigdelete' => true, 'deletelogentry' => true, 'deleterevision' => true, 'undelete' => true, ], 'oversight' => [ 'suppressrevision' => true, 'viewsuppressed' => true, ], 'protect' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editprotected' => true, 'protect' => true, ], 'viewmywatchlist' => [ 'viewmywatchlist' => true, ], 'editmywatchlist' => [ 'editmywatchlist' => true, ], 'sendemail' => [ 'sendemail' => true, ], 'createaccount' => [ 'createaccount' => true, ], 'privateinfo' => [ 'viewmyprivateinfo' => true, ], 'mergehistory' => [ 'mergehistory' => true, ], 'managesessions' => [ 'logout' => true, ], ], 'GrantPermissionGroups' => [ 'basic' => 'hidden', 'editpage' => 'page-interaction', 'createeditmovepage' => 'page-interaction', 'editprotected' => 'page-interaction', 'patrol' => 'page-interaction', 'uploadfile' => 'file-interaction', 'uploadeditmovefile' => 'file-interaction', 'sendemail' => 'email', 'viewmywatchlist' => 'watchlist-interaction', 'editmywatchlist' => 'watchlist-interaction', 'editmycssjs' => 'customization', 'editmyoptions' => 'customization', 'editinterface' => 'administration', 'editsiteconfig' => 'administration', 'rollback' => 'administration', 'blockusers' => 'administration', 'delete' => 'administration', 'viewdeleted' => 'administration', 'viewrestrictedlogs' => 'administration', 'protect' => 'administration', 'oversight' => 'administration', 'createaccount' => 'administration', 'mergehistory' => 'administration', 'import' => 'administration', 'highvolume' => 'high-volume', 'privateinfo' => 'private-information', 'managesessions' => 'private-information', ], 'GrantRiskGroups' => [ 'basic' => 'low', 'editpage' => 'low', 'createeditmovepage' => 'low', 'editprotected' => 'vandalism', 'patrol' => 'low', 'uploadfile' => 'low', 'uploadeditmovefile' => 'low', 'sendemail' => 'security', 'viewmywatchlist' => 'low', 'editmywatchlist' => 'low', 'editmycssjs' => 'security', 'editmyoptions' => 'security', 'editinterface' => 'vandalism', 'editsiteconfig' => 'security', 'rollback' => 'low', 'blockusers' => 'vandalism', 'delete' => 'vandalism', 'viewdeleted' => 'vandalism', 'viewrestrictedlogs' => 'security', 'protect' => 'vandalism', 'oversight' => 'security', 'createaccount' => 'low', 'mergehistory' => 'vandalism', 'import' => 'security', 'highvolume' => 'low', 'privateinfo' => 'low', 'managesessions' => 'low', ], 'EnableBotPasswords' => true, 'BotPasswordsCluster' => false, 'BotPasswordsDatabase' => false, 'BotPasswordsLimit' => 100, 'SecretKey' => false, 'JwtPrivateKey' => false, 'JwtPublicKey' => false, 'AllowUserJs' => false, 'ReauthenticateForActions' => [ 'edituserjs' => 'edituserjscss', 'editusercss' => 'edituserjscss', 'editsitejs' => 'editsitejscss', 'editsitecss' => 'editsitejscss', ], 'AllowUserCss' => false, 'AllowUserCssPrefs' => true, 'UseSiteJs' => true, 'UseSiteCss' => true, 'BreakFrames' => false, 'EditPageFrameOptions' => 'DENY', 'ApiFrameOptions' => 'DENY', 'CSPHeader' => false, 'CSPReportOnlyHeader' => false, 'CSPUseReportURIDirective' => false, 'CSPFalsePositiveUrls' => [ 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'chrome-extension' => true, ], 'AllowCrossOrigin' => false, 'RestAllowCrossOriginCookieAuth' => false, 'SessionSecret' => false, 'CookieExpiration' => 2592000, 'ExtendedLoginCookieExpiration' => 15552000, 'SessionCookieJwtExpiration' => 14400, 'CookieDomain' => '', 'CookiePath' => '/', 'CookieSecure' => 'detect', 'CookiePrefix' => false, 'CookieHttpOnly' => true, 'CookieSameSite' => null, 'CacheVaryCookies' => [ ], 'SessionName' => false, 'CookieSetOnAutoblock' => true, 'CookieSetOnIpBlock' => true, 'DebugLogFile' => '', 'DebugLogPrefix' => '', 'DebugRedirects' => false, 'DebugRawPage' => false, 'DebugComments' => false, 'DebugDumpSql' => false, 'TrxProfilerLimits' => [ 'GET' => [ 'masterConns' => 0, 'writes' => 0, 'readQueryTime' => 5, 'readQueryRows' => 10000, ], 'POST' => [ 'readQueryTime' => 5, 'writeQueryTime' => 1, 'readQueryRows' => 100000, 'maxAffected' => 1000, ], 'POST-nonwrite' => [ 'writes' => 0, 'readQueryTime' => 5, 'readQueryRows' => 10000, ], 'PostSend-GET' => [ 'readQueryTime' => 5, 'writeQueryTime' => 1, 'readQueryRows' => 10000, 'maxAffected' => 1000, 'masterConns' => 0, 'writes' => 0, ], 'PostSend-POST' => [ 'readQueryTime' => 5, 'writeQueryTime' => 1, 'readQueryRows' => 100000, 'maxAffected' => 1000, ], 'JobRunner' => [ 'readQueryTime' => 30, 'writeQueryTime' => 5, 'readQueryRows' => 100000, 'maxAffected' => 500, ], 'Maintenance' => [ 'writeQueryTime' => 5, 'maxAffected' => 1000, ], ], 'DebugLogGroups' => [ ], 'MWLoggerDefaultSpi' => [ 'class' => 'MediaWiki\\Logger\\LegacySpi', ], 'ShowDebug' => false, 'SpecialVersionShowHooks' => false, 'ShowExceptionDetails' => false, 'LogExceptionBacktrace' => true, 'PropagateErrors' => true, 'ShowHostnames' => false, 'OverrideHostname' => false, 'DevelopmentWarnings' => false, 'DeprecationReleaseLimit' => false, 'Profiler' => [ ], 'StatsdServer' => false, 'StatsdMetricPrefix' => 'MediaWiki', 'StatsTarget' => null, 'StatsFormat' => null, 'StatsPrefix' => 'mediawiki', 'OpenTelemetryConfig' => null, 'PageInfoTransclusionLimit' => 50, 'EnableJavaScriptTest' => false, 'DebugToolbar' => false, 'ApiClientErrorSampleRate' => 1.0, 'DisableTextSearch' => false, 'AdvancedSearchHighlighting' => false, 'SearchHighlightBoundaries' => '[\\p{Z}\\p{P}\\p{C}]', 'OpenSearchTemplates' => [ 'application/x-suggestions+json' => false, 'application/x-suggestions+xml' => false, ], 'OpenSearchDefaultLimit' => 10, 'OpenSearchDescriptionLength' => 100, 'SearchSuggestCacheExpiry' => 1200, 'DisableSearchUpdate' => false, 'NamespacesToBeSearchedDefault' => [ true, ], 'DisableInternalSearch' => false, 'SearchForwardUrl' => null, 'SitemapNamespaces' => false, 'SitemapNamespacesPriorities' => false, 'SitemapApiConfig' => [ ], 'SpecialSearchFormOptions' => [ ], 'SearchMatchRedirectPreference' => false, 'SearchRunSuggestedQuery' => true, 'Diff3' => '/usr/bin/diff3', 'Diff' => '/usr/bin/diff', 'PreviewOnOpenNamespaces' => [ 14 => true, ], 'UniversalEditButton' => true, 'UseAutomaticEditSummaries' => true, 'CommandLineDarkBg' => false, 'ReadOnly' => null, 'ReadOnlyWatchedItemStore' => false, 'ReadOnlyFile' => false, 'UpgradeKey' => false, 'GitBin' => '/usr/bin/git', 'GitRepositoryViewers' => [ 'https: 'ssh: 'https: 'git@github\\.com:(.*?)(\\.git)?' => 'https: ], 'InstallerInitialPages' => [ [ 'titlemsg' => 'mainpage', 'text' => '{{subst:int:mainpagetext}}{{subst:int:mainpagedocfooter}}', ], ], 'RCMaxAge' => 7776000, 'WatchersMaxAge' => 15552000, 'UnwatchedPageSecret' => 1, 'RCFilterByAge' => false, 'RCLinkLimits' => [ 50, 100, 250, 500, ], 'RCLinkDays' => [ 1, 3, 7, 14, 30, ], 'RCFeeds' => [ ], 'RCWatchCategoryMembership' => false, 'UseRCPatrol' => true, 'StructuredChangeFiltersLiveUpdatePollingRate' => 3, 'UseNPPatrol' => true, 'UseFilePatrol' => true, 'Feed' => true, 'FeedLimit' => 50, 'FeedCacheTimeout' => 60, 'FeedDiffCutoff' => 32768, 'OverrideSiteFeed' => [ ], 'FeedClasses' => [ 'rss' => 'MediaWiki\\Feed\\RSSFeed', 'atom' => 'MediaWiki\\Feed\\AtomFeed', ], 'AdvertisedFeedTypes' => [ 'atom', ], 'RCShowWatchingUsers' => false, 'RCShowChangedSize' => true, 'RCChangedSizeThreshold' => 500, 'ShowUpdatedMarker' => true, 'DisableAnonTalk' => false, 'UseTagFilter' => true, 'SoftwareTags' => [ 'mw-contentmodelchange' => true, 'mw-new-redirect' => true, 'mw-removed-redirect' => true, 'mw-changed-redirect-target' => true, 'mw-blank' => true, 'mw-replace' => true, 'mw-recreated' => true, 'mw-rollback' => true, 'mw-undo' => true, 'mw-manual-revert' => true, 'mw-reverted' => true, 'mw-server-side-upload' => true, 'mw-ipblock-appeal' => true, 'mw-edited-other-users-js' => true, 'mw-edited-other-users-css' => true, ], 'RestrictedTagViewRights' => [ ], 'UnwatchedPageThreshold' => false, 'RecentChangesFlags' => [ 'newpage' => [ 'letter' => 'newpageletter', 'title' => 'recentchanges-label-newpage', 'legend' => 'recentchanges-legend-newpage', 'grouping' => 'any', ], 'minor' => [ 'letter' => 'minoreditletter', 'title' => 'recentchanges-label-minor', 'legend' => 'recentchanges-legend-minor', 'class' => 'minoredit', 'grouping' => 'all', ], 'bot' => [ 'letter' => 'boteditletter', 'title' => 'recentchanges-label-bot', 'legend' => 'recentchanges-legend-bot', 'class' => 'botedit', 'grouping' => 'all', ], 'unpatrolled' => [ 'letter' => 'unpatrolledletter', 'title' => 'recentchanges-label-unpatrolled', 'legend' => 'recentchanges-legend-unpatrolled', 'grouping' => 'any', ], ], 'WatchlistExpiry' => false, 'EnableWatchstarPopover' => false, 'EnableWatchlistLabels' => false, 'WatchlistLabelsMaxPerUser' => 100, 'WatchlistPurgeRate' => 0.1, 'WatchlistExpiryMaxDuration' => '1 year', 'EnableChangesListQueryPartitioning' => false, 'RightsPage' => null, 'RightsUrl' => null, 'RightsText' => null, 'RightsIcon' => null, 'UseCopyrightUpload' => false, 'MaxCredits' => 0, 'ShowCreditsIfMax' => true, 'ImportSources' => [ ], 'ImportTargetNamespace' => null, 'ExportAllowHistory' => true, 'ExportMaxHistory' => 0, 'ExportAllowListContributors' => false, 'ExportMaxLinkDepth' => 0, 'ExportFromNamespaces' => false, 'ExportAllowAll' => false, 'ExportPagelistLimit' => 5000, 'XmlDumpSchemaVersion' => '0.11', 'WikiFarmSettingsDirectory' => null, 'WikiFarmSettingsExtension' => 'yaml', 'ExtensionFunctions' => [ ], 'ExtensionMessagesFiles' => [ ], 'MessagesDirs' => [ ], 'TranslationAliasesDirs' => [ ], 'ExtensionEntryPointListFiles' => [ ], 'EnableParserLimitReporting' => true, 'ValidSkinNames' => [ ], 'SpecialPages' => [ ], 'ExtensionCredits' => [ ], 'Hooks' => [ ], 'ServiceWiringFiles' => [ ], 'JobClasses' => [ 'deletePage' => 'MediaWiki\\Page\\DeletePageJob', 'refreshLinks' => 'MediaWiki\\JobQueue\\Jobs\\RefreshLinksJob', 'deleteLinks' => 'MediaWiki\\Page\\DeleteLinksJob', 'htmlCacheUpdate' => 'MediaWiki\\JobQueue\\Jobs\\HTMLCacheUpdateJob', 'sendMail' => [ 'class' => 'MediaWiki\\Mail\\EmaillingJob', 'services' => [ 'Emailer', ], ], 'enotifNotify' => [ 'class' => 'MediaWiki\\RecentChanges\\RecentChangeNotifyJob', 'services' => [ 'RecentChangeLookup', ], ], 'fixDoubleRedirect' => [ 'class' => 'MediaWiki\\JobQueue\\Jobs\\DoubleRedirectJob', 'services' => [ 'RevisionLookup', 'MagicWordFactory', 'WikiPageFactory', ], 'needsPage' => true, ], 'AssembleUploadChunks' => 'MediaWiki\\JobQueue\\Jobs\\AssembleUploadChunksJob', 'PublishStashedFile' => 'MediaWiki\\JobQueue\\Jobs\\PublishStashedFileJob', 'ThumbnailRender' => 'MediaWiki\\JobQueue\\Jobs\\ThumbnailRenderJob', 'UploadFromUrl' => 'MediaWiki\\JobQueue\\Jobs\\UploadFromUrlJob', 'recentChangesUpdate' => 'MediaWiki\\RecentChanges\\RecentChangesUpdateJob', 'refreshLinksPrioritized' => 'MediaWiki\\JobQueue\\Jobs\\RefreshLinksJob', 'refreshLinksDynamic' => 'MediaWiki\\JobQueue\\Jobs\\RefreshLinksJob', 'activityUpdateJob' => 'MediaWiki\\Watchlist\\ActivityUpdateJob', 'categoryMembershipChange' => [ 'class' => 'MediaWiki\\RecentChanges\\CategoryMembershipChangeJob', 'services' => [ 'RecentChangeFactory', ], ], 'CategoryCountUpdateJob' => [ 'class' => 'MediaWiki\\JobQueue\\Jobs\\CategoryCountUpdateJob', 'services' => [ 'ConnectionProvider', 'NamespaceInfo', ], ], 'clearUserWatchlist' => 'MediaWiki\\Watchlist\\ClearUserWatchlistJob', 'watchlistExpiry' => [ 'class' => 'MediaWiki\\Watchlist\\WatchlistExpiryJob', 'needsPage' => false, ], 'cdnPurge' => 'MediaWiki\\JobQueue\\Jobs\\CdnPurgeJob', 'userGroupExpiry' => 'MediaWiki\\User\\UserGroupExpiryJob', 'clearWatchlistNotifications' => 'MediaWiki\\Watchlist\\ClearWatchlistNotificationsJob', 'userOptionsUpdate' => 'MediaWiki\\User\\Options\\UserOptionsUpdateJob', 'revertedTagUpdate' => 'MediaWiki\\JobQueue\\Jobs\\RevertedTagUpdateJob', 'null' => 'MediaWiki\\JobQueue\\Jobs\\NullJob', 'userEditCountInit' => 'MediaWiki\\User\\UserEditCountInitJob', 'renameUserTable' => [ 'class' => 'MediaWiki\\RenameUser\\Job\\RenameUserTableJob', 'services' => [ 'MainConfig', 'DBLoadBalancerFactory', ], ], 'renameUserDerived' => [ 'class' => 'MediaWiki\\RenameUser\\Job\\RenameUserDerivedJob', 'services' => [ 'RenameUserFactory', 'UserFactory', ], ], 'renameUser' => [ 'class' => 'MediaWiki\\RenameUser\\Job\\RenameUserTableJob', 'services' => [ 'MainConfig', 'DBLoadBalancerFactory', ], ], ], 'JobTypesExcludedFromDefaultQueue' => [ 'AssembleUploadChunks', 'PublishStashedFile', 'UploadFromUrl', ], 'JobBackoffThrottling' => [ ], 'JobTypeConf' => [ 'default' => [ 'class' => 'MediaWiki\\JobQueue\\JobQueueDB', 'order' => 'random', 'claimTTL' => 3600, ], ], 'JobQueueIncludeInMaxLagFactor' => false, 'SpecialPageCacheUpdates' => [ 'Statistics' => [ 'MediaWiki\\Deferred\\SiteStatsUpdate', 'cacheUpdate', ], ], 'PagePropLinkInvalidations' => [ 'hiddencat' => 'categorylinks', ], 'CategoryMagicGallery' => true, 'CategoryPagingLimit' => 200, 'CategoryCollation' => 'uppercase', 'TempCategoryCollations' => [ ], 'SortedCategories' => false, 'TrackingCategories' => [ ], 'LogTypes' => [ '', 'block', 'protect', 'rights', 'delete', 'upload', 'move', 'import', 'interwiki', 'patrol', 'merge', 'suppress', 'tag', 'managetags', 'contentmodel', 'renameuser', ], 'LogRestrictions' => [ 'suppress' => 'suppressionlog', ], 'FilterLogTypes' => [ 'patrol' => true, 'tag' => true, 'newusers' => false, ], 'LogNames' => [ '' => 'all-logs-page', 'block' => 'blocklogpage', 'protect' => 'protectlogpage', 'rights' => 'rightslog', 'delete' => 'dellogpage', 'upload' => 'uploadlogpage', 'move' => 'movelogpage', 'import' => 'importlogpage', 'patrol' => 'patrol-log-page', 'merge' => 'mergelog', 'suppress' => 'suppressionlog', ], 'LogHeaders' => [ '' => 'alllogstext', 'block' => 'blocklogtext', 'delete' => 'dellogpagetext', 'import' => 'importlogpagetext', 'merge' => 'mergelogpagetext', 'move' => 'movelogpagetext', 'patrol' => 'patrol-log-header', 'protect' => 'protectlogtext', 'rights' => 'rightslogtext', 'suppress' => 'suppressionlogtext', 'upload' => 'uploadlogpagetext', ], 'LogActions' => [ ], 'LogActionsHandlers' => [ 'block/block' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'block/reblock' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'block/unblock' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'contentmodel/change' => 'MediaWiki\\Logging\\ContentModelLogFormatter', 'contentmodel/new' => 'MediaWiki\\Logging\\ContentModelLogFormatter', 'delete/delete' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/delete_redir' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/delete_redir2' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/event' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/restore' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/revision' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'import/interwiki' => 'MediaWiki\\Logging\\ImportLogFormatter', 'import/upload' => 'MediaWiki\\Logging\\ImportLogFormatter', 'interwiki/iw_add' => 'MediaWiki\\Logging\\InterwikiLogFormatter', 'interwiki/iw_delete' => 'MediaWiki\\Logging\\InterwikiLogFormatter', 'interwiki/iw_edit' => 'MediaWiki\\Logging\\InterwikiLogFormatter', 'managetags/activate' => 'MediaWiki\\Logging\\LogFormatter', 'managetags/create' => 'MediaWiki\\Logging\\LogFormatter', 'managetags/deactivate' => 'MediaWiki\\Logging\\LogFormatter', 'managetags/delete' => 'MediaWiki\\Logging\\LogFormatter', 'merge/merge' => [ 'class' => 'MediaWiki\\Logging\\MergeLogFormatter', 'services' => [ 'TitleParser', ], ], 'merge/merge-into' => [ 'class' => 'MediaWiki\\Logging\\MergeLogFormatter', 'services' => [ 'TitleParser', ], ], 'move/move' => [ 'class' => 'MediaWiki\\Logging\\MoveLogFormatter', 'services' => [ 'TitleParser', ], ], 'move/move_redir' => [ 'class' => 'MediaWiki\\Logging\\MoveLogFormatter', 'services' => [ 'TitleParser', ], ], 'patrol/patrol' => 'MediaWiki\\Logging\\PatrolLogFormatter', 'patrol/autopatrol' => 'MediaWiki\\Logging\\PatrolLogFormatter', 'protect/modify' => [ 'class' => 'MediaWiki\\Logging\\ProtectLogFormatter', 'services' => [ 'TitleParser', ], ], 'protect/move_prot' => [ 'class' => 'MediaWiki\\Logging\\ProtectLogFormatter', 'services' => [ 'TitleParser', ], ], 'protect/protect' => [ 'class' => 'MediaWiki\\Logging\\ProtectLogFormatter', 'services' => [ 'TitleParser', ], ], 'protect/unprotect' => [ 'class' => 'MediaWiki\\Logging\\ProtectLogFormatter', 'services' => [ 'TitleParser', ], ], 'renameuser/renameuser' => [ 'class' => 'MediaWiki\\Logging\\RenameuserLogFormatter', 'services' => [ 'TitleParser', ], ], 'rights/autopromote' => 'MediaWiki\\Logging\\RightsLogFormatter', 'rights/rights' => 'MediaWiki\\Logging\\RightsLogFormatter', 'suppress/block' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'suppress/delete' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'suppress/event' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'suppress/reblock' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'suppress/revision' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'tag/update' => 'MediaWiki\\Logging\\TagLogFormatter', 'upload/overwrite' => 'MediaWiki\\Logging\\UploadLogFormatter', 'upload/revert' => 'MediaWiki\\Logging\\UploadLogFormatter', 'upload/upload' => 'MediaWiki\\Logging\\UploadLogFormatter', ], 'ActionFilteredLogs' => [ 'block' => [ 'block' => [ 'block', ], 'reblock' => [ 'reblock', ], 'unblock' => [ 'unblock', ], ], 'contentmodel' => [ 'change' => [ 'change', ], 'new' => [ 'new', ], ], 'delete' => [ 'delete' => [ 'delete', ], 'delete_redir' => [ 'delete_redir', 'delete_redir2', ], 'restore' => [ 'restore', ], 'event' => [ 'event', ], 'revision' => [ 'revision', ], ], 'import' => [ 'interwiki' => [ 'interwiki', ], 'upload' => [ 'upload', ], ], 'managetags' => [ 'create' => [ 'create', ], 'delete' => [ 'delete', ], 'activate' => [ 'activate', ], 'deactivate' => [ 'deactivate', ], ], 'move' => [ 'move' => [ 'move', ], 'move_redir' => [ 'move_redir', ], ], 'newusers' => [ 'create' => [ 'create', 'newusers', ], 'create2' => [ 'create2', ], 'autocreate' => [ 'autocreate', ], 'byemail' => [ 'byemail', ], ], 'protect' => [ 'protect' => [ 'protect', ], 'modify' => [ 'modify', ], 'unprotect' => [ 'unprotect', ], 'move_prot' => [ 'move_prot', ], ], 'rights' => [ 'rights' => [ 'rights', ], 'autopromote' => [ 'autopromote', ], ], 'suppress' => [ 'event' => [ 'event', ], 'revision' => [ 'revision', ], 'delete' => [ 'delete', ], 'block' => [ 'block', ], 'reblock' => [ 'reblock', ], ], 'upload' => [ 'upload' => [ 'upload', ], 'overwrite' => [ 'overwrite', ], 'revert' => [ 'revert', ], ], ], 'NewUserLog' => true, 'PageCreationLog' => true, 'AllowSpecialInclusion' => true, 'DisableQueryPageUpdate' => false, 'CountCategorizedImagesAsUsed' => false, 'MaxRedirectLinksRetrieved' => 500, 'RangeContributionsCIDRLimit' => [ 'IPv4' => 16, 'IPv6' => 32, ], 'Actions' => [ ], 'DefaultRobotPolicy' => 'index,follow', 'NamespaceRobotPolicies' => [ ], 'ArticleRobotPolicies' => [ ], 'ExemptFromUserRobotsControl' => null, 'DebugAPI' => false, 'APIModules' => [ ], 'APIFormatModules' => [ ], 'APIMetaModules' => [ ], 'APIPropModules' => [ ], 'APIListModules' => [ ], 'APIMaxDBRows' => 5000, 'APIMaxResultSize' => 8388608, 'APIMaxUncachedDiffs' => 1, 'APIMaxLagThreshold' => 7, 'APICacheHelpTimeout' => 3600, 'APIUselessQueryPages' => [ 'MIMEsearch', 'LinkSearch', ], 'AjaxLicensePreview' => true, 'CrossSiteAJAXdomains' => [ ], 'CrossSiteAJAXdomainExceptions' => [ ], 'AllowedCorsHeaders' => [ 'Accept', 'Accept-Language', 'Content-Language', 'Content-Type', 'Accept-Encoding', 'DNT', 'Origin', 'User-Agent', 'Api-User-Agent', 'Promise-Non-Write-API-Action', 'Access-Control-Max-Age', 'Authorization', ], 'RestAPIAdditionalRouteFiles' => [ ], 'RestLocalModuleTestBaseUrl' => null, 'RestModuleOverrides' => [ ], 'RestExternalModules' => [ ], 'RestTermsOfServiceUrl' => null, 'MaxShellMemory' => 307200, 'MaxShellFileSize' => 102400, 'MaxShellTime' => 180, 'MaxShellWallClockTime' => 180, 'ShellCgroup' => false, 'PhpCli' => '/usr/bin/php', 'ShellRestrictionMethod' => 'autodetect', 'ShellboxUrls' => [ 'default' => null, ], 'ShellboxSecretKey' => null, 'ShellboxShell' => '/bin/sh', 'HTTPTimeout' => 25, 'HTTPConnectTimeout' => 5.0, 'HTTPMaxTimeout' => 0, 'HTTPMaxConnectTimeout' => 0, 'HTTPImportTimeout' => 25, 'HTTPUserAgentContact' => false, 'AsyncHTTPTimeout' => 25, 'HTTPProxy' => '', 'LocalVirtualHosts' => [ ], 'LocalHTTPProxy' => false, 'AllowExternalReqID' => false, 'GenerateReqIDFormat' => 'rand24', 'JobRunRate' => 1, 'RunJobsAsync' => false, 'UpdateRowsPerJob' => 300, 'UpdateRowsPerQuery' => 100, 'RedirectOnLogin' => null, 'EventRelayerConfig' => [ 'default' => [ 'class' => 'Wikimedia\\EventRelayer\\EventRelayerNull', ], ], 'Pingback' => false, 'OriginTrials' => [ ], 'ReportToExpiry' => 86400, 'ReportToEndpoints' => [ ], 'FeaturePolicyReportOnly' => [ ], 'SkinsPreferred' => [ 'vector-2022', 'vector', ], 'SpecialContributeSkinsEnabled' => [ ], 'SpecialContributeNewPageTarget' => null, 'EnableEditRecovery' => false, 'EditRecoveryExpiry' => 2592000, 'UseCodexSpecialBlock' => false, 'ShowLogoutConfirmation' => false, 'EnableProtectionIndicators' => true, 'OutputPipelineStages' => [ ], 'FeatureShutdown' => [ ], 'UseLeximorph' => false, 'UsePostprocCacheLegacy' => false, 'UsePostprocCacheParsoid' => true, 'ParserOptionsLogUnsafeSampleRate' => 0, 'ReturnExperimentalPFragmentTypes' => [ ], 'UseParsoidLinksUpdate' => null, 'UseParsoidMessages' => false, 'UseParsoidParser' => true, 'SiteLookup' => [ ], ], 'type' => [ 'ConfigRegistry' => 'object', 'AssumeProxiesUseDefaultProtocolPorts' => 'boolean', 'ForceHTTPS' => 'boolean', 'ExtensionDirectory' => [ 'string', 'null', ], 'StyleDirectory' => [ 'string', 'null', ], 'UploadDirectory' => [ 'string', 'boolean', 'null', ], 'Logos' => [ 'object', 'boolean', ], 'ReferrerPolicy' => [ 'array', 'string', 'boolean', ], 'ActionPaths' => 'object', 'MainPageIsDomainRoot' => 'boolean', 'ImgAuthUrlPathMap' => 'object', 'LocalFileRepo' => 'object', 'ForeignFileRepos' => 'array', 'UseSharedUploads' => 'boolean', 'SharedUploadDirectory' => [ 'string', 'null', ], 'SharedUploadPath' => [ 'string', 'null', ], 'HashedSharedUploadDirectory' => 'boolean', 'FetchCommonsDescriptions' => 'boolean', 'SharedUploadDBname' => [ 'boolean', 'string', ], 'SharedUploadDBprefix' => 'string', 'SharedUploadDBschema' => [ 'string', 'null', ], 'CacheSharedUploads' => 'boolean', 'ForeignUploadTargets' => 'array', 'UploadDialog' => 'object', 'FileBackends' => 'object', 'LockManagers' => 'array', 'DefaultLockManager' => [ 'string', 'null', ], 'CopyUploadsDomains' => 'array', 'CopyUploadTimeout' => [ 'boolean', 'integer', ], 'SharedThumbnailScriptPath' => [ 'string', 'boolean', ], 'HashedUploadDirectory' => 'boolean', 'CSPUploadEntryPoint' => 'boolean', 'FileExtensions' => 'array', 'ProhibitedFileExtensions' => 'array', 'MimeTypeExclusions' => 'array', 'TrustedMediaFormats' => 'array', 'MediaHandlers' => 'object', 'NativeImageLazyLoading' => 'boolean', 'ParserTestMediaHandlers' => 'object', 'MaxInterlacingAreas' => 'object', 'SVGConverters' => 'object', 'SVGNativeRendering' => [ 'string', 'boolean', ], 'MaxImageArea' => [ 'string', 'integer', 'boolean', ], 'WebPThumbnailType' => 'array', 'TiffThumbnailType' => 'array', 'GenerateThumbnailOnParse' => 'boolean', 'EnableAutoRotation' => [ 'boolean', 'null', ], 'Antivirus' => [ 'string', 'null', ], 'AntivirusSetup' => 'object', 'MimeDetectorCommand' => [ 'string', 'null', ], 'XMLMimeTypes' => 'object', 'ImageLimits' => 'array', 'ThumbLimits' => 'array', 'ThumbnailNamespaces' => 'array', 'ThumbnailSteps' => [ 'array', 'null', ], 'ThumbnailBuckets' => [ 'array', 'null', ], 'UploadThumbnailRenderMap' => 'object', 'GalleryOptions' => 'object', 'DjvuDump' => [ 'string', 'null', ], 'DjvuRenderer' => [ 'string', 'null', ], 'DjvuTxt' => [ 'string', 'null', ], 'DjvuPostProcessor' => [ 'string', 'null', ], 'SMTP' => [ 'boolean', 'object', ], 'EnotifFromEditor' => 'boolean', 'EmailConfirmationBanner' => 'boolean', 'EnotifRevealEditorAddress' => 'boolean', 'UsersNotifiedOnAllChanges' => 'object', 'DBmwschema' => [ 'string', 'null', ], 'SharedTables' => 'array', 'DBservers' => [ 'boolean', 'array', ], 'LBFactoryConf' => 'object', 'LocalDatabases' => 'array', 'VirtualDomainsMapping' => 'object', 'RemoteVirtualDomainsMapping' => 'object', 'FileSchemaMigrationStage' => 'integer', 'ExternalLinksDomainGaps' => 'object', 'ContentHandlers' => 'object', 'NamespaceContentModels' => 'object', 'TextModelsToParse' => 'array', 'ExternalStores' => 'array', 'ExternalServers' => 'object', 'DefaultExternalStore' => [ 'array', 'boolean', ], 'RevisionCacheExpiry' => 'integer', 'PageLanguageUseDB' => 'boolean', 'DiffEngine' => [ 'string', 'null', ], 'ExternalDiffEngine' => [ 'string', 'boolean', ], 'Wikidiff2Options' => 'object', 'RequestTimeLimit' => [ 'integer', 'null', ], 'CriticalSectionTimeLimit' => 'number', 'PoolCounterConf' => [ 'object', 'null', ], 'PoolCountClientConf' => 'object', 'MaxUserDBWriteDuration' => [ 'integer', 'boolean', ], 'MaxJobDBWriteDuration' => [ 'integer', 'boolean', ], 'MultiShardSiteStats' => 'boolean', 'ObjectCaches' => 'object', 'WANObjectCache' => 'object', 'MicroStashType' => [ 'string', 'integer', ], 'ParsoidCacheConfig' => 'object', 'ParsoidSelectiveUpdateSampleRate' => 'integer', 'SplitParsoidParserCache' => 'boolean', 'ParserCacheFilterConfig' => 'object', 'ChronologyProtectorSecret' => 'string', 'SuspiciousIpExpiry' => [ 'integer', 'boolean', ], 'MemCachedServers' => 'array', 'LocalisationCacheConf' => 'object', 'ExtensionInfoMTime' => [ 'integer', 'boolean', ], 'CdnServers' => 'object', 'CdnServersNoPurge' => 'object', 'HTCPRouting' => 'object', 'GrammarForms' => 'object', 'ExtraInterlanguageLinkPrefixes' => 'array', 'InterlanguageLinkCodeMap' => 'object', 'ExtraLanguageNames' => 'object', 'ExtraLanguageCodes' => 'object', 'DummyLanguageCodes' => 'object', 'DisabledVariants' => 'object', 'ForceUIMsgAsContentMsg' => 'object', 'RawHtmlMessages' => 'array', 'OverrideUcfirstCharacters' => 'object', 'XhtmlNamespaces' => 'object', 'BrowserFormatDetection' => 'string', 'SkinMetaTags' => 'object', 'SkipSkins' => 'object', 'FragmentMode' => 'array', 'FooterIcons' => 'object', 'PageShareSkinsEnabled' => 'array', 'InterwikiLogoOverride' => 'array', 'ResourceModules' => 'object', 'ResourceModuleSkinStyles' => 'object', 'ResourceLoaderSources' => 'object', 'ResourceLoaderMaxage' => 'object', 'ResourceLoaderMaxQueryLength' => [ 'integer', 'boolean', ], 'CanonicalNamespaceNames' => 'object', 'ExtraNamespaces' => 'object', 'ExtraGenderNamespaces' => 'object', 'NamespaceAliases' => 'object', 'CapitalLinkOverrides' => 'object', 'NamespacesWithSubpages' => 'object', 'NamespacesWithoutAutoSummaries' => 'array', 'ContentNamespaces' => 'array', 'ShortPagesNamespaceExclusions' => 'array', 'ExtraSignatureNamespaces' => 'array', 'InvalidRedirectTargets' => 'array', 'LocalInterwikis' => 'array', 'InterwikiCache' => [ 'boolean', 'object', ], 'SiteTypes' => 'object', 'UrlProtocols' => 'array', 'TidyConfig' => 'object', 'ParsoidSettings' => 'object', 'ParsoidExperimentalParserFunctionOutput' => 'boolean', 'NoFollowNsExceptions' => 'array', 'NoFollowDomainExceptions' => 'array', 'ExternalLinksIgnoreDomains' => 'array', 'EnableMagicLinks' => 'object', 'ManualRevertSearchRadius' => 'integer', 'RevertedTagMaxDepth' => 'integer', 'CentralIdLookupProviders' => 'object', 'CentralIdLookupProvider' => 'string', 'UserRegistrationProviders' => 'object', 'PasswordPolicy' => 'object', 'AuthManagerConfig' => [ 'object', 'null', ], 'AuthManagerAutoConfig' => 'object', 'RememberMe' => 'string', 'ReauthenticateTime' => 'object', 'ChangeCredentialsBlacklist' => 'array', 'RemoveCredentialsBlacklist' => 'array', 'PasswordConfig' => 'object', 'PasswordResetRoutes' => 'object', 'SignatureAllowedLintErrors' => 'array', 'ReservedUsernames' => 'array', 'DefaultUserOptions' => 'object', 'ConditionalUserOptions' => 'object', 'HiddenPrefs' => 'array', 'UserJsPrefLimit' => 'integer', 'AuthenticationTokenVersion' => [ 'string', 'null', ], 'SessionProviders' => 'object', 'AutoCreateTempUser' => 'object', 'AutoblockExemptions' => 'array', 'BlockCIDRLimit' => 'object', 'EnableMultiBlocks' => 'boolean', 'GroupPermissions' => 'object', 'PrivilegedGroups' => 'array', 'RevokePermissions' => 'object', 'GroupInheritsPermissions' => 'object', 'ImplicitGroups' => 'array', 'GroupsAddToSelf' => 'object', 'GroupsRemoveFromSelf' => 'object', 'RestrictedGroups' => 'object', 'UserRequirementsPrivateConditions' => 'array', 'RestrictionTypes' => 'array', 'RestrictionLevels' => 'array', 'CascadingRestrictionLevels' => 'array', 'SemiprotectedRestrictionLevels' => 'array', 'NamespaceProtection' => 'object', 'RestrictUserPageEditing' => 'boolean', 'NonincludableNamespaces' => 'object', 'Autopromote' => 'object', 'AutopromoteOnce' => 'object', 'AutopromoteOnceRCExcludedGroups' => 'array', 'AddGroups' => 'object', 'RemoveGroups' => 'object', 'AvailableRights' => 'array', 'ImplicitRights' => 'array', 'AccountCreationThrottle' => [ 'integer', 'array', ], 'TempAccountCreationThrottle' => 'array', 'TempAccountNameAcquisitionThrottle' => 'array', 'SpamRegex' => 'array', 'SummarySpamRegex' => 'array', 'DnsBlacklistUrls' => 'array', 'ProxyList' => [ 'string', 'array', ], 'ProxyWhitelist' => 'array', 'SoftBlockRanges' => 'array', 'RateLimits' => 'object', 'RateLimitsExcludedIPs' => 'array', 'ExternalQuerySources' => 'object', 'PasswordAttemptThrottle' => 'array', 'GrantPermissions' => 'object', 'GrantPermissionGroups' => 'object', 'GrantRiskGroups' => 'object', 'EnableBotPasswords' => 'boolean', 'BotPasswordsCluster' => [ 'string', 'boolean', ], 'BotPasswordsDatabase' => [ 'string', 'boolean', ], 'BotPasswordsLimit' => 'integer', 'ReauthenticateForActions' => 'object', 'CSPHeader' => [ 'boolean', 'object', ], 'CSPReportOnlyHeader' => [ 'boolean', 'object', ], 'CSPUseReportURIDirective' => [ 'boolean', 'object', ], 'CSPFalsePositiveUrls' => 'object', 'AllowCrossOrigin' => 'boolean', 'RestAllowCrossOriginCookieAuth' => 'boolean', 'CookieSameSite' => [ 'string', 'null', ], 'CacheVaryCookies' => 'array', 'TrxProfilerLimits' => 'object', 'DebugLogGroups' => 'object', 'MWLoggerDefaultSpi' => 'object', 'Profiler' => 'object', 'StatsTarget' => [ 'string', 'null', ], 'StatsFormat' => [ 'string', 'null', ], 'StatsPrefix' => 'string', 'OpenTelemetryConfig' => [ 'object', 'null', ], 'OpenSearchTemplates' => 'object', 'NamespacesToBeSearchedDefault' => 'object', 'SitemapNamespaces' => [ 'boolean', 'array', ], 'SitemapNamespacesPriorities' => [ 'boolean', 'object', ], 'SitemapApiConfig' => 'object', 'SpecialSearchFormOptions' => 'object', 'SearchMatchRedirectPreference' => 'boolean', 'SearchRunSuggestedQuery' => 'boolean', 'PreviewOnOpenNamespaces' => 'object', 'ReadOnlyWatchedItemStore' => 'boolean', 'GitRepositoryViewers' => 'object', 'InstallerInitialPages' => 'array', 'RCLinkLimits' => 'array', 'RCLinkDays' => 'array', 'RCFeeds' => 'object', 'OverrideSiteFeed' => 'object', 'FeedClasses' => 'object', 'AdvertisedFeedTypes' => 'array', 'SoftwareTags' => 'object', 'RestrictedTagViewRights' => 'object', 'RecentChangesFlags' => 'object', 'WatchlistExpiry' => 'boolean', 'EnableWatchstarPopover' => 'boolean', 'EnableWatchlistLabels' => 'boolean', 'WatchlistLabelsMaxPerUser' => 'integer', 'WatchlistPurgeRate' => 'number', 'WatchlistExpiryMaxDuration' => [ 'string', 'null', ], 'EnableChangesListQueryPartitioning' => 'boolean', 'ImportSources' => 'object', 'ExtensionFunctions' => 'array', 'ExtensionMessagesFiles' => 'object', 'MessagesDirs' => 'object', 'TranslationAliasesDirs' => 'object', 'ExtensionEntryPointListFiles' => 'object', 'ValidSkinNames' => 'object', 'SpecialPages' => 'object', 'ExtensionCredits' => 'object', 'Hooks' => 'object', 'ServiceWiringFiles' => 'array', 'JobClasses' => 'object', 'JobTypesExcludedFromDefaultQueue' => 'array', 'JobBackoffThrottling' => 'object', 'JobTypeConf' => 'object', 'SpecialPageCacheUpdates' => 'object', 'PagePropLinkInvalidations' => 'object', 'TempCategoryCollations' => 'array', 'SortedCategories' => 'boolean', 'TrackingCategories' => 'array', 'LogTypes' => 'array', 'LogRestrictions' => 'object', 'FilterLogTypes' => 'object', 'LogNames' => 'object', 'LogHeaders' => 'object', 'LogActions' => 'object', 'LogActionsHandlers' => 'object', 'ActionFilteredLogs' => 'object', 'RangeContributionsCIDRLimit' => 'object', 'Actions' => 'object', 'NamespaceRobotPolicies' => 'object', 'ArticleRobotPolicies' => 'object', 'ExemptFromUserRobotsControl' => [ 'array', 'null', ], 'APIModules' => 'object', 'APIFormatModules' => 'object', 'APIMetaModules' => 'object', 'APIPropModules' => 'object', 'APIListModules' => 'object', 'APIUselessQueryPages' => 'array', 'CrossSiteAJAXdomains' => 'object', 'CrossSiteAJAXdomainExceptions' => 'object', 'AllowedCorsHeaders' => 'array', 'RestAPIAdditionalRouteFiles' => 'array', 'RestLocalModuleTestBaseUrl' => [ 'string', 'null', ], 'RestModuleOverrides' => 'object', 'RestExternalModules' => 'object', 'RestTermsOfServiceUrl' => [ 'string', 'null', ], 'ShellRestrictionMethod' => [ 'string', 'boolean', ], 'ShellboxUrls' => 'object', 'ShellboxSecretKey' => [ 'string', 'null', ], 'ShellboxShell' => [ 'string', 'null', ], 'HTTPTimeout' => 'number', 'HTTPConnectTimeout' => 'number', 'HTTPMaxTimeout' => 'number', 'HTTPMaxConnectTimeout' => 'number', 'HTTPUserAgentContact' => [ 'string', 'boolean', ], 'LocalVirtualHosts' => 'object', 'LocalHTTPProxy' => [ 'string', 'boolean', ], 'GenerateReqIDFormat' => 'string', 'EventRelayerConfig' => 'object', 'Pingback' => 'boolean', 'OriginTrials' => 'array', 'ReportToExpiry' => 'integer', 'ReportToEndpoints' => 'array', 'FeaturePolicyReportOnly' => 'array', 'SkinsPreferred' => 'array', 'SpecialContributeSkinsEnabled' => 'array', 'SpecialContributeNewPageTarget' => [ 'string', 'null', ], 'EnableEditRecovery' => 'boolean', 'EditRecoveryExpiry' => 'integer', 'UseCodexSpecialBlock' => 'boolean', 'ShowLogoutConfirmation' => 'boolean', 'EnableProtectionIndicators' => 'boolean', 'OutputPipelineStages' => 'object', 'FeatureShutdown' => 'array', 'UseLeximorph' => 'boolean', 'UsePostprocCacheLegacy' => 'boolean', 'UsePostprocCacheParsoid' => 'boolean', 'ParserOptionsLogUnsafeSampleRate' => 'integer', 'ReturnExperimentalPFragmentTypes' => 'array', 'UseParsoidLinksUpdate' => [ 'boolean', 'null', ], 'UseParsoidMessages' => [ 'boolean', 'null', ], 'UseParsoidParser' => 'boolean', 'SiteLookup' => 'object', ], 'mergeStrategy' => [ 'WebPThumbnailType' => 'replace', 'TiffThumbnailType' => 'replace', 'LBFactoryConf' => 'replace', 'InterwikiCache' => 'replace', 'PasswordPolicy' => 'array_replace_recursive', 'AuthManagerAutoConfig' => 'array_plus_2d', 'GroupPermissions' => 'array_plus_2d', 'RevokePermissions' => 'array_plus_2d', 'AddGroups' => 'array_merge_recursive', 'RemoveGroups' => 'array_merge_recursive', 'RateLimits' => 'array_plus_2d', 'GrantPermissions' => 'array_plus_2d', 'MWLoggerDefaultSpi' => 'replace', 'Profiler' => 'replace', 'Hooks' => 'array_merge_recursive', 'RestModuleOverrides' => 'array_replace_recursive', 'RestExternalModules' => 'array_replace_recursive', ], 'dynamicDefault' => [ 'UsePathInfo' => [ 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultUsePathInfo', ], ], 'Script' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultScript', ], ], 'LoadScript' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLoadScript', ], ], 'RestPath' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultRestPath', ], ], 'StylePath' => [ 'use' => [ 'ResourceBasePath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultStylePath', ], ], 'LocalStylePath' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLocalStylePath', ], ], 'ExtensionAssetsPath' => [ 'use' => [ 'ResourceBasePath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultExtensionAssetsPath', ], ], 'ArticlePath' => [ 'use' => [ 'Script', 'UsePathInfo', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultArticlePath', ], ], 'UploadPath' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultUploadPath', ], ], 'FileCacheDirectory' => [ 'use' => [ 'UploadDirectory', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultFileCacheDirectory', ], ], 'Logo' => [ 'use' => [ 'ResourceBasePath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLogo', ], ], 'DeletedDirectory' => [ 'use' => [ 'UploadDirectory', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultDeletedDirectory', ], ], 'ShowEXIF' => [ 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultShowEXIF', ], ], 'SharedPrefix' => [ 'use' => [ 'DBprefix', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultSharedPrefix', ], ], 'SharedSchema' => [ 'use' => [ 'DBmwschema', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultSharedSchema', ], ], 'DBerrorLogTZ' => [ 'use' => [ 'Localtimezone', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultDBerrorLogTZ', ], ], 'Localtimezone' => [ 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLocaltimezone', ], ], 'LocalTZoffset' => [ 'use' => [ 'Localtimezone', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLocalTZoffset', ], ], 'ResourceBasePath' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultResourceBasePath', ], ], 'MetaNamespace' => [ 'use' => [ 'Sitename', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultMetaNamespace', ], ], 'CookieSecure' => [ 'use' => [ 'ForceHTTPS', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultCookieSecure', ], ], 'CookiePrefix' => [ 'use' => [ 'SharedDB', 'SharedPrefix', 'SharedTables', 'DBname', 'DBprefix', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultCookiePrefix', ], ], 'ReadOnlyFile' => [ 'use' => [ 'UploadDirectory', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultReadOnlyFile', ], ], ], ], 'config-schema' => [ 'UploadStashScalerBaseUrl' => [ 'deprecated' => 'since 1.36 Use thumbProxyUrl in $wgLocalFileRepo', ], 'IllegalFileChars' => [ 'deprecated' => 'since 1.41; no longer customizable', ], 'ThumbnailNamespaces' => [ 'items' => [ 'type' => 'integer', ], ], 'LocalDatabases' => [ 'items' => [ 'type' => 'string', ], ], 'ParserCacheFilterConfig' => [ 'additionalProperties' => [ 'type' => 'object', 'description' => 'A map of namespace IDs to filter definitions.', 'additionalProperties' => [ 'type' => 'object', 'description' => 'A map of filter names to values.', 'properties' => [ 'minCpuTime' => [ 'type' => 'number', ], ], ], ], ], 'RawHtmlMessages' => [ 'items' => [ 'type' => 'string', ], ], 'InterwikiLogoOverride' => [ 'items' => [ 'type' => 'string', ], ], 'LegalTitleChars' => [ 'deprecated' => 'since 1.41; use Extension:TitleBlacklist to customize', ], 'ReauthenticateTime' => [ 'additionalProperties' => [ 'type' => [ 'integer', 'array', ], ], ], 'ChangeCredentialsBlacklist' => [ 'items' => [ 'type' => 'string', ], ], 'RemoveCredentialsBlacklist' => [ 'items' => [ 'type' => 'string', ], ], 'GroupPermissions' => [ 'additionalProperties' => [ 'type' => 'object', 'additionalProperties' => [ 'type' => 'boolean', ], ], ], 'GroupInheritsPermissions' => [ 'additionalProperties' => [ 'type' => 'string', ], ], 'AvailableRights' => [ 'items' => [ 'type' => 'string', ], ], 'ImplicitRights' => [ 'items' => [ 'type' => 'string', ], ], 'SoftBlockRanges' => [ 'items' => [ 'type' => 'string', ], ], 'ExternalQuerySources' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'enabled' => [ 'type' => 'boolean', 'default' => false, ], 'url' => [ 'type' => 'string', 'format' => 'uri', ], 'timeout' => [ 'type' => 'integer', 'default' => 10, ], ], 'required' => [ 'enabled', 'url', ], 'additionalProperties' => false, ], ], 'GrantPermissions' => [ 'additionalProperties' => [ 'type' => 'object', 'additionalProperties' => [ 'type' => 'boolean', ], ], ], 'GrantPermissionGroups' => [ 'additionalProperties' => [ 'type' => 'string', ], ], 'SitemapNamespacesPriorities' => [ 'deprecated' => 'since 1.45 and ignored', ], 'SitemapApiConfig' => [ 'additionalProperties' => [ 'enabled' => [ 'type' => 'bool', ], 'sitemapsPerIndex' => [ 'type' => 'int', ], 'pagesPerSitemap' => [ 'type' => 'int', ], 'expiry' => [ 'type' => 'int', ], 'skipRedirects' => [ 'type' => 'bool', ], ], ], 'SoftwareTags' => [ 'additionalProperties' => [ 'type' => 'boolean', ], ], 'UseCopyrightUpload' => [ 'deprecated' => 'since 1.47 This feature is being removed.', ], 'JobBackoffThrottling' => [ 'additionalProperties' => [ 'type' => 'number', ], ], 'JobTypeConf' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'class' => [ 'type' => 'string', ], 'order' => [ 'type' => 'string', ], 'claimTTL' => [ 'type' => 'integer', ], ], ], ], 'TrackingCategories' => [ 'deprecated' => 'since 1.25 Extensions should now register tracking categories using the new extension registration system.', ], 'RangeContributionsCIDRLimit' => [ 'additionalProperties' => [ 'type' => 'integer', ], ], 'RestModuleOverrides' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'availability' => [ 'type' => 'string', ], 'groups' => [ 'type' => 'array', ], ], ], ], 'RestExternalModules' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'info' => [ 'type' => 'object', 'properties' => [ 'version' => [ 'type' => 'string', ], 'title' => [ 'type' => 'string', ], 'x-i18n-title' => [ 'type' => 'string', ], 'description' => [ 'type' => 'string', ], 'x-i18n-description' => [ 'type' => 'string', ], ], 'required' => [ 'version', ], ], 'base' => [ 'type' => 'string', 'format' => 'uri', ], 'spec' => [ 'type' => 'string', 'format' => 'uri', ], ], 'required' => [ 'info', 'base', 'spec', ], ], ], 'ShellboxUrls' => [ 'additionalProperties' => [ 'type' => [ 'string', 'boolean', 'null', ], ], ], ], 'obsolete-config' => [ 'MangleFlashPolicy' => 'Since 1.39; no longer has any effect.', 'EnableOpenSearchSuggest' => 'Since 1.35, no longer used', 'AutoloadAttemptLowercase' => 'Since 1.40; no longer has any effect.', ],]
Interface for configuration instances.
Definition Config.php:18
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
Content objects represent page content, e.g.
Definition Content.php:28
getModel()
Get the content model ID.
Interface for objects which can provide a MediaWiki context on request.
Serves as a common repository of constants for EditPage edit status results.
Interface for localizing messages in MediaWiki.
msg( $key,... $params)
This is the method for getting translated interface messages.
Interface for objects (potentially) representing an editable wiki page.
Interface for objects (potentially) representing a page that can be viewable and linked to on a wiki.
This interface represents the authority associated with the current execution context,...
Definition Authority.php:23
Interface for objects representing user identity.
Provide primary and replica IDatabase connections.
setStashKey(string $keyValue)
doReauthRedirect(PermissionStatus $status, array $queryParams, ?string $subaction=null)
Redirect to the reauth flow on Special:UserLogin.
trait DataStashTrait
Securely stash user-posted data in the session, modeled on AuthManagerSpecialPage::handleReauthBefore...
enableReauthPopup(string $jsPopupModule, ?string $operation, bool $currentlyRequired=true)
ParserOutputLinkTypes
Registry of flags used with ParserOutput::{getLinkList,appendLink}() within MediaWiki core.
msg( $key,... $params)