MediaWiki master
EditPage.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\EditPage;
8
9use BadMethodCallException;
21use MediaWiki\Debug\DeprecationHelper;
48use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
96use OOUI;
97use OOUI\ButtonWidget;
98use OOUI\CheckboxInputWidget;
99use OOUI\DropdownInputWidget;
100use OOUI\FieldLayout;
101use RuntimeException;
102use StatusValue;
103use Wikimedia\Assert\Assert;
109use Wikimedia\Timestamp\ConvertibleTimestamp;
110use Wikimedia\Timestamp\TimestampFormat as TS;
111
134#[\AllowDynamicProperties]
135class EditPage implements IEditObject {
136 use DeprecationHelper;
137 use ProtectedHookAccessorTrait;
138
143
147 public const EDITFORM_ID = 'editform';
148
153 public const POST_EDIT_COOKIE_KEY_PREFIX = 'PostEditRevision';
154
171 public const POST_EDIT_COOKIE_DURATION = 1200;
172
176 private $mArticle;
177
179 private $page;
180
182 private $mContextTitle = null;
183
188 private $action = 'submit';
189
194 public bool $isConflict = false;
195
197 private $isNew = false;
198
200 public $formtype;
201
207
209 private $mTokenOk = false;
210
212 private $mTriedSave = false;
213
215 private $incompleteForm = false;
216
218 private $missingSummary = false;
219
220 private bool $allowBlankSummary = false;
221
223 protected $blankArticle = false;
224
225 private bool $allowBlankArticle = false;
226
228 private $problematicRedirectTarget = null;
229
231 private $allowedProblematicRedirectTarget = null;
232
233 private bool $ignoreProblematicRedirects = false;
234
235 private string $autoSumm = '';
236
238 private $hookError = '';
239
241 private $mParserOutput;
242
244 public $mShowSummaryField = true;
245
246 # Form values
247
249 public $save = false;
250
252 public $preview = false;
253
255 private $diff = false;
256
257 private bool $minoredit = false;
258
259 private bool $watchthis = false;
260
262 private $watchlistExpiryEnabled;
263
264 private WatchedItemStoreInterface $watchedItemStore;
265
267 private ?string $watchlistExpiry = null;
268
269 private bool $recreate = false;
270
272 private $ignoreRevisionDeletedWarning = false;
273
277 public string $textbox1 = '';
278
283 private $textbox2 = '';
284
285 public string $summary = '';
286
291 private $nosummary = false;
292
297 public ?string $edittime = '';
298
311 private ?int $editRevId = null;
312
313 public string $section = '';
314
315 public ?string $sectiontitle = null;
316
317 private ?string $newSectionAnchor = null;
318
322 public ?string $starttime = '';
323
330 public int $oldid = 0;
331
338 private int $parentRevId = 0;
339
341 private $scrolltop = null;
342
343 private bool $markAsBot = true;
344
345 public string $contentModel;
346
347 public ?string $contentFormat = null;
348
349 private ?array $changeTags = null;
350
351 # Placeholders for text injection by hooks (must be HTML)
352 # extensions should take care to _append_ to the present value
353
355 public $editFormPageTop = '';
357 public $editFormTextTop = '';
370
372 public $didSave = false;
373 public int $undidRev = 0;
374 private int $undoAfter = 0;
375
377 public $suppressIntro = false;
378
379 private int|false $contentLength = false;
380
384 private bool $enableApiEditOverride = false;
385
387
391 private $isOldRev = false;
392
396 private ?string $unicodeCheck = null;
397
399 private $editConflictHelperFactory = null;
400 private ?TextConflictHelper $editConflictHelper = null;
401
402 private IContentHandlerFactory $contentHandlerFactory;
403 private PermissionManager $permManager;
404 private RevisionStore $revisionStore;
405 private WatchlistManager $watchlistManager;
406 private UserOptionsLookup $userOptionsLookup;
407 private TempUserCreator $tempUserCreator;
408 private UserFactory $userFactory;
409 private IConnectionProvider $dbProvider;
410 private AuthManager $authManager;
411 private UserRegistrationLookup $userRegistrationLookup;
412 private SessionManager $sessionManager;
413 private EditConstraintFactory $constraintFactory;
414 private PageEditingHelper $pageEditingHelper;
415
417 private $placeholderTempUser;
418
420 private $unsavedTempUser;
421
423 private $savedTempUser;
424
426 private bool $tempUserCreateActive = false;
427
429 private $tempUserName;
430
432 private $tempUserCreateDone = false;
433
435 private bool $unableToAcquireTempName = false;
436
437 private LinkRenderer $linkRenderer;
438 private LinkBatchFactory $linkBatchFactory;
439 private RestrictionStore $restrictionStore;
440
445 public function __construct( Article $article ) {
446 $this->mArticle = $article;
447 $this->page = $article->getPage(); // model object
448
449 // Make sure the local context is in sync with other member variables.
450 // Particularly make sure everything is using the same WikiPage instance.
451 // This should probably be the case in Article as well, but it's
452 // particularly important for EditPage, to make use of the in-place caching
453 // facility in WikiPage::prepareContentForEdit.
454 $this->context = new DerivativeContext( $article->getContext() );
455 $this->context->setWikiPage( $this->page );
456
457 $this->contentModel = $this->getTitle()->getContentModel();
458
459 $services = MediaWikiServices::getInstance();
460 $this->contentHandlerFactory = $services->getContentHandlerFactory();
461 $this->contentFormat = $this->contentHandlerFactory
462 ->getContentHandler( $this->contentModel )
463 ->getDefaultFormat();
464 $this->permManager = $services->getPermissionManager();
465 $this->revisionStore = $services->getRevisionStore();
466 $this->watchlistExpiryEnabled = $this->getContext()->getConfig() instanceof Config
467 && $this->getContext()->getConfig()->get( MainConfigNames::WatchlistExpiry );
468 $this->watchedItemStore = $services->getWatchedItemStore();
469 $this->watchlistManager = $services->getWatchlistManager();
470 $this->userOptionsLookup = $services->getUserOptionsLookup();
471 $this->tempUserCreator = $services->getTempUserCreator();
472 $this->userFactory = $services->getUserFactory();
473 $this->linkRenderer = $services->getLinkRenderer();
474 $this->linkBatchFactory = $services->getLinkBatchFactory();
475 $this->restrictionStore = $services->getRestrictionStore();
476 $this->dbProvider = $services->getConnectionProvider();
477 $this->authManager = $services->getAuthManager();
478 $this->userRegistrationLookup = $services->getUserRegistrationLookup();
479 $this->sessionManager = $services->getSessionManager();
480 $this->constraintFactory = $services->getService( '_EditConstraintFactory' );
481 $this->pageEditingHelper = $services->getService( '_PageEditingHelper' );
482
483 $this->deprecatePublicProperty( 'textbox2', '1.44', __CLASS__ );
484 $this->deprecatePublicProperty( 'action', '1.38', __CLASS__ );
485 }
486
490 public function getArticle() {
491 return $this->mArticle;
492 }
493
498 public function getContext() {
499 return $this->context;
500 }
501
506 public function getTitle() {
507 return $this->page->getTitle();
508 }
509
513 public function setContextTitle( $title ) {
514 $this->mContextTitle = $title;
515 }
516
521 public function getContextTitle() {
522 if ( $this->mContextTitle === null ) {
523 throw new RuntimeException( "EditPage does not have a context title set" );
524 } else {
525 return $this->mContextTitle;
526 }
527 }
528
534 public function setApiEditOverride( bool $enableOverride ): void {
535 $this->enableApiEditOverride = $enableOverride;
536 }
537
549 public function edit() {
550 // Allow extensions to modify/prevent this form or submission
551 if ( !$this->getHookRunner()->onAlternateEdit( $this ) ) {
552 return;
553 }
554
555 wfDebug( __METHOD__ . ": enter" );
556
557 $request = $this->context->getRequest();
558 // If they used redlink=1 and the page exists, redirect to the main article
559 if ( $request->getBool( 'redlink' ) && $this->page->exists() ) {
560 $this->context->getOutput()->redirect( $this->getTitle()->getFullURL() );
561 return;
562 }
563
564 $this->importFormData( $request );
565 $this->firsttime = false;
566
567 $readOnlyMode = MediaWikiServices::getInstance()->getReadOnlyMode();
568 if ( $this->save && $readOnlyMode->isReadOnly() ) {
569 // Force preview
570 $this->save = false;
571 $this->preview = true;
572 }
573
574 if ( $this->save ) {
575 $this->formtype = 'save';
576 } elseif ( $this->preview ) {
577 $this->formtype = 'preview';
578 } elseif ( $this->diff ) {
579 $this->formtype = 'diff';
580 } else { # First time through
581 $this->firsttime = true;
582 if ( $this->previewOnOpen() ) {
583 $this->formtype = 'preview';
584 } else {
585 $this->formtype = 'initial';
586 }
587 }
588
589 // Check permissions after possibly creating a placeholder temp user.
590 // This allows anonymous users to edit via a temporary account, if the site is
591 // configured to (1) disallow anonymous editing and (2) autocreate temporary
592 // accounts on edit.
593 $this->unableToAcquireTempName = !$this->maybeActivateTempUserCreate( !$this->firsttime )->isOK();
594
595 $status = $this->getEditPermissionStatus(
596 $this->save ? PermissionManager::RIGOR_SECURE : PermissionManager::RIGOR_FULL
597 );
598 if ( !$status->isGood() ) {
599 wfDebug( __METHOD__ . ": User can't edit" );
600
601 $user = $this->context->getUser();
602 if ( $user->getBlock() && !$readOnlyMode->isReadOnly() ) {
603 // Auto-block user's IP if the account was "hard" blocked
604 $user->scheduleSpreadBlock();
605 }
606 $this->displayPermissionStatus( $status );
607
608 return;
609 }
610
611 $revRecord = $this->mArticle->fetchRevisionRecord();
612 // Disallow editing revisions with content models different from the current one
613 // Undo edits being an exception in order to allow reverting content model changes.
614 $revContentModel = $revRecord ?
615 $revRecord->getMainContentModel() :
616 false;
617 if ( $revContentModel && $revContentModel !== $this->contentModel ) {
618 $prevRevRecord = null;
619 $prevContentModel = false;
620 if ( $this->undidRev ) {
621 $undidRevRecord = $this->revisionStore
622 ->getRevisionById( $this->undidRev );
623 $prevRevRecord = $undidRevRecord ?
624 $this->revisionStore->getPreviousRevision( $undidRevRecord ) :
625 null;
626
627 $prevContentModel = $prevRevRecord ?
628 $prevRevRecord->getMainContentModel() :
629 '';
630 }
631
632 if ( !$this->undidRev
633 || !$prevRevRecord
634 || $prevContentModel !== $this->contentModel
635 ) {
636 $this->displayViewSourcePage(
637 $this->getContentObject(),
638 $this->context->msg(
639 'contentmodelediterror',
640 $revContentModel,
641 $this->contentModel
642 )->plain()
643 );
644 return;
645 }
646 }
647
648 $this->isConflict = false;
649
650 # Attempt submission here. This will check for edit conflicts,
651 # and redundantly check for locked database, blocked IPs, etc.
652 # that edit() already checked just in case someone tries to sneak
653 # in the back door with a hand-edited submission URL.
654
655 if ( $this->formtype === 'save' ) {
656 $resultDetails = null;
657 $status = $this->attemptSave( $resultDetails );
658 if ( !( $status instanceof EditPageStatus ) ) {
659 // Hooks and subclasses can cause attemptSave to return a normal Status, so cast it if necessary
660 $status = EditPageStatus::cast( $status );
661 }
662 if ( !$this->handleStatus( $status, $resultDetails ) ) {
663 return;
664 }
665 }
666
667 # First time through: get contents, set time for conflict
668 # checking, etc.
669 if ( $this->formtype === 'initial' || $this->firsttime ) {
670 if ( !$this->initialiseForm() ) {
671 return;
672 }
673
674 if ( $this->page->getId() ) {
675 $this->getHookRunner()->onEditFormInitialText( $this );
676 }
677 }
678
679 // If we're displaying an old revision, and there are differences between it and the
680 // current revision outside the main slot, then we can't allow the old revision to be
681 // editable, as what would happen to the non-main-slot data if someone saves the old
682 // revision is undefined.
683 // When this is the case, display a read-only version of the page instead, with a link
684 // to a diff page from which the old revision can be restored
685 $curRevisionRecord = $this->page->getRevisionRecord();
686 if ( $curRevisionRecord
687 && $revRecord
688 && $curRevisionRecord->getId() !== $revRecord->getId()
689 && ( WikiPage::hasDifferencesOutsideMainSlot(
690 $revRecord,
691 $curRevisionRecord
692 ) || !$this->pageEditingHelper->isSupportedContentModel(
693 $revRecord->getSlot(
694 SlotRecord::MAIN,
695 RevisionRecord::RAW
696 )->getModel(),
697 $this->enableApiEditOverride
698 ) )
699 ) {
700 $restoreLink = $this->getTitle()->getFullURL(
701 [
702 'action' => 'mcrrestore',
703 'restore' => $revRecord->getId(),
704 ]
705 );
706 $this->displayViewSourcePage(
707 $this->getContentObject(),
708 $this->context->msg(
709 'nonmain-slot-differences-therefore-readonly',
710 $restoreLink
711 )->plain()
712 );
713 return;
714 }
715
716 $this->showEditForm();
717 }
718
728 public function maybeActivateTempUserCreate( $doAcquire ): Status {
729 if ( $this->tempUserCreateActive ) {
730 // Already done
731 return Status::newGood();
732 }
733 $user = $this->context->getUser();
734
735 // Log out any user using an expired temporary account, so that we can give them a new temporary account.
736 // As described in T389485, we need to do this because the maintenance script to expire temporary accounts
737 // may fail to run or not be configured to run.
738 if ( $user->isTemp() ) {
739 $expiryAfterDays = $this->tempUserCreator->getExpireAfterDays();
740 if ( $expiryAfterDays ) {
741 $expirationCutoff = (int)ConvertibleTimestamp::now( TS::UNIX ) - ( 86_400 * $expiryAfterDays );
742
743 // If the user was created before the expiration cutoff, then log them out, expire any other existing
744 // sessions, and revoke any access to the account that may exist.
745 // If no registration is set then do nothing, as if registration date system is broken it would
746 // cause a new temporary account for each edit.
747 $firstUserRegistration = $this->userRegistrationLookup->getFirstRegistration( $user );
748 if (
749 $firstUserRegistration &&
750 ConvertibleTimestamp::convert( TS::UNIX, $firstUserRegistration ) < $expirationCutoff
751 ) {
752 // Log the user out of the expired temporary account.
753 $user->logout();
754
755 // Clear any stashed temporary account name (if any is set), as we want a new name for the user.
756 $session = $this->context->getRequest()->getSession();
757 $session->set( 'TempUser:name', null );
758 $session->save();
759
760 // Invalidate any sessions for the expired temporary account
761 $this->sessionManager->invalidateSessionsForUser(
762 $this->userFactory->newFromUserIdentity( $user )
763 );
764 }
765 }
766 }
767
768 if ( $this->tempUserCreator->shouldAutoCreate( $user, 'edit' ) ) {
769 if ( $doAcquire ) {
770 $name = $this->tempUserCreator->acquireAndStashName(
771 $this->context->getRequest()->getSession() );
772 if ( $name === null ) {
773 $status = Status::newFatal( 'temp-user-unable-to-acquire' );
774 $status->value = self::AS_UNABLE_TO_ACQUIRE_TEMP_ACCOUNT;
775 return $status;
776 }
777 $this->unsavedTempUser = $this->userFactory->newUnsavedTempUser( $name );
778 $this->tempUserName = $name;
779 } else {
780 $this->placeholderTempUser = $this->userFactory->newTempPlaceholder();
781 }
782 $this->tempUserCreateActive = true;
783 }
784 return Status::newGood();
785 }
786
794 private function createTempUser(): Status {
795 if ( !$this->tempUserCreateActive ) {
796 return Status::newGood();
797 }
798 $request = $this->context->getRequest();
799 $status = $this->tempUserCreator->create(
800 $this->tempUserName,
801 $request
802 );
803 if ( $status->isOK() ) {
804 $this->placeholderTempUser = null;
805 $this->unsavedTempUser = null;
806 $this->savedTempUser = $status->getUser();
807 $this->authManager->setRequestContextUserFromSessionUser();
808 $this->tempUserCreateDone = true;
809 }
810 LoggerFactory::getInstance( 'authevents' )->info(
811 'Temporary account creation attempt: {user}',
812 [
813 'user' => $this->tempUserName,
814 'success' => $status->isOK(),
815 ] + $request->getSecurityLogContext( $status->isOK() ? $status->getUser() : null )
816 );
817 return $status;
818 }
819
829 private function getAuthority(): Authority {
830 return $this->getUserForPermissions();
831 }
832
839 private function getUserForPermissions() {
840 if ( $this->savedTempUser ) {
841 return $this->savedTempUser;
842 } elseif ( $this->unsavedTempUser ) {
843 return $this->unsavedTempUser;
844 } elseif ( $this->placeholderTempUser ) {
845 return $this->placeholderTempUser;
846 } else {
847 return $this->context->getUser();
848 }
849 }
850
857 private function getUserForPreview() {
858 if ( $this->savedTempUser ) {
859 return $this->savedTempUser;
860 } elseif ( $this->unsavedTempUser ) {
861 return $this->unsavedTempUser;
862 } elseif ( $this->firsttime && $this->placeholderTempUser ) {
863 // Mostly a GET request and no temp user was aquired,
864 // but needed for pst or content transform for preview,
865 // fallback to a placeholder for this situation (T330943)
866 return $this->placeholderTempUser;
867 } elseif ( $this->tempUserCreateActive ) {
868 throw new BadMethodCallException(
869 "Can't use the request user for preview with IP masking enabled" );
870 } else {
871 return $this->context->getUser();
872 }
873 }
874
881 private function getUserForSave() {
882 if ( $this->savedTempUser ) {
883 return $this->savedTempUser;
884 } elseif ( $this->tempUserCreateActive ) {
885 throw new BadMethodCallException(
886 "Can't use the request user for storage with IP masking enabled" );
887 } else {
888 return $this->context->getUser();
889 }
890 }
891
896 private function getEditPermissionStatus( string $rigor = PermissionManager::RIGOR_SECURE ): PermissionStatus {
897 $user = $this->getUserForPermissions();
898 return $this->permManager->getPermissionStatus(
899 'edit',
900 $user,
901 $this->getTitle(),
902 $rigor
903 );
904 }
905
917 private function displayPermissionStatus( PermissionStatus $status ): void {
918 $out = $this->context->getOutput();
919 if ( $this->context->getRequest()->getBool( 'redlink' ) ) {
920 // The edit page was reached via a red link.
921 // Redirect to the article page and let them click the edit tab if
922 // they really want a permission error.
923 $out->redirect( $this->getTitle()->getFullURL() );
924 return;
925 }
926
927 $content = $this->getContentObject();
928
929 // Use the normal message if there's nothing to display:
930 // page or section does not exist (T249978), and the user isn't in the middle of an edit
931 if ( !$content || ( $this->firsttime && !$this->page->exists() && $content->isEmpty() ) ) {
932 $action = $this->page->exists() ? 'edit' :
933 ( $this->getTitle()->isTalkPage() ? 'createtalk' : 'createpage' );
934 throw new PermissionsError( $action, $status );
935 }
936
937 $this->displayViewSourcePage(
938 $content,
939 $out->formatPermissionStatus( $status, 'edit' )
940 );
941 }
942
948 private function displayViewSourcePage( Content $content, string $errorMessage ): void {
949 $out = $this->context->getOutput();
950 $this->getHookRunner()->onEditPage__showReadOnlyForm_initial( $this, $out );
951
952 $out->setRobotPolicy( 'noindex,nofollow' );
953 $out->setPageTitleMsg( $this->context->msg(
954 'viewsource-title'
955 )->plaintextParams(
956 $this->getContextTitle()->getPrefixedText()
957 ) );
958 $out->addBacklinkSubtitle( $this->getContextTitle() );
959 $out->addHTML( $this->editFormPageTop );
960 $out->addHTML( $this->editFormTextTop );
961
962 if ( $errorMessage !== '' ) {
963 $out->addWikiTextAsInterface( $errorMessage );
964 $out->addHTML( "<hr />\n" );
965 }
966
967 # If the user made changes, preserve them when showing the markup
968 # (This happens when a user is blocked during edit, for instance)
969 if ( !$this->firsttime ) {
970 $text = $this->textbox1;
971 $out->addWikiMsg( 'viewyourtext' );
972 } else {
973 // Convert the content to editable text, or serialize using the default format if the content model is not
974 // supported (e.g. for an old revision with a different model)
975 $text = $this->pageEditingHelper->toEditText(
976 $content, $this->contentFormat, $this->enableApiEditOverride
977 ) ?? $content->serialize();
978 $out->addWikiMsg( 'viewsourcetext' );
979 }
980
981 $out->addHTML( $this->editFormTextBeforeContent );
982 $this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] );
983 $out->addHTML( $this->editFormTextAfterContent );
984
985 $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
986
987 $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
988
989 $out->addHTML( $this->editFormTextBottom );
990 if ( $this->page->exists() ) {
991 $out->returnToMain( null, $this->page );
992 }
993 }
994
1000 protected function previewOnOpen() {
1001 $config = $this->context->getConfig();
1002 $previewOnOpenNamespaces = $config->get( MainConfigNames::PreviewOnOpenNamespaces );
1003 $request = $this->context->getRequest();
1004 if ( $config->get( MainConfigNames::RawHtml ) ) {
1005 // If raw HTML is enabled, disable preview on open
1006 // since it has to be posted with a token for
1007 // security reasons
1008 return false;
1009 }
1010 $preview = $request->getRawVal( 'preview' );
1011 if ( $preview === 'yes' ) {
1012 // Explicit override from request
1013 return true;
1014 } elseif ( $preview === 'no' ) {
1015 // Explicit override from request
1016 return false;
1017 } elseif ( $this->section === 'new' ) {
1018 // Nothing *to* preview for new sections
1019 return false;
1020 } elseif ( ( $request->getCheck( 'preload' ) || $this->page->exists() )
1021 && $this->userOptionsLookup->getOption( $this->context->getUser(), 'previewonfirst' )
1022 ) {
1023 // Standard preference behavior
1024 return true;
1025 } elseif ( !$this->page->exists()
1026 && isset( $previewOnOpenNamespaces[$this->page->getNamespace()] )
1027 && $previewOnOpenNamespaces[$this->page->getNamespace()]
1028 ) {
1029 // Categories are special
1030 return true;
1031 } else {
1032 return false;
1033 }
1034 }
1035
1042 private function isSectionEditSupported(): bool {
1043 $currentRev = $this->page->getRevisionRecord();
1044
1045 // $currentRev is null for non-existing pages, use the page default content model.
1046 $revContentModel = $currentRev
1047 ? $currentRev->getMainContentModel()
1048 : $this->page->getContentModel();
1049
1050 return (
1051 ( $this->mArticle->getRevIdFetched() === $this->page->getLatest() ) &&
1052 $this->contentHandlerFactory->getContentHandler( $revContentModel )->supportsSections()
1053 );
1054 }
1055
1061 public function importFormData( &$request ) {
1062 # Section edit can come from either the form or a link
1063 $this->section = $request->getVal( 'wpSection', $request->getVal( 'section', '' ) );
1064
1065 if ( $this->section !== null && $this->section !== '' && !$this->isSectionEditSupported() ) {
1066 throw new ErrorPageError( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
1067 }
1068
1069 $this->isNew = !$this->page->exists() || $this->section === 'new';
1070
1071 if ( $request->wasPosted() ) {
1072 $this->importFormDataPosted( $request );
1073 } else {
1074 # Not a posted form? Start with nothing.
1075 wfDebug( __METHOD__ . ": Not a posted form." );
1076 $this->textbox1 = '';
1077 $this->summary = '';
1078 $this->sectiontitle = null;
1079 $this->edittime = '';
1080 $this->editRevId = null;
1081 $this->starttime = wfTimestampNow();
1082 $this->preview = false;
1083 $this->save = false;
1084 $this->diff = false;
1085 $this->minoredit = false;
1086 // Watch may be overridden by request parameters
1087 $this->watchthis = $request->getBool( 'watchthis', false );
1088 if ( $this->watchlistExpiryEnabled ) {
1089 $this->watchlistExpiry = null;
1090 }
1091 $this->recreate = false;
1092
1093 // When creating a new section, we can preload a section title by passing it as the
1094 // preloadtitle parameter in the URL (T15100)
1095 if ( $this->section === 'new' && $request->getCheck( 'preloadtitle' ) ) {
1096 $this->sectiontitle = $request->getVal( 'preloadtitle' );
1097 $this->setNewSectionSummary();
1098 } elseif ( $this->section !== 'new' && $request->getRawVal( 'summary' ) !== '' ) {
1099 $this->summary = $request->getText( 'summary' );
1100 if ( $this->summary !== '' ) {
1101 // If a summary has been preset using &summary= we don't want to prompt for
1102 // a different summary. Only prompt for a summary if the summary is blanked.
1103 // (T19416)
1104 $this->autoSumm = md5( '' );
1105 }
1106 }
1107
1108 if ( $request->getVal( 'minor' ) ) {
1109 $this->minoredit = true;
1110 }
1111 }
1112
1113 $this->oldid = $request->getInt( 'oldid' );
1114 $this->parentRevId = $request->getInt( 'parentRevId' );
1115
1116 $this->markAsBot = $request->getBool( 'bot', true );
1117 $this->nosummary = $request->getBool( 'nosummary' );
1118
1119 // May be overridden by revision.
1120 $this->contentModel = $request->getText( 'model', $this->contentModel );
1121 // May be overridden by revision.
1122 $this->contentFormat = $request->getText( 'format', $this->contentFormat );
1123
1124 try {
1125 $handler = $this->contentHandlerFactory->getContentHandler( $this->contentModel );
1126 } catch ( MWUnknownContentModelException ) {
1127 throw new ErrorPageError(
1128 'editpage-invalidcontentmodel-title',
1129 'editpage-invalidcontentmodel-text',
1130 [ wfEscapeWikiText( $this->contentModel ) ]
1131 );
1132 }
1133
1134 if ( !$handler->isSupportedFormat( $this->contentFormat ) ) {
1135 throw new ErrorPageError(
1136 'editpage-notsupportedcontentformat-title',
1137 'editpage-notsupportedcontentformat-text',
1138 [
1139 wfEscapeWikiText( $this->contentFormat ),
1140 wfEscapeWikiText( ContentHandler::getLocalizedName( $this->contentModel ) )
1141 ]
1142 );
1143 }
1144
1145 // Allow extensions to modify form data
1146 $this->getHookRunner()->onEditPage__importFormData( $this, $request );
1147 }
1148
1149 private function importFormDataPosted( WebRequest $request ): void {
1150 # These fields need to be checked for encoding.
1151 # Also remove trailing whitespace, but don't remove _initial_
1152 # whitespace from the text boxes. This may be significant formatting.
1153 $this->textbox1 = rtrim( $request->getText( 'wpTextbox1' ) );
1154 if ( !$request->getCheck( 'wpTextbox2' ) ) {
1155 // Skip this if wpTextbox2 has input, it indicates that we came
1156 // from a conflict page with raw page text, not a custom form
1157 // modified by subclasses
1158 $textbox1 = $this->importContentFormData( $request );
1159 if ( $textbox1 !== null ) {
1160 $this->textbox1 = $textbox1;
1161 }
1162 }
1163
1164 $this->unicodeCheck = $request->getText( 'wpUnicodeCheck' );
1165
1166 if ( $this->section === 'new' ) {
1167 # Allow setting sectiontitle different from the edit summary.
1168 # Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is
1169 # currently doing double duty as both edit summary and section title. Right now this
1170 # is just to allow API edits to work around this limitation, but this should be
1171 # incorporated into the actual edit form when EditPage is rewritten (T20654, T28312).
1172 if ( $request->getCheck( 'wpSectionTitle' ) ) {
1173 $this->sectiontitle = $request->getText( 'wpSectionTitle' );
1174 if ( $request->getCheck( 'wpSummary' ) ) {
1175 $this->summary = $request->getText( 'wpSummary' );
1176 }
1177 } else {
1178 $this->sectiontitle = $request->getText( 'wpSummary' );
1179 }
1180 } else {
1181 $this->sectiontitle = null;
1182 $this->summary = $request->getText( 'wpSummary' );
1183 }
1184
1185 # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the
1186 # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for
1187 # section titles. (T3600)
1188 # It is weird to modify 'sectiontitle', even when it is provided when using the API, but API
1189 # users have come to rely on it: https://github.com/wikimedia-gadgets/twinkle/issues/1625
1190 $this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary );
1191 if ( $this->sectiontitle !== null ) {
1192 $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
1193 }
1194
1195 if ( $this->section === 'new' ) {
1196 $this->setNewSectionSummary();
1197 }
1198
1199 $this->edittime = $request->getVal( 'wpEdittime' );
1200 $this->editRevId = $request->getIntOrNull( 'editRevId' );
1201 $this->starttime = $request->getVal( 'wpStarttime' );
1202
1203 $undidRev = $request->getInt( 'wpUndidRevision' );
1204 if ( $undidRev ) {
1205 $this->undidRev = $undidRev;
1206 }
1207 $undoAfter = $request->getInt( 'wpUndoAfter' );
1208 if ( $undoAfter ) {
1209 $this->undoAfter = $undoAfter;
1210 }
1211
1212 $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' );
1213
1214 if ( $this->textbox1 === '' && !$request->getCheck( 'wpTextbox1' ) ) {
1215 // wpTextbox1 field is missing, possibly due to being "too big"
1216 // according to some filter rules that may have been configured
1217 // for security reasons.
1218 $this->incompleteForm = true;
1219 } else {
1220 // If we receive the last parameter of the request, we can fairly
1221 // claim the POST request has not been truncated.
1222 $this->incompleteForm = !$request->getVal( 'wpUltimateParam' );
1223 }
1224 if ( $this->incompleteForm ) {
1225 # If the form is incomplete, force to preview.
1226 wfDebug( __METHOD__ . ": Form data appears to be incomplete" );
1227 wfDebug( "POST DATA: " . var_export( $request->getPostValues(), true ) );
1228 $this->preview = true;
1229 } else {
1230 $this->preview = $request->getCheck( 'wpPreview' );
1231 $this->diff = $request->getCheck( 'wpDiff' );
1232
1233 // Remember whether a save was requested, so we can indicate
1234 // if we forced preview due to session failure.
1235 $this->mTriedSave = !$this->preview;
1236
1237 if ( $this->tokenOk( $request ) ) {
1238 # Some browsers will not report any submit button
1239 # if the user hits enter in the comment box.
1240 # The unmarked state will be assumed to be a save,
1241 # if the form seems otherwise complete.
1242 wfDebug( __METHOD__ . ": Passed token check." );
1243 } elseif ( $this->diff ) {
1244 # Failed token check, but only requested "Show Changes".
1245 wfDebug( __METHOD__ . ": Failed token check; Show Changes requested." );
1246 } else {
1247 # Page might be a hack attempt posted from
1248 # an external site. Preview instead of saving.
1249 wfDebug( __METHOD__ . ": Failed token check; forcing preview" );
1250 $this->preview = true;
1251 }
1252 }
1253 $this->save = !$this->preview && !$this->diff;
1254 if ( !$this->edittime || !preg_match( '/^\d{14}$/', $this->edittime ) ) {
1255 $this->edittime = null;
1256 }
1257
1258 if ( !$this->starttime || !preg_match( '/^\d{14}$/', $this->starttime ) ) {
1259 $this->starttime = null;
1260 }
1261
1262 $this->recreate = $request->getCheck( 'wpRecreate' );
1263
1264 $this->ignoreRevisionDeletedWarning = $request->getCheck( 'wpIgnoreRevisionDeleted' );
1265
1266 $user = $this->context->getUser();
1267
1268 $this->minoredit = $request->getCheck( 'wpMinoredit' );
1269 $this->watchthis = $request->getCheck( 'wpWatchthis' );
1270 $submittedExpiry = $request->getText( 'wpWatchlistExpiry' );
1271 if ( $this->watchlistExpiryEnabled && $submittedExpiry !== '' ) {
1272 // This parsing of the user-posted expiry is done for both preview and saving. This
1273 // is necessary because ApiEditPage uses preview when it saves (yuck!). Note that it
1274 // only works because the unnormalized value is retrieved again below in
1275 // getCheckboxesDefinitionForWatchlist().
1276 $submittedExpiry = ExpiryDef::normalizeExpiry( $submittedExpiry, TS::ISO_8601 );
1277 if ( $submittedExpiry !== false ) {
1278 $this->watchlistExpiry = $submittedExpiry;
1279 }
1280 }
1281
1282 # Don't force edit summaries when a user is editing their own user or talk page
1283 if ( ( $this->page->getNamespace() === NS_USER || $this->page->getNamespace() === NS_USER_TALK )
1284 && $this->getTitle()->getText() === $user->getName()
1285 ) {
1286 $this->allowBlankSummary = true;
1287 } else {
1288 $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )
1289 || !$this->userOptionsLookup->getOption( $user, 'forceeditsummary' );
1290 }
1291
1292 $this->autoSumm = $request->getText( 'wpAutoSummary' );
1293
1294 $this->allowBlankArticle = $request->getBool( 'wpIgnoreBlankArticle' );
1295 $allowedProblematicRedirectTargetText = $request->getText( 'wpAllowedProblematicRedirectTarget' );
1296 $this->allowedProblematicRedirectTarget = $allowedProblematicRedirectTargetText === ''
1297 ? null : Title::newFromText( $allowedProblematicRedirectTargetText );
1298 $this->ignoreProblematicRedirects = $request->getBool( 'wpIgnoreProblematicRedirects' );
1299
1300 $changeTags = $request->getVal( 'wpChangeTags' );
1301 $changeTagsAfterPreview = $request->getVal( 'wpChangeTagsAfterPreview' );
1302 if ( $changeTags === null || $changeTags === '' ) {
1303 $this->changeTags = [];
1304 } else {
1305 $this->changeTags = array_filter(
1306 array_map(
1307 'trim',
1308 explode( ',', $changeTags )
1309 )
1310 );
1311 }
1312 if ( $changeTagsAfterPreview !== null && $changeTagsAfterPreview !== '' ) {
1313 $this->changeTags = array_merge( $this->changeTags, array_filter(
1314 array_map(
1315 'trim',
1316 explode( ',', $changeTagsAfterPreview )
1317 )
1318 ) );
1319 }
1320 }
1321
1331 protected function importContentFormData( &$request ) {
1332 return null; // Don't do anything, EditPage already extracted wpTextbox1
1333 }
1334
1340 private function initialiseForm(): bool {
1341 $this->edittime = $this->page->getTimestamp();
1342 $this->editRevId = $this->page->getLatest();
1343
1344 $dummy = $this->contentHandlerFactory
1345 ->getContentHandler( $this->contentModel )
1346 ->makeEmptyContent();
1347 $content = $this->getContentObject( $dummy ); # TODO: track content object?!
1348 if ( $content === $dummy ) { // Invalid section
1349 $this->noSuchSectionPage();
1350 return false;
1351 }
1352
1353 if ( !$content ) {
1354 $out = $this->context->getOutput();
1355 // FIXME Why is this double-parsing?
1356 $this->editFormPageTop .= Html::errorBox(
1357 $out->parseAsInterface( $this->context->msg( 'missing-revision-content',
1358 $this->oldid,
1359 Message::plaintextParam( $this->getTitle()->getPrefixedText() )
1360 )->parse() )
1361 );
1362 } elseif ( !$this->pageEditingHelper->isSupportedContentModel(
1363 $content->getModel(), $this->enableApiEditOverride,
1364 ) ) {
1365 $modelMsg = $this->getContext()->msg( 'content-model-' . $content->getModel() );
1366 $modelName = $modelMsg->exists() ? $modelMsg->text() : $content->getModel();
1367
1368 $out = $this->context->getOutput();
1369 $out->showErrorPage(
1370 'modeleditnotsupported-title',
1371 'modeleditnotsupported-text',
1372 [ $modelName ]
1373 );
1374 return false;
1375 }
1376
1377 $this->textbox1 = $this->pageEditingHelper->toEditText(
1378 $content, $this->contentFormat, $this->enableApiEditOverride
1379 );
1380
1381 $user = $this->context->getUser();
1382 // activate checkboxes if user wants them to be always active
1383 # Sort out the "watch" checkbox
1384 if ( $this->userOptionsLookup->getOption( $user, 'watchdefault' ) ) {
1385 # Watch all edits
1386 $this->watchthis = true;
1387 } elseif ( $this->userOptionsLookup->getOption( $user, 'watchcreations' ) && !$this->page->exists() ) {
1388 # Watch creations
1389 $this->watchthis = true;
1390 } elseif ( $this->watchlistManager->isWatched( $user, $this->page ) ) {
1391 # Already watched
1392 $this->watchthis = true;
1393 }
1394 if ( $this->watchthis && $this->watchlistExpiryEnabled ) {
1395 $watchedItem = $this->watchedItemStore->getWatchedItem( $user, $this->getTitle() );
1396 $this->watchlistExpiry = $watchedItem ? $watchedItem->getExpiry() : null;
1397 }
1398 if ( !$this->isNew && $this->userOptionsLookup->getOption( $user, 'minordefault' ) ) {
1399 $this->minoredit = true;
1400 }
1401 return true;
1402 }
1403
1409 protected function getContentObject( $defaultContent = null ) {
1410 $services = MediaWikiServices::getInstance();
1411 $request = $this->context->getRequest();
1412
1413 $content = false;
1414
1415 // For non-existent articles and new sections, use preload text if any.
1416 if ( !$this->page->exists() || $this->section === 'new' ) {
1417 $content = $services->getPreloadedContentBuilder()->getPreloadedContent(
1418 $this->page,
1419 $this->context->getUser(),
1420 $request->getVal( 'preload' ),
1421 array_filter( $request->getArray( 'preloadparams', [] ), is_string( ... ) ),
1422 $request->getVal( 'section' )
1423 );
1424 // For existing pages, get text based on "undo" or section parameters.
1425 } elseif ( $this->section !== '' ) {
1426 // Get section edit text (returns $def_text for invalid sections)
1427 $orig = $this->pageEditingHelper->getOriginalContent(
1428 $this->getAuthority(),
1429 $this->mArticle,
1430 $this->contentModel,
1431 $this->section,
1432 );
1433 $content = $orig ? $orig->getSection( $this->section ) : null;
1434
1435 if ( !$content ) {
1436 $content = $defaultContent;
1437 }
1438 } else {
1439 $undoafter = $request->getInt( 'undoafter' );
1440 $undo = $request->getInt( 'undo' );
1441
1442 if ( $undo > 0 && $undoafter > 0 ) {
1443 // The use of getRevisionByTitle() is intentional, as allowing access to
1444 // arbitrary revisions on arbitrary pages bypass partial visibility restrictions (T297322).
1445 $undorev = $this->revisionStore->getRevisionByTitle( $this->page, $undo );
1446 $oldrev = $this->revisionStore->getRevisionByTitle( $this->page, $undoafter );
1447 $undoMsg = null;
1448
1449 # Make sure it's the right page,
1450 # the revisions exist and they were not deleted.
1451 # Otherwise, $content will be left as-is.
1452 if ( $undorev !== null && $oldrev !== null &&
1453 !$undorev->isDeleted( RevisionRecord::DELETED_TEXT ) &&
1454 !$oldrev->isDeleted( RevisionRecord::DELETED_TEXT )
1455 ) {
1456 if ( WikiPage::hasDifferencesOutsideMainSlot( $undorev, $oldrev )
1457 || !$this->pageEditingHelper->isSupportedContentModel(
1458 $oldrev->getMainContentModel(), $this->enableApiEditOverride
1459 )
1460 ) {
1461 // Hack for undo while EditPage can't handle multi-slot editing
1462 $this->context->getOutput()->redirect( $this->getTitle()->getFullURL( [
1463 'action' => 'mcrundo',
1464 'undo' => $undo,
1465 'undoafter' => $undoafter,
1466 ] ) );
1467 return false;
1468 } else {
1469 $content = $this->pageEditingHelper->getUndoContent( $this->page, $undorev, $oldrev, $undoMsg );
1470 }
1471
1472 if ( $undoMsg === null ) {
1473 $oldContent = $this->page->getContent( RevisionRecord::RAW );
1474 $parserOptions = ParserOptions::newFromUserAndLang(
1475 $this->getUserForPreview(),
1476 $services->getContentLanguage()
1477 );
1478 $contentTransformer = $services->getContentTransformer();
1479 $newContent = $contentTransformer->preSaveTransform(
1480 $content, $this->page, $this->getUserForPreview(), $parserOptions
1481 );
1482
1483 if ( $newContent->getModel() !== $oldContent->getModel() ) {
1484 // The undo may change content
1485 // model if its reverting the top
1486 // edit. This can result in
1487 // mismatched content model/format.
1488 $this->contentModel = $newContent->getModel();
1489 $oldMainSlot = $oldrev->getSlot(
1490 SlotRecord::MAIN,
1491 RevisionRecord::RAW
1492 );
1493 $this->contentFormat = $oldMainSlot->getFormat();
1494 if ( $this->contentFormat === null ) {
1495 $this->contentFormat = $this->contentHandlerFactory
1496 ->getContentHandler( $oldMainSlot->getModel() )
1497 ->getDefaultFormat();
1498 }
1499 }
1500
1501 if ( $newContent->equals( $oldContent ) ) {
1502 # Tell the user that the undo results in no change,
1503 # i.e. the revisions were already undone.
1504 $undoMsg = 'nochange';
1505 $content = false;
1506 } else {
1507 # Inform the user of our success and set an automatic edit summary
1508 $undoMsg = 'success';
1509 $this->generateUndoEditSummary( $oldrev, $undo, $undorev, $services );
1510 $this->undidRev = $undo;
1511 $this->undoAfter = $undoafter;
1512 $this->formtype = 'diff';
1513 }
1514 }
1515 } else {
1516 // Failed basic checks.
1517 // Older revisions may have been removed since the link
1518 // was created, or we may simply have got bogus input.
1519 $undoMsg = 'norev';
1520 }
1521
1522 $out = $this->context->getOutput();
1523 // Messages: undo-success, undo-failure, undo-main-slot-only, undo-norev,
1524 // undo-nochange.
1525 $class = "mw-undo-{$undoMsg}";
1526 $html = $this->context->msg( 'undo-' . $undoMsg )->parse();
1527 if ( $undoMsg !== 'success' ) {
1528 $html = Html::errorBox( $html );
1529 }
1530 $this->editFormPageTop .= Html::rawElement(
1531 'div',
1532 [ 'class' => $class ],
1533 $html
1534 );
1535 }
1536
1537 if ( $content === false ) {
1538 $content = $this->pageEditingHelper->getOriginalContent(
1539 $this->getAuthority(),
1540 $this->mArticle,
1541 $this->contentModel,
1542 $this->section,
1543 );
1544 }
1545 }
1546
1547 return $content;
1548 }
1549
1559 private function generateUndoEditSummary(
1560 ?RevisionRecord $oldRev,
1561 int $undo,
1562 ?RevisionRecord $undoRev,
1563 MediaWikiServices $services
1564 ): void {
1565 // Generate an autosummary
1566 $firstRev = $this->revisionStore->getNextRevision( $oldRev );
1567 if ( $firstRev && $firstRev->getId() == $undo ) {
1568 // Undid just one revision
1569 $userText = $undoRev->getUser()?->getName();
1570 // @phan-suppress-next-line PhanImpossibleTypeComparison T418946
1571 if ( $userText === null ) {
1572 $undoSummary = $this->context->msg(
1573 'undo-summary-username-hidden',
1574 $undo
1575 )->inContentLanguage()->text();
1576 } elseif ( ExternalUserNames::isExternal( $userText ) ) {
1577 // Handle external users (imported revisions)
1578 $userLinkTitle = ExternalUserNames::getUserLinkTitle( $userText );
1579 if ( $userLinkTitle ) {
1580 $userLink = $userLinkTitle->getPrefixedText();
1581 $undoSummary = $this->context->msg(
1582 'undo-summary-import',
1583 $undo,
1584 $userLink,
1585 $userText
1586 )->inContentLanguage()->text();
1587 } else {
1588 $undoSummary = $this->context->msg(
1589 'undo-summary-import2',
1590 $undo,
1591 $userText
1592 )->inContentLanguage()->text();
1593 }
1594 } else {
1595 $undoIsAnon =
1596 !$undoRev->getUser() ||
1597 !$undoRev->getUser()->isRegistered();
1598 $disableAnonTalk = $services->getMainConfig()->get( MainConfigNames::DisableAnonTalk );
1599 $undoMessage = ( $undoIsAnon && $disableAnonTalk ) ?
1600 'undo-summary-anon' :
1601 'undo-summary';
1602 $undoSummary = $this->context->msg(
1603 $undoMessage,
1604 $undo,
1605 $userText
1606 )->inContentLanguage()->text();
1607 }
1608 } else {
1609 // Undid multiple revisions
1610 $firstRevisionId = $firstRev->getId();
1611 $lastRevisionId = $undoRev->getId();
1612 $revisionCount = $services->getRevisionStore()->countRevisionsBetween(
1613 $firstRev->getPageId(),
1614 $firstRev,
1615 $undoRev,
1616 null,
1617 [ RevisionStore::INCLUDE_BOTH, RevisionStore::INCLUDE_DELETED_REVISIONS ]
1618 );
1619 $undoSummary = $this->context->msg( 'undo-summary-multiple' )
1620 ->numParams( $revisionCount )
1621 ->params( $firstRevisionId, $lastRevisionId )
1622 ->inContentLanguage()
1623 ->text();
1624 }
1625 if ( $this->summary === '' ) {
1626 $this->summary = $undoSummary;
1627 } else {
1628 $this->summary = $undoSummary . $this->context->msg( 'colon-separator' )
1629 ->inContentLanguage()->text() . $this->summary;
1630 }
1631 }
1632
1644 private function getParentRevId() {
1645 if ( $this->parentRevId ) {
1646 return $this->parentRevId;
1647 } else {
1648 return $this->mArticle->getRevIdFetched();
1649 }
1650 }
1651
1660 protected function getCurrentContent() {
1661 return $this->pageEditingHelper->getCurrentContent( $this->contentModel, $this->page );
1662 }
1663
1670 private function tokenOk( WebRequest $request ): bool {
1671 $token = $request->getVal( 'wpEditToken' );
1672 $user = $this->context->getUser();
1673 $this->mTokenOk = $user->matchEditToken( $token );
1674 return $this->mTokenOk;
1675 }
1676
1691 private function setPostEditCookie( int $statusValue ): void {
1692 $revisionId = $this->page->getLatest();
1693 $postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
1694
1695 $val = 'saved';
1696 if ( $statusValue === self::AS_SUCCESS_NEW_ARTICLE ) {
1697 $val = 'created';
1698 } elseif ( $this->oldid ) {
1699 $val = 'restored';
1700 }
1701 if ( $this->tempUserCreateDone ) {
1702 $val .= '+tempuser';
1703 }
1704
1705 $response = $this->context->getRequest()->response();
1706 $response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION );
1707 }
1708
1715 public function attemptSave( &$resultDetails = false ) {
1716 // Allow bots to exempt some edits from bot flagging
1717 $markAsBot = $this->markAsBot
1718 && $this->getAuthority()->isAllowed( 'bot' );
1719
1720 // Allow trusted users to mark some edits as minor
1721 $markAsMinor = $this->minoredit && !$this->isNew
1722 && $this->getAuthority()->isAllowed( 'minoredit' );
1723
1724 $status = $this->internalAttemptSave( $resultDetails, $markAsBot, $markAsMinor );
1725 if ( !$status->isOK() ) {
1726 $this->handleFailedConstraint( $status );
1727 }
1728
1729 // Status::wrap() takes references to all internal variables, allowing hook handlers to modify
1730 // the $status, without changing the hook interface to use the EditPageStatus type.
1731 $this->getHookRunner()->onEditPage__attemptSave_after( $this, Status::wrap( $status ), $resultDetails );
1732
1733 return $status;
1734 }
1735
1739 private function incrementResolvedConflicts(): void {
1740 if ( $this->context->getRequest()->getText( 'mode' ) !== 'conflict' ) {
1741 return;
1742 }
1743
1744 $this->getEditConflictHelper()->incrementResolvedStats( $this->context->getUser() );
1745 }
1746
1756 private function handleStatus( EditPageStatus $status, $resultDetails ): bool {
1757 $statusValue = is_int( $status->value ) ? $status->value : 0;
1758
1763 if ( $statusValue === self::AS_SUCCESS_UPDATE
1764 || $statusValue === self::AS_SUCCESS_NEW_ARTICLE
1765 ) {
1766 $this->incrementResolvedConflicts();
1767
1768 $this->didSave = true;
1769 if ( !$resultDetails['nullEdit'] ) {
1770 $this->setPostEditCookie( $statusValue );
1771 }
1772 }
1773
1774 $out = $this->context->getOutput();
1775
1776 // "wpExtraQueryRedirect" is a hidden input to modify
1777 // after save URL and is not used by actual edit form
1778 $request = $this->context->getRequest();
1779 $extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
1780
1781 switch ( $statusValue ) {
1782 // Status codes for which the error/warning message is generated somewhere else in this class.
1783 // They should be refactored to provide their own messages and handled below (T384399).
1784 case self::AS_HOOK_ERROR_EXPECTED:
1785 case self::AS_CONFLICT_DETECTED:
1786 return true;
1787
1788 case self::AS_HOOK_ERROR:
1789 return false;
1790
1791 // Status codes that provide their own error/warning messages. Most error scenarios that don't
1792 // need custom user interface (e.g. edit conflicts) should be handled here, one day (T384399).
1793 case self::AS_ARTICLE_WAS_DELETED:
1794 case self::AS_BLANK_ARTICLE:
1795 case self::AS_BROKEN_REDIRECT:
1796 case self::AS_CONTENT_TOO_BIG:
1797 case self::AS_DOUBLE_REDIRECT:
1798 case self::AS_DOUBLE_REDIRECT_LOOP:
1799 case self::AS_END:
1800 case self::AS_INVALID_REDIRECT_TARGET:
1801 case self::AS_MAX_ARTICLE_SIZE_EXCEEDED:
1802 case self::AS_PARSE_ERROR:
1803 case self::AS_RATE_LIMITED:
1804 case self::AS_REVISION_MISSING:
1805 case self::AS_REVISION_WAS_DELETED:
1806 case self::AS_SELF_REDIRECT:
1807 case self::AS_SUMMARY_NEEDED:
1808 case self::AS_TEXTBOX_EMPTY:
1809 case self::AS_UNABLE_TO_ACQUIRE_TEMP_ACCOUNT:
1810 case self::AS_UNICODE_NOT_SUPPORTED:
1811 $out->addHTML( $this->formatConstraintStatus( $status ) );
1812 return true;
1813
1814 case self::AS_SUCCESS_NEW_ARTICLE:
1815 $queryParts = [];
1816 if ( $resultDetails['redirect'] ) {
1817 $queryParts[] = 'redirect=no';
1818 }
1819 if ( $extraQueryRedirect ) {
1820 $queryParts[] = $extraQueryRedirect;
1821 }
1822 $anchor = $resultDetails['sectionanchor'] ?? '';
1823 $this->doPostEditRedirect( implode( '&', $queryParts ), $anchor );
1824 return false;
1825
1826 case self::AS_SUCCESS_UPDATE:
1827 $extraQuery = '';
1828 $sectionanchor = $resultDetails['sectionanchor'];
1829 // Give extensions a chance to modify URL query on update
1830 $this->getHookRunner()->onArticleUpdateBeforeRedirect( $this->mArticle,
1831 $sectionanchor, $extraQuery );
1832
1833 $queryParts = [];
1834 if ( $resultDetails['redirect'] ) {
1835 $queryParts[] = 'redirect=no';
1836 }
1837 if ( $extraQuery ) {
1838 $queryParts[] = $extraQuery;
1839 }
1840 if ( $extraQueryRedirect ) {
1841 $queryParts[] = $extraQueryRedirect;
1842 }
1843 $this->doPostEditRedirect( implode( '&', $queryParts ), $sectionanchor );
1844 return false;
1845
1846 case self::AS_SPAM_ERROR:
1847 $this->spamPageWithContent( $resultDetails['spam'] ?? false );
1848 return false;
1849
1850 case self::AS_BLOCKED_PAGE_FOR_USER:
1851 case self::AS_IMAGE_REDIRECT_ANON:
1852 case self::AS_IMAGE_REDIRECT_LOGGED:
1853 case self::AS_NO_CHANGE_CONTENT_MODEL:
1854 case self::AS_NO_CREATE_PERMISSION:
1855 case self::AS_READ_ONLY_PAGE:
1856 case self::AS_READ_ONLY_PAGE_ANON:
1857 case self::AS_READ_ONLY_PAGE_LOGGED:
1858 $status->throwError();
1859 // No break statement here as throwError() will always throw an exception
1860
1861 default:
1862 // We don't recognize $statusValue. The only way that can happen
1863 // is if an extension hook aborted from inside ArticleSave.
1864 // Render the status object into $this->hookError
1865 // FIXME this sucks, we should just use the Status object throughout
1866 $this->hookError = Html::errorBox(
1867 "\n" . Status::cast( $status )->getWikiText( false, false, $this->context->getLanguage() )
1868 );
1869 return true;
1870 }
1871 }
1872
1877 private function formatConstraintStatus( StatusValue $status ): string {
1878 return $this->createMessageBox( $status->getMessages( 'error' ), 'error' ) .
1879 $this->createMessageBox( $status->getMessages( 'warning' ), 'warning' );
1880 }
1881
1887 private function createMessageBox( array $messages, string $type ): string {
1888 if ( !$messages ) {
1889 // Don't create a box if there are no messages
1890 return '';
1891 }
1892
1893 $html = implode(
1894 Html::openElement( 'br' ),
1895 array_map( fn ( $msg ) => $this->context->msg( $msg )->parse(), $messages )
1896 );
1897
1898 if ( $type === 'warning' ) {
1899 return Html::warningBox( $html );
1900 }
1901 return Html::errorBox( $html );
1902 }
1903
1911 private function doPostEditRedirect( $query, $anchor ) {
1912 $out = $this->context->getOutput();
1913 $url = $this->getTitle()->getFullURL( $query ) . $anchor;
1914 $user = $this->getUserForSave();
1915 // If the temporary account was created in this request,
1916 // or if the temporary account has zero edits (implying
1917 // that the account was created during a failed edit
1918 // attempt in a previous request), perform the top-level
1919 // redirect to ensure the account is attached.
1920 // Note that the temp user could already have performed
1921 // the top-level redirect if this a first edit on
1922 // a wiki that is not the user's home wiki.
1923 $shouldRedirectForTempUser = $this->tempUserCreateDone ||
1924 ( $user->isTemp() && ( $user->getEditCount() === 0 ) );
1925 if ( $shouldRedirectForTempUser ) {
1926 $this->getHookRunner()->onTempUserCreatedRedirect(
1927 $this->context->getRequest()->getSession(),
1928 $user,
1929 $this->getTitle()->getPrefixedDBkey(),
1930 $query,
1931 $anchor,
1932 $url
1933 );
1934 }
1935 $out->redirect( $url );
1936 }
1937
1941 private function setNewSectionSummary(): void {
1942 Assert::precondition( $this->section === 'new', 'This method can only be called for new sections' );
1943 Assert::precondition( $this->sectiontitle !== null, 'This method can only be called for new sections' );
1944
1945 $services = MediaWikiServices::getInstance();
1946 $parser = $services->getParser();
1947 $textFormatter = $services->getMessageFormatterFactory()->getTextFormatter(
1948 $services->getContentLanguageCode()->toString()
1949 );
1950
1951 if ( $this->sectiontitle !== '' ) {
1952 $this->newSectionAnchor = $this->pageEditingHelper->guessSectionName( $this->sectiontitle );
1953 // If no edit summary was specified, create one automatically from the section
1954 // title and have it link to the new section. Otherwise, respect the summary as
1955 // passed.
1956 if ( $this->summary === '' ) {
1957 $messageValue = MessageValue::new( 'newsectionsummary' )
1958 ->plaintextParams( $parser->stripSectionName( $this->sectiontitle ) );
1959 $this->summary = $textFormatter->format( $messageValue );
1960 }
1961 } else {
1962 $this->newSectionAnchor = '';
1963 }
1964 }
1965
1992 private function internalAttemptSave( &$result, $markAsBot = false, $markAsMinor = false ) {
1993 // If an attempt to acquire a temporary name failed, don't attempt to do anything else.
1994 if ( $this->unableToAcquireTempName ) {
1995 return EditPageStatus::newFatal( 'temp-user-unable-to-acquire' )
1996 ->setValue( self::AS_UNABLE_TO_ACQUIRE_TEMP_ACCOUNT );
1997 }
1998 // Auto-create the temporary account user, if the feature is enabled.
1999 // We create the account before any constraint checks or edit hooks fire, to ensure
2000 // that we have an actor and user account that can be used for any logs generated
2001 // by the edit attempt, and to ensure continuity in the user experience (if a constraint
2002 // denies an edit to a logged-out user, that history should be associated with the
2003 // eventually successful account creation)
2004 $tempAccountStatus = $this->createTempUser();
2005 if ( !$tempAccountStatus->isOK() ) {
2006 return EditPageStatus::cast( $tempAccountStatus );
2007 }
2008 if ( $tempAccountStatus instanceof CreateStatus ) {
2009 $result['savedTempUser'] = $tempAccountStatus->getUser();
2010 }
2011
2012 $useNPPatrol = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::UseNPPatrol );
2013 $useRCPatrol = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::UseRCPatrol );
2014 if ( !$this->getHookRunner()->onEditPage__attemptSave( $this ) ) {
2015 wfDebug( "Hook 'EditPage::attemptSave' aborted article saving" );
2016 return EditPageStatus::newFatal( 'hookaborted' )
2017 ->setValue( self::AS_HOOK_ERROR );
2018 }
2019
2020 if ( !$this->getHookRunner()->onEditFilter( $this, $this->textbox1, $this->section,
2021 $this->hookError, $this->summary )
2022 ) {
2023 # Error messages etc. could be handled within the hook...
2024 return EditPageStatus::newFatal( 'hookaborted' )
2025 ->setValue( self::AS_HOOK_ERROR );
2026 } elseif ( $this->hookError ) {
2027 # ...or the hook could be expecting us to produce an error
2028 return EditPageStatus::newFatal( 'hookaborted' )
2029 ->setValue( self::AS_HOOK_ERROR_EXPECTED );
2030 }
2031
2032 try {
2033 # Construct Content object
2034 $textbox_content = $this->toEditContent( $this->textbox1 );
2035 } catch ( MWContentSerializationException $ex ) {
2036 return EditPageStatus::newFatal(
2037 'content-failed-to-parse',
2038 $this->contentModel,
2039 $this->contentFormat,
2040 $ex->getMessage(),
2041 )->setValue( self::AS_PARSE_ERROR );
2042 }
2043 '@phan-var Content $textbox_content';
2044
2045 $this->contentLength = strlen( $this->textbox1 );
2046
2047 $requestUser = $this->context->getUser();
2048 $authority = $this->getAuthority();
2049 $pstUser = $this->getUserForPreview();
2050
2051 $changingContentModel = false;
2052 if ( $this->contentModel !== $this->getTitle()->getContentModel() ) {
2053 $changingContentModel = true;
2054 $oldContentModel = $this->getTitle()->getContentModel();
2055 }
2056
2057 // Load the page data from the primary DB. If anything changes in the meantime,
2058 // we detect it by using page_latest like a token in a 1 try compare-and-swap.
2059 $this->page->loadPageData( IDBAccessObject::READ_LATEST );
2060 $new = !$this->page->exists();
2061
2062 // Message key of the label of the submit button - used by some constraint error messages
2063 $submitButtonLabel = $this->getSubmitButtonLabel();
2064
2065 $preliminaryChecksRunner = $this->getPreliminaryChecksRunner(
2066 $authority,
2067 $new,
2068 $textbox_content,
2069 $requestUser,
2070 $submitButtonLabel,
2071 );
2072 $status = $preliminaryChecksRunner->checkConstraints();
2073 if ( !$status->isOK() ) {
2074 $failed = $status->getFailedConstraint();
2075
2076 // Need to check SpamRegexConstraint here, to avoid needing to pass
2077 // $result by reference again
2078 if ( $failed instanceof SpamRegexConstraint ) {
2079 $result['spam'] = $failed->getMatch();
2080 }
2081
2082 return $status;
2083 }
2084
2085 $flags = EDIT_AUTOSUMMARY |
2086 ( $new ? EDIT_NEW : EDIT_UPDATE ) |
2087 ( $markAsMinor ? EDIT_MINOR : 0 ) |
2088 ( $markAsBot ? EDIT_FORCE_BOT : 0 );
2089
2090 if ( $new ) {
2091 $content = $textbox_content;
2092
2093 $result['sectionanchor'] = '';
2094 if ( $this->section === 'new' ) {
2095 if ( $this->sectiontitle !== null ) {
2096 // Insert the section title above the content.
2097 $content = $content->addSectionHeader( $this->sectiontitle );
2098 }
2099 $result['sectionanchor'] = $this->newSectionAnchor;
2100 }
2101
2102 $pageUpdater = $this->page->newPageUpdater( $pstUser )
2103 ->setContent( SlotRecord::MAIN, $content );
2104 $pageUpdater->prepareUpdate( $flags );
2105
2106 $newPageChecksRunner = $this->getNewPageChecksRunner(
2107 $content,
2108 $markAsMinor,
2109 $pstUser,
2110 $submitButtonLabel,
2111 );
2112 $status = $newPageChecksRunner->checkConstraints();
2113 if ( !$status->isOK() ) {
2114 return $status;
2115 }
2116 } else { # not $new
2117
2118 # Article exists. Check for edit conflict.
2119
2120 $timestamp = $this->page->getTimestamp();
2121 $latest = $this->page->getLatest();
2122
2123 wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}" );
2124 wfDebug( "revision: {$latest}, editRevId: {$this->editRevId}" );
2125
2126 $editConflictLogger = LoggerFactory::getInstance( 'EditConflict' );
2127 // An edit conflict is detected if the current revision is different from the
2128 // revision that was current when editing was initiated on the client.
2129 // This is checked based on the timestamp and revision ID.
2130 // TODO: the timestamp based check can probably go away now.
2131 if ( ( $this->edittime !== null && $this->edittime != $timestamp )
2132 || ( $this->editRevId !== null && $this->editRevId != $latest )
2133 ) {
2134 $this->isConflict = true;
2135 if ( $this->section === 'new' ) {
2136 if ( $this->page->getUserText() === $requestUser->getName() &&
2137 $this->page->getComment() === $this->summary
2138 ) {
2139 // Probably a duplicate submission of a new comment.
2140 // This can happen when CDN resends a request after
2141 // a timeout but the first one actually went through.
2142 $editConflictLogger->debug(
2143 'Duplicate new section submission; trigger edit conflict!'
2144 );
2145 } else {
2146 // New comment; suppress conflict.
2147 $this->isConflict = false;
2148 $editConflictLogger->debug( 'Conflict suppressed; new section' );
2149 }
2150 } elseif ( $this->section === ''
2151 && $this->edittime
2152 && $this->revisionStore->userWasLastToEdit(
2153 $this->dbProvider->getPrimaryDatabase(),
2154 $this->getTitle()->getArticleID(),
2155 $requestUser->getId(),
2156 $this->edittime
2157 )
2158 ) {
2159 # Suppress edit conflict with self, except for section edits where merging is required.
2160 $editConflictLogger->debug( 'Suppressing edit conflict, same user.' );
2161 $this->isConflict = false;
2162 }
2163 }
2164
2165 if ( $this->isConflict ) {
2166 $editConflictLogger->debug(
2167 'Conflict! Getting section {section} for time {editTime}'
2168 . ' (id {editRevId}, article time {timestamp})',
2169 [
2170 'section' => $this->section,
2171 'editTime' => $this->edittime,
2172 'editRevId' => $this->editRevId,
2173 'timestamp' => $timestamp,
2174 ]
2175 );
2176 // @TODO: replaceSectionAtRev() with base ID (not prior current) for ?oldid=X case
2177 // ...or disable section editing for non-current revisions (not exposed anyway).
2178 if ( $this->editRevId !== null ) {
2179 $content = $this->page->replaceSectionAtRev(
2180 $this->section,
2181 $textbox_content,
2182 $this->sectiontitle,
2183 $this->editRevId
2184 );
2185 } else {
2186 $content = $this->page->replaceSectionContent(
2187 $this->section,
2188 $textbox_content,
2189 $this->sectiontitle,
2190 $this->edittime
2191 );
2192 }
2193 } else {
2194 $editConflictLogger->debug(
2195 'Getting section {section}',
2196 [ 'section' => $this->section ]
2197 );
2198 $content = $this->page->replaceSectionAtRev(
2199 $this->section,
2200 $textbox_content,
2201 $this->sectiontitle
2202 );
2203 }
2204
2205 if ( $content === null ) {
2206 $editConflictLogger->debug( 'Activating conflict; section replace failed.' );
2207 $this->isConflict = true;
2208 $content = $textbox_content; // do not try to merge here!
2209 } elseif ( $this->isConflict ) {
2210 // Attempt merge
2211 $mergedChange = $this->mergeChangesIntoContent( $content );
2212 if ( $mergedChange !== false ) {
2213 // Successful merge! Maybe we should tell the user the good news?
2214 $content = $mergedChange[0];
2215 $this->parentRevId = $mergedChange[1];
2216 $this->isConflict = false;
2217 $editConflictLogger->debug( 'Suppressing edit conflict, successful merge.' );
2218 } else {
2219 $this->section = '';
2220 $this->textbox1 = ( $content instanceof TextContent ) ? $content->getText() : '';
2221 $editConflictLogger->debug( 'Keeping edit conflict, failed merge.' );
2222 }
2223 }
2224
2225 if ( $this->isConflict ) {
2226 return EditPageStatus::newGood( self::AS_CONFLICT_DETECTED )->setOK( false );
2227 }
2228
2229 $pageUpdater = $this->page->newPageUpdater( $pstUser )
2230 ->setContent( SlotRecord::MAIN, $content );
2231 $pageUpdater->prepareUpdate( $flags );
2232
2233 $existingPageChecksRunner = $this->getExistingPageChecksRunner(
2234 $authority,
2235 $content,
2236 $markAsMinor,
2237 $pstUser,
2238 $submitButtonLabel,
2239 );
2240 $status = $existingPageChecksRunner->checkConstraints();
2241 if ( !$status->isOK() ) {
2242 return $status;
2243 }
2244
2245 # All's well
2246 $sectionAnchor = '';
2247 if ( $this->section === 'new' ) {
2248 $sectionAnchor = $this->newSectionAnchor;
2249 } elseif ( $this->section !== '' ) {
2250 # Try to get a section anchor from the section source, redirect
2251 # to edited section if header found.
2252 # XXX: Might be better to integrate this into WikiPage::replaceSectionAtRev
2253 # for duplicate heading checking and maybe parsing.
2254 $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
2255 # We can't deal with anchors, includes, html etc in the header for now,
2256 # headline would need to be parsed to improve this.
2257 if ( $hasmatch && $matches[2] !== '' ) {
2258 $sectionAnchor = $this->pageEditingHelper->guessSectionName( $matches[2] );
2259 }
2260 }
2261 $result['sectionanchor'] = $sectionAnchor;
2262
2263 // Save errors may fall down to the edit form, but we've now
2264 // merged the section into full text. Clear the section field
2265 // so that later submission of conflict forms won't try to
2266 // replace that into a duplicated mess.
2267 $this->textbox1 = $this->pageEditingHelper->toEditText(
2268 $content, $this->contentFormat, $this->enableApiEditOverride
2269 );
2270 $this->section = '';
2271 }
2272
2273 // Check for length errors again now that the section is merged in
2274 $this->contentLength = strlen( $this->pageEditingHelper->toEditText(
2275 $content, $this->contentFormat, $this->enableApiEditOverride
2276 ) ?? '' );
2277
2278 $postMergeChecksRunner = $this->getPostMergeChecksRunner(
2279 $content,
2280 $submitButtonLabel,
2281 );
2282 $status = $postMergeChecksRunner->checkConstraints();
2283 if ( !$status->isOK() ) {
2284 return $status;
2285 }
2286
2287 if ( $this->undidRev && $this->isUndoClean( $content ) ) {
2288 // As the user can change the edit's content before saving, we only mark
2289 // "clean" undos as reverts. This is to avoid abuse by marking irrelevant
2290 // edits as undos.
2291 $pageUpdater
2292 ->setOriginalRevisionId( $this->undoAfter ?: false )
2293 ->setCause( PageUpdateCauses::CAUSE_UNDO )
2294 ->markAsRevert(
2295 EditResult::REVERT_UNDO,
2296 $this->undidRev,
2297 $this->undoAfter ?: null
2298 );
2299 }
2300
2301 $needsPatrol = $useRCPatrol || ( $useNPPatrol && !$this->page->exists() );
2302 if ( $needsPatrol && $authority->authorizeWrite( 'autopatrol', $this->getTitle() ) ) {
2303 $pageUpdater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
2304 }
2305
2306 $pageUpdater
2307 ->addTags( $this->changeTags )
2308 ->saveRevision(
2309 CommentStoreComment::newUnsavedComment( trim( $this->summary ) ),
2310 $flags
2311 );
2312 $doEditStatus = $pageUpdater->getStatus();
2313
2314 if ( !$doEditStatus->isOK() ) {
2315 // Failure from doEdit()
2316 // Show the edit conflict page for certain recognized errors from doEdit(),
2317 // but don't show it for errors from extension hooks
2318 if (
2319 $doEditStatus->failedBecausePageMissing() ||
2320 $doEditStatus->failedBecausePageExists() ||
2321 $doEditStatus->failedBecauseOfConflict()
2322 ) {
2323 $this->isConflict = true;
2324 return EditPageStatus::cast( $doEditStatus )
2325 ->setValue( self::AS_END );
2326 }
2327 return EditPageStatus::cast( $doEditStatus );
2328 }
2329
2330 $result['nullEdit'] = !$doEditStatus->wasRevisionCreated();
2331 if ( $result['nullEdit'] ) {
2332 // We didn't know if it was a null edit until now, so bump the rate limit now
2333 $limitSubject = $requestUser->toRateLimitSubject();
2334 MediaWikiServices::getInstance()->getRateLimiter()->limit( $limitSubject, 'linkpurge' );
2335 }
2336 $result['redirect'] = $content->isRedirect();
2337
2338 $this->updateWatchlist();
2339
2340 // If the content model changed, add a log entry
2341 if ( $changingContentModel ) {
2342 $this->addContentModelChangeLogEntry(
2343 $this->getUserForSave(),
2344 // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable
2345 // $oldContentModel is set when $changingContentModel is true
2346 $new ? false : $oldContentModel,
2347 $this->contentModel,
2348 $this->summary
2349 );
2350 }
2351
2352 // Instead of carrying the same status object throughout, it is created right
2353 // when it is returned, either at an earlier point due to an error or here
2354 // due to a successful edit.
2355 $statusCode = ( $new ? self::AS_SUCCESS_NEW_ARTICLE : self::AS_SUCCESS_UPDATE );
2356 return EditPageStatus::newGood( $statusCode );
2357 }
2358
2359 private function getPreliminaryChecksRunner(
2360 Authority $authority,
2361 bool $new,
2362 Content $newContent,
2363 User $requestUser,
2364 string $submitButtonLabel,
2365 ): EditConstraintRunner {
2366 return new EditConstraintRunner(
2367 // Ensure that `$this->unicodeCheck` is the correct unicode
2368 new UnicodeConstraint( $this->unicodeCheck ),
2369
2370 // Ensure that the context request does not have `wpAntispam` set
2371 // Use $user since there is no permissions aspect
2372 $this->constraintFactory->newSimpleAntiSpamConstraint(
2373 $this->context->getRequest()->getText( 'wpAntispam' ),
2374 $requestUser,
2375 $this->getTitle()
2376 ),
2377
2378 // Ensure that the summary and text don't match the spam regex
2379 $this->constraintFactory->newSpamRegexConstraint(
2380 $this->summary,
2381 $this->sectiontitle,
2382 $this->textbox1,
2383 $this->context->getRequest()->getIP(),
2384 $this->getTitle()
2385 ),
2386
2387 new ImageRedirectConstraint(
2388 $newContent,
2389 $this->getTitle(),
2390 $authority
2391 ),
2392
2393 $this->constraintFactory->newReadOnlyConstraint(),
2394
2395 new AuthorizationConstraint(
2396 $authority,
2397 $this->page,
2398 $new
2399 ),
2400
2401 new ContentModelChangeConstraint(
2402 $authority,
2403 $this->getTitle(),
2404 $this->contentModel
2405 ),
2406
2407 $this->constraintFactory->newLinkPurgeRateLimitConstraint( $requestUser->toRateLimitSubject() ),
2408
2409 // Same constraint is used to check size before and after merging the
2410 // edits, which use different failure codes
2411 $this->constraintFactory->newPageSizeConstraint(
2412 $this->contentLength,
2413 PageSizeConstraint::BEFORE_MERGE
2414 ),
2415
2416 new ChangeTagsConstraint( $authority, $this->changeTags ),
2417
2418 // If the article has been deleted while editing, don't save it without confirmation
2419 $this->constraintFactory->newAccidentalRecreationConstraint(
2420 $this->getTitle(),
2421 $this->recreate,
2422 $this->starttime,
2423 $submitButtonLabel,
2424 )
2425 );
2426 }
2427
2428 private function getNewPageChecksRunner(
2429 Content $content,
2430 bool $markAsMinor,
2431 User $pstUser,
2432 string $submitButtonLabel,
2433 ): EditConstraintRunner {
2434 return new EditConstraintRunner(
2435 // Don't save a new page if it's blank or if it's a MediaWiki:
2436 // message with content equivalent to default (allow empty pages
2437 // in this case to disable messages, see T52124)
2438 new DefaultTextConstraint(
2439 $this->getTitle(),
2440 $this->allowBlankArticle,
2441 $this->textbox1,
2442 $submitButtonLabel
2443 ),
2444
2445 $this->constraintFactory->newEditFilterMergedContentHookConstraint(
2446 $content,
2447 $this->context,
2448 $this->summary,
2449 $markAsMinor,
2450 $this->context->getLanguage(),
2451 $pstUser
2452 ),
2453 );
2454 }
2455
2456 private function getExistingPageChecksRunner(
2457 Authority $authority,
2458 Content $content,
2459 bool $markAsMinor,
2460 User $pstUser,
2461 string $submitButtonLabel,
2462 ): EditConstraintRunner {
2463 return new EditConstraintRunner(
2464 $this->constraintFactory->newEditFilterMergedContentHookConstraint(
2465 $content,
2466 $this->context,
2467 $this->summary,
2468 $markAsMinor,
2469 $this->context->getLanguage(),
2470 $pstUser
2471 ),
2472 new NewSectionMissingSubjectConstraint(
2473 $this->section,
2474 $this->sectiontitle ?? '',
2475 $this->allowBlankSummary,
2476 $submitButtonLabel
2477 ),
2478 new MissingCommentConstraint( $this->section, $this->textbox1 ),
2479 new ExistingSectionEditConstraint(
2480 $this->section,
2481 $this->summary,
2482 $this->autoSumm,
2483 $this->allowBlankSummary,
2484 $content,
2485 $this->pageEditingHelper->getOriginalContent(
2486 $authority,
2487 $this->mArticle,
2488 $this->contentModel,
2489 $this->section,
2490 ),
2491 $submitButtonLabel
2492 ),
2493 new RevisionDeletedConstraint(
2494 $this->mArticle,
2495 $this->ignoreRevisionDeletedWarning,
2496 $this->oldid,
2497 $this->section,
2498 $this->getTitle(),
2499 $pstUser,
2500 MessageValue::new(
2501 'edit-constraint-warning-wrapper-save-deleted-revision',
2502 [ MessageValue::new( $submitButtonLabel ) ],
2503 ),
2504 ),
2505 );
2506 }
2507
2508 private function getPostMergeChecksRunner(
2509 Content $content,
2510 string $submitButtonLabel,
2511 ): EditConstraintRunner {
2512 $constraintRunner = new EditConstraintRunner();
2513 if ( !$this->ignoreProblematicRedirects ) {
2514 $constraintRunner->addConstraint(
2515 $this->constraintFactory->newRedirectConstraint(
2516 $this->allowedProblematicRedirectTarget,
2517 $content,
2518 $this->getCurrentContent(),
2519 $this->getTitle(),
2520 MessageValue::new(
2521 'edit-constraint-warning-wrapper-save',
2522 [ MessageValue::new( $submitButtonLabel ) ],
2523 ),
2524 $this->contentFormat,
2525 )
2526 );
2527 }
2528 $constraintRunner->addConstraint(
2529 // Same constraint is used to check size before and after merging the
2530 // edits, which use different failure codes
2531 $this->constraintFactory->newPageSizeConstraint(
2532 $this->contentLength,
2533 PageSizeConstraint::AFTER_MERGE
2534 )
2535 );
2536 return $constraintRunner;
2537 }
2538
2545 private function handleFailedConstraint( EditPageStatus $status ): void {
2546 $failed = $status->getFailedConstraint();
2547 if ( $failed instanceof AuthorizationConstraint ) {
2548 // Auto-block user's IP if the account was "hard" blocked
2549 if (
2550 !MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly()
2551 && $status->value === self::AS_BLOCKED_PAGE_FOR_USER
2552 ) {
2553 $this->context->getUser()->spreadAnyEditBlock();
2554 }
2555 } elseif ( $failed instanceof DefaultTextConstraint ) {
2556 $this->blankArticle = true;
2557 } elseif ( $failed instanceof EditFilterMergedContentHookConstraint ) {
2558 $this->hookError = $failed->getHookError();
2559 } elseif (
2560 // ExistingSectionEditConstraint also checks for revisions deleted
2561 // since the edit was loaded, which doesn't indicate a missing summary
2562 (
2563 $failed instanceof ExistingSectionEditConstraint
2564 && $status->value === self::AS_SUMMARY_NEEDED
2565 ) ||
2566 $failed instanceof NewSectionMissingSubjectConstraint
2567 ) {
2568 $this->missingSummary = true;
2569 } elseif ( $failed instanceof RedirectConstraint ) {
2570 $this->problematicRedirectTarget = $failed->problematicTarget;
2571 } elseif ( $failed instanceof AccidentalRecreationConstraint ) {
2572 $this->recreate = true;
2573 } elseif ( $failed instanceof RevisionDeletedConstraint ) {
2574 $this->ignoreRevisionDeletedWarning = true;
2575 }
2576 }
2577
2588 private function isUndoClean( Content $content ): bool {
2589 // Check whether the undo was "clean", that is the user has not modified
2590 // the automatically generated content.
2591 $undoRev = $this->revisionStore->getRevisionById( $this->undidRev );
2592 if ( $undoRev === null ) {
2593 return false;
2594 }
2595
2596 if ( $this->undoAfter ) {
2597 $oldRev = $this->revisionStore->getRevisionById( $this->undoAfter );
2598 } else {
2599 $oldRev = $this->revisionStore->getPreviousRevision( $undoRev );
2600 }
2601
2602 if ( $oldRev === null ||
2603 $undoRev->isDeleted( RevisionRecord::DELETED_TEXT ) ||
2604 $oldRev->isDeleted( RevisionRecord::DELETED_TEXT )
2605 ) {
2606 return false;
2607 }
2608
2609 $undoContent = $this->pageEditingHelper->getUndoContent( $this->page, $undoRev, $oldRev, $undoError );
2610 if ( !$undoContent ) {
2611 return false;
2612 }
2613
2614 // Do a pre-save transform on the retrieved undo content
2615 $services = MediaWikiServices::getInstance();
2616 $contentLanguage = $services->getContentLanguage();
2617 $user = $this->getUserForPreview();
2618 $parserOptions = ParserOptions::newFromUserAndLang( $user, $contentLanguage );
2619 $contentTransformer = $services->getContentTransformer();
2620 $undoContent = $contentTransformer->preSaveTransform( $undoContent, $this->page, $user, $parserOptions );
2621
2622 if ( $undoContent->equals( $content ) ) {
2623 return true;
2624 }
2625 return false;
2626 }
2627
2634 private function addContentModelChangeLogEntry( UserIdentity $user, $oldModel, $newModel, $reason = "" ): void {
2635 $new = $oldModel === false;
2636 $log = new ManualLogEntry( 'contentmodel', $new ? 'new' : 'change' );
2637 $log->setPerformer( $user );
2638 $log->setTarget( $this->page );
2639 $log->setComment( is_string( $reason ) ? $reason : "" );
2640 $log->setParameters( [
2641 '4::oldmodel' => $oldModel,
2642 '5::newmodel' => $newModel
2643 ] );
2644 $logid = $log->insert();
2645 $log->publish( $logid );
2646 }
2647
2651 private function updateWatchlist(): void {
2652 if ( $this->tempUserCreateActive ) {
2653 return;
2654 }
2655 $user = $this->getUserForSave();
2656 if ( !$user->isNamed() ) {
2657 return;
2658 }
2659
2660 $watch = $this->watchthis;
2661 $watchlistExpiry = $this->watchlistExpiry;
2662
2663 // This can't run as a DeferredUpdate due to a possible race condition
2664 // when the post-edit redirect happens if the pendingUpdates queue is
2665 // too large to finish in time (T259564)
2666 $this->watchlistManager->setWatch( $watch, $user, $this->page, $watchlistExpiry );
2667
2668 $this->watchedItemStore->maybeEnqueueWatchlistExpiryJob();
2669 }
2670
2681 private function mergeChangesIntoContent( Content $editContent ) {
2682 // This is the revision that was current at the time editing was initiated on the client,
2683 // even if the edit was based on an old revision.
2684 $baseRevRecord = $this->getExpectedParentRevision();
2685 $baseContent = $baseRevRecord ?
2686 $baseRevRecord->getContent( SlotRecord::MAIN ) :
2687 null;
2688
2689 if ( $baseContent === null ) {
2690 return false;
2691 } elseif ( $baseRevRecord->isCurrent() ) {
2692 // Impossible to have a conflict when the user just edited the latest revision. This can
2693 // happen e.g. when $wgDiff3 is badly configured.
2694 return [ $editContent, $baseRevRecord->getId() ];
2695 }
2696
2697 // The current state, we want to merge updates into it
2698 $currentRevisionRecord = $this->revisionStore->getRevisionByTitle(
2699 $this->page,
2700 0,
2701 IDBAccessObject::READ_LATEST
2702 );
2703 $currentContent = $currentRevisionRecord
2704 ? $currentRevisionRecord->getContent( SlotRecord::MAIN )
2705 : null;
2706
2707 if ( $currentContent === null ) {
2708 return false;
2709 }
2710
2711 $mergedContent = $this->contentHandlerFactory
2712 ->getContentHandler( $baseContent->getModel() )
2713 ->merge3( $baseContent, $editContent, $currentContent );
2714
2715 if ( $mergedContent ) {
2716 // Also need to update parentRevId to what we just merged.
2717 return [ $mergedContent, $currentRevisionRecord->getId() ];
2718 }
2719
2720 return false;
2721 }
2722
2730 public function getExpectedParentRevision() {
2731 return $this->pageEditingHelper->getExpectedParentRevision(
2732 $this->editRevId,
2733 $this->edittime,
2734 $this->page,
2735 );
2736 }
2737
2738 public function setHeaders() {
2739 $out = $this->context->getOutput();
2740
2741 $out->addModules( 'mediawiki.action.edit' );
2742 $out->addModuleStyles( [
2743 'mediawiki.action.edit.styles',
2744 'mediawiki.codex.messagebox.styles',
2745 'mediawiki.editfont.styles',
2746 'mediawiki.interface.helpers.styles',
2747 ] );
2748
2749 $user = $this->context->getUser();
2750
2751 if ( $this->userOptionsLookup->getOption( $user, 'uselivepreview' ) ) {
2752 $out->addModules( 'mediawiki.action.edit.preview' );
2753 }
2754
2755 if ( $this->userOptionsLookup->getOption( $user, 'useeditwarning' ) ) {
2756 $out->addModules( 'mediawiki.action.edit.editWarning' );
2757 }
2758
2759 if ( $this->context->getConfig()->get( MainConfigNames::EnableEditRecovery )
2760 && $this->userOptionsLookup->getOption( $user, 'editrecovery' )
2761 ) {
2762 $wasPosted = $this->getContext()->getRequest()->getMethod() === 'POST';
2763 $out->addJsConfigVars( 'wgEditRecoveryWasPosted', $wasPosted );
2764 $out->addModules( 'mediawiki.editRecovery.edit' );
2765 }
2766
2767 # Enabled article-related sidebar, toplinks, etc.
2768 $out->setArticleRelated( true );
2769
2770 $contextTitle = $this->getContextTitle();
2771 if ( $this->isConflict ) {
2772 $msg = 'editconflict';
2773 } elseif ( $contextTitle->exists() && $this->section != '' ) {
2774 $msg = $this->section === 'new' ? 'editingcomment' : 'editingsection';
2775 } else {
2776 $msg = $contextTitle->exists()
2777 || ( $contextTitle->getNamespace() === NS_MEDIAWIKI
2778 && $contextTitle->getDefaultMessageText() !== false
2779 )
2780 ? 'editing'
2781 : 'creating';
2782 }
2783
2784 # Use the title defined by DISPLAYTITLE magic word when present
2785 # NOTE: getDisplayTitle() returns HTML while getPrefixedText() returns plain text.
2786 # Escape ::getPrefixedText() so that we have HTML in all cases,
2787 # and pass as a "raw" parameter to ::setPageTitleMsg().
2788 $displayTitle = $this->mParserOutput ? $this->mParserOutput->getDisplayTitle() : false;
2789 if ( $displayTitle === false ) {
2790 $displayTitle = htmlspecialchars(
2791 $contextTitle->getPrefixedText(), ENT_QUOTES, 'UTF-8', false
2792 );
2793 } else {
2794 $out->setDisplayTitle( $displayTitle );
2795 }
2796
2797 // Enclose the title with an element. This is used on live preview to update the
2798 // preview of the display title.
2799 $displayTitle = Html::rawElement( 'span', [ 'id' => 'firstHeadingTitle' ], $displayTitle );
2800
2801 $out->setPageTitleMsg( $this->context->msg( $msg )->rawParams( $displayTitle ) );
2802
2803 $config = $this->context->getConfig();
2804
2805 # Transmit the name of the message to JavaScript. This was added for live preview.
2806 # Live preview doesn't use this anymore. The variable is still transmitted because
2807 # Edit Recovery and user scripts use it.
2808 $out->addJsConfigVars( [
2809 'wgEditMessage' => $msg,
2810 ] );
2811
2812 // Add whether to use 'save' or 'publish' messages to JavaScript for post-edit, other
2813 // editors, etc.
2814 $out->addJsConfigVars(
2815 'wgEditSubmitButtonLabelPublish',
2816 $config->get( MainConfigNames::EditSubmitButtonLabelPublish )
2817 );
2818 }
2819
2823 private function showIntro(): void {
2824 $services = MediaWikiServices::getInstance();
2825
2826 // Hardcoded list of notices that are suppressable for historical reasons.
2827 // This feature was originally added for LiquidThreads, to avoid showing non-essential messages
2828 // when commenting in a thread, but some messages were included (or excluded) by mistake before
2829 // its implementation was moved to one place, and this list doesn't make a lot of sense.
2830 // TODO: Remove the suppressIntro feature from EditPage, and invent a better way for extensions
2831 // to skip individual intro messages.
2832 $skip = $this->suppressIntro ? [
2833 'editintro',
2834 'code-editing-intro',
2835 'sharedupload-desc-create',
2836 'sharedupload-desc-edit',
2837 'userpage-userdoesnotexist',
2838 'blocked-notice-logextract',
2839 'newarticletext',
2840 'newarticletextanon',
2841 'recreate-moveddeleted-warn',
2842 ] : [];
2843
2844 $messages = $services->getIntroMessageBuilder()->getIntroMessages(
2845 IntroMessageBuilder::MORE_FRAMES,
2846 $skip,
2847 $this->context,
2848 $this->page,
2849 $this->mArticle->fetchRevisionRecord(),
2850 $this->context->getUser(),
2851 $this->context->getRequest()->getVal( 'editintro' ),
2853 array_diff_key(
2854 $this->context->getRequest()->getQueryValues(),
2855 [ 'title' => true, 'returnto' => true, 'returntoquery' => true ]
2856 )
2857 ),
2858 !$this->firsttime,
2859 $this->section !== '' ? $this->section : null
2860 );
2861
2862 foreach ( $messages as $message ) {
2863 $this->context->getOutput()->addHTML( $message );
2864 }
2865 }
2866
2883 protected function toEditContent( $text ) {
2884 if ( $text === false || $text === null ) {
2885 return $text;
2886 }
2887
2888 $content = ContentHandler::makeContent( $text, $this->getTitle(),
2889 $this->contentModel, $this->contentFormat );
2890
2891 if ( !$this->pageEditingHelper->isSupportedContentModel(
2892 $content->getModel(), $this->enableApiEditOverride
2893 ) ) {
2894 throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2895 }
2896
2897 return $content;
2898 }
2899
2903 public function showEditForm() {
2904 # need to parse the preview early so that we know which templates are used,
2905 # otherwise users with "show preview after edit box" will get a blank list
2906 # we parse this near the beginning so that setHeaders can do the title
2907 # setting work instead of leaving it in getPreviewText
2908 $previewOutput = '';
2909 if ( $this->formtype === 'preview' ) {
2910 $previewOutput = $this->getPreviewText();
2911 }
2912
2913 $out = $this->context->getOutput();
2914
2915 // FlaggedRevs depends on running this hook before adding edit notices in showIntro() (T337637)
2916 $this->getHookRunner()->onEditPage__showEditForm_initial( $this, $out );
2917
2918 $this->setHeaders();
2919
2920 // Show applicable editing introductions
2921 $this->showIntro();
2922
2923 if ( !$this->isConflict &&
2924 $this->section !== '' &&
2925 !$this->isSectionEditSupported()
2926 ) {
2927 // We use $this->section to much before this and getVal('wgSection') directly in other places
2928 // at this point we can't reset $this->section to '' to fallback to non-section editing.
2929 // Someone is welcome to try refactoring though
2930 $out->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
2931 return;
2932 }
2933
2934 $this->showHeader();
2935
2936 $out->addHTML( $this->editFormPageTop );
2937
2938 $user = $this->context->getUser();
2939 if ( $this->userOptionsLookup->getOption( $user, 'previewontop' ) ) {
2940 $this->displayPreviewArea( $previewOutput, true );
2941 }
2942
2943 $out->addHTML( $this->editFormTextTop );
2944
2945 // @todo add EditForm plugin interface and use it here!
2946 // search for textarea1 and textarea2, and allow EditForm to override all uses.
2947 $out->addHTML( Html::openElement(
2948 'form',
2949 [
2950 'class' => 'mw-editform',
2951 'id' => self::EDITFORM_ID,
2952 'name' => self::EDITFORM_ID,
2953 'method' => 'post',
2954 'action' => $this->getActionURL( $this->getContextTitle() ),
2955 'enctype' => 'multipart/form-data',
2956 'data-mw-editform-type' => $this->formtype
2957 ]
2958 ) );
2959
2960 // Add a check for Unicode support
2961 $out->addHTML( Html::hidden( 'wpUnicodeCheck', self::UNICODE_CHECK ) );
2962
2963 // Add an empty field to trip up spambots
2964 $out->addHTML(
2965 Html::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
2966 . Html::rawElement(
2967 'label',
2968 [ 'for' => 'wpAntispam' ],
2969 $this->context->msg( 'simpleantispam-label' )->parse()
2970 )
2971 . Html::element(
2972 'input',
2973 [
2974 'type' => 'text',
2975 'name' => 'wpAntispam',
2976 'id' => 'wpAntispam',
2977 'value' => ''
2978 ]
2979 )
2980 . Html::closeElement( 'div' )
2981 );
2982
2983 $this->getHookRunner()->onEditPage__showEditForm_fields( $this, $out );
2984
2985 // Put these up at the top to ensure they aren't lost on early form submission
2986 $this->showFormBeforeText();
2987
2988 # When the summary is hidden, also hide them on preview/show changes
2989 if ( $this->nosummary ) {
2990 $out->addHTML( Html::hidden( 'nosummary', true ) );
2991 }
2992
2993 # If a blank edit summary was previously provided, and the appropriate
2994 # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
2995 # user being bounced back more than once in the event that a summary
2996 # is not required.
2997 # ####
2998 # For a bit more sophisticated detection of blank summaries, hash the
2999 # automatic one and pass that in the hidden field wpAutoSummary.
3000 if (
3001 $this->missingSummary ||
3002 ( $this->section === 'new' && $this->nosummary ) ||
3003 $this->allowBlankSummary
3004 ) {
3005 $out->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
3006 }
3007
3008 if ( $this->undidRev ) {
3009 $out->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
3010 }
3011 if ( $this->undoAfter ) {
3012 $out->addHTML( Html::hidden( 'wpUndoAfter', $this->undoAfter ) );
3013 }
3014 if ( $this->recreate ) {
3015 $out->addHTML( Html::hidden( 'wpRecreate', $this->recreate ) );
3016 }
3017 if ( $this->ignoreRevisionDeletedWarning ) {
3018 $out->addHTML( Html::hidden( 'wpIgnoreRevisionDeleted', $this->ignoreRevisionDeletedWarning ) );
3019 }
3020
3021 if ( $this->problematicRedirectTarget !== null ) {
3022 // T395767, T395768: Save the target to a variable so the constraint can fail again if the redirect is
3023 // still problematic but has changed between two save attempts
3024 $out->addHTML( Html::hidden(
3025 'wpAllowedProblematicRedirectTarget',
3026 $this->problematicRedirectTarget->getFullText()
3027 ) );
3028 }
3029
3030 $autosumm = $this->autoSumm !== '' ? $this->autoSumm : md5( $this->summary );
3031 $out->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
3032
3033 $out->addHTML( Html::hidden( 'oldid', $this->oldid ) );
3034 $out->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
3035
3036 $out->addHTML( Html::hidden( 'format', $this->contentFormat ) );
3037 $out->addHTML( Html::hidden( 'model', $this->contentModel ) );
3038 if ( $this->changeTags ) {
3039 $out->addHTML( Html::hidden( 'wpChangeTagsAfterPreview', implode( ',', $this->changeTags ) ) );
3040 }
3041
3042 $out->enableOOUI();
3043
3044 if ( $this->section === 'new' ) {
3045 $this->showSummaryInput( true );
3046 $out->addHTML( $this->getSummaryPreview( true ) );
3047 }
3048
3049 $out->addHTML( $this->editFormTextBeforeContent );
3050 if ( $this->isConflict ) {
3051 $currentText = $this->pageEditingHelper->toEditText(
3052 $this->getCurrentContent(), $this->contentFormat, $this->enableApiEditOverride
3053 ) ?? '';
3054
3055 $editConflictHelper = $this->getEditConflictHelper();
3056 $editConflictHelper->setTextboxes( $this->textbox1, $currentText );
3057 $editConflictHelper->setContentModel( $this->contentModel );
3058 $editConflictHelper->setContentFormat( $this->contentFormat );
3059 $out->addHTML( $editConflictHelper->getEditFormHtmlBeforeContent() );
3060
3061 $this->textbox2 = $this->textbox1;
3062 $this->textbox1 = $currentText;
3063 }
3064
3065 if ( !$this->getTitle()->isUserConfigPage() ) {
3066 $out->addHTML( self::getEditToolbar() );
3067 }
3068
3069 if ( $this->blankArticle ) {
3070 $out->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
3071 }
3072
3073 if ( $this->isConflict ) {
3074 // In an edit conflict bypass the overridable content form method
3075 // and fallback to the raw wpTextbox1 since editconflicts can't be
3076 // resolved between page source edits and custom ui edits using the
3077 // custom edit ui.
3078 $conflictTextBoxAttribs = [];
3079 if ( $this->isOldRev ) {
3080 $conflictTextBoxAttribs['class'] = 'mw-textarea-oldrev';
3081 }
3082
3083 // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable
3084 // $editConflictHelper is declard, when isConflict is true
3085 $out->addHTML( $editConflictHelper->getEditConflictMainTextBox( $conflictTextBoxAttribs ) );
3086 // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable
3087 // $editConflictHelper is declard, when isConflict is true
3088 $out->addHTML( $editConflictHelper->getEditFormHtmlAfterContent() );
3089 } else {
3090 $this->showContentForm();
3091 }
3092
3093 $out->addHTML( $this->editFormTextAfterContent );
3094
3095 $this->showStandardInputs();
3096
3097 $this->showFormAfterText();
3098
3099 $this->showTosSummary();
3100
3101 $this->showEditTools();
3102
3103 $out->addHTML( $this->editFormTextAfterTools . "\n" );
3104
3105 $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
3106
3107 $out->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
3108 Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
3109
3110 $out->addHTML( Html::rawElement( 'div', [ 'class' => 'limitreport' ],
3111 self::getPreviewLimitReport( $this->mParserOutput ) ) );
3112
3113 $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
3114
3115 if ( $this->isConflict ) {
3116 try {
3117 $this->showConflict();
3118 } catch ( MWContentSerializationException $ex ) {
3119 // this can't really happen, but be nice if it does.
3120 $out->addHTML( Html::errorBox(
3121 $this->context->msg(
3122 'content-failed-to-parse',
3123 $this->contentModel,
3124 $this->contentFormat,
3125 $ex->getMessage()
3126 )->parse()
3127 ) );
3128 }
3129 }
3130
3131 // Set a hidden field so JS knows what edit form mode we are in
3132 if ( $this->isConflict ) {
3133 $mode = 'conflict';
3134 } elseif ( $this->preview ) {
3135 $mode = 'preview';
3136 } elseif ( $this->diff ) {
3137 $mode = 'diff';
3138 } else {
3139 $mode = 'text';
3140 }
3141 $out->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
3142
3143 // Marker for detecting truncated form data. This must be the last
3144 // parameter sent in order to be of use, so do not move me.
3145 $out->addHTML( Html::hidden( 'wpUltimateParam', true ) );
3146 $out->addHTML( $this->editFormTextBottom . "\n</form>\n" );
3147
3148 if ( !$this->userOptionsLookup->getOption( $user, 'previewontop' ) ) {
3149 $this->displayPreviewArea( $previewOutput, false );
3150 }
3151 }
3152
3160 public function makeTemplatesOnThisPageList( array $templates ) {
3161 $templateListFormatter = new TemplatesOnThisPageFormatter(
3162 $this->context,
3163 $this->linkRenderer,
3164 $this->linkBatchFactory,
3165 $this->restrictionStore
3166 );
3167
3168 // preview if preview, else section if section, else false
3169 $type = false;
3170 if ( $this->preview ) {
3171 $type = 'preview';
3172 } elseif ( $this->section !== '' ) {
3173 $type = 'section';
3174 }
3175
3176 return Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
3177 $templateListFormatter->format( $templates, $type )
3178 );
3179 }
3180
3187 private static function extractSectionTitle( $text ) {
3188 if ( preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches ) ) {
3189 return MediaWikiServices::getInstance()->getParser()
3190 ->stripSectionName( trim( $matches[2] ) );
3191 } else {
3192 return false;
3193 }
3194 }
3195
3196 private function showHeader(): void {
3197 $out = $this->context->getOutput();
3198 if ( $this->isConflict ) {
3199 $this->addExplainConflictHeader();
3200 $this->editRevId = $this->page->getLatest();
3201 } else {
3202 if ( $this->section !== '' && $this->section !== 'new' && $this->summary === '' &&
3203 !$this->preview && !$this->diff
3204 ) {
3205 $sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object
3206 if ( $sectionTitle !== false && $sectionTitle !== '' ) {
3207 $this->summary = "/* $sectionTitle */ ";
3208 }
3209 }
3210
3211 if ( $this->hookError !== '' ) {
3212 $out->addWikiTextAsInterface( $this->hookError );
3213 }
3214
3215 if ( $this->section != 'new' ) {
3216 $revRecord = $this->mArticle->fetchRevisionRecord();
3217 if ( $revRecord instanceof RevisionStoreRecord && !$revRecord->isCurrent() ) {
3218 $this->mArticle->setOldSubtitle( $revRecord->getId() );
3219 $this->isOldRev = true;
3220 }
3221 }
3222 }
3223
3224 if ( $this->formtype !== 'save' ) {
3226 $constraintFactory = MediaWikiServices::getInstance()->getService( '_EditConstraintFactory' );
3227 $constraintRunner = new EditConstraintRunner(
3228 $constraintFactory->newAccidentalRecreationConstraint(
3229 $this->getTitle(),
3230 // Ignore wpRedirect so the warning is still shown after a save attempt
3231 false,
3232 $this->starttime,
3233 ),
3234 new RevisionDeletedConstraint(
3235 $this->mArticle,
3236 false,
3237 $this->oldid,
3238 $this->section,
3239 $this->getTitle(),
3240 $this->context->getUser(),
3241 ),
3242 );
3243
3244 // No call to $this->handleFailedConstraint() here to avoid setting wpRedirect
3245 $out->addHTML( $this->formatConstraintStatus( $constraintRunner->checkAllConstraints() ) );
3246 }
3247
3248 $this->addLongPageWarningHeader();
3249 }
3250
3258 private function getSummaryInputAttributes( array $inputAttrs ): array {
3259 // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
3260 // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
3261 // Unicode codepoints.
3262 return $inputAttrs + [
3263 'id' => 'wpSummary',
3264 'name' => 'wpSummary',
3265 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
3266 'tabindex' => 1,
3267 'size' => 60,
3268 'spellcheck' => 'true',
3269 ];
3270 }
3271
3281 private function getSummaryInputWidget( $summary, string $labelText, array $inputAttrs ): FieldLayout {
3282 $inputAttrs = OOUI\Element::configFromHtmlAttributes(
3283 $this->getSummaryInputAttributes( $inputAttrs )
3284 );
3285 $inputAttrs += [
3286 'title' => Linker::titleAttrib( 'summary' ),
3287 'accessKey' => Linker::accesskey( 'summary' ),
3288 ];
3289
3290 // For compatibility with old scripts and extensions, we want the legacy 'id' on the `<input>`
3291 $inputAttrs['inputId'] = $inputAttrs['id'];
3292 $inputAttrs['id'] = 'wpSummaryWidget';
3293
3294 return new OOUI\FieldLayout(
3295 new OOUI\TextInputWidget( [
3296 'value' => $summary,
3297 'infusable' => true,
3298 ] + $inputAttrs ),
3299 [
3300 'label' => new OOUI\HtmlSnippet( $labelText ),
3301 'align' => 'top',
3302 'id' => 'wpSummaryLabel',
3303 'classes' => [ $this->missingSummary ? 'mw-summarymissed' : 'mw-summary' ],
3304 ]
3305 );
3306 }
3307
3313 private function showSummaryInput( bool $isSubjectPreview ): void {
3314 # Add a class if 'missingsummary' is triggered to allow styling of the summary line
3315 $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
3316 if ( $isSubjectPreview ) {
3317 if ( $this->nosummary ) {
3318 return;
3319 }
3320 } elseif ( !$this->mShowSummaryField ) {
3321 return;
3322 }
3323
3324 $labelText = $this->context->msg( $isSubjectPreview ? 'subject' : 'summary' )->parse();
3325 $this->context->getOutput()->addHTML(
3326 $this->getSummaryInputWidget(
3327 $isSubjectPreview ? $this->sectiontitle : $this->summary,
3328 $labelText,
3329 [ 'class' => $summaryClass ]
3330 )->toString()
3331 );
3332 }
3333
3340 private function getSummaryPreview( bool $isSubjectPreview ): string {
3341 // avoid spaces in preview, gets always trimmed on save
3342 $summary = trim( $this->summary );
3343 if ( $summary === '' || ( !$this->preview && !$this->diff ) ) {
3344 return "";
3345 }
3346
3347 $commentFormatter = MediaWikiServices::getInstance()->getCommentFormatter();
3348 $summary = $this->context->msg( 'summary-preview' )->parse()
3349 . $commentFormatter->formatBlock( $summary, $this->getTitle(), $isSubjectPreview );
3350 return Html::rawElement( 'div', [ 'class' => 'mw-summary-preview' ], $summary );
3351 }
3352
3353 private function showFormBeforeText(): void {
3354 $out = $this->context->getOutput();
3355 $out->addHTML( Html::hidden( 'wpSection', $this->section ) );
3356 $out->addHTML( Html::hidden( 'wpStarttime', $this->starttime ) );
3357 $out->addHTML( Html::hidden( 'wpEdittime', $this->edittime ) );
3358 $out->addHTML( Html::hidden( 'editRevId', $this->editRevId ) );
3359 $out->addHTML( Html::hidden( 'wpScrolltop', $this->scrolltop, [ 'id' => 'wpScrolltop' ] ) );
3360 }
3361
3362 protected function showFormAfterText() {
3375 $this->context->getOutput()->addHTML(
3376 "\n" .
3377 Html::hidden( "wpEditToken", $this->context->getUser()->getEditToken() ) .
3378 "\n"
3379 );
3380 }
3381
3390 protected function showContentForm() {
3391 $this->showTextbox1();
3392 }
3393
3394 private function showTextbox1(): void {
3395 $builder = new TextboxBuilder();
3396 $classes = $builder->getTextboxProtectionCSSClasses( $this->getTitle() );
3397
3398 # Is an old revision being edited?
3399 if ( $this->isOldRev ) {
3400 $classes[] = 'mw-textarea-oldrev';
3401 }
3402
3403 $attribs = [
3404 'aria-label' => $this->context->msg( 'edit-textarea-aria-label' )->text(),
3405 'tabindex' => 1,
3406 'class' => $classes,
3407 ];
3408
3409 $this->showTextbox(
3410 $this->textbox1,
3411 'wpTextbox1',
3412 $attribs
3413 );
3414 }
3415
3416 protected function showTextbox( string $text, string $name, array $customAttribs = [] ) {
3417 $builder = new TextboxBuilder();
3418 $attribs = $builder->buildTextboxAttribs(
3419 $name,
3420 $customAttribs,
3421 $this->context->getUser(),
3422 $this->page
3423 );
3424
3425 $this->context->getOutput()->addHTML(
3426 Html::textarea( $name, $builder->addNewLineAtEnd( $text ), $attribs )
3427 );
3428 }
3429
3430 private function displayPreviewArea( string $previewOutput, bool $isOnTop ): void {
3431 $attribs = [ 'id' => 'wikiPreview' ];
3432 if ( $isOnTop ) {
3433 $attribs['class'] = 'ontop';
3434 }
3435 if ( $this->formtype !== 'preview' ) {
3436 $attribs['style'] = 'display: none;';
3437 }
3438
3439 $out = $this->context->getOutput();
3440 $out->addHTML( Html::openElement( 'div', $attribs ) );
3441
3442 if ( $this->formtype === 'preview' ) {
3443 $this->showPreview( $previewOutput );
3444 }
3445
3446 $out->addHTML( '</div>' );
3447
3448 if ( $this->formtype === 'diff' ) {
3449 try {
3450 $this->showDiff();
3451 } catch ( MWContentSerializationException $ex ) {
3452 $out->addHTML( Html::errorBox(
3453 $this->context->msg(
3454 'content-failed-to-parse',
3455 $this->contentModel,
3456 $this->contentFormat,
3457 $ex->getMessage()
3458 )->parse()
3459 ) );
3460 }
3461 }
3462 }
3463
3470 private function showPreview( string $text ): void {
3471 if ( $this->mArticle instanceof CategoryPage ) {
3472 $this->mArticle->openShowCategory();
3473 }
3474 # This hook seems slightly odd here, but makes things more
3475 # consistent for extensions.
3476 $out = $this->context->getOutput();
3477 $this->getHookRunner()->onOutputPageBeforeHTML( $out, $text );
3478 $out->addHTML( $text );
3479 if ( $this->mArticle instanceof CategoryPage ) {
3480 $this->mArticle->closeShowCategory();
3481 }
3482 }
3483
3491 public function showDiff() {
3492 $oldtitlemsg = 'currentrev';
3493 # if message does not exist, show diff against the preloaded default
3494 if ( $this->page->getNamespace() === NS_MEDIAWIKI && !$this->page->exists() ) {
3495 $oldtext = $this->getTitle()->getDefaultMessageText();
3496 if ( $oldtext !== false ) {
3497 $oldtitlemsg = 'defaultmessagetext';
3498 $oldContent = $this->toEditContent( $oldtext );
3499 } else {
3500 $oldContent = null;
3501 }
3502 } else {
3503 $oldContent = $this->getCurrentContent();
3504 }
3505
3506 $textboxContent = $this->toEditContent( $this->textbox1 );
3507 if ( $this->editRevId !== null ) {
3508 $newContent = $this->page->replaceSectionAtRev(
3509 $this->section, $textboxContent, $this->sectiontitle, $this->editRevId
3510 );
3511 } else {
3512 $newContent = $this->page->replaceSectionContent(
3513 $this->section, $textboxContent, $this->sectiontitle, $this->edittime
3514 );
3515 }
3516
3517 if ( $newContent ) {
3518 $this->getHookRunner()->onEditPageGetDiffContent( $this, $newContent );
3519
3520 $user = $this->getUserForPreview();
3521 $parserOptions = ParserOptions::newFromUserAndLang( $user,
3522 MediaWikiServices::getInstance()->getContentLanguage() );
3523 $services = MediaWikiServices::getInstance();
3524 $contentTransformer = $services->getContentTransformer();
3525 $newContent = $contentTransformer->preSaveTransform( $newContent, $this->page, $user, $parserOptions );
3526 }
3527
3528 if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
3529 $oldtitle = $this->context->msg( $oldtitlemsg )->parse();
3530 $newtitle = $this->context->msg( 'yourtext' )->parse();
3531
3532 if ( !$oldContent ) {
3533 $oldContent = $newContent->getContentHandler()->makeEmptyContent();
3534 }
3535
3536 if ( !$newContent ) {
3537 $newContent = $oldContent->getContentHandler()->makeEmptyContent();
3538 }
3539
3540 $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->context );
3541 $de->setContent( $oldContent, $newContent );
3542
3543 $difftext = $de->getDiff( $oldtitle, $newtitle );
3544 $de->showDiffStyle();
3545 } else {
3546 $difftext = '';
3547 }
3548
3549 $this->context->getOutput()->addHTML( Html::rawElement( 'div', [ 'id' => 'wikiDiff' ], $difftext ) );
3550 }
3551
3560 private function showTosSummary(): void {
3561 $msgKey = 'editpage-tos-summary';
3562 $this->getHookRunner()->onEditPageTosSummary( $this->getTitle(), $msgKey );
3563 $msg = $this->context->msg( $msgKey );
3564 if ( !$msg->isDisabled() ) {
3565 $this->context->getOutput()->addHTML( Html::rawElement(
3566 'div',
3567 [ 'class' => 'mw-tos-summary' ],
3568 $msg->parseAsBlock()
3569 ) );
3570 }
3571 }
3572
3577 private function showEditTools(): void {
3578 $this->context->getOutput()->addHTML( Html::rawElement(
3579 'div',
3580 [ 'class' => 'mw-editTools' ],
3581 $this->context->msg( 'edittools' )->inContentLanguage()->parse()
3582 ) );
3583 }
3584
3594 public static function getCopyrightWarning( PageReference $page, string $format, MessageLocalizer $localizer ) {
3595 $services = MediaWikiServices::getInstance();
3596 $rightsText = $services->getMainConfig()->get( MainConfigNames::RightsText );
3597 if ( $rightsText ) {
3598 $copywarnMsg = [ 'copyrightwarning',
3599 '[[' . $localizer->msg( 'copyrightpage' )->inContentLanguage()->text() . ']]',
3600 $rightsText ];
3601 } else {
3602 $copywarnMsg = [ 'copyrightwarning2',
3603 '[[' . $localizer->msg( 'copyrightpage' )->inContentLanguage()->text() . ']]' ];
3604 }
3605 // Allow for site and per-namespace customization of contribution/copyright notice.
3606 $title = Title::newFromPageReference( $page );
3607 ( new HookRunner( $services->getHookContainer() ) )->onEditPageCopyrightWarning( $title, $copywarnMsg );
3608 if ( !$copywarnMsg ) {
3609 return '';
3610 }
3611
3612 $msg = $localizer->msg( ...$copywarnMsg )->page( $page );
3613 return Html::rawElement( 'div', [ 'id' => 'editpage-copywarn' ], $msg->$format() );
3614 }
3615
3623 public static function getPreviewLimitReport( ?ParserOutput $output = null ) {
3624 if ( !$output || !$output->getLimitReportData() ) {
3625 return '';
3626 }
3627
3628 $limitReport = Html::rawElement( 'div', [ 'class' => 'mw-limitReportExplanation' ],
3629 wfMessage( 'limitreport-title' )->parseAsBlock()
3630 );
3631
3632 // Show/hide animation doesn't work correctly on a table, so wrap it in a div.
3633 $limitReport .= Html::openElement( 'div', [ 'class' => 'preview-limit-report-wrapper' ] );
3634
3635 $limitReport .= Html::openElement( 'table', [
3636 'class' => 'preview-limit-report wikitable'
3637 ] ) .
3638 Html::openElement( 'tbody' );
3639
3640 $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
3641 foreach ( $output->getLimitReportData() as $key => $value ) {
3642 if ( in_array( $key, [
3643 'cachereport-origin',
3644 'cachereport-timestamp',
3645 'cachereport-ttl',
3646 'cachereport-transientcontent',
3647 'limitreport-timingprofile',
3648 ] ) ) {
3649 // These entries have non-numeric parameters, and can't be displayed by this code.
3650 // They are used by the plaintext limit report (see RenderDebugInfo::debugInfo()).
3651 // TODO: Display this information in the table somehow.
3652 continue;
3653 }
3654
3655 if ( $hookRunner->onParserLimitReportFormat( $key, $value, $limitReport, true, true ) ) {
3656 $keyMsg = wfMessage( $key );
3657 $valueMsg = wfMessage( "$key-value" );
3658 if ( !$valueMsg->exists() ) {
3659 // This is formatted raw, not as localized number.
3660 // If you want the parameter formatted as a number,
3661 // define the `$key-value` message.
3662 $valueMsg = ( new RawMessage( '$1' ) )->params( $value );
3663 } else {
3664 // If you define the `$key-value` or `$key-value-html`
3665 // message then the argument *must* be numeric.
3666 $valueMsg = $valueMsg->numParams( $value );
3667 }
3668 if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
3669 $limitReport .= Html::rawElement( 'tr', [],
3670 Html::rawElement( 'th', [], $keyMsg->parse() ) .
3671 Html::rawElement( 'td', [], $valueMsg->parse() )
3672 );
3673 }
3674 }
3675 }
3676
3677 $limitReport .= Html::closeElement( 'tbody' ) .
3678 Html::closeElement( 'table' ) .
3679 Html::closeElement( 'div' );
3680
3681 return $limitReport;
3682 }
3683
3684 protected function showStandardInputs( int &$tabindex = 2 ) {
3685 $out = $this->context->getOutput();
3686 $out->addHTML( "<div class='editOptions'>\n" );
3687
3688 if ( $this->section !== 'new' ) {
3689 $this->showSummaryInput( false );
3690 $out->addHTML( $this->getSummaryPreview( false ) );
3691 }
3692
3693 // When previewing, override the selected dropdown option to select whatever was posted
3694 // (if it's a valid option) rather than the current value for watchlistExpiry.
3695 // See also above in $this->importFormDataPosted().
3696 $expiryFromRequest = null;
3697 if ( $this->preview || $this->diff || $this->isConflict ) {
3698 $expiryFromRequest = $this->getContext()->getRequest()->getText( 'wpWatchlistExpiry' );
3699 }
3700
3701 $checkboxes = $this->getCheckboxesWidget(
3702 $tabindex,
3703 [ 'minor' => $this->minoredit, 'watch' => $this->watchthis, 'wpWatchlistExpiry' => $expiryFromRequest ]
3704 );
3705 $checkboxesHTML = new OOUI\HorizontalLayout( [ 'items' => array_values( $checkboxes ) ] );
3706
3707 $out->addHTML( "<div class='editCheckboxes'>" . $checkboxesHTML . "</div>\n" );
3708
3709 // Show copyright warning.
3710 $out->addHTML( self::getCopyrightWarning( $this->page, 'parse', $this->context ) );
3711 $out->addHTML( $this->editFormTextAfterWarn );
3712
3713 $out->addHTML( "<div class='editButtons'>\n" );
3714 $out->addHTML( implode( "\n", $this->getEditButtons( $tabindex ) ) . "\n" );
3715
3716 $cancel = $this->getCancelLink( $tabindex++ );
3717
3718 $edithelp = $this->getHelpLink() .
3719 $this->context->msg( 'word-separator' )->escaped() .
3720 $this->context->msg( 'newwindow' )->parse();
3721
3722 $out->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" );
3723 $out->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" );
3724 $out->addHTML( "</div><!-- editButtons -->\n" );
3725
3726 $this->getHookRunner()->onEditPage__showStandardInputs_options( $this, $out, $tabindex );
3727
3728 $out->addHTML( "</div><!-- editOptions -->\n" );
3729 }
3730
3735 private function showConflict(): void {
3736 $out = $this->context->getOutput();
3737 if ( $this->getHookRunner()->onEditPageBeforeConflictDiff( $this, $out ) ) {
3738 $this->incrementConflictStats();
3739
3740 $this->getEditConflictHelper()->showEditFormTextAfterFooters();
3741 }
3742 }
3743
3744 private function incrementConflictStats(): void {
3745 $this->getEditConflictHelper()->incrementConflictStats( $this->context->getUser() );
3746 }
3747
3748 private function getHelpLink(): string {
3749 $message = $this->context->msg( 'edithelppage' )->inContentLanguage()->text();
3750 $editHelpUrl = Skin::makeInternalOrExternalUrl( $message );
3751 return Html::element( 'a', [
3752 'href' => $editHelpUrl,
3753 'target' => 'helpwindow'
3754 ], $this->context->msg( 'edithelp' )->text() );
3755 }
3756
3761 private function getCancelLink( int $tabindex ): ButtonWidget {
3762 $cancelParams = [];
3763 if ( !$this->isConflict && $this->oldid > 0 ) {
3764 $cancelParams['oldid'] = $this->oldid;
3765 } elseif ( $this->getContextTitle()->isRedirect() ) {
3766 $cancelParams['redirect'] = 'no';
3767 }
3768
3769 return new OOUI\ButtonWidget( [
3770 'id' => 'mw-editform-cancel',
3771 'tabIndex' => $tabindex,
3772 'href' => $this->getContextTitle()->getLinkURL( $cancelParams ),
3773 'label' => new OOUI\HtmlSnippet( $this->context->msg( 'cancel' )->parse() ),
3774 'framed' => false,
3775 'infusable' => true,
3776 'flags' => 'destructive',
3777 ] );
3778 }
3779
3789 protected function getActionURL( Title $title ) {
3790 $request = $this->context->getRequest();
3791 $params = $request->getQueryValuesOnly();
3792
3793 $allowedFormParams = [
3794 'section', 'oldid', 'preloadtitle', 'undo', 'undoafter',
3795 // Considered safe in all contexts
3796 'uselang', 'useskin', 'useformat', 'variant', 'debug', 'safemode'
3797 ];
3798 $formParams = [ 'action' => $this->action ];
3799 foreach ( $params as $arg => $val ) {
3800 if ( in_array( $arg, $allowedFormParams, true ) ) {
3801 $formParams[$arg] = $val;
3802 }
3803 }
3804
3805 return $title->getLocalURL( $formParams );
3806 }
3807
3813 public function getPreviewText() {
3814 $out = $this->context->getOutput();
3815 $config = $this->context->getConfig();
3816
3817 if ( $config->get( MainConfigNames::RawHtml ) && !$this->mTokenOk ) {
3818 // Could be an offsite preview attempt. This is very unsafe if
3819 // HTML is enabled, as it could be an attack.
3820 $parsedNote = '';
3821 if ( $this->textbox1 !== '' ) {
3822 // Do not put big scary notice, if previewing the empty
3823 // string, which happens when you initially edit
3824 // a category page, due to automatic preview-on-open.
3825 $parsedNote = Html::rawElement( 'div', [ 'class' => 'previewnote' ],
3826 $out->parseAsInterface(
3827 $this->context->msg( 'session_fail_preview_html' )->plain()
3828 ) );
3829 }
3830 $this->incrementEditFailureStats( 'session_loss' );
3831 return $parsedNote;
3832 }
3833
3834 $previewStatus = StatusValue::newGood();
3835 $previewNoteHtml = '';
3836
3837 try {
3838 $content = $this->toEditContent( $this->textbox1 );
3839
3840 $previewHTML = '';
3841 if ( !$this->getHookRunner()->onAlternateEditPreview(
3842 $this, $content, $previewHTML, $this->mParserOutput )
3843 ) {
3844 return $previewHTML;
3845 }
3846
3847 $continueEditingHtml = Html::rawElement(
3848 'span',
3849 [ 'class' => 'mw-continue-editing' ],
3850 $this->linkRenderer->makePreloadedLink(
3851 new TitleValue( NS_MAIN, '', self::EDITFORM_ID ),
3852 $this->context->getLanguage()->getArrow() . ' ' . $this->context->msg( 'continue-editing' )->text()
3853 )
3854 );
3855
3856 if ( $this->mTriedSave && !$this->mTokenOk ) {
3857 $previewStatus->fatal( 'session_fail_preview' );
3858 $this->incrementEditFailureStats( 'session_loss' );
3859 } elseif ( $this->incompleteForm ) {
3860 $previewStatus->fatal( 'edit_form_incomplete' );
3861 if ( $this->mTriedSave ) {
3862 $this->incrementEditFailureStats( 'incomplete_form' );
3863 }
3864 } else {
3865 $previewNoteHtml = Html::noticeBox(
3866 $this->context->msg( 'previewnote' )->parse() . ' ' . $continueEditingHtml
3867 );
3868 }
3869
3870 # don't parse non-wikitext pages, show message about preview
3871 if ( $this->getTitle()->isUserConfigPage() || $this->getTitle()->isSiteConfigPage() ) {
3872 if ( $this->getTitle()->isUserConfigPage() ) {
3873 $level = 'user';
3874 } elseif ( $this->getTitle()->isSiteConfigPage() ) {
3875 $level = 'site';
3876 } else {
3877 $level = false;
3878 }
3879
3880 if ( $content->getModel() === CONTENT_MODEL_CSS ) {
3881 $format = 'css';
3882 if ( $level === 'user' && !$config->get( MainConfigNames::AllowUserCss ) ) {
3883 $format = false;
3884 }
3885 } elseif ( $content->getModel() === CONTENT_MODEL_JSON ) {
3886 $format = 'json';
3887 if ( $level === 'user' /* No comparable 'AllowUserJson' */ ) {
3888 $format = false;
3889 }
3890 } elseif ( $content->getModel() === CONTENT_MODEL_JAVASCRIPT ) {
3891 $format = 'js';
3892 if ( $level === 'user' && !$config->get( MainConfigNames::AllowUserJs ) ) {
3893 $format = false;
3894 }
3895 } elseif ( $content->getModel() === CONTENT_MODEL_VUE ) {
3896 $format = 'vue';
3897 if ( $level === 'user' && !$config->get( MainConfigNames::AllowUserJs ) ) {
3898 $format = false;
3899 }
3900 } else {
3901 $format = false;
3902 }
3903
3904 # Used messages to make sure grep find them:
3905 # Messages: usercsspreview, userjsonpreview, userjspreview,
3906 # sitecsspreview, sitejsonpreview, sitejspreview
3907 if ( $level && $format ) {
3908 $previewNoteHtml = Html::noticeBox( Html::rawElement(
3909 'div',
3910 [ 'id' => "mw-{$level}{$format}preview" ],
3911 $this->context->msg( "{$level}{$format}preview" )->parse() . $continueEditingHtml
3912 ) );
3913 }
3914 }
3915
3916 if ( $this->section === "new" ) {
3917 $content = $content->addSectionHeader( $this->sectiontitle );
3918 }
3919
3920 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
3921 $this->getHookRunner()->onEditPageGetPreviewContent( $this, $content );
3922
3923 $parserResult = $this->doPreviewParse( $content );
3924 $parserOutput = $parserResult['parserOutput'];
3925 $previewHTML = $parserResult['html'];
3926 $this->mParserOutput = $parserOutput;
3927 $out->addParserOutputMetadata( $parserOutput );
3928 if ( $out->userCanPreview() ) {
3929 $out->addContentOverride( $this->getTitle(), $content );
3930 }
3931
3932 // T394016 - Run some edit constraints on page preview
3934 $constraintFactory = MediaWikiServices::getInstance()->getService( '_EditConstraintFactory' );
3935 $constraintRunner = new EditConstraintRunner();
3936
3937 $constraintRunner->addConstraint( $constraintFactory->newRedirectConstraint(
3938 null,
3939 $content,
3940 null,
3941 $this->getTitle(),
3942 MessageValue::new( 'edit-constraint-warning-wrapper' ),
3943 $this->contentFormat,
3944 ) );
3945
3946 $previewStatus->merge( $constraintRunner->checkAllConstraints() );
3947
3948 foreach ( $parserOutput->getWarningMsgs() as $warning ) {
3949 $previewStatus->warning( $warning );
3950 }
3951 } catch ( MWContentSerializationException $ex ) {
3952 $previewStatus->fatal(
3953 'content-failed-to-parse',
3954 $this->contentModel,
3955 $this->contentFormat,
3956 $ex->getMessage()
3957 );
3958 $previewHTML = '';
3959 }
3960
3961 if ( $this->isConflict ) {
3962 $conflict = Html::warningBox(
3963 $this->context->msg( 'previewconflict' )->escaped(),
3964 'mw-previewconflict'
3965 );
3966 } else {
3967 $conflict = '';
3968 }
3969
3970 $previewhead = Html::rawElement(
3971 'div', [ 'class' => 'previewnote' ],
3972 Html::element(
3973 'h2', [ 'id' => 'mw-previewheader' ],
3974 $this->context->msg( 'preview' )->text()
3975 ) . $this->formatConstraintStatus( $previewStatus ) . $previewNoteHtml . $conflict
3976 );
3977
3978 return $previewhead . $previewHTML . $this->previewTextAfterContent;
3979 }
3980
3981 private function incrementEditFailureStats( string $failureType ): void {
3982 MediaWikiServices::getInstance()->getStatsFactory()
3983 ->getCounter( 'edit_failure_total' )
3984 ->setLabel( 'cause', $failureType )
3985 ->setLabel( 'namespace', 'n/a' )
3986 ->setLabel( 'user_bucket', 'n/a' )
3987 ->increment();
3988 }
3989
3994 protected function getPreviewParserOptions() {
3995 $parserOptions = $this->page->makeParserOptions( $this->context );
3996 $parserOptions->setRenderReason( 'page-preview' );
3997 $parserOptions->setIsPreview( true );
3998 $parserOptions->setIsSectionPreview( $this->section !== '' );
3999 $parserOptions->setSuppressSectionEditLinks();
4000
4001 // XXX: we could call $parserOptions->setCurrentRevisionRecordCallback here to force the
4002 // current revision to be null during PST, until setupFakeRevision is called on
4003 // the ParserOptions. Currently, we rely on Parser::getRevisionRecordObject() to ignore
4004 // existing revisions in preview mode.
4005
4006 return $parserOptions;
4007 }
4008
4018 protected function doPreviewParse( Content $content ) {
4019 $user = $this->getUserForPreview();
4020 $parserOptions = $this->getPreviewParserOptions();
4021
4022 // NOTE: preSaveTransform doesn't have a fake revision to operate on.
4023 // Parser::getRevisionRecordObject() will return null in preview mode,
4024 // causing the context user to be used for {{subst:REVISIONUSER}}.
4025 // XXX: Alternatively, we could also call setupFakeRevision()
4026 // before PST with $content.
4027 $services = MediaWikiServices::getInstance();
4028 $contentTransformer = $services->getContentTransformer();
4029 $contentRenderer = $services->getContentRenderer();
4030 $pstContent = $contentTransformer->preSaveTransform( $content, $this->page, $user, $parserOptions );
4031 $parserOutput = $contentRenderer->getParserOutput( $pstContent, $this->page, null, $parserOptions );
4032 $out = $this->context->getOutput();
4033 $skin = $out->getSkin();
4034 $skinOptions = $skin->getOptions();
4035 // TODO T371004 move runOutputPipeline out of $parserOutput
4036 // TODO T371022 ideally we clone here, but for now let's reproduce getText behaviour
4037 $oldHtml = $parserOutput->getRawText();
4038 $html = $parserOutput->runOutputPipeline( $parserOptions, [
4039 'allowClone' => 'false',
4040 'userLang' => $skin->getLanguage(),
4041 'injectTOC' => $skinOptions['toc'],
4042 'includeDebugInfo' => true,
4043 ] )->getContentHolderText();
4044 $parserOutput->setRawText( $oldHtml );
4045 return [
4046 'parserOutput' => $parserOutput,
4047 'html' => $html
4048 ];
4049 }
4050
4054 public function getTemplates() {
4055 if ( $this->preview || $this->section !== '' ) {
4056 $templates = [];
4057 if ( !$this->mParserOutput ) {
4058 return $templates;
4059 }
4060 foreach (
4061 $this->mParserOutput->getLinkList( ParserOutputLinkTypes::TEMPLATE )
4062 as [ 'link' => $link ]
4063 ) {
4064 $templates[] = Title::newFromLinkTarget( $link );
4065 }
4066 return $templates;
4067 } else {
4068 return $this->getTitle()->getTemplateLinksFrom();
4069 }
4070 }
4071
4077 public static function getEditToolbar() {
4078 $startingToolbar = '<div id="toolbar"></div>';
4079 $toolbar = $startingToolbar;
4080
4081 $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
4082 if ( !$hookRunner->onEditPageBeforeEditToolbar( $toolbar ) ) {
4083 return null;
4084 }
4085 // Don't add a pointless `<div>` to the page unless a hook caller populated it
4086 return ( $toolbar === $startingToolbar ) ? null : $toolbar;
4087 }
4088
4114 public function getCheckboxesDefinition( $values ) {
4115 $checkboxes = [];
4116
4117 $user = $this->context->getUser();
4118 // don't show the minor edit checkbox if it's a new page or section
4119 if ( !$this->isNew && $this->permManager->userHasRight( $user, 'minoredit' ) ) {
4120 $checkboxes['wpMinoredit'] = [
4121 'id' => 'wpMinoredit',
4122 'label-message' => 'minoredit',
4123 // Uses messages: tooltip-minoredit, accesskey-minoredit
4124 'tooltip' => 'minoredit',
4125 'label-id' => 'mw-editpage-minoredit',
4126 'legacy-name' => 'minor',
4127 'default' => $values['minor'],
4128 ];
4129 }
4130
4131 if ( $user->isNamed() ) {
4132 $checkboxes = array_merge(
4133 $checkboxes,
4134 $this->getCheckboxesDefinitionForWatchlist( $values['watch'], $values['wpWatchlistExpiry'] ?? null )
4135 );
4136 }
4137
4138 $this->getHookRunner()->onEditPageGetCheckboxesDefinition( $this, $checkboxes );
4139
4140 return $checkboxes;
4141 }
4142
4150 private function getCheckboxesDefinitionForWatchlist( $watch, $watchexpiry ): array {
4151 $fieldDefs = [
4152 'wpWatchthis' => [
4153 'id' => 'wpWatchthis',
4154 'label-message' => 'watchthis',
4155 // Uses messages: tooltip-watch, accesskey-watch
4156 'tooltip' => 'watch',
4157 'label-id' => 'mw-editpage-watch',
4158 'legacy-name' => 'watch',
4159 'default' => $watch,
4160 ]
4161 ];
4162 if ( $this->watchlistExpiryEnabled ) {
4163 $watchedItem = $this->watchedItemStore->getWatchedItem( $this->getContext()->getUser(), $this->getTitle() );
4164 if ( $watchedItem instanceof WatchedItem && $watchedItem->getExpiry() === null ) {
4165 // Not temporarily watched, so we always default to infinite.
4166 $userPreferredExpiry = 'infinite';
4167 } else {
4168 $userPreferredExpiryOption = !$this->getTitle()->exists()
4169 ? 'watchcreations-expiry'
4170 : 'watchdefault-expiry';
4171 $userPreferredExpiry = $this->userOptionsLookup->getOption(
4172 $this->getContext()->getUser(),
4173 $userPreferredExpiryOption,
4174 'infinite'
4175 );
4176 }
4177
4178 $expiryOptions = WatchAction::getExpiryOptions(
4179 $this->getContext(),
4180 $watchedItem,
4181 $userPreferredExpiry
4182 );
4183
4184 if ( $watchexpiry && in_array( $watchexpiry, $expiryOptions['options'] ) ) {
4185 $expiryOptions['default'] = $watchexpiry;
4186 }
4187 // When previewing, override the selected dropdown option to select whatever was posted
4188 // (if it's a valid option) rather than the current value for watchlistExpiry.
4189 // See also above in $this->importFormDataPosted().
4190 $expiryFromRequest = $this->getContext()->getRequest()->getText( 'wpWatchlistExpiry' );
4191 if ( ( $this->preview || $this->diff ) && in_array( $expiryFromRequest, $expiryOptions['options'] ) ) {
4192 $expiryOptions['default'] = $expiryFromRequest;
4193 }
4194
4195 // Reformat the options to match what DropdownInputWidget wants.
4196 $options = [];
4197 foreach ( $expiryOptions['options'] as $label => $value ) {
4198 $options[] = [ 'data' => $value, 'label' => $label ];
4199 }
4200
4201 $fieldDefs['wpWatchlistExpiry'] = [
4202 'id' => 'wpWatchlistExpiry',
4203 'label-message' => 'confirm-watch-label',
4204 // Uses messages: tooltip-watchlist-expiry, accesskey-watchlist-expiry
4205 'tooltip' => 'watchlist-expiry',
4206 'label-id' => 'mw-editpage-watchlist-expiry',
4207 'default' => $expiryOptions['default'],
4208 'value-attr' => 'value',
4209 'class' => DropdownInputWidget::class,
4210 'options' => $options,
4211 'invisibleLabel' => true,
4212 ];
4213 }
4214 return $fieldDefs;
4215 }
4216
4227 public function getCheckboxesWidget( &$tabindex, $values ) {
4228 $checkboxes = [];
4229 $checkboxesDef = $this->getCheckboxesDefinition( $values );
4230
4231 foreach ( $checkboxesDef as $name => $options ) {
4232 $legacyName = $options['legacy-name'] ?? $name;
4233
4234 $title = null;
4235 $accesskey = null;
4236 if ( isset( $options['tooltip'] ) ) {
4237 $accesskey = $this->context->msg( "accesskey-{$options['tooltip']}" )->text();
4238 $title = Linker::titleAttrib( $options['tooltip'] );
4239 }
4240 if ( isset( $options['title-message'] ) ) {
4241 $title = $this->context->msg( $options['title-message'] )->text();
4242 }
4243 // Allow checkbox definitions to set their own class and value-attribute names.
4244 // See $this->getCheckboxesDefinition() for details.
4245 $className = $options['class'] ?? CheckboxInputWidget::class;
4246 $valueAttr = $options['value-attr'] ?? 'selected';
4247 $checkboxes[ $legacyName ] = new FieldLayout(
4248 new $className( [
4249 'tabIndex' => ++$tabindex,
4250 'accessKey' => $accesskey,
4251 'id' => $options['id'] . 'Widget',
4252 'inputId' => $options['id'],
4253 'name' => $name,
4254 $valueAttr => $options['default'],
4255 'infusable' => true,
4256 'options' => $options['options'] ?? null,
4257 ] ),
4258 [
4259 'align' => 'inline',
4260 'label' => new OOUI\HtmlSnippet( $this->context->msg( $options['label-message'] )->parse() ),
4261 'title' => $title,
4262 'id' => $options['label-id'] ?? null,
4263 'invisibleLabel' => $options['invisibleLabel'] ?? null,
4264 ]
4265 );
4266 }
4267
4268 return $checkboxes;
4269 }
4270
4274 private function getSubmitButtonLabel(): string {
4275 $labelAsPublish =
4276 $this->context->getConfig()->get( MainConfigNames::EditSubmitButtonLabelPublish );
4277
4278 // Can't use $this->isNew as that's also true if we're adding a new section to an extant page
4279 $newPage = !$this->page->exists();
4280
4281 if ( $labelAsPublish ) {
4282 $buttonLabelKey = $newPage ? 'publishpage' : 'publishchanges';
4283 } else {
4284 $buttonLabelKey = $newPage ? 'savearticle' : 'savechanges';
4285 }
4286
4287 return $buttonLabelKey;
4288 }
4289
4300 public function getEditButtons( &$tabindex ) {
4301 $buttons = [];
4302
4303 $labelAsPublish =
4304 $this->context->getConfig()->get( MainConfigNames::EditSubmitButtonLabelPublish );
4305
4306 $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
4307 $buttonTooltip = $labelAsPublish ? 'publish' : 'save';
4308
4309 $buttons['save'] = new OOUI\ButtonInputWidget( [
4310 'name' => 'wpSave',
4311 'tabIndex' => ++$tabindex,
4312 'id' => 'wpSaveWidget',
4313 'inputId' => 'wpSave',
4314 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4315 'useInputTag' => true,
4316 'flags' => [ 'progressive', 'primary' ],
4317 'label' => $buttonLabel,
4318 'infusable' => true,
4319 'type' => 'submit',
4320 // Messages used: tooltip-save, tooltip-publish
4321 'title' => Linker::titleAttrib( $buttonTooltip ),
4322 // Messages used: accesskey-save, accesskey-publish
4323 'accessKey' => Linker::accesskey( $buttonTooltip ),
4324 ] );
4325
4326 $buttons['preview'] = new OOUI\ButtonInputWidget( [
4327 'name' => 'wpPreview',
4328 'tabIndex' => ++$tabindex,
4329 'id' => 'wpPreviewWidget',
4330 'inputId' => 'wpPreview',
4331 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4332 'useInputTag' => true,
4333 'label' => $this->context->msg( 'showpreview' )->text(),
4334 'infusable' => true,
4335 'type' => 'submit',
4336 // Allow previewing even when the form is in invalid state (T343585)
4337 'formNoValidate' => true,
4338 // Message used: tooltip-preview
4339 'title' => Linker::titleAttrib( 'preview' ),
4340 // Message used: accesskey-preview
4341 'accessKey' => Linker::accesskey( 'preview' ),
4342 ] );
4343
4344 $buttons['diff'] = new OOUI\ButtonInputWidget( [
4345 'name' => 'wpDiff',
4346 'tabIndex' => ++$tabindex,
4347 'id' => 'wpDiffWidget',
4348 'inputId' => 'wpDiff',
4349 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4350 'useInputTag' => true,
4351 'label' => $this->context->msg( 'showdiff' )->text(),
4352 'infusable' => true,
4353 'type' => 'submit',
4354 // Allow previewing even when the form is in invalid state (T343585)
4355 'formNoValidate' => true,
4356 // Message used: tooltip-diff
4357 'title' => Linker::titleAttrib( 'diff' ),
4358 // Message used: accesskey-diff
4359 'accessKey' => Linker::accesskey( 'diff' ),
4360 ] );
4361
4362 $this->getHookRunner()->onEditPageBeforeEditButtons( $this, $buttons, $tabindex );
4363
4364 return $buttons;
4365 }
4366
4371 private function noSuchSectionPage(): void {
4372 $out = $this->context->getOutput();
4373 $out->prepareErrorPage();
4374 $out->setPageTitleMsg( $this->context->msg( 'nosuchsectiontitle' ) );
4375
4376 $res = $this->context->msg( 'nosuchsectiontext', $this->section )->parseAsBlock();
4377
4378 $this->getHookRunner()->onEditPageNoSuchSection( $this, $res );
4379 $out->addHTML( $res );
4380
4381 $out->returnToMain( false, $this->page );
4382 }
4383
4389 public function spamPageWithContent( $match = false ) {
4390 $this->textbox2 = $this->textbox1;
4391
4392 $out = $this->context->getOutput();
4393 $out->prepareErrorPage();
4394 $out->setPageTitleMsg( $this->context->msg( 'spamprotectiontitle' ) );
4395
4396 $spamText = $this->context->msg( 'spamprotectiontext' )->parseAsBlock();
4397
4398 if ( $match ) {
4399 if ( is_array( $match ) ) {
4400 // Do not use `wfEscapeWikiText( ... )` here for compatibility with PHP <8.1.4
4401 // https://gerrit.wikimedia.org/r/c/mediawiki/core/+/1160800/comment/92e67687_ab221188/
4402 $matchText = $this->context->getLanguage()->listToText( array_map( 'wfEscapeWikiText', $match ) );
4403 } else {
4404 $matchText = wfEscapeWikiText( $match );
4405 }
4406
4407 $spamText .= $this->context->msg( 'spamprotectionmatch', $matchText )->parseAsBlock();
4408 }
4409 $out->addHTML( Html::rawElement(
4410 'div',
4411 [ 'id' => 'spamprotected' ],
4412 $spamText
4413 ) );
4414
4415 $out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
4416 $this->showDiff();
4417
4418 $out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
4419 $this->showTextbox( $this->textbox2, 'wpTextbox2', [ 'tabindex' => 6, 'readonly' ] );
4420
4421 $out->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
4422 }
4423
4424 private function addLongPageWarningHeader(): void {
4425 if ( $this->contentLength === false ) {
4426 $this->contentLength = strlen( $this->textbox1 );
4427 }
4428
4429 $out = $this->context->getOutput();
4430 $longPageHint = $this->context->msg( 'longpage-hint' );
4431 if ( !$longPageHint->isDisabled() ) {
4432 $msgText = trim( $longPageHint->sizeParams( $this->contentLength )
4433 ->params( $this->contentLength ) // Keep this unformatted for math inside message
4434 ->parse() );
4435 if ( $msgText !== '' && $msgText !== '-' ) {
4436 $out->addHTML( "<div id='mw-edit-longpage-hint'>\n$msgText\n</div>" );
4437 }
4438 }
4439 }
4440
4441 private function addExplainConflictHeader(): void {
4442 $this->context->getOutput()->addHTML(
4443 $this->getEditConflictHelper()->getExplainHeader()
4444 );
4445 }
4446
4451 public function setEditConflictHelperFactory( callable $factory ) {
4452 Assert::precondition( !$this->editConflictHelperFactory,
4453 'Can only have one extension that resolves edit conflicts' );
4454 $this->editConflictHelperFactory = $factory;
4455 }
4456
4457 private function getEditConflictHelper(): TextConflictHelper {
4458 if ( !$this->editConflictHelper ) {
4459 $label = $this->getSubmitButtonLabel();
4460 if ( $this->editConflictHelperFactory ) {
4461 $this->editConflictHelper = ( $this->editConflictHelperFactory )( $label );
4462 } else {
4463 $this->editConflictHelper = new TextConflictHelper(
4464 $this->getTitle(),
4465 $this->getContext()->getOutput(),
4466 MediaWikiServices::getInstance()->getStatsFactory(),
4467 $label,
4468 MediaWikiServices::getInstance()->getContentHandlerFactory()
4469 );
4470 }
4471 }
4472 return $this->editConflictHelper;
4473 }
4474}
const EDIT_FORCE_BOT
Mark the edit a "bot" edit regardless of user rights.
Definition Defines.php:129
const CONTENT_MODEL_VUE
Definition Defines.php:241
const EDIT_UPDATE
Article is assumed to be pre-existing, fail if it doesn't exist.
Definition Defines.php:117
const NS_USER
Definition Defines.php:53
const CONTENT_MODEL_CSS
Definition Defines.php:237
const NS_MAIN
Definition Defines.php:51
const NS_MEDIAWIKI
Definition Defines.php:59
const CONTENT_MODEL_JSON
Definition Defines.php:239
const NS_USER_TALK
Definition Defines.php:54
const EDIT_MINOR
Mark this edit minor, if the user is allowed to do so.
Definition Defines.php:120
const CONTENT_MODEL_JAVASCRIPT
Definition Defines.php:236
const EDIT_AUTOSUMMARY
Fill in blank summaries with generated text where possible.
Definition Defines.php:135
const EDIT_NEW
Article is assumed to be non-existent, fail if it exists.
Definition Defines.php:114
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.
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:69
Page addition to a user's watchlist.
AuthManager is the authentication system in MediaWiki and serves entry point for authentication.
Value object for a comment stored by CommentStore.
Handle database storage of comments such as edit summaries and log reasons.
Base class for content handling.
Content object implementation for representing flat text.
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).
Verify user permissions if changing content model: Must have editcontentmodel rights Must be able to ...
Don't save a new page if it's blank or if it's a MediaWiki: message with content equivalent to defaul...
Constraints reflect possible errors that need to be checked.
newRedirectConstraint(?Title $allowedProblematicRedirectTarget, Content $newContent, ?Content $originalContent, LinkTarget $title, MessageValue $errorMessageWrapper, ?string $contentFormat,)
newAccidentalRecreationConstraint(Title $title, bool $allowRecreation, ?string $startTime, ?string $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...
Verify user permissions: If creating a redirect in the file namespace, must have upload rights.
Do not allow the user to post an empty comment (only used for new section)
For a new section, do not allow the user to post with an empty subject (section title) unless they ch...
Verify the page isn't larger than the maximum.
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.
Status returned by edit constraints and other page editing checks.
The HTML user interface for page editing.
Definition EditPage.php:135
showEditForm()
Send the edit form and related headers to OutputPage.
int $oldid
Revision ID the edit is based on, or 0 if it's the current revision.
Definition EditPage.php:330
showTextbox(string $text, string $name, array $customAttribs=[])
setEditConflictHelperFactory(callable $factory)
showStandardInputs(int &$tabindex=2)
__construct(Article $article)
Definition EditPage.php:445
doPreviewParse(Content $content)
Parse the page for a preview.
static getPreviewLimitReport(?ParserOutput $output=null)
Get the Limit report for page previews.
importFormData(&$request)
This function collects the form data and uses it to populate various member variables.
string $starttime
Timestamp from the first time the edit form was rendered.
Definition EditPage.php:322
edit()
This is the function that gets called for "action=edit".
Definition EditPage.php:549
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:372
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:277
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:297
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:355
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:142
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:147
const POST_EDIT_COOKIE_DURATION
Duration of PostEdit cookie, in seconds.
Definition EditPage.php:171
bool $firsttime
True the first time the edit form is rendered, false after re-rendering with diff,...
Definition EditPage.php:206
showContentForm()
Subpage overridable method for printing the form for page content editing By default this simply outp...
previewOnOpen()
Should we show a preview when the edit form is first shown?
getCurrentContent()
Get the current content of the page.
getContentObject( $defaultContent=null)
const POST_EDIT_COOKIE_KEY_PREFIX
Prefix of key for cookie used to pass post-edit state.
Definition EditPage.php:153
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:534
bool $isConflict
Whether an edit conflict needs to be resolved.
Definition EditPage.php:194
maybeActivateTempUserCreate( $doAcquire)
Check the configuration and current user and enable automatic temporary user creation if possible.
Definition EditPage.php:728
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.
getEditFormHtmlAfterContent()
Content to go in the edit form after textbox1.
getEditConflictMainTextBox(array $customAttribs=[])
HTML to build the textbox1 on edit conflicts.
setTextboxes( $yourtext, $storedversion)
getEditFormHtmlBeforeContent()
Content to go in the edit form before textbox1.
Helps EditPage build textboxes.
getTextboxProtectionCSSClasses(PageIdentity $page)
An error page which can definitely be safely rendered using the OutputPage.
Exception representing a failure to serialize or unserialize a content object.
Exception thrown when an unregistered content model is requested.
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:43
Variant of the Message class.
Class that generates HTML for internal links.
Some internal bits split of from Skin.php.
Definition Linker.php:47
Create PSR-3 logger objects.
Class for creating new log entries and inserting them into the database.
A class containing constants representing the names of configuration variables.
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
Legacy class representing an editable page and handling UI for some page actions.
Definition Article.php:64
getPage()
Get the WikiPage object of this instance.
Definition Article.php:257
getContext()
Gets the context this Article is executed in.
Definition Article.php:2129
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:82
Set options of the Parser.
ParserOutput is a rendering of a Content object or a message.
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
A StatusValue for permission errors.
Utility class for creating and reading rows in the recentchanges table.
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form,...
wasPosted()
Returns true if the present request was reached by a POST operation, false otherwise (GET,...
getVal( $name, $default=null)
Fetch a text string from this web request's $_GET, $_POST or path router vars and partially normalize...
getSecurityLogContext(?UserIdentity $user=null)
Returns an array suitable for addition to a PSR-3 log context that will contain information about the...
getText( $name, $default='')
Fetch a text string from this web request's $_GET, $_POST or path router vars and return it in normal...
getBool( $name, $default=false)
Fetch a boolean value from this web request's $_GET, $_POST or path router vars or return $default if...
getQueryValuesOnly()
Get the values passed in $_GET only, not including the path router parameters.
getRawVal( $name)
Fetch a string from this web request's $_GET, $_POST or path router vars WITHOUT any Unicode or line ...
getArray( $name, $default=null)
Fetch an array from this web request's $_GET, $_POST or path router vars, or return $default if it's ...
getInt( $name, $default=0)
Fetch an integer value from this web request's $_GET, $_POST or path router vars, or return $default ...
getCheck( $name)
Return true if the named value is set in this web request's $_GET, $_POST or path router vars,...
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.
isDeleted( $field)
MCR migration note: this replaced Revision::isDeleted.
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.
The base class for all skins.
Definition Skin.php:52
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
Object for storing information about the effects of an edit.
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:2215
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.
Create User objects.
User class for the MediaWiki software.
Definition User.php:130
Representation of a pair of user and title for watchlist entries.
getExpiry(int|TS|null $style=TS::MW)
When the watched item will expire.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
static cast(StatusValue $sv)
Succinct helper method to wrap a StatusValue in some other specific subclass.
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, 'ThumbnailStepsRatio'=> 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, 'UserEmailConfirmationUseHTML'=> false, 'PasswordExpirationDays'=> false, 'PasswordExpireGrace'=> 604800, 'SMTP'=> false, 'AdditionalMailParams'=> null, 'AllowHTMLEmail'=> false, 'EnotifFromEditor'=> false, 'EmailAuthentication'=> true, '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, 'ImageLinksSchemaMigrationStage'=> 768, 'ExternalLinksDomainGaps'=>[], 'ContentHandlers'=>['wikitext'=>['class'=> 'MediaWiki\\Content\\WikitextContentHandler', 'services'=>['TitleFactory', 'ParserFactory', 'GlobalIdGenerator', 'LanguageNameUtils', 'LinkRenderer', 'MagicWordFactory', 'ParsoidParserFactory',],], 'javascript'=>['class'=> 'MediaWiki\\Content\\JavaScriptContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'UserOptionsLookup',],], 'json'=>['class'=> 'MediaWiki\\Content\\JsonContentHandler', 'services'=>['ParsoidParserFactory', 'TitleFactory',],], 'css'=>['class'=> 'MediaWiki\\Content\\CssContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'UserOptionsLookup',],], 'vue'=>['class'=> 'MediaWiki\\Content\\VueContentHandler', 'services'=>['MainConfig', 'ParserFactory',],], '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,],], 'parsoid-pcache'=>['default'=>['minCpuTime'=> 0,],], 'postproc-pcache'=>['default'=>['minCpuTime'=> 9223372036854775807,],], 'postproc-parsoid-pcache'=>['default'=>['minCpuTime'=> 9223372036854775807,],],], 'ChronologyProtectorSecret'=> '', 'ParserCacheExpireTime'=> 86400, 'ParserCacheAsyncExpireTime'=> 60, 'ParserCacheAsyncRefreshJobs'=> true, 'OldRevisionParserCacheExpireTime'=> 3600, 'ObjectCacheSessionExpiry'=> 3600, 'PHPSessionHandling'=> 'warn', 'SuspiciousIpExpiry'=> false, 'SessionPbkdf2Iterations'=> 10001, 'UseSessionCookieJwt'=> false, 'UseSessionCookieForBotPasswords'=> 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, ], '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, 'UseLegacyMediaStyles' => 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, ], ], '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, '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, ], '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, '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, 'pagelang' => true, ], 'editprotected' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'editprotected' => true, ], 'editmycssjs' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'editmyusercss' => true, 'editmyuserjson' => true, 'editmyuserjs' => true, ], 'editmyoptions' => [ 'editmyoptions' => true, 'editmyuserjson' => true, ], 'editinterface' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'editinterface' => true, 'edituserjson' => true, 'editsitejson' => true, ], 'editsiteconfig' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => 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, '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, '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, 'editprotected' => true, 'protect' => true, ], 'viewmywatchlist' => [ 'viewmywatchlist' => true, ], 'editmywatchlist' => [ 'editmywatchlist' => true, ], 'sendemail' => [ 'sendemail' => true, ], 'createaccount' => [ 'createaccount' => true, ], 'privateinfo' => [ 'viewmyprivateinfo' => true, ], 'mergehistory' => [ 'mergehistory' => 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', ], '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, '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, '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: ], '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' => [ ], 'RCEngines' => [ 'redis' => 'MediaWiki\\RCFeed\\RedisPubSubFeedEngine', 'udp' => 'MediaWiki\\RCFeed\\UDPRCFeedEngine', ], '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, ], '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, '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\\JobQueue\\Jobs\\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', 'Access-Control-Max-Age', 'Authorization', ], 'RestAPIAdditionalRouteFiles' => [ ], 'RestSandboxSpecs' => [ ], '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, 'UsePostprocCache' => false, 'UsePostprocCacheLegacy' => false, 'UsePostprocCacheParsoid' => false, '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', ], 'ThumbnailStepsRatio' => [ 'number', 'null', ], 'ThumbnailBuckets' => [ 'array', 'null', ], 'UploadThumbnailRenderMap' => 'object', 'GalleryOptions' => 'object', 'DjvuDump' => [ 'string', 'null', ], 'DjvuRenderer' => [ 'string', 'null', ], 'DjvuTxt' => [ 'string', 'null', ], 'DjvuPostProcessor' => [ 'string', 'null', ], 'UserEmailConfirmationUseHTML' => 'boolean', 'SMTP' => [ 'boolean', 'object', ], 'EnotifFromEditor' => 'boolean', 'EnotifRevealEditorAddress' => 'boolean', 'UsersNotifiedOnAllChanges' => 'object', 'DBmwschema' => [ 'string', 'null', ], 'SharedTables' => 'array', 'DBservers' => [ 'boolean', 'array', ], 'LBFactoryConf' => 'object', 'LocalDatabases' => 'array', 'VirtualDomainsMapping' => 'object', 'FileSchemaMigrationStage' => 'integer', 'ImageLinksSchemaMigrationStage' => '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', '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', ], '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', 'RCEngines' => 'object', 'OverrideSiteFeed' => 'object', 'FeedClasses' => 'object', 'AdvertisedFeedTypes' => 'array', 'SoftwareTags' => 'object', 'RecentChangesFlags' => 'object', 'WatchlistExpiry' => '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', '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', 'UsePostprocCache' => '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', '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', ], ], ], ], '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
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
Constants for representing well known causes for page updates.
Interface for objects representing user identity.
Provide primary and replica IDatabase connections.
Interface for database access objects.
msg( $key,... $params)