MediaWiki master
EditPage.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\EditPage;
8
9use BadMethodCallException;
39use 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\Timestamp\ConvertibleTimestamp;
106use Wikimedia\Timestamp\TimestampFormat as TS;
107
130#[\AllowDynamicProperties]
131class EditPage implements IEditObject {
132 use ProtectedHookAccessorTrait;
133
137 public const UNICODE_CHECK = 'ℳ𝒲β™₯π“Šπ“ƒπ’Ύπ’Έβ„΄π’Ήβ„―';
138
142 public const EDITFORM_ID = 'editform';
143
148 public const POST_EDIT_COOKIE_KEY_PREFIX = 'PostEditRevision';
149
166 public const POST_EDIT_COOKIE_DURATION = 1200;
167
171 private $mArticle;
172
174 private $page;
175
177 private $mContextTitle = null;
178
183 public bool $isConflict = false;
184
186 private bool $isNew = false;
187
189 public $formtype;
190
196
197 private bool $mTokenOk = false;
198
199 private bool $mTriedSave = false;
200
201 private bool $incompleteForm = false;
202
203 private bool $missingSummary = false;
204
205 private bool $allowBlankSummary = false;
206
207 private bool $blankArticle = false;
208
209 private bool $allowBlankArticle = false;
210
212 private $problematicRedirectTarget = null;
213
215 private $allowedProblematicRedirectTarget = null;
216
217 private bool $ignoreProblematicRedirects = false;
218
219 private string $autoSumm = '';
220
221 private string $hookError = '';
222
223 private ?ParserOutput $mParserOutput = null;
224
225 public bool $mShowSummaryField = true;
226
227 # Form values
228
229 public bool $save = false;
230
231 public bool $preview = false;
232
233 private bool $diff = false;
234
235 private bool $minoredit = false;
236
237 private bool $watchthis = false;
238
240 private bool $watchlistExpiryEnabled;
241
243 private ?string $watchlistExpiry = null;
244
246 private bool $watchlistLabelsEnabled;
247
249 private array $watchlistLabels = [];
250
251 private bool $recreate = false;
252
253 private bool $ignoreRevisionDeletedWarning = false;
254
258 public string $textbox1 = '';
259
260 public string $summary = '';
261
263 private bool $nosummary = false;
264
269 public ?string $edittime = '';
270
283 private ?int $editRevId = null;
284
285 public string $section = '';
286
287 public ?string $sectiontitle = null;
288
289 private ?string $newSectionAnchor = null;
290
294 public ?string $starttime = '';
295
302 public int $oldid = 0;
303
310 private int $parentRevId = 0;
311
312 private ?int $scrolltop = null;
313
314 private bool $markAsBot = true;
315
316 public string $contentModel;
317
318 public ?string $contentFormat = null;
319
320 private array $changeTags = [];
321
322 # Placeholders for text injection by hooks (must be HTML)
323 # extensions should take care to _append_ to the present value
324
326 public $editFormPageTop = '';
328 public $editFormTextTop = '';
341
343 public bool $didSave = false;
344 public int $undidRev = 0;
345 private int $undoAfter = 0;
346
347 public bool $suppressIntro = false;
348
349 private int|false $contentLength = false;
350
354 private bool $enableApiEditOverride = false;
355
357
359 private bool $isOldRev = false;
360
362 private string $unicodeCheck = '';
363
365 private $editConflictHelperFactory = null;
366 private ?TextConflictHelper $editConflictHelper = null;
367
368 private AuthManager $authManager;
369 private EditConstraintFactory $constraintFactory;
370 private IContentHandlerFactory $contentHandlerFactory;
371 private IConnectionProvider $dbProvider;
372 private LinkBatchFactory $linkBatchFactory;
373 private LinkRenderer $linkRenderer;
374 private PageEditFactory $pageEditFactory;
375 private PageEditingHelper $pageEditingHelper;
376 private PermissionManager $permManager;
377 private RestrictionStore $restrictionStore;
378 private RevisionStore $revisionStore;
379 private SessionManager $sessionManager;
380 private ShadowPageLoader $shadowPageLoader;
381 private TempUserCreator $tempUserCreator;
382 private TextboxBuilder $textboxBuilder;
383 private UserEditTracker $userEditTracker;
384 private UserFactory $userFactory;
385 private UserIdentityUtils $userIdentityUtils;
386 private UserOptionsLookup $userOptionsLookup;
387 private UserRegistrationLookup $userRegistrationLookup;
388 private WatchedItemStoreInterface $watchedItemStore;
389 private WatchlistLabelStore $watchlistLabelStore;
390 private WatchlistManager $watchlistManager;
391
393 private $placeholderTempUser;
394
396 private $unsavedTempUser;
397
399 private $savedTempUser;
400
402 private bool $tempUserCreateActive = false;
403
405 private ?string $tempUserName = null;
406
408 private bool $tempUserCreateDone = false;
409
411 private bool $unableToAcquireTempName = false;
412
417 public function __construct( Article $article ) {
418 $this->mArticle = $article;
419 $this->page = $article->getPage(); // model object
420
421 // Make sure the local context is in sync with other member variables.
422 // Particularly make sure everything is using the same WikiPage instance.
423 // This should probably be the case in Article as well, but it's
424 // particularly important for EditPage, to make use of the in-place caching
425 // facility in WikiPage::prepareContentForEdit.
426 $this->context = new DerivativeContext( $article->getContext() );
427 $this->context->setWikiPage( $this->page );
428
429 $services = MediaWikiServices::getInstance();
430 $this->authManager = $services->getAuthManager();
431 $this->constraintFactory = $services->getService( '_EditConstraintFactory' );
432 $this->contentHandlerFactory = $services->getContentHandlerFactory();
433 $this->dbProvider = $services->getConnectionProvider();
434 $this->linkBatchFactory = $services->getLinkBatchFactory();
435 $this->linkRenderer = $services->getLinkRenderer();
436 $this->pageEditFactory = $services->getService( '_PageEditFactory' );
437 $this->pageEditingHelper = $services->getService( '_PageEditingHelper' );
438 $this->permManager = $services->getPermissionManager();
439 $this->restrictionStore = $services->getRestrictionStore();
440 $this->revisionStore = $services->getRevisionStore();
441 $this->sessionManager = $services->getSessionManager();
442 $this->shadowPageLoader = $services->getShadowPageLoader();
443 $this->tempUserCreator = $services->getTempUserCreator();
444 $this->textboxBuilder = $services->getTextboxBuilder();
445 $this->userEditTracker = $services->getUserEditTracker();
446 $this->userFactory = $services->getUserFactory();
447 $this->userIdentityUtils = $services->getUserIdentityUtils();
448 $this->userOptionsLookup = $services->getUserOptionsLookup();
449 $this->userRegistrationLookup = $services->getUserRegistrationLookup();
450 $this->watchedItemStore = $services->getWatchedItemStore();
451 $this->watchlistLabelStore = $services->getWatchlistLabelStore();
452 $this->watchlistManager = $services->getWatchlistManager();
453
454 $this->watchlistExpiryEnabled = $this->getContext()->getConfig() instanceof Config
455 && $this->getContext()->getConfig()->get( MainConfigNames::WatchlistExpiry );
456 $this->watchlistLabelsEnabled = $this->getContext()->getConfig() instanceof Config
457 && $this->getContext()->getConfig()->get( MainConfigNames::EnableWatchlistLabels );
458
459 $this->contentModel = $this->getTitle()->getContentModel();
460 $this->contentFormat = $this->contentHandlerFactory
461 ->getContentHandler( $this->contentModel )
462 ->getDefaultFormat();
463 }
464
468 public function getArticle() {
469 return $this->mArticle;
470 }
471
476 public function getContext() {
477 return $this->context;
478 }
479
484 public function getTitle() {
485 return $this->page->getTitle();
486 }
487
491 public function setContextTitle( $title ) {
492 $this->mContextTitle = $title;
493 }
494
499 public function getContextTitle() {
500 if ( $this->mContextTitle === null ) {
501 throw new RuntimeException( "EditPage does not have a context title set" );
502 } else {
503 return $this->mContextTitle;
504 }
505 }
506
512 public function setApiEditOverride( bool $enableOverride ): void {
513 $this->enableApiEditOverride = $enableOverride;
514 }
515
527 public function edit() {
528 // Allow extensions to modify/prevent this form or submission
529 if ( !$this->getHookRunner()->onAlternateEdit( $this ) ) {
530 return;
531 }
532
533 wfDebug( __METHOD__ . ": enter" );
534
535 $request = $this->context->getRequest();
536 // If they used redlink=1 and the page exists, redirect to the main article
537 if ( $request->getBool( 'redlink' ) && $this->page->exists() ) {
538 $this->context->getOutput()->redirect( $this->getTitle()->getFullURL() );
539 return;
540 }
541
542 $this->importFormData();
543 $this->firsttime = false;
544
545 $readOnlyMode = MediaWikiServices::getInstance()->getReadOnlyMode();
546 if ( $this->save && $readOnlyMode->isReadOnly() ) {
547 // Force preview
548 $this->save = false;
549 $this->preview = true;
550 }
551
552 if ( $this->save ) {
553 $this->formtype = 'save';
554 } elseif ( $this->preview ) {
555 $this->formtype = 'preview';
556 } elseif ( $this->diff ) {
557 $this->formtype = 'diff';
558 } else { # First time through
559 $this->firsttime = true;
560 if ( $this->previewOnOpen() ) {
561 $this->formtype = 'preview';
562 } else {
563 $this->formtype = 'initial';
564 }
565 }
566
567 // Check permissions after possibly creating a placeholder temp user.
568 // This allows anonymous users to edit via a temporary account, if the site is
569 // configured to (1) disallow anonymous editing and (2) autocreate temporary
570 // accounts on edit.
571 $this->unableToAcquireTempName = !$this->maybeActivateTempUserCreate( !$this->firsttime )->isOK();
572
573 $status = $this->getEditPermissionStatus(
574 $this->save ? PermissionManager::RIGOR_SECURE : PermissionManager::RIGOR_FULL
575 );
576 if ( !$status->isGood() ) {
577 wfDebug( __METHOD__ . ": User can't edit" );
578
579 $user = $this->context->getUser();
580 if ( $user->getBlock() && !$readOnlyMode->isReadOnly() ) {
581 // Auto-block user's IP if the account was "hard" blocked
582 $user->scheduleSpreadBlock();
583 }
584 $this->displayPermissionStatus( $status );
585
586 return;
587 }
588
589 $revRecord = $this->mArticle->fetchRevisionRecord();
590 // Disallow editing revisions with content models different from the current one
591 // Undo edits being an exception in order to allow reverting content model changes.
592 $revContentModel = $revRecord ?
593 $revRecord->getMainContentModel() :
594 false;
595 if ( $revContentModel && $revContentModel !== $this->contentModel ) {
596 $prevRevRecord = null;
597 $prevContentModel = false;
598 if ( $this->undidRev ) {
599 $undidRevRecord = $this->revisionStore
600 ->getRevisionById( $this->undidRev );
601 $prevRevRecord = $undidRevRecord ?
602 $this->revisionStore->getPreviousRevision( $undidRevRecord ) :
603 null;
604
605 $prevContentModel = $prevRevRecord ?
606 $prevRevRecord->getMainContentModel() :
607 '';
608 }
609
610 if ( !$this->undidRev
611 || !$prevRevRecord
612 || $prevContentModel !== $this->contentModel
613 ) {
614 $this->displayViewSourcePage(
615 $this->getContentObject(),
616 $this->context->msg(
617 'contentmodelediterror',
618 $revContentModel,
619 $this->contentModel
620 )->plain()
621 );
622 return;
623 }
624 }
625
626 $this->isConflict = false;
627
628 # Attempt submission here. This will check for edit conflicts,
629 # and redundantly check for locked database, blocked IPs, etc.
630 # that edit() already checked just in case someone tries to sneak
631 # in the back door with a hand-edited submission URL.
632
633 if ( $this->formtype === 'save' ) {
634 $resultDetails = null;
635 $status = $this->attemptSave( $resultDetails );
636 if ( !( $status instanceof PageEditStatus ) ) {
637 // Hooks and subclasses can cause attemptSave to return a normal Status, so cast it if necessary
638 $status = PageEditStatus::cast( $status );
639 }
640 if ( !$this->handleStatus( $status, $resultDetails ) ) {
641 return;
642 }
643 }
644
645 # First time through: get contents, set time for conflict
646 # checking, etc.
647 if ( $this->formtype === 'initial' || $this->firsttime ) {
648 if ( !$this->initialiseForm() ) {
649 return;
650 }
651
652 if ( $this->page->getId() ) {
653 $this->getHookRunner()->onEditFormInitialText( $this );
654 }
655 }
656
657 // If we're displaying an old revision, and there are differences between it and the
658 // latest revision outside the main slot, then we can't allow the old revision to be
659 // editable, as what would happen to the non-main-slot data if someone saves the old
660 // revision is undefined.
661 // When this is the case, display a read-only version of the page instead, with a link
662 // to a diff page from which the old revision can be restored
663 $curRevisionRecord = $this->page->getRevisionRecord();
664 if ( $curRevisionRecord
665 && $revRecord
666 && $curRevisionRecord->getId() !== $revRecord->getId()
667 && ( WikiPage::hasDifferencesOutsideMainSlot(
668 $revRecord,
669 $curRevisionRecord
670 ) || !$this->pageEditingHelper->isSupportedContentModel(
671 $revRecord->getSlot(
672 SlotRecord::MAIN,
673 RevisionRecord::RAW
674 )->getModel(),
675 $this->enableApiEditOverride
676 ) )
677 ) {
678 $restoreLink = $this->getTitle()->getFullURL(
679 [
680 'action' => 'mcrrestore',
681 'restore' => $revRecord->getId(),
682 ]
683 );
684 $this->displayViewSourcePage(
685 $this->getContentObject(),
686 $this->context->msg(
687 'nonmain-slot-differences-therefore-readonly',
688 $restoreLink
689 )->plain()
690 );
691 return;
692 }
693
694 $this->showEditForm();
695 }
696
706 public function maybeActivateTempUserCreate( $doAcquire ): Status {
707 if ( $this->tempUserCreateActive ) {
708 // Already done
709 return Status::newGood();
710 }
711 $user = $this->context->getUser();
712
713 // Log out any user using an expired temporary account, so that we can give them a new temporary account.
714 // As described in T389485, we need to do this because the maintenance script to expire temporary accounts
715 // may fail to run or not be configured to run.
716 if ( $user->isTemp() ) {
717 $expireAfterDays = $this->tempUserCreator->getExpireAfterDays();
718 if ( $expireAfterDays ) {
719 $expirationCutoff = (int)ConvertibleTimestamp::now( TS::UNIX ) - ( 86_400 * $expireAfterDays );
720
721 // If the user was created before the expiration cutoff, then log them out, expire any other existing
722 // sessions, and revoke any access to the account that may exist.
723 // If no registration is set then do nothing, as if registration date system is broken it would
724 // cause a new temporary account for each edit.
725 $firstUserRegistration = $this->userRegistrationLookup->getFirstRegistration( $user );
726 if (
727 $firstUserRegistration &&
728 ConvertibleTimestamp::convert( TS::UNIX, $firstUserRegistration ) < $expirationCutoff
729 ) {
730 // Log the user out of the expired temporary account.
731 $user->logout();
732
733 // Clear any stashed temporary account name (if any is set), as we want a new name for the user.
734 $session = $this->context->getRequest()->getSession();
735 $session->set( 'TempUser:name', null );
736 $session->save();
737
738 // Invalidate any sessions for the expired temporary account
739 $this->sessionManager->invalidateSessionsForUser(
740 $this->userFactory->newFromUserIdentity( $user )
741 );
742 }
743 }
744 }
745
746 if ( $this->tempUserCreator->shouldAutoCreate( $user, 'edit' ) ) {
747 if ( $doAcquire ) {
748 $name = $this->tempUserCreator->acquireAndStashName(
749 $this->context->getRequest()->getSession() );
750 if ( $name === null ) {
751 $status = Status::newFatal( 'temp-user-unable-to-acquire' );
752 $status->value = self::AS_UNABLE_TO_ACQUIRE_TEMP_ACCOUNT;
753 return $status;
754 }
755 $this->unsavedTempUser = $this->userFactory->newUnsavedTempUser( $name );
756 $this->tempUserName = $name;
757 } else {
758 $this->placeholderTempUser = $this->userFactory->newTempPlaceholder();
759 }
760 $this->tempUserCreateActive = true;
761 }
762 return Status::newGood();
763 }
764
772 private function createTempUser(): Status {
773 if ( !$this->tempUserCreateActive ) {
774 return Status::newGood();
775 }
776 $request = $this->context->getRequest();
777 $status = $this->tempUserCreator->create(
778 $this->tempUserName,
780 );
781 if ( $status->isOK() ) {
782 $this->placeholderTempUser = null;
783 $this->unsavedTempUser = null;
784 $this->savedTempUser = $status->getUser();
785 $this->authManager->setRequestContextUserFromSessionUser();
786 $this->tempUserCreateDone = true;
787 }
788 LoggerFactory::getInstance( 'authevents' )->info(
789 'Temporary account creation attempt: {user}',
790 [
791 'user' => $this->tempUserName,
792 'success' => $status->isOK(),
793 ] + $request->getSecurityLogContext( $status->isOK() ? $status->getUser() : null )
794 );
795 return $status;
796 }
797
807 private function getAuthority(): Authority {
808 return $this->getUserForPermissions();
809 }
810
817 private function getUserForPermissions() {
818 if ( $this->savedTempUser ) {
819 return $this->savedTempUser;
820 } elseif ( $this->unsavedTempUser ) {
821 return $this->unsavedTempUser;
822 } elseif ( $this->placeholderTempUser ) {
823 return $this->placeholderTempUser;
824 } else {
825 return $this->context->getUser();
826 }
827 }
828
833 private function getUserForPreview(): UserIdentity {
834 if ( $this->savedTempUser ) {
835 return $this->savedTempUser;
836 } elseif ( $this->unsavedTempUser ) {
837 return $this->unsavedTempUser;
838 } elseif ( $this->firsttime && $this->placeholderTempUser ) {
839 // Mostly a GET request and no temp user was acquired,
840 // but needed for pst or content transform for preview,
841 // fallback to a placeholder for this situation (T330943)
842 return $this->placeholderTempUser;
843 } elseif ( $this->tempUserCreateActive ) {
844 throw new BadMethodCallException(
845 "Can't use the request user for preview with IP masking enabled" );
846 } else {
847 return $this->context->getUser();
848 }
849 }
850
855 private function getUserForSave(): UserIdentity {
856 if ( $this->savedTempUser ) {
857 return $this->savedTempUser;
858 } elseif ( $this->tempUserCreateActive ) {
859 throw new BadMethodCallException(
860 "Can't use the request user for storage with IP masking enabled" );
861 } else {
862 return $this->context->getUser();
863 }
864 }
865
870 private function getEditPermissionStatus( string $rigor = PermissionManager::RIGOR_SECURE ): PermissionStatus {
871 $user = $this->getUserForPermissions();
872 return $this->permManager->getPermissionStatus(
873 'edit',
874 $user,
875 $this->getTitle(),
876 $rigor
877 );
878 }
879
891 private function displayPermissionStatus( PermissionStatus $status ): void {
892 $out = $this->context->getOutput();
893 if ( $this->context->getRequest()->getBool( 'redlink' ) ) {
894 // The edit page was reached via a red link.
895 // Redirect to the article page and let them click the edit tab if
896 // they really want a permission error.
897 $out->redirect( $this->getTitle()->getFullURL() );
898 return;
899 }
900
901 $content = $this->getContentObject();
902
903 // Use the normal message if there's nothing to display:
904 // page or section does not exist (T249978), and the user isn't in the middle of an edit
905 if ( !$content || ( $this->firsttime && !$this->page->exists() && $content->isEmpty() ) ) {
906 $action = $this->page->exists() ? 'edit' :
907 ( $this->getTitle()->isTalkPage() ? 'createtalk' : 'createpage' );
908 throw new PermissionsError( $action, $status );
909 }
910
911 $this->displayViewSourcePage(
912 $content,
913 $out->formatPermissionStatus( $status, 'edit' )
914 );
915 }
916
922 private function displayViewSourcePage( Content $content, string $errorMessage ): void {
923 $out = $this->context->getOutput();
924 $this->getHookRunner()->onEditPage__showReadOnlyForm_initial( $this, $out );
925
926 $out->setRobotPolicy( 'noindex,nofollow' );
927 $out->setPageTitleMsg( $this->context->msg(
928 'viewsource-title'
929 )->plaintextParams(
930 $this->getContextTitle()->getPrefixedText()
931 ) );
932 $out->addBacklinkSubtitle( $this->getContextTitle() );
933 $out->addHTML( $this->editFormPageTop );
934 $out->addHTML( $this->editFormTextTop );
935
936 if ( $errorMessage !== '' ) {
937 $out->addWikiTextAsInterface( $errorMessage );
938 $out->addHTML( "<hr />\n" );
939 }
940
941 # If the user made changes, preserve them when showing the markup
942 # (This happens when a user is blocked during edit, for instance)
943 if ( !$this->firsttime ) {
944 $text = $this->textbox1;
945 $out->addWikiMsg( 'viewyourtext' );
946 } else {
947 // Convert the content to editable text, or serialize using the default format if the content model is not
948 // supported (e.g. for an old revision with a different model)
949 try {
950 $text = $this->pageEditingHelper->toEditText(
951 $content, $this->contentFormat, $this->enableApiEditOverride
952 ) ?? $content->serialize();
953 } catch ( UnsupportedContentFormatException ) {
954 // T419883: If the content format isn't supported, Content::serialize throws an exception
955 $text = $content->serialize();
956 }
957 $out->addWikiMsg( 'viewsourcetext' );
958 }
959
960 $out->addHTML( $this->editFormTextBeforeContent );
961 $this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] );
962 $out->addHTML( $this->editFormTextAfterContent );
963
964 $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
965
966 $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
967
968 $out->addHTML( $this->editFormTextBottom );
969 if ( $this->page->exists() ) {
970 $out->returnToMain( null, $this->page );
971 }
972 }
973
979 protected function previewOnOpen() {
980 $config = $this->context->getConfig();
981 $previewOnOpenNamespaces = $config->get( MainConfigNames::PreviewOnOpenNamespaces );
982 $request = $this->context->getRequest();
983 if ( $config->get( MainConfigNames::RawHtml ) ) {
984 // If raw HTML is enabled, disable preview on open
985 // since it has to be posted with a token for
986 // security reasons
987 return false;
988 }
989 $preview = $request->getRawVal( 'preview' );
990 if ( $preview === 'yes' ) {
991 // Explicit override from request
992 return true;
993 } elseif ( $preview === 'no' ) {
994 // Explicit override from request
995 return false;
996 } elseif ( $this->section === 'new' ) {
997 // Nothing *to* preview for new sections
998 return false;
999 } elseif ( ( $request->getCheck( 'preload' ) || $this->page->exists() )
1000 && $this->userOptionsLookup->getOption( $this->context->getUser(), 'previewonfirst' )
1001 ) {
1002 // Standard preference behavior
1003 return true;
1004 } elseif ( !$this->page->exists()
1005 && isset( $previewOnOpenNamespaces[$this->page->getNamespace()] )
1006 && $previewOnOpenNamespaces[$this->page->getNamespace()]
1007 ) {
1008 // Categories are special
1009 return true;
1010 } else {
1011 return false;
1012 }
1013 }
1014
1021 private function isSectionEditSupported(): bool {
1022 $currentRev = $this->page->getRevisionRecord();
1023
1024 // $currentRev is null for non-existing pages, use the page default content model.
1025 $revContentModel = $currentRev
1026 ? $currentRev->getMainContentModel()
1027 : $this->page->getContentModel();
1028
1029 return (
1030 ( $this->mArticle->getRevIdFetched() === $this->page->getLatest() ) &&
1031 $this->contentHandlerFactory->getContentHandler( $revContentModel )->supportsSections()
1032 );
1033 }
1034
1040 public function importFormData( $request = null ) {
1041 if ( $request !== null ) {
1042 wfDeprecated( __METHOD__ . ' with non-null $request', '1.47' );
1043 }
1044 $request ??= $this->getContext()->getRequest();
1045
1046 # Section edit can come from either the form or a link
1047 $this->section = $request->getVal( 'wpSection', $request->getVal( 'section', '' ) );
1048
1049 if ( $this->section !== '' && !$this->isSectionEditSupported() ) {
1050 throw new ErrorPageError( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
1051 }
1052
1053 $this->isNew = !$this->page->exists() || $this->section === 'new';
1054
1055 if ( $request->wasPosted() ) {
1056 $this->importFormDataPosted( $request );
1057 } else {
1058 # Not a posted form? Start with nothing.
1059 wfDebug( __METHOD__ . ": Not a posted form." );
1060 $this->textbox1 = '';
1061 $this->summary = '';
1062 $this->sectiontitle = null;
1063 $this->edittime = '';
1064 $this->editRevId = null;
1065 $this->starttime = wfTimestampNow();
1066 $this->preview = false;
1067 $this->save = false;
1068 $this->diff = false;
1069 $this->minoredit = false;
1070 // Watch may be overridden by request parameters
1071 $this->watchthis = $request->getBool( 'watchthis', false );
1072 if ( $this->watchlistExpiryEnabled ) {
1073 $this->watchlistExpiry = null;
1074 }
1075 $this->recreate = false;
1076
1077 // When creating a new section, we can preload a section title by passing it as the
1078 // preloadtitle parameter in the URL (T15100)
1079 if ( $this->section === 'new' && $request->getCheck( 'preloadtitle' ) ) {
1080 $this->sectiontitle = $request->getVal( 'preloadtitle' );
1081 $this->setNewSectionSummary();
1082 } elseif ( $this->section !== 'new' && $request->getRawVal( 'summary' ) !== '' ) {
1083 $this->summary = $request->getText( 'summary' );
1084 if ( $this->summary !== '' ) {
1085 // If a summary has been preset using &summary= we don't want to prompt for
1086 // a different summary. Only prompt for a summary if the summary is blanked.
1087 // (T19416)
1088 $this->autoSumm = md5( '' );
1089 }
1090 }
1091
1092 if ( $request->getVal( 'minor' ) ) {
1093 $this->minoredit = true;
1094 }
1095 }
1096
1097 if ( $request !== $this->getContext()->getRequest() ) {
1098 // TODO Remove this once the $request parameter has been removed from this method
1099 $this->oldid = $request->getInt( 'oldid' );
1100 } else {
1101 // Article::getOldID falls back to the request if no oldid is set
1102 $this->oldid = $this->mArticle->getOldID();
1103 }
1104 $this->parentRevId = $request->getInt( 'parentRevId' );
1105
1106 $this->markAsBot = $request->getBool( 'bot', true );
1107 $this->nosummary = $request->getBool( 'nosummary' );
1108
1109 // May be overridden by revision.
1110 $this->contentModel = $request->getText( 'model', $this->contentModel );
1111 // May be overridden by revision.
1112 $this->contentFormat = $request->getText( 'format', $this->contentFormat );
1113
1114 try {
1115 $handler = $this->contentHandlerFactory->getContentHandler( $this->contentModel );
1116 } catch ( UnknownContentModelException ) {
1117 throw new ErrorPageError(
1118 'editpage-invalidcontentmodel-title',
1119 'editpage-invalidcontentmodel-text',
1120 [ wfEscapeWikiText( $this->contentModel ) ]
1121 );
1122 }
1123
1124 if ( !$handler->isSupportedFormat( $this->contentFormat ) ) {
1125 throw new ErrorPageError(
1126 'editpage-notsupportedcontentformat-title',
1127 'editpage-notsupportedcontentformat-text',
1128 [
1129 wfEscapeWikiText( $this->contentFormat ),
1130 wfEscapeWikiText( ContentHandler::getLocalizedName( $this->contentModel ) )
1131 ]
1132 );
1133 }
1134
1135 // Allow extensions to modify form data
1136 $this->getHookRunner()->onEditPage__importFormData( $this, $request );
1137 }
1138
1139 private function importFormDataPosted( WebRequest $request ): void {
1140 # These fields need to be checked for encoding.
1141 # Also remove trailing whitespace, but don't remove _initial_
1142 # whitespace from the text boxes. This may be significant formatting.
1143 $this->textbox1 = rtrim( $request->getText( 'wpTextbox1' ) );
1144 if ( !$request->getCheck( 'wpTextbox2' ) ) {
1145 // Skip this if wpTextbox2 has input, it indicates that we came
1146 // from a conflict page with raw page text, not a custom form
1147 // modified by subclasses
1148 $textbox1 = $this->importContentFormData( $request );
1149 if ( $textbox1 !== null ) {
1150 $this->textbox1 = $textbox1;
1151 }
1152 }
1153
1154 $this->unicodeCheck = $request->getText( 'wpUnicodeCheck' );
1155
1156 if ( $this->section === 'new' ) {
1157 # Allow setting sectiontitle different from the edit summary.
1158 # Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is
1159 # currently doing double duty as both edit summary and section title. Right now this
1160 # is just to allow API edits to work around this limitation, but this should be
1161 # incorporated into the actual edit form when EditPage is rewritten (T20654, T28312).
1162 if ( $request->getCheck( 'wpSectionTitle' ) ) {
1163 $this->sectiontitle = $request->getText( 'wpSectionTitle' );
1164 if ( $request->getCheck( 'wpSummary' ) ) {
1165 $this->summary = $request->getText( 'wpSummary' );
1166 }
1167 } else {
1168 $this->sectiontitle = $request->getText( 'wpSummary' );
1169 }
1170 } else {
1171 $this->sectiontitle = null;
1172 $this->summary = $request->getText( 'wpSummary' );
1173 }
1174
1175 # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the
1176 # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for
1177 # section titles. (T3600)
1178 # It is weird to modify 'sectiontitle', even when it is provided when using the API, but API
1179 # users have come to rely on it: https://github.com/wikimedia-gadgets/twinkle/issues/1625
1180 $this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary );
1181 if ( $this->sectiontitle !== null ) {
1182 $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
1183 }
1184
1185 if ( $this->section === 'new' ) {
1186 $this->setNewSectionSummary();
1187 }
1188
1189 $this->edittime = $request->getVal( 'wpEdittime' );
1190 $this->editRevId = $request->getIntOrNull( 'editRevId' );
1191 $this->starttime = $request->getVal( 'wpStarttime' );
1192
1193 $undidRev = $request->getInt( 'wpUndidRevision' );
1194 if ( $undidRev ) {
1195 $this->undidRev = $undidRev;
1196 }
1197 $undoAfter = $request->getInt( 'wpUndoAfter' );
1198 if ( $undoAfter ) {
1199 $this->undoAfter = $undoAfter;
1200 }
1201
1202 $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' );
1203
1204 if ( $this->textbox1 === '' && !$request->getCheck( 'wpTextbox1' ) ) {
1205 // wpTextbox1 field is missing, possibly due to being "too big"
1206 // according to some filter rules that may have been configured
1207 // for security reasons.
1208 $this->incompleteForm = true;
1209 } else {
1210 // If we receive the last parameter of the request, we can fairly
1211 // claim the POST request has not been truncated.
1212 $this->incompleteForm = !$request->getVal( 'wpUltimateParam' );
1213 }
1214 if ( $this->incompleteForm ) {
1215 # If the form is incomplete, force to preview.
1216 wfDebug( __METHOD__ . ": Form data appears to be incomplete" );
1217 wfDebug( "POST DATA: " . var_export( $request->getPostValues(), true ) );
1218 $this->preview = true;
1219 } else {
1220 $this->preview = $request->getCheck( 'wpPreview' );
1221 $this->diff = $request->getCheck( 'wpDiff' );
1222
1223 // Remember whether a save was requested, so we can indicate
1224 // if we forced preview due to session failure.
1225 $this->mTriedSave = !$this->preview;
1226
1227 if ( $this->tokenOk( $request ) ) {
1228 # Some browsers will not report any submit button
1229 # if the user hits enter in the comment box.
1230 # The unmarked state will be assumed to be a save,
1231 # if the form seems otherwise complete.
1232 wfDebug( __METHOD__ . ": Passed token check." );
1233 } elseif ( $this->diff ) {
1234 # Failed token check, but only requested "Show Changes".
1235 wfDebug( __METHOD__ . ": Failed token check; Show Changes requested." );
1236 } else {
1237 # Page might be a hack attempt posted from
1238 # an external site. Preview instead of saving.
1239 wfDebug( __METHOD__ . ": Failed token check; forcing preview" );
1240 $this->preview = true;
1241 }
1242 }
1243 $this->save = !$this->preview && !$this->diff;
1244 if ( !$this->edittime || !preg_match( '/^\d{14}$/', $this->edittime ) ) {
1245 $this->edittime = null;
1246 }
1247
1248 if ( !$this->starttime || !preg_match( '/^\d{14}$/', $this->starttime ) ) {
1249 $this->starttime = null;
1250 }
1251
1252 $this->recreate = $request->getCheck( 'wpRecreate' );
1253
1254 $this->ignoreRevisionDeletedWarning = $request->getCheck( 'wpIgnoreRevisionDeleted' );
1255
1256 $user = $this->context->getUser();
1257
1258 $this->minoredit = $request->getCheck( 'wpMinoredit' );
1259 $this->watchthis = $request->getCheck( 'wpWatchthis' );
1260 $submittedExpiry = $request->getText( 'wpWatchlistExpiry' );
1261 if ( $this->watchlistExpiryEnabled && $submittedExpiry !== '' ) {
1262 // This parsing of the user-posted expiry is done for both preview and saving. This
1263 // is necessary because ApiEditPage uses preview when it saves (yuck!). Note that it
1264 // only works because the unnormalized value is retrieved again below in
1265 // getCheckboxesDefinitionForWatchlist().
1266 $submittedExpiry = ExpiryDef::normalizeExpiry( $submittedExpiry, TS::ISO_8601 );
1267 if ( $submittedExpiry !== false ) {
1268 $this->watchlistExpiry = $submittedExpiry;
1269 }
1270 }
1271 if ( $this->watchlistLabelsEnabled ) {
1272 $this->watchlistLabels = $request->getIntArray( 'wpWatchlistLabels', [] );
1273 }
1274
1275 # Don't force edit summaries when a user is editing their own user or talk page
1276 if ( ( $this->page->getNamespace() === NS_USER || $this->page->getNamespace() === NS_USER_TALK )
1277 && $this->getTitle()->getText() === $user->getName()
1278 ) {
1279 $this->allowBlankSummary = true;
1280 } else {
1281 $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )
1282 || !$this->userOptionsLookup->getOption( $user, 'forceeditsummary' );
1283 }
1284
1285 $this->autoSumm = $request->getText( 'wpAutoSummary' );
1286
1287 $this->allowBlankArticle = $request->getBool( 'wpIgnoreBlankArticle' );
1288 $allowedProblematicRedirectTargetText = $request->getText( 'wpAllowedProblematicRedirectTarget' );
1289 $this->allowedProblematicRedirectTarget = $allowedProblematicRedirectTargetText === ''
1290 ? null : Title::newFromText( $allowedProblematicRedirectTargetText );
1291 $this->ignoreProblematicRedirects = $request->getBool( 'wpIgnoreProblematicRedirects' );
1292
1293 $changeTags = $request->getVal( 'wpChangeTags' );
1294 $changeTagsAfterPreview = $request->getVal( 'wpChangeTagsAfterPreview' );
1295 if ( $changeTags === null || $changeTags === '' ) {
1296 $this->changeTags = [];
1297 } else {
1298 $this->changeTags = array_filter(
1299 array_map(
1300 'trim',
1301 explode( ',', $changeTags )
1302 )
1303 );
1304 }
1305 if ( $changeTagsAfterPreview !== null && $changeTagsAfterPreview !== '' ) {
1306 $this->changeTags = array_merge( $this->changeTags, array_filter(
1307 array_map(
1308 'trim',
1309 explode( ',', $changeTagsAfterPreview )
1310 )
1311 ) );
1312 }
1313 }
1314
1324 protected function importContentFormData( &$request ) {
1325 return null; // Don't do anything, EditPage already extracted wpTextbox1
1326 }
1327
1333 private function initialiseForm(): bool {
1334 $this->edittime = $this->page->getTimestamp();
1335 $this->editRevId = $this->page->getLatest();
1336
1337 $dummy = $this->contentHandlerFactory
1338 ->getContentHandler( $this->contentModel )
1339 ->makeEmptyContent();
1340 $content = $this->getContentObject( $dummy ); # TODO: track content object?!
1341 if ( $content === $dummy ) { // Invalid section
1342 $this->noSuchSectionPage();
1343 return false;
1344 }
1345
1346 if ( !$content ) {
1347 $this->editFormPageTop .= Html::errorBox( $this->context->msg(
1348 'missing-revision-content',
1349 $this->oldid,
1350 wfEscapeWikiText( $this->getTitle()->getPrefixedText() )
1351 )->parse() );
1352 } elseif ( !$this->pageEditingHelper->isSupportedContentModel(
1353 $content->getModel(), $this->enableApiEditOverride,
1354 ) ) {
1355 $modelMsg = $this->getContext()->msg( 'content-model-' . $content->getModel() );
1356 $modelName = $modelMsg->exists() ? $modelMsg->text() : $content->getModel();
1357
1358 $out = $this->context->getOutput();
1359 $out->showErrorPage(
1360 'modeleditnotsupported-title',
1361 'modeleditnotsupported-text',
1362 [ $modelName ]
1363 );
1364 return false;
1365 }
1366
1367 try {
1368 $this->textbox1 = $this->pageEditingHelper->toEditText(
1369 $content, $this->contentFormat, $this->enableApiEditOverride
1370 ) ?? $content->serialize();
1371 } catch ( UnsupportedContentFormatException ) {
1372 // T391524: If the content format isn't supported, Content::serialize throws an exception
1373 $this->textbox1 = $content->serialize();
1374 }
1375
1376 $user = $this->context->getUser();
1377 // activate checkboxes if user wants them to be always active
1378 # Sort out the "watch" checkbox
1379 if ( $this->userOptionsLookup->getOption( $user, 'watchdefault' ) ) {
1380 # Watch all edits
1381 $this->watchthis = true;
1382 } elseif ( $this->userOptionsLookup->getOption( $user, 'watchcreations' ) && !$this->page->exists() ) {
1383 # Watch creations
1384 $this->watchthis = true;
1385 } elseif ( $this->watchlistManager->isWatched( $user, $this->page ) ) {
1386 # Already watched
1387 $this->watchthis = true;
1388 }
1389 if ( $this->watchthis && $this->watchlistExpiryEnabled ) {
1390 $watchedItem = $this->watchedItemStore->getWatchedItem( $user, $this->getTitle() );
1391 $this->watchlistExpiry = $watchedItem ? $watchedItem->getExpiry() : null;
1392 }
1393 if ( !$this->isNew && $this->userOptionsLookup->getOption( $user, 'minordefault' ) ) {
1394 $this->minoredit = true;
1395 }
1396 return true;
1397 }
1398
1404 protected function getContentObject( $defaultContent = null ) {
1405 $services = MediaWikiServices::getInstance();
1406 $request = $this->context->getRequest();
1407
1408 $content = false;
1409
1410 // For non-existent articles and new sections, use preload text if any.
1411 if ( !$this->page->exists() || $this->section === 'new' ) {
1412 $content = $services->getPreloadedContentBuilder()->getPreloadedContent(
1413 $this->page,
1414 $this->context->getUser(),
1415 $request->getVal( 'preload' ),
1416 array_filter( $request->getArray( 'preloadparams', [] ), is_string( ... ) ),
1417 $request->getVal( 'section' )
1418 );
1419 // For existing pages, get text based on "undo" or section parameters.
1420 } elseif ( $this->section !== '' ) {
1421 // Get section edit text (returns $def_text for invalid sections)
1422 $orig = $this->pageEditingHelper->getOriginalContent(
1423 $this->getAuthority(),
1424 $this->page,
1425 $this->mArticle->fetchRevisionRecord(),
1426 $this->contentModel,
1427 $this->section,
1428 );
1429 $content = $orig ? $orig->getSection( $this->section ) : null;
1430
1431 if ( !$content ) {
1432 $content = $defaultContent;
1433 }
1434 } else {
1435 $undoafter = $request->getInt( 'undoafter' );
1436 $undo = $request->getInt( 'undo' );
1437
1438 if ( $undo > 0 && $undoafter > 0 ) {
1439 // The use of getRevisionByTitle() is intentional, as allowing access to
1440 // arbitrary revisions on arbitrary pages bypass partial visibility restrictions (T297322).
1441 $undorev = $this->revisionStore->getRevisionByTitle( $this->page, $undo );
1442 $oldrev = $this->revisionStore->getRevisionByTitle( $this->page, $undoafter );
1443 $undoMsg = null;
1444
1445 # Make sure it's the right page,
1446 # the revisions exist and they were not deleted.
1447 # Otherwise, $content will be left as-is.
1448 if ( $undorev !== null && $oldrev !== null &&
1449 !$undorev->isDeleted( RevisionRecord::DELETED_TEXT ) &&
1450 !$oldrev->isDeleted( RevisionRecord::DELETED_TEXT )
1451 ) {
1452 if ( WikiPage::hasDifferencesOutsideMainSlot( $undorev, $oldrev )
1453 || !$this->pageEditingHelper->isSupportedContentModel(
1454 $oldrev->getMainContentModel(), $this->enableApiEditOverride
1455 )
1456 ) {
1457 // Hack for undo while EditPage can't handle multi-slot editing
1458 $this->context->getOutput()->redirect( $this->getTitle()->getFullURL( [
1459 'action' => 'mcrundo',
1460 'undo' => $undo,
1461 'undoafter' => $undoafter,
1462 ] ) );
1463 return false;
1464 } else {
1465 $content = $this->pageEditingHelper->getUndoContent( $this->page, $undorev, $oldrev, $undoMsg );
1466 }
1467
1468 if ( $undoMsg === null ) {
1469 $oldContent = $this->page->getContent( RevisionRecord::RAW );
1470 $parserOptions = ParserOptions::newFromUserAndLang(
1471 $this->getUserForPreview(),
1472 $services->getContentLanguage()
1473 );
1474 $contentTransformer = $services->getContentTransformer();
1475 $newContent = $contentTransformer->preSaveTransform(
1476 $content, $this->page, $this->getUserForPreview(), $parserOptions
1477 );
1478
1479 if ( $newContent->getModel() !== $oldContent->getModel() ) {
1480 // The undo may change content
1481 // model if its reverting the top
1482 // edit. This can result in
1483 // mismatched content model/format.
1484 $this->contentModel = $newContent->getModel();
1485 $oldMainSlot = $oldrev->getSlot(
1486 SlotRecord::MAIN,
1487 RevisionRecord::RAW
1488 );
1489 $this->contentFormat = $oldMainSlot->getFormat();
1490 if ( $this->contentFormat === null ) {
1491 $this->contentFormat = $this->contentHandlerFactory
1492 ->getContentHandler( $oldMainSlot->getModel() )
1493 ->getDefaultFormat();
1494 }
1495 }
1496
1497 if ( $newContent->equals( $oldContent ) ) {
1498 # Tell the user that the undo results in no change,
1499 # i.e. the revisions were already undone.
1500 $undoMsg = 'nochange';
1501 $content = false;
1502 } else {
1503 # Inform the user of our success and set an automatic edit summary
1504 $undoMsg = 'success';
1505 $this->generateUndoEditSummary( $oldrev, $undo, $undorev, $services );
1506 $this->undidRev = $undo;
1507 $this->undoAfter = $undoafter;
1508 $this->formtype = 'diff';
1509 }
1510 }
1511 } else {
1512 // Failed basic checks.
1513 // Older revisions may have been removed since the link
1514 // was created, or we may simply have got bogus input.
1515 $undoMsg = 'norev';
1516 }
1517
1518 $out = $this->context->getOutput();
1519 // Messages: undo-success, undo-failure, undo-main-slot-only, undo-norev,
1520 // undo-nochange.
1521 $class = "mw-undo-{$undoMsg}";
1522 $html = $this->context->msg( 'undo-' . $undoMsg )->parse();
1523 if ( $undoMsg !== 'success' ) {
1524 $html = Html::errorBox( $html );
1525 }
1526 $this->editFormPageTop .= Html::rawElement(
1527 'div',
1528 [ 'class' => $class ],
1529 $html
1530 );
1531 }
1532
1533 if ( $content === false ) {
1534 $content = $this->pageEditingHelper->getOriginalContent(
1535 $this->getAuthority(),
1536 $this->page,
1537 $this->mArticle->fetchRevisionRecord(),
1538 $this->contentModel,
1539 $this->section,
1540 );
1541 }
1542 }
1543
1544 return $content;
1545 }
1546
1556 private function generateUndoEditSummary(
1557 ?RevisionRecord $oldRev,
1558 int $undo,
1559 ?RevisionRecord $undoRev,
1560 MediaWikiServices $services
1561 ): void {
1562 // Generate an autosummary
1563 $firstRev = $this->revisionStore->getNextRevision( $oldRev );
1564 if ( $firstRev && $firstRev->getId() == $undo ) {
1565 // Undid just one revision
1566 $userText = $undoRev->getUser()?->getName();
1567 if ( $userText === null ) {
1568 $undoSummary = $this->context->msg(
1569 'undo-summary-username-hidden',
1570 $undo
1571 )->inContentLanguage()->text();
1572 } elseif ( ExternalUserNames::isExternal( $userText ) ) {
1573 // Handle external users (imported revisions)
1574 $userLinkTitle = ExternalUserNames::getUserLinkTitle( $userText );
1575 if ( $userLinkTitle ) {
1576 $userLink = $userLinkTitle->getPrefixedText();
1577 $undoSummary = $this->context->msg(
1578 'undo-summary-import',
1579 $undo,
1580 $userLink,
1581 $userText
1582 )->inContentLanguage()->text();
1583 } else {
1584 $undoSummary = $this->context->msg(
1585 'undo-summary-import2',
1586 $undo,
1587 $userText
1588 )->inContentLanguage()->text();
1589 }
1590 } else {
1591 $undoIsAnon =
1592 !$undoRev->getUser() ||
1593 !$undoRev->getUser()->isRegistered();
1594 $disableAnonTalk = $services->getMainConfig()->get( MainConfigNames::DisableAnonTalk );
1595 $undoMessage = ( $undoIsAnon && $disableAnonTalk ) ?
1596 'undo-summary-anon' :
1597 'undo-summary';
1598 $undoSummary = $this->context->msg(
1599 $undoMessage,
1600 $undo,
1601 $userText
1602 )->inContentLanguage()->text();
1603 }
1604 } else {
1605 // Undid multiple revisions
1606 $firstRevisionId = $firstRev->getId();
1607 $lastRevisionId = $undoRev->getId();
1608 $revisionCount = $services->getRevisionStore()->countRevisionsBetween(
1609 $firstRev->getPageId(),
1610 $firstRev,
1611 $undoRev,
1612 null,
1613 [ RevisionStore::INCLUDE_BOTH, RevisionStore::INCLUDE_DELETED_REVISIONS ]
1614 );
1615 $undoSummary = $this->context->msg( 'undo-summary-multiple' )
1616 ->numParams( $revisionCount )
1617 ->params( $firstRevisionId, $lastRevisionId )
1618 ->inContentLanguage()
1619 ->text();
1620 }
1621 if ( $this->summary === '' ) {
1622 $this->summary = $undoSummary;
1623 } else {
1624 $this->summary = $undoSummary . $this->context->msg( 'colon-separator' )
1625 ->inContentLanguage()->text() . $this->summary;
1626 }
1627 }
1628
1640 private function getParentRevId() {
1641 if ( $this->parentRevId ) {
1642 return $this->parentRevId;
1643 } else {
1644 return $this->mArticle->getRevIdFetched();
1645 }
1646 }
1647
1656 protected function getCurrentContent() {
1657 return $this->pageEditingHelper->getCurrentContent( $this->contentModel, $this->page );
1658 }
1659
1666 private function tokenOk( WebRequest $request ): bool {
1667 $token = $request->getVal( 'wpEditToken' );
1668 $user = $this->context->getUser();
1669 $this->mTokenOk = $user->matchEditToken( $token );
1670 return $this->mTokenOk;
1671 }
1672
1687 private function setPostEditCookie( int $statusValue ): void {
1688 $revisionId = $this->page->getLatest();
1689 $postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
1690
1691 $val = 'saved';
1692 if ( $statusValue === self::AS_SUCCESS_NEW_ARTICLE ) {
1693 $val = 'created';
1694 } elseif ( $this->oldid ) {
1695 $val = 'restored';
1696 }
1697 if ( $this->tempUserCreateDone ) {
1698 $val .= '+tempuser';
1699 }
1700
1701 $response = $this->context->getRequest()->response();
1702 $response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION );
1703 }
1704
1711 public function attemptSave( &$resultDetails = false ) {
1712 $status = $this->internalAttemptSave( $resultDetails );
1713 if ( !$status->isOK() ) {
1714 $this->handleFailedConstraint( $status );
1715 }
1716
1717 // Status::wrap() takes references to all internal variables, allowing hook handlers to modify
1718 // the $status, without changing the hook interface to use the PageEditStatus type.
1719 $this->getHookRunner()->onEditPage__attemptSave_after( $this, Status::wrap( $status ), $resultDetails );
1720
1721 return $status;
1722 }
1723
1727 private function incrementResolvedConflicts(): void {
1728 if ( $this->context->getRequest()->getText( 'mode' ) !== 'conflict' ) {
1729 return;
1730 }
1731
1732 $this->getEditConflictHelper()->incrementResolvedStats( $this->context->getUser() );
1733 }
1734
1744 private function handleStatus( PageEditStatus $status, $resultDetails ): bool {
1745 $statusValue = is_int( $status->value ) ? $status->value : 0;
1746
1751 if ( $statusValue === self::AS_SUCCESS_UPDATE
1752 || $statusValue === self::AS_SUCCESS_NEW_ARTICLE
1753 ) {
1754 $this->incrementResolvedConflicts();
1755
1756 $this->didSave = true;
1757 if ( !$resultDetails['nullEdit'] ) {
1758 $this->setPostEditCookie( $statusValue );
1759 }
1760 }
1761
1762 $out = $this->context->getOutput();
1763
1764 // "wpExtraQueryRedirect" is a hidden input to modify
1765 // after save URL and is not used by actual edit form
1766 $request = $this->context->getRequest();
1767 $extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
1768
1769 switch ( $statusValue ) {
1770 // Status codes for which the error/warning message is generated somewhere else in this class.
1771 // They should be refactored to provide their own messages and handled below (T384399).
1772 case self::AS_HOOK_ERROR_EXPECTED:
1773 case self::AS_CONFLICT_DETECTED:
1774 return true;
1775
1776 case self::AS_HOOK_ERROR:
1777 return false;
1778
1779 // Status codes that provide their own error/warning messages. Most error scenarios that don't
1780 // need custom user interface (e.g. edit conflicts) should be handled here, one day (T384399).
1781 case self::AS_ARTICLE_WAS_DELETED:
1782 case self::AS_BLANK_ARTICLE:
1783 case self::AS_BROKEN_REDIRECT:
1784 case self::AS_CONTENT_TOO_BIG:
1785 case self::AS_DOUBLE_REDIRECT:
1786 case self::AS_DOUBLE_REDIRECT_LOOP:
1787 case self::AS_END:
1788 case self::AS_INVALID_REDIRECT_TARGET:
1789 case self::AS_MAX_ARTICLE_SIZE_EXCEEDED:
1790 case self::AS_PARSE_ERROR:
1791 case self::AS_RATE_LIMITED:
1792 case self::AS_REVISION_MISSING:
1793 case self::AS_REVISION_WAS_DELETED:
1794 case self::AS_SELF_REDIRECT:
1795 case self::AS_SUMMARY_NEEDED:
1796 case self::AS_TEXTBOX_EMPTY:
1797 case self::AS_UNABLE_TO_ACQUIRE_TEMP_ACCOUNT:
1798 case self::AS_UNICODE_NOT_SUPPORTED:
1799 $out->addHTML( $this->formatConstraintStatus( $status ) );
1800 return true;
1801
1802 case self::AS_SUCCESS_NEW_ARTICLE:
1803 $queryParts = [];
1804 if ( $resultDetails['redirect'] ) {
1805 $queryParts[] = 'redirect=no';
1806 }
1807 if ( $extraQueryRedirect ) {
1808 $queryParts[] = $extraQueryRedirect;
1809 }
1810 $anchor = $resultDetails['sectionanchor'] ?? '';
1811 $this->doPostEditRedirect( implode( '&', $queryParts ), $anchor );
1812 return false;
1813
1814 case self::AS_SUCCESS_UPDATE:
1815 $extraQuery = '';
1816 $sectionanchor = $resultDetails['sectionanchor'];
1817 // Give extensions a chance to modify URL query on update
1818 $this->getHookRunner()->onArticleUpdateBeforeRedirect( $this->mArticle,
1819 $sectionanchor, $extraQuery );
1820
1821 $queryParts = [];
1822 if ( $resultDetails['redirect'] ) {
1823 $queryParts[] = 'redirect=no';
1824 }
1825 if ( $extraQuery ) {
1826 $queryParts[] = $extraQuery;
1827 }
1828 if ( $extraQueryRedirect ) {
1829 $queryParts[] = $extraQueryRedirect;
1830 }
1831 $this->doPostEditRedirect( implode( '&', $queryParts ), $sectionanchor );
1832 return false;
1833
1834 case self::AS_SPAM_ERROR:
1835 $this->spamPageWithContent( $resultDetails['spam'] ?? false );
1836 return false;
1837
1838 case self::AS_BLOCKED_PAGE_FOR_USER:
1839 case self::AS_IMAGE_REDIRECT_ANON:
1840 case self::AS_IMAGE_REDIRECT_LOGGED:
1841 case self::AS_NO_CHANGE_CONTENT_MODEL:
1842 case self::AS_NO_CREATE_PERMISSION:
1843 case self::AS_READ_ONLY_PAGE:
1844 case self::AS_READ_ONLY_PAGE_ANON:
1845 case self::AS_READ_ONLY_PAGE_LOGGED:
1846 $status->throwError();
1847 // No break statement here as throwError() will always throw an exception
1848
1849 default:
1850 // We don't recognize $statusValue. The only way that can happen
1851 // is if an extension hook aborted from inside ArticleSave.
1852 // Render the status object into $this->hookError
1853 // FIXME this sucks, we should just use the Status object throughout
1854 $this->hookError = Html::errorBox(
1855 "\n" . Status::cast( $status )->getWikiText( false, false, $this->context->getLanguage() )
1856 );
1857 return true;
1858 }
1859 }
1860
1865 private function formatConstraintStatus( StatusValue $status ): string {
1866 return $this->createMessageBox( $status->getMessages( 'error' ), 'error' ) .
1867 $this->createMessageBox( $status->getMessages( 'warning' ), 'warning' );
1868 }
1869
1875 private function createMessageBox( array $messages, string $type ): string {
1876 if ( !$messages ) {
1877 // Don't create a box if there are no messages
1878 return '';
1879 }
1880
1881 $html = implode(
1882 Html::openElement( 'br' ),
1883 array_map( fn ( $msg ) => $this->context->msg( $msg )->parse(), $messages )
1884 );
1885
1886 if ( $type === 'warning' ) {
1887 return Html::warningBox( $html );
1888 }
1889 return Html::errorBox( $html );
1890 }
1891
1895 private function doPostEditRedirect( string $query, string $anchor ): void {
1896 $out = $this->context->getOutput();
1897 $url = $this->getTitle()->getFullURL( $query ) . $anchor;
1898 $user = $this->getUserForSave();
1899 // If the temporary account was created in this request,
1900 // or if the temporary account has zero edits (implying
1901 // that the account was created during a failed edit
1902 // attempt in a previous request), perform the top-level
1903 // redirect to ensure the account is attached.
1904 // Note that the temp user could already have performed
1905 // the top-level redirect if this a first edit on
1906 // a wiki that is not the user's home wiki.
1907 $shouldRedirectForTempUser = $this->tempUserCreateDone ||
1908 ( $this->userIdentityUtils->isTemp( $user ) && $this->userEditTracker->getUserEditCount( $user ) === 0 );
1909 if ( $shouldRedirectForTempUser ) {
1910 $this->getHookRunner()->onTempUserCreatedRedirect(
1911 $this->context->getRequest()->getSession(),
1912 $user,
1913 $this->getTitle()->getPrefixedDBkey(),
1914 $query,
1915 $anchor,
1916 $url
1917 );
1918 }
1919 $out->redirect( $url );
1920 }
1921
1925 private function setNewSectionSummary(): void {
1926 Assert::precondition( $this->section === 'new', 'This method can only be called for new sections' );
1927 Assert::precondition( $this->sectiontitle !== null, 'This method can only be called for new sections' );
1928
1929 $services = MediaWikiServices::getInstance();
1930 $parser = $services->getParser();
1931 $textFormatter = $services->getMessageFormatterFactory()->getTextFormatter(
1932 $services->getContentLanguageCode()->toString()
1933 );
1934
1935 if ( $this->sectiontitle !== '' ) {
1936 $this->newSectionAnchor = $this->pageEditingHelper->guessSectionName( $this->sectiontitle );
1937 // If no edit summary was specified, create one automatically from the section
1938 // title and have it link to the new section. Otherwise, respect the summary as
1939 // passed.
1940 if ( $this->summary === '' ) {
1941 $messageValue = MessageValue::new( 'newsectionsummary' )
1942 ->plaintextParams( $parser->stripSectionName( $this->sectiontitle ) );
1943 $this->summary = $textFormatter->format( $messageValue );
1944 }
1945 } else {
1946 $this->newSectionAnchor = '';
1947 }
1948 }
1949
1973 private function internalAttemptSave( &$result ) {
1974 // If an attempt to acquire a temporary name failed, don't attempt to do anything else.
1975 if ( $this->unableToAcquireTempName ) {
1976 return PageEditStatus::newFatal( 'temp-user-unable-to-acquire' )
1977 ->setValue( self::AS_UNABLE_TO_ACQUIRE_TEMP_ACCOUNT );
1978 }
1979 // Auto-create the temporary account user, if the feature is enabled.
1980 // We create the account before any constraint checks or edit hooks fire, to ensure
1981 // that we have an actor and user account that can be used for any logs generated
1982 // by the edit attempt, and to ensure continuity in the user experience (if a constraint
1983 // denies an edit to a logged-out user, that history should be associated with the
1984 // eventually successful account creation)
1985 $tempAccountStatus = $this->createTempUser();
1986 if ( !$tempAccountStatus->isOK() ) {
1987 return PageEditStatus::cast( $tempAccountStatus );
1988 }
1989 if ( $tempAccountStatus instanceof CreateStatus ) {
1990 $result['savedTempUser'] = $tempAccountStatus->getUser();
1991 }
1992
1993 if ( !$this->getHookRunner()->onEditPage__attemptSave( $this ) ) {
1994 wfDebug( "Hook 'EditPage::attemptSave' aborted article saving" );
1995 return PageEditStatus::newFatal( 'hookaborted' )
1996 ->setValue( self::AS_HOOK_ERROR );
1997 }
1998
1999 if ( $this->unicodeCheck !== self::UNICODE_CHECK ) {
2000 return PageEditStatus::newFatal( 'unicode-support-fail' )
2001 ->setValue( self::AS_UNICODE_NOT_SUPPORTED );
2002 }
2003
2004 $antispam = $this->getContext()->getRequest()->getText( 'wpAntispam' );
2005 if ( $antispam !== '' ) {
2006 LoggerFactory::getInstance( 'SimpleAntiSpam' )->debug(
2007 '{name} editing "{title}" submitted bogus field "{input}"',
2008 [
2009 // Use the context user since there is no permissions aspect
2010 'name' => $this->context->getUser()->getName(),
2011 'title' => $this->page,
2012 'input' => $antispam,
2013 ]
2014 );
2015 return PageEditStatus::newFatal( 'spamprotectionmatch', '' )
2016 ->setValue( self::AS_SPAM_ERROR );
2017 }
2018
2019 $inputs = ( new PageEditInputs(
2020 authority: $this->getAuthority(),
2021 contentModel: $this->contentModel,
2022 context: $this->context,
2023 page: $this->page,
2024 summary: $this->summary,
2025 textbox1: $this->textbox1,
2026 ) )->setAllowBlankArticle( $this->allowBlankArticle )
2027 ->setAllowBlankSummary( $this->allowBlankSummary )
2028 ->setAllowedProblematicRedirectTarget( $this->allowedProblematicRedirectTarget )
2029 ->setAutoSumm( $this->autoSumm )
2030 ->setChangeTags( $this->changeTags )
2031 ->setContentFormat( $this->contentFormat )
2032 ->setContextPage( $this->mContextTitle )
2033 ->setEdittime( $this->edittime )
2034 ->setEditRevId( $this->editRevId )
2035 ->setEnableApiEditOverride( $this->enableApiEditOverride )
2036 ->setIgnoreProblematicRedirects( $this->ignoreProblematicRedirects )
2037 ->setIgnoreRevisionDeletedWarning( $this->ignoreRevisionDeletedWarning )
2038 ->setMarkAsBot( $this->markAsBot )
2039 ->setMarkAsMinor( $this->minoredit )
2040 ->setNewSectionAnchor( $this->newSectionAnchor )
2041 ->setOldid( $this->oldid )
2042 ->setParentRevId( $this->parentRevId )
2043 ->setRecreate( $this->recreate )
2044 ->setSection( $this->section )
2045 ->setSectiontitle( $this->sectiontitle )
2046 ->setStarttime( $this->starttime )
2047 ->setUndidRev( $this->undidRev )
2048 ->setUndoAfter( $this->undoAfter )
2049 ->setUserForPreview( $this->getUserForPreview() )
2050 ->setUserForSave( $this->getUserForSave() )
2051 ->setWatchlistExpiry( $this->watchlistExpiry )
2052 ->setWatchlistLabels( $this->watchlistLabels )
2053 ->setWatchthis( $this->watchthis );
2054
2055 $pageEdit = $this->pageEditFactory->newPageEdit( $inputs );
2056 $pageEditResult = $pageEdit->edit();
2057
2058 // PageEdit writes to those properties, so update them for backwards compatibility
2059 $this->contentLength = $pageEditResult->getContentLength();
2060 $this->isConflict = $pageEditResult->isConflict();
2061 $this->parentRevId = $pageEditResult->getParentRevId();
2062 $this->section = $pageEditResult->getSection();
2063 $this->summary = $pageEditResult->getSummary();
2064 $this->textbox1 = $pageEditResult->getTextbox1();
2065
2066 if ( !$pageEditResult->getStatus()->isOK() ) {
2067 $failed = $pageEditResult->getStatus()->getFailedConstraint();
2068
2069 // Need to check SpamRegexConstraint here, to avoid needing to pass
2070 // $result by reference again
2071 if ( $failed instanceof SpamRegexConstraint ) {
2072 $result['spam'] = $failed->getMatch();
2073 } else {
2074 $this->handleFailedConstraint( $pageEditResult->getStatus() );
2075 }
2076 }
2077
2078 $result['sectionanchor'] = $pageEditResult->getSectionanchor();
2079 if ( $pageEditResult->isNullEdit() !== null ) {
2080 $result['nullEdit'] = $pageEditResult->isNullEdit();
2081 }
2082 if ( $pageEditResult->isRedirect() !== null ) {
2083 $result['redirect'] = $pageEditResult->isRedirect();
2084 }
2085
2086 return $pageEditResult->getStatus();
2087 }
2088
2095 private function handleFailedConstraint( PageEditStatus $status ): void {
2096 $failed = $status->getFailedConstraint();
2097 if ( $failed instanceof AuthorizationConstraint ) {
2098 // Auto-block user's IP if the account was "hard" blocked
2099 if (
2100 !MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly()
2101 && $status->value === self::AS_BLOCKED_PAGE_FOR_USER
2102 ) {
2103 $this->context->getUser()->spreadAnyEditBlock();
2104 }
2105 } elseif ( $failed instanceof DefaultTextConstraint ) {
2106 $this->blankArticle = true;
2107 } elseif ( $failed instanceof EditFilterMergedContentHookConstraint ) {
2108 $this->hookError = $failed->getHookError();
2109 } elseif (
2110 $failed instanceof ExistingSectionEditConstraint ||
2111 $failed instanceof NewSectionMissingSubjectConstraint
2112 ) {
2113 $this->missingSummary = true;
2114 } elseif ( $failed instanceof RedirectConstraint ) {
2115 $this->problematicRedirectTarget = $failed->problematicTarget;
2116 } elseif ( $failed instanceof AccidentalRecreationConstraint ) {
2117 $this->recreate = true;
2118 } elseif ( $failed instanceof RevisionDeletedConstraint ) {
2119 $this->ignoreRevisionDeletedWarning = true;
2120 }
2121 }
2122
2131 return $this->pageEditingHelper->getExpectedParentRevision(
2132 $this->editRevId,
2133 $this->edittime,
2134 $this->page,
2135 );
2136 }
2137
2138 public function setHeaders() {
2139 $out = $this->context->getOutput();
2140
2141 $out->addModules( 'mediawiki.action.edit' );
2142 $out->addModuleStyles( [
2143 'mediawiki.action.edit.styles',
2144 'mediawiki.codex.messagebox.styles',
2145 'mediawiki.editfont.styles',
2146 'mediawiki.interface.helpers.styles',
2147 ] );
2148
2149 $user = $this->context->getUser();
2150
2151 if ( $this->userOptionsLookup->getOption( $user, 'uselivepreview' ) ) {
2152 $out->addModules( 'mediawiki.action.edit.preview' );
2153 }
2154
2155 if ( $this->userOptionsLookup->getOption( $user, 'useeditwarning' ) ) {
2156 $out->addModules( 'mediawiki.action.edit.editWarning' );
2157 }
2158
2159 if ( $this->context->getConfig()->get( MainConfigNames::EnableEditRecovery )
2160 && $this->userOptionsLookup->getOption( $user, 'editrecovery' )
2161 ) {
2162 $wasPosted = $this->getContext()->getRequest()->getMethod() === 'POST';
2163 $out->addJsConfigVars( 'wgEditRecoveryWasPosted', $wasPosted );
2164 $out->addModules( 'mediawiki.editRecovery.edit' );
2165 }
2166
2167 # Enabled article-related sidebar, toplinks, etc.
2168 $out->setArticleRelated( true );
2169
2170 $contextTitle = $this->getContextTitle();
2171 if ( $this->isConflict ) {
2172 $msg = 'editconflict';
2173 } elseif ( $contextTitle->exists() && $this->section != '' ) {
2174 $msg = $this->section === 'new' ? 'editingcomment' : 'editingsection';
2175 } else {
2176 $msg = $contextTitle->exists()
2177 || $this->shadowPageLoader->get( $contextTitle )?->existsForEdit()
2178 ? 'editing'
2179 : 'creating';
2180 }
2181
2182 # Use the title defined by DISPLAYTITLE magic word when present
2183 # NOTE: getDisplayTitle() returns HTML while getPrefixedText() returns plain text.
2184 # Escape ::getPrefixedText() so that we have HTML in all cases,
2185 # and pass as a "raw" parameter to ::setPageTitleMsg().
2186 $displayTitle = $this->mParserOutput ? $this->mParserOutput->getDisplayTitle() : false;
2187 if ( $displayTitle === false ) {
2188 $displayTitle = Parser::formatPageTitle(
2189 str_replace( '_', ' ', $contextTitle->getNsText() ),
2190 ':',
2191 $contextTitle->getText(),
2192 $contextTitle->getPageLanguage()
2193 );
2194 } else {
2195 $out->setDisplayTitle( $displayTitle );
2196 }
2197
2198 // Enclose the title with an element. This is used on live preview to update the
2199 // preview of the display title.
2200 $displayTitle = Html::rawElement( 'span', [ 'id' => 'firstHeadingTitle' ], $displayTitle );
2201
2202 $out->setPageTitleMsg( $this->context->msg( $msg )->rawParams( $displayTitle ) );
2203
2204 $config = $this->context->getConfig();
2205
2206 # Transmit the name of the message to JavaScript. This was added for live preview.
2207 # Live preview doesn't use this anymore. The variable is still transmitted because
2208 # Edit Recovery and user scripts use it.
2209 $out->addJsConfigVars( [
2210 'wgEditMessage' => $msg,
2211 ] );
2212
2213 // Add whether to use 'save' or 'publish' messages to JavaScript for post-edit, other
2214 // editors, etc.
2215 $out->addJsConfigVars(
2216 'wgEditSubmitButtonLabelPublish',
2217 $config->get( MainConfigNames::EditSubmitButtonLabelPublish )
2218 );
2219 }
2220
2224 private function showIntro(): void {
2225 $services = MediaWikiServices::getInstance();
2226
2227 // Hardcoded list of notices that are suppressable for historical reasons.
2228 // This feature was originally added for LiquidThreads, to avoid showing non-essential messages
2229 // when commenting in a thread, but some messages were included (or excluded) by mistake before
2230 // its implementation was moved to one place, and this list doesn't make a lot of sense.
2231 // TODO: Remove the suppressIntro feature from EditPage, and invent a better way for extensions
2232 // to skip individual intro messages.
2233 $skip = $this->suppressIntro ? [
2234 'editintro',
2235 'code-editing-intro',
2236 'sharedupload-desc-create',
2237 'sharedupload-desc-edit',
2238 'userpage-userdoesnotexist',
2239 'blocked-notice-logextract',
2240 'newarticletext',
2241 'newarticletextanon',
2242 'recreate-moveddeleted-warn',
2243 ] : [];
2244
2245 $messages = $services->getIntroMessageBuilder()->getIntroMessages(
2246 IntroMessageBuilder::MORE_FRAMES,
2247 $skip,
2248 $this->context,
2249 $this->page,
2250 $this->mArticle->fetchRevisionRecord(),
2251 $this->context->getUser(),
2252 $this->context->getRequest()->getVal( 'editintro' ),
2254 array_diff_key(
2255 $this->context->getRequest()->getQueryValues(),
2256 [ 'title' => true, 'returnto' => true, 'returntoquery' => true ]
2257 )
2258 ),
2259 !$this->firsttime,
2260 $this->section !== '' ? $this->section : null
2261 );
2262
2263 foreach ( $messages as $message ) {
2264 $this->context->getOutput()->addHTML( $message );
2265 }
2266 }
2267
2284 protected function toEditContent( $text ) {
2285 if ( $text === false || $text === null ) {
2286 return $text;
2287 }
2288
2289 $content = ContentHandler::makeContent( $text, $this->getTitle(),
2290 $this->contentModel, $this->contentFormat );
2291 $this->assertSupportedContent( $content );
2292 return $content;
2293 }
2294
2300 private function assertSupportedContent( Content $content ): void {
2301 if ( !$this->pageEditingHelper->isSupportedContentModel(
2302 $content->getModel(), $this->enableApiEditOverride
2303 ) ) {
2304 throw new NotDirectlyEditableException( 'This content model is not supported: ' . $content->getModel() );
2305 }
2306 }
2307
2311 public function showEditForm() {
2312 # need to parse the preview early so that we know which templates are used,
2313 # otherwise users with "show preview after edit box" will get a blank list
2314 # we parse this near the beginning so that setHeaders can do the title
2315 # setting work instead of leaving it in getPreviewText
2316 $previewOutput = '';
2317 if ( $this->formtype === 'preview' ) {
2318 $previewOutput = $this->getPreviewText();
2319 }
2320
2321 $out = $this->context->getOutput();
2322
2323 // FlaggedRevs depends on running this hook before adding edit notices in showIntro() (T337637)
2324 $this->getHookRunner()->onEditPage__showEditForm_initial( $this, $out );
2325
2326 $this->setHeaders();
2327
2328 // Show applicable editing introductions
2329 $this->showIntro();
2330
2331 if ( !$this->isConflict &&
2332 $this->section !== '' &&
2333 !$this->isSectionEditSupported()
2334 ) {
2335 // We use $this->section to much before this and getVal('wgSection') directly in other places
2336 // at this point we can't reset $this->section to '' to fallback to non-section editing.
2337 // Someone is welcome to try refactoring though
2338 $out->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
2339 return;
2340 }
2341
2342 $this->showHeader();
2343
2344 $out->addHTML( $this->editFormPageTop );
2345
2346 $user = $this->context->getUser();
2347 if ( $this->userOptionsLookup->getOption( $user, 'previewontop' ) ) {
2348 $this->displayPreviewArea( $previewOutput, true );
2349 }
2350
2351 $out->addHTML( $this->editFormTextTop );
2352
2353 // @todo add EditForm plugin interface and use it here!
2354 // search for textarea1 and textarea2, and allow EditForm to override all uses.
2355 $out->addHTML( Html::openElement(
2356 'form',
2357 [
2358 'class' => 'mw-editform',
2359 'id' => self::EDITFORM_ID,
2360 'name' => self::EDITFORM_ID,
2361 'method' => 'post',
2362 'action' => $this->getActionURL( $this->getContextTitle() ),
2363 'enctype' => 'multipart/form-data',
2364 'data-mw-editform-type' => $this->formtype
2365 ]
2366 ) );
2367
2368 // Add a check for Unicode support
2369 $out->addHTML( Html::hidden( 'wpUnicodeCheck', self::UNICODE_CHECK ) );
2370
2371 // Add an empty field to trip up spambots
2372 $out->addHTML(
2373 Html::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
2374 . Html::rawElement(
2375 'label',
2376 [ 'for' => 'wpAntispam' ],
2377 $this->context->msg( 'simpleantispam-label' )->parse()
2378 )
2379 . Html::element(
2380 'input',
2381 [
2382 'type' => 'text',
2383 'name' => 'wpAntispam',
2384 'id' => 'wpAntispam',
2385 'value' => ''
2386 ]
2387 )
2388 . Html::closeElement( 'div' )
2389 );
2390
2391 $this->getHookRunner()->onEditPage__showEditForm_fields( $this, $out );
2392
2393 // Put these up at the top to ensure they aren't lost on early form submission
2394 $this->showFormBeforeText();
2395
2396 # When the summary is hidden, also hide them on preview/show changes
2397 if ( $this->nosummary ) {
2398 $out->addHTML( Html::hidden( 'nosummary', true ) );
2399 }
2400
2401 # If a blank edit summary was previously provided, and the appropriate
2402 # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
2403 # user being bounced back more than once in the event that a summary
2404 # is not required.
2405 # ####
2406 # For a bit more sophisticated detection of blank summaries, hash the
2407 # automatic one and pass that in the hidden field wpAutoSummary.
2408 if (
2409 $this->missingSummary ||
2410 ( $this->section === 'new' && $this->nosummary ) ||
2411 $this->allowBlankSummary
2412 ) {
2413 $out->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
2414 }
2415
2416 if ( $this->undidRev ) {
2417 $out->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
2418 }
2419 if ( $this->undoAfter ) {
2420 $out->addHTML( Html::hidden( 'wpUndoAfter', $this->undoAfter ) );
2421 }
2422 if ( $this->recreate ) {
2423 $out->addHTML( Html::hidden( 'wpRecreate', $this->recreate ) );
2424 }
2425 if ( $this->ignoreRevisionDeletedWarning ) {
2426 $out->addHTML( Html::hidden( 'wpIgnoreRevisionDeleted', $this->ignoreRevisionDeletedWarning ) );
2427 }
2428
2429 if ( $this->problematicRedirectTarget !== null ) {
2430 // T395767, T395768: Save the target to a variable so the constraint can fail again if the redirect is
2431 // still problematic but has changed between two save attempts
2432 $out->addHTML( Html::hidden(
2433 'wpAllowedProblematicRedirectTarget',
2434 $this->problematicRedirectTarget->getFullText()
2435 ) );
2436 }
2437
2438 $autosumm = $this->autoSumm !== '' ? $this->autoSumm : md5( $this->summary );
2439 $out->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
2440
2441 $out->addHTML( Html::hidden( 'oldid', $this->oldid ) );
2442 $out->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
2443
2444 $out->addHTML( Html::hidden( 'format', $this->contentFormat ) );
2445 $out->addHTML( Html::hidden( 'model', $this->contentModel ) );
2446 if ( $this->changeTags ) {
2447 $out->addHTML( Html::hidden( 'wpChangeTagsAfterPreview', implode( ',', $this->changeTags ) ) );
2448 }
2449
2450 $out->enableOOUI();
2451
2452 if ( $this->section === 'new' ) {
2453 $this->showSummaryInput( true );
2454 $out->addHTML( $this->getSummaryPreview( true ) );
2455 }
2456
2457 $out->addHTML( $this->editFormTextBeforeContent );
2458 if ( $this->isConflict ) {
2459 $currentText = $this->pageEditingHelper->toEditText(
2460 $this->getCurrentContent(), $this->contentFormat, $this->enableApiEditOverride
2461 ) ?? '';
2462
2463 $editConflictHelper = $this->getEditConflictHelper();
2464 $editConflictHelper->setTextboxes( $this->textbox1, $currentText );
2465 $editConflictHelper->setContentModel( $this->contentModel );
2466 $editConflictHelper->setContentFormat( $this->contentFormat );
2467 $out->addHTML( $editConflictHelper->getEditFormHtmlBeforeContent() );
2468
2469 $this->textbox1 = $currentText;
2470 }
2471
2472 if ( !$this->getTitle()->isUserConfigPage() ) {
2473 $out->addHTML( self::getEditToolbar() ?? '' );
2474 }
2475
2476 if ( $this->blankArticle ) {
2477 $out->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
2478 }
2479
2480 if ( $this->isConflict ) {
2481 // In an edit conflict bypass the overridable content form method
2482 // and fallback to the raw wpTextbox1 since editconflicts can't be
2483 // resolved between page source edits and custom ui edits using the
2484 // custom edit ui.
2485 $conflictTextBoxAttribs = [];
2486 if ( $this->isOldRev ) {
2487 $conflictTextBoxAttribs['class'] = 'mw-textarea-oldrev';
2488 }
2489
2490 // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable
2491 // $editConflictHelper is declared, when isConflict is true
2492 $out->addHTML( $editConflictHelper->getEditConflictMainTextBox( $conflictTextBoxAttribs ) );
2493 // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable
2494 // $editConflictHelper is declared, when isConflict is true
2495 $out->addHTML( $editConflictHelper->getEditFormHtmlAfterContent() );
2496 } else {
2497 $this->showContentForm();
2498 }
2499
2500 $out->addHTML( $this->editFormTextAfterContent );
2501
2502 $this->showStandardInputs();
2503
2504 $this->showFormAfterText();
2505
2506 $this->showTosSummary();
2507
2508 $this->showEditTools();
2509
2510 $out->addHTML( $this->editFormTextAfterTools . "\n" );
2511
2512 $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
2513
2514 $out->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
2515 Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
2516
2517 $out->addHTML( Html::rawElement( 'div', [ 'class' => 'limitreport' ],
2518 self::getPreviewLimitReport( $this->mParserOutput ) ) );
2519
2520 $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
2521
2522 if ( $this->isConflict ) {
2523 try {
2524 $this->showConflict();
2525 } catch ( ContentSerializationException $ex ) {
2526 // this can't really happen, but be nice if it does.
2527 $out->addHTML( Html::errorBox(
2528 $this->context->msg(
2529 'content-failed-to-parse',
2530 $this->contentModel,
2531 $this->contentFormat,
2532 $ex->getMessage()
2533 )->parse()
2534 ) );
2535 }
2536 }
2537
2538 // Set a hidden field so JS knows what edit form mode we are in
2539 if ( $this->isConflict ) {
2540 $mode = 'conflict';
2541 } elseif ( $this->preview ) {
2542 $mode = 'preview';
2543 } elseif ( $this->diff ) {
2544 $mode = 'diff';
2545 } else {
2546 $mode = 'text';
2547 }
2548 $out->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
2549
2550 // Marker for detecting truncated form data. This must be the last
2551 // parameter sent in order to be of use, so do not move me.
2552 $out->addHTML( Html::hidden( 'wpUltimateParam', true ) );
2553 $out->addHTML( $this->editFormTextBottom . "\n</form>\n" );
2554
2555 if ( !$this->userOptionsLookup->getOption( $user, 'previewontop' ) ) {
2556 $this->displayPreviewArea( $previewOutput, false );
2557 }
2558 }
2559
2567 public function makeTemplatesOnThisPageList( array $templates ): string {
2568 $templateListFormatter = new TemplatesOnThisPageFormatter(
2569 $this->context,
2570 $this->linkRenderer,
2571 $this->linkBatchFactory,
2572 $this->restrictionStore
2573 );
2574
2575 // preview if preview, else section if section, else false
2576 $type = false;
2577 if ( $this->preview ) {
2578 $type = 'preview';
2579 } elseif ( $this->section !== '' ) {
2580 $type = 'section';
2581 }
2582
2583 return Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
2584 $templateListFormatter->format( $templates, $type )
2585 );
2586 }
2587
2591 private static function extractSectionTitle( string $text ): string|false {
2592 if ( preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches ) ) {
2593 return MediaWikiServices::getInstance()->getParser()
2594 ->stripSectionName( trim( $matches[2] ) );
2595 } else {
2596 return false;
2597 }
2598 }
2599
2600 private function showHeader(): void {
2601 $out = $this->context->getOutput();
2602 if ( $this->isConflict ) {
2603 $this->addExplainConflictHeader();
2604 $this->editRevId = $this->page->getLatest();
2605 } else {
2606 if ( $this->section !== '' && $this->section !== 'new' && $this->summary === '' &&
2607 !$this->preview && !$this->diff
2608 ) {
2609 // https://github.com/phan/phan/issues/5514
2610 // @phan-suppress-next-line PhanImpossibleTypeComparison
2611 if ( $this->section === '0' ) {
2612 $this->summary = "/* */ ";
2613 } else {
2614 $sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object
2615 if ( $sectionTitle !== false && $sectionTitle !== '' ) {
2616 $this->summary = "/* $sectionTitle */ ";
2617 }
2618 }
2619 }
2620
2621 if ( $this->hookError !== '' ) {
2622 $out->addWikiTextAsInterface( $this->hookError );
2623 }
2624
2625 if ( $this->section != 'new' ) {
2626 $revRecord = $this->mArticle->fetchRevisionRecord();
2627 if ( $revRecord instanceof RevisionStoreRecord && !$revRecord->isCurrent() ) {
2628 $this->mArticle->setOldSubtitle( $revRecord->getId() );
2629 $this->isOldRev = true;
2630 }
2631 }
2632 }
2633
2634 if ( $this->formtype !== 'save' ) {
2636 $constraintFactory = MediaWikiServices::getInstance()->getService( '_EditConstraintFactory' );
2637 $constraintRunner = new EditConstraintRunner(
2638 $constraintFactory->newAccidentalRecreationConstraint(
2639 $this->getTitle(),
2640 // Ignore wpRedirect so the warning is still shown after a save attempt
2641 false,
2642 $this->starttime,
2643 ),
2644 new RevisionDeletedConstraint(
2645 false,
2646 $this->oldid,
2647 $this->mArticle->fetchRevisionRecord(),
2648 $this->section,
2649 $this->getTitle(),
2650 $this->context->getUser(),
2651 ),
2652 );
2653
2654 // No call to $this->handleFailedConstraint() here to avoid setting wpRedirect
2655 $out->addHTML( $this->formatConstraintStatus( $constraintRunner->checkAllConstraints() ) );
2656 }
2657
2658 $this->addLongPageWarningHeader();
2659 }
2660
2665 private function getSummaryInputAttributes( array $inputAttrs ): array {
2666 // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
2667 // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
2668 // Unicode codepoints.
2669 return $inputAttrs + [
2670 'id' => 'wpSummary',
2671 'name' => 'wpSummary',
2672 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
2673 'tabindex' => 1,
2674 'size' => 60,
2675 'spellcheck' => 'true',
2676 ];
2677 }
2678
2688 private function getSummaryInputWidget( $summary, string $labelText, array $inputAttrs ): FieldLayout {
2689 $inputAttrs = OOUI\Element::configFromHtmlAttributes(
2690 $this->getSummaryInputAttributes( $inputAttrs )
2691 );
2692 $inputAttrs += [
2693 'title' => Linker::titleAttrib( 'summary' ),
2694 'accessKey' => Linker::accesskey( 'summary' ),
2695 ];
2696
2697 // For compatibility with old scripts and extensions, we want the legacy 'id' on the `<input>`
2698 $inputAttrs['inputId'] = $inputAttrs['id'];
2699 $inputAttrs['id'] = 'wpSummaryWidget';
2700
2701 return new OOUI\FieldLayout(
2702 new OOUI\TextInputWidget( [
2703 'value' => $summary,
2704 'infusable' => true,
2705 ] + $inputAttrs ),
2706 [
2707 'label' => new OOUI\HtmlSnippet( $labelText ),
2708 'align' => 'top',
2709 'id' => 'wpSummaryLabel',
2710 'classes' => [ $this->missingSummary ? 'mw-summarymissed' : 'mw-summary' ],
2711 ]
2712 );
2713 }
2714
2720 private function showSummaryInput( bool $isSubjectPreview ): void {
2721 # Add a class if 'missingsummary' is triggered to allow styling of the summary line
2722 $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
2723 if ( $isSubjectPreview ) {
2724 if ( $this->nosummary ) {
2725 return;
2726 }
2727 } elseif ( !$this->mShowSummaryField ) {
2728 return;
2729 }
2730
2731 $labelText = $this->context->msg( $isSubjectPreview ? 'subject' : 'summary' )->parse();
2732 $this->context->getOutput()->addHTML(
2733 $this->getSummaryInputWidget(
2734 $isSubjectPreview ? $this->sectiontitle : $this->summary,
2735 $labelText,
2736 [ 'class' => $summaryClass ]
2737 )->toString()
2738 );
2739 }
2740
2746 private function getSummaryPreview( bool $isSubjectPreview ): string {
2747 // avoid spaces in preview, gets always trimmed on save
2748 $summary = trim( $this->summary );
2749 if ( $summary === '' || ( !$this->preview && !$this->diff ) ) {
2750 return "";
2751 }
2752
2753 $commentFormatter = MediaWikiServices::getInstance()->getCommentFormatter();
2754 $summary = $this->context->msg( 'summary-preview' )->parse()
2755 . $commentFormatter->formatBlock( $summary, $this->getTitle(), $isSubjectPreview );
2756 return Html::rawElement( 'div', [ 'class' => 'mw-summary-preview' ], $summary );
2757 }
2758
2759 private function showFormBeforeText(): void {
2760 $out = $this->context->getOutput();
2761 $out->addHTML( Html::hidden( 'wpSection', $this->section ) );
2762 $out->addHTML( Html::hidden( 'wpStarttime', $this->starttime ) );
2763 $out->addHTML( Html::hidden( 'wpEdittime', $this->edittime ) );
2764 $out->addHTML( Html::hidden( 'editRevId', $this->editRevId ) );
2765 $out->addHTML( Html::hidden( 'wpScrolltop', $this->scrolltop, [ 'id' => 'wpScrolltop' ] ) );
2766 }
2767
2768 protected function showFormAfterText() {
2781 $this->context->getOutput()->addHTML(
2782 "\n" .
2783 Html::hidden( "wpEditToken", $this->context->getUser()->getEditToken() ) .
2784 "\n"
2785 );
2786 }
2787
2796 protected function showContentForm() {
2797 $this->showTextbox1();
2798 }
2799
2800 private function showTextbox1(): void {
2801 $classes = $this->textboxBuilder->getTextboxProtectionCSSClasses( $this->getTitle() );
2802
2803 # Is an old revision being edited?
2804 if ( $this->isOldRev ) {
2805 $classes[] = 'mw-textarea-oldrev';
2806 }
2807
2808 $attribs = [
2809 'aria-label' => $this->context->msg( 'edit-textarea-aria-label' )->text(),
2810 'tabindex' => 1,
2811 'class' => $classes,
2812 ];
2813
2814 $this->showTextbox(
2815 $this->textbox1,
2816 'wpTextbox1',
2817 $attribs
2818 );
2819 }
2820
2821 protected function showTextbox( string $text, string $name, array $customAttribs = [] ): void {
2822 $attribs = $this->textboxBuilder->buildTextboxAttribs(
2823 $name,
2824 $customAttribs,
2825 $this->context->getUser(),
2826 $this->page
2827 );
2828
2829 $this->context->getOutput()->addHTML(
2830 Html::textarea( $name, $this->textboxBuilder->addNewLineAtEnd( $text ), $attribs )
2831 );
2832 }
2833
2834 private function displayPreviewArea( string $previewOutput, bool $isOnTop ): void {
2835 $attribs = [ 'id' => 'wikiPreview' ];
2836 if ( $isOnTop ) {
2837 $attribs['class'] = 'ontop';
2838 }
2839 if ( $this->formtype !== 'preview' ) {
2840 $attribs['style'] = 'display: none;';
2841 }
2842
2843 $out = $this->context->getOutput();
2844 $out->addHTML( Html::openElement( 'div', $attribs ) );
2845
2846 if ( $this->formtype === 'preview' ) {
2847 $this->showPreview( $previewOutput );
2848 }
2849
2850 $out->addHTML( '</div>' );
2851
2852 if ( $this->formtype === 'diff' ) {
2853 try {
2854 $this->showDiff();
2855 } catch ( ContentSerializationException $ex ) {
2856 $out->addHTML( Html::errorBox(
2857 $this->context->msg(
2858 'content-failed-to-parse',
2859 $this->contentModel,
2860 $this->contentFormat,
2861 $ex->getMessage()
2862 )->parse()
2863 ) );
2864 }
2865 }
2866 }
2867
2874 private function showPreview( string $text ): void {
2875 if ( $this->mArticle instanceof CategoryPage ) {
2876 $this->mArticle->openShowCategory();
2877 }
2878 # This hook seems slightly odd here, but makes things more
2879 # consistent for extensions.
2880 $out = $this->context->getOutput();
2881 $this->getHookRunner()->onOutputPageBeforeHTML( $out, $text );
2882 $out->addHTML( $text );
2883 if ( $this->mArticle instanceof CategoryPage ) {
2884 $this->mArticle->closeShowCategory();
2885 }
2886 }
2887
2895 public function showDiff(): void {
2896 $oldtitlemsg = 'currentrev';
2897 # if page does not exist, show diff against the preloaded default
2898 if ( !$this->page->exists() ) {
2899 $oldShadow = $this->shadowPageLoader->get( $this->getTitle() );
2900 $oldContent = $oldShadow?->getPreloadContent();
2901 if ( $oldContent ) {
2902 $this->assertSupportedContent( $oldContent );
2903 $oldtitlemsg = $oldShadow->getDiffTitleMessage() ?? $oldtitlemsg;
2904 }
2905 } else {
2906 $oldContent = $this->getCurrentContent();
2907 }
2908
2909 $textboxContent = $this->toEditContent( $this->textbox1 );
2910 if ( $this->editRevId !== null ) {
2911 $newContent = $this->page->replaceSectionAtRev(
2912 $this->section, $textboxContent, $this->sectiontitle, $this->editRevId
2913 );
2914 } else {
2915 $newContent = $this->page->replaceSectionContent(
2916 $this->section, $textboxContent, $this->sectiontitle, $this->edittime
2917 );
2918 }
2919
2920 if ( $newContent ) {
2921 $this->getHookRunner()->onEditPageGetDiffContent( $this, $newContent );
2922
2923 $user = $this->getUserForPreview();
2924 $parserOptions = ParserOptions::newFromUserAndLang( $user,
2925 MediaWikiServices::getInstance()->getContentLanguage() );
2926 $services = MediaWikiServices::getInstance();
2927 $contentTransformer = $services->getContentTransformer();
2928 $newContent = $contentTransformer->preSaveTransform( $newContent, $this->page, $user, $parserOptions );
2929 }
2930
2931 if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
2932 $oldtitle = $this->context->msg( $oldtitlemsg )->parse();
2933 $newtitle = $this->context->msg( 'yourtext' )->parse();
2934
2935 if ( !$oldContent ) {
2936 $oldContent = $newContent->getContentHandler()->makeEmptyContent();
2937 }
2938
2939 if ( !$newContent ) {
2940 $newContent = $oldContent->getContentHandler()->makeEmptyContent();
2941 }
2942
2943 $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->context );
2944 $de->setContent( $oldContent, $newContent );
2945
2946 $difftext = $de->getDiff( $oldtitle, $newtitle );
2947 $de->showDiffStyle();
2948 } else {
2949 $difftext = '';
2950 }
2951
2952 $this->context->getOutput()->addHTML( Html::rawElement( 'div', [ 'id' => 'wikiDiff' ], $difftext ) );
2953 }
2954
2963 private function showTosSummary(): void {
2964 $msgKey = 'editpage-tos-summary';
2965 $this->getHookRunner()->onEditPageTosSummary( $this->getTitle(), $msgKey );
2966 $msg = $this->context->msg( $msgKey );
2967 if ( !$msg->isDisabled() ) {
2968 $this->context->getOutput()->addHTML( Html::rawElement(
2969 'div',
2970 [ 'class' => 'mw-tos-summary' ],
2971 $msg->parseAsBlock()
2972 ) );
2973 }
2974 }
2975
2980 private function showEditTools(): void {
2981 $this->context->getOutput()->addHTML( Html::rawElement(
2982 'div',
2983 [ 'class' => 'mw-editTools' ],
2984 $this->context->msg( 'edittools' )->inContentLanguage()->parse()
2985 ) );
2986 }
2987
2996 public static function getCopyrightWarning(
2997 PageReference $page,
2998 string $format,
2999 MessageLocalizer $localizer
3000 ): string {
3001 $services = MediaWikiServices::getInstance();
3002 $rightsText = $services->getMainConfig()->get( MainConfigNames::RightsText );
3003 if ( $rightsText ) {
3004 $copywarnMsg = [ 'copyrightwarning',
3005 '[[' . $localizer->msg( 'copyrightpage' )->inContentLanguage()->text() . ']]',
3006 $rightsText ];
3007 } else {
3008 $copywarnMsg = [ 'copyrightwarning2',
3009 '[[' . $localizer->msg( 'copyrightpage' )->inContentLanguage()->text() . ']]' ];
3010 }
3011 // Allow for site and per-namespace customization of contribution/copyright notice.
3012 $title = Title::newFromPageReference( $page );
3013 ( new HookRunner( $services->getHookContainer() ) )->onEditPageCopyrightWarning( $title, $copywarnMsg );
3014 if ( !$copywarnMsg ) {
3015 return '';
3016 }
3017
3018 $msg = $localizer->msg( ...$copywarnMsg )->page( $page );
3019 return Html::rawElement( 'div', [ 'id' => 'editpage-copywarn' ], $msg->$format() );
3020 }
3021
3029 public static function getPreviewLimitReport( ?ParserOutput $output = null ): string {
3030 if ( !$output || !$output->getLimitReportData() ) {
3031 return '';
3032 }
3033
3034 $limitReport = Html::rawElement( 'div', [ 'class' => 'mw-limitReportExplanation' ],
3035 wfMessage( 'limitreport-title' )->parseAsBlock()
3036 );
3037
3038 // Show/hide animation doesn't work correctly on a table, so wrap it in a div.
3039 $limitReport .= Html::openElement( 'div', [ 'class' => 'preview-limit-report-wrapper' ] );
3040
3041 $limitReport .= Html::openElement( 'table', [
3042 'class' => 'preview-limit-report wikitable'
3043 ] ) .
3044 Html::openElement( 'tbody' );
3045
3046 $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
3047 foreach ( $output->getLimitReportData() as $key => $value ) {
3048 if ( in_array( $key, [
3049 'cachereport-origin',
3050 'cachereport-timestamp',
3051 'cachereport-ttl',
3052 'cachereport-transientcontent',
3053 'limitreport-timingprofile',
3054 ] ) ) {
3055 // These entries have non-numeric parameters, and can't be displayed by this code.
3056 // They are used by the plaintext limit report (see RenderDebugInfo::debugInfo()).
3057 // TODO: Display this information in the table somehow.
3058 continue;
3059 }
3060
3061 if ( $hookRunner->onParserLimitReportFormat( $key, $value, $limitReport, true, true ) ) {
3062 $keyMsg = wfMessage( $key );
3063 $valueMsg = wfMessage( "$key-value" );
3064 if ( !$valueMsg->exists() ) {
3065 // This is formatted raw, not as localized number.
3066 // If you want the parameter formatted as a number,
3067 // define the `$key-value` message.
3068 $valueMsg = ( new RawMessage( '$1' ) )->params( $value );
3069 } else {
3070 // If you define the `$key-value` or `$key-value-html`
3071 // message then the argument *must* be numeric.
3072 $valueMsg = $valueMsg->numParams( $value );
3073 }
3074 if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
3075 $limitReport .= Html::rawElement( 'tr', [],
3076 Html::rawElement( 'th', [], $keyMsg->parse() ) .
3077 Html::rawElement( 'td', [], $valueMsg->parse() )
3078 );
3079 }
3080 }
3081 }
3082
3083 $limitReport .= Html::closeElement( 'tbody' ) .
3084 Html::closeElement( 'table' ) .
3085 Html::closeElement( 'div' );
3086
3087 return $limitReport;
3088 }
3089
3090 protected function showStandardInputs( int &$tabindex = 2 ): void {
3091 $out = $this->context->getOutput();
3092 $out->addHTML( "<div class='editOptions'>\n" );
3093
3094 if ( $this->section !== 'new' ) {
3095 $this->showSummaryInput( false );
3096 $out->addHTML( $this->getSummaryPreview( false ) );
3097 }
3098
3099 // When previewing, override the selected dropdown option to select whatever was posted
3100 // (if it's a valid option) rather than the current value for watchlistExpiry.
3101 // See also above in $this->importFormDataPosted().
3102 $expiryFromRequest = null;
3103 if ( $this->preview || $this->diff || $this->isConflict ) {
3104 $expiryFromRequest = $this->getContext()->getRequest()->getText( 'wpWatchlistExpiry' );
3105 }
3106
3107 $checkboxes = $this->getCheckboxesWidget(
3108 $tabindex,
3109 [ 'minor' => $this->minoredit, 'watch' => $this->watchthis, 'wpWatchlistExpiry' => $expiryFromRequest ]
3110 );
3111 $watchlistLabelsWidget = $this->getWatchlistLabelsWidget( $tabindex );
3112 $watchlistLabelClass = 'mw-watchlistlabels-component';
3113 // Add a message (inline with the checkboxes) when the user doesn't have any labels.
3114 if ( $this->context->getUser()->isNamed() && !$watchlistLabelsWidget && $this->watchlistLabelsEnabled ) {
3115 $checkboxes['watchlistlabels'] = new OOUI\LabelWidget( [
3116 'label' => new OOUI\HtmlSnippet( $this->context->msg( 'watchlistlabels-editpage-nolabels' )->parse() ),
3117 'classes' => [ $watchlistLabelClass ],
3118 ] );
3119 }
3120 $checkboxesHTML = new OOUI\HorizontalLayout( [ 'items' => array_values( $checkboxes ) ] );
3121
3122 $out->addHTML( Html::rawElement( 'div', [ 'class' => 'editCheckboxes' ], (string)$checkboxesHTML ) . "\n" );
3123 if ( $watchlistLabelsWidget ) {
3124 $out->addHTML( Html::rawElement(
3125 'div',
3126 [ 'class' => $watchlistLabelClass . ' mw-editpage-watchlistLabels' ],
3127 (string)$watchlistLabelsWidget
3128 ) . "\n" );
3129 }
3130
3131 // Show copyright warning.
3132 $out->addHTML( self::getCopyrightWarning( $this->page, 'parse', $this->context ) );
3133 $out->addHTML( $this->editFormTextAfterWarn );
3134
3135 $out->addHTML( "<div class='editButtons'>\n" );
3136 $out->addHTML( implode( "\n", $this->getEditButtons( $tabindex ) ) . "\n" );
3137
3138 $cancel = $this->getCancelLink( $tabindex++ );
3139
3140 $edithelp = $this->getHelpLink() .
3141 $this->context->msg( 'word-separator' )->escaped() .
3142 $this->context->msg( 'newwindow' )->parse();
3143
3144 $out->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" );
3145 $out->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" );
3146 $out->addHTML( "</div><!-- editButtons -->\n" );
3147
3148 $this->getHookRunner()->onEditPage__showStandardInputs_options( $this, $out, $tabindex );
3149
3150 $out->addHTML( "</div><!-- editOptions -->\n" );
3151 }
3152
3157 private function showConflict(): void {
3158 $out = $this->context->getOutput();
3159 if ( $this->getHookRunner()->onEditPageBeforeConflictDiff( $this, $out ) ) {
3160 $this->incrementConflictStats();
3161
3162 $this->getEditConflictHelper()->showEditFormTextAfterFooters();
3163 }
3164 }
3165
3166 private function incrementConflictStats(): void {
3167 $this->getEditConflictHelper()->incrementConflictStats( $this->context->getUser() );
3168 }
3169
3170 private function getHelpLink(): string {
3171 $message = $this->context->msg( 'edithelppage' )->inContentLanguage()->text();
3172 $editHelpUrl = Skin::makeInternalOrExternalUrl( $message );
3173 return Html::element( 'a', [
3174 'href' => $editHelpUrl,
3175 'target' => 'helpwindow'
3176 ], $this->context->msg( 'edithelp' )->text() );
3177 }
3178
3183 private function getCancelLink( int $tabindex ): ButtonWidget {
3184 $cancelParams = [];
3185 if ( !$this->isConflict && $this->oldid > 0 ) {
3186 $cancelParams['oldid'] = $this->oldid;
3187 } elseif ( $this->getContextTitle()->isRedirect() ) {
3188 $cancelParams['redirect'] = 'no';
3189 }
3190
3191 return new OOUI\ButtonWidget( [
3192 'id' => 'mw-editform-cancel',
3193 'tabIndex' => $tabindex,
3194 'href' => $this->getContextTitle()->getLinkURL( $cancelParams ),
3195 'label' => new OOUI\HtmlSnippet( $this->context->msg( 'cancel' )->parse() ),
3196 'framed' => false,
3197 'infusable' => true,
3198 'flags' => 'destructive',
3199 ] );
3200 }
3201
3211 protected function getActionURL( Title $title ) {
3212 $request = $this->context->getRequest();
3213 $params = $request->getQueryValuesOnly();
3214
3215 $allowedFormParams = [
3216 'section', 'oldid', 'preloadtitle', 'undo', 'undoafter',
3217 // Considered safe in all contexts
3218 'uselang', 'useskin', 'useformat', 'variant', 'debug', 'safemode'
3219 ];
3220 $formParams = [ 'action' => 'submit' ];
3221 foreach ( $params as $arg => $val ) {
3222 if ( in_array( $arg, $allowedFormParams, true ) ) {
3223 $formParams[$arg] = $val;
3224 }
3225 }
3226
3227 return $title->getLocalURL( $formParams );
3228 }
3229
3235 public function getPreviewText() {
3236 $out = $this->context->getOutput();
3237 $config = $this->context->getConfig();
3238
3239 if ( $config->get( MainConfigNames::RawHtml ) && !$this->mTokenOk ) {
3240 // Could be an offsite preview attempt. This is very unsafe if
3241 // HTML is enabled, as it could be an attack.
3242 $parsedNote = '';
3243 if ( $this->textbox1 !== '' ) {
3244 // Do not put big scary notice, if previewing the empty
3245 // string, which happens when you initially edit
3246 // a category page, due to automatic preview-on-open.
3247 $parsedNote = Html::rawElement( 'div', [ 'class' => 'previewnote' ],
3248 $out->parseAsInterface(
3249 $this->context->msg( 'session_fail_preview_html' )->plain()
3250 ) );
3251 }
3252 $this->incrementEditFailureStats( 'session_loss' );
3253 return $parsedNote;
3254 }
3255
3256 $previewStatus = StatusValue::newGood();
3257 $previewNoteHtml = '';
3258
3259 try {
3260 $content = $this->toEditContent( $this->textbox1 );
3261
3262 $previewHTML = '';
3263 if ( !$this->getHookRunner()->onAlternateEditPreview(
3264 $this, $content, $previewHTML, $this->mParserOutput )
3265 ) {
3266 return $previewHTML;
3267 }
3268
3269 $continueEditingHtml = Html::rawElement(
3270 'span',
3271 [ 'class' => 'mw-continue-editing' ],
3272 $this->linkRenderer->makePreloadedLink(
3273 new TitleValue( NS_MAIN, '', self::EDITFORM_ID ),
3274 $this->context->getLanguage()->getArrow() . ' ' . $this->context->msg( 'continue-editing' )->text()
3275 )
3276 );
3277
3278 if ( $this->mTriedSave && !$this->mTokenOk ) {
3279 $previewStatus->fatal( 'session_fail_preview' );
3280 $this->incrementEditFailureStats( 'session_loss' );
3281 } elseif ( $this->incompleteForm ) {
3282 $previewStatus->fatal( 'edit_form_incomplete' );
3283 if ( $this->mTriedSave ) {
3284 $this->incrementEditFailureStats( 'incomplete_form' );
3285 }
3286 } else {
3287 $previewNoteHtml = Html::noticeBox(
3288 $this->context->msg( 'previewnote' )->parse() . ' ' . $continueEditingHtml
3289 );
3290 }
3291
3292 # don't parse non-wikitext pages, show message about preview
3293 if ( $this->getTitle()->isUserConfigPage() || $this->getTitle()->isSiteConfigPage() ) {
3294 if ( $this->getTitle()->isUserConfigPage() ) {
3295 $level = 'user';
3296 } elseif ( $this->getTitle()->isSiteConfigPage() ) {
3297 $level = 'site';
3298 } else {
3299 $level = false;
3300 }
3301
3302 if ( $content->getModel() === CONTENT_MODEL_CSS ) {
3303 $format = 'css';
3304 if ( $level === 'user' && !$config->get( MainConfigNames::AllowUserCss ) ) {
3305 $format = false;
3306 }
3307 } elseif ( $content->getModel() === CONTENT_MODEL_JSON ) {
3308 $format = 'json';
3309 if ( $level === 'user' /* No comparable 'AllowUserJson' */ ) {
3310 $format = false;
3311 }
3312 } elseif ( $content->getModel() === CONTENT_MODEL_JAVASCRIPT ) {
3313 $format = 'js';
3314 if ( $level === 'user' && !$config->get( MainConfigNames::AllowUserJs ) ) {
3315 $format = false;
3316 }
3317 } elseif ( $content->getModel() === CONTENT_MODEL_VUE ) {
3318 $format = 'vue';
3319 if ( $level === 'user' && !$config->get( MainConfigNames::AllowUserJs ) ) {
3320 $format = false;
3321 }
3322 } else {
3323 $format = false;
3324 }
3325
3326 # Used messages to make sure grep find them:
3327 # Messages: usercsspreview, userjsonpreview, userjspreview, uservuepreview,
3328 # sitecsspreview, sitejsonpreview, sitejspreview, sitevuepreview
3329 if ( $level && $format ) {
3330 $previewNoteHtml = Html::noticeBox( Html::rawElement(
3331 'div',
3332 [ 'id' => "mw-{$level}{$format}preview" ],
3333 $this->context->msg( "{$level}{$format}preview" )->parse() . $continueEditingHtml
3334 ) );
3335 }
3336 }
3337
3338 if ( $this->section === "new" ) {
3339 $content = $content->addSectionHeader( $this->sectiontitle );
3340 }
3341
3342 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
3343 $this->getHookRunner()->onEditPageGetPreviewContent( $this, $content );
3344
3345 $parserResult = $this->doPreviewParse( $content );
3346 $parserOutput = $parserResult['parserOutput'];
3347 $previewHTML = $parserResult['html'];
3348 $this->mParserOutput = $parserOutput;
3349 $out->addParserOutputMetadata( $parserOutput );
3350 if ( $out->userCanPreview() ) {
3351 $out->addContentOverride( $this->getTitle(), $content );
3352 }
3353
3354 // T394016 - Run some edit constraints on page preview
3356 $constraintFactory = MediaWikiServices::getInstance()->getService( '_EditConstraintFactory' );
3357 $constraintRunner = new EditConstraintRunner();
3358
3359 $constraintRunner->addConstraint( $constraintFactory->newRedirectConstraint(
3360 null,
3361 $content,
3362 null,
3363 $this->getTitle(),
3364 MessageValue::new( 'edit-constraint-warning-wrapper' ),
3365 $this->contentFormat,
3366 ) );
3367
3368 $previewStatus->merge( $constraintRunner->checkAllConstraints() );
3369
3370 foreach ( $parserOutput->getWarningMsgs() as $warning ) {
3371 $previewStatus->warning( $warning );
3372 }
3373 } catch ( ContentSerializationException $ex ) {
3374 $previewStatus->fatal(
3375 'content-failed-to-parse',
3376 $this->contentModel,
3377 $this->contentFormat,
3378 $ex->getMessage()
3379 );
3380 $previewHTML = '';
3381 }
3382
3383 if ( $this->isConflict ) {
3384 $conflict = Html::warningBox(
3385 $this->context->msg( 'previewconflict' )->escaped(),
3386 'mw-previewconflict'
3387 );
3388 } else {
3389 $conflict = '';
3390 }
3391
3392 $previewhead = Html::rawElement(
3393 'div', [ 'class' => 'previewnote' ],
3394 Html::element(
3395 'h2', [ 'id' => 'mw-previewheader' ],
3396 $this->context->msg( 'preview' )->text()
3397 ) . $this->formatConstraintStatus( $previewStatus ) . $previewNoteHtml . $conflict
3398 );
3399
3400 return $previewhead . $previewHTML . $this->previewTextAfterContent;
3401 }
3402
3403 private function incrementEditFailureStats( string $failureType ): void {
3404 MediaWikiServices::getInstance()->getStatsFactory()
3405 ->getCounter( 'edit_failure_total' )
3406 ->setLabel( 'cause', $failureType )
3407 ->setLabel( 'namespace', 'n/a' )
3408 ->setLabel( 'user_bucket', 'n/a' )
3409 ->increment();
3410 }
3411
3416 $parserOptions = $this->page->makeParserOptions( $this->context );
3417 $parserOptions->setRenderReason( 'page-preview' );
3418 $parserOptions->setIsPreview( true );
3419 $parserOptions->setIsSectionPreview( $this->section !== '' );
3420 $parserOptions->setSuppressSectionEditLinks();
3421
3422 // XXX: we could call $parserOptions->setCurrentRevisionRecordCallback here to force the
3423 // current revision to be null during PST, until setupFakeRevision is called on
3424 // the ParserOptions. Currently, we rely on Parser::getRevisionRecordObject() to ignore
3425 // existing revisions in preview mode.
3426
3427 return $parserOptions;
3428 }
3429
3439 protected function doPreviewParse( Content $content ) {
3440 $user = $this->getUserForPreview();
3441 $parserOptions = $this->getPreviewParserOptions();
3442
3443 // NOTE: preSaveTransform doesn't have a fake revision to operate on.
3444 // Parser::getRevisionRecordObject() will return null in preview mode,
3445 // causing the context user to be used for {{subst:REVISIONUSER}}.
3446 // XXX: Alternatively, we could also call setupFakeRevision()
3447 // before PST with $content.
3448 $services = MediaWikiServices::getInstance();
3449 $contentTransformer = $services->getContentTransformer();
3450 $contentRenderer = $services->getContentRenderer();
3451 $pstContent = $contentTransformer->preSaveTransform( $content, $this->page, $user, $parserOptions );
3452 $parserOutput = $contentRenderer->getParserOutput( $pstContent, $this->page, null, $parserOptions );
3453 $out = $this->context->getOutput();
3454 $skin = $out->getSkin();
3455 $skinOptions = $skin->getOptions();
3456 // TODO T371004 move runOutputPipeline out of $parserOutput
3457 // TODO T371022 ideally we clone here, but for now let's reproduce getText behaviour
3458 $oldHtml = $parserOutput->getContentHolderText();
3459 $html = $parserOutput->runOutputPipeline( $parserOptions, [
3460 'allowClone' => 'false',
3461 'userLang' => $skin->getLanguage(),
3462 'injectTOC' => $skinOptions['toc'],
3463 'includeDebugInfo' => true,
3464 ] )->getContentHolderText();
3465 $parserOutput->setContentHolderText( $oldHtml );
3466 return [
3467 'parserOutput' => $parserOutput,
3468 'html' => $html
3469 ];
3470 }
3471
3475 public function getTemplates() {
3476 if ( $this->preview || $this->section !== '' ) {
3477 $templates = [];
3478 if ( !$this->mParserOutput ) {
3479 return $templates;
3480 }
3481 foreach (
3482 $this->mParserOutput->getLinkList( ParserOutputLinkTypes::TEMPLATE )
3483 as [ 'link' => $link ]
3484 ) {
3485 $templates[] = Title::newFromLinkTarget( $link );
3486 }
3487 return $templates;
3488 } else {
3489 return $this->getTitle()->getTemplateLinksFrom();
3490 }
3491 }
3492
3496 public static function getEditToolbar(): ?string {
3497 $startingToolbar = '<div id="toolbar"></div>';
3498 $toolbar = $startingToolbar;
3499
3500 $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
3501 if ( !$hookRunner->onEditPageBeforeEditToolbar( $toolbar ) ) {
3502 return null;
3503 }
3504 // Don't add a pointless `<div>` to the page unless a hook caller populated it
3505 return ( $toolbar === $startingToolbar ) ? null : $toolbar;
3506 }
3507
3533 public function getCheckboxesDefinition( $values ): array {
3534 $checkboxes = [];
3535
3536 $user = $this->context->getUser();
3537 // don't show the minor edit checkbox if it's a new page or section
3538 if ( !$this->isNew && $this->permManager->userHasRight( $user, 'minoredit' ) ) {
3539 $checkboxes['wpMinoredit'] = [
3540 'id' => 'wpMinoredit',
3541 'label-message' => 'minoredit',
3542 // Uses messages: tooltip-minoredit, accesskey-minoredit
3543 'tooltip' => 'minoredit',
3544 'label-id' => 'mw-editpage-minoredit',
3545 'legacy-name' => 'minor',
3546 'default' => $values['minor'],
3547 ];
3548 }
3549
3550 if ( $user->isNamed() ) {
3551 $checkboxes = array_merge(
3552 $checkboxes,
3553 $this->getCheckboxesDefinitionForWatchlist( $values['watch'], $values['wpWatchlistExpiry'] ?? null )
3554 );
3555 }
3556
3557 $this->getHookRunner()->onEditPageGetCheckboxesDefinition( $this, $checkboxes );
3558
3559 return $checkboxes;
3560 }
3561
3569 private function getCheckboxesDefinitionForWatchlist( $watch, $watchexpiry ): array {
3570 $fieldDefs = [
3571 'wpWatchthis' => [
3572 'id' => 'wpWatchthis',
3573 'label-message' => 'watchthis',
3574 // Uses messages: tooltip-watch, accesskey-watch
3575 'tooltip' => 'watch',
3576 'label-id' => 'mw-editpage-watch',
3577 'legacy-name' => 'watch',
3578 'default' => $watch,
3579 ]
3580 ];
3581 if ( $this->watchlistExpiryEnabled ) {
3582 $watchedItem = $this->watchedItemStore->getWatchedItem( $this->getContext()->getUser(), $this->getTitle() );
3583 if ( $watchedItem instanceof WatchedItem && $watchedItem->getExpiry() === null ) {
3584 // Not temporarily watched, so we always default to infinite.
3585 $userPreferredExpiry = 'infinite';
3586 } else {
3587 $userPreferredExpiryOption = !$this->page->exists()
3588 ? 'watchcreations-expiry'
3589 : 'watchdefault-expiry';
3590 $userPreferredExpiry = $this->userOptionsLookup->getOption(
3591 $this->getContext()->getUser(),
3592 $userPreferredExpiryOption,
3593 'infinite'
3594 );
3595 }
3596
3597 $expiryOptions = WatchAction::getExpiryOptions(
3598 $this->getContext(),
3599 $watchedItem,
3600 $userPreferredExpiry
3601 );
3602
3603 if ( $watchexpiry && in_array( $watchexpiry, $expiryOptions['options'] ) ) {
3604 $expiryOptions['default'] = $watchexpiry;
3605 }
3606 // When previewing, override the selected dropdown option to select whatever was posted
3607 // (if it's a valid option) rather than the current value for watchlistExpiry.
3608 // See also above in $this->importFormDataPosted().
3609 $expiryFromRequest = $this->getContext()->getRequest()->getText( 'wpWatchlistExpiry' );
3610 if ( ( $this->preview || $this->diff ) && in_array( $expiryFromRequest, $expiryOptions['options'] ) ) {
3611 $expiryOptions['default'] = $expiryFromRequest;
3612 }
3613
3614 // Reformat the options to match what DropdownInputWidget wants.
3615 $options = [];
3616 foreach ( $expiryOptions['options'] as $label => $value ) {
3617 $options[] = [ 'data' => $value, 'label' => $label ];
3618 }
3619
3620 $fieldDefs['wpWatchlistExpiry'] = [
3621 'id' => 'wpWatchlistExpiry',
3622 'label-message' => 'confirm-watch-label',
3623 // Uses messages: tooltip-watchlist-expiry, accesskey-watchlist-expiry
3624 'tooltip' => 'watchlist-expiry',
3625 'label-id' => 'mw-editpage-watchlist-expiry',
3626 'default' => $expiryOptions['default'],
3627 'value-attr' => 'value',
3628 'class' => DropdownInputWidget::class,
3629 'options' => $options,
3630 'invisibleLabel' => true,
3631 ];
3632 }
3633 return $fieldDefs;
3634 }
3635
3646 public function getCheckboxesWidget( &$tabindex, $values ) {
3647 $checkboxes = [];
3648 $checkboxesDef = $this->getCheckboxesDefinition( $values );
3649
3650 foreach ( $checkboxesDef as $name => $options ) {
3651 $legacyName = $options['legacy-name'] ?? $name;
3652
3653 $title = null;
3654 $accesskey = null;
3655 if ( isset( $options['tooltip'] ) ) {
3656 $accesskey = $this->context->msg( "accesskey-{$options['tooltip']}" )->text();
3657 $title = Linker::titleAttrib( $options['tooltip'] );
3658 }
3659 if ( isset( $options['title-message'] ) ) {
3660 $title = $this->context->msg( $options['title-message'] )->text();
3661 }
3662 // Allow checkbox definitions to set their own class and value-attribute names.
3663 // See $this->getCheckboxesDefinition() for details.
3664 $className = $options['class'] ?? CheckboxInputWidget::class;
3665 $valueAttr = $options['value-attr'] ?? 'selected';
3666 $checkboxes[ $legacyName ] = new FieldLayout(
3667 new $className( [
3668 'tabIndex' => ++$tabindex,
3669 'accessKey' => $accesskey,
3670 'id' => $options['id'] . 'Widget',
3671 'inputId' => $options['id'],
3672 'name' => $name,
3673 $valueAttr => $options['default'],
3674 'infusable' => true,
3675 'options' => $options['options'] ?? null,
3676 ] ),
3677 [
3678 'align' => 'inline',
3679 'label' => new OOUI\HtmlSnippet( $this->context->msg( $options['label-message'] )->parse() ),
3680 'title' => $title,
3681 'id' => $options['label-id'] ?? null,
3682 'invisibleLabel' => $options['invisibleLabel'] ?? null,
3683 ]
3684 );
3685 }
3686
3687 return $checkboxes;
3688 }
3689
3696 private function getWatchlistLabelsWidget( int &$tabindex ): ?FieldLayout {
3697 if ( !$this->watchlistLabelsEnabled ) {
3698 return null;
3699 }
3700
3701 $user = $this->context->getUser();
3702 if ( !$user->isNamed() ) {
3703 return null;
3704 }
3705
3706 $userLabels = $this->watchlistLabelStore->loadAllForUser( $user );
3707 if ( !$userLabels ) {
3708 return null;
3709 }
3710
3711 $this->context->getOutput()->addModuleStyles( 'mediawiki.widgets.TagMultiselectWidget.styles' );
3712 $this->context->getOutput()->addModules( 'mediawiki.widgets.MenuTagMultiselectWidget' );
3713
3714 // Determine which labels are currently selected.
3715 // After the initial render, use submitted values to preserve state across retries.
3716 if ( !$this->firsttime ) {
3717 // getIntArray() should already return int[], but keep this non-null for static analysis.
3718 $selectedLabelIds = (array)$this->getContext()->getRequest()->getIntArray( 'wpWatchlistLabels', [] );
3719 } else {
3720 // Use the labels currently assigned to this watched item.
3721 $watchedItem = $this->watchedItemStore->getWatchedItem( $user, $this->page );
3722 $selectedLabelIds = [];
3723 // Keep this as int[] so array_map( strval(...) ) is always called with an array.
3724 if ( $watchedItem instanceof WatchedItem ) {
3725 foreach ( $watchedItem->getLabels() as $label ) {
3726 $labelId = $label->getId();
3727 if ( $labelId !== null ) {
3728 $selectedLabelIds[] = $labelId;
3729 }
3730 }
3731 }
3732 }
3733
3734 // Build options for MenuTagMultiselectWidget (flat list, no group header).
3735 $options = [];
3736 foreach ( $userLabels as $label ) {
3737 $options[] = [ 'data' => (string)$label->getId(), 'label' => $label->getName() ];
3738 }
3739
3740 $selectedStrings = array_map( strval( ... ), $selectedLabelIds );
3741
3742 // Build no-JS fallback: a CheckboxMultiselectInputWidget
3743 $noJsFallback = [ new CheckboxMultiselectInputWidget( [
3744 'name' => 'wpWatchlistLabels[]',
3745 'value' => $selectedStrings,
3746 'options' => $options,
3747 ] ) ];
3748
3749 $widget = new MenuTagMultiselectWidget( [
3750 'name' => 'wpWatchlistLabels',
3751 'options' => [ '' => $options ],
3752 'default' => $selectedStrings,
3753 'noJsFallback' => $noJsFallback,
3754 'infusable' => true,
3755 'id' => 'wpWatchlistLabelsWidget',
3756 'tabIndex' => ++$tabindex,
3757 'allowReordering' => false,
3758 'placeholder' => $this->context->msg( 'watchlistlabels-editpage-placeholder' )->text(),
3759 ] );
3760
3761 $helpMsg = $this->context->msg( 'watchlistlabels-editpage-help' );
3762 $help = null;
3763 if ( !$helpMsg->isDisabled() ) {
3764 $help = new OOUI\HtmlSnippet( $helpMsg->parse() );
3765 }
3766 return new FieldLayout(
3767 $widget,
3768 [
3769 'label' => new OOUI\HtmlSnippet(
3770 $this->context->msg( 'watchlistlabels-editpage-label' )->parse()
3771 ),
3772 'help' => $help,
3773 'helpInline' => true,
3774 'align' => 'top',
3775 'id' => 'mw-editpage-watchlist-labels',
3776 'classes' => [ 'mw-editpage-watchlistLabels' ],
3777 ]
3778 );
3779 }
3780
3791 public function getEditButtons( &$tabindex ) {
3792 $buttons = [];
3793
3794 $labelAsPublish =
3795 $this->context->getConfig()->get( MainConfigNames::EditSubmitButtonLabelPublish );
3796
3797 $buttonLabel = $this->context->msg( $this->pageEditingHelper->getSubmitButtonLabel( $this->page ) )->text();
3798 $buttonTooltip = $labelAsPublish ? 'publish' : 'save';
3799
3800 $buttons['save'] = new OOUI\ButtonInputWidget( [
3801 'name' => 'wpSave',
3802 'tabIndex' => ++$tabindex,
3803 'id' => 'wpSaveWidget',
3804 'inputId' => 'wpSave',
3805 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
3806 'useInputTag' => true,
3807 'flags' => [ 'progressive', 'primary' ],
3808 'label' => $buttonLabel,
3809 'infusable' => true,
3810 'type' => 'submit',
3811 // Messages used: tooltip-save, tooltip-publish
3812 'title' => Linker::titleAttrib( $buttonTooltip ),
3813 // Messages used: accesskey-save, accesskey-publish
3814 'accessKey' => Linker::accesskey( $buttonTooltip ),
3815 ] );
3816
3817 $buttons['preview'] = new OOUI\ButtonInputWidget( [
3818 'name' => 'wpPreview',
3819 'tabIndex' => ++$tabindex,
3820 'id' => 'wpPreviewWidget',
3821 'inputId' => 'wpPreview',
3822 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
3823 'useInputTag' => true,
3824 'label' => $this->context->msg( 'showpreview' )->text(),
3825 'infusable' => true,
3826 'type' => 'submit',
3827 // Allow previewing even when the form is in invalid state (T343585)
3828 'formNoValidate' => true,
3829 // Message used: tooltip-preview
3830 'title' => Linker::titleAttrib( 'preview' ),
3831 // Message used: accesskey-preview
3832 'accessKey' => Linker::accesskey( 'preview' ),
3833 ] );
3834
3835 $buttons['diff'] = new OOUI\ButtonInputWidget( [
3836 'name' => 'wpDiff',
3837 'tabIndex' => ++$tabindex,
3838 'id' => 'wpDiffWidget',
3839 'inputId' => 'wpDiff',
3840 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
3841 'useInputTag' => true,
3842 'label' => $this->context->msg( 'showdiff' )->text(),
3843 'infusable' => true,
3844 'type' => 'submit',
3845 // Allow previewing even when the form is in invalid state (T343585)
3846 'formNoValidate' => true,
3847 // Message used: tooltip-diff
3848 'title' => Linker::titleAttrib( 'diff' ),
3849 // Message used: accesskey-diff
3850 'accessKey' => Linker::accesskey( 'diff' ),
3851 ] );
3852
3853 $this->getHookRunner()->onEditPageBeforeEditButtons( $this, $buttons, $tabindex );
3854
3855 return $buttons;
3856 }
3857
3862 private function noSuchSectionPage(): void {
3863 $out = $this->context->getOutput();
3864 $out->prepareErrorPage();
3865 $out->setPageTitleMsg( $this->context->msg( 'nosuchsectiontitle' ) );
3866
3867 $res = $this->context->msg( 'nosuchsectiontext', $this->section )->parseAsBlock();
3868
3869 $this->getHookRunner()->onEditPageNoSuchSection( $this, $res );
3870 $out->addHTML( $res );
3871
3872 $out->returnToMain( false, $this->page );
3873 }
3874
3880 public function spamPageWithContent( $match = false ): void {
3881 $out = $this->context->getOutput();
3882 $out->prepareErrorPage();
3883 $out->setPageTitleMsg( $this->context->msg( 'spamprotectiontitle' ) );
3884
3885 $spamText = $this->context->msg( 'spamprotectiontext' )->parseAsBlock();
3886
3887 if ( $match ) {
3888 if ( is_array( $match ) ) {
3889 $matchText = Message::listParam( array_map( wfEscapeWikiText( ... ), $match ) );
3890 } else {
3891 $matchText = wfEscapeWikiText( $match );
3892 }
3893
3894 $spamText .= $this->context->msg( 'spamprotectionmatch', $matchText )->parseAsBlock();
3895 }
3896 $out->addHTML( Html::rawElement(
3897 'div',
3898 [ 'id' => 'spamprotected' ],
3899 $spamText
3900 ) );
3901
3902 $out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
3903 $this->showDiff();
3904
3905 $out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
3906 $this->showTextbox( $this->textbox1, 'wpTextbox2', [ 'tabindex' => 6, 'readonly' ] );
3907
3908 $out->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
3909 }
3910
3911 private function addLongPageWarningHeader(): void {
3912 if ( $this->contentLength === false ) {
3913 $this->contentLength = strlen( $this->textbox1 );
3914 }
3915
3916 $out = $this->context->getOutput();
3917 $longPageHint = $this->context->msg( 'longpage-hint' );
3918 if ( !$longPageHint->isDisabled() ) {
3919 $msgText = trim( $longPageHint->sizeParams( $this->contentLength )
3920 ->params( $this->contentLength ) // Keep this unformatted for math inside message
3921 ->parse() );
3922 if ( $msgText !== '' && $msgText !== '-' ) {
3923 $out->addHTML( "<div id='mw-edit-longpage-hint'>\n$msgText\n</div>" );
3924 }
3925 }
3926 }
3927
3928 private function addExplainConflictHeader(): void {
3929 $this->context->getOutput()->addHTML(
3930 $this->getEditConflictHelper()->getExplainHeader()
3931 );
3932 }
3933
3938 public function setEditConflictHelperFactory( callable $factory ): void {
3939 Assert::precondition( !$this->editConflictHelperFactory,
3940 'Can only have one extension that resolves edit conflicts' );
3941 $this->editConflictHelperFactory = $factory;
3942 }
3943
3944 private function getEditConflictHelper(): TextConflictHelper {
3945 if ( !$this->editConflictHelper ) {
3946 $label = $this->pageEditingHelper->getSubmitButtonLabel( $this->page );
3947 if ( $this->editConflictHelperFactory ) {
3948 $this->editConflictHelper = ( $this->editConflictHelperFactory )( $label );
3949 } else {
3950 $this->editConflictHelper = new TextConflictHelper(
3951 $this->getTitle(),
3952 $this->getContext()->getOutput(),
3953 MediaWikiServices::getInstance()->getStatsFactory(),
3954 $label,
3955 MediaWikiServices::getInstance()->getContentHandlerFactory()
3956 );
3957 }
3958 }
3959 return $this->editConflictHelper;
3960 }
3961}
const CONTENT_MODEL_VUE
Definition Defines.php:241
const NS_USER
Definition Defines.php:53
const CONTENT_MODEL_CSS
Definition Defines.php:237
const NS_MAIN
Definition Defines.php:51
const CONTENT_MODEL_JSON
Definition Defines.php:239
const NS_USER_TALK
Definition Defines.php:54
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:131
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:302
showTextbox(string $text, string $name, array $customAttribs=[])
setEditConflictHelperFactory(callable $factory)
showStandardInputs(int &$tabindex=2)
__construct(Article $article)
Definition EditPage.php:417
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:294
edit()
This is the function that gets called for "action=edit".
Definition EditPage.php:527
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:343
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:258
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:269
static getCopyrightWarning(PageReference $page, string $format, MessageLocalizer $localizer)
Get the copyright warning.
attemptSave(&$resultDetails=false)
Attempt submission.
static getEditToolbar()
Allow extensions to provide a toolbar.
string $editFormPageTop
Before even the preview.
Definition EditPage.php:326
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:137
getCheckboxesDefinition( $values)
Return an array of field definitions.
getEditButtons(&$tabindex)
Returns an array of html code of the following buttons: save, diff and preview.
importContentFormData(&$request)
Subpage overridable method for extracting the page content data from the posted form to be placed in ...
const EDITFORM_ID
HTML id and name for the beginning of the edit form.
Definition EditPage.php:142
const POST_EDIT_COOKIE_DURATION
Duration of PostEdit cookie, in seconds.
Definition EditPage.php:166
bool $firsttime
True the first time the edit form is rendered, false after re-rendering with diff,...
Definition EditPage.php:195
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?
Definition EditPage.php:979
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:148
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:512
bool $isConflict
Whether an edit conflict needs to be resolved.
Definition EditPage.php:183
maybeActivateTempUserCreate( $doAcquire)
Check the configuration and current user and enable automatic temporary user creation if possible.
Definition EditPage.php:706
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.
getParser()
Get the main Parser instance.
getLinkRenderer()
LinkRenderer instance that can be used if no custom options are needed.
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:67
getPage()
Get the WikiPage object of this instance.
Definition Article.php:260
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.
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form,...
Page revision base class.
getUser( $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:2219
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:130
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.
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, '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'=> '', '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'=>[], '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'=> false, '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 -o $output $input', 'ImagickExt'=>['SvgHandler::rasterizeImagickExt',],], 'SVGConverter'=> 'ImageMagick', 'SVGConverterPath'=> '', 'SVGMaxSize'=> 5120, 'SVGMetadataCutoff'=> 5242880, 'SVGNativeRendering'=> true, 'SVGNativeRenderingSizeLimit'=> 51200, 'MediaInTargetLanguage'=> true, 'MaxImageArea'=> 12500000, 'MaxAnimatedGifArea'=> 12500000, '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'=>[], '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, 'WarmParsoidParserCache'=> false,], 'ParsoidSelectiveUpdateSampleRate'=> 0, '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, 'PHPSessionHandling'=> 'warn', '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',],],], '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, ], 'AllowSecuritySensitiveOperationIfCannotReauthenticate' => [ 'default' => true, ], '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' => [ ], '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', 'editviewmywatchlist' => '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', 'editviewmywatchlist' => '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', ], 'EnableBotPasswords' => true, 'BotPasswordsCluster' => false, 'BotPasswordsDatabase' => false, 'BotPasswordsLimit' => 100, 'SecretKey' => false, 'JwtPrivateKey' => false, 'JwtPublicKey' => false, 'AllowUserJs' => false, '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, 'CachePrefix' => 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, ], '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' => 'MediaWiki\\Watchlist\\WatchlistExpiryJob', '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', 'parsoidCachePrewarm' => [ 'class' => 'MediaWiki\\JobQueue\\Jobs\\ParsoidCachePrewarmJob', 'services' => [ 'ParserOutputAccess', 'PageStore', 'RevisionLookup', 'ParsoidSiteConfig', ], 'needsPage' => false, ], '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' => [ ], 'RestSandboxSpecs' => [ ], 'RestModuleOverrides' => [ ], '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, 'AsyncHTTPTimeout' => 25, 'HTTPProxy' => '', 'LocalVirtualHosts' => [ ], 'LocalHTTPProxy' => false, 'AllowExternalReqID' => false, 'GenerateReqIDFormat' => 'rand24', 'JobRunRate' => 1, 'RunJobsAsync' => false, 'UpdateRowsPerJob' => 300, 'UpdateRowsPerQuery' => 100, 'RedirectOnLogin' => null, 'VirtualRestConfig' => [ 'paths' => [ ], 'modules' => [ ], 'global' => [ 'timeout' => 360, 'forwardCookies' => false, 'HTTPProxy' => 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' => [ ], 'CloneArticleParserOutput' => true, 'UseLeximorph' => false, 'UsePostprocCacheLegacy' => false, 'UsePostprocCacheParsoid' => true, 'ParserOptionsLogUnsafeSampleRate' => 0, ], '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', 'CacheSharedUploads' => 'boolean', 'ForeignUploadTargets' => 'array', 'UploadDialog' => 'object', 'FileBackends' => 'object', 'LockManagers' => 'array', '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', ], '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', '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', 'ParserCacheFilterConfig' => 'object', 'ChronologyProtectorSecret' => 'string', 'PHPSessionHandling' => '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', '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', 'AllowSecuritySensitiveOperationIfCannotReauthenticate' => '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', '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', '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', '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', 'RestSandboxSpecs' => 'object', 'RestModuleOverrides' => 'object', 'ShellRestrictionMethod' => [ 'string', 'boolean', ], 'ShellboxUrls' => 'object', 'ShellboxSecretKey' => [ 'string', 'null', ], 'ShellboxShell' => [ 'string', 'null', ], 'HTTPTimeout' => 'number', 'HTTPConnectTimeout' => 'number', 'HTTPMaxTimeout' => 'number', 'HTTPMaxConnectTimeout' => 'number', 'LocalVirtualHosts' => 'object', 'LocalHTTPProxy' => [ 'string', 'boolean', ], 'GenerateReqIDFormat' => 'string', 'VirtualRestConfig' => 'object', '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', 'CloneArticleParserOutput' => 'boolean', 'UseLeximorph' => 'boolean', 'UsePostprocCacheLegacy' => 'boolean', 'UsePostprocCacheParsoid' => 'boolean', 'ParserOptionsLogUnsafeSampleRate' => 'integer', ], 'mergeStrategy' => [ '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', 'VirtualRestConfig' => 'array_plus_2d', ], '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', ], ], ], ], ], 'PHPSessionHandling' => [ 'deprecated' => 'since 1.45 Integration with PHP session handling will be removed in the future', ], 'RawHtmlMessages' => [ 'items' => [ 'type' => 'string', ], ], 'InterwikiLogoOverride' => [ 'items' => [ 'type' => 'string', ], ], 'LegalTitleChars' => [ 'deprecated' => 'since 1.41; use Extension:TitleBlacklist to customize', ], 'ReauthenticateTime' => [ 'additionalProperties' => [ 'type' => 'integer', ], ], 'AllowSecuritySensitiveOperationIfCannotReauthenticate' => [ 'additionalProperties' => [ 'type' => 'boolean', ], ], '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', ], ], ], 'SoftwareTags' => [ 'additionalProperties' => [ 'type' => 'boolean', ], ], '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', ], ], 'RestSandboxSpecs' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'url' => [ 'type' => 'string', 'format' => 'url', ], 'name' => [ 'type' => 'string', ], 'file' => [ 'type' => 'string', ], 'msg' => [ 'type' => 'string', 'description' => 'a message key', ], ], ], ], 'RestModuleOverrides' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'mode' => [ 'type' => 'string', ], ], 'required' => [ 'mode', ], ], ], '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.
msg( $key,... $params)