27 use Wikimedia\ScopedCallback;
414 # Placeholders for text injection by hooks (must be HTML)
415 # extensions should take care to _append_ to the present value
481 $this->contentModel = $this->mTitle->getContentModel();
484 $this->contentFormat =
$handler->getDefaultFormat();
485 $this->editConflictHelperFactory = [ $this,
'newTextConflictHelper' ];
517 $this->mContextTitle =
$title;
529 if ( is_null( $this->mContextTitle ) ) {
530 wfDeprecated( __METHOD__ .
' called with no title set',
'1.32' );
546 return $this->enableApiEditOverride ===
true ||
557 $this->enableApiEditOverride = $enableOverride;
581 if ( !
Hooks::run(
'AlternateEdit', [ $this ] ) ) {
585 wfDebug( __METHOD__ .
": enter\n" );
587 $request = $this->context->getRequest();
589 if (
$request->getBool(
'redlink' ) && $this->mTitle->exists() ) {
590 $this->context->getOutput()->redirect( $this->mTitle->getFullURL() );
595 $this->firsttime =
false;
600 $this->preview =
true;
604 $this->formtype =
'save';
605 } elseif ( $this->preview ) {
606 $this->formtype =
'preview';
607 } elseif ( $this->
diff ) {
608 $this->formtype =
'diff';
609 }
else { #
First time through
610 $this->firsttime =
true;
612 $this->formtype =
'preview';
614 $this->formtype =
'initial';
620 wfDebug( __METHOD__ .
": User can't edit\n" );
623 $this->context->getUser()->trackBlockWithCookie();
628 $this->context->getUser()->spreadAnyEditBlock();
636 $revision = $this->mArticle->getRevisionFetched();
643 if ( $this->undidRev ) {
645 $prevRev = $undidRevObj ? $undidRevObj->getPrevious() :
null;
647 if ( !$this->undidRev
654 'contentmodelediterror',
655 $revision->getContentModel(),
663 $this->isConflict =
false;
665 # Show applicable editing introductions
666 if ( $this->formtype ==
'initial' || $this->firsttime ) {
670 # Attempt submission here. This will check for edit conflicts,
671 # and redundantly check for locked database, blocked IPs, etc.
672 # that edit() already checked just in case someone tries to sneak
673 # in the back door with a hand-edited submission URL.
675 if (
'save' == $this->formtype ) {
676 $resultDetails =
null;
683 # First time through: get contents, set time for conflict
685 if (
'initial' == $this->formtype || $this->firsttime ) {
687 $out = $this->context->getOutput();
688 if (
$out->getRedirect() ===
'' ) {
694 if ( !$this->mTitle->getArticleID() ) {
695 Hooks::run(
'EditFormPreloadText', [ &$this->textbox1, &$this->mTitle ] );
697 Hooks::run(
'EditFormInitialText', [ $this ] );
710 $user = $this->context->getUser();
711 $permErrors = $this->mTitle->getUserPermissionsErrors(
'edit',
$user, $rigor );
712 # Can this title be created?
713 if ( !$this->mTitle->exists() ) {
714 $permErrors = array_merge(
717 $this->mTitle->getUserPermissionsErrors(
'create',
$user, $rigor ),
722 # Ignore some permissions errors when a user is just previewing/viewing diffs
724 foreach ( $permErrors
as $error ) {
725 if ( ( $this->preview || $this->
diff )
727 $error[0] ==
'blockedtext' ||
728 $error[0] ==
'autoblockedtext' ||
729 $error[0] ==
'systemblockedtext'
754 $out = $this->context->getOutput();
755 if ( $this->context->getRequest()->getBool(
'redlink' ) ) {
759 $out->redirect( $this->mTitle->getFullURL() );
765 # Use the normal message if there's nothing to display
767 $action = $this->mTitle->exists() ?
'edit' :
768 ( $this->mTitle->isTalkPage() ?
'createtalk' :
'createpage' );
774 $out->formatPermissionsErrorMessage( $permErrors,
'edit' )
784 $out = $this->context->getOutput();
785 Hooks::run(
'EditPage::showReadOnlyForm:initial', [ $this, &
$out ] );
787 $out->setRobotPolicy(
'noindex,nofollow' );
788 $out->setPageTitle( $this->context->msg(
790 $this->getContextTitle()->getPrefixedText()
793 $out->addHTML( $this->editFormPageTop );
794 $out->addHTML( $this->editFormTextTop );
796 if ( $errorMessage !==
'' ) {
797 $out->addWikiText( $errorMessage );
798 $out->addHTML(
"<hr />\n" );
801 # If the user made changes, preserve them when showing the markup
802 # (This happens when a user is blocked during edit, for instance)
803 if ( !$this->firsttime ) {
805 $out->addWikiMsg(
'viewyourtext' );
810 # Serialize using the default format if the content model is not supported
811 # (e.g. for an old revision with a different model)
814 $out->addWikiMsg(
'viewsourcetext' );
817 $out->addHTML( $this->editFormTextBeforeContent );
818 $this->
showTextbox( $text,
'wpTextbox1', [
'readonly' ] );
819 $out->addHTML( $this->editFormTextAfterContent );
823 $out->addModules(
'mediawiki.action.edit.collapsibleFooter' );
825 $out->addHTML( $this->editFormTextBottom );
826 if ( $this->mTitle->exists() ) {
827 $out->returnToMain(
null, $this->mTitle );
837 $config = $this->context->getConfig();
838 $previewOnOpenNamespaces = $config->get(
'PreviewOnOpenNamespaces' );
839 $request = $this->context->getRequest();
840 if ( $config->get(
'RawHtml' ) ) {
846 if (
$request->getVal(
'preview' ) ==
'yes' ) {
849 } elseif (
$request->getVal(
'preview' ) ==
'no' ) {
852 } elseif ( $this->section ==
'new' ) {
855 } elseif ( (
$request->getVal(
'preload' ) !==
null || $this->mTitle->exists() )
856 && $this->context->getUser()->getOption(
'previewonfirst' )
860 } elseif ( !$this->mTitle->exists()
861 && isset( $previewOnOpenNamespaces[$this->mTitle->getNamespace()] )
862 && $previewOnOpenNamespaces[$this->mTitle->getNamespace()]
878 if ( $this->mTitle->isUserConfigPage() ) {
879 $name = $this->mTitle->getSkinFromConfigSubpage();
880 $skins = array_merge(
884 return !in_array(
$name, $skins )
885 && in_array( strtolower(
$name ), $skins );
900 return $contentHandler->supportsSections();
909 # Section edit can come from either the form or a link
910 $this->section =
$request->getVal(
'wpSection',
$request->getVal(
'section' ) );
913 throw new ErrorPageError(
'sectioneditnotsupported-title',
'sectioneditnotsupported-text' );
916 $this->isNew = !$this->mTitle->exists() || $this->section ==
'new';
919 # These fields need to be checked for encoding.
920 # Also remove trailing whitespace, but don't remove _initial_
921 # whitespace from the text boxes. This may be significant formatting.
922 $this->textbox1 = rtrim(
$request->getText(
'wpTextbox1' ) );
923 if ( !
$request->getCheck(
'wpTextbox2' ) ) {
933 $this->unicodeCheck =
$request->getText(
'wpUnicodeCheck' );
935 $this->summary =
$request->getText(
'wpSummary' );
937 # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the
938 # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for
940 $this->summary = preg_replace(
'/^\s*=+\s*(.*?)\s*=+\s*$/',
'$1', $this->summary );
942 # Treat sectiontitle the same way as summary.
943 # Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is
944 # currently doing double duty as both edit summary and section title. Right now this
945 # is just to allow API edits to work around this limitation, but this should be
946 # incorporated into the actual edit form when EditPage is rewritten (T20654, T28312).
947 $this->sectiontitle =
$request->getText(
'wpSectionTitle' );
948 $this->sectiontitle = preg_replace(
'/^\s*=+\s*(.*?)\s*=+\s*$/',
'$1', $this->sectiontitle );
950 $this->edittime =
$request->getVal(
'wpEdittime' );
951 $this->editRevId =
$request->getIntOrNull(
'editRevId' );
952 $this->starttime =
$request->getVal(
'wpStarttime' );
959 $this->scrolltop =
$request->getIntOrNull(
'wpScrolltop' );
961 if ( $this->textbox1 ===
'' &&
$request->getVal(
'wpTextbox1' ) === null ) {
965 $this->incompleteForm =
true;
969 $this->incompleteForm = !
$request->getVal(
'wpUltimateParam' );
971 if ( $this->incompleteForm ) {
972 # If the form is incomplete, force to preview.
973 wfDebug( __METHOD__ .
": Form data appears to be incomplete\n" );
975 $this->preview =
true;
977 $this->preview =
$request->getCheck(
'wpPreview' );
985 # Some browsers will not report any submit button
986 # if the user hits enter in the comment box.
987 # The unmarked state will be assumed to be a save,
988 # if the form seems otherwise complete.
989 wfDebug( __METHOD__ .
": Passed token check.\n" );
990 } elseif ( $this->
diff ) {
991 # Failed token check, but only requested "Show Changes".
992 wfDebug( __METHOD__ .
": Failed token check; Show Changes requested.\n" );
994 # Page might be a hack attempt posted from
995 # an external site. Preview instead of saving.
996 wfDebug( __METHOD__ .
": Failed token check; forcing preview\n" );
997 $this->preview =
true;
1001 if ( !preg_match(
'/^\d{14}$/', $this->edittime ) ) {
1002 $this->edittime =
null;
1005 if ( !preg_match(
'/^\d{14}$/', $this->starttime ) ) {
1006 $this->starttime =
null;
1009 $this->recreate =
$request->getCheck(
'wpRecreate' );
1011 $this->minoredit =
$request->getCheck(
'wpMinoredit' );
1012 $this->watchthis =
$request->getCheck(
'wpWatchthis' );
1014 $user = $this->context->getUser();
1015 # Don't force edit summaries when a user is editing their own user or talk page
1016 if ( ( $this->mTitle->mNamespace ==
NS_USER || $this->mTitle->mNamespace ==
NS_USER_TALK )
1017 && $this->mTitle->getText() ==
$user->getName()
1019 $this->allowBlankSummary =
true;
1021 $this->allowBlankSummary =
$request->getBool(
'wpIgnoreBlankSummary' )
1022 || !
$user->getOption(
'forceeditsummary' );
1025 $this->autoSumm =
$request->getText(
'wpAutoSummary' );
1027 $this->allowBlankArticle =
$request->getBool(
'wpIgnoreBlankArticle' );
1028 $this->allowSelfRedirect =
$request->getBool(
'wpIgnoreSelfRedirect' );
1032 $this->changeTags = [];
1034 $this->changeTags = array_filter( array_map(
'trim', explode(
',',
1038 # Not a posted form? Start with nothing.
1039 wfDebug( __METHOD__ .
": Not a posted form.\n" );
1040 $this->textbox1 =
'';
1041 $this->summary =
'';
1042 $this->sectiontitle =
'';
1043 $this->edittime =
'';
1044 $this->editRevId =
null;
1046 $this->
edit =
false;
1047 $this->preview =
false;
1048 $this->
save =
false;
1049 $this->
diff =
false;
1050 $this->minoredit =
false;
1052 $this->watchthis =
$request->getBool(
'watchthis',
false );
1053 $this->recreate =
false;
1057 if ( $this->section ==
'new' &&
$request->getVal(
'preloadtitle' ) ) {
1058 $this->sectiontitle =
$request->getVal(
'preloadtitle' );
1060 $this->summary =
$request->getVal(
'preloadtitle' );
1061 } elseif ( $this->section !=
'new' &&
$request->getVal(
'summary' ) !==
'' ) {
1062 $this->summary =
$request->getText(
'summary' );
1063 if ( $this->summary !==
'' ) {
1064 $this->hasPresetSummary =
true;
1068 if (
$request->getVal(
'minor' ) ) {
1069 $this->minoredit =
true;
1073 $this->oldid =
$request->getInt(
'oldid' );
1074 $this->parentRevId =
$request->getInt(
'parentRevId' );
1076 $this->bot =
$request->getBool(
'bot',
true );
1077 $this->nosummary =
$request->getBool(
'nosummary' );
1080 $this->contentModel =
$request->getText(
'model', $this->contentModel );
1082 $this->contentFormat =
$request->getText(
'format', $this->contentFormat );
1088 'editpage-invalidcontentmodel-title',
1089 'editpage-invalidcontentmodel-text',
1094 if ( !
$handler->isSupportedFormat( $this->contentFormat ) ) {
1096 'editpage-notsupportedcontentformat-title',
1097 'editpage-notsupportedcontentformat-text',
1111 $this->editintro =
$request->getText(
'editintro',
1113 $this->section ===
'new' ?
'MediaWiki:addsection-editintro' :
'' );
1138 $this->edittime = $this->
page->getTimestamp();
1139 $this->editRevId = $this->
page->getLatest();
1147 $user = $this->context->getUser();
1149 # Sort out the "watch" checkbox
1150 if (
$user->getOption(
'watchdefault' ) ) {
1152 $this->watchthis =
true;
1153 } elseif (
$user->getOption(
'watchcreations' ) && !$this->mTitle->exists() ) {
1155 $this->watchthis =
true;
1156 } elseif (
$user->isWatched( $this->mTitle ) ) {
1158 $this->watchthis =
true;
1161 $this->minoredit =
true;
1163 if ( $this->textbox1 ===
false ) {
1179 $user = $this->context->getUser();
1180 $request = $this->context->getRequest();
1183 if ( !$this->mTitle->exists() || $this->section ==
'new' ) {
1184 if ( $this->mTitle->getNamespace() ==
NS_MEDIAWIKI && $this->section !=
'new' ) {
1185 # If this is a system message, get the default text.
1186 $msg = $this->mTitle->getDefaultMessageText();
1191 # If requested, preload some text.
1192 $preload =
$request->getVal(
'preload',
1194 $this->section ===
'new' ?
'MediaWiki:addsection-preload' :
'' );
1201 if ( $this->section !=
'' ) {
1204 $content = $orig ? $orig->getSection( $this->section ) :
null;
1210 $undoafter =
$request->getInt(
'undoafter' );
1211 $undo =
$request->getInt(
'undo' );
1213 if ( $undo > 0 && $undoafter > 0 ) {
1218 # Sanity check, make sure it's the right page,
1219 # the revisions exist and they were not deleted.
1220 # Otherwise, $content will be left as-is.
1221 if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
1229 $this->context->getOutput()->redirect( $this->mTitle->getFullURL( [
1230 'action' =>
'mcrundo',
1232 'undoafter' => $undoafter,
1236 $content = $this->
page->getUndoContent( $undorev, $oldrev );
1239 # Warn the user that something went wrong
1240 $undoMsg =
'failure';
1244 if ( $undoMsg ===
null ) {
1247 $user, MediaWikiServices::getInstance()->getContentLanguage() );
1248 $newContent =
$content->preSaveTransform( $this->mTitle,
$user, $popts );
1249 if ( $newContent->getModel() !== $oldContent->getModel() ) {
1254 $this->contentModel = $newContent->getModel();
1255 $this->contentFormat = $oldrev->getContentFormat();
1258 if ( $newContent->equals( $oldContent ) ) {
1259 # Tell the user that the undo results in no change,
1260 # i.e. the revisions were already undone.
1261 $undoMsg =
'nochange';
1264 # Inform the user of our success and set an automatic edit summary
1265 $undoMsg =
'success';
1267 # If we just undid one rev, use an autosummary
1268 $firstrev = $oldrev->getNext();
1269 if ( $firstrev && $firstrev->getId() == $undo ) {
1270 $userText = $undorev->getUserText();
1271 if ( $userText ===
'' ) {
1272 $undoSummary = $this->context->msg(
1273 'undo-summary-username-hidden',
1275 )->inContentLanguage()->text();
1277 $undoSummary = $this->context->msg(
1281 )->inContentLanguage()->text();
1283 if ( $this->summary ===
'' ) {
1284 $this->summary = $undoSummary;
1286 $this->summary = $undoSummary . $this->context->msg(
'colon-separator' )
1289 $this->undidRev = $undo;
1291 $this->formtype =
'diff';
1301 $out = $this->context->getOutput();
1304 $class = ( $undoMsg ==
'success' ?
'' :
'error ' ) .
"mw-undo-{$undoMsg}";
1305 $this->editFormPageTop .=
$out->parse(
"<div class=\"{$class}\">" .
1306 $this->context->msg(
'undo-' . $undoMsg )->plain() .
'</div>',
true,
true );
1313 $curRevision = $this->
page->getRevision();
1314 $oldRevision = $this->mArticle->getRevisionFetched();
1318 && $curRevision->getId() !== $oldRevision->getId()
1322 $this->context->getOutput()->redirect(
1323 $this->mTitle->getFullURL(
1325 'action' =>
'mcrrestore',
1326 'restore' => $oldRevision->getId(),
1360 if ( $this->section ==
'new' ) {
1363 $revision = $this->mArticle->getRevisionFetched();
1364 if ( $revision ===
null ) {
1366 return $handler->makeEmptyContent();
1385 if ( $this->parentRevId ) {
1388 return $this->mArticle->getRevIdFetched();
1406 return $handler->makeEmptyContent();
1407 } elseif ( !$this->undidRev ) {
1412 $logger = LoggerFactory::getInstance(
'editpage' );
1413 if ( $this->contentModel !==
$rev->getContentModel() ) {
1414 $logger->warning(
"Overriding content model from current edit {prev} to {new}", [
1415 'prev' => $this->contentModel,
1416 'new' =>
$rev->getContentModel(),
1417 'title' => $this->
getTitle()->getPrefixedDBkey(),
1418 'method' => __METHOD__
1420 $this->contentModel =
$rev->getContentModel();
1425 if ( !
$content->isSupportedFormat( $this->contentFormat ) ) {
1426 $logger->warning(
"Current revision content format unsupported. Overriding {prev} to {new}", [
1428 'prev' => $this->contentFormat,
1429 'new' =>
$rev->getContentFormat(),
1430 'title' => $this->
getTitle()->getPrefixedDBkey(),
1431 'method' => __METHOD__
1433 $this->contentFormat =
$rev->getContentFormat();
1462 if ( !empty( $this->mPreloadContent ) ) {
1468 if ( $preload ===
'' ) {
1469 return $handler->makeEmptyContent();
1472 $user = $this->context->getUser();
1474 # Check for existence to avoid getting MediaWiki:Noarticletext
1477 return $handler->makeEmptyContent();
1486 return $handler->makeEmptyContent();
1496 return $handler->makeEmptyContent();
1502 if ( !$converted ) {
1504 wfDebug(
"Attempt to preload incompatible content: " .
1505 "can't convert " .
$content->getModel() .
1508 return $handler->makeEmptyContent();
1525 $token =
$request->getVal(
'wpEditToken' );
1526 $user = $this->context->getUser();
1527 $this->mTokenOk =
$user->matchEditToken( $token );
1528 $this->mTokenOkExceptSuffix =
$user->matchEditTokenNoSuffix( $token );
1547 $revisionId = $this->
page->getLatest();
1548 $postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
1551 if ( $statusValue == self::AS_SUCCESS_NEW_ARTICLE ) {
1553 } elseif ( $this->oldid ) {
1557 $response = $this->context->getRequest()->response();
1558 $response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION );
1573 $bot = $this->context->getUser()->isAllowed(
'bot' ) &&
$this->bot;
1576 Hooks::run(
'EditPage::attemptSave:after', [ $this,
$status, $resultDetails ] );
1585 if ( $this->context->getRequest()->getText(
'mode' ) !==
'conflict' ) {
1606 if (
$status->value == self::AS_SUCCESS_UPDATE
1607 ||
$status->value == self::AS_SUCCESS_NEW_ARTICLE
1611 $this->didSave =
true;
1612 if ( !$resultDetails[
'nullEdit'] ) {
1617 $out = $this->context->getOutput();
1621 $request = $this->context->getRequest();
1622 $extraQueryRedirect =
$request->getVal(
'wpExtraQueryRedirect' );
1643 $out->addWikiText(
'<div class="error">' .
"\n" .
$status->getWikiText() .
'</div>' );
1647 $query = $resultDetails[
'redirect'] ?
'redirect=no' :
'';
1648 if ( $extraQueryRedirect ) {
1650 $query = $extraQueryRedirect;
1655 $anchor = $resultDetails[
'sectionanchor'] ??
'';
1656 $out->redirect( $this->mTitle->getFullURL(
$query ) . $anchor );
1661 $sectionanchor = $resultDetails[
'sectionanchor'];
1665 'ArticleUpdateBeforeRedirect',
1666 [ $this->mArticle, &$sectionanchor, &$extraQuery ]
1669 if ( $resultDetails[
'redirect'] ) {
1670 if ( $extraQuery ==
'' ) {
1671 $extraQuery =
'redirect=no';
1673 $extraQuery =
'redirect=no&' . $extraQuery;
1676 if ( $extraQueryRedirect ) {
1677 if ( $extraQuery ===
'' ) {
1678 $extraQuery = $extraQueryRedirect;
1680 $extraQuery = $extraQuery .
'&' . $extraQueryRedirect;
1684 $out->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
1709 $permission = $this->mTitle->isTalkPage() ?
'createtalk' :
'createpage';
1720 $this->hookError =
'<div class="error">' .
"\n" .
$status->getWikiText() .
1737 if ( $this->hookError !=
'' ) {
1738 # ...or the hook could be expecting us to produce an error
1739 $status->fatal(
'hookaborted' );
1747 $user, $this->minoredit ] )
1749 # Error messages etc. could be handled within the hook...
1751 $status->fatal(
'hookaborted' );
1764 } elseif ( !
$status->isOK() ) {
1765 # ...or the hook could be expecting us to produce an error
1768 $status->fatal(
'hookaborted' );
1783 $errmsg =
$status->getWikiText(
1786 $this->context->getLanguage()
1789 <
div class=
"errorbox">
1805 if ( $this->sectiontitle !==
'' ) {
1810 if ( $this->summary ===
'' ) {
1811 $cleanSectionTitle =
$wgParser->stripSectionName( $this->sectiontitle );
1812 return $this->context->msg(
'newsectionsummary' )
1813 ->plaintextParams( $cleanSectionTitle )->inContentLanguage()->text();
1815 } elseif ( $this->summary !==
'' ) {
1817 # This is a new section, so create a link to the new section
1818 # in the revision summary.
1819 $cleanSummary =
$wgParser->stripSectionName( $this->summary );
1820 return $this->context->msg(
'newsectionsummary' )
1821 ->plaintextParams( $cleanSummary )->inContentLanguage()->text();
1852 $user = $this->context->getUser();
1854 if ( !
Hooks::run(
'EditPage::attemptSave', [ $this ] ) ) {
1855 wfDebug(
"Hook 'EditPage::attemptSave' aborted article saving\n" );
1856 $status->fatal(
'hookaborted' );
1861 if ( $this->unicodeCheck !== self::UNICODE_CHECK ) {
1862 $status->fatal(
'unicode-support-fail' );
1867 $request = $this->context->getRequest();
1868 $spam =
$request->getText(
'wpAntispam' );
1869 if ( $spam !==
'' ) {
1874 $this->mTitle->getPrefixedText() .
1875 '" submitted bogus field "' .
1879 $status->fatal(
'spamprotectionmatch',
false );
1885 # Construct Content object
1889 'content-failed-to-parse',
1890 $this->contentModel,
1891 $this->contentFormat,
1898 # Check image redirect
1899 if ( $this->mTitle->getNamespace() ==
NS_FILE &&
1900 $textbox_content->isRedirect() &&
1901 !
$user->isAllowed(
'upload' )
1911 if ( $match ===
false && $this->section ==
'new' ) {
1912 # $wgSpamRegex is enforced on this new heading/summary because, unlike
1913 # regular summaries, it is added to the actual wikitext.
1914 if ( $this->sectiontitle !==
'' ) {
1915 # This branch is taken when the API is used with the 'sectiontitle' parameter.
1918 # This branch is taken when the "Add Topic" user interface is used, or the API
1919 # is used with the 'summary' parameter.
1923 if ( $match ===
false ) {
1926 if ( $match !==
false ) {
1929 $pdbk = $this->mTitle->getPrefixedDBkey();
1930 $match = str_replace(
"\n",
'', $match );
1931 wfDebugLog(
'SpamRegex',
"$ip spam regex hit [[$pdbk]]: \"$match\"" );
1932 $status->fatal(
'spamprotectionmatch', $match );
1938 [ $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ] )
1940 # Error messages etc. could be handled within the hook...
1941 $status->fatal(
'hookaborted' );
1944 } elseif ( $this->hookError !=
'' ) {
1945 # ...or the hook could be expecting us to produce an error
1946 $status->fatal(
'hookaborted' );
1951 if (
$user->isBlockedFrom( $this->mTitle,
false ) ) {
1954 $user->spreadAnyEditBlock();
1956 # Check block state against master, thus 'false'.
1957 $status->setResult(
false, self::AS_BLOCKED_PAGE_FOR_USER );
1961 $this->contentLength = strlen( $this->textbox1 );
1962 $config = $this->context->getConfig();
1963 $maxArticleSize = $config->get(
'MaxArticleSize' );
1964 if ( $this->contentLength > $maxArticleSize * 1024 ) {
1966 $this->tooBig =
true;
1967 $status->setResult(
false, self::AS_CONTENT_TOO_BIG );
1971 if ( !
$user->isAllowed(
'edit' ) ) {
1972 if (
$user->isAnon() ) {
1973 $status->setResult(
false, self::AS_READ_ONLY_PAGE_ANON );
1976 $status->fatal(
'readonlytext' );
1982 $changingContentModel =
false;
1983 if ( $this->contentModel !== $this->mTitle->getContentModel() ) {
1984 if ( !$config->get(
'ContentHandlerUseDB' ) ) {
1985 $status->fatal(
'editpage-cannot-use-custom-model' );
1988 } elseif ( !
$user->isAllowed(
'editcontentmodel' ) ) {
1989 $status->setResult(
false, self::AS_NO_CHANGE_CONTENT_MODEL );
1995 if ( !$titleWithNewContentModel->userCan(
'editcontentmodel',
$user )
1996 || !$titleWithNewContentModel->userCan(
'edit',
$user )
1998 $status->setResult(
false, self::AS_NO_CHANGE_CONTENT_MODEL );
2002 $changingContentModel =
true;
2003 $oldContentModel = $this->mTitle->getContentModel();
2006 if ( $this->changeTags ) {
2008 $this->changeTags,
$user );
2009 if ( !$changeTagsStatus->isOK() ) {
2011 return $changeTagsStatus;
2016 $status->fatal(
'readonlytext' );
2020 if (
$user->pingLimiter() ||
$user->pingLimiter(
'linkpurge', 0 )
2021 || ( $changingContentModel &&
$user->pingLimiter(
'editcontentmodel' ) )
2023 $status->fatal(
'actionthrottledtext' );
2028 # If the article has been deleted while editing, don't save it without
2031 $status->setResult(
false, self::AS_ARTICLE_WAS_DELETED );
2035 # Load the page data from the master. If anything changes in the meantime,
2036 # we detect it by using page_latest like a token in a 1 try compare-and-swap.
2037 $this->
page->loadPageData(
'fromdbmaster' );
2038 $new = !$this->
page->exists();
2042 if ( !$this->mTitle->userCan(
'create',
$user ) ) {
2043 $status->fatal(
'nocreatetext' );
2045 wfDebug( __METHOD__ .
": no create permission\n" );
2052 $defaultMessageText = $this->mTitle->getDefaultMessageText();
2053 if ( $this->mTitle->getNamespace() ===
NS_MEDIAWIKI && $defaultMessageText !==
false ) {
2054 $defaultText = $defaultMessageText;
2059 if ( !$this->allowBlankArticle && $this->textbox1 === $defaultText ) {
2060 $this->blankArticle =
true;
2061 $status->fatal(
'blankarticle' );
2062 $status->setResult(
false, self::AS_BLANK_ARTICLE );
2072 $result[
'sectionanchor'] =
'';
2073 if ( $this->section ==
'new' ) {
2074 if ( $this->sectiontitle !==
'' ) {
2077 } elseif ( $this->summary !==
'' ) {
2088 # Article exists. Check for edit conflict.
2090 $this->
page->clear(); # Force reload
of dates,
etc.
2091 $timestamp = $this->
page->getTimestamp();
2092 $latest = $this->
page->getLatest();
2094 wfDebug(
"timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
2100 if ( $timestamp != $this->edittime
2101 || ( $this->editRevId !==
null && $this->editRevId != $latest )
2103 $this->isConflict =
true;
2104 if ( $this->section ==
'new' ) {
2105 if ( $this->
page->getUserText() ==
$user->getName() &&
2112 .
": duplicate new section submission; trigger edit conflict!\n" );
2115 $this->isConflict =
false;
2116 wfDebug( __METHOD__ .
": conflict suppressed; new section\n" );
2118 } elseif ( $this->section ==
''
2120 DB_MASTER, $this->mTitle->getArticleID(),
2124 # Suppress edit conflict with self, except for section edits where merging is required.
2125 wfDebug( __METHOD__ .
": Suppressing edit conflict, same user.\n" );
2126 $this->isConflict =
false;
2131 if ( $this->sectiontitle !==
'' ) {
2139 if ( $this->isConflict ) {
2141 .
": conflict! getting section '{$this->section}' for time '{$this->edittime}'"
2142 .
" (id '{$this->editRevId}') (article time '{$timestamp}')\n" );
2145 if ( $this->editRevId !==
null ) {
2161 wfDebug( __METHOD__ .
": getting section '{$this->section}'\n" );
2170 wfDebug( __METHOD__ .
": activating conflict; section replace failed.\n" );
2171 $this->isConflict =
true;
2173 } elseif ( $this->isConflict ) {
2177 $this->isConflict =
false;
2178 wfDebug( __METHOD__ .
": Suppressing edit conflict, successful merge.\n" );
2180 $this->section =
'';
2182 wfDebug( __METHOD__ .
": Keeping edit conflict, failed merge.\n" );
2186 if ( $this->isConflict ) {
2187 $status->setResult(
false, self::AS_CONFLICT_DETECTED );
2195 if ( $this->section ==
'new' ) {
2197 if ( !$this->allowBlankSummary && trim( $this->summary ) ==
'' ) {
2198 $this->missingSummary =
true;
2199 $status->fatal(
'missingsummary' );
2205 if ( $this->textbox1 ==
'' ) {
2206 $this->missingComment =
true;
2207 $status->fatal(
'missingcommenttext' );
2211 } elseif ( !$this->allowBlankSummary
2216 $this->missingSummary =
true;
2217 $status->fatal(
'missingsummary' );
2223 $sectionanchor =
'';
2224 if ( $this->section ==
'new' ) {
2226 } elseif ( $this->section !=
'' ) {
2227 # Try to get a section anchor from the section source, redirect
2228 # to edited section if header found.
2229 # XXX: Might be better to integrate this into Article::replaceSectionAtRev
2230 # for duplicate heading checking and maybe parsing.
2231 $hasmatch = preg_match(
"/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1,
$matches );
2232 # We can't deal with anchors, includes, html etc in the header for now,
2233 # headline would need to be parsed to improve this.
2234 if ( $hasmatch && strlen(
$matches[2] ) > 0 ) {
2238 $result[
'sectionanchor'] = $sectionanchor;
2245 $this->section =
'';
2250 if ( !$this->allowSelfRedirect
2256 if ( !$currentTarget || !$currentTarget->equals( $this->getTitle() ) ) {
2257 $this->selfRedirect =
true;
2258 $status->fatal(
'selfredirect' );
2266 if ( $this->contentLength > $maxArticleSize * 1024 ) {
2267 $this->tooBig =
true;
2268 $status->setResult(
false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
2274 ( ( $this->minoredit && !$this->isNew ) ?
EDIT_MINOR : 0 ) |
2277 $doEditStatus = $this->
page->doEditContent(
2288 if ( !$doEditStatus->isOK() ) {
2292 $errors = $doEditStatus->getErrorsArray();
2293 if ( in_array( $errors[0][0],
2294 [
'edit-gone-missing',
'edit-conflict',
'edit-already-exists' ] )
2296 $this->isConflict =
true;
2300 return $doEditStatus;
2303 $result[
'nullEdit'] = $doEditStatus->hasMessage(
'edit-no-change' );
2306 $user->pingLimiter(
'linkpurge' );
2313 if ( $changingContentModel ) {
2316 $new ?
false : $oldContentModel,
2317 $this->contentModel,
2332 $new = $oldModel ===
false;
2333 $log =
new ManualLogEntry(
'contentmodel', $new ?
'new' :
'change' );
2334 $log->setPerformer(
$user );
2335 $log->setTarget( $this->mTitle );
2336 $log->setComment( $reason );
2337 $log->setParameters( [
2338 '4::oldmodel' => $oldModel,
2339 '5::newmodel' => $newModel
2341 $logid = $log->insert();
2342 $log->publish( $logid );
2349 $user = $this->context->getUser();
2350 if ( !
$user->isLoggedIn() ) {
2382 $baseContent = $baseRevision ? $baseRevision->getContent() :
null;
2384 if ( is_null( $baseContent ) ) {
2390 $currentContent = $currentRevision ? $currentRevision->getContent() :
null;
2392 if ( is_null( $currentContent ) ) {
2398 $result =
$handler->merge3( $baseContent, $editContent, $currentContent );
2403 $this->parentRevId = $currentRevision->getId();
2423 if ( !$this->mBaseRevision ) {
2425 $this->mBaseRevision = $this->editRevId
2465 foreach ( $regexes
as $regex ) {
2467 if ( preg_match( $regex, $text,
$matches ) ) {
2475 $out = $this->context->getOutput();
2477 $out->addModules(
'mediawiki.action.edit' );
2478 $out->addModuleStyles(
'mediawiki.action.edit.styles' );
2479 $out->addModuleStyles(
'mediawiki.editfont.styles' );
2481 $user = $this->context->getUser();
2483 if (
$user->getOption(
'uselivepreview' ) ) {
2484 $out->addModules(
'mediawiki.action.edit.preview' );
2487 if (
$user->getOption(
'useeditwarning' ) ) {
2488 $out->addModules(
'mediawiki.action.edit.editWarning' );
2491 # Enabled article-related sidebar, toplinks, etc.
2492 $out->setArticleRelated(
true );
2495 if ( $this->isConflict ) {
2496 $msg =
'editconflict';
2497 } elseif ( $contextTitle->exists() && $this->section !=
'' ) {
2498 $msg = $this->section ==
'new' ?
'editingcomment' :
'editingsection';
2500 $msg = $contextTitle->exists()
2502 && $contextTitle->getDefaultMessageText() !==
false
2508 # Use the title defined by DISPLAYTITLE magic word when present
2509 # NOTE: getDisplayTitle() returns HTML while getPrefixedText() returns plain text.
2510 # setPageTitle() treats the input as wikitext, which should be safe in either case.
2511 $displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() :
false;
2512 if ( $displayTitle ===
false ) {
2513 $displayTitle = $contextTitle->getPrefixedText();
2515 $out->setDisplayTitle( $displayTitle );
2517 $out->setPageTitle( $this->context->msg( $msg, $displayTitle ) );
2519 $config = $this->context->getConfig();
2521 # Transmit the name of the message to JavaScript for live preview
2522 # Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys
2523 $out->addJsConfigVars( [
2524 'wgEditMessage' => $msg,
2525 'wgAjaxEditStash' => $config->get(
'AjaxEditStash' ),
2530 $out->addJsConfigVars(
2531 'wgEditSubmitButtonLabelPublish',
2532 $config->get(
'EditSubmitButtonLabelPublish' )
2540 if ( $this->suppressIntro ) {
2544 $out = $this->context->getOutput();
2545 $namespace = $this->mTitle->getNamespace();
2548 # Show a warning if editing an interface message
2549 $out->wrapWikiMsg(
"<div class='mw-editinginterface'>\n$1\n</div>",
'editinginterface' );
2550 # If this is a default message (but not css, json, or js),
2551 # show a hint that it is translatable on translatewiki.net
2557 $defaultMessageText = $this->mTitle->getDefaultMessageText();
2558 if ( $defaultMessageText !==
false ) {
2559 $out->wrapWikiMsg(
"<div class='mw-translateinterface'>\n$1\n</div>",
2560 'translateinterface' );
2563 } elseif ( $namespace ==
NS_FILE ) {
2564 # Show a hint to shared repo
2566 if ( $file && !$file->isLocal() ) {
2567 $descUrl = $file->getDescriptionUrl();
2568 # there must be a description url to show a hint to shared repo
2570 if ( !$this->mTitle->exists() ) {
2571 $out->wrapWikiMsg(
"<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
2572 'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
2575 $out->wrapWikiMsg(
"<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
2576 'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl
2583 # Show a warning message when someone creates/edits a user (talk) page but the user does not exist
2584 # Show log extract when the user is currently blocked
2586 $username = explode(
'/', $this->mTitle->getText(), 2 )[0];
2591 $out->wrapWikiMsg(
"<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
2594 # Show log extract if the user is currently blocked
2602 'showIfEmpty' =>
false,
2604 'blocked-notice-logextract',
2605 $user->getName() # Support GENDER
in notice
2611 # Try to add a custom edit intro, or use the standard one if this is not possible.
2614 $this->context->msg(
'helppage' )->inContentLanguage()->text()
2616 if ( $this->context->getUser()->isLoggedIn() ) {
2619 "<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>",
2628 "<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>",
2630 'newarticletextanon',
2636 # Give a notice if the user is editing a deleted/moved page...
2637 if ( !$this->mTitle->exists() ) {
2644 'conds' => [
'log_action != ' .
$dbr->addQuotes(
'revision' ) ],
2645 'showIfEmpty' =>
false,
2646 'msgKey' => [
'recreate-moveddeleted-warn' ]
2658 if ( $this->editintro ) {
2662 $this->context->getOutput()->addWikiTextAsContent(
2663 '<div class="mw-editintro">{{:' .
$title->getFullText() .
'}}</div>',
2700 return $content->serialize( $this->contentFormat );
2720 if ( $text ===
false || $text ===
null ) {
2725 $this->contentModel, $this->contentFormat );
2743 # need to parse the preview early so that we know which templates are used,
2744 # otherwise users with "show preview after edit box" will get a blank list
2745 # we parse this near the beginning so that setHeaders can do the title
2746 # setting work instead of leaving it in getPreviewText
2747 $previewOutput =
'';
2748 if ( $this->formtype ==
'preview' ) {
2752 $out = $this->context->getOutput();
2756 Hooks::run(
'EditPage::showEditForm:initial', [ &$editPage, &
$out ] );
2763 if ( !$this->isConflict &&
2764 $this->section !=
'' &&
2769 $out->showErrorPage(
'sectioneditnotsupported-title',
'sectioneditnotsupported-text' );
2775 $out->addHTML( $this->editFormPageTop );
2777 $user = $this->context->getUser();
2778 if (
$user->getOption(
'previewontop' ) ) {
2782 $out->addHTML( $this->editFormTextTop );
2785 if ( $this->formtype !==
'save' ) {
2786 $out->wrapWikiMsg(
"<div class='error mw-deleted-while-editing'>\n$1\n</div>",
2787 'deletedwhileediting' );
2796 'class' =>
'mw-editform',
2797 'id' => self::EDITFORM_ID,
2798 'name' => self::EDITFORM_ID,
2801 'enctype' =>
'multipart/form-data'
2805 if ( is_callable( $formCallback ) ) {
2806 wfWarn(
'The $formCallback parameter to ' . __METHOD__ .
'is deprecated' );
2807 call_user_func_array( $formCallback, [ &
$out ] );
2815 Xml::openElement(
'div', [
'id' =>
'antispam-container',
'style' =>
'display: none;' ] )
2818 [
'for' =>
'wpAntispam' ],
2819 $this->context->msg(
'simpleantispam-label' )->parse()
2825 'name' =>
'wpAntispam',
2826 'id' =>
'wpAntispam',
2835 Hooks::run(
'EditPage::showEditForm:fields', [ &$editPage, &
$out ] );
2841 $username = $this->lastDelete->user_name;
2843 ->getComment(
'log_comment', $this->lastDelete )->text;
2847 $key = $comment ===
''
2848 ?
'confirmrecreate-noreason'
2849 :
'confirmrecreate';
2851 '<div class="mw-confirm-recreate">' .
2852 $this->context->msg( $key,
$username,
"<nowiki>$comment</nowiki>" )->parse() .
2853 Xml::checkLabel( $this->context->msg(
'recreate' )->text(),
'wpRecreate',
'wpRecreate',
false,
2860 # When the summary is hidden, also hide them on preview/show changes
2861 if ( $this->nosummary ) {
2865 # If a blank edit summary was previously provided, and the appropriate
2866 # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
2867 # user being bounced back more than once in the event that a summary
2870 # For a bit more sophisticated detection of blank summaries, hash the
2871 # automatic one and pass that in the hidden field wpAutoSummary.
2872 if ( $this->missingSummary || ( $this->section ==
'new' && $this->nosummary ) ) {
2876 if ( $this->undidRev ) {
2880 if ( $this->selfRedirect ) {
2884 if ( $this->hasPresetSummary ) {
2888 $this->autoSumm = md5(
'' );
2891 $autosumm = $this->autoSumm !==
'' ? $this->autoSumm : md5( $this->summary );
2902 if ( $this->section ==
'new' ) {
2907 $out->addHTML( $this->editFormTextBeforeContent );
2908 if ( $this->isConflict ) {
2923 if ( !$this->mTitle->isUserConfigPage() ) {
2924 $out->addHTML( self::getEditToolbar( $this->mTitle ) );
2927 if ( $this->blankArticle ) {
2931 if ( $this->isConflict ) {
2936 $conflictTextBoxAttribs = [];
2938 $conflictTextBoxAttribs[
'style'] =
'display:none;';
2939 } elseif ( $this->isOldRev ) {
2940 $conflictTextBoxAttribs[
'class'] =
'mw-textarea-oldrev';
2949 $out->addHTML( $this->editFormTextAfterContent );
2959 $out->addHTML( $this->editFormTextAfterTools .
"\n" );
2967 self::getPreviewLimitReport( $this->mParserOutput ) ) );
2969 $out->addModules(
'mediawiki.action.edit.collapsibleFooter' );
2971 if ( $this->isConflict ) {
2976 $msg = $this->context->msg(
2977 'content-failed-to-parse',
2978 $this->contentModel,
2979 $this->contentFormat,
2982 $out->addWikiText(
'<div class="error">' . $msg->plain() .
'</div>' );
2987 if ( $this->isConflict ) {
2989 } elseif ( $this->preview ) {
2991 } elseif ( $this->
diff ) {
2996 $out->addHTML(
Html::hidden(
'mode', $mode, [
'id' =>
'mw-edit-mode' ] ) );
3001 $out->addHTML( $this->editFormTextBottom .
"\n</form>\n" );
3003 if ( !
$user->getOption(
'previewontop' ) ) {
3017 $this->context, MediaWikiServices::getInstance()->getLinkRenderer()
3022 if ( $this->preview ) {
3024 } elseif ( $this->section !=
'' ) {
3029 $templateListFormatter->format( $templates,
$type )
3040 preg_match(
"/^(=+)(.+)\\1\\s*(\n|$)/i", $text,
$matches );
3050 $out = $this->context->getOutput();
3051 $user = $this->context->getUser();
3052 if ( $this->isConflict ) {
3054 $this->editRevId = $this->
page->getLatest();
3056 if ( $this->section !=
'' && $this->section !=
'new' ) {
3057 if ( !$this->summary && !$this->preview && !$this->
diff ) {
3059 if ( $sectionTitle !==
false ) {
3060 $this->summary =
"/* $sectionTitle */ ";
3067 if ( $this->missingComment ) {
3068 $out->wrapWikiMsg(
"<div id='mw-missingcommenttext'>\n$1\n</div>",
'missingcommenttext' );
3071 if ( $this->missingSummary && $this->section !=
'new' ) {
3073 "<div id='mw-missingsummary'>\n$1\n</div>",
3074 [
'missingsummary', $buttonLabel ]
3078 if ( $this->missingSummary && $this->section ==
'new' ) {
3080 "<div id='mw-missingcommentheader'>\n$1\n</div>",
3081 [
'missingcommentheader', $buttonLabel ]
3085 if ( $this->blankArticle ) {
3087 "<div id='mw-blankarticle'>\n$1\n</div>",
3088 [
'blankarticle', $buttonLabel ]
3092 if ( $this->selfRedirect ) {
3094 "<div id='mw-selfredirect'>\n$1\n</div>",
3095 [
'selfredirect', $buttonLabel ]
3099 if ( $this->hookError !==
'' ) {
3100 $out->addWikiText( $this->hookError );
3103 if ( $this->section !=
'new' ) {
3104 $revision = $this->mArticle->getRevisionFetched();
3110 "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
3111 'rev-deleted-text-permission'
3115 "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
3116 'rev-deleted-text-view'
3120 if ( !$revision->isCurrent() ) {
3121 $this->mArticle->setOldSubtitle( $revision->getId() );
3122 $out->addWikiMsg(
'editingold' );
3123 $this->isOldRev =
true;
3125 } elseif ( $this->mTitle->exists() ) {
3128 $out->wrapWikiMsg(
"<div class='errorbox'>\n$1\n</div>\n",
3129 [
'missing-revision', $this->oldid ] );
3136 "<div id=\"mw-read-only-warning\">\n$1\n</div>",
3139 } elseif (
$user->isAnon() ) {
3140 if ( $this->formtype !=
'preview' ) {
3142 "<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
3143 [
'anoneditwarning',
3146 'returnto' => $this->
getTitle()->getPrefixedDBkey()
3150 'returnto' => $this->
getTitle()->getPrefixedDBkey()
3155 $out->wrapWikiMsg(
"<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
3156 'anonpreviewwarning'
3160 if ( $this->mTitle->isUserConfigPage() ) {
3161 # Check the skin exists
3164 "<div class='error' id='mw-userinvalidconfigtitle'>\n$1\n</div>",
3165 [
'userinvalidconfigtitle', $this->mTitle->getSkinFromConfigSubpage() ]
3168 if ( $this->
getTitle()->isSubpageOf(
$user->getUserPage() ) ) {
3169 $isUserCssConfig = $this->mTitle->isUserCssConfigPage();
3170 $isUserJsonConfig = $this->mTitle->isUserJsonConfigPage();
3171 $isUserJsConfig = $this->mTitle->isUserJsConfigPage();
3173 $warning = $isUserCssConfig
3175 : ( $isUserJsonConfig ?
'userjsonispublic' :
'userjsispublic' );
3177 $out->wrapWikiMsg(
'<div class="mw-userconfigpublic">$1</div>', $warning );
3179 if ( $this->formtype !==
'preview' ) {
3180 $config = $this->context->getConfig();
3181 if ( $isUserCssConfig && $config->get(
'AllowUserCss' ) ) {
3183 "<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
3184 [
'usercssyoucanpreview' ]
3186 } elseif ( $isUserJsonConfig ) {
3188 "<div id='mw-userjsonyoucanpreview'>\n$1\n</div>",
3189 [
'userjsonyoucanpreview' ]
3191 } elseif ( $isUserJsConfig && $config->get(
'AllowUserJs' ) ) {
3193 "<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
3194 [
'userjsyoucanpreview' ]
3206 # Add header copyright warning
3218 $conf = $this->context->getConfig();
3219 $oldCommentSchema = $conf->get(
'CommentTableSchemaMigrationStage' ) ===
MIGRATION_OLD;
3223 return ( is_array( $inputAttrs ) ? $inputAttrs : [] ) + [
3224 'id' =>
'wpSummary',
3225 'name' =>
'wpSummary',
3229 'spellcheck' =>
'true',
3243 $inputAttrs = OOUI\Element::configFromHtmlAttributes(
3252 $inputAttrs[
'inputId'] = $inputAttrs[
'id'];
3253 $inputAttrs[
'id'] =
'wpSummaryWidget';
3255 return new OOUI\FieldLayout(
3256 new OOUI\TextInputWidget( [
3258 'infusable' =>
true,
3261 'label' =>
new OOUI\HtmlSnippet( $labelText ),
3263 'id' =>
'wpSummaryLabel',
3264 'classes' => [ $this->missingSummary ?
'mw-summarymissed' :
'mw-summary' ],
3276 # Add a class if 'missingsummary' is triggered to allow styling of the summary line
3277 $summaryClass = $this->missingSummary ?
'mw-summarymissed' :
'mw-summary';
3278 if ( $isSubjectPreview ) {
3279 if ( $this->nosummary ) {
3283 if ( !$this->mShowSummaryField ) {
3288 $labelText = $this->context->msg( $isSubjectPreview ?
'subject' :
'summary' )->parse();
3292 [
'class' => $summaryClass ]
3306 if ( !
$summary || ( !$this->preview && !$this->
diff ) ) {
3312 if ( $isSubjectPreview ) {
3313 $summary = $this->context->msg(
'newsectionsummary' )
3315 ->inContentLanguage()->text();
3318 $message = $isSubjectPreview ?
'subject-preview' :
'summary-preview';
3320 $summary = $this->context->msg( $message )->parse()
3326 $out = $this->context->getOutput();
3331 $out->addHTML(
Html::hidden(
'wpScrolltop', $this->scrolltop, [
'id' =>
'wpScrolltop' ] ) );
3347 $this->context->getOutput()->addHTML(
3349 Html::hidden(
"wpEditToken", $this->context->getUser()->getEditToken() ) .
3376 $attribs = [
'style' =>
'display:none;' ];
3379 $classes = $builder->getTextboxProtectionCSSClasses( $this->
getTitle() );
3381 # Is an old revision being edited?
3382 if ( $this->isOldRev ) {
3383 $classes[] =
'mw-textarea-oldrev';
3396 $textoverride ?? $this->textbox1,
3403 $this->
showTextbox( $this->textbox2,
'wpTextbox2', [
'tabindex' => 6,
'readonly' ] );
3408 $attribs = $builder->buildTextboxAttribs(
3411 $this->context->getUser(),
3415 $this->context->getOutput()->addHTML(
3423 $classes[] =
'ontop';
3426 $attribs = [
'id' =>
'wikiPreview',
'class' => implode(
' ', $classes ) ];
3428 if ( $this->formtype !=
'preview' ) {
3429 $attribs[
'style'] =
'display: none;';
3432 $out = $this->context->getOutput();
3435 if ( $this->formtype ==
'preview' ) {
3439 $pageViewLang = $this->mTitle->getPageViewLanguage();
3440 $attribs = [
'lang' => $pageViewLang->getHtmlCode(),
'dir' => $pageViewLang->getDir(),
3441 'class' =>
'mw-content-' . $pageViewLang->getDir() ];
3445 $out->addHTML(
'</div>' );
3447 if ( $this->formtype ==
'diff' ) {
3451 $msg = $this->context->msg(
3452 'content-failed-to-parse',
3453 $this->contentModel,
3454 $this->contentFormat,
3457 $out->addWikiText(
'<div class="error">' . $msg->plain() .
'</div>' );
3470 $this->mArticle->openShowCategory();
3472 # This hook seems slightly odd here, but makes things more
3473 # consistent for extensions.
3474 $out = $this->context->getOutput();
3476 $out->addHTML( $text );
3478 $this->mArticle->closeShowCategory();
3490 $oldtitlemsg =
'currentrev';
3491 # if message does not exist, show diff against the preloaded default
3492 if ( $this->mTitle->getNamespace() ==
NS_MEDIAWIKI && !$this->mTitle->exists() ) {
3493 $oldtext = $this->mTitle->getDefaultMessageText();
3494 if ( $oldtext !==
false ) {
3495 $oldtitlemsg =
'defaultmessagetext';
3505 if ( $this->editRevId !==
null ) {
3506 $newContent = $this->
page->replaceSectionAtRev(
3507 $this->section, $textboxContent, $this->summary, $this->editRevId
3510 $newContent = $this->
page->replaceSectionContent(
3511 $this->section, $textboxContent, $this->summary, $this->edittime
3515 if ( $newContent ) {
3516 Hooks::run(
'EditPageGetDiffContent', [ $this, &$newContent ] );
3518 $user = $this->context->getUser();
3520 MediaWikiServices::getInstance()->getContentLanguage() );
3521 $newContent = $newContent->preSaveTransform( $this->mTitle,
$user, $popts );
3524 if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
3525 $oldtitle = $this->context->msg( $oldtitlemsg )->parse();
3526 $newtitle = $this->context->msg(
'yourtext' )->parse();
3528 if ( !$oldContent ) {
3529 $oldContent = $newContent->getContentHandler()->makeEmptyContent();
3532 if ( !$newContent ) {
3533 $newContent = $oldContent->getContentHandler()->makeEmptyContent();
3536 $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->context );
3537 $de->setContent( $oldContent, $newContent );
3539 $difftext = $de->getDiff( $oldtitle, $newtitle );
3540 $de->showDiffStyle();
3545 $this->context->getOutput()->addHTML(
'<div id="wikiDiff">' . $difftext .
'</div>' );
3552 $msg =
'editpage-head-copy-warn';
3553 if ( !$this->context->msg( $msg )->isDisabled() ) {
3554 $this->context->getOutput()->wrapWikiMsg(
"<div class='editpage-head-copywarn'>\n$1\n</div>",
3555 'editpage-head-copy-warn' );
3568 $msg =
'editpage-tos-summary';
3569 Hooks::run(
'EditPageTosSummary', [ $this->mTitle, &$msg ] );
3570 if ( !$this->context->msg( $msg )->isDisabled() ) {
3571 $out = $this->context->getOutput();
3572 $out->addHTML(
'<div class="mw-tos-summary">' );
3573 $out->addWikiMsg( $msg );
3574 $out->addHTML(
'</div>' );
3583 $this->context->getOutput()->addHTML(
'<div class="mw-editTools">' .
3584 $this->context->msg(
'edittools' )->inContentLanguage()->parse() .
3609 $copywarnMsg = [
'copyrightwarning',
3610 '[[' .
wfMessage(
'copyrightpage' )->inContentLanguage()->text() .
']]',
3613 $copywarnMsg = [
'copyrightwarning2',
3614 '[[' .
wfMessage(
'copyrightpage' )->inContentLanguage()->text() .
']]' ];
3621 $msg->inLanguage( $langcode );
3623 return "<div id=\"editpage-copywarn\">\n" .
3624 $msg->$format() .
"\n</div>";
3641 $limitReport =
Html::rawElement(
'div', [
'class' =>
'mw-limitReportExplanation' ],
3642 wfMessage(
'limitreport-title' )->parseAsBlock()
3646 $limitReport .=
Html::openElement(
'div', [
'class' =>
'preview-limit-report-wrapper' ] );
3649 'class' =>
'preview-limit-report wikitable'
3655 [ $key, &
$value, &$limitReport,
true,
true ]
3658 $valueMsg =
wfMessage( [
"$key-value-html",
"$key-value" ] );
3659 if ( !$valueMsg->exists() ) {
3662 if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
3677 return $limitReport;
3681 $out = $this->context->getOutput();
3682 $out->addHTML(
"<div class='editOptions'>\n" );
3684 if ( $this->section !=
'new' ) {
3691 [
'minor' => $this->minoredit,
'watch' => $this->watchthis ]
3693 $checkboxesHTML =
new OOUI\HorizontalLayout( [
'items' => $checkboxes ] );
3695 $out->addHTML(
"<div class='editCheckboxes'>" . $checkboxesHTML .
"</div>\n" );
3699 $out->addHTML( $this->editFormTextAfterWarn );
3701 $out->addHTML(
"<div class='editButtons'>\n" );
3706 $message = $this->context->msg(
'edithelppage' )->inContentLanguage()->text();
3710 $this->context->msg(
'edithelp' )->text(),
3711 [
'target' =>
'helpwindow',
'href' => $edithelpurl ],
3714 $this->context->msg(
'word-separator' )->escaped() .
3715 $this->context->msg(
'newwindow' )->parse();
3717 $out->addHTML(
" <span class='cancelLink'>{$cancel}</span>\n" );
3718 $out->addHTML(
" <span class='editHelp'>{$edithelp}</span>\n" );
3719 $out->addHTML(
"</div><!-- editButtons -->\n" );
3723 $out->addHTML(
"</div><!-- editOptions -->\n" );
3731 $out = $this->context->getOutput();
3734 if (
Hooks::run(
'EditPageBeforeConflictDiff', [ &$editPage, &
$out ] ) ) {
3750 if ( !$this->isConflict && $this->oldid > 0 ) {
3753 $cancelParams[
'redirect'] =
'no';
3756 return new OOUI\ButtonWidget( [
3757 'id' =>
'mw-editform-cancel',
3759 'label' =>
new OOUI\HtmlSnippet( $this->context->msg(
'cancel' )->parse() ),
3761 'infusable' =>
true,
3762 'flags' =>
'destructive',
3776 return $title->getLocalURL( [
'action' => $this->
action ] );
3787 if ( $this->deletedSinceEdit !==
null ) {
3791 $this->deletedSinceEdit =
false;
3793 if ( !$this->mTitle->exists() && $this->mTitle->isDeletedQuick() ) {
3795 if ( $this->lastDelete ) {
3796 $deleteTime =
wfTimestamp( TS_MW, $this->lastDelete->log_timestamp );
3797 if ( $deleteTime > $this->starttime ) {
3798 $this->deletedSinceEdit =
true;
3815 $data =
$dbr->selectRow(
3816 array_merge( [
'logging' ], $commentQuery[
'tables'], $actorQuery[
'tables'], [
'user' ] ),
3826 ] + $commentQuery[
'fields'] + $actorQuery[
'fields'],
3828 'log_namespace' => $this->mTitle->getNamespace(),
3829 'log_title' => $this->mTitle->getDBkey(),
3830 'log_type' =>
'delete',
3831 'log_action' =>
'delete',
3834 [
'LIMIT' => 1,
'ORDER BY' =>
'log_timestamp DESC' ],
3836 'user' => [
'JOIN',
'user_id=' . $actorQuery[
'fields'][
'log_user'] ],
3837 ] + $commentQuery[
'joins'] + $actorQuery[
'joins']
3840 if ( is_object( $data ) ) {
3842 $data->user_name = $this->context->msg(
'rev-deleted-user' )->escaped();
3846 $data->log_comment_text = $this->context->msg(
'rev-deleted-comment' )->escaped();
3847 $data->log_comment_data =
null;
3860 $out = $this->context->getOutput();
3861 $config = $this->context->getConfig();
3867 if ( $this->textbox1 !==
'' ) {
3871 $parsedNote =
$out->parse(
"<div class='previewnote'>" .
3872 $this->context->msg(
'session_fail_preview_html' )->text() .
"</div>",
3886 'AlternateEditPreview',
3887 [ $this, &
$content, &$previewHTML, &$this->mParserOutput ] )
3889 return $previewHTML;
3892 # provide a anchor link to the editform
3893 $continueEditing =
'<span class="mw-continue-editing">' .
3894 '[[#' . self::EDITFORM_ID .
'|' .
3895 $this->context->getLanguage()->getArrow() .
' ' .
3896 $this->context->msg(
'continue-editing' )->text() .
']]</span>';
3897 if ( $this->mTriedSave && !$this->mTokenOk ) {
3898 if ( $this->mTokenOkExceptSuffix ) {
3899 $note = $this->context->msg(
'token_suffix_mismatch' )->plain();
3902 $note = $this->context->msg(
'session_fail_preview' )->plain();
3905 } elseif ( $this->incompleteForm ) {
3906 $note = $this->context->msg(
'edit_form_incomplete' )->plain();
3907 if ( $this->mTriedSave ) {
3911 $note = $this->context->msg(
'previewnote' )->plain() .
' ' . $continueEditing;
3914 # don't parse non-wikitext pages, show message about preview
3915 if ( $this->mTitle->isUserConfigPage() || $this->mTitle->isSiteConfigPage() ) {
3916 if ( $this->mTitle->isUserConfigPage() ) {
3918 } elseif ( $this->mTitle->isSiteConfigPage() ) {
3926 if ( $level ===
'user' && !$config->get(
'AllowUserCss' ) ) {
3931 if ( $level ===
'user' ) {
3936 if ( $level ===
'user' && !$config->get(
'AllowUserJs' ) ) {
3943 # Used messages to make sure grep find them:
3944 # Messages: usercsspreview, userjsonpreview, userjspreview,
3945 # sitecsspreview, sitejsonpreview, sitejspreview
3946 if ( $level && $format ) {
3947 $note =
"<div id='mw-{$level}{$format}preview'>" .
3948 $this->context->msg(
"{$level}{$format}preview" )->text() .
3949 ' ' . $continueEditing .
"</div>";
3953 # If we're adding a comment, we need to show the
3954 # summary as the headline
3955 if ( $this->section ===
"new" && $this->summary !==
"" ) {
3960 Hooks::run(
'EditPageGetPreviewContent', $hook_args );
3963 $parserOutput = $parserResult[
'parserOutput'];
3964 $previewHTML = $parserResult[
'html'];
3965 $this->mParserOutput = $parserOutput;
3966 $out->addParserOutputMetadata( $parserOutput );
3967 if (
$out->userCanPreview() ) {
3971 if (
count( $parserOutput->getWarnings() ) ) {
3972 $note .=
"\n\n" . implode(
"\n\n", $parserOutput->getWarnings() );
3976 $m = $this->context->msg(
3977 'content-failed-to-parse',
3978 $this->contentModel,
3979 $this->contentFormat,
3982 $note .=
"\n\n" . $m->parse();
3986 if ( $this->isConflict ) {
3987 $conflict =
'<h2 id="mw-previewconflict">'
3988 . $this->context->msg(
'previewconflict' )->escaped() .
"</h2>\n";
3990 $conflict =
'<hr />';
3993 $previewhead =
"<div class='previewnote'>\n" .
3994 '<h2 id="mw-previewheader">' . $this->context->msg(
'preview' )->escaped() .
"</h2>" .
3995 $out->parse( $note,
true,
true ) . $conflict .
"</div>\n";
3997 $pageViewLang = $this->mTitle->getPageViewLanguage();
3998 $attribs = [
'lang' => $pageViewLang->getHtmlCode(),
'dir' => $pageViewLang->getDir(),
3999 'class' =>
'mw-content-' . $pageViewLang->getDir() ];
4006 $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
4007 $stats->increment(
'edit.failures.' . $failureType );
4015 $parserOptions = $this->
page->makeParserOptions( $this->context );
4016 $parserOptions->setIsPreview(
true );
4017 $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !==
'' );
4018 $parserOptions->enableLimitReport();
4025 return $parserOptions;
4038 $user = $this->context->getUser();
4046 $pstContent =
$content->preSaveTransform( $this->mTitle,
$user, $parserOptions );
4047 $scopedCallback = $parserOptions->setupFakeRevision( $this->mTitle, $pstContent,
$user );
4048 $parserOutput = $pstContent->getParserOutput( $this->mTitle,
null, $parserOptions );
4049 ScopedCallback::consume( $scopedCallback );
4051 'parserOutput' => $parserOutput,
4052 'html' => $parserOutput->getText( [
4053 'enableSectionEditLinks' =>
false
4062 if ( $this->preview || $this->section !=
'' ) {
4064 if ( !isset( $this->mParserOutput ) ) {
4067 foreach ( $this->mParserOutput->getTemplates()
as $ns =>
$template ) {
4074 return $this->mTitle->getTemplateLinksFrom();
4085 $startingToolbar =
'<div id="toolbar"></div>';
4086 $toolbar = $startingToolbar;
4088 if ( !
Hooks::run(
'EditPageBeforeEditToolbar', [ &$toolbar ] ) ) {
4092 return ( $toolbar === $startingToolbar ) ? null : $toolbar;
4116 $user = $this->context->getUser();
4118 if ( !$this->isNew &&
$user->isAllowed(
'minoredit' ) ) {
4119 $checkboxes[
'wpMinoredit'] = [
4120 'id' =>
'wpMinoredit',
4121 'label-message' =>
'minoredit',
4123 'tooltip' =>
'minoredit',
4124 'label-id' =>
'mw-editpage-minoredit',
4125 'legacy-name' =>
'minor',
4126 'default' => $checked[
'minor'],
4130 if (
$user->isLoggedIn() ) {
4131 $checkboxes[
'wpWatchthis'] = [
4132 'id' =>
'wpWatchthis',
4133 'label-message' =>
'watchthis',
4135 'tooltip' =>
'watch',
4136 'label-id' =>
'mw-editpage-watch',
4137 'legacy-name' =>
'watch',
4138 'default' => $checked[
'watch'],
4143 Hooks::run(
'EditPageGetCheckboxesDefinition', [ $editPage, &$checkboxes ] );
4167 if ( isset(
$options[
'tooltip'] ) ) {
4168 $accesskey = $this->context->msg(
"accesskey-{$options['tooltip']}" )->text();
4171 if ( isset(
$options[
'title-message'] ) ) {
4172 $title = $this->context->msg(
$options[
'title-message'] )->text();
4175 $checkboxes[ $legacyName ] =
new OOUI\FieldLayout(
4176 new OOUI\CheckboxInputWidget( [
4178 'accessKey' => $accesskey,
4183 'infusable' =>
true,
4186 'align' =>
'inline',
4187 'label' =>
new OOUI\HtmlSnippet( $this->context->msg(
$options[
'label-message'] )->parse() ),
4189 'id' =>
$options[
'label-id'] ??
null,
4205 $this->context->getConfig()->get(
'EditSubmitButtonLabelPublish' );
4208 $newPage = !$this->mTitle->exists();
4210 if ( $labelAsPublish ) {
4211 $buttonLabelKey = $newPage ?
'publishpage' :
'publishchanges';
4213 $buttonLabelKey = $newPage ?
'savearticle' :
'savechanges';
4216 return $buttonLabelKey;
4231 $this->context->getConfig()->get(
'EditSubmitButtonLabelPublish' );
4234 $buttonTooltip = $labelAsPublish ?
'publish' :
'save';
4236 $buttons[
'save'] =
new OOUI\ButtonInputWidget( [
4239 'id' =>
'wpSaveWidget',
4240 'inputId' =>
'wpSave',
4242 'useInputTag' =>
true,
4243 'flags' => [
'progressive',
'primary' ],
4244 'label' => $buttonLabel,
4245 'infusable' =>
true,
4253 $buttons[
'preview'] =
new OOUI\ButtonInputWidget( [
4254 'name' =>
'wpPreview',
4256 'id' =>
'wpPreviewWidget',
4257 'inputId' =>
'wpPreview',
4259 'useInputTag' =>
true,
4260 'label' => $this->context->msg(
'showpreview' )->text(),
4261 'infusable' =>
true,
4269 $buttons[
'diff'] =
new OOUI\ButtonInputWidget( [
4272 'id' =>
'wpDiffWidget',
4273 'inputId' =>
'wpDiff',
4275 'useInputTag' =>
true,
4276 'label' => $this->context->msg(
'showdiff' )->text(),
4277 'infusable' =>
true,
4297 $out = $this->context->getOutput();
4298 $out->prepareErrorPage( $this->context->msg(
'nosuchsectiontitle' ) );
4300 $res = $this->context->msg(
'nosuchsectiontext', $this->section )->parseAsBlock();
4307 $out->returnToMain(
false, $this->mTitle );
4318 if ( is_array( $match ) ) {
4319 $match = $this->context->getLanguage()->listToText( $match );
4321 $out = $this->context->getOutput();
4322 $out->prepareErrorPage( $this->context->msg(
'spamprotectiontitle' ) );
4324 $out->addHTML(
'<div id="spamprotected">' );
4325 $out->addWikiMsg(
'spamprotectiontext' );
4329 $out->addHTML(
'</div>' );
4331 $out->wrapWikiMsg(
'<h2>$1</h2>',
"yourdiff" );
4334 $out->wrapWikiMsg(
'<h2>$1</h2>',
"yourtext" );
4351 return rtrim(
$request->getText( $field ) );
4371 $out = $this->context->getOutput();
4372 $editNotices = $this->mTitle->getEditNotices( $this->oldid );
4373 if (
count( $editNotices ) ) {
4374 $out->addHTML( implode(
"\n", $editNotices ) );
4376 $msg = $this->context->msg(
'editnotice-notext' );
4377 if ( !$msg->isDisabled() ) {
4379 '<div class="mw-editnotice-notext">'
4380 . $msg->parseAsBlock()
4391 if ( $this->mTitle->isTalkPage() ) {
4392 $this->context->getOutput()->addWikiMsg(
'talkpagetext' );
4400 if ( $this->contentLength ===
false ) {
4401 $this->contentLength = strlen( $this->textbox1 );
4404 $out = $this->context->getOutput();
4405 $lang = $this->context->getLanguage();
4406 $maxArticleSize = $this->context->getConfig()->get(
'MaxArticleSize' );
4407 if ( $this->tooBig || $this->contentLength > $maxArticleSize * 1024 ) {
4408 $out->wrapWikiMsg(
"<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
4411 $lang->formatNum( round( $this->contentLength / 1024, 3 ) ),
4412 $lang->formatNum( $maxArticleSize )
4416 if ( !$this->context->msg(
'longpage-hint' )->isDisabled() ) {
4417 $out->wrapWikiMsg(
"<div id='mw-edit-longpage-hint'>\n$1\n</div>",
4420 $lang->formatSize( strlen( $this->textbox1 ) ),
4421 strlen( $this->textbox1 )
4432 $out = $this->context->getOutput();
4433 if ( $this->mTitle->isProtected(
'edit' ) &&
4436 # Is the title semi-protected?
4437 if ( $this->mTitle->isSemiProtected() ) {
4438 $noticeMsg =
'semiprotectedpagewarning';
4440 # Then it must be protected based on static groups (regular)
4441 $noticeMsg =
'protectedpagewarning';
4444 [
'lim' => 1,
'msgKey' => [ $noticeMsg ] ] );
4446 if ( $this->mTitle->isCascadeProtected() ) {
4447 # Is this page under cascading protection from some source pages?
4449 list( $cascadeSources, ) = $this->mTitle->getCascadeProtectionSources();
4450 $notice =
"<div class='mw-cascadeprotectedwarning'>\n$1\n";
4451 $cascadeSourcesCount =
count( $cascadeSources );
4452 if ( $cascadeSourcesCount > 0 ) {
4453 # Explain, and list the titles responsible
4454 foreach ( $cascadeSources
as $page ) {
4455 $notice .=
'* [[:' .
$page->getPrefixedText() .
"]]\n";
4458 $notice .=
'</div>';
4459 $out->wrapWikiMsg( $notice, [
'cascadeprotectedwarning', $cascadeSourcesCount ] );
4461 if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions(
'create' ) ) {
4464 'showIfEmpty' =>
false,
4465 'msgKey' => [
'titleprotectedwarning' ],
4466 'wrap' =>
"<div class=\"mw-titleprotectedwarning\">\n$1</div>" ] );
4516 $userAgent = $this->context->getRequest()->getHeader(
'User-Agent' );
4517 if ( $userAgent && preg_match(
'/MSIE|Edge/', $userAgent ) ) {
4519 return $wgParser->guessLegacySectionNameFromWikiText( $text );
4525 return '#' . urlencode( mb_substr(
$name, 1 ) );
4535 $this->editConflictHelperFactory = $factory;
4536 $this->editConflictHelper =
null;
4543 if ( !$this->editConflictHelper ) {
4544 $this->editConflictHelper = call_user_func(
4545 $this->editConflictHelperFactory,
4561 MediaWikiServices::getInstance()->getStatsdDataFactory(),