MediaWiki master
EditPage.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\EditPage;
8
9use BadMethodCallException;
12use MediaWiki\Cache\LinkBatchFactory;
22use MediaWiki\Debug\DeprecationHelper;
49use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
97use OOUI;
98use OOUI\ButtonWidget;
99use OOUI\CheckboxInputWidget;
100use OOUI\DropdownInputWidget;
101use OOUI\FieldLayout;
102use RuntimeException;
103use stdClass;
104use Wikimedia\Assert\Assert;
110use Wikimedia\Timestamp\ConvertibleTimestamp;
111use Wikimedia\Timestamp\TimestampFormat as TS;
112
135#[\AllowDynamicProperties]
136class EditPage implements IEditObject {
137 use DeprecationHelper;
138 use ProtectedHookAccessorTrait;
139
144
148 public const EDITFORM_ID = 'editform';
149
154 public const POST_EDIT_COOKIE_KEY_PREFIX = 'PostEditRevision';
155
172 public const POST_EDIT_COOKIE_DURATION = 1200;
173
177 private $mArticle;
178
180 private $page;
181
185 private $mTitle;
186
188 private $mContextTitle = null;
189
194 private $action = 'submit';
195
200 public $isConflict = false;
201
203 private $isNew = false;
204
206 private $deletedSinceEdit;
207
209 public $formtype;
210
216
218 private $lastDelete;
219
221 private $mTokenOk = false;
222
224 private $mTriedSave = false;
225
227 private $incompleteForm = false;
228
230 private $missingSummary = false;
231
233 private $allowBlankSummary = false;
234
236 protected $blankArticle = false;
237
239 private $allowBlankArticle = false;
240
242 private $problematicRedirectTarget = null;
243
245 private $allowedProblematicRedirectTarget = null;
246
248 private $ignoreProblematicRedirects = false;
249
251 private $autoSumm = '';
252
254 private $hookError = '';
255
257 private $mParserOutput;
258
264 private $mExpectedParentRevision = false;
265
267 public $mShowSummaryField = true;
268
269 # Form values
270
272 public $save = false;
273
275 public $preview = false;
276
278 private $diff = false;
279
281 private $minoredit = false;
282
284 private $watchthis = false;
285
287 private $watchlistExpiryEnabled;
288
289 private WatchedItemStoreInterface $watchedItemStore;
290
292 private $watchlistExpiry;
293
295 private $recreate = false;
296
300 public $textbox1 = '';
301
306 private $textbox2 = '';
307
309 public $summary = '';
310
315 private $nosummary = false;
316
321 public $edittime = '';
322
334 private $editRevId = null;
335
337 public $section = '';
338
340 public $sectiontitle = null;
341
343 private $newSectionAnchor = null;
344
348 public $starttime = '';
349
355 public $oldid = 0;
356
363 private $parentRevId = 0;
364
366 private $scrolltop = null;
367
369 private $markAsBot = true;
370
373
375 public $contentFormat = null;
376
378 private $changeTags = null;
379
380 # Placeholders for text injection by hooks (must be HTML)
381 # extensions should take care to _append_ to the present value
382
384 public $editFormPageTop = '';
386 public $editFormTextTop = '';
399
401 public $didSave = false;
403 public $undidRev = 0;
405 private $undoAfter = 0;
406
408 public $suppressIntro = false;
409
411 private $edit;
412
414 private $contentLength = false;
415
419 private $enableApiEditOverride = false;
420
424 protected $context;
425
429 private $isOldRev = false;
430
434 private $unicodeCheck;
435
437 private $editConflictHelperFactory = null;
438 private ?TextConflictHelper $editConflictHelper = null;
439
440 private IContentHandlerFactory $contentHandlerFactory;
441 private PermissionManager $permManager;
442 private RevisionStore $revisionStore;
443 private WatchlistManager $watchlistManager;
444 private RedirectLookup $redirectLookup;
445 private UserOptionsLookup $userOptionsLookup;
446 private TempUserCreator $tempUserCreator;
447 private UserFactory $userFactory;
448 private IConnectionProvider $dbProvider;
449 private AuthManager $authManager;
450 private UserRegistrationLookup $userRegistrationLookup;
451 private SessionManager $sessionManager;
452
454 private $placeholderTempUser;
455
457 private $unsavedTempUser;
458
460 private $savedTempUser;
461
463 private $tempUserCreateActive = false;
464
466 private $tempUserName;
467
469 private $tempUserCreateDone = false;
470
472 private $unableToAcquireTempName = false;
473
474 private LinkRenderer $linkRenderer;
475 private LinkBatchFactory $linkBatchFactory;
476 private RestrictionStore $restrictionStore;
477 private CommentStore $commentStore;
478
483 public function __construct( Article $article ) {
484 $this->mArticle = $article;
485 $this->page = $article->getPage(); // model object
486 $this->mTitle = $article->getTitle();
487
488 // Make sure the local context is in sync with other member variables.
489 // Particularly make sure everything is using the same WikiPage instance.
490 // This should probably be the case in Article as well, but it's
491 // particularly important for EditPage, to make use of the in-place caching
492 // facility in WikiPage::prepareContentForEdit.
493 $this->context = new DerivativeContext( $article->getContext() );
494 $this->context->setWikiPage( $this->page );
495 $this->context->setTitle( $this->mTitle );
496
497 $this->contentModel = $this->mTitle->getContentModel();
498
499 $services = MediaWikiServices::getInstance();
500 $this->contentHandlerFactory = $services->getContentHandlerFactory();
501 $this->contentFormat = $this->contentHandlerFactory
502 ->getContentHandler( $this->contentModel )
503 ->getDefaultFormat();
504 $this->permManager = $services->getPermissionManager();
505 $this->revisionStore = $services->getRevisionStore();
506 $this->watchlistExpiryEnabled = $this->getContext()->getConfig() instanceof Config
507 && $this->getContext()->getConfig()->get( MainConfigNames::WatchlistExpiry );
508 $this->watchedItemStore = $services->getWatchedItemStore();
509 $this->watchlistManager = $services->getWatchlistManager();
510 $this->redirectLookup = $services->getRedirectLookup();
511 $this->userOptionsLookup = $services->getUserOptionsLookup();
512 $this->tempUserCreator = $services->getTempUserCreator();
513 $this->userFactory = $services->getUserFactory();
514 $this->linkRenderer = $services->getLinkRenderer();
515 $this->linkBatchFactory = $services->getLinkBatchFactory();
516 $this->restrictionStore = $services->getRestrictionStore();
517 $this->commentStore = $services->getCommentStore();
518 $this->dbProvider = $services->getConnectionProvider();
519 $this->authManager = $services->getAuthManager();
520 $this->userRegistrationLookup = $services->getUserRegistrationLookup();
521 $this->sessionManager = $services->getSessionManager();
522
523 $this->deprecatePublicProperty( 'textbox2', '1.44', __CLASS__ );
524 $this->deprecatePublicProperty( 'action', '1.38', __CLASS__ );
525 }
526
530 public function getArticle() {
531 return $this->mArticle;
532 }
533
538 public function getContext() {
539 return $this->context;
540 }
541
546 public function getTitle() {
547 return $this->mTitle;
548 }
549
553 public function setContextTitle( $title ) {
554 $this->mContextTitle = $title;
555 }
556
561 public function getContextTitle() {
562 if ( $this->mContextTitle === null ) {
563 throw new RuntimeException( "EditPage does not have a context title set" );
564 } else {
565 return $this->mContextTitle;
566 }
567 }
568
576 private function isSupportedContentModel( string $modelId ): bool {
577 return $this->enableApiEditOverride === true ||
578 $this->contentHandlerFactory->getContentHandler( $modelId )->supportsDirectEditing();
579 }
580
588 public function setApiEditOverride( $enableOverride ) {
589 $this->enableApiEditOverride = $enableOverride;
590 }
591
603 public function edit() {
604 // Allow extensions to modify/prevent this form or submission
605 if ( !$this->getHookRunner()->onAlternateEdit( $this ) ) {
606 return;
607 }
608
609 wfDebug( __METHOD__ . ": enter" );
610
611 $request = $this->context->getRequest();
612 // If they used redlink=1 and the page exists, redirect to the main article
613 if ( $request->getBool( 'redlink' ) && $this->mTitle->exists() ) {
614 $this->context->getOutput()->redirect( $this->mTitle->getFullURL() );
615 return;
616 }
617
618 $this->importFormData( $request );
619 $this->firsttime = false;
620
621 $readOnlyMode = MediaWikiServices::getInstance()->getReadOnlyMode();
622 if ( $this->save && $readOnlyMode->isReadOnly() ) {
623 // Force preview
624 $this->save = false;
625 $this->preview = true;
626 }
627
628 if ( $this->save ) {
629 $this->formtype = 'save';
630 } elseif ( $this->preview ) {
631 $this->formtype = 'preview';
632 } elseif ( $this->diff ) {
633 $this->formtype = 'diff';
634 } else { # First time through
635 $this->firsttime = true;
636 if ( $this->previewOnOpen() ) {
637 $this->formtype = 'preview';
638 } else {
639 $this->formtype = 'initial';
640 }
641 }
642
643 // Check permissions after possibly creating a placeholder temp user.
644 // This allows anonymous users to edit via a temporary account, if the site is
645 // configured to (1) disallow anonymous editing and (2) autocreate temporary
646 // accounts on edit.
647 $this->unableToAcquireTempName = !$this->maybeActivateTempUserCreate( !$this->firsttime )->isOK();
648
649 $status = $this->getEditPermissionStatus(
650 $this->save ? PermissionManager::RIGOR_SECURE : PermissionManager::RIGOR_FULL
651 );
652 if ( !$status->isGood() ) {
653 wfDebug( __METHOD__ . ": User can't edit" );
654
655 $user = $this->context->getUser();
656 if ( $user->getBlock() && !$readOnlyMode->isReadOnly() ) {
657 // Auto-block user's IP if the account was "hard" blocked
658 $user->scheduleSpreadBlock();
659 }
660 $this->displayPermissionStatus( $status );
661
662 return;
663 }
664
665 $revRecord = $this->mArticle->fetchRevisionRecord();
666 // Disallow editing revisions with content models different from the current one
667 // Undo edits being an exception in order to allow reverting content model changes.
668 $revContentModel = $revRecord ?
669 $revRecord->getMainContentModel() :
670 false;
671 if ( $revContentModel && $revContentModel !== $this->contentModel ) {
672 $prevRevRecord = null;
673 $prevContentModel = false;
674 if ( $this->undidRev ) {
675 $undidRevRecord = $this->revisionStore
676 ->getRevisionById( $this->undidRev );
677 $prevRevRecord = $undidRevRecord ?
678 $this->revisionStore->getPreviousRevision( $undidRevRecord ) :
679 null;
680
681 $prevContentModel = $prevRevRecord ?
682 $prevRevRecord->getMainContentModel() :
683 '';
684 }
685
686 if ( !$this->undidRev
687 || !$prevRevRecord
688 || $prevContentModel !== $this->contentModel
689 ) {
690 $this->displayViewSourcePage(
691 $this->getContentObject(),
692 $this->context->msg(
693 'contentmodelediterror',
694 $revContentModel,
695 $this->contentModel
696 )->plain()
697 );
698 return;
699 }
700 }
701
702 $this->isConflict = false;
703
704 # Attempt submission here. This will check for edit conflicts,
705 # and redundantly check for locked database, blocked IPs, etc.
706 # that edit() already checked just in case someone tries to sneak
707 # in the back door with a hand-edited submission URL.
708
709 if ( $this->formtype === 'save' ) {
710 $resultDetails = null;
711 $status = $this->attemptSave( $resultDetails );
712 if ( !$this->handleStatus( $status, $resultDetails ) ) {
713 return;
714 }
715 }
716
717 # First time through: get contents, set time for conflict
718 # checking, etc.
719 if ( $this->formtype === 'initial' || $this->firsttime ) {
720 if ( !$this->initialiseForm() ) {
721 return;
722 }
723
724 if ( $this->mTitle->getArticleID() ) {
725 $this->getHookRunner()->onEditFormInitialText( $this );
726 }
727 }
728
729 // If we're displaying an old revision, and there are differences between it and the
730 // current revision outside the main slot, then we can't allow the old revision to be
731 // editable, as what would happen to the non-main-slot data if someone saves the old
732 // revision is undefined.
733 // When this is the case, display a read-only version of the page instead, with a link
734 // to a diff page from which the old revision can be restored
735 $curRevisionRecord = $this->page->getRevisionRecord();
736 if ( $curRevisionRecord
737 && $revRecord
738 && $curRevisionRecord->getId() !== $revRecord->getId()
739 && ( WikiPage::hasDifferencesOutsideMainSlot(
740 $revRecord,
741 $curRevisionRecord
742 ) || !$this->isSupportedContentModel(
743 $revRecord->getSlot(
744 SlotRecord::MAIN,
745 RevisionRecord::RAW
746 )->getModel()
747 ) )
748 ) {
749 $restoreLink = $this->mTitle->getFullURL(
750 [
751 'action' => 'mcrrestore',
752 'restore' => $revRecord->getId(),
753 ]
754 );
755 $this->displayViewSourcePage(
756 $this->getContentObject(),
757 $this->context->msg(
758 'nonmain-slot-differences-therefore-readonly',
759 $restoreLink
760 )->plain()
761 );
762 return;
763 }
764
765 $this->showEditForm();
766 }
767
777 public function maybeActivateTempUserCreate( $doAcquire ): Status {
778 if ( $this->tempUserCreateActive ) {
779 // Already done
780 return Status::newGood();
781 }
782 $user = $this->context->getUser();
783
784 // Log out any user using an expired temporary account, so that we can give them a new temporary account.
785 // As described in T389485, we need to do this because the maintenance script to expire temporary accounts
786 // may fail to run or not be configured to run.
787 if ( $user->isTemp() ) {
788 $expiryAfterDays = $this->tempUserCreator->getExpireAfterDays();
789 if ( $expiryAfterDays ) {
790 $expirationCutoff = (int)ConvertibleTimestamp::now( TS::UNIX ) - ( 86_400 * $expiryAfterDays );
791
792 // If the user was created before the expiration cutoff, then log them out, expire any other existing
793 // sessions, and revoke any access to the account that may exist.
794 // If no registration is set then do nothing, as if registration date system is broken it would
795 // cause a new temporary account for each edit.
796 $firstUserRegistration = $this->userRegistrationLookup->getFirstRegistration( $user );
797 if (
798 $firstUserRegistration &&
799 ConvertibleTimestamp::convert( TS::UNIX, $firstUserRegistration ) < $expirationCutoff
800 ) {
801 // Log the user out of the expired temporary account.
802 $user->logout();
803
804 // Clear any stashed temporary account name (if any is set), as we want a new name for the user.
805 $session = $this->context->getRequest()->getSession();
806 $session->set( 'TempUser:name', null );
807 $session->save();
808
809 // Invalidate any sessions for the expired temporary account
810 $this->sessionManager->invalidateSessionsForUser(
811 $this->userFactory->newFromUserIdentity( $user )
812 );
813 }
814 }
815 }
816
817 if ( $this->tempUserCreator->shouldAutoCreate( $user, 'edit' ) ) {
818 if ( $doAcquire ) {
819 $name = $this->tempUserCreator->acquireAndStashName(
820 $this->context->getRequest()->getSession() );
821 if ( $name === null ) {
822 $status = Status::newFatal( 'temp-user-unable-to-acquire' );
823 $status->value = self::AS_UNABLE_TO_ACQUIRE_TEMP_ACCOUNT;
824 return $status;
825 }
826 $this->unsavedTempUser = $this->userFactory->newUnsavedTempUser( $name );
827 $this->tempUserName = $name;
828 } else {
829 $this->placeholderTempUser = $this->userFactory->newTempPlaceholder();
830 }
831 $this->tempUserCreateActive = true;
832 }
833 return Status::newGood();
834 }
835
843 private function createTempUser(): Status {
844 if ( !$this->tempUserCreateActive ) {
845 return Status::newGood();
846 }
847 $request = $this->context->getRequest();
848 $status = $this->tempUserCreator->create(
849 $this->tempUserName,
850 $request
851 );
852 if ( $status->isOK() ) {
853 $this->placeholderTempUser = null;
854 $this->unsavedTempUser = null;
855 $this->savedTempUser = $status->getUser();
856 $this->authManager->setRequestContextUserFromSessionUser();
857 $this->tempUserCreateDone = true;
858 }
859 LoggerFactory::getInstance( 'authevents' )->info(
860 'Temporary account creation attempt: {user}',
861 [
862 'user' => $this->tempUserName,
863 'success' => $status->isOK(),
864 ] + $request->getSecurityLogContext( $status->isOK() ? $status->getUser() : null )
865 );
866 return $status;
867 }
868
878 private function getAuthority(): Authority {
879 return $this->getUserForPermissions();
880 }
881
888 private function getUserForPermissions() {
889 if ( $this->savedTempUser ) {
890 return $this->savedTempUser;
891 } elseif ( $this->unsavedTempUser ) {
892 return $this->unsavedTempUser;
893 } elseif ( $this->placeholderTempUser ) {
894 return $this->placeholderTempUser;
895 } else {
896 return $this->context->getUser();
897 }
898 }
899
906 private function getUserForPreview() {
907 if ( $this->savedTempUser ) {
908 return $this->savedTempUser;
909 } elseif ( $this->unsavedTempUser ) {
910 return $this->unsavedTempUser;
911 } elseif ( $this->firsttime && $this->placeholderTempUser ) {
912 // Mostly a GET request and no temp user was aquired,
913 // but needed for pst or content transform for preview,
914 // fallback to a placeholder for this situation (T330943)
915 return $this->placeholderTempUser;
916 } elseif ( $this->tempUserCreateActive ) {
917 throw new BadMethodCallException(
918 "Can't use the request user for preview with IP masking enabled" );
919 } else {
920 return $this->context->getUser();
921 }
922 }
923
930 private function getUserForSave() {
931 if ( $this->savedTempUser ) {
932 return $this->savedTempUser;
933 } elseif ( $this->tempUserCreateActive ) {
934 throw new BadMethodCallException(
935 "Can't use the request user for storage with IP masking enabled" );
936 } else {
937 return $this->context->getUser();
938 }
939 }
940
945 private function getEditPermissionStatus( string $rigor = PermissionManager::RIGOR_SECURE ): PermissionStatus {
946 $user = $this->getUserForPermissions();
947 return $this->permManager->getPermissionStatus(
948 'edit',
949 $user,
950 $this->mTitle,
951 $rigor
952 );
953 }
954
966 private function displayPermissionStatus( PermissionStatus $status ): void {
967 $out = $this->context->getOutput();
968 if ( $this->context->getRequest()->getBool( 'redlink' ) ) {
969 // The edit page was reached via a red link.
970 // Redirect to the article page and let them click the edit tab if
971 // they really want a permission error.
972 $out->redirect( $this->mTitle->getFullURL() );
973 return;
974 }
975
976 $content = $this->getContentObject();
977
978 // Use the normal message if there's nothing to display:
979 // page or section does not exist (T249978), and the user isn't in the middle of an edit
980 if ( !$content || ( $this->firsttime && !$this->mTitle->exists() && $content->isEmpty() ) ) {
981 $action = $this->mTitle->exists() ? 'edit' :
982 ( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
983 throw new PermissionsError( $action, $status );
984 }
985
986 $this->displayViewSourcePage(
987 $content,
988 $out->formatPermissionStatus( $status, 'edit' )
989 );
990 }
991
997 private function displayViewSourcePage( Content $content, string $errorMessage ): void {
998 $out = $this->context->getOutput();
999 $this->getHookRunner()->onEditPage__showReadOnlyForm_initial( $this, $out );
1000
1001 $out->setRobotPolicy( 'noindex,nofollow' );
1002 $out->setPageTitleMsg( $this->context->msg(
1003 'viewsource-title'
1004 )->plaintextParams(
1005 $this->getContextTitle()->getPrefixedText()
1006 ) );
1007 $out->addBacklinkSubtitle( $this->getContextTitle() );
1008 $out->addHTML( $this->editFormPageTop );
1009 $out->addHTML( $this->editFormTextTop );
1010
1011 if ( $errorMessage !== '' ) {
1012 $out->addWikiTextAsInterface( $errorMessage );
1013 $out->addHTML( "<hr />\n" );
1014 }
1015
1016 # If the user made changes, preserve them when showing the markup
1017 # (This happens when a user is blocked during edit, for instance)
1018 if ( !$this->firsttime ) {
1019 $text = $this->textbox1;
1020 $out->addWikiMsg( 'viewyourtext' );
1021 } else {
1022 try {
1023 $text = $this->toEditText( $content );
1024 } catch ( MWException ) {
1025 # Serialize using the default format if the content model is not supported
1026 # (e.g. for an old revision with a different model)
1027 $text = $content->serialize();
1028 }
1029 $out->addWikiMsg( 'viewsourcetext' );
1030 }
1031
1032 $out->addHTML( $this->editFormTextBeforeContent );
1033 $this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] );
1034 $out->addHTML( $this->editFormTextAfterContent );
1035
1036 $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
1037
1038 $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
1039
1040 $out->addHTML( $this->editFormTextBottom );
1041 if ( $this->mTitle->exists() ) {
1042 $out->returnToMain( null, $this->mTitle );
1043 }
1044 }
1045
1051 protected function previewOnOpen() {
1052 $config = $this->context->getConfig();
1053 $previewOnOpenNamespaces = $config->get( MainConfigNames::PreviewOnOpenNamespaces );
1054 $request = $this->context->getRequest();
1055 if ( $config->get( MainConfigNames::RawHtml ) ) {
1056 // If raw HTML is enabled, disable preview on open
1057 // since it has to be posted with a token for
1058 // security reasons
1059 return false;
1060 }
1061 $preview = $request->getRawVal( 'preview' );
1062 if ( $preview === 'yes' ) {
1063 // Explicit override from request
1064 return true;
1065 } elseif ( $preview === 'no' ) {
1066 // Explicit override from request
1067 return false;
1068 } elseif ( $this->section === 'new' ) {
1069 // Nothing *to* preview for new sections
1070 return false;
1071 } elseif ( ( $request->getCheck( 'preload' ) || $this->mTitle->exists() )
1072 && $this->userOptionsLookup->getOption( $this->context->getUser(), 'previewonfirst' )
1073 ) {
1074 // Standard preference behavior
1075 return true;
1076 } elseif ( !$this->mTitle->exists()
1077 && isset( $previewOnOpenNamespaces[$this->mTitle->getNamespace()] )
1078 && $previewOnOpenNamespaces[$this->mTitle->getNamespace()]
1079 ) {
1080 // Categories are special
1081 return true;
1082 } else {
1083 return false;
1084 }
1085 }
1086
1093 private function isSectionEditSupported(): bool {
1094 $currentRev = $this->page->getRevisionRecord();
1095
1096 // $currentRev is null for non-existing pages, use the page default content model.
1097 $revContentModel = $currentRev
1098 ? $currentRev->getMainContentModel()
1099 : $this->page->getContentModel();
1100
1101 return (
1102 ( $this->mArticle->getRevIdFetched() === $this->page->getLatest() ) &&
1103 $this->contentHandlerFactory->getContentHandler( $revContentModel )->supportsSections()
1104 );
1105 }
1106
1112 public function importFormData( &$request ) {
1113 # Section edit can come from either the form or a link
1114 $this->section = $request->getVal( 'wpSection', $request->getVal( 'section', '' ) );
1115
1116 if ( $this->section !== null && $this->section !== '' && !$this->isSectionEditSupported() ) {
1117 throw new ErrorPageError( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
1118 }
1119
1120 $this->isNew = !$this->mTitle->exists() || $this->section === 'new';
1121
1122 if ( $request->wasPosted() ) {
1123 $this->importFormDataPosted( $request );
1124 } else {
1125 # Not a posted form? Start with nothing.
1126 wfDebug( __METHOD__ . ": Not a posted form." );
1127 $this->textbox1 = '';
1128 $this->summary = '';
1129 $this->sectiontitle = null;
1130 $this->edittime = '';
1131 $this->editRevId = null;
1132 $this->starttime = wfTimestampNow();
1133 $this->edit = false;
1134 $this->preview = false;
1135 $this->save = false;
1136 $this->diff = false;
1137 $this->minoredit = false;
1138 // Watch may be overridden by request parameters
1139 $this->watchthis = $request->getBool( 'watchthis', false );
1140 if ( $this->watchlistExpiryEnabled ) {
1141 $this->watchlistExpiry = null;
1142 }
1143 $this->recreate = false;
1144
1145 // When creating a new section, we can preload a section title by passing it as the
1146 // preloadtitle parameter in the URL (T15100)
1147 if ( $this->section === 'new' && $request->getCheck( 'preloadtitle' ) ) {
1148 $this->sectiontitle = $request->getVal( 'preloadtitle' );
1149 $this->setNewSectionSummary();
1150 } elseif ( $this->section !== 'new' && $request->getRawVal( 'summary' ) !== '' ) {
1151 $this->summary = $request->getText( 'summary' );
1152 if ( $this->summary !== '' ) {
1153 // If a summary has been preset using &summary= we don't want to prompt for
1154 // a different summary. Only prompt for a summary if the summary is blanked.
1155 // (T19416)
1156 $this->autoSumm = md5( '' );
1157 }
1158 }
1159
1160 if ( $request->getVal( 'minor' ) ) {
1161 $this->minoredit = true;
1162 }
1163 }
1164
1165 $this->oldid = $request->getInt( 'oldid' );
1166 $this->parentRevId = $request->getInt( 'parentRevId' );
1167
1168 $this->markAsBot = $request->getBool( 'bot', true );
1169 $this->nosummary = $request->getBool( 'nosummary' );
1170
1171 // May be overridden by revision.
1172 $this->contentModel = $request->getText( 'model', $this->contentModel );
1173 // May be overridden by revision.
1174 $this->contentFormat = $request->getText( 'format', $this->contentFormat );
1175
1176 try {
1177 $handler = $this->contentHandlerFactory->getContentHandler( $this->contentModel );
1178 } catch ( MWUnknownContentModelException ) {
1179 throw new ErrorPageError(
1180 'editpage-invalidcontentmodel-title',
1181 'editpage-invalidcontentmodel-text',
1182 [ wfEscapeWikiText( $this->contentModel ) ]
1183 );
1184 }
1185
1186 if ( !$handler->isSupportedFormat( $this->contentFormat ) ) {
1187 throw new ErrorPageError(
1188 'editpage-notsupportedcontentformat-title',
1189 'editpage-notsupportedcontentformat-text',
1190 [
1191 wfEscapeWikiText( $this->contentFormat ),
1192 wfEscapeWikiText( ContentHandler::getLocalizedName( $this->contentModel ) )
1193 ]
1194 );
1195 }
1196
1197 // Allow extensions to modify form data
1198 $this->getHookRunner()->onEditPage__importFormData( $this, $request );
1199 }
1200
1201 private function importFormDataPosted( WebRequest $request ): void {
1202 # These fields need to be checked for encoding.
1203 # Also remove trailing whitespace, but don't remove _initial_
1204 # whitespace from the text boxes. This may be significant formatting.
1205 $this->textbox1 = rtrim( $request->getText( 'wpTextbox1' ) );
1206 if ( !$request->getCheck( 'wpTextbox2' ) ) {
1207 // Skip this if wpTextbox2 has input, it indicates that we came
1208 // from a conflict page with raw page text, not a custom form
1209 // modified by subclasses
1210 $textbox1 = $this->importContentFormData( $request );
1211 if ( $textbox1 !== null ) {
1212 $this->textbox1 = $textbox1;
1213 }
1214 }
1215
1216 $this->unicodeCheck = $request->getText( 'wpUnicodeCheck' );
1217
1218 if ( $this->section === 'new' ) {
1219 # Allow setting sectiontitle different from the edit summary.
1220 # Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is
1221 # currently doing double duty as both edit summary and section title. Right now this
1222 # is just to allow API edits to work around this limitation, but this should be
1223 # incorporated into the actual edit form when EditPage is rewritten (T20654, T28312).
1224 if ( $request->getCheck( 'wpSectionTitle' ) ) {
1225 $this->sectiontitle = $request->getText( 'wpSectionTitle' );
1226 if ( $request->getCheck( 'wpSummary' ) ) {
1227 $this->summary = $request->getText( 'wpSummary' );
1228 }
1229 } else {
1230 $this->sectiontitle = $request->getText( 'wpSummary' );
1231 }
1232 } else {
1233 $this->sectiontitle = null;
1234 $this->summary = $request->getText( 'wpSummary' );
1235 }
1236
1237 # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the
1238 # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for
1239 # section titles. (T3600)
1240 # It is weird to modify 'sectiontitle', even when it is provided when using the API, but API
1241 # users have come to rely on it: https://github.com/wikimedia-gadgets/twinkle/issues/1625
1242 $this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary );
1243 if ( $this->sectiontitle !== null ) {
1244 $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
1245 }
1246
1247 // @phan-suppress-next-line PhanSuspiciousValueComparison
1248 if ( $this->section === 'new' ) {
1249 $this->setNewSectionSummary();
1250 }
1251
1252 $this->edittime = $request->getVal( 'wpEdittime' );
1253 $this->editRevId = $request->getIntOrNull( 'editRevId' );
1254 $this->starttime = $request->getVal( 'wpStarttime' );
1255
1256 $undidRev = $request->getInt( 'wpUndidRevision' );
1257 if ( $undidRev ) {
1258 $this->undidRev = $undidRev;
1259 }
1260 $undoAfter = $request->getInt( 'wpUndoAfter' );
1261 if ( $undoAfter ) {
1262 $this->undoAfter = $undoAfter;
1263 }
1264
1265 $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' );
1266
1267 if ( $this->textbox1 === '' && !$request->getCheck( 'wpTextbox1' ) ) {
1268 // wpTextbox1 field is missing, possibly due to being "too big"
1269 // according to some filter rules that may have been configured
1270 // for security reasons.
1271 $this->incompleteForm = true;
1272 } else {
1273 // If we receive the last parameter of the request, we can fairly
1274 // claim the POST request has not been truncated.
1275 $this->incompleteForm = !$request->getVal( 'wpUltimateParam' );
1276 }
1277 if ( $this->incompleteForm ) {
1278 # If the form is incomplete, force to preview.
1279 wfDebug( __METHOD__ . ": Form data appears to be incomplete" );
1280 wfDebug( "POST DATA: " . var_export( $request->getPostValues(), true ) );
1281 $this->preview = true;
1282 } else {
1283 $this->preview = $request->getCheck( 'wpPreview' );
1284 $this->diff = $request->getCheck( 'wpDiff' );
1285
1286 // Remember whether a save was requested, so we can indicate
1287 // if we forced preview due to session failure.
1288 $this->mTriedSave = !$this->preview;
1289
1290 if ( $this->tokenOk( $request ) ) {
1291 # Some browsers will not report any submit button
1292 # if the user hits enter in the comment box.
1293 # The unmarked state will be assumed to be a save,
1294 # if the form seems otherwise complete.
1295 wfDebug( __METHOD__ . ": Passed token check." );
1296 } elseif ( $this->diff ) {
1297 # Failed token check, but only requested "Show Changes".
1298 wfDebug( __METHOD__ . ": Failed token check; Show Changes requested." );
1299 } else {
1300 # Page might be a hack attempt posted from
1301 # an external site. Preview instead of saving.
1302 wfDebug( __METHOD__ . ": Failed token check; forcing preview" );
1303 $this->preview = true;
1304 }
1305 }
1306 $this->save = !$this->preview && !$this->diff;
1307 if ( !$this->edittime || !preg_match( '/^\d{14}$/', $this->edittime ) ) {
1308 $this->edittime = null;
1309 }
1310
1311 if ( !$this->starttime || !preg_match( '/^\d{14}$/', $this->starttime ) ) {
1312 $this->starttime = null;
1313 }
1314
1315 $this->recreate = $request->getCheck( 'wpRecreate' );
1316
1317 $user = $this->context->getUser();
1318
1319 $this->minoredit = $request->getCheck( 'wpMinoredit' );
1320 $this->watchthis = $request->getCheck( 'wpWatchthis' );
1321 $submittedExpiry = $request->getText( 'wpWatchlistExpiry' );
1322 if ( $this->watchlistExpiryEnabled && $submittedExpiry !== '' ) {
1323 // This parsing of the user-posted expiry is done for both preview and saving. This
1324 // is necessary because ApiEditPage uses preview when it saves (yuck!). Note that it
1325 // only works because the unnormalized value is retrieved again below in
1326 // getCheckboxesDefinitionForWatchlist().
1327 $submittedExpiry = ExpiryDef::normalizeExpiry( $submittedExpiry, TS::ISO_8601 );
1328 if ( $submittedExpiry !== false ) {
1329 $this->watchlistExpiry = $submittedExpiry;
1330 }
1331 }
1332
1333 # Don't force edit summaries when a user is editing their own user or talk page
1334 if ( ( $this->mTitle->getNamespace() === NS_USER || $this->mTitle->getNamespace() === NS_USER_TALK )
1335 && $this->mTitle->getText() === $user->getName()
1336 ) {
1337 $this->allowBlankSummary = true;
1338 } else {
1339 $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )
1340 || !$this->userOptionsLookup->getOption( $user, 'forceeditsummary' );
1341 }
1342
1343 $this->autoSumm = $request->getText( 'wpAutoSummary' );
1344
1345 $this->allowBlankArticle = $request->getBool( 'wpIgnoreBlankArticle' );
1346 $allowedProblematicRedirectTargetText = $request->getText( 'wpAllowedProblematicRedirectTarget' );
1347 $this->allowedProblematicRedirectTarget = $allowedProblematicRedirectTargetText === ''
1348 ? null : Title::newFromText( $allowedProblematicRedirectTargetText );
1349 $this->ignoreProblematicRedirects = $request->getBool( 'wpIgnoreProblematicRedirects' );
1350
1351 $changeTags = $request->getVal( 'wpChangeTags' );
1352 $changeTagsAfterPreview = $request->getVal( 'wpChangeTagsAfterPreview' );
1353 if ( $changeTags === null || $changeTags === '' ) {
1354 $this->changeTags = [];
1355 } else {
1356 $this->changeTags = array_filter(
1357 array_map(
1358 'trim',
1359 explode( ',', $changeTags )
1360 )
1361 );
1362 }
1363 if ( $changeTagsAfterPreview !== null && $changeTagsAfterPreview !== '' ) {
1364 $this->changeTags = array_merge( $this->changeTags, array_filter(
1365 array_map(
1366 'trim',
1367 explode( ',', $changeTagsAfterPreview )
1368 )
1369 ) );
1370 }
1371 }
1372
1382 protected function importContentFormData( &$request ) {
1383 return null; // Don't do anything, EditPage already extracted wpTextbox1
1384 }
1385
1391 private function initialiseForm(): bool {
1392 $this->edittime = $this->page->getTimestamp();
1393 $this->editRevId = $this->page->getLatest();
1394
1395 $dummy = $this->contentHandlerFactory
1396 ->getContentHandler( $this->contentModel )
1397 ->makeEmptyContent();
1398 $content = $this->getContentObject( $dummy ); # TODO: track content object?!
1399 if ( $content === $dummy ) { // Invalid section
1400 $this->noSuchSectionPage();
1401 return false;
1402 }
1403
1404 if ( !$content ) {
1405 $out = $this->context->getOutput();
1406 // FIXME Why is this double-parsing?
1407 $this->editFormPageTop .= Html::errorBox(
1408 $out->parseAsInterface( $this->context->msg( 'missing-revision-content',
1409 $this->oldid,
1410 Message::plaintextParam( $this->mTitle->getPrefixedText() )
1411 )->parse() )
1412 );
1413 } elseif ( !$this->isSupportedContentModel( $content->getModel() ) ) {
1414 $modelMsg = $this->getContext()->msg( 'content-model-' . $content->getModel() );
1415 $modelName = $modelMsg->exists() ? $modelMsg->text() : $content->getModel();
1416
1417 $out = $this->context->getOutput();
1418 $out->showErrorPage(
1419 'modeleditnotsupported-title',
1420 'modeleditnotsupported-text',
1421 [ $modelName ]
1422 );
1423 return false;
1424 }
1425
1426 $this->textbox1 = $this->toEditText( $content );
1427
1428 $user = $this->context->getUser();
1429 // activate checkboxes if user wants them to be always active
1430 # Sort out the "watch" checkbox
1431 if ( $this->userOptionsLookup->getOption( $user, 'watchdefault' ) ) {
1432 # Watch all edits
1433 $this->watchthis = true;
1434 } elseif ( $this->userOptionsLookup->getOption( $user, 'watchcreations' ) && !$this->mTitle->exists() ) {
1435 # Watch creations
1436 $this->watchthis = true;
1437 } elseif ( $this->watchlistManager->isWatched( $user, $this->mTitle ) ) {
1438 # Already watched
1439 $this->watchthis = true;
1440 }
1441 if ( $this->watchthis && $this->watchlistExpiryEnabled ) {
1442 $watchedItem = $this->watchedItemStore->getWatchedItem( $user, $this->getTitle() );
1443 $this->watchlistExpiry = $watchedItem ? $watchedItem->getExpiry() : null;
1444 }
1445 if ( !$this->isNew && $this->userOptionsLookup->getOption( $user, 'minordefault' ) ) {
1446 $this->minoredit = true;
1447 }
1448 if ( $this->textbox1 === false ) {
1449 return false;
1450 }
1451 return true;
1452 }
1453
1459 protected function getContentObject( $defaultContent = null ) {
1460 $services = MediaWikiServices::getInstance();
1461 $request = $this->context->getRequest();
1462
1463 $content = false;
1464
1465 // For non-existent articles and new sections, use preload text if any.
1466 if ( !$this->mTitle->exists() || $this->section === 'new' ) {
1467 $content = $services->getPreloadedContentBuilder()->getPreloadedContent(
1468 $this->mTitle->toPageIdentity(),
1469 $this->context->getUser(),
1470 $request->getVal( 'preload' ),
1471 $request->getArray( 'preloadparams', [] ),
1472 $request->getVal( 'section' )
1473 );
1474 // For existing pages, get text based on "undo" or section parameters.
1475 } elseif ( $this->section !== '' ) {
1476 // Get section edit text (returns $def_text for invalid sections)
1477 $orig = $this->getOriginalContent( $this->getAuthority() );
1478 $content = $orig ? $orig->getSection( $this->section ) : null;
1479
1480 if ( !$content ) {
1481 $content = $defaultContent;
1482 }
1483 } else {
1484 $undoafter = $request->getInt( 'undoafter' );
1485 $undo = $request->getInt( 'undo' );
1486
1487 if ( $undo > 0 && $undoafter > 0 ) {
1488 // The use of getRevisionByTitle() is intentional, as allowing access to
1489 // arbitrary revisions on arbitrary pages bypass partial visibility restrictions (T297322).
1490 $undorev = $this->revisionStore->getRevisionByTitle( $this->mTitle, $undo );
1491 $oldrev = $this->revisionStore->getRevisionByTitle( $this->mTitle, $undoafter );
1492 $undoMsg = null;
1493
1494 # Make sure it's the right page,
1495 # the revisions exist and they were not deleted.
1496 # Otherwise, $content will be left as-is.
1497 if ( $undorev !== null && $oldrev !== null &&
1498 !$undorev->isDeleted( RevisionRecord::DELETED_TEXT ) &&
1499 !$oldrev->isDeleted( RevisionRecord::DELETED_TEXT )
1500 ) {
1501 if ( WikiPage::hasDifferencesOutsideMainSlot( $undorev, $oldrev )
1502 || !$this->isSupportedContentModel(
1503 $oldrev->getMainContentModel()
1504 )
1505 ) {
1506 // Hack for undo while EditPage can't handle multi-slot editing
1507 $this->context->getOutput()->redirect( $this->mTitle->getFullURL( [
1508 'action' => 'mcrundo',
1509 'undo' => $undo,
1510 'undoafter' => $undoafter,
1511 ] ) );
1512 return false;
1513 } else {
1514 $content = $this->getUndoContent( $undorev, $oldrev, $undoMsg );
1515 }
1516
1517 if ( $undoMsg === null ) {
1518 $oldContent = $this->page->getContent( RevisionRecord::RAW );
1519 $parserOptions = ParserOptions::newFromUserAndLang(
1520 $this->getUserForPreview(),
1521 $services->getContentLanguage()
1522 );
1523 $contentTransformer = $services->getContentTransformer();
1524 $newContent = $contentTransformer->preSaveTransform(
1525 $content, $this->mTitle, $this->getUserForPreview(), $parserOptions
1526 );
1527
1528 if ( $newContent->getModel() !== $oldContent->getModel() ) {
1529 // The undo may change content
1530 // model if its reverting the top
1531 // edit. This can result in
1532 // mismatched content model/format.
1533 $this->contentModel = $newContent->getModel();
1534 $oldMainSlot = $oldrev->getSlot(
1535 SlotRecord::MAIN,
1536 RevisionRecord::RAW
1537 );
1538 $this->contentFormat = $oldMainSlot->getFormat();
1539 if ( $this->contentFormat === null ) {
1540 $this->contentFormat = $this->contentHandlerFactory
1541 ->getContentHandler( $oldMainSlot->getModel() )
1542 ->getDefaultFormat();
1543 }
1544 }
1545
1546 if ( $newContent->equals( $oldContent ) ) {
1547 # Tell the user that the undo results in no change,
1548 # i.e. the revisions were already undone.
1549 $undoMsg = 'nochange';
1550 $content = false;
1551 } else {
1552 # Inform the user of our success and set an automatic edit summary
1553 $undoMsg = 'success';
1554 $this->generateUndoEditSummary( $oldrev, $undo, $undorev, $services );
1555 $this->undidRev = $undo;
1556 $this->undoAfter = $undoafter;
1557 $this->formtype = 'diff';
1558 }
1559 }
1560 } else {
1561 // Failed basic checks.
1562 // Older revisions may have been removed since the link
1563 // was created, or we may simply have got bogus input.
1564 $undoMsg = 'norev';
1565 }
1566
1567 $out = $this->context->getOutput();
1568 // Messages: undo-success, undo-failure, undo-main-slot-only, undo-norev,
1569 // undo-nochange.
1570 $class = "mw-undo-{$undoMsg}";
1571 $html = $this->context->msg( 'undo-' . $undoMsg )->parse();
1572 if ( $undoMsg !== 'success' ) {
1573 $html = Html::errorBox( $html );
1574 }
1575 $this->editFormPageTop .= Html::rawElement(
1576 'div',
1577 [ 'class' => $class ],
1578 $html
1579 );
1580 }
1581
1582 if ( $content === false ) {
1583 $content = $this->getOriginalContent( $this->getAuthority() );
1584 }
1585 }
1586
1587 return $content;
1588 }
1589
1600 private function generateUndoEditSummary( ?RevisionRecord $oldrev, int $undo,
1601 ?RevisionRecord $undorev, MediaWikiServices $services
1602 ) {
1603 // If we just undid one rev, use an autosummary
1604 $firstrev = $this->revisionStore->getNextRevision( $oldrev );
1605 if ( $firstrev && $firstrev->getId() == $undo ) {
1606 $userText = $undorev->getUser() ?
1607 $undorev->getUser()->getName() :
1608 '';
1609 if ( $userText === '' ) {
1610 $undoSummary = $this->context->msg(
1611 'undo-summary-username-hidden',
1612 $undo
1613 )->inContentLanguage()->text();
1614 // Handle external users (imported revisions)
1615 } elseif ( ExternalUserNames::isExternal( $userText ) ) {
1616 $userLinkTitle = ExternalUserNames::getUserLinkTitle( $userText );
1617 if ( $userLinkTitle ) {
1618 $userLink = $userLinkTitle->getPrefixedText();
1619 $undoSummary = $this->context->msg(
1620 'undo-summary-import',
1621 $undo,
1622 $userLink,
1623 $userText
1624 )->inContentLanguage()->text();
1625 } else {
1626 $undoSummary = $this->context->msg(
1627 'undo-summary-import2',
1628 $undo,
1629 $userText
1630 )->inContentLanguage()->text();
1631 }
1632 } else {
1633 $undoIsAnon =
1634 !$undorev->getUser() ||
1635 !$undorev->getUser()->isRegistered();
1636 $disableAnonTalk = $services->getMainConfig()->get( MainConfigNames::DisableAnonTalk );
1637 $undoMessage = ( $undoIsAnon && $disableAnonTalk ) ?
1638 'undo-summary-anon' :
1639 'undo-summary';
1640 $undoSummary = $this->context->msg(
1641 $undoMessage,
1642 $undo,
1643 $userText
1644 )->inContentLanguage()->text();
1645 }
1646 if ( $this->summary === '' ) {
1647 $this->summary = $undoSummary;
1648 } else {
1649 $this->summary = $undoSummary . $this->context->msg( 'colon-separator' )
1650 ->inContentLanguage()->text() . $this->summary;
1651 }
1652 }
1653 }
1654
1668 private function getUndoContent( RevisionRecord $undoRev, RevisionRecord $oldRev, &$error ) {
1669 $handler = $this->contentHandlerFactory
1670 ->getContentHandler( $undoRev->getSlot(
1671 SlotRecord::MAIN,
1672 RevisionRecord::RAW
1673 )->getModel() );
1674 $currentContent = $this->page->getRevisionRecord()
1675 ->getContent( SlotRecord::MAIN );
1676 $undoContent = $undoRev->getContent( SlotRecord::MAIN );
1677 $undoAfterContent = $oldRev->getContent( SlotRecord::MAIN );
1678 $undoIsLatest = $this->page->getRevisionRecord()->getId() === $undoRev->getId();
1679 if ( $currentContent === null
1680 || $undoContent === null
1681 || $undoAfterContent === null
1682 ) {
1683 $error = 'norev';
1684 return false;
1685 }
1686
1687 $content = $handler->getUndoContent(
1688 $currentContent,
1689 $undoContent,
1690 $undoAfterContent,
1691 $undoIsLatest
1692 );
1693 if ( $content === false ) {
1694 $error = 'failure';
1695 }
1696 return $content;
1697 }
1698
1713 private function getOriginalContent( Authority $performer ): ?Content {
1714 if ( $this->section === 'new' ) {
1715 return $this->getCurrentContent();
1716 }
1717 $revRecord = $this->mArticle->fetchRevisionRecord();
1718 if ( $revRecord === null ) {
1719 return $this->contentHandlerFactory
1720 ->getContentHandler( $this->contentModel )
1721 ->makeEmptyContent();
1722 }
1723 return $revRecord->getContent( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $performer );
1724 }
1725
1737 private function getParentRevId() {
1738 if ( $this->parentRevId ) {
1739 return $this->parentRevId;
1740 } else {
1741 return $this->mArticle->getRevIdFetched();
1742 }
1743 }
1744
1753 protected function getCurrentContent() {
1754 $revRecord = $this->page->getRevisionRecord();
1755 $content = $revRecord ? $revRecord->getContent(
1756 SlotRecord::MAIN,
1757 RevisionRecord::RAW
1758 ) : null;
1759
1760 if ( $content === null ) {
1761 return $this->contentHandlerFactory
1762 ->getContentHandler( $this->contentModel )
1763 ->makeEmptyContent();
1764 }
1765
1766 return $content;
1767 }
1768
1775 private function tokenOk( WebRequest $request ): bool {
1776 $token = $request->getVal( 'wpEditToken' );
1777 $user = $this->context->getUser();
1778 $this->mTokenOk = $user->matchEditToken( $token );
1779 return $this->mTokenOk;
1780 }
1781
1796 private function setPostEditCookie( int $statusValue ): void {
1797 $revisionId = $this->page->getLatest();
1798 $postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
1799
1800 $val = 'saved';
1801 if ( $statusValue === self::AS_SUCCESS_NEW_ARTICLE ) {
1802 $val = 'created';
1803 } elseif ( $this->oldid ) {
1804 $val = 'restored';
1805 }
1806 if ( $this->tempUserCreateDone ) {
1807 $val .= '+tempuser';
1808 }
1809
1810 $response = $this->context->getRequest()->response();
1811 $response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION );
1812 }
1813
1820 public function attemptSave( &$resultDetails = false ) {
1821 // Allow bots to exempt some edits from bot flagging
1822 $markAsBot = $this->markAsBot
1823 && $this->getAuthority()->isAllowed( 'bot' );
1824
1825 // Allow trusted users to mark some edits as minor
1826 $markAsMinor = $this->minoredit && !$this->isNew
1827 && $this->getAuthority()->isAllowed( 'minoredit' );
1828
1829 $status = $this->internalAttemptSave( $resultDetails, $markAsBot, $markAsMinor );
1830
1831 $this->getHookRunner()->onEditPage__attemptSave_after( $this, $status, $resultDetails );
1832
1833 return $status;
1834 }
1835
1839 private function incrementResolvedConflicts(): void {
1840 if ( $this->context->getRequest()->getText( 'mode' ) !== 'conflict' ) {
1841 return;
1842 }
1843
1844 $this->getEditConflictHelper()->incrementResolvedStats( $this->context->getUser() );
1845 }
1846
1856 private function handleStatus( Status $status, $resultDetails ): bool {
1857 $statusValue = is_int( $status->value ) ? $status->value : 0;
1858
1863 if ( $statusValue === self::AS_SUCCESS_UPDATE
1864 || $statusValue === self::AS_SUCCESS_NEW_ARTICLE
1865 ) {
1866 $this->incrementResolvedConflicts();
1867
1868 $this->didSave = true;
1869 if ( !$resultDetails['nullEdit'] ) {
1870 $this->setPostEditCookie( $statusValue );
1871 }
1872 }
1873
1874 $out = $this->context->getOutput();
1875
1876 // "wpExtraQueryRedirect" is a hidden input to modify
1877 // after save URL and is not used by actual edit form
1878 $request = $this->context->getRequest();
1879 $extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
1880
1881 switch ( $statusValue ) {
1882 // Status codes for which the error/warning message is generated somewhere else in this class.
1883 // They should be refactored to provide their own messages and handled below (T384399).
1884 case self::AS_HOOK_ERROR_EXPECTED:
1885 case self::AS_ARTICLE_WAS_DELETED:
1886 case self::AS_CONFLICT_DETECTED:
1887 case self::AS_SUMMARY_NEEDED:
1888 case self::AS_END:
1889 case self::AS_REVISION_WAS_DELETED:
1890 return true;
1891
1892 case self::AS_HOOK_ERROR:
1893 return false;
1894
1895 // Status codes that provide their own error/warning messages. Most error scenarios that don't
1896 // need custom user interface (e.g. edit conflicts) should be handled here, one day (T384399).
1897 case self::AS_BLANK_ARTICLE:
1898 case self::AS_BROKEN_REDIRECT:
1899 case self::AS_DOUBLE_REDIRECT:
1900 case self::AS_DOUBLE_REDIRECT_LOOP:
1901 case self::AS_CONTENT_TOO_BIG:
1902 case self::AS_INVALID_REDIRECT_TARGET:
1903 case self::AS_MAX_ARTICLE_SIZE_EXCEEDED:
1904 case self::AS_PARSE_ERROR:
1905 case self::AS_SELF_REDIRECT:
1906 case self::AS_TEXTBOX_EMPTY:
1907 case self::AS_UNABLE_TO_ACQUIRE_TEMP_ACCOUNT:
1908 case self::AS_UNICODE_NOT_SUPPORTED:
1909 foreach ( $status->getMessages() as $msg ) {
1910 $out->addHTML( Html::errorBox(
1911 $this->context->msg( $msg )->parse()
1912 ) );
1913 }
1914 return true;
1915
1916 case self::AS_SUCCESS_NEW_ARTICLE:
1917 $queryParts = [];
1918 if ( $resultDetails['redirect'] ) {
1919 $queryParts[] = 'redirect=no';
1920 }
1921 if ( $extraQueryRedirect ) {
1922 $queryParts[] = $extraQueryRedirect;
1923 }
1924 $anchor = $resultDetails['sectionanchor'] ?? '';
1925 $this->doPostEditRedirect( implode( '&', $queryParts ), $anchor );
1926 return false;
1927
1928 case self::AS_SUCCESS_UPDATE:
1929 $extraQuery = '';
1930 $sectionanchor = $resultDetails['sectionanchor'];
1931 // Give extensions a chance to modify URL query on update
1932 $this->getHookRunner()->onArticleUpdateBeforeRedirect( $this->mArticle,
1933 $sectionanchor, $extraQuery );
1934
1935 $queryParts = [];
1936 if ( $resultDetails['redirect'] ) {
1937 $queryParts[] = 'redirect=no';
1938 }
1939 if ( $extraQuery ) {
1940 $queryParts[] = $extraQuery;
1941 }
1942 if ( $extraQueryRedirect ) {
1943 $queryParts[] = $extraQueryRedirect;
1944 }
1945 $this->doPostEditRedirect( implode( '&', $queryParts ), $sectionanchor );
1946 return false;
1947
1948 case self::AS_SPAM_ERROR:
1949 $this->spamPageWithContent( $resultDetails['spam'] ?? false );
1950 return false;
1951
1952 case self::AS_BLOCKED_PAGE_FOR_USER:
1953 throw new UserBlockedError(
1954 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
1955 $this->context->getUser()->getBlock(),
1956 $this->context->getUser(),
1957 $this->context->getLanguage(),
1958 $request->getIP()
1959 );
1960
1961 case self::AS_IMAGE_REDIRECT_ANON:
1962 case self::AS_IMAGE_REDIRECT_LOGGED:
1963 throw new PermissionsError( 'upload' );
1964
1965 case self::AS_READ_ONLY_PAGE_ANON:
1966 case self::AS_READ_ONLY_PAGE_LOGGED:
1967 throw new PermissionsError( 'edit' );
1968
1969 case self::AS_READ_ONLY_PAGE:
1970 throw new ReadOnlyError;
1971
1972 case self::AS_RATE_LIMITED:
1973 $out->addHTML( Html::errorBox(
1974 $this->context->msg( 'actionthrottledtext' )->parse()
1975 ) );
1976 return true;
1977
1978 case self::AS_NO_CREATE_PERMISSION:
1979 $permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage';
1980 throw new PermissionsError( $permission );
1981
1982 case self::AS_NO_CHANGE_CONTENT_MODEL:
1983 throw new PermissionsError( 'editcontentmodel' );
1984
1985 default:
1986 // We don't recognize $statusValue. The only way that can happen
1987 // is if an extension hook aborted from inside ArticleSave.
1988 // Render the status object into $this->hookError
1989 // FIXME this sucks, we should just use the Status object throughout
1990 $this->hookError = Html::errorBox(
1991 "\n" . $status->getWikiText( false, false, $this->context->getLanguage() )
1992 );
1993 return true;
1994 }
1995 }
1996
2004 private function doPostEditRedirect( $query, $anchor ) {
2005 $out = $this->context->getOutput();
2006 $url = $this->mTitle->getFullURL( $query ) . $anchor;
2007 $user = $this->getUserForSave();
2008 // If the temporary account was created in this request,
2009 // or if the temporary account has zero edits (implying
2010 // that the account was created during a failed edit
2011 // attempt in a previous request), perform the top-level
2012 // redirect to ensure the account is attached.
2013 // Note that the temp user could already have performed
2014 // the top-level redirect if this a first edit on
2015 // a wiki that is not the user's home wiki.
2016 $shouldRedirectForTempUser = $this->tempUserCreateDone ||
2017 ( $user->isTemp() && ( $user->getEditCount() === 0 ) );
2018 if ( $shouldRedirectForTempUser ) {
2019 $this->getHookRunner()->onTempUserCreatedRedirect(
2020 $this->context->getRequest()->getSession(),
2021 $user,
2022 $this->mTitle->getPrefixedDBkey(),
2023 $query,
2024 $anchor,
2025 $url
2026 );
2027 }
2028 $out->redirect( $url );
2029 }
2030
2034 private function setNewSectionSummary(): void {
2035 Assert::precondition( $this->section === 'new', 'This method can only be called for new sections' );
2036 Assert::precondition( $this->sectiontitle !== null, 'This method can only be called for new sections' );
2037
2038 $services = MediaWikiServices::getInstance();
2039 $parser = $services->getParser();
2040 $textFormatter = $services->getMessageFormatterFactory()->getTextFormatter(
2041 $services->getContentLanguageCode()->toString()
2042 );
2043
2044 if ( $this->sectiontitle !== '' ) {
2045 $this->newSectionAnchor = $this->guessSectionName( $this->sectiontitle );
2046 // If no edit summary was specified, create one automatically from the section
2047 // title and have it link to the new section. Otherwise, respect the summary as
2048 // passed.
2049 if ( $this->summary === '' ) {
2050 $messageValue = MessageValue::new( 'newsectionsummary' )
2051 ->plaintextParams( $parser->stripSectionName( $this->sectiontitle ) );
2052 $this->summary = $textFormatter->format( $messageValue );
2053 }
2054 } else {
2055 $this->newSectionAnchor = '';
2056 }
2057 }
2058
2085 private function internalAttemptSave( &$result, $markAsBot = false, $markAsMinor = false ) {
2086 // If an attempt to acquire a temporary name failed, don't attempt to do anything else.
2087 if ( $this->unableToAcquireTempName ) {
2088 $status = Status::newFatal( 'temp-user-unable-to-acquire' );
2089 $status->value = self::AS_UNABLE_TO_ACQUIRE_TEMP_ACCOUNT;
2090 return $status;
2091 }
2092 // Auto-create the temporary account user, if the feature is enabled.
2093 // We create the account before any constraint checks or edit hooks fire, to ensure
2094 // that we have an actor and user account that can be used for any logs generated
2095 // by the edit attempt, and to ensure continuity in the user experience (if a constraint
2096 // denies an edit to a logged-out user, that history should be associated with the
2097 // eventually successful account creation)
2098 $tempAccountStatus = $this->createTempUser();
2099 if ( !$tempAccountStatus->isOK() ) {
2100 return $tempAccountStatus;
2101 }
2102 if ( $tempAccountStatus instanceof CreateStatus ) {
2103 $result['savedTempUser'] = $tempAccountStatus->getUser();
2104 }
2105
2106 $useNPPatrol = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::UseNPPatrol );
2107 $useRCPatrol = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::UseRCPatrol );
2108 if ( !$this->getHookRunner()->onEditPage__attemptSave( $this ) ) {
2109 wfDebug( "Hook 'EditPage::attemptSave' aborted article saving" );
2110 $status = Status::newFatal( 'hookaborted' );
2111 $status->value = self::AS_HOOK_ERROR;
2112 return $status;
2113 }
2114
2115 if ( !$this->getHookRunner()->onEditFilter( $this, $this->textbox1, $this->section,
2116 $this->hookError, $this->summary )
2117 ) {
2118 # Error messages etc. could be handled within the hook...
2119 $status = Status::newFatal( 'hookaborted' );
2120 $status->value = self::AS_HOOK_ERROR;
2121 return $status;
2122 } elseif ( $this->hookError ) {
2123 # ...or the hook could be expecting us to produce an error
2124 $status = Status::newFatal( 'hookaborted' );
2125 $status->value = self::AS_HOOK_ERROR_EXPECTED;
2126 return $status;
2127 }
2128
2129 try {
2130 # Construct Content object
2131 $textbox_content = $this->toEditContent( $this->textbox1 );
2132 } catch ( MWContentSerializationException $ex ) {
2133 $status = Status::newFatal(
2134 'content-failed-to-parse',
2135 $this->contentModel,
2136 $this->contentFormat,
2137 $ex->getMessage()
2138 );
2139 $status->value = self::AS_PARSE_ERROR;
2140 return $status;
2141 }
2142
2143 $this->contentLength = strlen( $this->textbox1 );
2144
2145 $requestUser = $this->context->getUser();
2146 $authority = $this->getAuthority();
2147 $pstUser = $this->getUserForPreview();
2148
2149 $changingContentModel = false;
2150 if ( $this->contentModel !== $this->mTitle->getContentModel() ) {
2151 $changingContentModel = true;
2152 $oldContentModel = $this->mTitle->getContentModel();
2153 }
2154
2155 // BEGINNING OF MIGRATION TO EDITCONSTRAINT SYSTEM (see T157658)
2157 $constraintFactory = MediaWikiServices::getInstance()->getService( '_EditConstraintFactory' );
2158 $constraintRunner = new EditConstraintRunner();
2159
2160 // Message key of the label of the submit button - used by some constraint error messages
2161 $submitButtonLabel = $this->getSubmitButtonLabel();
2162
2163 // UnicodeConstraint: ensure that `$this->unicodeCheck` is the correct unicode
2164 $constraintRunner->addConstraint(
2165 new UnicodeConstraint( $this->unicodeCheck )
2166 );
2167
2168 // SimpleAntiSpamConstraint: ensure that the context request does not have
2169 // `wpAntispam` set
2170 // Use $user since there is no permissions aspect
2171 $constraintRunner->addConstraint(
2172 $constraintFactory->newSimpleAntiSpamConstraint(
2173 $this->context->getRequest()->getText( 'wpAntispam' ),
2174 $requestUser,
2175 $this->mTitle
2176 )
2177 );
2178
2179 // SpamRegexConstraint: ensure that the summary and text don't match the spam regex
2180 $constraintRunner->addConstraint(
2181 $constraintFactory->newSpamRegexConstraint(
2182 $this->summary,
2183 $this->sectiontitle,
2184 $this->textbox1,
2185 $this->context->getRequest()->getIP(),
2186 $this->mTitle
2187 )
2188 );
2189 $constraintRunner->addConstraint(
2190 new ImageRedirectConstraint(
2191 $textbox_content,
2192 $this->mTitle,
2193 $authority
2194 )
2195 );
2196 $constraintRunner->addConstraint(
2197 $constraintFactory->newReadOnlyConstraint()
2198 );
2199
2200 // Load the page data from the primary DB. If anything changes in the meantime,
2201 // we detect it by using page_latest like a token in a 1 try compare-and-swap.
2202 $this->page->loadPageData( IDBAccessObject::READ_LATEST );
2203 $new = !$this->page->exists();
2204
2205 $constraintRunner->addConstraint(
2206 new AuthorizationConstraint(
2207 $authority,
2208 $this->mTitle,
2209 $new
2210 )
2211 );
2212 $constraintRunner->addConstraint(
2213 new ContentModelChangeConstraint(
2214 $authority,
2215 $this->mTitle,
2216 $this->contentModel
2217 )
2218 );
2219 $constraintRunner->addConstraint(
2220 $constraintFactory->newLinkPurgeRateLimitConstraint(
2221 $requestUser->toRateLimitSubject()
2222 )
2223 );
2224 $constraintRunner->addConstraint(
2225 // Same constraint is used to check size before and after merging the
2226 // edits, which use different failure codes
2227 $constraintFactory->newPageSizeConstraint(
2228 $this->contentLength,
2229 PageSizeConstraint::BEFORE_MERGE
2230 )
2231 );
2232 $constraintRunner->addConstraint(
2233 new ChangeTagsConstraint( $authority, $this->changeTags )
2234 );
2235
2236 // If the article has been deleted while editing, don't save it without
2237 // confirmation
2238 $constraintRunner->addConstraint(
2239 new AccidentalRecreationConstraint(
2240 $this->wasDeletedSinceLastEdit(),
2241 $this->recreate
2242 )
2243 );
2244
2245 // Check the constraints
2246 if ( !$constraintRunner->checkConstraints() ) {
2247 $failed = $constraintRunner->getFailedConstraint();
2248
2249 // Need to check SpamRegexConstraint here, to avoid needing to pass
2250 // $result by reference again
2251 if ( $failed instanceof SpamRegexConstraint ) {
2252 $result['spam'] = $failed->getMatch();
2253 } else {
2254 $this->handleFailedConstraint( $failed );
2255 }
2256
2257 return Status::wrap( $failed->getLegacyStatus() );
2258 }
2259 // END OF MIGRATION TO EDITCONSTRAINT SYSTEM (continued below)
2260
2261 $flags = EDIT_AUTOSUMMARY |
2262 ( $new ? EDIT_NEW : EDIT_UPDATE ) |
2263 ( $markAsMinor ? EDIT_MINOR : 0 ) |
2264 ( $markAsBot ? EDIT_FORCE_BOT : 0 );
2265
2266 if ( $new ) {
2267 $content = $textbox_content;
2268
2269 $result['sectionanchor'] = '';
2270 if ( $this->section === 'new' ) {
2271 if ( $this->sectiontitle !== null ) {
2272 // Insert the section title above the content.
2273 $content = $content->addSectionHeader( $this->sectiontitle );
2274 }
2275 $result['sectionanchor'] = $this->newSectionAnchor;
2276 }
2277
2278 $pageUpdater = $this->page->newPageUpdater( $pstUser )
2279 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
2280 ->setContent( SlotRecord::MAIN, $content );
2281 $pageUpdater->prepareUpdate( $flags );
2282
2283 // BEGINNING OF MIGRATION TO EDITCONSTRAINT SYSTEM (see T157658)
2284 // Create a new runner to avoid rechecking the prior constraints, use the same factory
2285 $constraintRunner = new EditConstraintRunner();
2286
2287 // Don't save a new page if it's blank or if it's a MediaWiki:
2288 // message with content equivalent to default (allow empty pages
2289 // in this case to disable messages, see T52124)
2290 $constraintRunner->addConstraint(
2291 new DefaultTextConstraint(
2292 $this->mTitle,
2293 $this->allowBlankArticle,
2294 $this->textbox1,
2295 $submitButtonLabel
2296 )
2297 );
2298
2299 $constraintRunner->addConstraint(
2300 $constraintFactory->newEditFilterMergedContentHookConstraint(
2301 $content,
2302 $this->context,
2303 $this->summary,
2304 $markAsMinor,
2305 $this->context->getLanguage(),
2306 $pstUser
2307 )
2308 );
2309
2310 // Check the constraints
2311 if ( !$constraintRunner->checkConstraints() ) {
2312 $failed = $constraintRunner->getFailedConstraint();
2313 $this->handleFailedConstraint( $failed );
2314 return Status::wrap( $failed->getLegacyStatus() );
2315 }
2316 // END OF MIGRATION TO EDITCONSTRAINT SYSTEM (continued below)
2317 } else { # not $new
2318
2319 # Article exists. Check for edit conflict.
2320
2321 $timestamp = $this->page->getTimestamp();
2322 $latest = $this->page->getLatest();
2323
2324 wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}" );
2325 wfDebug( "revision: {$latest}, editRevId: {$this->editRevId}" );
2326
2327 $editConflictLogger = LoggerFactory::getInstance( 'EditConflict' );
2328 // An edit conflict is detected if the current revision is different from the
2329 // revision that was current when editing was initiated on the client.
2330 // This is checked based on the timestamp and revision ID.
2331 // TODO: the timestamp based check can probably go away now.
2332 if ( ( $this->edittime !== null && $this->edittime != $timestamp )
2333 || ( $this->editRevId !== null && $this->editRevId != $latest )
2334 ) {
2335 $this->isConflict = true;
2336 if ( $this->section === 'new' ) {
2337 if ( $this->page->getUserText() === $requestUser->getName() &&
2338 $this->page->getComment() === $this->summary
2339 ) {
2340 // Probably a duplicate submission of a new comment.
2341 // This can happen when CDN resends a request after
2342 // a timeout but the first one actually went through.
2343 $editConflictLogger->debug(
2344 'Duplicate new section submission; trigger edit conflict!'
2345 );
2346 } else {
2347 // New comment; suppress conflict.
2348 $this->isConflict = false;
2349 $editConflictLogger->debug( 'Conflict suppressed; new section' );
2350 }
2351 } elseif ( $this->section === ''
2352 && $this->edittime
2353 && $this->revisionStore->userWasLastToEdit(
2354 $this->dbProvider->getPrimaryDatabase(),
2355 $this->mTitle->getArticleID(),
2356 $requestUser->getId(),
2357 $this->edittime
2358 )
2359 ) {
2360 # Suppress edit conflict with self, except for section edits where merging is required.
2361 $editConflictLogger->debug( 'Suppressing edit conflict, same user.' );
2362 $this->isConflict = false;
2363 }
2364 }
2365
2366 if ( $this->isConflict ) {
2367 $editConflictLogger->debug(
2368 'Conflict! Getting section {section} for time {editTime}'
2369 . ' (id {editRevId}, article time {timestamp})',
2370 [
2371 'section' => $this->section,
2372 'editTime' => $this->edittime,
2373 'editRevId' => $this->editRevId,
2374 'timestamp' => $timestamp,
2375 ]
2376 );
2377 // @TODO: replaceSectionAtRev() with base ID (not prior current) for ?oldid=X case
2378 // ...or disable section editing for non-current revisions (not exposed anyway).
2379 if ( $this->editRevId !== null ) {
2380 $content = $this->page->replaceSectionAtRev(
2381 $this->section,
2382 $textbox_content,
2383 $this->sectiontitle,
2384 $this->editRevId
2385 );
2386 } else {
2387 $content = $this->page->replaceSectionContent(
2388 $this->section,
2389 $textbox_content,
2390 $this->sectiontitle,
2391 $this->edittime
2392 );
2393 }
2394 } else {
2395 $editConflictLogger->debug(
2396 'Getting section {section}',
2397 [ 'section' => $this->section ]
2398 );
2399 $content = $this->page->replaceSectionAtRev(
2400 $this->section,
2401 $textbox_content,
2402 $this->sectiontitle
2403 );
2404 }
2405
2406 if ( $content === null ) {
2407 $editConflictLogger->debug( 'Activating conflict; section replace failed.' );
2408 $this->isConflict = true;
2409 $content = $textbox_content; // do not try to merge here!
2410 } elseif ( $this->isConflict ) {
2411 // Attempt merge
2412 $mergedChange = $this->mergeChangesIntoContent( $content );
2413 if ( $mergedChange !== false ) {
2414 // Successful merge! Maybe we should tell the user the good news?
2415 $content = $mergedChange[0];
2416 $this->parentRevId = $mergedChange[1];
2417 $this->isConflict = false;
2418 $editConflictLogger->debug( 'Suppressing edit conflict, successful merge.' );
2419 } else {
2420 $this->section = '';
2421 $this->textbox1 = ( $content instanceof TextContent ) ? $content->getText() : '';
2422 $editConflictLogger->debug( 'Keeping edit conflict, failed merge.' );
2423 }
2424 }
2425
2426 if ( $this->isConflict ) {
2427 return Status::newGood( self::AS_CONFLICT_DETECTED )->setOK( false );
2428 }
2429
2430 $pageUpdater = $this->page->newPageUpdater( $pstUser )
2431 ->setContent( SlotRecord::MAIN, $content );
2432 $pageUpdater->prepareUpdate( $flags );
2433
2434 // BEGINNING OF MIGRATION TO EDITCONSTRAINT SYSTEM (see T157658)
2435 // Create a new runner to avoid rechecking the prior constraints, use the same factory
2436 $constraintRunner = new EditConstraintRunner();
2437 $constraintRunner->addConstraint(
2438 $constraintFactory->newEditFilterMergedContentHookConstraint(
2439 $content,
2440 $this->context,
2441 $this->summary,
2442 $markAsMinor,
2443 $this->context->getLanguage(),
2444 $pstUser
2445 )
2446 );
2447 $constraintRunner->addConstraint(
2448 new NewSectionMissingSubjectConstraint(
2449 $this->section,
2450 $this->sectiontitle ?? '',
2451 $this->allowBlankSummary
2452 )
2453 );
2454 $constraintRunner->addConstraint(
2455 new MissingCommentConstraint( $this->section, $this->textbox1 )
2456 );
2457 $constraintRunner->addConstraint(
2458 new ExistingSectionEditConstraint(
2459 $this->section,
2460 $this->summary,
2461 $this->autoSumm,
2462 $this->allowBlankSummary,
2463 $content,
2464 $this->getOriginalContent( $authority )
2465 )
2466 );
2467 // Check the constraints
2468 if ( !$constraintRunner->checkConstraints() ) {
2469 $failed = $constraintRunner->getFailedConstraint();
2470 $this->handleFailedConstraint( $failed );
2471 return Status::wrap( $failed->getLegacyStatus() );
2472 }
2473 // END OF MIGRATION TO EDITCONSTRAINT SYSTEM (continued below)
2474
2475 # All's well
2476 $sectionAnchor = '';
2477 if ( $this->section === 'new' ) {
2478 $sectionAnchor = $this->newSectionAnchor;
2479 } elseif ( $this->section !== '' ) {
2480 # Try to get a section anchor from the section source, redirect
2481 # to edited section if header found.
2482 # XXX: Might be better to integrate this into WikiPage::replaceSectionAtRev
2483 # for duplicate heading checking and maybe parsing.
2484 $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
2485 # We can't deal with anchors, includes, html etc in the header for now,
2486 # headline would need to be parsed to improve this.
2487 if ( $hasmatch && $matches[2] !== '' ) {
2488 $sectionAnchor = $this->guessSectionName( $matches[2] );
2489 }
2490 }
2491 $result['sectionanchor'] = $sectionAnchor;
2492
2493 // Save errors may fall down to the edit form, but we've now
2494 // merged the section into full text. Clear the section field
2495 // so that later submission of conflict forms won't try to
2496 // replace that into a duplicated mess.
2497 $this->textbox1 = $this->toEditText( $content );
2498 $this->section = '';
2499 }
2500
2501 // Check for length errors again now that the section is merged in
2502 $this->contentLength = strlen( $this->toEditText( $content ) );
2503
2504 // BEGINNING OF MIGRATION TO EDITCONSTRAINT SYSTEM (see T157658)
2505 // Create a new runner to avoid rechecking the prior constraints, use the same factory
2506 $constraintRunner = new EditConstraintRunner();
2507 if ( !$this->ignoreProblematicRedirects ) {
2508 $constraintRunner->addConstraint(
2509 new RedirectConstraint(
2510 $this->allowedProblematicRedirectTarget,
2511 $content,
2512 $this->getCurrentContent(),
2513 $this->getTitle(),
2514 $submitButtonLabel,
2515 $this->contentFormat,
2516 $this->redirectLookup
2517 )
2518 );
2519 }
2520 $constraintRunner->addConstraint(
2521 // Same constraint is used to check size before and after merging the
2522 // edits, which use different failure codes
2523 $constraintFactory->newPageSizeConstraint(
2524 $this->contentLength,
2525 PageSizeConstraint::AFTER_MERGE
2526 )
2527 );
2528 // Check the constraints
2529 if ( !$constraintRunner->checkConstraints() ) {
2530 $failed = $constraintRunner->getFailedConstraint();
2531 $this->handleFailedConstraint( $failed );
2532 return Status::wrap( $failed->getLegacyStatus() );
2533 }
2534 // END OF MIGRATION TO EDITCONSTRAINT SYSTEM
2535
2536 if ( $this->undidRev && $this->isUndoClean( $content ) ) {
2537 // As the user can change the edit's content before saving, we only mark
2538 // "clean" undos as reverts. This is to avoid abuse by marking irrelevant
2539 // edits as undos.
2540 $pageUpdater
2541 ->setOriginalRevisionId( $this->undoAfter ?: false )
2542 ->setCause( PageUpdateCauses::CAUSE_UNDO )
2543 ->markAsRevert(
2544 EditResult::REVERT_UNDO,
2545 $this->undidRev,
2546 $this->undoAfter ?: null
2547 );
2548 }
2549
2550 $needsPatrol = $useRCPatrol || ( $useNPPatrol && !$this->page->exists() );
2551 if ( $needsPatrol && $authority->authorizeWrite( 'autopatrol', $this->getTitle() ) ) {
2552 $pageUpdater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
2553 }
2554
2555 $pageUpdater
2556 ->addTags( $this->changeTags )
2557 ->saveRevision(
2558 CommentStoreComment::newUnsavedComment( trim( $this->summary ) ),
2559 $flags
2560 );
2561 $doEditStatus = $pageUpdater->getStatus();
2562
2563 if ( !$doEditStatus->isOK() ) {
2564 // Failure from doEdit()
2565 // Show the edit conflict page for certain recognized errors from doEdit(),
2566 // but don't show it for errors from extension hooks
2567 if (
2568 $doEditStatus->failedBecausePageMissing() ||
2569 $doEditStatus->failedBecausePageExists() ||
2570 $doEditStatus->failedBecauseOfConflict()
2571 ) {
2572 $this->isConflict = true;
2573 // Destroys data doEdit() put in $status->value but who cares
2574 // TODO: We should care, this puts an `int` value into a `Status<array>`
2575 // @phan-suppress-next-line PhanTypeMismatchPropertyProbablyReal
2576 $doEditStatus->value = self::AS_END;
2577 }
2578 return $doEditStatus;
2579 }
2580
2581 $result['nullEdit'] = !$doEditStatus->wasRevisionCreated();
2582 if ( $result['nullEdit'] ) {
2583 // We didn't know if it was a null edit until now, so bump the rate limit now
2584 $limitSubject = $requestUser->toRateLimitSubject();
2585 MediaWikiServices::getInstance()->getRateLimiter()->limit( $limitSubject, 'linkpurge' );
2586 }
2587 $result['redirect'] = $content->isRedirect();
2588
2589 $this->updateWatchlist();
2590
2591 // If the content model changed, add a log entry
2592 if ( $changingContentModel ) {
2593 $this->addContentModelChangeLogEntry(
2594 $this->getUserForSave(),
2595 // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable
2596 // $oldContentModel is set when $changingContentModel is true
2597 $new ? false : $oldContentModel,
2598 $this->contentModel,
2599 $this->summary
2600 );
2601 }
2602
2603 // Instead of carrying the same status object throughout, it is created right
2604 // when it is returned, either at an earlier point due to an error or here
2605 // due to a successful edit.
2606 $statusCode = ( $new ? self::AS_SUCCESS_NEW_ARTICLE : self::AS_SUCCESS_UPDATE );
2607 return Status::newGood( $statusCode );
2608 }
2609
2616 private function handleFailedConstraint( IEditConstraint $failed ): void {
2617 if ( $failed instanceof AuthorizationConstraint ) {
2618 // Auto-block user's IP if the account was "hard" blocked
2619 if (
2620 !MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly()
2621 && $failed->getLegacyStatus()->value === self::AS_BLOCKED_PAGE_FOR_USER
2622 ) {
2623 $this->context->getUser()->spreadAnyEditBlock();
2624 }
2625 } elseif ( $failed instanceof DefaultTextConstraint ) {
2626 $this->blankArticle = true;
2627 } elseif ( $failed instanceof EditFilterMergedContentHookConstraint ) {
2628 $this->hookError = $failed->getHookError();
2629 } elseif (
2630 // ExistingSectionEditConstraint also checks for revisions deleted
2631 // since the edit was loaded, which doesn't indicate a missing summary
2632 (
2633 $failed instanceof ExistingSectionEditConstraint
2634 && $failed->getLegacyStatus()->value === self::AS_SUMMARY_NEEDED
2635 ) ||
2636 $failed instanceof NewSectionMissingSubjectConstraint
2637 ) {
2638 $this->missingSummary = true;
2639 } elseif ( $failed instanceof RedirectConstraint ) {
2640 $this->problematicRedirectTarget = $failed->problematicTarget;
2641 }
2642 }
2643
2654 private function isUndoClean( Content $content ): bool {
2655 // Check whether the undo was "clean", that is the user has not modified
2656 // the automatically generated content.
2657 $undoRev = $this->revisionStore->getRevisionById( $this->undidRev );
2658 if ( $undoRev === null ) {
2659 return false;
2660 }
2661
2662 if ( $this->undoAfter ) {
2663 $oldRev = $this->revisionStore->getRevisionById( $this->undoAfter );
2664 } else {
2665 $oldRev = $this->revisionStore->getPreviousRevision( $undoRev );
2666 }
2667
2668 if ( $oldRev === null ||
2669 $undoRev->isDeleted( RevisionRecord::DELETED_TEXT ) ||
2670 $oldRev->isDeleted( RevisionRecord::DELETED_TEXT )
2671 ) {
2672 return false;
2673 }
2674
2675 $undoContent = $this->getUndoContent( $undoRev, $oldRev, $undoError );
2676 if ( !$undoContent ) {
2677 return false;
2678 }
2679
2680 // Do a pre-save transform on the retrieved undo content
2681 $services = MediaWikiServices::getInstance();
2682 $contentLanguage = $services->getContentLanguage();
2683 $user = $this->getUserForPreview();
2684 $parserOptions = ParserOptions::newFromUserAndLang( $user, $contentLanguage );
2685 $contentTransformer = $services->getContentTransformer();
2686 $undoContent = $contentTransformer->preSaveTransform( $undoContent, $this->mTitle, $user, $parserOptions );
2687
2688 if ( $undoContent->equals( $content ) ) {
2689 return true;
2690 }
2691 return false;
2692 }
2693
2700 private function addContentModelChangeLogEntry( UserIdentity $user, $oldModel, $newModel, $reason = "" ): void {
2701 $new = $oldModel === false;
2702 $log = new ManualLogEntry( 'contentmodel', $new ? 'new' : 'change' );
2703 $log->setPerformer( $user );
2704 $log->setTarget( $this->mTitle );
2705 $log->setComment( is_string( $reason ) ? $reason : "" );
2706 $log->setParameters( [
2707 '4::oldmodel' => $oldModel,
2708 '5::newmodel' => $newModel
2709 ] );
2710 $logid = $log->insert();
2711 $log->publish( $logid );
2712 }
2713
2717 private function updateWatchlist(): void {
2718 if ( $this->tempUserCreateActive ) {
2719 return;
2720 }
2721 $user = $this->getUserForSave();
2722 if ( !$user->isNamed() ) {
2723 return;
2724 }
2725
2726 $title = $this->mTitle;
2727 $watch = $this->watchthis;
2728 $watchlistExpiry = $this->watchlistExpiry;
2729
2730 // This can't run as a DeferredUpdate due to a possible race condition
2731 // when the post-edit redirect happens if the pendingUpdates queue is
2732 // too large to finish in time (T259564)
2733 $this->watchlistManager->setWatch( $watch, $user, $title, $watchlistExpiry );
2734
2735 $this->watchedItemStore->maybeEnqueueWatchlistExpiryJob();
2736 }
2737
2748 private function mergeChangesIntoContent( Content $editContent ) {
2749 // This is the revision that was current at the time editing was initiated on the client,
2750 // even if the edit was based on an old revision.
2751 $baseRevRecord = $this->getExpectedParentRevision();
2752 $baseContent = $baseRevRecord ?
2753 $baseRevRecord->getContent( SlotRecord::MAIN ) :
2754 null;
2755
2756 if ( $baseContent === null ) {
2757 return false;
2758 } elseif ( $baseRevRecord->isCurrent() ) {
2759 // Impossible to have a conflict when the user just edited the latest revision. This can
2760 // happen e.g. when $wgDiff3 is badly configured.
2761 return [ $editContent, $baseRevRecord->getId() ];
2762 }
2763
2764 // The current state, we want to merge updates into it
2765 $currentRevisionRecord = $this->revisionStore->getRevisionByTitle(
2766 $this->mTitle,
2767 0,
2768 IDBAccessObject::READ_LATEST
2769 );
2770 $currentContent = $currentRevisionRecord
2771 ? $currentRevisionRecord->getContent( SlotRecord::MAIN )
2772 : null;
2773
2774 if ( $currentContent === null ) {
2775 return false;
2776 }
2777
2778 $mergedContent = $this->contentHandlerFactory
2779 ->getContentHandler( $baseContent->getModel() )
2780 ->merge3( $baseContent, $editContent, $currentContent );
2781
2782 if ( $mergedContent ) {
2783 // Also need to update parentRevId to what we just merged.
2784 return [ $mergedContent, $currentRevisionRecord->getId() ];
2785 }
2786
2787 return false;
2788 }
2789
2797 public function getExpectedParentRevision() {
2798 if ( $this->mExpectedParentRevision === false ) {
2799 $revRecord = null;
2800 if ( $this->editRevId ) {
2801 $revRecord = $this->revisionStore->getRevisionById(
2802 $this->editRevId,
2803 IDBAccessObject::READ_LATEST
2804 );
2805 } elseif ( $this->edittime ) {
2806 $revRecord = $this->revisionStore->getRevisionByTimestamp(
2807 $this->getTitle(),
2808 $this->edittime,
2809 IDBAccessObject::READ_LATEST
2810 );
2811 }
2812 $this->mExpectedParentRevision = $revRecord;
2813 }
2814 return $this->mExpectedParentRevision;
2815 }
2816
2817 public function setHeaders() {
2818 $out = $this->context->getOutput();
2819
2820 $out->addModules( 'mediawiki.action.edit' );
2821 $out->addModuleStyles( [
2822 'mediawiki.action.edit.styles',
2823 'mediawiki.codex.messagebox.styles',
2824 'mediawiki.editfont.styles',
2825 'mediawiki.interface.helpers.styles',
2826 ] );
2827
2828 $user = $this->context->getUser();
2829
2830 if ( $this->userOptionsLookup->getOption( $user, 'uselivepreview' ) ) {
2831 $out->addModules( 'mediawiki.action.edit.preview' );
2832 }
2833
2834 if ( $this->userOptionsLookup->getOption( $user, 'useeditwarning' ) ) {
2835 $out->addModules( 'mediawiki.action.edit.editWarning' );
2836 }
2837
2838 if ( $this->context->getConfig()->get( MainConfigNames::EnableEditRecovery )
2839 && $this->userOptionsLookup->getOption( $user, 'editrecovery' )
2840 ) {
2841 $wasPosted = $this->getContext()->getRequest()->getMethod() === 'POST';
2842 $out->addJsConfigVars( 'wgEditRecoveryWasPosted', $wasPosted );
2843 $out->addModules( 'mediawiki.editRecovery.edit' );
2844 }
2845
2846 # Enabled article-related sidebar, toplinks, etc.
2847 $out->setArticleRelated( true );
2848
2849 $contextTitle = $this->getContextTitle();
2850 if ( $this->isConflict ) {
2851 $msg = 'editconflict';
2852 } elseif ( $contextTitle->exists() && $this->section != '' ) {
2853 $msg = $this->section === 'new' ? 'editingcomment' : 'editingsection';
2854 } else {
2855 $msg = $contextTitle->exists()
2856 || ( $contextTitle->getNamespace() === NS_MEDIAWIKI
2857 && $contextTitle->getDefaultMessageText() !== false
2858 )
2859 ? 'editing'
2860 : 'creating';
2861 }
2862
2863 # Use the title defined by DISPLAYTITLE magic word when present
2864 # NOTE: getDisplayTitle() returns HTML while getPrefixedText() returns plain text.
2865 # Escape ::getPrefixedText() so that we have HTML in all cases,
2866 # and pass as a "raw" parameter to ::setPageTitleMsg().
2867 $displayTitle = $this->mParserOutput ? $this->mParserOutput->getDisplayTitle() : false;
2868 if ( $displayTitle === false ) {
2869 $displayTitle = htmlspecialchars(
2870 $contextTitle->getPrefixedText(), ENT_QUOTES, 'UTF-8', false
2871 );
2872 } else {
2873 $out->setDisplayTitle( $displayTitle );
2874 }
2875
2876 // Enclose the title with an element. This is used on live preview to update the
2877 // preview of the display title.
2878 $displayTitle = Html::rawElement( 'span', [ 'id' => 'firstHeadingTitle' ], $displayTitle );
2879
2880 $out->setPageTitleMsg( $this->context->msg( $msg )->rawParams( $displayTitle ) );
2881
2882 $config = $this->context->getConfig();
2883
2884 # Transmit the name of the message to JavaScript. This was added for live preview.
2885 # Live preview doesn't use this anymore. The variable is still transmitted because
2886 # Edit Recovery and user scripts use it.
2887 $out->addJsConfigVars( [
2888 'wgEditMessage' => $msg,
2889 ] );
2890
2891 // Add whether to use 'save' or 'publish' messages to JavaScript for post-edit, other
2892 // editors, etc.
2893 $out->addJsConfigVars(
2894 'wgEditSubmitButtonLabelPublish',
2895 $config->get( MainConfigNames::EditSubmitButtonLabelPublish )
2896 );
2897 }
2898
2902 private function showIntro(): void {
2903 $services = MediaWikiServices::getInstance();
2904
2905 // Hardcoded list of notices that are suppressable for historical reasons.
2906 // This feature was originally added for LiquidThreads, to avoid showing non-essential messages
2907 // when commenting in a thread, but some messages were included (or excluded) by mistake before
2908 // its implementation was moved to one place, and this list doesn't make a lot of sense.
2909 // TODO: Remove the suppressIntro feature from EditPage, and invent a better way for extensions
2910 // to skip individual intro messages.
2911 $skip = $this->suppressIntro ? [
2912 'editintro',
2913 'code-editing-intro',
2914 'sharedupload-desc-create',
2915 'sharedupload-desc-edit',
2916 'userpage-userdoesnotexist',
2917 'blocked-notice-logextract',
2918 'newarticletext',
2919 'newarticletextanon',
2920 'recreate-moveddeleted-warn',
2921 ] : [];
2922
2923 $messages = $services->getIntroMessageBuilder()->getIntroMessages(
2924 IntroMessageBuilder::MORE_FRAMES,
2925 $skip,
2926 $this->context,
2927 $this->mTitle->toPageIdentity(),
2928 $this->mArticle->fetchRevisionRecord(),
2929 $this->context->getUser(),
2930 $this->context->getRequest()->getVal( 'editintro' ),
2932 array_diff_key(
2933 $this->context->getRequest()->getQueryValues(),
2934 [ 'title' => true, 'returnto' => true, 'returntoquery' => true ]
2935 )
2936 ),
2937 !$this->firsttime,
2938 $this->section !== '' ? $this->section : null
2939 );
2940
2941 foreach ( $messages as $message ) {
2942 $this->context->getOutput()->addHTML( $message );
2943 }
2944 }
2945
2964 private function toEditText( $content ) {
2965 if ( $content === null || $content === false ) {
2966 return '';
2967 }
2968 if ( is_string( $content ) ) {
2969 return $content;
2970 }
2971
2972 if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2973 throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2974 }
2975
2976 return $content->serialize( $this->contentFormat );
2977 }
2978
2995 protected function toEditContent( $text ) {
2996 if ( $text === false || $text === null ) {
2997 return $text;
2998 }
2999
3000 $content = ContentHandler::makeContent( $text, $this->getTitle(),
3001 $this->contentModel, $this->contentFormat );
3002
3003 if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
3004 throw new MWException( 'This content model is not supported: ' . $content->getModel() );
3005 }
3006
3007 return $content;
3008 }
3009
3013 public function showEditForm() {
3014 # need to parse the preview early so that we know which templates are used,
3015 # otherwise users with "show preview after edit box" will get a blank list
3016 # we parse this near the beginning so that setHeaders can do the title
3017 # setting work instead of leaving it in getPreviewText
3018 $previewOutput = '';
3019 if ( $this->formtype === 'preview' ) {
3020 $previewOutput = $this->getPreviewText();
3021 }
3022
3023 $out = $this->context->getOutput();
3024
3025 // FlaggedRevs depends on running this hook before adding edit notices in showIntro() (T337637)
3026 $this->getHookRunner()->onEditPage__showEditForm_initial( $this, $out );
3027
3028 $this->setHeaders();
3029
3030 // Show applicable editing introductions
3031 $this->showIntro();
3032
3033 if ( !$this->isConflict &&
3034 $this->section !== '' &&
3035 !$this->isSectionEditSupported()
3036 ) {
3037 // We use $this->section to much before this and getVal('wgSection') directly in other places
3038 // at this point we can't reset $this->section to '' to fallback to non-section editing.
3039 // Someone is welcome to try refactoring though
3040 $out->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
3041 return;
3042 }
3043
3044 $this->showHeader();
3045
3046 $out->addHTML( $this->editFormPageTop );
3047
3048 $user = $this->context->getUser();
3049 if ( $this->userOptionsLookup->getOption( $user, 'previewontop' ) ) {
3050 $this->displayPreviewArea( $previewOutput, true );
3051 }
3052
3053 $out->addHTML( $this->editFormTextTop );
3054
3055 if ( $this->formtype !== 'save' && $this->wasDeletedSinceLastEdit() ) {
3056 $out->addHTML( Html::errorBox(
3057 $out->msg( 'deletedwhileediting' )->parse(),
3058 '',
3059 'mw-deleted-while-editing'
3060 ) );
3061 }
3062
3063 // @todo add EditForm plugin interface and use it here!
3064 // search for textarea1 and textarea2, and allow EditForm to override all uses.
3065 $out->addHTML( Html::openElement(
3066 'form',
3067 [
3068 'class' => 'mw-editform',
3069 'id' => self::EDITFORM_ID,
3070 'name' => self::EDITFORM_ID,
3071 'method' => 'post',
3072 'action' => $this->getActionURL( $this->getContextTitle() ),
3073 'enctype' => 'multipart/form-data',
3074 'data-mw-editform-type' => $this->formtype
3075 ]
3076 ) );
3077
3078 // Add a check for Unicode support
3079 $out->addHTML( Html::hidden( 'wpUnicodeCheck', self::UNICODE_CHECK ) );
3080
3081 // Add an empty field to trip up spambots
3082 $out->addHTML(
3083 Html::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
3084 . Html::rawElement(
3085 'label',
3086 [ 'for' => 'wpAntispam' ],
3087 $this->context->msg( 'simpleantispam-label' )->parse()
3088 )
3089 . Html::element(
3090 'input',
3091 [
3092 'type' => 'text',
3093 'name' => 'wpAntispam',
3094 'id' => 'wpAntispam',
3095 'value' => ''
3096 ]
3097 )
3098 . Html::closeElement( 'div' )
3099 );
3100
3101 $this->getHookRunner()->onEditPage__showEditForm_fields( $this, $out );
3102
3103 // Put these up at the top to ensure they aren't lost on early form submission
3104 $this->showFormBeforeText();
3105
3106 if ( $this->formtype === 'save' && $this->wasDeletedSinceLastEdit() ) {
3107 $username = $this->lastDelete->actor_name;
3108 $comment = $this->commentStore->getComment( 'log_comment', $this->lastDelete )->text;
3109
3110 // It is better to not parse the comment at all than to have templates expanded in the middle
3111 // TODO: can the label be moved outside of the div so that wrapWikiMsg could be used?
3112 $key = $comment === ''
3113 ? 'confirmrecreate-noreason'
3114 : 'confirmrecreate';
3115 $out->addHTML( Html::rawElement(
3116 'div',
3117 [ 'class' => 'mw-confirm-recreate' ],
3118 $this->context->msg( $key )
3119 ->params( $username )
3120 ->plaintextParams( $comment )
3121 ->parse() .
3122 Html::rawElement(
3123 'div',
3124 [],
3125 Html::check(
3126 'wpRecreate',
3127 false,
3128 [ 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ]
3129 )
3130 . "\u{00A0}" .
3131 Html::label(
3132 $this->context->msg( 'recreate' )->text(),
3133 'wpRecreate',
3134 [ 'title' => Linker::titleAttrib( 'recreate' ) ]
3135 )
3136 )
3137 ) );
3138 }
3139
3140 # When the summary is hidden, also hide them on preview/show changes
3141 if ( $this->nosummary ) {
3142 $out->addHTML( Html::hidden( 'nosummary', true ) );
3143 }
3144
3145 # If a blank edit summary was previously provided, and the appropriate
3146 # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
3147 # user being bounced back more than once in the event that a summary
3148 # is not required.
3149 # ####
3150 # For a bit more sophisticated detection of blank summaries, hash the
3151 # automatic one and pass that in the hidden field wpAutoSummary.
3152 if (
3153 $this->missingSummary ||
3154 // @phan-suppress-next-line PhanSuspiciousValueComparison
3155 ( $this->section === 'new' && $this->nosummary ) ||
3156 $this->allowBlankSummary
3157 ) {
3158 $out->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
3159 }
3160
3161 if ( $this->undidRev ) {
3162 $out->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
3163 }
3164 if ( $this->undoAfter ) {
3165 $out->addHTML( Html::hidden( 'wpUndoAfter', $this->undoAfter ) );
3166 }
3167
3168 if ( $this->problematicRedirectTarget !== null ) {
3169 // T395767, T395768: Save the target to a variable so the constraint can fail again if the redirect is
3170 // still problematic but has changed between two save attempts
3171 $out->addHTML( Html::hidden(
3172 'wpAllowedProblematicRedirectTarget',
3173 $this->problematicRedirectTarget->getFullText()
3174 ) );
3175 }
3176
3177 $autosumm = $this->autoSumm !== '' ? $this->autoSumm : md5( $this->summary );
3178 $out->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
3179
3180 $out->addHTML( Html::hidden( 'oldid', $this->oldid ) );
3181 $out->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
3182
3183 $out->addHTML( Html::hidden( 'format', $this->contentFormat ) );
3184 $out->addHTML( Html::hidden( 'model', $this->contentModel ) );
3185 if ( $this->changeTags ) {
3186 $out->addHTML( Html::hidden( 'wpChangeTagsAfterPreview', implode( ',', $this->changeTags ) ) );
3187 }
3188
3189 $out->enableOOUI();
3190
3191 if ( $this->section === 'new' ) {
3192 $this->showSummaryInput( true );
3193 $out->addHTML( $this->getSummaryPreview( true ) );
3194 }
3195
3196 $out->addHTML( $this->editFormTextBeforeContent );
3197 if ( $this->isConflict ) {
3198 $currentText = $this->toEditText( $this->getCurrentContent() );
3199
3200 $editConflictHelper = $this->getEditConflictHelper();
3201 $editConflictHelper->setTextboxes( $this->textbox1, $currentText );
3202 $editConflictHelper->setContentModel( $this->contentModel );
3203 $editConflictHelper->setContentFormat( $this->contentFormat );
3204 $out->addHTML( $editConflictHelper->getEditFormHtmlBeforeContent() );
3205
3206 $this->textbox2 = $this->textbox1;
3207 $this->textbox1 = $currentText;
3208 }
3209
3210 if ( !$this->mTitle->isUserConfigPage() ) {
3211 $out->addHTML( self::getEditToolbar() );
3212 }
3213
3214 if ( $this->blankArticle ) {
3215 $out->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
3216 }
3217
3218 if ( $this->isConflict ) {
3219 // In an edit conflict bypass the overridable content form method
3220 // and fallback to the raw wpTextbox1 since editconflicts can't be
3221 // resolved between page source edits and custom ui edits using the
3222 // custom edit ui.
3223 $conflictTextBoxAttribs = [];
3224 if ( $this->wasDeletedSinceLastEdit() ) {
3225 $conflictTextBoxAttribs['style'] = 'display:none;';
3226 } elseif ( $this->isOldRev ) {
3227 $conflictTextBoxAttribs['class'] = 'mw-textarea-oldrev';
3228 }
3229
3230 // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable
3231 // $editConflictHelper is declard, when isConflict is true
3232 $out->addHTML( $editConflictHelper->getEditConflictMainTextBox( $conflictTextBoxAttribs ) );
3233 // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable
3234 // $editConflictHelper is declard, when isConflict is true
3235 $out->addHTML( $editConflictHelper->getEditFormHtmlAfterContent() );
3236 } else {
3237 $this->showContentForm();
3238 }
3239
3240 $out->addHTML( $this->editFormTextAfterContent );
3241
3242 $this->showStandardInputs();
3243
3244 $this->showFormAfterText();
3245
3246 $this->showTosSummary();
3247
3248 $this->showEditTools();
3249
3250 $out->addHTML( $this->editFormTextAfterTools . "\n" );
3251
3252 $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
3253
3254 $out->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
3255 Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
3256
3257 $out->addHTML( Html::rawElement( 'div', [ 'class' => 'limitreport' ],
3258 self::getPreviewLimitReport( $this->mParserOutput ) ) );
3259
3260 $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
3261
3262 if ( $this->isConflict ) {
3263 try {
3264 $this->showConflict();
3265 } catch ( MWContentSerializationException $ex ) {
3266 // this can't really happen, but be nice if it does.
3267 $out->addHTML( Html::errorBox(
3268 $this->context->msg(
3269 'content-failed-to-parse',
3270 $this->contentModel,
3271 $this->contentFormat,
3272 $ex->getMessage()
3273 )->parse()
3274 ) );
3275 }
3276 }
3277
3278 // Set a hidden field so JS knows what edit form mode we are in
3279 if ( $this->isConflict ) {
3280 $mode = 'conflict';
3281 } elseif ( $this->preview ) {
3282 $mode = 'preview';
3283 } elseif ( $this->diff ) {
3284 $mode = 'diff';
3285 } else {
3286 $mode = 'text';
3287 }
3288 $out->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
3289
3290 // Marker for detecting truncated form data. This must be the last
3291 // parameter sent in order to be of use, so do not move me.
3292 $out->addHTML( Html::hidden( 'wpUltimateParam', true ) );
3293 $out->addHTML( $this->editFormTextBottom . "\n</form>\n" );
3294
3295 if ( !$this->userOptionsLookup->getOption( $user, 'previewontop' ) ) {
3296 $this->displayPreviewArea( $previewOutput, false );
3297 }
3298 }
3299
3307 public function makeTemplatesOnThisPageList( array $templates ) {
3308 $templateListFormatter = new TemplatesOnThisPageFormatter(
3309 $this->context,
3310 $this->linkRenderer,
3311 $this->linkBatchFactory,
3312 $this->restrictionStore
3313 );
3314
3315 // preview if preview, else section if section, else false
3316 $type = false;
3317 if ( $this->preview ) {
3318 $type = 'preview';
3319 } elseif ( $this->section !== '' ) {
3320 $type = 'section';
3321 }
3322
3323 return Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
3324 $templateListFormatter->format( $templates, $type )
3325 );
3326 }
3327
3334 private static function extractSectionTitle( $text ) {
3335 if ( preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches ) ) {
3336 return MediaWikiServices::getInstance()->getParser()
3337 ->stripSectionName( trim( $matches[2] ) );
3338 } else {
3339 return false;
3340 }
3341 }
3342
3343 private function showHeader(): void {
3344 $out = $this->context->getOutput();
3345 $user = $this->context->getUser();
3346 if ( $this->isConflict ) {
3347 $this->addExplainConflictHeader();
3348 $this->editRevId = $this->page->getLatest();
3349 } else {
3350 if ( $this->section !== '' && $this->section !== 'new' && $this->summary === '' &&
3351 !$this->preview && !$this->diff
3352 ) {
3353 $sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object
3354 if ( $sectionTitle !== false ) {
3355 $this->summary = "/* $sectionTitle */ ";
3356 }
3357 }
3358
3359 $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
3360
3361 if ( $this->missingSummary && $this->section !== 'new' ) {
3362 $out->wrapWikiMsg(
3363 "<div id='mw-missingsummary'>\n$1\n</div>",
3364 [ 'missingsummary', $buttonLabel ]
3365 );
3366 }
3367
3368 if ( $this->missingSummary && $this->section === 'new' ) {
3369 $out->wrapWikiMsg(
3370 "<div id='mw-missingcommentheader'>\n$1\n</div>",
3371 [ 'missingcommentheader', $buttonLabel ]
3372 );
3373 }
3374
3375 if ( $this->hookError !== '' ) {
3376 $out->addWikiTextAsInterface( $this->hookError );
3377 }
3378
3379 if ( $this->section != 'new' ) {
3380 $revRecord = $this->mArticle->fetchRevisionRecord();
3381 if ( $revRecord && $revRecord instanceof RevisionStoreRecord ) {
3382 // Let sysop know that this will make private content public if saved
3383
3384 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $user ) ) {
3385 $out->addHTML(
3386 Html::warningBox(
3387 $out->msg( 'rev-deleted-text-permission', $this->mTitle->getPrefixedURL() )->parse(),
3388 'plainlinks'
3389 )
3390 );
3391 } elseif ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
3392 $out->addHTML(
3393 Html::warningBox(
3394 // title used in wikilinks, should not contain whitespaces
3395 $out->msg( 'rev-deleted-text-view', $this->mTitle->getPrefixedURL() )->parse(),
3396 'plainlinks'
3397 )
3398 );
3399 }
3400
3401 if ( !$revRecord->isCurrent() ) {
3402 $this->mArticle->setOldSubtitle( $revRecord->getId() );
3403 $this->isOldRev = true;
3404 }
3405 } elseif ( $this->mTitle->exists() ) {
3406 // Something went wrong
3407
3408 $out->addHTML(
3409 Html::errorBox(
3410 $out->msg( 'missing-revision', $this->oldid )->parse()
3411 )
3412 );
3413 }
3414 }
3415 }
3416
3417 $this->addLongPageWarningHeader();
3418 }
3419
3427 private function getSummaryInputAttributes( array $inputAttrs ): array {
3428 // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
3429 // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
3430 // Unicode codepoints.
3431 return $inputAttrs + [
3432 'id' => 'wpSummary',
3433 'name' => 'wpSummary',
3434 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
3435 'tabindex' => 1,
3436 'size' => 60,
3437 'spellcheck' => 'true',
3438 ];
3439 }
3440
3450 private function getSummaryInputWidget( $summary, string $labelText, array $inputAttrs ): FieldLayout {
3451 $inputAttrs = OOUI\Element::configFromHtmlAttributes(
3452 $this->getSummaryInputAttributes( $inputAttrs )
3453 );
3454 $inputAttrs += [
3455 'title' => Linker::titleAttrib( 'summary' ),
3456 'accessKey' => Linker::accesskey( 'summary' ),
3457 ];
3458
3459 // For compatibility with old scripts and extensions, we want the legacy 'id' on the `<input>`
3460 $inputAttrs['inputId'] = $inputAttrs['id'];
3461 $inputAttrs['id'] = 'wpSummaryWidget';
3462
3463 return new OOUI\FieldLayout(
3464 new OOUI\TextInputWidget( [
3465 'value' => $summary,
3466 'infusable' => true,
3467 ] + $inputAttrs ),
3468 [
3469 'label' => new OOUI\HtmlSnippet( $labelText ),
3470 'align' => 'top',
3471 'id' => 'wpSummaryLabel',
3472 'classes' => [ $this->missingSummary ? 'mw-summarymissed' : 'mw-summary' ],
3473 ]
3474 );
3475 }
3476
3482 private function showSummaryInput( bool $isSubjectPreview ): void {
3483 # Add a class if 'missingsummary' is triggered to allow styling of the summary line
3484 $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
3485 if ( $isSubjectPreview ) {
3486 if ( $this->nosummary ) {
3487 return;
3488 }
3489 } elseif ( !$this->mShowSummaryField ) {
3490 return;
3491 }
3492
3493 $labelText = $this->context->msg( $isSubjectPreview ? 'subject' : 'summary' )->parse();
3494 $this->context->getOutput()->addHTML(
3495 $this->getSummaryInputWidget(
3496 $isSubjectPreview ? $this->sectiontitle : $this->summary,
3497 $labelText,
3498 [ 'class' => $summaryClass ]
3499 )->toString()
3500 );
3501 }
3502
3509 private function getSummaryPreview( bool $isSubjectPreview ): string {
3510 // avoid spaces in preview, gets always trimmed on save
3511 $summary = trim( $this->summary );
3512 if ( $summary === '' || ( !$this->preview && !$this->diff ) ) {
3513 return "";
3514 }
3515
3516 $commentFormatter = MediaWikiServices::getInstance()->getCommentFormatter();
3517 $summary = $this->context->msg( 'summary-preview' )->parse()
3518 . $commentFormatter->formatBlock( $summary, $this->mTitle, $isSubjectPreview );
3519 return Html::rawElement( 'div', [ 'class' => 'mw-summary-preview' ], $summary );
3520 }
3521
3522 private function showFormBeforeText(): void {
3523 $out = $this->context->getOutput();
3524 $out->addHTML( Html::hidden( 'wpSection', $this->section ) );
3525 $out->addHTML( Html::hidden( 'wpStarttime', $this->starttime ) );
3526 $out->addHTML( Html::hidden( 'wpEdittime', $this->edittime ) );
3527 $out->addHTML( Html::hidden( 'editRevId', $this->editRevId ) );
3528 $out->addHTML( Html::hidden( 'wpScrolltop', $this->scrolltop, [ 'id' => 'wpScrolltop' ] ) );
3529 }
3530
3531 protected function showFormAfterText() {
3544 $this->context->getOutput()->addHTML(
3545 "\n" .
3546 Html::hidden( "wpEditToken", $this->context->getUser()->getEditToken() ) .
3547 "\n"
3548 );
3549 }
3550
3559 protected function showContentForm() {
3560 $this->showTextbox1();
3561 }
3562
3563 private function showTextbox1(): void {
3564 if ( $this->formtype === 'save' && $this->wasDeletedSinceLastEdit() ) {
3565 $attribs = [ 'style' => 'display:none;' ];
3566 } else {
3567 $builder = new TextboxBuilder();
3568 $classes = $builder->getTextboxProtectionCSSClasses( $this->getTitle() );
3569
3570 # Is an old revision being edited?
3571 if ( $this->isOldRev ) {
3572 $classes[] = 'mw-textarea-oldrev';
3573 }
3574
3575 $attribs = [
3576 'aria-label' => $this->context->msg( 'edit-textarea-aria-label' )->text(),
3577 'tabindex' => 1,
3578 'class' => $classes,
3579 ];
3580 }
3581
3582 $this->showTextbox(
3583 $this->textbox1,
3584 'wpTextbox1',
3585 $attribs
3586 );
3587 }
3588
3589 protected function showTextbox( string $text, string $name, array $customAttribs = [] ) {
3590 $builder = new TextboxBuilder();
3591 $attribs = $builder->buildTextboxAttribs(
3592 $name,
3593 $customAttribs,
3594 $this->context->getUser(),
3595 $this->mTitle
3596 );
3597
3598 $this->context->getOutput()->addHTML(
3599 Html::textarea( $name, $builder->addNewLineAtEnd( $text ), $attribs )
3600 );
3601 }
3602
3603 private function displayPreviewArea( string $previewOutput, bool $isOnTop ): void {
3604 $attribs = [ 'id' => 'wikiPreview' ];
3605 if ( $isOnTop ) {
3606 $attribs['class'] = 'ontop';
3607 }
3608 if ( $this->formtype !== 'preview' ) {
3609 $attribs['style'] = 'display: none;';
3610 }
3611
3612 $out = $this->context->getOutput();
3613 $out->addHTML( Html::openElement( 'div', $attribs ) );
3614
3615 if ( $this->formtype === 'preview' ) {
3616 $this->showPreview( $previewOutput );
3617 }
3618
3619 $out->addHTML( '</div>' );
3620
3621 if ( $this->formtype === 'diff' ) {
3622 try {
3623 $this->showDiff();
3624 } catch ( MWContentSerializationException $ex ) {
3625 $out->addHTML( Html::errorBox(
3626 $this->context->msg(
3627 'content-failed-to-parse',
3628 $this->contentModel,
3629 $this->contentFormat,
3630 $ex->getMessage()
3631 )->parse()
3632 ) );
3633 }
3634 }
3635 }
3636
3643 private function showPreview( string $text ): void {
3644 if ( $this->mArticle instanceof CategoryPage ) {
3645 $this->mArticle->openShowCategory();
3646 }
3647 # This hook seems slightly odd here, but makes things more
3648 # consistent for extensions.
3649 $out = $this->context->getOutput();
3650 $this->getHookRunner()->onOutputPageBeforeHTML( $out, $text );
3651 $out->addHTML( $text );
3652 if ( $this->mArticle instanceof CategoryPage ) {
3653 $this->mArticle->closeShowCategory();
3654 }
3655 }
3656
3664 public function showDiff() {
3665 $oldtitlemsg = 'currentrev';
3666 # if message does not exist, show diff against the preloaded default
3667 if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI && !$this->mTitle->exists() ) {
3668 $oldtext = $this->mTitle->getDefaultMessageText();
3669 if ( $oldtext !== false ) {
3670 $oldtitlemsg = 'defaultmessagetext';
3671 $oldContent = $this->toEditContent( $oldtext );
3672 } else {
3673 $oldContent = null;
3674 }
3675 } else {
3676 $oldContent = $this->getCurrentContent();
3677 }
3678
3679 $textboxContent = $this->toEditContent( $this->textbox1 );
3680 if ( $this->editRevId !== null ) {
3681 $newContent = $this->page->replaceSectionAtRev(
3682 $this->section, $textboxContent, $this->sectiontitle, $this->editRevId
3683 );
3684 } else {
3685 $newContent = $this->page->replaceSectionContent(
3686 $this->section, $textboxContent, $this->sectiontitle, $this->edittime
3687 );
3688 }
3689
3690 if ( $newContent ) {
3691 $this->getHookRunner()->onEditPageGetDiffContent( $this, $newContent );
3692
3693 $user = $this->getUserForPreview();
3694 $parserOptions = ParserOptions::newFromUserAndLang( $user,
3695 MediaWikiServices::getInstance()->getContentLanguage() );
3696 $services = MediaWikiServices::getInstance();
3697 $contentTransformer = $services->getContentTransformer();
3698 $newContent = $contentTransformer->preSaveTransform( $newContent, $this->mTitle, $user, $parserOptions );
3699 }
3700
3701 if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
3702 $oldtitle = $this->context->msg( $oldtitlemsg )->parse();
3703 $newtitle = $this->context->msg( 'yourtext' )->parse();
3704
3705 if ( !$oldContent ) {
3706 $oldContent = $newContent->getContentHandler()->makeEmptyContent();
3707 }
3708
3709 if ( !$newContent ) {
3710 $newContent = $oldContent->getContentHandler()->makeEmptyContent();
3711 }
3712
3713 $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->context );
3714 $de->setContent( $oldContent, $newContent );
3715
3716 $difftext = $de->getDiff( $oldtitle, $newtitle );
3717 $de->showDiffStyle();
3718 } else {
3719 $difftext = '';
3720 }
3721
3722 $this->context->getOutput()->addHTML( Html::rawElement( 'div', [ 'id' => 'wikiDiff' ], $difftext ) );
3723 }
3724
3733 private function showTosSummary(): void {
3734 $msgKey = 'editpage-tos-summary';
3735 $this->getHookRunner()->onEditPageTosSummary( $this->mTitle, $msgKey );
3736 $msg = $this->context->msg( $msgKey );
3737 if ( !$msg->isDisabled() ) {
3738 $this->context->getOutput()->addHTML( Html::rawElement(
3739 'div',
3740 [ 'class' => 'mw-tos-summary' ],
3741 $msg->parseAsBlock()
3742 ) );
3743 }
3744 }
3745
3750 private function showEditTools(): void {
3751 $this->context->getOutput()->addHTML( Html::rawElement(
3752 'div',
3753 [ 'class' => 'mw-editTools' ],
3754 $this->context->msg( 'edittools' )->inContentLanguage()->parse()
3755 ) );
3756 }
3757
3767 public static function getCopyrightWarning( PageReference $page, string $format, MessageLocalizer $localizer ) {
3768 $services = MediaWikiServices::getInstance();
3769 $rightsText = $services->getMainConfig()->get( MainConfigNames::RightsText );
3770 if ( $rightsText ) {
3771 $copywarnMsg = [ 'copyrightwarning',
3772 '[[' . $localizer->msg( 'copyrightpage' )->inContentLanguage()->text() . ']]',
3773 $rightsText ];
3774 } else {
3775 $copywarnMsg = [ 'copyrightwarning2',
3776 '[[' . $localizer->msg( 'copyrightpage' )->inContentLanguage()->text() . ']]' ];
3777 }
3778 // Allow for site and per-namespace customization of contribution/copyright notice.
3779 $title = Title::newFromPageReference( $page );
3780 ( new HookRunner( $services->getHookContainer() ) )->onEditPageCopyrightWarning( $title, $copywarnMsg );
3781 if ( !$copywarnMsg ) {
3782 return '';
3783 }
3784
3785 $msg = $localizer->msg( ...$copywarnMsg )->page( $page );
3786 return Html::rawElement( 'div', [ 'id' => 'editpage-copywarn' ], $msg->$format() );
3787 }
3788
3796 public static function getPreviewLimitReport( ?ParserOutput $output = null ) {
3797 if ( !$output || !$output->getLimitReportData() ) {
3798 return '';
3799 }
3800
3801 $limitReport = Html::rawElement( 'div', [ 'class' => 'mw-limitReportExplanation' ],
3802 wfMessage( 'limitreport-title' )->parseAsBlock()
3803 );
3804
3805 // Show/hide animation doesn't work correctly on a table, so wrap it in a div.
3806 $limitReport .= Html::openElement( 'div', [ 'class' => 'preview-limit-report-wrapper' ] );
3807
3808 $limitReport .= Html::openElement( 'table', [
3809 'class' => 'preview-limit-report wikitable'
3810 ] ) .
3811 Html::openElement( 'tbody' );
3812
3813 $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
3814 foreach ( $output->getLimitReportData() as $key => $value ) {
3815 if ( in_array( $key, [
3816 'cachereport-origin',
3817 'cachereport-timestamp',
3818 'cachereport-ttl',
3819 'cachereport-transientcontent',
3820 'limitreport-timingprofile',
3821 ] ) ) {
3822 // These entries have non-numeric parameters, and can't be displayed by this code.
3823 // They are used by the plaintext limit report (see RenderDebugInfo::debugInfo()).
3824 // TODO: Display this information in the table somehow.
3825 continue;
3826 }
3827
3828 if ( $hookRunner->onParserLimitReportFormat( $key, $value, $limitReport, true, true ) ) {
3829 $keyMsg = wfMessage( $key );
3830 $valueMsg = wfMessage( "$key-value" );
3831 if ( !$valueMsg->exists() ) {
3832 // This is formatted raw, not as localized number.
3833 // If you want the parameter formatted as a number,
3834 // define the `$key-value` message.
3835 $valueMsg = ( new RawMessage( '$1' ) )->params( $value );
3836 } else {
3837 // If you define the `$key-value` or `$key-value-html`
3838 // message then the argument *must* be numeric.
3839 $valueMsg = $valueMsg->numParams( $value );
3840 }
3841 if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
3842 $limitReport .= Html::openElement( 'tr' ) .
3843 Html::rawElement( 'th', [], $keyMsg->parse() ) .
3844 Html::rawElement( 'td', [], $valueMsg->parse() ) .
3845 Html::closeElement( 'tr' );
3846 }
3847 }
3848 }
3849
3850 $limitReport .= Html::closeElement( 'tbody' ) .
3851 Html::closeElement( 'table' ) .
3852 Html::closeElement( 'div' );
3853
3854 return $limitReport;
3855 }
3856
3857 protected function showStandardInputs( int &$tabindex = 2 ) {
3858 $out = $this->context->getOutput();
3859 $out->addHTML( "<div class='editOptions'>\n" );
3860
3861 if ( $this->section !== 'new' ) {
3862 $this->showSummaryInput( false );
3863 $out->addHTML( $this->getSummaryPreview( false ) );
3864 }
3865
3866 // When previewing, override the selected dropdown option to select whatever was posted
3867 // (if it's a valid option) rather than the current value for watchlistExpiry.
3868 // See also above in $this->importFormDataPosted().
3869 $expiryFromRequest = null;
3870 if ( $this->preview || $this->diff || $this->isConflict ) {
3871 $expiryFromRequest = $this->getContext()->getRequest()->getText( 'wpWatchlistExpiry' );
3872 }
3873
3874 $checkboxes = $this->getCheckboxesWidget(
3875 $tabindex,
3876 [ 'minor' => $this->minoredit, 'watch' => $this->watchthis, 'wpWatchlistExpiry' => $expiryFromRequest ]
3877 );
3878 $checkboxesHTML = new OOUI\HorizontalLayout( [ 'items' => array_values( $checkboxes ) ] );
3879
3880 $out->addHTML( "<div class='editCheckboxes'>" . $checkboxesHTML . "</div>\n" );
3881
3882 // Show copyright warning.
3883 $out->addHTML( self::getCopyrightWarning( $this->mTitle, 'parse', $this->context ) );
3884 $out->addHTML( $this->editFormTextAfterWarn );
3885
3886 $out->addHTML( "<div class='editButtons'>\n" );
3887 $out->addHTML( implode( "\n", $this->getEditButtons( $tabindex ) ) . "\n" );
3888
3889 $cancel = $this->getCancelLink( $tabindex++ );
3890
3891 $edithelp = $this->getHelpLink() .
3892 $this->context->msg( 'word-separator' )->escaped() .
3893 $this->context->msg( 'newwindow' )->parse();
3894
3895 $out->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" );
3896 $out->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" );
3897 $out->addHTML( "</div><!-- editButtons -->\n" );
3898
3899 $this->getHookRunner()->onEditPage__showStandardInputs_options( $this, $out, $tabindex );
3900
3901 $out->addHTML( "</div><!-- editOptions -->\n" );
3902 }
3903
3908 private function showConflict(): void {
3909 $out = $this->context->getOutput();
3910 if ( $this->getHookRunner()->onEditPageBeforeConflictDiff( $this, $out ) ) {
3911 $this->incrementConflictStats();
3912
3913 $this->getEditConflictHelper()->showEditFormTextAfterFooters();
3914 }
3915 }
3916
3917 private function incrementConflictStats(): void {
3918 $this->getEditConflictHelper()->incrementConflictStats( $this->context->getUser() );
3919 }
3920
3921 private function getHelpLink(): string {
3922 $message = $this->context->msg( 'edithelppage' )->inContentLanguage()->text();
3923 $editHelpUrl = Skin::makeInternalOrExternalUrl( $message );
3924 return Html::element( 'a', [
3925 'href' => $editHelpUrl,
3926 'target' => 'helpwindow'
3927 ], $this->context->msg( 'edithelp' )->text() );
3928 }
3929
3934 private function getCancelLink( int $tabindex ): ButtonWidget {
3935 $cancelParams = [];
3936 if ( !$this->isConflict && $this->oldid > 0 ) {
3937 $cancelParams['oldid'] = $this->oldid;
3938 } elseif ( $this->getContextTitle()->isRedirect() ) {
3939 $cancelParams['redirect'] = 'no';
3940 }
3941
3942 return new OOUI\ButtonWidget( [
3943 'id' => 'mw-editform-cancel',
3944 'tabIndex' => $tabindex,
3945 'href' => $this->getContextTitle()->getLinkURL( $cancelParams ),
3946 'label' => new OOUI\HtmlSnippet( $this->context->msg( 'cancel' )->parse() ),
3947 'framed' => false,
3948 'infusable' => true,
3949 'flags' => 'destructive',
3950 ] );
3951 }
3952
3962 protected function getActionURL( Title $title ) {
3963 $request = $this->context->getRequest();
3964 $params = $request->getQueryValuesOnly();
3965
3966 $allowedFormParams = [
3967 'section', 'oldid', 'preloadtitle', 'undo', 'undoafter',
3968 // Considered safe in all contexts
3969 'uselang', 'useskin', 'useformat', 'variant', 'debug', 'safemode'
3970 ];
3971 $formParams = [ 'action' => $this->action ];
3972 foreach ( $params as $arg => $val ) {
3973 if ( in_array( $arg, $allowedFormParams, true ) ) {
3974 $formParams[$arg] = $val;
3975 }
3976 }
3977
3978 return $title->getLocalURL( $formParams );
3979 }
3980
3987 private function wasDeletedSinceLastEdit(): bool {
3988 if ( $this->deletedSinceEdit !== null ) {
3989 return $this->deletedSinceEdit;
3990 }
3991
3992 $this->deletedSinceEdit = false;
3993
3994 if ( !$this->mTitle->exists() && $this->mTitle->hasDeletedEdits() ) {
3995 $this->lastDelete = $this->getLastDelete();
3996 if ( $this->lastDelete ) {
3997 $deleteTime = wfTimestamp( TS::MW, $this->lastDelete->log_timestamp );
3998 if ( $deleteTime > $this->starttime ) {
3999 $this->deletedSinceEdit = true;
4000 }
4001 }
4002 }
4003
4004 return $this->deletedSinceEdit;
4005 }
4006
4012 private function getLastDelete(): ?stdClass {
4013 $dbr = $this->dbProvider->getReplicaDatabase();
4014 $commentQuery = $this->commentStore->getJoin( 'log_comment' );
4015 $data = $dbr->newSelectQueryBuilder()
4016 ->select( [
4017 'log_type',
4018 'log_action',
4019 'log_timestamp',
4020 'log_namespace',
4021 'log_title',
4022 'log_params',
4023 'log_deleted',
4024 'actor_name'
4025 ] )
4026 ->from( 'logging' )
4027 ->join( 'actor', null, 'actor_id=log_actor' )
4028 ->where( [
4029 'log_namespace' => $this->mTitle->getNamespace(),
4030 'log_title' => $this->mTitle->getDBkey(),
4031 'log_type' => 'delete',
4032 'log_action' => 'delete',
4033 ] )
4034 ->orderBy( [ 'log_timestamp', 'log_id' ], SelectQueryBuilder::SORT_DESC )
4035 ->queryInfo( $commentQuery )
4036 ->caller( __METHOD__ )
4037 ->fetchRow();
4038 // Quick paranoid permission checks...
4039 if ( $data !== false ) {
4040 if ( $data->log_deleted & LogPage::DELETED_USER ) {
4041 $data->actor_name = $this->context->msg( 'rev-deleted-user' )->escaped();
4042 }
4043
4044 if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
4045 $data->log_comment_text = $this->context->msg( 'rev-deleted-comment' )->escaped();
4046 $data->log_comment_data = null;
4047 }
4048 }
4049
4050 return $data ?: null;
4051 }
4052
4058 public function getPreviewText() {
4059 $out = $this->context->getOutput();
4060 $config = $this->context->getConfig();
4061
4062 if ( $config->get( MainConfigNames::RawHtml ) && !$this->mTokenOk ) {
4063 // Could be an offsite preview attempt. This is very unsafe if
4064 // HTML is enabled, as it could be an attack.
4065 $parsedNote = '';
4066 if ( $this->textbox1 !== '' ) {
4067 // Do not put big scary notice, if previewing the empty
4068 // string, which happens when you initially edit
4069 // a category page, due to automatic preview-on-open.
4070 $parsedNote = Html::rawElement( 'div', [ 'class' => 'previewnote' ],
4071 $out->parseAsInterface(
4072 $this->context->msg( 'session_fail_preview_html' )->plain()
4073 ) );
4074 }
4075 $this->incrementEditFailureStats( 'session_loss' );
4076 return $parsedNote;
4077 }
4078
4079 $note = '';
4080
4081 try {
4082 $content = $this->toEditContent( $this->textbox1 );
4083
4084 $previewHTML = '';
4085 if ( !$this->getHookRunner()->onAlternateEditPreview(
4086 $this, $content, $previewHTML, $this->mParserOutput )
4087 ) {
4088 return $previewHTML;
4089 }
4090
4091 # provide a anchor link to the editform
4092 $continueEditing = '<span class="mw-continue-editing">' .
4093 '[[#' . self::EDITFORM_ID . '|' .
4094 $this->context->getLanguage()->getArrow() . ' ' .
4095 $this->context->msg( 'continue-editing' )->text() . ']]</span>';
4096 if ( $this->mTriedSave && !$this->mTokenOk ) {
4097 $note = $this->context->msg( 'session_fail_preview' )->plain();
4098 $this->incrementEditFailureStats( 'session_loss' );
4099 } elseif ( $this->incompleteForm ) {
4100 $note = $this->context->msg( 'edit_form_incomplete' )->plain();
4101 if ( $this->mTriedSave ) {
4102 $this->incrementEditFailureStats( 'incomplete_form' );
4103 }
4104 } else {
4105 $note = $this->context->msg( 'previewnote' )->plain() . ' ' . $continueEditing;
4106 }
4107
4108 # don't parse non-wikitext pages, show message about preview
4109 if ( $this->mTitle->isUserConfigPage() || $this->mTitle->isSiteConfigPage() ) {
4110 if ( $this->mTitle->isUserConfigPage() ) {
4111 $level = 'user';
4112 } elseif ( $this->mTitle->isSiteConfigPage() ) {
4113 $level = 'site';
4114 } else {
4115 $level = false;
4116 }
4117
4118 if ( $content->getModel() === CONTENT_MODEL_CSS ) {
4119 $format = 'css';
4120 if ( $level === 'user' && !$config->get( MainConfigNames::AllowUserCss ) ) {
4121 $format = false;
4122 }
4123 } elseif ( $content->getModel() === CONTENT_MODEL_JSON ) {
4124 $format = 'json';
4125 if ( $level === 'user' /* No comparable 'AllowUserJson' */ ) {
4126 $format = false;
4127 }
4128 } elseif ( $content->getModel() === CONTENT_MODEL_JAVASCRIPT ) {
4129 $format = 'js';
4130 if ( $level === 'user' && !$config->get( MainConfigNames::AllowUserJs ) ) {
4131 $format = false;
4132 }
4133 } elseif ( $content->getModel() === CONTENT_MODEL_VUE ) {
4134 $format = 'vue';
4135 if ( $level === 'user' && !$config->get( MainConfigNames::AllowUserJs ) ) {
4136 $format = false;
4137 }
4138 } else {
4139 $format = false;
4140 }
4141
4142 # Used messages to make sure grep find them:
4143 # Messages: usercsspreview, userjsonpreview, userjspreview,
4144 # sitecsspreview, sitejsonpreview, sitejspreview
4145 if ( $level && $format ) {
4146 $note = "<div id='mw-{$level}{$format}preview'>" .
4147 $this->context->msg( "{$level}{$format}preview" )->plain() .
4148 ' ' . $continueEditing . "</div>";
4149 }
4150 }
4151
4152 if ( $this->section === "new" ) {
4153 $content = $content->addSectionHeader( $this->sectiontitle );
4154 }
4155
4156 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
4157 $this->getHookRunner()->onEditPageGetPreviewContent( $this, $content );
4158
4159 $parserResult = $this->doPreviewParse( $content );
4160 $parserOutput = $parserResult['parserOutput'];
4161 $previewHTML = $parserResult['html'];
4162 $this->mParserOutput = $parserOutput;
4163 $out->addParserOutputMetadata( $parserOutput );
4164 if ( $out->userCanPreview() ) {
4165 $out->addContentOverride( $this->getTitle(), $content );
4166 }
4167
4168 foreach ( $parserOutput->getWarningMsgs() as $mv ) {
4169 $note .= "\n\n" . $this->context->msg( $mv )->text();
4170 }
4171
4172 } catch ( MWContentSerializationException $ex ) {
4173 $m = $this->context->msg(
4174 'content-failed-to-parse',
4175 $this->contentModel,
4176 $this->contentFormat,
4177 $ex->getMessage()
4178 );
4179 $note .= "\n\n" . $m->plain(); # gets parsed down below
4180 $previewHTML = '';
4181 }
4182
4183 if ( $this->isConflict ) {
4184 $conflict = Html::warningBox(
4185 $this->context->msg( 'previewconflict' )->escaped(),
4186 'mw-previewconflict'
4187 );
4188 } else {
4189 $conflict = '';
4190 }
4191
4192 $previewhead = Html::rawElement(
4193 'div', [ 'class' => 'previewnote' ],
4194 Html::rawElement(
4195 'h2', [ 'id' => 'mw-previewheader' ],
4196 $this->context->msg( 'preview' )->escaped()
4197 ) .
4198 Html::warningBox(
4199 $out->parseAsInterface( $note )
4200 ) . $conflict
4201 );
4202
4203 return $previewhead . $previewHTML . $this->previewTextAfterContent;
4204 }
4205
4206 private function incrementEditFailureStats( string $failureType ): void {
4207 MediaWikiServices::getInstance()->getStatsFactory()
4208 ->getCounter( 'edit_failure_total' )
4209 ->setLabel( 'cause', $failureType )
4210 ->setLabel( 'namespace', 'n/a' )
4211 ->setLabel( 'user_bucket', 'n/a' )
4212 ->increment();
4213 }
4214
4219 protected function getPreviewParserOptions() {
4220 $parserOptions = $this->page->makeParserOptions( $this->context );
4221 $parserOptions->setRenderReason( 'page-preview' );
4222 $parserOptions->setIsPreview( true );
4223 $parserOptions->setIsSectionPreview( $this->section !== null && $this->section !== '' );
4224
4225 // XXX: we could call $parserOptions->setCurrentRevisionRecordCallback here to force the
4226 // current revision to be null during PST, until setupFakeRevision is called on
4227 // the ParserOptions. Currently, we rely on Parser::getRevisionRecordObject() to ignore
4228 // existing revisions in preview mode.
4229
4230 return $parserOptions;
4231 }
4232
4242 protected function doPreviewParse( Content $content ) {
4243 $user = $this->getUserForPreview();
4244 $parserOptions = $this->getPreviewParserOptions();
4245
4246 // NOTE: preSaveTransform doesn't have a fake revision to operate on.
4247 // Parser::getRevisionRecordObject() will return null in preview mode,
4248 // causing the context user to be used for {{subst:REVISIONUSER}}.
4249 // XXX: Alternatively, we could also call setupFakeRevision()
4250 // before PST with $content.
4251 $services = MediaWikiServices::getInstance();
4252 $contentTransformer = $services->getContentTransformer();
4253 $contentRenderer = $services->getContentRenderer();
4254 $pstContent = $contentTransformer->preSaveTransform( $content, $this->mTitle, $user, $parserOptions );
4255 $parserOutput = $contentRenderer->getParserOutput( $pstContent, $this->mTitle, null, $parserOptions );
4256 $out = $this->context->getOutput();
4257 $skin = $out->getSkin();
4258 $skinOptions = $skin->getOptions();
4259 // TODO T371004 move runOutputPipeline out of $parserOutput
4260 // TODO T371022 ideally we clone here, but for now let's reproduce getText behaviour
4261 $oldHtml = $parserOutput->getRawText();
4262 $html = $parserOutput->runOutputPipeline( $parserOptions, [
4263 'allowClone' => 'false',
4264 'userLang' => $skin->getLanguage(),
4265 'injectTOC' => $skinOptions['toc'],
4266 'enableSectionEditLinks' => false,
4267 'includeDebugInfo' => true,
4268 ] )->getContentHolderText();
4269 $parserOutput->setRawText( $oldHtml );
4270 return [
4271 'parserOutput' => $parserOutput,
4272 'html' => $html
4273 ];
4274 }
4275
4279 public function getTemplates() {
4280 if ( $this->preview || $this->section !== '' ) {
4281 $templates = [];
4282 if ( !$this->mParserOutput ) {
4283 return $templates;
4284 }
4285 foreach (
4286 $this->mParserOutput->getLinkList( ParserOutputLinkTypes::TEMPLATE )
4287 as [ 'link' => $link ]
4288 ) {
4289 $templates[] = Title::newFromLinkTarget( $link );
4290 }
4291 return $templates;
4292 } else {
4293 return $this->mTitle->getTemplateLinksFrom();
4294 }
4295 }
4296
4302 public static function getEditToolbar() {
4303 $startingToolbar = '<div id="toolbar"></div>';
4304 $toolbar = $startingToolbar;
4305
4306 $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
4307 if ( !$hookRunner->onEditPageBeforeEditToolbar( $toolbar ) ) {
4308 return null;
4309 }
4310 // Don't add a pointless `<div>` to the page unless a hook caller populated it
4311 return ( $toolbar === $startingToolbar ) ? null : $toolbar;
4312 }
4313
4339 public function getCheckboxesDefinition( $values ) {
4340 $checkboxes = [];
4341
4342 $user = $this->context->getUser();
4343 // don't show the minor edit checkbox if it's a new page or section
4344 if ( !$this->isNew && $this->permManager->userHasRight( $user, 'minoredit' ) ) {
4345 $checkboxes['wpMinoredit'] = [
4346 'id' => 'wpMinoredit',
4347 'label-message' => 'minoredit',
4348 // Uses messages: tooltip-minoredit, accesskey-minoredit
4349 'tooltip' => 'minoredit',
4350 'label-id' => 'mw-editpage-minoredit',
4351 'legacy-name' => 'minor',
4352 'default' => $values['minor'],
4353 ];
4354 }
4355
4356 if ( $user->isNamed() ) {
4357 $checkboxes = array_merge(
4358 $checkboxes,
4359 $this->getCheckboxesDefinitionForWatchlist( $values['watch'], $values['wpWatchlistExpiry'] ?? null )
4360 );
4361 }
4362
4363 $this->getHookRunner()->onEditPageGetCheckboxesDefinition( $this, $checkboxes );
4364
4365 return $checkboxes;
4366 }
4367
4375 private function getCheckboxesDefinitionForWatchlist( $watch, $watchexpiry ): array {
4376 $fieldDefs = [
4377 'wpWatchthis' => [
4378 'id' => 'wpWatchthis',
4379 'label-message' => 'watchthis',
4380 // Uses messages: tooltip-watch, accesskey-watch
4381 'tooltip' => 'watch',
4382 'label-id' => 'mw-editpage-watch',
4383 'legacy-name' => 'watch',
4384 'default' => $watch,
4385 ]
4386 ];
4387 if ( $this->watchlistExpiryEnabled ) {
4388 $watchedItem = $this->watchedItemStore->getWatchedItem( $this->getContext()->getUser(), $this->getTitle() );
4389 if ( $watchedItem instanceof WatchedItem && $watchedItem->getExpiry() === null ) {
4390 // Not temporarily watched, so we always default to infinite.
4391 $userPreferredExpiry = 'infinite';
4392 } else {
4393 $userPreferredExpiryOption = !$this->getTitle()->exists()
4394 ? 'watchcreations-expiry'
4395 : 'watchdefault-expiry';
4396 $userPreferredExpiry = $this->userOptionsLookup->getOption(
4397 $this->getContext()->getUser(),
4398 $userPreferredExpiryOption,
4399 'infinite'
4400 );
4401 }
4402
4403 $expiryOptions = WatchAction::getExpiryOptions(
4404 $this->getContext(),
4405 $watchedItem,
4406 $userPreferredExpiry
4407 );
4408
4409 if ( $watchexpiry && in_array( $watchexpiry, $expiryOptions['options'] ) ) {
4410 $expiryOptions['default'] = $watchexpiry;
4411 }
4412 // When previewing, override the selected dropdown option to select whatever was posted
4413 // (if it's a valid option) rather than the current value for watchlistExpiry.
4414 // See also above in $this->importFormDataPosted().
4415 $expiryFromRequest = $this->getContext()->getRequest()->getText( 'wpWatchlistExpiry' );
4416 if ( ( $this->preview || $this->diff ) && in_array( $expiryFromRequest, $expiryOptions['options'] ) ) {
4417 $expiryOptions['default'] = $expiryFromRequest;
4418 }
4419
4420 // Reformat the options to match what DropdownInputWidget wants.
4421 $options = [];
4422 foreach ( $expiryOptions['options'] as $label => $value ) {
4423 $options[] = [ 'data' => $value, 'label' => $label ];
4424 }
4425
4426 $fieldDefs['wpWatchlistExpiry'] = [
4427 'id' => 'wpWatchlistExpiry',
4428 'label-message' => 'confirm-watch-label',
4429 // Uses messages: tooltip-watchlist-expiry, accesskey-watchlist-expiry
4430 'tooltip' => 'watchlist-expiry',
4431 'label-id' => 'mw-editpage-watchlist-expiry',
4432 'default' => $expiryOptions['default'],
4433 'value-attr' => 'value',
4434 'class' => DropdownInputWidget::class,
4435 'options' => $options,
4436 'invisibleLabel' => true,
4437 ];
4438 }
4439 return $fieldDefs;
4440 }
4441
4452 public function getCheckboxesWidget( &$tabindex, $values ) {
4453 $checkboxes = [];
4454 $checkboxesDef = $this->getCheckboxesDefinition( $values );
4455
4456 foreach ( $checkboxesDef as $name => $options ) {
4457 $legacyName = $options['legacy-name'] ?? $name;
4458
4459 $title = null;
4460 $accesskey = null;
4461 if ( isset( $options['tooltip'] ) ) {
4462 $accesskey = $this->context->msg( "accesskey-{$options['tooltip']}" )->text();
4463 $title = Linker::titleAttrib( $options['tooltip'] );
4464 }
4465 if ( isset( $options['title-message'] ) ) {
4466 $title = $this->context->msg( $options['title-message'] )->text();
4467 }
4468 // Allow checkbox definitions to set their own class and value-attribute names.
4469 // See $this->getCheckboxesDefinition() for details.
4470 $className = $options['class'] ?? CheckboxInputWidget::class;
4471 $valueAttr = $options['value-attr'] ?? 'selected';
4472 $checkboxes[ $legacyName ] = new FieldLayout(
4473 new $className( [
4474 'tabIndex' => ++$tabindex,
4475 'accessKey' => $accesskey,
4476 'id' => $options['id'] . 'Widget',
4477 'inputId' => $options['id'],
4478 'name' => $name,
4479 $valueAttr => $options['default'],
4480 'infusable' => true,
4481 'options' => $options['options'] ?? null,
4482 ] ),
4483 [
4484 'align' => 'inline',
4485 'label' => new OOUI\HtmlSnippet( $this->context->msg( $options['label-message'] )->parse() ),
4486 'title' => $title,
4487 'id' => $options['label-id'] ?? null,
4488 'invisibleLabel' => $options['invisibleLabel'] ?? null,
4489 ]
4490 );
4491 }
4492
4493 return $checkboxes;
4494 }
4495
4499 private function getSubmitButtonLabel(): string {
4500 $labelAsPublish =
4501 $this->context->getConfig()->get( MainConfigNames::EditSubmitButtonLabelPublish );
4502
4503 // Can't use $this->isNew as that's also true if we're adding a new section to an extant page
4504 $newPage = !$this->mTitle->exists();
4505
4506 if ( $labelAsPublish ) {
4507 $buttonLabelKey = $newPage ? 'publishpage' : 'publishchanges';
4508 } else {
4509 $buttonLabelKey = $newPage ? 'savearticle' : 'savechanges';
4510 }
4511
4512 return $buttonLabelKey;
4513 }
4514
4525 public function getEditButtons( &$tabindex ) {
4526 $buttons = [];
4527
4528 $labelAsPublish =
4529 $this->context->getConfig()->get( MainConfigNames::EditSubmitButtonLabelPublish );
4530
4531 $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
4532 $buttonTooltip = $labelAsPublish ? 'publish' : 'save';
4533
4534 $buttons['save'] = new OOUI\ButtonInputWidget( [
4535 'name' => 'wpSave',
4536 'tabIndex' => ++$tabindex,
4537 'id' => 'wpSaveWidget',
4538 'inputId' => 'wpSave',
4539 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4540 'useInputTag' => true,
4541 'flags' => [ 'progressive', 'primary' ],
4542 'label' => $buttonLabel,
4543 'infusable' => true,
4544 'type' => 'submit',
4545 // Messages used: tooltip-save, tooltip-publish
4546 'title' => Linker::titleAttrib( $buttonTooltip ),
4547 // Messages used: accesskey-save, accesskey-publish
4548 'accessKey' => Linker::accesskey( $buttonTooltip ),
4549 ] );
4550
4551 $buttons['preview'] = new OOUI\ButtonInputWidget( [
4552 'name' => 'wpPreview',
4553 'tabIndex' => ++$tabindex,
4554 'id' => 'wpPreviewWidget',
4555 'inputId' => 'wpPreview',
4556 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4557 'useInputTag' => true,
4558 'label' => $this->context->msg( 'showpreview' )->text(),
4559 'infusable' => true,
4560 'type' => 'submit',
4561 // Allow previewing even when the form is in invalid state (T343585)
4562 'formNoValidate' => true,
4563 // Message used: tooltip-preview
4564 'title' => Linker::titleAttrib( 'preview' ),
4565 // Message used: accesskey-preview
4566 'accessKey' => Linker::accesskey( 'preview' ),
4567 ] );
4568
4569 $buttons['diff'] = new OOUI\ButtonInputWidget( [
4570 'name' => 'wpDiff',
4571 'tabIndex' => ++$tabindex,
4572 'id' => 'wpDiffWidget',
4573 'inputId' => 'wpDiff',
4574 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4575 'useInputTag' => true,
4576 'label' => $this->context->msg( 'showdiff' )->text(),
4577 'infusable' => true,
4578 'type' => 'submit',
4579 // Allow previewing even when the form is in invalid state (T343585)
4580 'formNoValidate' => true,
4581 // Message used: tooltip-diff
4582 'title' => Linker::titleAttrib( 'diff' ),
4583 // Message used: accesskey-diff
4584 'accessKey' => Linker::accesskey( 'diff' ),
4585 ] );
4586
4587 $this->getHookRunner()->onEditPageBeforeEditButtons( $this, $buttons, $tabindex );
4588
4589 return $buttons;
4590 }
4591
4596 private function noSuchSectionPage(): void {
4597 $out = $this->context->getOutput();
4598 $out->prepareErrorPage();
4599 $out->setPageTitleMsg( $this->context->msg( 'nosuchsectiontitle' ) );
4600
4601 $res = $this->context->msg( 'nosuchsectiontext', $this->section )->parseAsBlock();
4602
4603 $this->getHookRunner()->onEditPageNoSuchSection( $this, $res );
4604 $out->addHTML( $res );
4605
4606 $out->returnToMain( false, $this->mTitle );
4607 }
4608
4614 public function spamPageWithContent( $match = false ) {
4615 $this->textbox2 = $this->textbox1;
4616
4617 $out = $this->context->getOutput();
4618 $out->prepareErrorPage();
4619 $out->setPageTitleMsg( $this->context->msg( 'spamprotectiontitle' ) );
4620
4621 $spamText = $this->context->msg( 'spamprotectiontext' )->parseAsBlock();
4622
4623 if ( $match ) {
4624 if ( is_array( $match ) ) {
4625 // Do not use `wfEscapeWikiText( ... )` here for compatibility with PHP <8.1.4
4626 // https://gerrit.wikimedia.org/r/c/mediawiki/core/+/1160800/comment/92e67687_ab221188/
4627 $matchText = $this->context->getLanguage()->listToText( array_map( 'wfEscapeWikiText', $match ) );
4628 } else {
4629 $matchText = wfEscapeWikiText( $match );
4630 }
4631
4632 $spamText .= $this->context->msg( 'spamprotectionmatch' )
4633 ->params( $matchText )
4634 ->parseAsBlock();
4635 }
4636 $out->addHTML( Html::rawElement(
4637 'div',
4638 [ 'id' => 'spamprotected' ],
4639 $spamText
4640 ) );
4641
4642 $out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
4643 $this->showDiff();
4644
4645 $out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
4646 $this->showTextbox( $this->textbox2, 'wpTextbox2', [ 'tabindex' => 6, 'readonly' ] );
4647
4648 $out->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
4649 }
4650
4651 private function addLongPageWarningHeader(): void {
4652 if ( $this->contentLength === false ) {
4653 $this->contentLength = strlen( $this->textbox1 );
4654 }
4655
4656 $out = $this->context->getOutput();
4657 $longPageHint = $this->context->msg( 'longpage-hint' );
4658 if ( !$longPageHint->isDisabled() ) {
4659 $msgText = trim( $longPageHint->sizeParams( $this->contentLength )
4660 ->params( $this->contentLength ) // Keep this unformatted for math inside message
4661 ->parse() );
4662 if ( $msgText !== '' && $msgText !== '-' ) {
4663 $out->addHTML( "<div id='mw-edit-longpage-hint'>\n$msgText\n</div>" );
4664 }
4665 }
4666 }
4667
4668 private function addExplainConflictHeader(): void {
4669 $this->context->getOutput()->addHTML(
4670 $this->getEditConflictHelper()->getExplainHeader()
4671 );
4672 }
4673
4680 private function guessSectionName( $text ): string {
4681 $parser = MediaWikiServices::getInstance()->getParser();
4682 $name = $parser->guessSectionNameFromWikiText( $text );
4683 // Per T216029, fragments in HTTP redirects need to be urlencoded,
4684 // otherwise Chrome double-escapes the rest of the URL.
4685 return '#' . urlencode( mb_substr( $name, 1 ) );
4686 }
4687
4692 public function setEditConflictHelperFactory( callable $factory ) {
4693 Assert::precondition( !$this->editConflictHelperFactory,
4694 'Can only have one extension that resolves edit conflicts' );
4695 $this->editConflictHelperFactory = $factory;
4696 }
4697
4698 private function getEditConflictHelper(): TextConflictHelper {
4699 if ( !$this->editConflictHelper ) {
4700 $label = $this->getSubmitButtonLabel();
4701 if ( $this->editConflictHelperFactory ) {
4702 $this->editConflictHelper = ( $this->editConflictHelperFactory )( $label );
4703 } else {
4704 $this->editConflictHelper = new TextConflictHelper(
4705 $this->getTitle(),
4706 $this->getContext()->getOutput(),
4707 MediaWikiServices::getInstance()->getStatsFactory(),
4708 $label,
4709 MediaWikiServices::getInstance()->getContentHandlerFactory()
4710 );
4711 }
4712 }
4713 return $this->editConflictHelper;
4714 }
4715}
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_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,...
wfTimestamp( $outputtype=TS::UNIX, $ts=0)
Get a timestamp string in one of various formats.
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:68
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.
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.
Verify summary and text do not match spam regexes.
The HTML user interface for page editing.
Definition EditPage.php:136
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:355
showTextbox(string $text, string $name, array $customAttribs=[])
setEditConflictHelperFactory(callable $factory)
showStandardInputs(int &$tabindex=2)
__construct(Article $article)
Definition EditPage.php:483
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.
edit()
This is the function that gets called for "action=edit".
Definition EditPage.php:603
toEditContent( $text)
Turns the given text into a Content object by unserializing it.
string null $starttime
Timestamp from the first time the edit form was rendered.
Definition EditPage.php:348
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:401
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:300
getCheckboxesWidget(&$tabindex, $values)
Returns an array of fields for the edit form, including 'minor' and 'watch' checkboxes and any other ...
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 null $edittime
Timestamp of the latest revision of the page when editing was initiated on the client.
Definition EditPage.php:321
string $editFormPageTop
Before even the preview.
Definition EditPage.php:384
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:143
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:148
const POST_EDIT_COOKIE_DURATION
Duration of PostEdit cookie, in seconds.
Definition EditPage.php:172
bool $firsttime
True the first time the edit form is rendered, false after re-rendering with diff,...
Definition EditPage.php:215
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)
setApiEditOverride( $enableOverride)
Allow editing of content that supports API direct editing, but not general direct editing.
Definition EditPage.php:588
const POST_EDIT_COOKIE_KEY_PREFIX
Prefix of key for cookie used to pass post-edit state.
Definition EditPage.php:154
getPreviewParserOptions()
Get parser options for a preview.
bool $isConflict
Whether an edit conflict needs to be resolved.
Definition EditPage.php:200
maybeActivateTempUserCreate( $doAcquire)
Check the configuration and current user and enable automatic temporary user creation if possible.
Definition EditPage.php:777
getExpectedParentRevision()
Returns the RevisionRecord corresponding to the revision that was current at the time editing was ini...
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.
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 to simplify the use of log pages.
Definition LogPage.php:35
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:245
getTitle()
Get the title object of the article.
Definition Article.php:235
getContext()
Gets the context this Article is executed in.
Definition Article.php:2123
Special handling for category description pages.
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.
getIP()
Work out the IP address based on various globals For trusted proxies, use the XFF client IP (first of...
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.
getSlot( $role, $audience=self::FOR_PUBLIC, ?Authority $performer=null)
Returns meta-data for the given slot.
getUser( $audience=self::FOR_PUBLIC, ?Authority $performer=null)
Fetch revision's author's user identity, if it's available to the specified audience.
getMainContentModel()
Returns the content model of the main slot of this revision.
isDeleted( $field)
MCR migration note: this replaced Revision::isDeleted.
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 a title within MediaWiki.
Definition Title.php:70
getLocalURL( $query='')
Get a URL with no fragment or server name (relative URL) from a Title object.
Definition Title.php:2216
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:110
Representation of a pair of user and title for watchlist entries.
getExpiry(int|TS|null $style=TS::MW)
When the watched item will expire.
Value object representing a message for i18n.
Type definition for expiry timestamps.
Definition ExpiryDef.php:18
Build SELECT queries with a fluent interface.
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', 'sodipodi'=> ' $path/sodipodi -z -w $width -f $input -e $output', 'inkscape'=> ' $path/inkscape -z -w $width -f $input -e $output', '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', 'imgserv'=> ' $path/imgserv-wrapper -i svg -o png -w$width $input $output', 'ImagickExt'=>['SvgHandler::rasterizeImagickExt',],], 'SVGConverter'=> 'ImageMagick', 'SVGConverterPath'=> '', 'SVGMaxSize'=> 5120, 'SVGMetadataCutoff'=> 5242880, 'SVGNativeRendering'=> false, '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, 250, 300,], '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, 'DjvuUseBoxedCommand'=> false, 'DjvuDump'=> null, 'DjvuRenderer'=> null, 'DjvuTxt'=> null, 'DjvuPostProcessor'=> 'pnmtojpeg', 'DjvuOutputExtension'=> 'jpg', 'EmergencyContact'=> false, 'PasswordSender'=> false, 'NoReplyAddress'=> false, 'EnableEmail'=> true, 'EnableUserEmail'=> true, 'EnableSpecialMute'=> false, 'EnableUserEmailMuteList'=> false, '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, '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'=> '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, 'MemCachedServers'=>['127.0.0.1:11211',], 'MemCachedPersistent'=> false, 'MemCachedTimeout'=> 500000, 'UseLocalMessageCache'=> false, 'AdaptiveMessageCache'=> false, 'LocalisationCacheConf'=>['class'=> '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, 'BlockTargetMigrationStage' => 768, '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' => [ ], '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, '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, '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, ], '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', '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', 'BlockTargetMigrationStage' => 'integer', 'GroupPermissions' => 'object', 'PrivilegedGroups' => 'array', 'RevokePermissions' => 'object', 'GroupInheritsPermissions' => 'object', 'ImplicitGroups' => 'array', 'GroupsAddToSelf' => 'object', 'GroupsRemoveFromSelf' => 'object', 'RestrictedGroups' => 'object', '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', ], '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', ], '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', ], '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', ], 'msg' => [ 'type' => 'string', 'description' => 'a message key', ], ], 'required' => [ 'url', ], ], ], '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.
Interface for all constraints that can prevent edits.
Serves as a common repository of constants for EditPage edit status results.
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.
Service for resolving a wiki page redirect.
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.
Interface for localizing messages in MediaWiki.
msg( $key,... $params)
This is the method for getting translated interface messages.
Provide primary and replica IDatabase connections.
Interface for database access objects.
msg( $key,... $params)