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 // Generate an autosummary
1604 $firstrev = $this->revisionStore->getNextRevision( $oldrev );
1605 // Undid just one revision
1606 if ( $firstrev && $firstrev->getId() == $undo ) {
1607 $userText = $undorev->getUser() ?
1608 $undorev->getUser()->getName() :
1609 '';
1610 if ( $userText === '' ) {
1611 $undoSummary = $this->context->msg(
1612 'undo-summary-username-hidden',
1613 $undo
1614 )->inContentLanguage()->text();
1615 // Handle external users (imported revisions)
1616 } elseif ( ExternalUserNames::isExternal( $userText ) ) {
1617 $userLinkTitle = ExternalUserNames::getUserLinkTitle( $userText );
1618 if ( $userLinkTitle ) {
1619 $userLink = $userLinkTitle->getPrefixedText();
1620 $undoSummary = $this->context->msg(
1621 'undo-summary-import',
1622 $undo,
1623 $userLink,
1624 $userText
1625 )->inContentLanguage()->text();
1626 } else {
1627 $undoSummary = $this->context->msg(
1628 'undo-summary-import2',
1629 $undo,
1630 $userText
1631 )->inContentLanguage()->text();
1632 }
1633 } else {
1634 $undoIsAnon =
1635 !$undorev->getUser() ||
1636 !$undorev->getUser()->isRegistered();
1637 $disableAnonTalk = $services->getMainConfig()->get( MainConfigNames::DisableAnonTalk );
1638 $undoMessage = ( $undoIsAnon && $disableAnonTalk ) ?
1639 'undo-summary-anon' :
1640 'undo-summary';
1641 $undoSummary = $this->context->msg(
1642 $undoMessage,
1643 $undo,
1644 $userText
1645 )->inContentLanguage()->text();
1646 }
1647 if ( $this->summary === '' ) {
1648 $this->summary = $undoSummary;
1649 } else {
1650 $this->summary = $undoSummary . $this->context->msg( 'colon-separator' )
1651 ->inContentLanguage()->text() . $this->summary;
1652 }
1653 // Undid multiple revisions
1654 } else {
1655 $firstRevisionId = $firstrev->getId();
1656 $lastRevisionId = $undorev->getId();
1657 $revisionCount = $services->getRevisionStore()->countRevisionsBetween(
1658 $firstrev->getPageId(),
1659 $firstrev,
1660 $undorev,
1661 null,
1662 [ RevisionStore::INCLUDE_BOTH, RevisionStore::INCLUDE_DELETED_REVISIONS ]
1663 );
1664 $undoSummary = $this->context->msg( 'undo-summary-multiple' )
1665 ->numParams( $revisionCount )
1666 ->params( $firstRevisionId, $lastRevisionId )
1667 ->inContentLanguage()
1668 ->text();
1669 if ( $this->summary === '' ) {
1670 $this->summary = $undoSummary;
1671 } else {
1672 $this->summary = $undoSummary . $this->context->msg( 'colon-separator' )
1673 ->inContentLanguage()->text() . $this->summary;
1674 }
1675 }
1676 }
1677
1691 private function getUndoContent( RevisionRecord $undoRev, RevisionRecord $oldRev, &$error ) {
1692 $handler = $this->contentHandlerFactory
1693 ->getContentHandler( $undoRev->getSlot(
1694 SlotRecord::MAIN,
1695 RevisionRecord::RAW
1696 )->getModel() );
1697 $currentContent = $this->page->getRevisionRecord()
1698 ->getContent( SlotRecord::MAIN );
1699 $undoContent = $undoRev->getContent( SlotRecord::MAIN );
1700 $undoAfterContent = $oldRev->getContent( SlotRecord::MAIN );
1701 $undoIsLatest = $this->page->getRevisionRecord()->getId() === $undoRev->getId();
1702 if ( $currentContent === null
1703 || $undoContent === null
1704 || $undoAfterContent === null
1705 ) {
1706 $error = 'norev';
1707 return false;
1708 }
1709
1710 $content = $handler->getUndoContent(
1711 $currentContent,
1712 $undoContent,
1713 $undoAfterContent,
1714 $undoIsLatest
1715 );
1716 if ( $content === false ) {
1717 $error = 'failure';
1718 }
1719 return $content;
1720 }
1721
1736 private function getOriginalContent( Authority $performer ): ?Content {
1737 if ( $this->section === 'new' ) {
1738 return $this->getCurrentContent();
1739 }
1740 $revRecord = $this->mArticle->fetchRevisionRecord();
1741 if ( $revRecord === null ) {
1742 return $this->contentHandlerFactory
1743 ->getContentHandler( $this->contentModel )
1744 ->makeEmptyContent();
1745 }
1746 return $revRecord->getContent( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $performer );
1747 }
1748
1760 private function getParentRevId() {
1761 if ( $this->parentRevId ) {
1762 return $this->parentRevId;
1763 } else {
1764 return $this->mArticle->getRevIdFetched();
1765 }
1766 }
1767
1776 protected function getCurrentContent() {
1777 $revRecord = $this->page->getRevisionRecord();
1778 $content = $revRecord ? $revRecord->getContent(
1779 SlotRecord::MAIN,
1780 RevisionRecord::RAW
1781 ) : null;
1782
1783 if ( $content === null ) {
1784 return $this->contentHandlerFactory
1785 ->getContentHandler( $this->contentModel )
1786 ->makeEmptyContent();
1787 }
1788
1789 return $content;
1790 }
1791
1798 private function tokenOk( WebRequest $request ): bool {
1799 $token = $request->getVal( 'wpEditToken' );
1800 $user = $this->context->getUser();
1801 $this->mTokenOk = $user->matchEditToken( $token );
1802 return $this->mTokenOk;
1803 }
1804
1819 private function setPostEditCookie( int $statusValue ): void {
1820 $revisionId = $this->page->getLatest();
1821 $postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
1822
1823 $val = 'saved';
1824 if ( $statusValue === self::AS_SUCCESS_NEW_ARTICLE ) {
1825 $val = 'created';
1826 } elseif ( $this->oldid ) {
1827 $val = 'restored';
1828 }
1829 if ( $this->tempUserCreateDone ) {
1830 $val .= '+tempuser';
1831 }
1832
1833 $response = $this->context->getRequest()->response();
1834 $response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION );
1835 }
1836
1843 public function attemptSave( &$resultDetails = false ) {
1844 // Allow bots to exempt some edits from bot flagging
1845 $markAsBot = $this->markAsBot
1846 && $this->getAuthority()->isAllowed( 'bot' );
1847
1848 // Allow trusted users to mark some edits as minor
1849 $markAsMinor = $this->minoredit && !$this->isNew
1850 && $this->getAuthority()->isAllowed( 'minoredit' );
1851
1852 $status = $this->internalAttemptSave( $resultDetails, $markAsBot, $markAsMinor );
1853
1854 $this->getHookRunner()->onEditPage__attemptSave_after( $this, $status, $resultDetails );
1855
1856 return $status;
1857 }
1858
1862 private function incrementResolvedConflicts(): void {
1863 if ( $this->context->getRequest()->getText( 'mode' ) !== 'conflict' ) {
1864 return;
1865 }
1866
1867 $this->getEditConflictHelper()->incrementResolvedStats( $this->context->getUser() );
1868 }
1869
1879 private function handleStatus( Status $status, $resultDetails ): bool {
1880 $statusValue = is_int( $status->value ) ? $status->value : 0;
1881
1886 if ( $statusValue === self::AS_SUCCESS_UPDATE
1887 || $statusValue === self::AS_SUCCESS_NEW_ARTICLE
1888 ) {
1889 $this->incrementResolvedConflicts();
1890
1891 $this->didSave = true;
1892 if ( !$resultDetails['nullEdit'] ) {
1893 $this->setPostEditCookie( $statusValue );
1894 }
1895 }
1896
1897 $out = $this->context->getOutput();
1898
1899 // "wpExtraQueryRedirect" is a hidden input to modify
1900 // after save URL and is not used by actual edit form
1901 $request = $this->context->getRequest();
1902 $extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
1903
1904 switch ( $statusValue ) {
1905 // Status codes for which the error/warning message is generated somewhere else in this class.
1906 // They should be refactored to provide their own messages and handled below (T384399).
1907 case self::AS_HOOK_ERROR_EXPECTED:
1908 case self::AS_ARTICLE_WAS_DELETED:
1909 case self::AS_CONFLICT_DETECTED:
1910 case self::AS_SUMMARY_NEEDED:
1911 case self::AS_END:
1912 case self::AS_REVISION_WAS_DELETED:
1913 return true;
1914
1915 case self::AS_HOOK_ERROR:
1916 return false;
1917
1918 // Status codes that provide their own error/warning messages. Most error scenarios that don't
1919 // need custom user interface (e.g. edit conflicts) should be handled here, one day (T384399).
1920 case self::AS_BLANK_ARTICLE:
1921 case self::AS_BROKEN_REDIRECT:
1922 case self::AS_DOUBLE_REDIRECT:
1923 case self::AS_DOUBLE_REDIRECT_LOOP:
1924 case self::AS_CONTENT_TOO_BIG:
1925 case self::AS_INVALID_REDIRECT_TARGET:
1926 case self::AS_MAX_ARTICLE_SIZE_EXCEEDED:
1927 case self::AS_PARSE_ERROR:
1928 case self::AS_SELF_REDIRECT:
1929 case self::AS_TEXTBOX_EMPTY:
1930 case self::AS_UNABLE_TO_ACQUIRE_TEMP_ACCOUNT:
1931 case self::AS_UNICODE_NOT_SUPPORTED:
1932 foreach ( $status->getMessages() as $msg ) {
1933 $out->addHTML( Html::errorBox(
1934 $this->context->msg( $msg )->parse()
1935 ) );
1936 }
1937 return true;
1938
1939 case self::AS_SUCCESS_NEW_ARTICLE:
1940 $queryParts = [];
1941 if ( $resultDetails['redirect'] ) {
1942 $queryParts[] = 'redirect=no';
1943 }
1944 if ( $extraQueryRedirect ) {
1945 $queryParts[] = $extraQueryRedirect;
1946 }
1947 $anchor = $resultDetails['sectionanchor'] ?? '';
1948 $this->doPostEditRedirect( implode( '&', $queryParts ), $anchor );
1949 return false;
1950
1951 case self::AS_SUCCESS_UPDATE:
1952 $extraQuery = '';
1953 $sectionanchor = $resultDetails['sectionanchor'];
1954 // Give extensions a chance to modify URL query on update
1955 $this->getHookRunner()->onArticleUpdateBeforeRedirect( $this->mArticle,
1956 $sectionanchor, $extraQuery );
1957
1958 $queryParts = [];
1959 if ( $resultDetails['redirect'] ) {
1960 $queryParts[] = 'redirect=no';
1961 }
1962 if ( $extraQuery ) {
1963 $queryParts[] = $extraQuery;
1964 }
1965 if ( $extraQueryRedirect ) {
1966 $queryParts[] = $extraQueryRedirect;
1967 }
1968 $this->doPostEditRedirect( implode( '&', $queryParts ), $sectionanchor );
1969 return false;
1970
1971 case self::AS_SPAM_ERROR:
1972 $this->spamPageWithContent( $resultDetails['spam'] ?? false );
1973 return false;
1974
1975 case self::AS_BLOCKED_PAGE_FOR_USER:
1976 throw new UserBlockedError(
1977 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
1978 $this->context->getUser()->getBlock(),
1979 $this->context->getUser(),
1980 $this->context->getLanguage(),
1981 $request->getIP()
1982 );
1983
1984 case self::AS_IMAGE_REDIRECT_ANON:
1985 case self::AS_IMAGE_REDIRECT_LOGGED:
1986 throw new PermissionsError( 'upload' );
1987
1988 case self::AS_READ_ONLY_PAGE_ANON:
1989 case self::AS_READ_ONLY_PAGE_LOGGED:
1990 throw new PermissionsError( 'edit' );
1991
1992 case self::AS_READ_ONLY_PAGE:
1993 throw new ReadOnlyError;
1994
1995 case self::AS_RATE_LIMITED:
1996 $out->addHTML( Html::errorBox(
1997 $this->context->msg( 'actionthrottledtext' )->parse()
1998 ) );
1999 return true;
2000
2001 case self::AS_NO_CREATE_PERMISSION:
2002 $permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage';
2003 throw new PermissionsError( $permission );
2004
2005 case self::AS_NO_CHANGE_CONTENT_MODEL:
2006 throw new PermissionsError( 'editcontentmodel' );
2007
2008 default:
2009 // We don't recognize $statusValue. The only way that can happen
2010 // is if an extension hook aborted from inside ArticleSave.
2011 // Render the status object into $this->hookError
2012 // FIXME this sucks, we should just use the Status object throughout
2013 $this->hookError = Html::errorBox(
2014 "\n" . $status->getWikiText( false, false, $this->context->getLanguage() )
2015 );
2016 return true;
2017 }
2018 }
2019
2027 private function doPostEditRedirect( $query, $anchor ) {
2028 $out = $this->context->getOutput();
2029 $url = $this->mTitle->getFullURL( $query ) . $anchor;
2030 $user = $this->getUserForSave();
2031 // If the temporary account was created in this request,
2032 // or if the temporary account has zero edits (implying
2033 // that the account was created during a failed edit
2034 // attempt in a previous request), perform the top-level
2035 // redirect to ensure the account is attached.
2036 // Note that the temp user could already have performed
2037 // the top-level redirect if this a first edit on
2038 // a wiki that is not the user's home wiki.
2039 $shouldRedirectForTempUser = $this->tempUserCreateDone ||
2040 ( $user->isTemp() && ( $user->getEditCount() === 0 ) );
2041 if ( $shouldRedirectForTempUser ) {
2042 $this->getHookRunner()->onTempUserCreatedRedirect(
2043 $this->context->getRequest()->getSession(),
2044 $user,
2045 $this->mTitle->getPrefixedDBkey(),
2046 $query,
2047 $anchor,
2048 $url
2049 );
2050 }
2051 $out->redirect( $url );
2052 }
2053
2057 private function setNewSectionSummary(): void {
2058 Assert::precondition( $this->section === 'new', 'This method can only be called for new sections' );
2059 Assert::precondition( $this->sectiontitle !== null, 'This method can only be called for new sections' );
2060
2061 $services = MediaWikiServices::getInstance();
2062 $parser = $services->getParser();
2063 $textFormatter = $services->getMessageFormatterFactory()->getTextFormatter(
2064 $services->getContentLanguageCode()->toString()
2065 );
2066
2067 if ( $this->sectiontitle !== '' ) {
2068 $this->newSectionAnchor = $this->guessSectionName( $this->sectiontitle );
2069 // If no edit summary was specified, create one automatically from the section
2070 // title and have it link to the new section. Otherwise, respect the summary as
2071 // passed.
2072 if ( $this->summary === '' ) {
2073 $messageValue = MessageValue::new( 'newsectionsummary' )
2074 ->plaintextParams( $parser->stripSectionName( $this->sectiontitle ) );
2075 $this->summary = $textFormatter->format( $messageValue );
2076 }
2077 } else {
2078 $this->newSectionAnchor = '';
2079 }
2080 }
2081
2108 private function internalAttemptSave( &$result, $markAsBot = false, $markAsMinor = false ) {
2109 // If an attempt to acquire a temporary name failed, don't attempt to do anything else.
2110 if ( $this->unableToAcquireTempName ) {
2111 $status = Status::newFatal( 'temp-user-unable-to-acquire' );
2112 $status->value = self::AS_UNABLE_TO_ACQUIRE_TEMP_ACCOUNT;
2113 return $status;
2114 }
2115 // Auto-create the temporary account user, if the feature is enabled.
2116 // We create the account before any constraint checks or edit hooks fire, to ensure
2117 // that we have an actor and user account that can be used for any logs generated
2118 // by the edit attempt, and to ensure continuity in the user experience (if a constraint
2119 // denies an edit to a logged-out user, that history should be associated with the
2120 // eventually successful account creation)
2121 $tempAccountStatus = $this->createTempUser();
2122 if ( !$tempAccountStatus->isOK() ) {
2123 return $tempAccountStatus;
2124 }
2125 if ( $tempAccountStatus instanceof CreateStatus ) {
2126 $result['savedTempUser'] = $tempAccountStatus->getUser();
2127 }
2128
2129 $useNPPatrol = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::UseNPPatrol );
2130 $useRCPatrol = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::UseRCPatrol );
2131 if ( !$this->getHookRunner()->onEditPage__attemptSave( $this ) ) {
2132 wfDebug( "Hook 'EditPage::attemptSave' aborted article saving" );
2133 $status = Status::newFatal( 'hookaborted' );
2134 $status->value = self::AS_HOOK_ERROR;
2135 return $status;
2136 }
2137
2138 if ( !$this->getHookRunner()->onEditFilter( $this, $this->textbox1, $this->section,
2139 $this->hookError, $this->summary )
2140 ) {
2141 # Error messages etc. could be handled within the hook...
2142 $status = Status::newFatal( 'hookaborted' );
2143 $status->value = self::AS_HOOK_ERROR;
2144 return $status;
2145 } elseif ( $this->hookError ) {
2146 # ...or the hook could be expecting us to produce an error
2147 $status = Status::newFatal( 'hookaborted' );
2148 $status->value = self::AS_HOOK_ERROR_EXPECTED;
2149 return $status;
2150 }
2151
2152 try {
2153 # Construct Content object
2154 $textbox_content = $this->toEditContent( $this->textbox1 );
2155 } catch ( MWContentSerializationException $ex ) {
2156 $status = Status::newFatal(
2157 'content-failed-to-parse',
2158 $this->contentModel,
2159 $this->contentFormat,
2160 $ex->getMessage()
2161 );
2162 $status->value = self::AS_PARSE_ERROR;
2163 return $status;
2164 }
2165
2166 $this->contentLength = strlen( $this->textbox1 );
2167
2168 $requestUser = $this->context->getUser();
2169 $authority = $this->getAuthority();
2170 $pstUser = $this->getUserForPreview();
2171
2172 $changingContentModel = false;
2173 if ( $this->contentModel !== $this->mTitle->getContentModel() ) {
2174 $changingContentModel = true;
2175 $oldContentModel = $this->mTitle->getContentModel();
2176 }
2177
2178 // BEGINNING OF MIGRATION TO EDITCONSTRAINT SYSTEM (see T157658)
2180 $constraintFactory = MediaWikiServices::getInstance()->getService( '_EditConstraintFactory' );
2181 $constraintRunner = new EditConstraintRunner();
2182
2183 // Message key of the label of the submit button - used by some constraint error messages
2184 $submitButtonLabel = $this->getSubmitButtonLabel();
2185
2186 // UnicodeConstraint: ensure that `$this->unicodeCheck` is the correct unicode
2187 $constraintRunner->addConstraint(
2188 new UnicodeConstraint( $this->unicodeCheck )
2189 );
2190
2191 // SimpleAntiSpamConstraint: ensure that the context request does not have
2192 // `wpAntispam` set
2193 // Use $user since there is no permissions aspect
2194 $constraintRunner->addConstraint(
2195 $constraintFactory->newSimpleAntiSpamConstraint(
2196 $this->context->getRequest()->getText( 'wpAntispam' ),
2197 $requestUser,
2198 $this->mTitle
2199 )
2200 );
2201
2202 // SpamRegexConstraint: ensure that the summary and text don't match the spam regex
2203 $constraintRunner->addConstraint(
2204 $constraintFactory->newSpamRegexConstraint(
2205 $this->summary,
2206 $this->sectiontitle,
2207 $this->textbox1,
2208 $this->context->getRequest()->getIP(),
2209 $this->mTitle
2210 )
2211 );
2212 $constraintRunner->addConstraint(
2213 new ImageRedirectConstraint(
2214 $textbox_content,
2215 $this->mTitle,
2216 $authority
2217 )
2218 );
2219 $constraintRunner->addConstraint(
2220 $constraintFactory->newReadOnlyConstraint()
2221 );
2222
2223 // Load the page data from the primary DB. If anything changes in the meantime,
2224 // we detect it by using page_latest like a token in a 1 try compare-and-swap.
2225 $this->page->loadPageData( IDBAccessObject::READ_LATEST );
2226 $new = !$this->page->exists();
2227
2228 $constraintRunner->addConstraint(
2229 new AuthorizationConstraint(
2230 $authority,
2231 $this->mTitle,
2232 $new
2233 )
2234 );
2235 $constraintRunner->addConstraint(
2236 new ContentModelChangeConstraint(
2237 $authority,
2238 $this->mTitle,
2239 $this->contentModel
2240 )
2241 );
2242 $constraintRunner->addConstraint(
2243 $constraintFactory->newLinkPurgeRateLimitConstraint(
2244 $requestUser->toRateLimitSubject()
2245 )
2246 );
2247 $constraintRunner->addConstraint(
2248 // Same constraint is used to check size before and after merging the
2249 // edits, which use different failure codes
2250 $constraintFactory->newPageSizeConstraint(
2251 $this->contentLength,
2252 PageSizeConstraint::BEFORE_MERGE
2253 )
2254 );
2255 $constraintRunner->addConstraint(
2256 new ChangeTagsConstraint( $authority, $this->changeTags )
2257 );
2258
2259 // If the article has been deleted while editing, don't save it without
2260 // confirmation
2261 $constraintRunner->addConstraint(
2262 new AccidentalRecreationConstraint(
2263 $this->wasDeletedSinceLastEdit(),
2264 $this->recreate
2265 )
2266 );
2267
2268 // Check the constraints
2269 if ( !$constraintRunner->checkConstraints() ) {
2270 $failed = $constraintRunner->getFailedConstraint();
2271
2272 // Need to check SpamRegexConstraint here, to avoid needing to pass
2273 // $result by reference again
2274 if ( $failed instanceof SpamRegexConstraint ) {
2275 $result['spam'] = $failed->getMatch();
2276 } else {
2277 $this->handleFailedConstraint( $failed );
2278 }
2279
2280 return Status::wrap( $failed->getLegacyStatus() );
2281 }
2282 // END OF MIGRATION TO EDITCONSTRAINT SYSTEM (continued below)
2283
2284 $flags = EDIT_AUTOSUMMARY |
2285 ( $new ? EDIT_NEW : EDIT_UPDATE ) |
2286 ( $markAsMinor ? EDIT_MINOR : 0 ) |
2287 ( $markAsBot ? EDIT_FORCE_BOT : 0 );
2288
2289 if ( $new ) {
2290 $content = $textbox_content;
2291
2292 $result['sectionanchor'] = '';
2293 if ( $this->section === 'new' ) {
2294 if ( $this->sectiontitle !== null ) {
2295 // Insert the section title above the content.
2296 $content = $content->addSectionHeader( $this->sectiontitle );
2297 }
2298 $result['sectionanchor'] = $this->newSectionAnchor;
2299 }
2300
2301 $pageUpdater = $this->page->newPageUpdater( $pstUser )
2302 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
2303 ->setContent( SlotRecord::MAIN, $content );
2304 $pageUpdater->prepareUpdate( $flags );
2305
2306 // BEGINNING OF MIGRATION TO EDITCONSTRAINT SYSTEM (see T157658)
2307 // Create a new runner to avoid rechecking the prior constraints, use the same factory
2308 $constraintRunner = new EditConstraintRunner();
2309
2310 // Don't save a new page if it's blank or if it's a MediaWiki:
2311 // message with content equivalent to default (allow empty pages
2312 // in this case to disable messages, see T52124)
2313 $constraintRunner->addConstraint(
2314 new DefaultTextConstraint(
2315 $this->mTitle,
2316 $this->allowBlankArticle,
2317 $this->textbox1,
2318 $submitButtonLabel
2319 )
2320 );
2321
2322 $constraintRunner->addConstraint(
2323 $constraintFactory->newEditFilterMergedContentHookConstraint(
2324 $content,
2325 $this->context,
2326 $this->summary,
2327 $markAsMinor,
2328 $this->context->getLanguage(),
2329 $pstUser
2330 )
2331 );
2332
2333 // Check the constraints
2334 if ( !$constraintRunner->checkConstraints() ) {
2335 $failed = $constraintRunner->getFailedConstraint();
2336 $this->handleFailedConstraint( $failed );
2337 return Status::wrap( $failed->getLegacyStatus() );
2338 }
2339 // END OF MIGRATION TO EDITCONSTRAINT SYSTEM (continued below)
2340 } else { # not $new
2341
2342 # Article exists. Check for edit conflict.
2343
2344 $timestamp = $this->page->getTimestamp();
2345 $latest = $this->page->getLatest();
2346
2347 wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}" );
2348 wfDebug( "revision: {$latest}, editRevId: {$this->editRevId}" );
2349
2350 $editConflictLogger = LoggerFactory::getInstance( 'EditConflict' );
2351 // An edit conflict is detected if the current revision is different from the
2352 // revision that was current when editing was initiated on the client.
2353 // This is checked based on the timestamp and revision ID.
2354 // TODO: the timestamp based check can probably go away now.
2355 if ( ( $this->edittime !== null && $this->edittime != $timestamp )
2356 || ( $this->editRevId !== null && $this->editRevId != $latest )
2357 ) {
2358 $this->isConflict = true;
2359 if ( $this->section === 'new' ) {
2360 if ( $this->page->getUserText() === $requestUser->getName() &&
2361 $this->page->getComment() === $this->summary
2362 ) {
2363 // Probably a duplicate submission of a new comment.
2364 // This can happen when CDN resends a request after
2365 // a timeout but the first one actually went through.
2366 $editConflictLogger->debug(
2367 'Duplicate new section submission; trigger edit conflict!'
2368 );
2369 } else {
2370 // New comment; suppress conflict.
2371 $this->isConflict = false;
2372 $editConflictLogger->debug( 'Conflict suppressed; new section' );
2373 }
2374 } elseif ( $this->section === ''
2375 && $this->edittime
2376 && $this->revisionStore->userWasLastToEdit(
2377 $this->dbProvider->getPrimaryDatabase(),
2378 $this->mTitle->getArticleID(),
2379 $requestUser->getId(),
2380 $this->edittime
2381 )
2382 ) {
2383 # Suppress edit conflict with self, except for section edits where merging is required.
2384 $editConflictLogger->debug( 'Suppressing edit conflict, same user.' );
2385 $this->isConflict = false;
2386 }
2387 }
2388
2389 if ( $this->isConflict ) {
2390 $editConflictLogger->debug(
2391 'Conflict! Getting section {section} for time {editTime}'
2392 . ' (id {editRevId}, article time {timestamp})',
2393 [
2394 'section' => $this->section,
2395 'editTime' => $this->edittime,
2396 'editRevId' => $this->editRevId,
2397 'timestamp' => $timestamp,
2398 ]
2399 );
2400 // @TODO: replaceSectionAtRev() with base ID (not prior current) for ?oldid=X case
2401 // ...or disable section editing for non-current revisions (not exposed anyway).
2402 if ( $this->editRevId !== null ) {
2403 $content = $this->page->replaceSectionAtRev(
2404 $this->section,
2405 $textbox_content,
2406 $this->sectiontitle,
2407 $this->editRevId
2408 );
2409 } else {
2410 $content = $this->page->replaceSectionContent(
2411 $this->section,
2412 $textbox_content,
2413 $this->sectiontitle,
2414 $this->edittime
2415 );
2416 }
2417 } else {
2418 $editConflictLogger->debug(
2419 'Getting section {section}',
2420 [ 'section' => $this->section ]
2421 );
2422 $content = $this->page->replaceSectionAtRev(
2423 $this->section,
2424 $textbox_content,
2425 $this->sectiontitle
2426 );
2427 }
2428
2429 if ( $content === null ) {
2430 $editConflictLogger->debug( 'Activating conflict; section replace failed.' );
2431 $this->isConflict = true;
2432 $content = $textbox_content; // do not try to merge here!
2433 } elseif ( $this->isConflict ) {
2434 // Attempt merge
2435 $mergedChange = $this->mergeChangesIntoContent( $content );
2436 if ( $mergedChange !== false ) {
2437 // Successful merge! Maybe we should tell the user the good news?
2438 $content = $mergedChange[0];
2439 $this->parentRevId = $mergedChange[1];
2440 $this->isConflict = false;
2441 $editConflictLogger->debug( 'Suppressing edit conflict, successful merge.' );
2442 } else {
2443 $this->section = '';
2444 $this->textbox1 = ( $content instanceof TextContent ) ? $content->getText() : '';
2445 $editConflictLogger->debug( 'Keeping edit conflict, failed merge.' );
2446 }
2447 }
2448
2449 if ( $this->isConflict ) {
2450 return Status::newGood( self::AS_CONFLICT_DETECTED )->setOK( false );
2451 }
2452
2453 $pageUpdater = $this->page->newPageUpdater( $pstUser )
2454 ->setContent( SlotRecord::MAIN, $content );
2455 $pageUpdater->prepareUpdate( $flags );
2456
2457 // BEGINNING OF MIGRATION TO EDITCONSTRAINT SYSTEM (see T157658)
2458 // Create a new runner to avoid rechecking the prior constraints, use the same factory
2459 $constraintRunner = new EditConstraintRunner();
2460 $constraintRunner->addConstraint(
2461 $constraintFactory->newEditFilterMergedContentHookConstraint(
2462 $content,
2463 $this->context,
2464 $this->summary,
2465 $markAsMinor,
2466 $this->context->getLanguage(),
2467 $pstUser
2468 )
2469 );
2470 $constraintRunner->addConstraint(
2471 new NewSectionMissingSubjectConstraint(
2472 $this->section,
2473 $this->sectiontitle ?? '',
2474 $this->allowBlankSummary
2475 )
2476 );
2477 $constraintRunner->addConstraint(
2478 new MissingCommentConstraint( $this->section, $this->textbox1 )
2479 );
2480 $constraintRunner->addConstraint(
2481 new ExistingSectionEditConstraint(
2482 $this->section,
2483 $this->summary,
2484 $this->autoSumm,
2485 $this->allowBlankSummary,
2486 $content,
2487 $this->getOriginalContent( $authority )
2488 )
2489 );
2490 // Check the constraints
2491 if ( !$constraintRunner->checkConstraints() ) {
2492 $failed = $constraintRunner->getFailedConstraint();
2493 $this->handleFailedConstraint( $failed );
2494 return Status::wrap( $failed->getLegacyStatus() );
2495 }
2496 // END OF MIGRATION TO EDITCONSTRAINT SYSTEM (continued below)
2497
2498 # All's well
2499 $sectionAnchor = '';
2500 if ( $this->section === 'new' ) {
2501 $sectionAnchor = $this->newSectionAnchor;
2502 } elseif ( $this->section !== '' ) {
2503 # Try to get a section anchor from the section source, redirect
2504 # to edited section if header found.
2505 # XXX: Might be better to integrate this into WikiPage::replaceSectionAtRev
2506 # for duplicate heading checking and maybe parsing.
2507 $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
2508 # We can't deal with anchors, includes, html etc in the header for now,
2509 # headline would need to be parsed to improve this.
2510 if ( $hasmatch && $matches[2] !== '' ) {
2511 $sectionAnchor = $this->guessSectionName( $matches[2] );
2512 }
2513 }
2514 $result['sectionanchor'] = $sectionAnchor;
2515
2516 // Save errors may fall down to the edit form, but we've now
2517 // merged the section into full text. Clear the section field
2518 // so that later submission of conflict forms won't try to
2519 // replace that into a duplicated mess.
2520 $this->textbox1 = $this->toEditText( $content );
2521 $this->section = '';
2522 }
2523
2524 // Check for length errors again now that the section is merged in
2525 $this->contentLength = strlen( $this->toEditText( $content ) );
2526
2527 // BEGINNING OF MIGRATION TO EDITCONSTRAINT SYSTEM (see T157658)
2528 // Create a new runner to avoid rechecking the prior constraints, use the same factory
2529 $constraintRunner = new EditConstraintRunner();
2530 if ( !$this->ignoreProblematicRedirects ) {
2531 $constraintRunner->addConstraint(
2532 new RedirectConstraint(
2533 $this->allowedProblematicRedirectTarget,
2534 $content,
2535 $this->getCurrentContent(),
2536 $this->getTitle(),
2537 $submitButtonLabel,
2538 $this->contentFormat,
2539 $this->redirectLookup
2540 )
2541 );
2542 }
2543 $constraintRunner->addConstraint(
2544 // Same constraint is used to check size before and after merging the
2545 // edits, which use different failure codes
2546 $constraintFactory->newPageSizeConstraint(
2547 $this->contentLength,
2548 PageSizeConstraint::AFTER_MERGE
2549 )
2550 );
2551 // Check the constraints
2552 if ( !$constraintRunner->checkConstraints() ) {
2553 $failed = $constraintRunner->getFailedConstraint();
2554 $this->handleFailedConstraint( $failed );
2555 return Status::wrap( $failed->getLegacyStatus() );
2556 }
2557 // END OF MIGRATION TO EDITCONSTRAINT SYSTEM
2558
2559 if ( $this->undidRev && $this->isUndoClean( $content ) ) {
2560 // As the user can change the edit's content before saving, we only mark
2561 // "clean" undos as reverts. This is to avoid abuse by marking irrelevant
2562 // edits as undos.
2563 $pageUpdater
2564 ->setOriginalRevisionId( $this->undoAfter ?: false )
2565 ->setCause( PageUpdateCauses::CAUSE_UNDO )
2566 ->markAsRevert(
2567 EditResult::REVERT_UNDO,
2568 $this->undidRev,
2569 $this->undoAfter ?: null
2570 );
2571 }
2572
2573 $needsPatrol = $useRCPatrol || ( $useNPPatrol && !$this->page->exists() );
2574 if ( $needsPatrol && $authority->authorizeWrite( 'autopatrol', $this->getTitle() ) ) {
2575 $pageUpdater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
2576 }
2577
2578 $pageUpdater
2579 ->addTags( $this->changeTags )
2580 ->saveRevision(
2581 CommentStoreComment::newUnsavedComment( trim( $this->summary ) ),
2582 $flags
2583 );
2584 $doEditStatus = $pageUpdater->getStatus();
2585
2586 if ( !$doEditStatus->isOK() ) {
2587 // Failure from doEdit()
2588 // Show the edit conflict page for certain recognized errors from doEdit(),
2589 // but don't show it for errors from extension hooks
2590 if (
2591 $doEditStatus->failedBecausePageMissing() ||
2592 $doEditStatus->failedBecausePageExists() ||
2593 $doEditStatus->failedBecauseOfConflict()
2594 ) {
2595 $this->isConflict = true;
2596 // Destroys data doEdit() put in $status->value but who cares
2597 // TODO: We should care, this puts an `int` value into a `Status<array>`
2598 // @phan-suppress-next-line PhanTypeMismatchPropertyProbablyReal
2599 $doEditStatus->value = self::AS_END;
2600 }
2601 return $doEditStatus;
2602 }
2603
2604 $result['nullEdit'] = !$doEditStatus->wasRevisionCreated();
2605 if ( $result['nullEdit'] ) {
2606 // We didn't know if it was a null edit until now, so bump the rate limit now
2607 $limitSubject = $requestUser->toRateLimitSubject();
2608 MediaWikiServices::getInstance()->getRateLimiter()->limit( $limitSubject, 'linkpurge' );
2609 }
2610 $result['redirect'] = $content->isRedirect();
2611
2612 $this->updateWatchlist();
2613
2614 // If the content model changed, add a log entry
2615 if ( $changingContentModel ) {
2616 $this->addContentModelChangeLogEntry(
2617 $this->getUserForSave(),
2618 // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable
2619 // $oldContentModel is set when $changingContentModel is true
2620 $new ? false : $oldContentModel,
2621 $this->contentModel,
2622 $this->summary
2623 );
2624 }
2625
2626 // Instead of carrying the same status object throughout, it is created right
2627 // when it is returned, either at an earlier point due to an error or here
2628 // due to a successful edit.
2629 $statusCode = ( $new ? self::AS_SUCCESS_NEW_ARTICLE : self::AS_SUCCESS_UPDATE );
2630 return Status::newGood( $statusCode );
2631 }
2632
2639 private function handleFailedConstraint( IEditConstraint $failed ): void {
2640 if ( $failed instanceof AuthorizationConstraint ) {
2641 // Auto-block user's IP if the account was "hard" blocked
2642 if (
2643 !MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly()
2644 && $failed->getLegacyStatus()->value === self::AS_BLOCKED_PAGE_FOR_USER
2645 ) {
2646 $this->context->getUser()->spreadAnyEditBlock();
2647 }
2648 } elseif ( $failed instanceof DefaultTextConstraint ) {
2649 $this->blankArticle = true;
2650 } elseif ( $failed instanceof EditFilterMergedContentHookConstraint ) {
2651 $this->hookError = $failed->getHookError();
2652 } elseif (
2653 // ExistingSectionEditConstraint also checks for revisions deleted
2654 // since the edit was loaded, which doesn't indicate a missing summary
2655 (
2656 $failed instanceof ExistingSectionEditConstraint
2657 && $failed->getLegacyStatus()->value === self::AS_SUMMARY_NEEDED
2658 ) ||
2659 $failed instanceof NewSectionMissingSubjectConstraint
2660 ) {
2661 $this->missingSummary = true;
2662 } elseif ( $failed instanceof RedirectConstraint ) {
2663 $this->problematicRedirectTarget = $failed->problematicTarget;
2664 }
2665 }
2666
2677 private function isUndoClean( Content $content ): bool {
2678 // Check whether the undo was "clean", that is the user has not modified
2679 // the automatically generated content.
2680 $undoRev = $this->revisionStore->getRevisionById( $this->undidRev );
2681 if ( $undoRev === null ) {
2682 return false;
2683 }
2684
2685 if ( $this->undoAfter ) {
2686 $oldRev = $this->revisionStore->getRevisionById( $this->undoAfter );
2687 } else {
2688 $oldRev = $this->revisionStore->getPreviousRevision( $undoRev );
2689 }
2690
2691 if ( $oldRev === null ||
2692 $undoRev->isDeleted( RevisionRecord::DELETED_TEXT ) ||
2693 $oldRev->isDeleted( RevisionRecord::DELETED_TEXT )
2694 ) {
2695 return false;
2696 }
2697
2698 $undoContent = $this->getUndoContent( $undoRev, $oldRev, $undoError );
2699 if ( !$undoContent ) {
2700 return false;
2701 }
2702
2703 // Do a pre-save transform on the retrieved undo content
2704 $services = MediaWikiServices::getInstance();
2705 $contentLanguage = $services->getContentLanguage();
2706 $user = $this->getUserForPreview();
2707 $parserOptions = ParserOptions::newFromUserAndLang( $user, $contentLanguage );
2708 $contentTransformer = $services->getContentTransformer();
2709 $undoContent = $contentTransformer->preSaveTransform( $undoContent, $this->mTitle, $user, $parserOptions );
2710
2711 if ( $undoContent->equals( $content ) ) {
2712 return true;
2713 }
2714 return false;
2715 }
2716
2723 private function addContentModelChangeLogEntry( UserIdentity $user, $oldModel, $newModel, $reason = "" ): void {
2724 $new = $oldModel === false;
2725 $log = new ManualLogEntry( 'contentmodel', $new ? 'new' : 'change' );
2726 $log->setPerformer( $user );
2727 $log->setTarget( $this->mTitle );
2728 $log->setComment( is_string( $reason ) ? $reason : "" );
2729 $log->setParameters( [
2730 '4::oldmodel' => $oldModel,
2731 '5::newmodel' => $newModel
2732 ] );
2733 $logid = $log->insert();
2734 $log->publish( $logid );
2735 }
2736
2740 private function updateWatchlist(): void {
2741 if ( $this->tempUserCreateActive ) {
2742 return;
2743 }
2744 $user = $this->getUserForSave();
2745 if ( !$user->isNamed() ) {
2746 return;
2747 }
2748
2749 $title = $this->mTitle;
2750 $watch = $this->watchthis;
2751 $watchlistExpiry = $this->watchlistExpiry;
2752
2753 // This can't run as a DeferredUpdate due to a possible race condition
2754 // when the post-edit redirect happens if the pendingUpdates queue is
2755 // too large to finish in time (T259564)
2756 $this->watchlistManager->setWatch( $watch, $user, $title, $watchlistExpiry );
2757
2758 $this->watchedItemStore->maybeEnqueueWatchlistExpiryJob();
2759 }
2760
2771 private function mergeChangesIntoContent( Content $editContent ) {
2772 // This is the revision that was current at the time editing was initiated on the client,
2773 // even if the edit was based on an old revision.
2774 $baseRevRecord = $this->getExpectedParentRevision();
2775 $baseContent = $baseRevRecord ?
2776 $baseRevRecord->getContent( SlotRecord::MAIN ) :
2777 null;
2778
2779 if ( $baseContent === null ) {
2780 return false;
2781 } elseif ( $baseRevRecord->isCurrent() ) {
2782 // Impossible to have a conflict when the user just edited the latest revision. This can
2783 // happen e.g. when $wgDiff3 is badly configured.
2784 return [ $editContent, $baseRevRecord->getId() ];
2785 }
2786
2787 // The current state, we want to merge updates into it
2788 $currentRevisionRecord = $this->revisionStore->getRevisionByTitle(
2789 $this->mTitle,
2790 0,
2791 IDBAccessObject::READ_LATEST
2792 );
2793 $currentContent = $currentRevisionRecord
2794 ? $currentRevisionRecord->getContent( SlotRecord::MAIN )
2795 : null;
2796
2797 if ( $currentContent === null ) {
2798 return false;
2799 }
2800
2801 $mergedContent = $this->contentHandlerFactory
2802 ->getContentHandler( $baseContent->getModel() )
2803 ->merge3( $baseContent, $editContent, $currentContent );
2804
2805 if ( $mergedContent ) {
2806 // Also need to update parentRevId to what we just merged.
2807 return [ $mergedContent, $currentRevisionRecord->getId() ];
2808 }
2809
2810 return false;
2811 }
2812
2820 public function getExpectedParentRevision() {
2821 if ( $this->mExpectedParentRevision === false ) {
2822 $revRecord = null;
2823 if ( $this->editRevId ) {
2824 $revRecord = $this->revisionStore->getRevisionById(
2825 $this->editRevId,
2826 IDBAccessObject::READ_LATEST
2827 );
2828 } elseif ( $this->edittime ) {
2829 $revRecord = $this->revisionStore->getRevisionByTimestamp(
2830 $this->getTitle(),
2831 $this->edittime,
2832 IDBAccessObject::READ_LATEST
2833 );
2834 }
2835 $this->mExpectedParentRevision = $revRecord;
2836 }
2837 return $this->mExpectedParentRevision;
2838 }
2839
2840 public function setHeaders() {
2841 $out = $this->context->getOutput();
2842
2843 $out->addModules( 'mediawiki.action.edit' );
2844 $out->addModuleStyles( [
2845 'mediawiki.action.edit.styles',
2846 'mediawiki.codex.messagebox.styles',
2847 'mediawiki.editfont.styles',
2848 'mediawiki.interface.helpers.styles',
2849 ] );
2850
2851 $user = $this->context->getUser();
2852
2853 if ( $this->userOptionsLookup->getOption( $user, 'uselivepreview' ) ) {
2854 $out->addModules( 'mediawiki.action.edit.preview' );
2855 }
2856
2857 if ( $this->userOptionsLookup->getOption( $user, 'useeditwarning' ) ) {
2858 $out->addModules( 'mediawiki.action.edit.editWarning' );
2859 }
2860
2861 if ( $this->context->getConfig()->get( MainConfigNames::EnableEditRecovery )
2862 && $this->userOptionsLookup->getOption( $user, 'editrecovery' )
2863 ) {
2864 $wasPosted = $this->getContext()->getRequest()->getMethod() === 'POST';
2865 $out->addJsConfigVars( 'wgEditRecoveryWasPosted', $wasPosted );
2866 $out->addModules( 'mediawiki.editRecovery.edit' );
2867 }
2868
2869 # Enabled article-related sidebar, toplinks, etc.
2870 $out->setArticleRelated( true );
2871
2872 $contextTitle = $this->getContextTitle();
2873 if ( $this->isConflict ) {
2874 $msg = 'editconflict';
2875 } elseif ( $contextTitle->exists() && $this->section != '' ) {
2876 $msg = $this->section === 'new' ? 'editingcomment' : 'editingsection';
2877 } else {
2878 $msg = $contextTitle->exists()
2879 || ( $contextTitle->getNamespace() === NS_MEDIAWIKI
2880 && $contextTitle->getDefaultMessageText() !== false
2881 )
2882 ? 'editing'
2883 : 'creating';
2884 }
2885
2886 # Use the title defined by DISPLAYTITLE magic word when present
2887 # NOTE: getDisplayTitle() returns HTML while getPrefixedText() returns plain text.
2888 # Escape ::getPrefixedText() so that we have HTML in all cases,
2889 # and pass as a "raw" parameter to ::setPageTitleMsg().
2890 $displayTitle = $this->mParserOutput ? $this->mParserOutput->getDisplayTitle() : false;
2891 if ( $displayTitle === false ) {
2892 $displayTitle = htmlspecialchars(
2893 $contextTitle->getPrefixedText(), ENT_QUOTES, 'UTF-8', false
2894 );
2895 } else {
2896 $out->setDisplayTitle( $displayTitle );
2897 }
2898
2899 // Enclose the title with an element. This is used on live preview to update the
2900 // preview of the display title.
2901 $displayTitle = Html::rawElement( 'span', [ 'id' => 'firstHeadingTitle' ], $displayTitle );
2902
2903 $out->setPageTitleMsg( $this->context->msg( $msg )->rawParams( $displayTitle ) );
2904
2905 $config = $this->context->getConfig();
2906
2907 # Transmit the name of the message to JavaScript. This was added for live preview.
2908 # Live preview doesn't use this anymore. The variable is still transmitted because
2909 # Edit Recovery and user scripts use it.
2910 $out->addJsConfigVars( [
2911 'wgEditMessage' => $msg,
2912 ] );
2913
2914 // Add whether to use 'save' or 'publish' messages to JavaScript for post-edit, other
2915 // editors, etc.
2916 $out->addJsConfigVars(
2917 'wgEditSubmitButtonLabelPublish',
2918 $config->get( MainConfigNames::EditSubmitButtonLabelPublish )
2919 );
2920 }
2921
2925 private function showIntro(): void {
2926 $services = MediaWikiServices::getInstance();
2927
2928 // Hardcoded list of notices that are suppressable for historical reasons.
2929 // This feature was originally added for LiquidThreads, to avoid showing non-essential messages
2930 // when commenting in a thread, but some messages were included (or excluded) by mistake before
2931 // its implementation was moved to one place, and this list doesn't make a lot of sense.
2932 // TODO: Remove the suppressIntro feature from EditPage, and invent a better way for extensions
2933 // to skip individual intro messages.
2934 $skip = $this->suppressIntro ? [
2935 'editintro',
2936 'code-editing-intro',
2937 'sharedupload-desc-create',
2938 'sharedupload-desc-edit',
2939 'userpage-userdoesnotexist',
2940 'blocked-notice-logextract',
2941 'newarticletext',
2942 'newarticletextanon',
2943 'recreate-moveddeleted-warn',
2944 ] : [];
2945
2946 $messages = $services->getIntroMessageBuilder()->getIntroMessages(
2947 IntroMessageBuilder::MORE_FRAMES,
2948 $skip,
2949 $this->context,
2950 $this->mTitle->toPageIdentity(),
2951 $this->mArticle->fetchRevisionRecord(),
2952 $this->context->getUser(),
2953 $this->context->getRequest()->getVal( 'editintro' ),
2955 array_diff_key(
2956 $this->context->getRequest()->getQueryValues(),
2957 [ 'title' => true, 'returnto' => true, 'returntoquery' => true ]
2958 )
2959 ),
2960 !$this->firsttime,
2961 $this->section !== '' ? $this->section : null
2962 );
2963
2964 foreach ( $messages as $message ) {
2965 $this->context->getOutput()->addHTML( $message );
2966 }
2967 }
2968
2987 private function toEditText( $content ) {
2988 if ( $content === null || $content === false ) {
2989 return '';
2990 }
2991 if ( is_string( $content ) ) {
2992 return $content;
2993 }
2994
2995 if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2996 throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2997 }
2998
2999 return $content->serialize( $this->contentFormat );
3000 }
3001
3018 protected function toEditContent( $text ) {
3019 if ( $text === false || $text === null ) {
3020 return $text;
3021 }
3022
3023 $content = ContentHandler::makeContent( $text, $this->getTitle(),
3024 $this->contentModel, $this->contentFormat );
3025
3026 if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
3027 throw new MWException( 'This content model is not supported: ' . $content->getModel() );
3028 }
3029
3030 return $content;
3031 }
3032
3036 public function showEditForm() {
3037 # need to parse the preview early so that we know which templates are used,
3038 # otherwise users with "show preview after edit box" will get a blank list
3039 # we parse this near the beginning so that setHeaders can do the title
3040 # setting work instead of leaving it in getPreviewText
3041 $previewOutput = '';
3042 if ( $this->formtype === 'preview' ) {
3043 $previewOutput = $this->getPreviewText();
3044 }
3045
3046 $out = $this->context->getOutput();
3047
3048 // FlaggedRevs depends on running this hook before adding edit notices in showIntro() (T337637)
3049 $this->getHookRunner()->onEditPage__showEditForm_initial( $this, $out );
3050
3051 $this->setHeaders();
3052
3053 // Show applicable editing introductions
3054 $this->showIntro();
3055
3056 if ( !$this->isConflict &&
3057 $this->section !== '' &&
3058 !$this->isSectionEditSupported()
3059 ) {
3060 // We use $this->section to much before this and getVal('wgSection') directly in other places
3061 // at this point we can't reset $this->section to '' to fallback to non-section editing.
3062 // Someone is welcome to try refactoring though
3063 $out->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
3064 return;
3065 }
3066
3067 $this->showHeader();
3068
3069 $out->addHTML( $this->editFormPageTop );
3070
3071 $user = $this->context->getUser();
3072 if ( $this->userOptionsLookup->getOption( $user, 'previewontop' ) ) {
3073 $this->displayPreviewArea( $previewOutput, true );
3074 }
3075
3076 $out->addHTML( $this->editFormTextTop );
3077
3078 if ( $this->formtype !== 'save' && $this->wasDeletedSinceLastEdit() ) {
3079 $out->addHTML( Html::errorBox(
3080 $out->msg( 'deletedwhileediting' )->parse(),
3081 '',
3082 'mw-deleted-while-editing'
3083 ) );
3084 }
3085
3086 // @todo add EditForm plugin interface and use it here!
3087 // search for textarea1 and textarea2, and allow EditForm to override all uses.
3088 $out->addHTML( Html::openElement(
3089 'form',
3090 [
3091 'class' => 'mw-editform',
3092 'id' => self::EDITFORM_ID,
3093 'name' => self::EDITFORM_ID,
3094 'method' => 'post',
3095 'action' => $this->getActionURL( $this->getContextTitle() ),
3096 'enctype' => 'multipart/form-data',
3097 'data-mw-editform-type' => $this->formtype
3098 ]
3099 ) );
3100
3101 // Add a check for Unicode support
3102 $out->addHTML( Html::hidden( 'wpUnicodeCheck', self::UNICODE_CHECK ) );
3103
3104 // Add an empty field to trip up spambots
3105 $out->addHTML(
3106 Html::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
3107 . Html::rawElement(
3108 'label',
3109 [ 'for' => 'wpAntispam' ],
3110 $this->context->msg( 'simpleantispam-label' )->parse()
3111 )
3112 . Html::element(
3113 'input',
3114 [
3115 'type' => 'text',
3116 'name' => 'wpAntispam',
3117 'id' => 'wpAntispam',
3118 'value' => ''
3119 ]
3120 )
3121 . Html::closeElement( 'div' )
3122 );
3123
3124 $this->getHookRunner()->onEditPage__showEditForm_fields( $this, $out );
3125
3126 // Put these up at the top to ensure they aren't lost on early form submission
3127 $this->showFormBeforeText();
3128
3129 if ( $this->formtype === 'save' && $this->wasDeletedSinceLastEdit() ) {
3130 $username = $this->lastDelete->actor_name;
3131 $comment = $this->commentStore->getComment( 'log_comment', $this->lastDelete )->text;
3132
3133 // It is better to not parse the comment at all than to have templates expanded in the middle
3134 // TODO: can the label be moved outside of the div so that wrapWikiMsg could be used?
3135 $key = $comment === ''
3136 ? 'confirmrecreate-noreason'
3137 : 'confirmrecreate';
3138 $out->addHTML( Html::rawElement(
3139 'div',
3140 [ 'class' => 'mw-confirm-recreate' ],
3141 $this->context->msg( $key )
3142 ->params( $username )
3143 ->plaintextParams( $comment )
3144 ->parse() .
3145 Html::rawElement(
3146 'div',
3147 [],
3148 Html::check(
3149 'wpRecreate',
3150 false,
3151 [ 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ]
3152 )
3153 . "\u{00A0}" .
3154 Html::label(
3155 $this->context->msg( 'recreate' )->text(),
3156 'wpRecreate',
3157 [ 'title' => Linker::titleAttrib( 'recreate' ) ]
3158 )
3159 )
3160 ) );
3161 }
3162
3163 # When the summary is hidden, also hide them on preview/show changes
3164 if ( $this->nosummary ) {
3165 $out->addHTML( Html::hidden( 'nosummary', true ) );
3166 }
3167
3168 # If a blank edit summary was previously provided, and the appropriate
3169 # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
3170 # user being bounced back more than once in the event that a summary
3171 # is not required.
3172 # ####
3173 # For a bit more sophisticated detection of blank summaries, hash the
3174 # automatic one and pass that in the hidden field wpAutoSummary.
3175 if (
3176 $this->missingSummary ||
3177 // @phan-suppress-next-line PhanSuspiciousValueComparison
3178 ( $this->section === 'new' && $this->nosummary ) ||
3179 $this->allowBlankSummary
3180 ) {
3181 $out->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
3182 }
3183
3184 if ( $this->undidRev ) {
3185 $out->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
3186 }
3187 if ( $this->undoAfter ) {
3188 $out->addHTML( Html::hidden( 'wpUndoAfter', $this->undoAfter ) );
3189 }
3190
3191 if ( $this->problematicRedirectTarget !== null ) {
3192 // T395767, T395768: Save the target to a variable so the constraint can fail again if the redirect is
3193 // still problematic but has changed between two save attempts
3194 $out->addHTML( Html::hidden(
3195 'wpAllowedProblematicRedirectTarget',
3196 $this->problematicRedirectTarget->getFullText()
3197 ) );
3198 }
3199
3200 $autosumm = $this->autoSumm !== '' ? $this->autoSumm : md5( $this->summary );
3201 $out->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
3202
3203 $out->addHTML( Html::hidden( 'oldid', $this->oldid ) );
3204 $out->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
3205
3206 $out->addHTML( Html::hidden( 'format', $this->contentFormat ) );
3207 $out->addHTML( Html::hidden( 'model', $this->contentModel ) );
3208 if ( $this->changeTags ) {
3209 $out->addHTML( Html::hidden( 'wpChangeTagsAfterPreview', implode( ',', $this->changeTags ) ) );
3210 }
3211
3212 $out->enableOOUI();
3213
3214 if ( $this->section === 'new' ) {
3215 $this->showSummaryInput( true );
3216 $out->addHTML( $this->getSummaryPreview( true ) );
3217 }
3218
3219 $out->addHTML( $this->editFormTextBeforeContent );
3220 if ( $this->isConflict ) {
3221 $currentText = $this->toEditText( $this->getCurrentContent() );
3222
3223 $editConflictHelper = $this->getEditConflictHelper();
3224 $editConflictHelper->setTextboxes( $this->textbox1, $currentText );
3225 $editConflictHelper->setContentModel( $this->contentModel );
3226 $editConflictHelper->setContentFormat( $this->contentFormat );
3227 $out->addHTML( $editConflictHelper->getEditFormHtmlBeforeContent() );
3228
3229 $this->textbox2 = $this->textbox1;
3230 $this->textbox1 = $currentText;
3231 }
3232
3233 if ( !$this->mTitle->isUserConfigPage() ) {
3234 $out->addHTML( self::getEditToolbar() );
3235 }
3236
3237 if ( $this->blankArticle ) {
3238 $out->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
3239 }
3240
3241 if ( $this->isConflict ) {
3242 // In an edit conflict bypass the overridable content form method
3243 // and fallback to the raw wpTextbox1 since editconflicts can't be
3244 // resolved between page source edits and custom ui edits using the
3245 // custom edit ui.
3246 $conflictTextBoxAttribs = [];
3247 if ( $this->wasDeletedSinceLastEdit() ) {
3248 $conflictTextBoxAttribs['style'] = 'display:none;';
3249 } elseif ( $this->isOldRev ) {
3250 $conflictTextBoxAttribs['class'] = 'mw-textarea-oldrev';
3251 }
3252
3253 // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable
3254 // $editConflictHelper is declard, when isConflict is true
3255 $out->addHTML( $editConflictHelper->getEditConflictMainTextBox( $conflictTextBoxAttribs ) );
3256 // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable
3257 // $editConflictHelper is declard, when isConflict is true
3258 $out->addHTML( $editConflictHelper->getEditFormHtmlAfterContent() );
3259 } else {
3260 $this->showContentForm();
3261 }
3262
3263 $out->addHTML( $this->editFormTextAfterContent );
3264
3265 $this->showStandardInputs();
3266
3267 $this->showFormAfterText();
3268
3269 $this->showTosSummary();
3270
3271 $this->showEditTools();
3272
3273 $out->addHTML( $this->editFormTextAfterTools . "\n" );
3274
3275 $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
3276
3277 $out->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
3278 Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
3279
3280 $out->addHTML( Html::rawElement( 'div', [ 'class' => 'limitreport' ],
3281 self::getPreviewLimitReport( $this->mParserOutput ) ) );
3282
3283 $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
3284
3285 if ( $this->isConflict ) {
3286 try {
3287 $this->showConflict();
3288 } catch ( MWContentSerializationException $ex ) {
3289 // this can't really happen, but be nice if it does.
3290 $out->addHTML( Html::errorBox(
3291 $this->context->msg(
3292 'content-failed-to-parse',
3293 $this->contentModel,
3294 $this->contentFormat,
3295 $ex->getMessage()
3296 )->parse()
3297 ) );
3298 }
3299 }
3300
3301 // Set a hidden field so JS knows what edit form mode we are in
3302 if ( $this->isConflict ) {
3303 $mode = 'conflict';
3304 } elseif ( $this->preview ) {
3305 $mode = 'preview';
3306 } elseif ( $this->diff ) {
3307 $mode = 'diff';
3308 } else {
3309 $mode = 'text';
3310 }
3311 $out->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
3312
3313 // Marker for detecting truncated form data. This must be the last
3314 // parameter sent in order to be of use, so do not move me.
3315 $out->addHTML( Html::hidden( 'wpUltimateParam', true ) );
3316 $out->addHTML( $this->editFormTextBottom . "\n</form>\n" );
3317
3318 if ( !$this->userOptionsLookup->getOption( $user, 'previewontop' ) ) {
3319 $this->displayPreviewArea( $previewOutput, false );
3320 }
3321 }
3322
3330 public function makeTemplatesOnThisPageList( array $templates ) {
3331 $templateListFormatter = new TemplatesOnThisPageFormatter(
3332 $this->context,
3333 $this->linkRenderer,
3334 $this->linkBatchFactory,
3335 $this->restrictionStore
3336 );
3337
3338 // preview if preview, else section if section, else false
3339 $type = false;
3340 if ( $this->preview ) {
3341 $type = 'preview';
3342 } elseif ( $this->section !== '' ) {
3343 $type = 'section';
3344 }
3345
3346 return Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
3347 $templateListFormatter->format( $templates, $type )
3348 );
3349 }
3350
3357 private static function extractSectionTitle( $text ) {
3358 if ( preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches ) ) {
3359 return MediaWikiServices::getInstance()->getParser()
3360 ->stripSectionName( trim( $matches[2] ) );
3361 } else {
3362 return false;
3363 }
3364 }
3365
3366 private function showHeader(): void {
3367 $out = $this->context->getOutput();
3368 $user = $this->context->getUser();
3369 if ( $this->isConflict ) {
3370 $this->addExplainConflictHeader();
3371 $this->editRevId = $this->page->getLatest();
3372 } else {
3373 if ( $this->section !== '' && $this->section !== 'new' && $this->summary === '' &&
3374 !$this->preview && !$this->diff
3375 ) {
3376 $sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object
3377 if ( $sectionTitle !== false ) {
3378 $this->summary = "/* $sectionTitle */ ";
3379 }
3380 }
3381
3382 $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
3383
3384 if ( $this->missingSummary && $this->section !== 'new' ) {
3385 $out->wrapWikiMsg(
3386 "<div id='mw-missingsummary'>\n$1\n</div>",
3387 [ 'missingsummary', $buttonLabel ]
3388 );
3389 }
3390
3391 if ( $this->missingSummary && $this->section === 'new' ) {
3392 $out->wrapWikiMsg(
3393 "<div id='mw-missingcommentheader'>\n$1\n</div>",
3394 [ 'missingcommentheader', $buttonLabel ]
3395 );
3396 }
3397
3398 if ( $this->hookError !== '' ) {
3399 $out->addWikiTextAsInterface( $this->hookError );
3400 }
3401
3402 if ( $this->section != 'new' ) {
3403 $revRecord = $this->mArticle->fetchRevisionRecord();
3404 if ( $revRecord && $revRecord instanceof RevisionStoreRecord ) {
3405 // Let sysop know that this will make private content public if saved
3406
3407 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $user ) ) {
3408 $out->addHTML(
3409 Html::warningBox(
3410 $out->msg( 'rev-deleted-text-permission', $this->mTitle->getPrefixedURL() )->parse(),
3411 'plainlinks'
3412 )
3413 );
3414 } elseif ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
3415 $out->addHTML(
3416 Html::warningBox(
3417 // title used in wikilinks, should not contain whitespaces
3418 $out->msg( 'rev-deleted-text-view', $this->mTitle->getPrefixedURL() )->parse(),
3419 'plainlinks'
3420 )
3421 );
3422 }
3423
3424 if ( !$revRecord->isCurrent() ) {
3425 $this->mArticle->setOldSubtitle( $revRecord->getId() );
3426 $this->isOldRev = true;
3427 }
3428 } elseif ( $this->mTitle->exists() ) {
3429 // Something went wrong
3430
3431 $out->addHTML(
3432 Html::errorBox(
3433 $out->msg( 'missing-revision', $this->oldid )->parse()
3434 )
3435 );
3436 }
3437 }
3438 }
3439
3440 $this->addLongPageWarningHeader();
3441 }
3442
3450 private function getSummaryInputAttributes( array $inputAttrs ): array {
3451 // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
3452 // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
3453 // Unicode codepoints.
3454 return $inputAttrs + [
3455 'id' => 'wpSummary',
3456 'name' => 'wpSummary',
3457 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
3458 'tabindex' => 1,
3459 'size' => 60,
3460 'spellcheck' => 'true',
3461 ];
3462 }
3463
3473 private function getSummaryInputWidget( $summary, string $labelText, array $inputAttrs ): FieldLayout {
3474 $inputAttrs = OOUI\Element::configFromHtmlAttributes(
3475 $this->getSummaryInputAttributes( $inputAttrs )
3476 );
3477 $inputAttrs += [
3478 'title' => Linker::titleAttrib( 'summary' ),
3479 'accessKey' => Linker::accesskey( 'summary' ),
3480 ];
3481
3482 // For compatibility with old scripts and extensions, we want the legacy 'id' on the `<input>`
3483 $inputAttrs['inputId'] = $inputAttrs['id'];
3484 $inputAttrs['id'] = 'wpSummaryWidget';
3485
3486 return new OOUI\FieldLayout(
3487 new OOUI\TextInputWidget( [
3488 'value' => $summary,
3489 'infusable' => true,
3490 ] + $inputAttrs ),
3491 [
3492 'label' => new OOUI\HtmlSnippet( $labelText ),
3493 'align' => 'top',
3494 'id' => 'wpSummaryLabel',
3495 'classes' => [ $this->missingSummary ? 'mw-summarymissed' : 'mw-summary' ],
3496 ]
3497 );
3498 }
3499
3505 private function showSummaryInput( bool $isSubjectPreview ): void {
3506 # Add a class if 'missingsummary' is triggered to allow styling of the summary line
3507 $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
3508 if ( $isSubjectPreview ) {
3509 if ( $this->nosummary ) {
3510 return;
3511 }
3512 } elseif ( !$this->mShowSummaryField ) {
3513 return;
3514 }
3515
3516 $labelText = $this->context->msg( $isSubjectPreview ? 'subject' : 'summary' )->parse();
3517 $this->context->getOutput()->addHTML(
3518 $this->getSummaryInputWidget(
3519 $isSubjectPreview ? $this->sectiontitle : $this->summary,
3520 $labelText,
3521 [ 'class' => $summaryClass ]
3522 )->toString()
3523 );
3524 }
3525
3532 private function getSummaryPreview( bool $isSubjectPreview ): string {
3533 // avoid spaces in preview, gets always trimmed on save
3534 $summary = trim( $this->summary );
3535 if ( $summary === '' || ( !$this->preview && !$this->diff ) ) {
3536 return "";
3537 }
3538
3539 $commentFormatter = MediaWikiServices::getInstance()->getCommentFormatter();
3540 $summary = $this->context->msg( 'summary-preview' )->parse()
3541 . $commentFormatter->formatBlock( $summary, $this->mTitle, $isSubjectPreview );
3542 return Html::rawElement( 'div', [ 'class' => 'mw-summary-preview' ], $summary );
3543 }
3544
3545 private function showFormBeforeText(): void {
3546 $out = $this->context->getOutput();
3547 $out->addHTML( Html::hidden( 'wpSection', $this->section ) );
3548 $out->addHTML( Html::hidden( 'wpStarttime', $this->starttime ) );
3549 $out->addHTML( Html::hidden( 'wpEdittime', $this->edittime ) );
3550 $out->addHTML( Html::hidden( 'editRevId', $this->editRevId ) );
3551 $out->addHTML( Html::hidden( 'wpScrolltop', $this->scrolltop, [ 'id' => 'wpScrolltop' ] ) );
3552 }
3553
3554 protected function showFormAfterText() {
3567 $this->context->getOutput()->addHTML(
3568 "\n" .
3569 Html::hidden( "wpEditToken", $this->context->getUser()->getEditToken() ) .
3570 "\n"
3571 );
3572 }
3573
3582 protected function showContentForm() {
3583 $this->showTextbox1();
3584 }
3585
3586 private function showTextbox1(): void {
3587 if ( $this->formtype === 'save' && $this->wasDeletedSinceLastEdit() ) {
3588 $attribs = [ 'style' => 'display:none;' ];
3589 } else {
3590 $builder = new TextboxBuilder();
3591 $classes = $builder->getTextboxProtectionCSSClasses( $this->getTitle() );
3592
3593 # Is an old revision being edited?
3594 if ( $this->isOldRev ) {
3595 $classes[] = 'mw-textarea-oldrev';
3596 }
3597
3598 $attribs = [
3599 'aria-label' => $this->context->msg( 'edit-textarea-aria-label' )->text(),
3600 'tabindex' => 1,
3601 'class' => $classes,
3602 ];
3603 }
3604
3605 $this->showTextbox(
3606 $this->textbox1,
3607 'wpTextbox1',
3608 $attribs
3609 );
3610 }
3611
3612 protected function showTextbox( string $text, string $name, array $customAttribs = [] ) {
3613 $builder = new TextboxBuilder();
3614 $attribs = $builder->buildTextboxAttribs(
3615 $name,
3616 $customAttribs,
3617 $this->context->getUser(),
3618 $this->mTitle
3619 );
3620
3621 $this->context->getOutput()->addHTML(
3622 Html::textarea( $name, $builder->addNewLineAtEnd( $text ), $attribs )
3623 );
3624 }
3625
3626 private function displayPreviewArea( string $previewOutput, bool $isOnTop ): void {
3627 $attribs = [ 'id' => 'wikiPreview' ];
3628 if ( $isOnTop ) {
3629 $attribs['class'] = 'ontop';
3630 }
3631 if ( $this->formtype !== 'preview' ) {
3632 $attribs['style'] = 'display: none;';
3633 }
3634
3635 $out = $this->context->getOutput();
3636 $out->addHTML( Html::openElement( 'div', $attribs ) );
3637
3638 if ( $this->formtype === 'preview' ) {
3639 $this->showPreview( $previewOutput );
3640 }
3641
3642 $out->addHTML( '</div>' );
3643
3644 if ( $this->formtype === 'diff' ) {
3645 try {
3646 $this->showDiff();
3647 } catch ( MWContentSerializationException $ex ) {
3648 $out->addHTML( Html::errorBox(
3649 $this->context->msg(
3650 'content-failed-to-parse',
3651 $this->contentModel,
3652 $this->contentFormat,
3653 $ex->getMessage()
3654 )->parse()
3655 ) );
3656 }
3657 }
3658 }
3659
3666 private function showPreview( string $text ): void {
3667 if ( $this->mArticle instanceof CategoryPage ) {
3668 $this->mArticle->openShowCategory();
3669 }
3670 # This hook seems slightly odd here, but makes things more
3671 # consistent for extensions.
3672 $out = $this->context->getOutput();
3673 $this->getHookRunner()->onOutputPageBeforeHTML( $out, $text );
3674 $out->addHTML( $text );
3675 if ( $this->mArticle instanceof CategoryPage ) {
3676 $this->mArticle->closeShowCategory();
3677 }
3678 }
3679
3687 public function showDiff() {
3688 $oldtitlemsg = 'currentrev';
3689 # if message does not exist, show diff against the preloaded default
3690 if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI && !$this->mTitle->exists() ) {
3691 $oldtext = $this->mTitle->getDefaultMessageText();
3692 if ( $oldtext !== false ) {
3693 $oldtitlemsg = 'defaultmessagetext';
3694 $oldContent = $this->toEditContent( $oldtext );
3695 } else {
3696 $oldContent = null;
3697 }
3698 } else {
3699 $oldContent = $this->getCurrentContent();
3700 }
3701
3702 $textboxContent = $this->toEditContent( $this->textbox1 );
3703 if ( $this->editRevId !== null ) {
3704 $newContent = $this->page->replaceSectionAtRev(
3705 $this->section, $textboxContent, $this->sectiontitle, $this->editRevId
3706 );
3707 } else {
3708 $newContent = $this->page->replaceSectionContent(
3709 $this->section, $textboxContent, $this->sectiontitle, $this->edittime
3710 );
3711 }
3712
3713 if ( $newContent ) {
3714 $this->getHookRunner()->onEditPageGetDiffContent( $this, $newContent );
3715
3716 $user = $this->getUserForPreview();
3717 $parserOptions = ParserOptions::newFromUserAndLang( $user,
3718 MediaWikiServices::getInstance()->getContentLanguage() );
3719 $services = MediaWikiServices::getInstance();
3720 $contentTransformer = $services->getContentTransformer();
3721 $newContent = $contentTransformer->preSaveTransform( $newContent, $this->mTitle, $user, $parserOptions );
3722 }
3723
3724 if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
3725 $oldtitle = $this->context->msg( $oldtitlemsg )->parse();
3726 $newtitle = $this->context->msg( 'yourtext' )->parse();
3727
3728 if ( !$oldContent ) {
3729 $oldContent = $newContent->getContentHandler()->makeEmptyContent();
3730 }
3731
3732 if ( !$newContent ) {
3733 $newContent = $oldContent->getContentHandler()->makeEmptyContent();
3734 }
3735
3736 $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->context );
3737 $de->setContent( $oldContent, $newContent );
3738
3739 $difftext = $de->getDiff( $oldtitle, $newtitle );
3740 $de->showDiffStyle();
3741 } else {
3742 $difftext = '';
3743 }
3744
3745 $this->context->getOutput()->addHTML( Html::rawElement( 'div', [ 'id' => 'wikiDiff' ], $difftext ) );
3746 }
3747
3756 private function showTosSummary(): void {
3757 $msgKey = 'editpage-tos-summary';
3758 $this->getHookRunner()->onEditPageTosSummary( $this->mTitle, $msgKey );
3759 $msg = $this->context->msg( $msgKey );
3760 if ( !$msg->isDisabled() ) {
3761 $this->context->getOutput()->addHTML( Html::rawElement(
3762 'div',
3763 [ 'class' => 'mw-tos-summary' ],
3764 $msg->parseAsBlock()
3765 ) );
3766 }
3767 }
3768
3773 private function showEditTools(): void {
3774 $this->context->getOutput()->addHTML( Html::rawElement(
3775 'div',
3776 [ 'class' => 'mw-editTools' ],
3777 $this->context->msg( 'edittools' )->inContentLanguage()->parse()
3778 ) );
3779 }
3780
3790 public static function getCopyrightWarning( PageReference $page, string $format, MessageLocalizer $localizer ) {
3791 $services = MediaWikiServices::getInstance();
3792 $rightsText = $services->getMainConfig()->get( MainConfigNames::RightsText );
3793 if ( $rightsText ) {
3794 $copywarnMsg = [ 'copyrightwarning',
3795 '[[' . $localizer->msg( 'copyrightpage' )->inContentLanguage()->text() . ']]',
3796 $rightsText ];
3797 } else {
3798 $copywarnMsg = [ 'copyrightwarning2',
3799 '[[' . $localizer->msg( 'copyrightpage' )->inContentLanguage()->text() . ']]' ];
3800 }
3801 // Allow for site and per-namespace customization of contribution/copyright notice.
3802 $title = Title::newFromPageReference( $page );
3803 ( new HookRunner( $services->getHookContainer() ) )->onEditPageCopyrightWarning( $title, $copywarnMsg );
3804 if ( !$copywarnMsg ) {
3805 return '';
3806 }
3807
3808 $msg = $localizer->msg( ...$copywarnMsg )->page( $page );
3809 return Html::rawElement( 'div', [ 'id' => 'editpage-copywarn' ], $msg->$format() );
3810 }
3811
3819 public static function getPreviewLimitReport( ?ParserOutput $output = null ) {
3820 if ( !$output || !$output->getLimitReportData() ) {
3821 return '';
3822 }
3823
3824 $limitReport = Html::rawElement( 'div', [ 'class' => 'mw-limitReportExplanation' ],
3825 wfMessage( 'limitreport-title' )->parseAsBlock()
3826 );
3827
3828 // Show/hide animation doesn't work correctly on a table, so wrap it in a div.
3829 $limitReport .= Html::openElement( 'div', [ 'class' => 'preview-limit-report-wrapper' ] );
3830
3831 $limitReport .= Html::openElement( 'table', [
3832 'class' => 'preview-limit-report wikitable'
3833 ] ) .
3834 Html::openElement( 'tbody' );
3835
3836 $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
3837 foreach ( $output->getLimitReportData() as $key => $value ) {
3838 if ( in_array( $key, [
3839 'cachereport-origin',
3840 'cachereport-timestamp',
3841 'cachereport-ttl',
3842 'cachereport-transientcontent',
3843 'limitreport-timingprofile',
3844 ] ) ) {
3845 // These entries have non-numeric parameters, and can't be displayed by this code.
3846 // They are used by the plaintext limit report (see RenderDebugInfo::debugInfo()).
3847 // TODO: Display this information in the table somehow.
3848 continue;
3849 }
3850
3851 if ( $hookRunner->onParserLimitReportFormat( $key, $value, $limitReport, true, true ) ) {
3852 $keyMsg = wfMessage( $key );
3853 $valueMsg = wfMessage( "$key-value" );
3854 if ( !$valueMsg->exists() ) {
3855 // This is formatted raw, not as localized number.
3856 // If you want the parameter formatted as a number,
3857 // define the `$key-value` message.
3858 $valueMsg = ( new RawMessage( '$1' ) )->params( $value );
3859 } else {
3860 // If you define the `$key-value` or `$key-value-html`
3861 // message then the argument *must* be numeric.
3862 $valueMsg = $valueMsg->numParams( $value );
3863 }
3864 if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
3865 $limitReport .= Html::openElement( 'tr' ) .
3866 Html::rawElement( 'th', [], $keyMsg->parse() ) .
3867 Html::rawElement( 'td', [], $valueMsg->parse() ) .
3868 Html::closeElement( 'tr' );
3869 }
3870 }
3871 }
3872
3873 $limitReport .= Html::closeElement( 'tbody' ) .
3874 Html::closeElement( 'table' ) .
3875 Html::closeElement( 'div' );
3876
3877 return $limitReport;
3878 }
3879
3880 protected function showStandardInputs( int &$tabindex = 2 ) {
3881 $out = $this->context->getOutput();
3882 $out->addHTML( "<div class='editOptions'>\n" );
3883
3884 if ( $this->section !== 'new' ) {
3885 $this->showSummaryInput( false );
3886 $out->addHTML( $this->getSummaryPreview( false ) );
3887 }
3888
3889 // When previewing, override the selected dropdown option to select whatever was posted
3890 // (if it's a valid option) rather than the current value for watchlistExpiry.
3891 // See also above in $this->importFormDataPosted().
3892 $expiryFromRequest = null;
3893 if ( $this->preview || $this->diff || $this->isConflict ) {
3894 $expiryFromRequest = $this->getContext()->getRequest()->getText( 'wpWatchlistExpiry' );
3895 }
3896
3897 $checkboxes = $this->getCheckboxesWidget(
3898 $tabindex,
3899 [ 'minor' => $this->minoredit, 'watch' => $this->watchthis, 'wpWatchlistExpiry' => $expiryFromRequest ]
3900 );
3901 $checkboxesHTML = new OOUI\HorizontalLayout( [ 'items' => array_values( $checkboxes ) ] );
3902
3903 $out->addHTML( "<div class='editCheckboxes'>" . $checkboxesHTML . "</div>\n" );
3904
3905 // Show copyright warning.
3906 $out->addHTML( self::getCopyrightWarning( $this->mTitle, 'parse', $this->context ) );
3907 $out->addHTML( $this->editFormTextAfterWarn );
3908
3909 $out->addHTML( "<div class='editButtons'>\n" );
3910 $out->addHTML( implode( "\n", $this->getEditButtons( $tabindex ) ) . "\n" );
3911
3912 $cancel = $this->getCancelLink( $tabindex++ );
3913
3914 $edithelp = $this->getHelpLink() .
3915 $this->context->msg( 'word-separator' )->escaped() .
3916 $this->context->msg( 'newwindow' )->parse();
3917
3918 $out->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" );
3919 $out->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" );
3920 $out->addHTML( "</div><!-- editButtons -->\n" );
3921
3922 $this->getHookRunner()->onEditPage__showStandardInputs_options( $this, $out, $tabindex );
3923
3924 $out->addHTML( "</div><!-- editOptions -->\n" );
3925 }
3926
3931 private function showConflict(): void {
3932 $out = $this->context->getOutput();
3933 if ( $this->getHookRunner()->onEditPageBeforeConflictDiff( $this, $out ) ) {
3934 $this->incrementConflictStats();
3935
3936 $this->getEditConflictHelper()->showEditFormTextAfterFooters();
3937 }
3938 }
3939
3940 private function incrementConflictStats(): void {
3941 $this->getEditConflictHelper()->incrementConflictStats( $this->context->getUser() );
3942 }
3943
3944 private function getHelpLink(): string {
3945 $message = $this->context->msg( 'edithelppage' )->inContentLanguage()->text();
3946 $editHelpUrl = Skin::makeInternalOrExternalUrl( $message );
3947 return Html::element( 'a', [
3948 'href' => $editHelpUrl,
3949 'target' => 'helpwindow'
3950 ], $this->context->msg( 'edithelp' )->text() );
3951 }
3952
3957 private function getCancelLink( int $tabindex ): ButtonWidget {
3958 $cancelParams = [];
3959 if ( !$this->isConflict && $this->oldid > 0 ) {
3960 $cancelParams['oldid'] = $this->oldid;
3961 } elseif ( $this->getContextTitle()->isRedirect() ) {
3962 $cancelParams['redirect'] = 'no';
3963 }
3964
3965 return new OOUI\ButtonWidget( [
3966 'id' => 'mw-editform-cancel',
3967 'tabIndex' => $tabindex,
3968 'href' => $this->getContextTitle()->getLinkURL( $cancelParams ),
3969 'label' => new OOUI\HtmlSnippet( $this->context->msg( 'cancel' )->parse() ),
3970 'framed' => false,
3971 'infusable' => true,
3972 'flags' => 'destructive',
3973 ] );
3974 }
3975
3985 protected function getActionURL( Title $title ) {
3986 $request = $this->context->getRequest();
3987 $params = $request->getQueryValuesOnly();
3988
3989 $allowedFormParams = [
3990 'section', 'oldid', 'preloadtitle', 'undo', 'undoafter',
3991 // Considered safe in all contexts
3992 'uselang', 'useskin', 'useformat', 'variant', 'debug', 'safemode'
3993 ];
3994 $formParams = [ 'action' => $this->action ];
3995 foreach ( $params as $arg => $val ) {
3996 if ( in_array( $arg, $allowedFormParams, true ) ) {
3997 $formParams[$arg] = $val;
3998 }
3999 }
4000
4001 return $title->getLocalURL( $formParams );
4002 }
4003
4010 private function wasDeletedSinceLastEdit(): bool {
4011 if ( $this->deletedSinceEdit !== null ) {
4012 return $this->deletedSinceEdit;
4013 }
4014
4015 $this->deletedSinceEdit = false;
4016
4017 if ( !$this->mTitle->exists() && $this->mTitle->hasDeletedEdits() ) {
4018 $this->lastDelete = $this->getLastDelete();
4019 if ( $this->lastDelete ) {
4020 $deleteTime = wfTimestamp( TS::MW, $this->lastDelete->log_timestamp );
4021 if ( $deleteTime > $this->starttime ) {
4022 $this->deletedSinceEdit = true;
4023 }
4024 }
4025 }
4026
4027 return $this->deletedSinceEdit;
4028 }
4029
4035 private function getLastDelete(): ?stdClass {
4036 $dbr = $this->dbProvider->getReplicaDatabase();
4037 $commentQuery = $this->commentStore->getJoin( 'log_comment' );
4038 $data = $dbr->newSelectQueryBuilder()
4039 ->select( [
4040 'log_type',
4041 'log_action',
4042 'log_timestamp',
4043 'log_namespace',
4044 'log_title',
4045 'log_params',
4046 'log_deleted',
4047 'actor_name'
4048 ] )
4049 ->from( 'logging' )
4050 ->join( 'actor', null, 'actor_id=log_actor' )
4051 ->where( [
4052 'log_namespace' => $this->mTitle->getNamespace(),
4053 'log_title' => $this->mTitle->getDBkey(),
4054 'log_type' => 'delete',
4055 'log_action' => 'delete',
4056 ] )
4057 ->orderBy( [ 'log_timestamp', 'log_id' ], SelectQueryBuilder::SORT_DESC )
4058 ->queryInfo( $commentQuery )
4059 ->caller( __METHOD__ )
4060 ->fetchRow();
4061 // Quick paranoid permission checks...
4062 if ( $data !== false ) {
4063 if ( $data->log_deleted & LogPage::DELETED_USER ) {
4064 $data->actor_name = $this->context->msg( 'rev-deleted-user' )->escaped();
4065 }
4066
4067 if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
4068 $data->log_comment_text = $this->context->msg( 'rev-deleted-comment' )->escaped();
4069 $data->log_comment_data = null;
4070 }
4071 }
4072
4073 return $data ?: null;
4074 }
4075
4081 public function getPreviewText() {
4082 $out = $this->context->getOutput();
4083 $config = $this->context->getConfig();
4084
4085 if ( $config->get( MainConfigNames::RawHtml ) && !$this->mTokenOk ) {
4086 // Could be an offsite preview attempt. This is very unsafe if
4087 // HTML is enabled, as it could be an attack.
4088 $parsedNote = '';
4089 if ( $this->textbox1 !== '' ) {
4090 // Do not put big scary notice, if previewing the empty
4091 // string, which happens when you initially edit
4092 // a category page, due to automatic preview-on-open.
4093 $parsedNote = Html::rawElement( 'div', [ 'class' => 'previewnote' ],
4094 $out->parseAsInterface(
4095 $this->context->msg( 'session_fail_preview_html' )->plain()
4096 ) );
4097 }
4098 $this->incrementEditFailureStats( 'session_loss' );
4099 return $parsedNote;
4100 }
4101
4102 $note = '';
4103
4104 try {
4105 $content = $this->toEditContent( $this->textbox1 );
4106
4107 $previewHTML = '';
4108 if ( !$this->getHookRunner()->onAlternateEditPreview(
4109 $this, $content, $previewHTML, $this->mParserOutput )
4110 ) {
4111 return $previewHTML;
4112 }
4113
4114 # provide a anchor link to the editform
4115 $continueEditing = '<span class="mw-continue-editing">' .
4116 '[[#' . self::EDITFORM_ID . '|' .
4117 $this->context->getLanguage()->getArrow() . ' ' .
4118 $this->context->msg( 'continue-editing' )->text() . ']]</span>';
4119 if ( $this->mTriedSave && !$this->mTokenOk ) {
4120 $note = $this->context->msg( 'session_fail_preview' )->plain();
4121 $this->incrementEditFailureStats( 'session_loss' );
4122 } elseif ( $this->incompleteForm ) {
4123 $note = $this->context->msg( 'edit_form_incomplete' )->plain();
4124 if ( $this->mTriedSave ) {
4125 $this->incrementEditFailureStats( 'incomplete_form' );
4126 }
4127 } else {
4128 $note = $this->context->msg( 'previewnote' )->plain() . ' ' . $continueEditing;
4129 }
4130
4131 # don't parse non-wikitext pages, show message about preview
4132 if ( $this->mTitle->isUserConfigPage() || $this->mTitle->isSiteConfigPage() ) {
4133 if ( $this->mTitle->isUserConfigPage() ) {
4134 $level = 'user';
4135 } elseif ( $this->mTitle->isSiteConfigPage() ) {
4136 $level = 'site';
4137 } else {
4138 $level = false;
4139 }
4140
4141 if ( $content->getModel() === CONTENT_MODEL_CSS ) {
4142 $format = 'css';
4143 if ( $level === 'user' && !$config->get( MainConfigNames::AllowUserCss ) ) {
4144 $format = false;
4145 }
4146 } elseif ( $content->getModel() === CONTENT_MODEL_JSON ) {
4147 $format = 'json';
4148 if ( $level === 'user' /* No comparable 'AllowUserJson' */ ) {
4149 $format = false;
4150 }
4151 } elseif ( $content->getModel() === CONTENT_MODEL_JAVASCRIPT ) {
4152 $format = 'js';
4153 if ( $level === 'user' && !$config->get( MainConfigNames::AllowUserJs ) ) {
4154 $format = false;
4155 }
4156 } elseif ( $content->getModel() === CONTENT_MODEL_VUE ) {
4157 $format = 'vue';
4158 if ( $level === 'user' && !$config->get( MainConfigNames::AllowUserJs ) ) {
4159 $format = false;
4160 }
4161 } else {
4162 $format = false;
4163 }
4164
4165 # Used messages to make sure grep find them:
4166 # Messages: usercsspreview, userjsonpreview, userjspreview,
4167 # sitecsspreview, sitejsonpreview, sitejspreview
4168 if ( $level && $format ) {
4169 $note = "<div id='mw-{$level}{$format}preview'>" .
4170 $this->context->msg( "{$level}{$format}preview" )->plain() .
4171 ' ' . $continueEditing . "</div>";
4172 }
4173 }
4174
4175 if ( $this->section === "new" ) {
4176 $content = $content->addSectionHeader( $this->sectiontitle );
4177 }
4178
4179 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
4180 $this->getHookRunner()->onEditPageGetPreviewContent( $this, $content );
4181
4182 $parserResult = $this->doPreviewParse( $content );
4183 $parserOutput = $parserResult['parserOutput'];
4184 $previewHTML = $parserResult['html'];
4185 $this->mParserOutput = $parserOutput;
4186 $out->addParserOutputMetadata( $parserOutput );
4187 if ( $out->userCanPreview() ) {
4188 $out->addContentOverride( $this->getTitle(), $content );
4189 }
4190
4191 foreach ( $parserOutput->getWarningMsgs() as $mv ) {
4192 $note .= "\n\n" . $this->context->msg( $mv )->text();
4193 }
4194
4195 } catch ( MWContentSerializationException $ex ) {
4196 $m = $this->context->msg(
4197 'content-failed-to-parse',
4198 $this->contentModel,
4199 $this->contentFormat,
4200 $ex->getMessage()
4201 );
4202 $note .= "\n\n" . $m->plain(); # gets parsed down below
4203 $previewHTML = '';
4204 }
4205
4206 if ( $this->isConflict ) {
4207 $conflict = Html::warningBox(
4208 $this->context->msg( 'previewconflict' )->escaped(),
4209 'mw-previewconflict'
4210 );
4211 } else {
4212 $conflict = '';
4213 }
4214
4215 $previewhead = Html::rawElement(
4216 'div', [ 'class' => 'previewnote' ],
4217 Html::rawElement(
4218 'h2', [ 'id' => 'mw-previewheader' ],
4219 $this->context->msg( 'preview' )->escaped()
4220 ) .
4221 Html::warningBox(
4222 $out->parseAsInterface( $note )
4223 ) . $conflict
4224 );
4225
4226 return $previewhead . $previewHTML . $this->previewTextAfterContent;
4227 }
4228
4229 private function incrementEditFailureStats( string $failureType ): void {
4230 MediaWikiServices::getInstance()->getStatsFactory()
4231 ->getCounter( 'edit_failure_total' )
4232 ->setLabel( 'cause', $failureType )
4233 ->setLabel( 'namespace', 'n/a' )
4234 ->setLabel( 'user_bucket', 'n/a' )
4235 ->increment();
4236 }
4237
4242 protected function getPreviewParserOptions() {
4243 $parserOptions = $this->page->makeParserOptions( $this->context );
4244 $parserOptions->setRenderReason( 'page-preview' );
4245 $parserOptions->setIsPreview( true );
4246 $parserOptions->setIsSectionPreview( $this->section !== null && $this->section !== '' );
4247
4248 // XXX: we could call $parserOptions->setCurrentRevisionRecordCallback here to force the
4249 // current revision to be null during PST, until setupFakeRevision is called on
4250 // the ParserOptions. Currently, we rely on Parser::getRevisionRecordObject() to ignore
4251 // existing revisions in preview mode.
4252
4253 return $parserOptions;
4254 }
4255
4265 protected function doPreviewParse( Content $content ) {
4266 $user = $this->getUserForPreview();
4267 $parserOptions = $this->getPreviewParserOptions();
4268
4269 // NOTE: preSaveTransform doesn't have a fake revision to operate on.
4270 // Parser::getRevisionRecordObject() will return null in preview mode,
4271 // causing the context user to be used for {{subst:REVISIONUSER}}.
4272 // XXX: Alternatively, we could also call setupFakeRevision()
4273 // before PST with $content.
4274 $services = MediaWikiServices::getInstance();
4275 $contentTransformer = $services->getContentTransformer();
4276 $contentRenderer = $services->getContentRenderer();
4277 $pstContent = $contentTransformer->preSaveTransform( $content, $this->mTitle, $user, $parserOptions );
4278 $parserOutput = $contentRenderer->getParserOutput( $pstContent, $this->mTitle, null, $parserOptions );
4279 $out = $this->context->getOutput();
4280 $skin = $out->getSkin();
4281 $skinOptions = $skin->getOptions();
4282 // TODO T371004 move runOutputPipeline out of $parserOutput
4283 // TODO T371022 ideally we clone here, but for now let's reproduce getText behaviour
4284 $oldHtml = $parserOutput->getRawText();
4285 $html = $parserOutput->runOutputPipeline( $parserOptions, [
4286 'allowClone' => 'false',
4287 'userLang' => $skin->getLanguage(),
4288 'injectTOC' => $skinOptions['toc'],
4289 'enableSectionEditLinks' => false,
4290 'includeDebugInfo' => true,
4291 ] )->getContentHolderText();
4292 $parserOutput->setRawText( $oldHtml );
4293 return [
4294 'parserOutput' => $parserOutput,
4295 'html' => $html
4296 ];
4297 }
4298
4302 public function getTemplates() {
4303 if ( $this->preview || $this->section !== '' ) {
4304 $templates = [];
4305 if ( !$this->mParserOutput ) {
4306 return $templates;
4307 }
4308 foreach (
4309 $this->mParserOutput->getLinkList( ParserOutputLinkTypes::TEMPLATE )
4310 as [ 'link' => $link ]
4311 ) {
4312 $templates[] = Title::newFromLinkTarget( $link );
4313 }
4314 return $templates;
4315 } else {
4316 return $this->mTitle->getTemplateLinksFrom();
4317 }
4318 }
4319
4325 public static function getEditToolbar() {
4326 $startingToolbar = '<div id="toolbar"></div>';
4327 $toolbar = $startingToolbar;
4328
4329 $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
4330 if ( !$hookRunner->onEditPageBeforeEditToolbar( $toolbar ) ) {
4331 return null;
4332 }
4333 // Don't add a pointless `<div>` to the page unless a hook caller populated it
4334 return ( $toolbar === $startingToolbar ) ? null : $toolbar;
4335 }
4336
4362 public function getCheckboxesDefinition( $values ) {
4363 $checkboxes = [];
4364
4365 $user = $this->context->getUser();
4366 // don't show the minor edit checkbox if it's a new page or section
4367 if ( !$this->isNew && $this->permManager->userHasRight( $user, 'minoredit' ) ) {
4368 $checkboxes['wpMinoredit'] = [
4369 'id' => 'wpMinoredit',
4370 'label-message' => 'minoredit',
4371 // Uses messages: tooltip-minoredit, accesskey-minoredit
4372 'tooltip' => 'minoredit',
4373 'label-id' => 'mw-editpage-minoredit',
4374 'legacy-name' => 'minor',
4375 'default' => $values['minor'],
4376 ];
4377 }
4378
4379 if ( $user->isNamed() ) {
4380 $checkboxes = array_merge(
4381 $checkboxes,
4382 $this->getCheckboxesDefinitionForWatchlist( $values['watch'], $values['wpWatchlistExpiry'] ?? null )
4383 );
4384 }
4385
4386 $this->getHookRunner()->onEditPageGetCheckboxesDefinition( $this, $checkboxes );
4387
4388 return $checkboxes;
4389 }
4390
4398 private function getCheckboxesDefinitionForWatchlist( $watch, $watchexpiry ): array {
4399 $fieldDefs = [
4400 'wpWatchthis' => [
4401 'id' => 'wpWatchthis',
4402 'label-message' => 'watchthis',
4403 // Uses messages: tooltip-watch, accesskey-watch
4404 'tooltip' => 'watch',
4405 'label-id' => 'mw-editpage-watch',
4406 'legacy-name' => 'watch',
4407 'default' => $watch,
4408 ]
4409 ];
4410 if ( $this->watchlistExpiryEnabled ) {
4411 $watchedItem = $this->watchedItemStore->getWatchedItem( $this->getContext()->getUser(), $this->getTitle() );
4412 if ( $watchedItem instanceof WatchedItem && $watchedItem->getExpiry() === null ) {
4413 // Not temporarily watched, so we always default to infinite.
4414 $userPreferredExpiry = 'infinite';
4415 } else {
4416 $userPreferredExpiryOption = !$this->getTitle()->exists()
4417 ? 'watchcreations-expiry'
4418 : 'watchdefault-expiry';
4419 $userPreferredExpiry = $this->userOptionsLookup->getOption(
4420 $this->getContext()->getUser(),
4421 $userPreferredExpiryOption,
4422 'infinite'
4423 );
4424 }
4425
4426 $expiryOptions = WatchAction::getExpiryOptions(
4427 $this->getContext(),
4428 $watchedItem,
4429 $userPreferredExpiry
4430 );
4431
4432 if ( $watchexpiry && in_array( $watchexpiry, $expiryOptions['options'] ) ) {
4433 $expiryOptions['default'] = $watchexpiry;
4434 }
4435 // When previewing, override the selected dropdown option to select whatever was posted
4436 // (if it's a valid option) rather than the current value for watchlistExpiry.
4437 // See also above in $this->importFormDataPosted().
4438 $expiryFromRequest = $this->getContext()->getRequest()->getText( 'wpWatchlistExpiry' );
4439 if ( ( $this->preview || $this->diff ) && in_array( $expiryFromRequest, $expiryOptions['options'] ) ) {
4440 $expiryOptions['default'] = $expiryFromRequest;
4441 }
4442
4443 // Reformat the options to match what DropdownInputWidget wants.
4444 $options = [];
4445 foreach ( $expiryOptions['options'] as $label => $value ) {
4446 $options[] = [ 'data' => $value, 'label' => $label ];
4447 }
4448
4449 $fieldDefs['wpWatchlistExpiry'] = [
4450 'id' => 'wpWatchlistExpiry',
4451 'label-message' => 'confirm-watch-label',
4452 // Uses messages: tooltip-watchlist-expiry, accesskey-watchlist-expiry
4453 'tooltip' => 'watchlist-expiry',
4454 'label-id' => 'mw-editpage-watchlist-expiry',
4455 'default' => $expiryOptions['default'],
4456 'value-attr' => 'value',
4457 'class' => DropdownInputWidget::class,
4458 'options' => $options,
4459 'invisibleLabel' => true,
4460 ];
4461 }
4462 return $fieldDefs;
4463 }
4464
4475 public function getCheckboxesWidget( &$tabindex, $values ) {
4476 $checkboxes = [];
4477 $checkboxesDef = $this->getCheckboxesDefinition( $values );
4478
4479 foreach ( $checkboxesDef as $name => $options ) {
4480 $legacyName = $options['legacy-name'] ?? $name;
4481
4482 $title = null;
4483 $accesskey = null;
4484 if ( isset( $options['tooltip'] ) ) {
4485 $accesskey = $this->context->msg( "accesskey-{$options['tooltip']}" )->text();
4486 $title = Linker::titleAttrib( $options['tooltip'] );
4487 }
4488 if ( isset( $options['title-message'] ) ) {
4489 $title = $this->context->msg( $options['title-message'] )->text();
4490 }
4491 // Allow checkbox definitions to set their own class and value-attribute names.
4492 // See $this->getCheckboxesDefinition() for details.
4493 $className = $options['class'] ?? CheckboxInputWidget::class;
4494 $valueAttr = $options['value-attr'] ?? 'selected';
4495 $checkboxes[ $legacyName ] = new FieldLayout(
4496 new $className( [
4497 'tabIndex' => ++$tabindex,
4498 'accessKey' => $accesskey,
4499 'id' => $options['id'] . 'Widget',
4500 'inputId' => $options['id'],
4501 'name' => $name,
4502 $valueAttr => $options['default'],
4503 'infusable' => true,
4504 'options' => $options['options'] ?? null,
4505 ] ),
4506 [
4507 'align' => 'inline',
4508 'label' => new OOUI\HtmlSnippet( $this->context->msg( $options['label-message'] )->parse() ),
4509 'title' => $title,
4510 'id' => $options['label-id'] ?? null,
4511 'invisibleLabel' => $options['invisibleLabel'] ?? null,
4512 ]
4513 );
4514 }
4515
4516 return $checkboxes;
4517 }
4518
4522 private function getSubmitButtonLabel(): string {
4523 $labelAsPublish =
4524 $this->context->getConfig()->get( MainConfigNames::EditSubmitButtonLabelPublish );
4525
4526 // Can't use $this->isNew as that's also true if we're adding a new section to an extant page
4527 $newPage = !$this->mTitle->exists();
4528
4529 if ( $labelAsPublish ) {
4530 $buttonLabelKey = $newPage ? 'publishpage' : 'publishchanges';
4531 } else {
4532 $buttonLabelKey = $newPage ? 'savearticle' : 'savechanges';
4533 }
4534
4535 return $buttonLabelKey;
4536 }
4537
4548 public function getEditButtons( &$tabindex ) {
4549 $buttons = [];
4550
4551 $labelAsPublish =
4552 $this->context->getConfig()->get( MainConfigNames::EditSubmitButtonLabelPublish );
4553
4554 $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
4555 $buttonTooltip = $labelAsPublish ? 'publish' : 'save';
4556
4557 $buttons['save'] = new OOUI\ButtonInputWidget( [
4558 'name' => 'wpSave',
4559 'tabIndex' => ++$tabindex,
4560 'id' => 'wpSaveWidget',
4561 'inputId' => 'wpSave',
4562 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4563 'useInputTag' => true,
4564 'flags' => [ 'progressive', 'primary' ],
4565 'label' => $buttonLabel,
4566 'infusable' => true,
4567 'type' => 'submit',
4568 // Messages used: tooltip-save, tooltip-publish
4569 'title' => Linker::titleAttrib( $buttonTooltip ),
4570 // Messages used: accesskey-save, accesskey-publish
4571 'accessKey' => Linker::accesskey( $buttonTooltip ),
4572 ] );
4573
4574 $buttons['preview'] = new OOUI\ButtonInputWidget( [
4575 'name' => 'wpPreview',
4576 'tabIndex' => ++$tabindex,
4577 'id' => 'wpPreviewWidget',
4578 'inputId' => 'wpPreview',
4579 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4580 'useInputTag' => true,
4581 'label' => $this->context->msg( 'showpreview' )->text(),
4582 'infusable' => true,
4583 'type' => 'submit',
4584 // Allow previewing even when the form is in invalid state (T343585)
4585 'formNoValidate' => true,
4586 // Message used: tooltip-preview
4587 'title' => Linker::titleAttrib( 'preview' ),
4588 // Message used: accesskey-preview
4589 'accessKey' => Linker::accesskey( 'preview' ),
4590 ] );
4591
4592 $buttons['diff'] = new OOUI\ButtonInputWidget( [
4593 'name' => 'wpDiff',
4594 'tabIndex' => ++$tabindex,
4595 'id' => 'wpDiffWidget',
4596 'inputId' => 'wpDiff',
4597 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4598 'useInputTag' => true,
4599 'label' => $this->context->msg( 'showdiff' )->text(),
4600 'infusable' => true,
4601 'type' => 'submit',
4602 // Allow previewing even when the form is in invalid state (T343585)
4603 'formNoValidate' => true,
4604 // Message used: tooltip-diff
4605 'title' => Linker::titleAttrib( 'diff' ),
4606 // Message used: accesskey-diff
4607 'accessKey' => Linker::accesskey( 'diff' ),
4608 ] );
4609
4610 $this->getHookRunner()->onEditPageBeforeEditButtons( $this, $buttons, $tabindex );
4611
4612 return $buttons;
4613 }
4614
4619 private function noSuchSectionPage(): void {
4620 $out = $this->context->getOutput();
4621 $out->prepareErrorPage();
4622 $out->setPageTitleMsg( $this->context->msg( 'nosuchsectiontitle' ) );
4623
4624 $res = $this->context->msg( 'nosuchsectiontext', $this->section )->parseAsBlock();
4625
4626 $this->getHookRunner()->onEditPageNoSuchSection( $this, $res );
4627 $out->addHTML( $res );
4628
4629 $out->returnToMain( false, $this->mTitle );
4630 }
4631
4637 public function spamPageWithContent( $match = false ) {
4638 $this->textbox2 = $this->textbox1;
4639
4640 $out = $this->context->getOutput();
4641 $out->prepareErrorPage();
4642 $out->setPageTitleMsg( $this->context->msg( 'spamprotectiontitle' ) );
4643
4644 $spamText = $this->context->msg( 'spamprotectiontext' )->parseAsBlock();
4645
4646 if ( $match ) {
4647 if ( is_array( $match ) ) {
4648 // Do not use `wfEscapeWikiText( ... )` here for compatibility with PHP <8.1.4
4649 // https://gerrit.wikimedia.org/r/c/mediawiki/core/+/1160800/comment/92e67687_ab221188/
4650 $matchText = $this->context->getLanguage()->listToText( array_map( 'wfEscapeWikiText', $match ) );
4651 } else {
4652 $matchText = wfEscapeWikiText( $match );
4653 }
4654
4655 $spamText .= $this->context->msg( 'spamprotectionmatch' )
4656 ->params( $matchText )
4657 ->parseAsBlock();
4658 }
4659 $out->addHTML( Html::rawElement(
4660 'div',
4661 [ 'id' => 'spamprotected' ],
4662 $spamText
4663 ) );
4664
4665 $out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
4666 $this->showDiff();
4667
4668 $out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
4669 $this->showTextbox( $this->textbox2, 'wpTextbox2', [ 'tabindex' => 6, 'readonly' ] );
4670
4671 $out->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
4672 }
4673
4674 private function addLongPageWarningHeader(): void {
4675 if ( $this->contentLength === false ) {
4676 $this->contentLength = strlen( $this->textbox1 );
4677 }
4678
4679 $out = $this->context->getOutput();
4680 $longPageHint = $this->context->msg( 'longpage-hint' );
4681 if ( !$longPageHint->isDisabled() ) {
4682 $msgText = trim( $longPageHint->sizeParams( $this->contentLength )
4683 ->params( $this->contentLength ) // Keep this unformatted for math inside message
4684 ->parse() );
4685 if ( $msgText !== '' && $msgText !== '-' ) {
4686 $out->addHTML( "<div id='mw-edit-longpage-hint'>\n$msgText\n</div>" );
4687 }
4688 }
4689 }
4690
4691 private function addExplainConflictHeader(): void {
4692 $this->context->getOutput()->addHTML(
4693 $this->getEditConflictHelper()->getExplainHeader()
4694 );
4695 }
4696
4703 private function guessSectionName( $text ): string {
4704 $parser = MediaWikiServices::getInstance()->getParser();
4705 $name = $parser->guessSectionNameFromWikiText( $text );
4706 // Per T216029, fragments in HTTP redirects need to be urlencoded,
4707 // otherwise Chrome double-escapes the rest of the URL.
4708 return '#' . urlencode( mb_substr( $name, 1 ) );
4709 }
4710
4715 public function setEditConflictHelperFactory( callable $factory ) {
4716 Assert::precondition( !$this->editConflictHelperFactory,
4717 'Can only have one extension that resolves edit conflicts' );
4718 $this->editConflictHelperFactory = $factory;
4719 }
4720
4721 private function getEditConflictHelper(): TextConflictHelper {
4722 if ( !$this->editConflictHelper ) {
4723 $label = $this->getSubmitButtonLabel();
4724 if ( $this->editConflictHelperFactory ) {
4725 $this->editConflictHelper = ( $this->editConflictHelperFactory )( $label );
4726 } else {
4727 $this->editConflictHelper = new TextConflictHelper(
4728 $this->getTitle(),
4729 $this->getContext()->getOutput(),
4730 MediaWikiServices::getInstance()->getStatsFactory(),
4731 $label,
4732 MediaWikiServices::getInstance()->getContentHandlerFactory()
4733 );
4734 }
4735 }
4736 return $this->editConflictHelper;
4737 }
4738}
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.
getId( $wikiId=self::LOCAL)
Get revision ID.
A RevisionRecord representing an existing revision persisted in the revision table.
Service for looking up page revisions.
Value object representing a content slot associated with a page revision.
This serves as the entry point to the MediaWiki session handling system.
The base class for all skins.
Definition Skin.php:52
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
Object for storing information about the effects of an edit.
Represents 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)