From fa3aa9653e027e332e0a0e09bb5326642ddaa36f Mon Sep 17 00:00:00 2001 From: Daniel Friesen Date: Wed, 2 Dec 2009 07:22:29 +0000 Subject: [PATCH] EditPage refactor and improvements. - EditPage::showEditForm broken up into task specific methods - Subclasses can indicate they can't support section mode - Standard inputs should all be now in methods they can be grabbed from by subclasses that want to re-arange things - Many more places to override and hook into to change behavior - showTextbox1 parameters changed from $classes to $customAttribs and $textoverride - showContentForm and importContentFormData added; New workflow to override the wpTextbox1 behavior to use an alternate edit form ui or handle wpTextbox1 content in an alternate way. - getActionURL added for EditPage subclasses used in places where $this->action isn't enough (ie: EditPage on special pages) Html::textarea added --- RELEASE-NOTES | 1 + includes/EditPage.php | 780 ++++++++++++++++++------------ includes/Html.php | 25 + languages/messages/MessagesEn.php | 2 + skins/common/edit.js | 16 +- 5 files changed, 492 insertions(+), 332 deletions(-) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index d9346e9abc..b73b43e6db 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -292,6 +292,7 @@ Hopefully we will remove this configuration var soon) * Allow \pagecolor and \definecolor in texvc * $wgTexvcBackgroundColor contains background color for texvc call * (bug 21574) Redirects can now have "303 See Other" HTTP status +* EditPage refactored to allow extensions to derive new edit modes much easier. === Bug fixes in 1.16 === diff --git a/includes/EditPage.php b/includes/EditPage.php index 3d41d867c7..e8867a5452 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -555,6 +555,29 @@ class EditPage { } } + /** + * Does this EditPage class support section editing? + * This is used by EditPage subclasses to indicate their ui cannot handle section edits + * + * @return bool + */ + protected function isSectionEditSupported() { + return true; + } + + /** + * Returns the URL to use in the form's action attribute. + * This is used by EditPage subclasses when simply customizing the action + * variable in the constructor is not enough. This can be used when the + * EditPage lives inside of a Special page rather than a custom page action. + * + * @param Title $title The title for which is being edited (where we go to for &action= links) + * @return string + */ + protected function getActionURL( Title $title ) { + return $title->getLocalURL( array( 'action' => $this->action ) ); + } + /** * @todo document * @param $request @@ -572,7 +595,16 @@ class EditPage { # Also remove trailing whitespace, but don't remove _initial_ # whitespace from the text boxes. This may be significant formatting. $this->textbox1 = $this->safeUnicodeInput( $request, 'wpTextbox1' ); - $this->textbox2 = $this->safeUnicodeInput( $request, 'wpTextbox2' ); + if ( !$request->getCheck('wpTextbox2') ) { + // Skip this if wpTextbox2 has input, it indicates that we came + // from a conflict page with raw page text, not a custom form + // modified by subclasses + wfProfileIn( get_class($this)."::importContentFormData" ); + $textbox1 = $this->importContentFormData( $request ); + if ( isset($textbox1) ) + $this->textbox1 = $textbox1; + wfProfileOut( get_class($this)."::importContentFormData" ); + } $this->mMetaData = rtrim( $request->getText( 'metadata' ) ); # Truncate for whole multibyte characters. +5 bytes for ellipsis $this->summary = $wgLang->truncate( $request->getText( 'wpSummary' ), 250, '' ); @@ -643,7 +675,6 @@ class EditPage { # Not a posted form? Start with nothing. wfDebug( __METHOD__ . ": Not a posted form.\n" ); $this->textbox1 = ''; - $this->textbox2 = ''; $this->mMetaData = ''; $this->summary = ''; $this->edittime = ''; @@ -680,6 +711,18 @@ class EditPage { wfRunHooks( 'EditPage::importFormData', array( $this, $request ) ); } + /** + * Subpage overridable method for extracting the page content data from the + * posted form to be placed in $this->textbox1, if using customized input + * this method should be overrided and return the page text that will be used + * for saving, preview parsing and so on... + * + * @praram WebRequest $request + */ + protected function importContentFormData( &$request ) { + return; // Don't do anything, EditPage already extracted wpTextbox1 + } + /** * Make sure the form isn't faking a user's credentials. * @@ -1168,7 +1211,7 @@ class EditPage { * near the top, for captchas and the like. */ function showEditForm( $formCallback=null ) { - global $wgOut, $wgUser, $wgLang, $wgContLang, $wgMaxArticleSize, $wgTitle, $wgRequest; + global $wgOut, $wgUser, $wgTitle, $wgRequest; # If $wgTitle is null, that means we're in API mode. # Some hook probably called this function without checking @@ -1197,15 +1240,175 @@ class EditPage { # Enabled article-related sidebar, toplinks, etc. $wgOut->setArticleRelated( true ); - $cancelParams = array(); + if ( $this->editFormHeadInit() === false ) + return; + + $action = htmlspecialchars($this->getActionURL($wgTitle)); + + if ( $wgUser->getOption( 'showtoolbar' ) and !$this->isCssJsSubpage ) { + # prepare toolbar for edit buttons + $toolbar = EditPage::getEditToolbar(); + } else { + $toolbar = ''; + } + + + // activate checkboxes if user wants them to be always active + if ( !$this->preview && !$this->diff ) { + # Sort out the "watch" checkbox + if ( $wgUser->getOption( 'watchdefault' ) ) { + # Watch all edits + $this->watchthis = true; + } elseif ( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) { + # Watch creations + $this->watchthis = true; + } elseif ( $this->mTitle->userIsWatching() ) { + # Already watched + $this->watchthis = true; + } + + # May be overriden by request parameters + if( $wgRequest->getBool( 'watchthis' ) ) { + $this->watchthis = true; + } + + if ( $wgUser->getOption( 'minordefault' ) ) $this->minoredit = true; + } + + $wgOut->addHTML( $this->editFormPageTop ); + + if ( $wgUser->getOption( 'previewontop' ) ) { + $this->displayPreviewArea( $previewOutput, true ); + } + + $wgOut->addHTML( $this->editFormTextTop ); + + $templates = $this->getTemplates(); + $formattedtemplates = $sk->formatTemplates( $templates, $this->preview, $this->section != ''); + + $hiddencats = $this->mArticle->getHiddenCategories(); + $formattedhiddencats = $sk->formatHiddenCategories( $hiddencats ); + + if ( $this->wasDeletedSinceLastEdit() && 'save' != $this->formtype ) { + $wgOut->wrapWikiMsg( + "
\n$1
", + 'deletedwhileediting' ); + } elseif ( $this->wasDeletedSinceLastEdit() ) { + // Hide the toolbar and edit area, user can click preview to get it back + // Add an confirmation checkbox and explanation. + $toolbar = ''; + // @todo move this to a cleaner conditional instead of blanking a variable + } + $wgOut->addHTML( << +END +); + + if ( is_callable( $formCallback ) ) { + call_user_func_array( $formCallback, array( &$wgOut ) ); + } + + wfRunHooks( 'EditPage::showEditForm:fields', array( &$this, &$wgOut ) ); + + // Put these up at the top to ensure they aren't lost on early form submission + $this->showFormBeforeText(); + + if ( $this->wasDeletedSinceLastEdit() && 'save' == $this->formtype ) { + $wgOut->addHTML( + '
' . + $wgOut->parse( wfMsg( 'confirmrecreate', $this->lastDelete->user_name , $this->lastDelete->log_comment ) ) . + Xml::checkLabel( wfMsg( 'recreate' ), 'wpRecreate', 'wpRecreate', false, + array( 'title' => $sk->titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ) + ) . + '
' + ); + } + + # If a blank edit summary was previously provided, and the appropriate + # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the + # user being bounced back more than once in the event that a summary + # is not required. + ##### + # For a bit more sophisticated detection of blank summaries, hash the + # automatic one and pass that in the hidden field wpAutoSummary. + if ( $this->missingSummary || + ( $this->section == 'new' && $wgRequest->getBool( 'nosummary' ) ) ) + $wgOut->addHTML( Xml::hidden( 'wpIgnoreBlankSummary', true ) ); + $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary ); + $wgOut->addHTML( Xml::hidden( 'wpAutoSummary', $autosumm ) ); + + if ( $this->section == 'new' ) { + $this->showSummaryInput( true, $this->summary ); + $wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) ); + } + $wgOut->addHTML( $this->editFormTextBeforeContent ); + if ( $this->isConflict ) { - $wgOut->wrapWikiMsg( "
\n$1
", 'explainconflict' ); + // In an edit conflict bypass the overrideable content form method + // and fallback to the raw wpTextbox1 since editconflicts can't be + // resolved between page source edits and custom ui edits using the + // custom edit ui. + $this->showTextbox1(); + } else { + $this->showContentForm(); + } + + $wgOut->addHTML( $this->editFormTextAfterContent ); + + $wgOut->addWikiText( $this->getCopywarn() ); + if ( isset($this->editFormTextAfterWarn) && $this->editFormTextAfterWarn !== '' ) + $wgOut->addHTML( $this->editFormTextAfterWarn ); + + global $wgUseMetadataEdit; + if ( $wgUseMetadataEdit ) + $this->showMetaData(); + + $this->showStandardInputs(); + + $this->showFormAfterText(); + + $this->showTosSummary(); + $this->showEditTools(); + + $wgOut->addHTML( <<editFormTextAfterTools} +
+{$formattedtemplates} +
+
+{$formattedhiddencats} +
+END +); + + if ( $this->isConflict ) + $this->showConflict(); + + $wgOut->addHTML( $this->editFormTextBottom ); + $wgOut->addHTML( "\n" ); + if ( !$wgUser->getOption( 'previewontop' ) ) { + $this->displayPreviewArea( $previewOutput, false ); + } - $this->textbox2 = $this->textbox1; - $this->textbox1 = $this->getContent(); + wfProfileOut( __METHOD__ ); + } + + protected function editFormHeadInit() { + global $wgOut, $wgParser, $wgUser, $wgMaxArticleSize, $wgLang; + if ( $this->isConflict ) { + $wgOut->wrapWikiMsg( "
\n$1
", 'explainconflict' ); $this->edittime = $this->mArticle->getTimestamp(); } else { + if ( $this->section != '' && !$this->isSectionEditSupported() ) { + // We use $this->section to much before this and getVal('wgSection') directly in other places + // at this point we can't reset $this->section to '' to fallback to non-section editing. + // Someone is welcome to try refactoring though + $wgOut->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' ); + return false; + } + if ( $this->section != '' && $this->section != 'new' ) { $matches = array(); if ( !$this->summary && !$this->preview && !$this->diff ) { @@ -1219,25 +1422,21 @@ class EditPage { } } - if ( $this->missingComment ) { + if ( $this->missingComment ) $wgOut->wrapWikiMsg( '
$1
', 'missingcommenttext' ); - } - if ( $this->missingSummary && $this->section != 'new' ) { + if ( $this->missingSummary && $this->section != 'new' ) $wgOut->wrapWikiMsg( '
$1
', 'missingsummary' ); - } - if ( $this->missingSummary && $this->section == 'new' ) { + if ( $this->missingSummary && $this->section == 'new' ) $wgOut->wrapWikiMsg( '
$1
', 'missingcommentheader' ); - } - if ( $this->hookError !== '' ) { + if ( $this->hookError !== '' ) $wgOut->addWikiText( $this->hookError ); - } - if ( !$this->checkUnicodeCompliantBrowser() ) { + if ( !$this->checkUnicodeCompliantBrowser() ) $wgOut->addWikiMsg( 'nonunicodebrowser' ); - } + if ( isset( $this->mArticle ) && isset( $this->mArticle->mRevision ) ) { // Let sysop know that this will make private content public if saved @@ -1249,7 +1448,6 @@ class EditPage { if ( !$this->mArticle->mRevision->isCurrent() ) { $this->mArticle->setOldSubtitle( $this->mArticle->mRevision->getId() ); - $cancelParams['oldid'] = $this->mArticle->mRevision->getId(); $wgOut->addWikiMsg( 'editingold' ); } } @@ -1274,17 +1472,13 @@ class EditPage { } } - $classes = array(); // Textarea CSS - if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { - } elseif ( $this->mTitle->isProtected( 'edit' ) ) { + if ( $this->mTitle->getNamespace() != NS_MEDIAWIKI && $this->mTitle->isProtected( 'edit' ) ) { # Is the title semi-protected? if ( $this->mTitle->isSemiProtected() ) { $noticeMsg = 'semiprotectedpagewarning'; - $classes[] = 'mw-textarea-sprotected'; } else { # Then it must be protected based on static groups (regular) $noticeMsg = 'protectedpagewarning'; - $classes[] = 'mw-textarea-protected'; } LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle->getPrefixedText(), '', array( 'lim' => 1, 'msgKey' => array( $noticeMsg ) ) ); @@ -1307,9 +1501,9 @@ class EditPage { $wgOut->wrapWikiMsg( '
$1
', 'titleprotectedwarning' ); } - if ( $this->kblength === false ) { + if ( $this->kblength === false ) $this->kblength = (int)( strlen( $this->textbox1 ) / 1024 ); - } + if ( $this->tooBig || $this->kblength > $wgMaxArticleSize ) { $wgOut->addHTML( "
\n" ); $wgOut->addWikiMsg( 'longpageerror', $wgLang->formatNum( $this->kblength ), $wgLang->formatNum( $wgMaxArticleSize ) ); @@ -1319,261 +1513,113 @@ class EditPage { $wgOut->addWikiMsg( 'longpagewarning', $wgLang->formatNum( $this->kblength ) ); $wgOut->addHTML( "
\n" ); } + } - $action = $wgTitle->escapeLocalURL( array( 'action' => $this->action ) ); - - $summary = wfMsgExt( 'summary', 'parseinline' ); - $subject = wfMsgExt( 'subject', 'parseinline' ); - - $cancel = $sk->link( - $wgTitle, - wfMsgExt( 'cancel', array( 'parseinline' ) ), - array( 'id' => 'mw-editform-cancel' ), - $cancelParams, - array( 'known', 'noclasses' ) + /** + * Standard summary input and label (wgSummary), abstracted so EditPage + * subclasses may reorganize the form. + * Note that you do not need to worry about the label's for=, it will be + * inferred by the id given to the input. You can remove them both by + * passing array( 'id' => false ) to $userInputAttrs. + * + * @param $summary The value of the summary input + * @param $labelText The html to place inside the label + * @param $userInputAttrs An array of attrs to use on the input + * @param $userSpanAttrs An array of attrs to use on the span inside the label + * + * @return array An array in the format array( $label, $input ) + */ + function getSummaryInput($summary = "", $labelText = null, $userInputAttrs = null, $userSpanLabelAttrs = null) { + $inputAttrs = array( + 'id' => 'wpSummary', + 'maxlength' => '200', + 'tabindex' => '1', + 'size' => 60, + 'spellcheck' => 'true', + 'onfocus' => "currentFocused = this;", ); - $separator = wfMsgExt( 'pipe-separator' , 'escapenoentities' ); - $edithelpurl = Skin::makeInternalOrExternalUrl( wfMsgForContent( 'edithelppage' ) ); - $edithelp = ''. - htmlspecialchars( wfMsg( 'edithelp' ) ).' '. - htmlspecialchars( wfMsg( 'newwindow' ) ); - - global $wgRightsText; - if ( $wgRightsText ) { - $copywarnMsg = array( 'copyrightwarning', - '[[' . wfMsgForContent( 'copyrightpage' ) . ']]', - $wgRightsText ); - } else { - $copywarnMsg = array( 'copyrightwarning2', - '[[' . wfMsgForContent( 'copyrightpage' ) . ']]' ); - } - // Allow for site and per-namespace customization of contribution/copyright notice. - wfRunHooks( 'EditPageCopyrightWarning', array( $this->mTitle, &$copywarnMsg ) ); - - if ( $wgUser->getOption( 'showtoolbar' ) and !$this->isCssJsSubpage ) { - # prepare toolbar for edit buttons - $toolbar = EditPage::getEditToolbar(); - } else { - $toolbar = ''; - } - - - // activate checkboxes if user wants them to be always active - if ( !$this->preview && !$this->diff ) { - # Sort out the "watch" checkbox - if ( $wgUser->getOption( 'watchdefault' ) ) { - # Watch all edits - $this->watchthis = true; - } elseif ( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) { - # Watch creations - $this->watchthis = true; - } elseif ( $this->mTitle->userIsWatching() ) { - # Already watched - $this->watchthis = true; - } - - # May be overriden by request parameters - if( $wgRequest->getBool( 'watchthis' ) ) { - $this->watchthis = true; - } - - if ( $wgUser->getOption( 'minordefault' ) ) $this->minoredit = true; - } - - $wgOut->addHTML( $this->editFormPageTop ); + if ( $userInputAttrs ) + $inputAttrs += $userInputAttrs; + $spanLabelAttrs = array( + 'class' => $summaryClass, + 'id' => "wpSummaryLabel" + ); + if ( is_array($userSpanLabelAttrs) ) + $spanLabelAttrs += $userSpanLabelAttrs; - if ( $wgUser->getOption( 'previewontop' ) ) { - $this->displayPreviewArea( $previewOutput, true ); + $label = null; + if ( $labelText ) { + $label = Xml::tags( 'label', $inputAttrs['id'] ? array( 'for' => $inputAttrs['id'] ) : null, $labelText ); + $label = Xml::tags( 'span', $spanLabelAttrs, $label ); } + $input = Html::input( 'wpSummary', $summary, 'text', $inputAttrs ); - $wgOut->addHTML( $this->editFormTextTop ); - - # if this is a comment, show a subject line at the top, which is also the edit summary. - # Otherwise, show a summary field at the bottom - $summarytext = $wgContLang->recodeForEdit( $this->summary ); + return array( $label, $input ); + } - # If a blank edit summary was previously provided, and the appropriate - # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the - # user being bounced back more than once in the event that a summary - # is not required. - ##### - # For a bit more sophisticated detection of blank summaries, hash the - # automatic one and pass that in the hidden field wpAutoSummary. - $summaryhiddens = ''; - if ( $this->missingSummary ) $summaryhiddens .= Xml::hidden( 'wpIgnoreBlankSummary', true ); - $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary ); - $summaryhiddens .= Xml::hidden( 'wpAutoSummary', $autosumm ); - if ( $this->section == 'new' ) { - $commentsubject = ''; - if ( !$wgRequest->getBool( 'nosummary' ) ) { - # Add a class if 'missingsummary' is triggered to allow styling of the summary line - $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary'; - - $commentsubject = - Xml::tags( 'label', array( 'for' => 'wpSummary' ), $subject ); - $commentsubject = - Xml::tags( 'span', array( 'class' => $summaryClass, 'id' => "wpSummaryLabel" ), - $commentsubject ); - $commentsubject .= ' '; - $commentsubject .= Html::input( 'wpSummary', - $summarytext, - 'text', - array( - 'id' => 'wpSummary', - 'maxlength' => '200', - 'tabindex' => '1', - 'size' => '60', - 'spellcheck' => 'true' - ) ); - } else { - $summaryhiddens .= Xml::hidden( 'wpIgnoreBlankSummary', true ); # bug 18699 - } - $editsummary = "
\n"; - global $wgParser; - $formattedSummary = wfMsgForContent( 'newsectionsummary', $wgParser->stripSectionName( $this->summary ) ); - $subjectpreview = $summarytext && ( $this->preview || $this->diff ) ? - "
". wfMsgExt( 'subject-preview', 'parseinline' ) . $sk->commentBlock( $formattedSummary, $this->mTitle, true )."
\n" : ''; - $summarypreview = ''; + /** + * @param bool $isSubjectPreview true if this is the section subject/title + * up top, or false if this is the comment + * summary down below the textarea + * @param string $summary The text of the summary to display + * @return string + */ + protected function showSummaryInput( $isSubjectPreview, $summary = "" ) { + global $wgOut, $wgContLang; + # Add a class if 'missingsummary' is triggered to allow styling of the summary line + $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary'; + if ( $isSubjectPreview ) { + if ( $wgRequest->getBool( 'nosummary' ) ) + return; } else { - $commentsubject = ''; - - # Add a class if 'missingsummary' is triggered to allow styling of the summary line - $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary'; - - $editsummary = Xml::tags( 'label', array( 'for' => 'wpSummary' ), $summary ); - $editsummary = Xml::tags( 'span', array( 'class' => $summaryClass, 'id' => "wpSummaryLabel" ), - $editsummary ) . ' '; - - $editsummary .= Html::input( 'wpSummary', - $summarytext, - 'text', - array( - 'id' => 'wpSummary', - 'maxlength' => '200', - 'tabindex' => '1', - 'size' => '60', - 'spellcheck' => 'true' - ) ); - - // No idea where this is closed. - $editsummary .= '
'; - $editsummary = Xml::openElement( 'div', array( 'class' => 'editOptions' ) ) - . ($this->mShowSummaryField ? $editsummary : ''); - - $summarypreview = ''; - if ( $summarytext && ( $this->preview || $this->diff ) ) { - $summarypreview = - Xml::tags( 'div', - array( 'class' => 'mw-summary-preview' ), - wfMsgExt( 'summary-preview', 'parseinline' ) . - $sk->commentBlock( $this->summary, $this->mTitle ) - ); - } - $subjectpreview = ''; - } - $commentsubject .= $summaryhiddens; - - # Set focus to the edit box on load, except on preview or diff, where it would interfere with the display - if ( !$this->preview && !$this->diff ) { - $wgOut->setOnloadHandler( 'document.editform.wpTextbox1.focus()' ); - } - $templates = $this->getTemplates(); - $formattedtemplates = $sk->formatTemplates( $templates, $this->preview, $this->section != ''); - - $hiddencats = $this->mArticle->getHiddenCategories(); - $formattedhiddencats = $sk->formatHiddenCategories( $hiddencats ); - - global $wgUseMetadataEdit ; - if ( $wgUseMetadataEdit ) { - $metadata = $this->mMetaData ; - $metadata = htmlspecialchars( $wgContLang->recodeForEdit( $metadata ) ) ; - $top = wfMsgWikiHtml( 'metadata_help' ); - /* ToDo: Replace with clean code */ - $ew = $wgUser->getOption( 'editwidth' ); - if ( $ew ) $ew = " style=\"width:100%\""; - else $ew = ''; - $cols = $wgUser->getIntOption( 'cols' ); - /* /ToDo */ - $metadata = $top . "" ; - } - else $metadata = "" ; - - $recreate = ''; - if ( $this->wasDeletedSinceLastEdit() ) { - if ( 'save' != $this->formtype ) { - $wgOut->wrapWikiMsg( - "
\n$1
", - 'deletedwhileediting' ); - } else { - // Hide the toolbar and edit area, user can click preview to get it back - // Add an confirmation checkbox and explanation. - $toolbar = ''; - $recreate = '
' . - $wgOut->parse( wfMsg( 'confirmrecreate', $this->lastDelete->user_name , $this->lastDelete->log_comment ) ) . - Xml::checkLabel( wfMsg( 'recreate' ), 'wpRecreate', 'wpRecreate', false, - array( 'title' => $sk->titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ) - ) . '
'; - } - } - - $tabindex = 2; - - $checkboxes = $this->getCheckboxes( $tabindex, $sk, - array( 'minor' => $this->minoredit, 'watch' => $this->watchthis ) ); - - $checkboxhtml = implode( $checkboxes, "\n" ); - - $buttons = $this->getEditButtons( $tabindex ); - $buttonshtml = implode( $buttons, "\n" ); - - $safemodehtml = $this->checkUnicodeCompliantBrowser() - ? '' : Xml::hidden( 'safemode', '1' ); - - $wgOut->addHTML( << -END -); - - if ( is_callable( $formCallback ) ) { - call_user_func_array( $formCallback, array( &$wgOut ) ); + if ( !$this->mShowSummaryField ) + return; } + $summary = $wgContLang->recodeForEdit( $summary ); + $labelText = wfMsgExt( $isSubjectPreview ? 'subject' : 'summary', 'parseinline' ); + list($label, $input) = $this->getSummaryInput($summary, $labelText, array( 'class' => $summaryClass ), array()); + $wgOut->addHTML("{$label} {$input}"); + } - wfRunHooks( 'EditPage::showEditForm:fields', array( &$this, &$wgOut ) ); - - // Put these up at the top to ensure they aren't lost on early form submission - $this->showFormBeforeText(); - - $wgOut->addHTML( <<editFormTextBeforeContent} -END -); - $this->showTextbox1( $classes ); - - $wgOut->addHTML( $this->editFormTextAfterContent ); - - $wgOut->wrapWikiMsg( "
\n$1\n
", $copywarnMsg ); - $wgOut->addHTML( <<editFormTextAfterWarn} -{$metadata} -{$editsummary} -{$summarypreview} -{$checkboxhtml} -{$safemodehtml} -END -); + /** + * @param bool $isSubjectPreview true if this is the section subject/title + * up top, or false if this is the comment + * summary down below the textarea + * @param string $summary The text of the summary to display + * @return string + */ + protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) { + if ( !$summary || ( !$this->preview && !$this->diff ) ) + return ""; + + global $wgParser, $wgUser; + $sk = $wgUser->getSkin(); + + if ( $isSubjectPreview ) + $summary = wfMsgForContent( 'newsectionsummary', $wgParser->stripSectionName( $summary ) ); - $wgOut->addHTML( -"
-{$buttonshtml} - {$cancel}{$separator}{$edithelp} -
-
"); + $summary = wfMsgExt( 'subject-preview', 'parseinline' ) . $sk->commentBlock( $summary, $this->mTitle, !!$isSubjectPreview ); + return Xml::tags( 'div', array( 'class' => 'mw-summary-preview' ), $summary ); + } + protected function showFormBeforeText() { + global $wgOut; + $section = htmlspecialchars( $this->section ); + $wgOut->addHTML( << + + + + +INPUTS + ); + if ( !$this->checkUnicodeCompliantBrowser() ) + $wgOut->addHTML(Xml::hidden( 'safemode', '1' )); + } + + protected function showFormAfterText() { + global $wgOut, $wgUser; /** * To make it harder for someone to slip a user a page * which submits an edit form to the wiki without their @@ -1586,67 +1632,68 @@ END * include the constant suffix to prevent editing from * broken text-mangling proxies. */ - $token = htmlspecialchars( $wgUser->editToken() ); - $wgOut->addHTML( "\n\n" ); - - $this->showTosSummary(); - $this->showEditTools(); - - $wgOut->addHTML( <<editFormTextAfterTools} -
-{$formattedtemplates} -
-
-{$formattedhiddencats} -
-END -); - - if ( $this->isConflict && wfRunHooks( 'EditPageBeforeConflictDiff', array( &$this, &$wgOut ) ) ) { - $wgOut->wrapWikiMsg( '

$1

', "yourdiff" ); - - $de = new DifferenceEngine( $this->mTitle ); - $de->setText( $this->textbox2, $this->textbox1 ); - $de->showDiff( wfMsg( "yourtext" ), wfMsg( "storedversion" ) ); - - $wgOut->wrapWikiMsg( '

$1

', "yourtext" ); - $this->showTextbox2(); - } - $wgOut->addHTML( $this->editFormTextBottom ); - $wgOut->addHTML( "\n" ); - if ( !$wgUser->getOption( 'previewontop' ) ) { - $this->displayPreviewArea( $previewOutput, false ); - } - - wfProfileOut( __METHOD__ ); + $wgOut->addHTML( "\n" . Xml::hidden( "wpEditToken", $wgUser->editToken() ) . "\n" ); } - protected function showFormBeforeText() { - global $wgOut; - $wgOut->addHTML( " -section ) . "\" name=\"wpSection\" /> -starttime}\" name=\"wpStarttime\" />\n -edittime}\" name=\"wpEdittime\" />\n -scrolltop}\" name=\"wpScrolltop\" id=\"wpScrolltop\" />\n" ); + /** + * Subpage overridable method for printing the form for page content editing + * By default this simply outputs wpTextbox1 + * Subclasses can override this to provide a custom UI for editing; + * be it a form, or simply wpTextbox1 with a modified content that will be + * reverse modified when extracted from the post data. + * Note that this is basically the inverse for importContentFormData + * + * @praram WebRequest $request + */ + protected function showContentForm() { + $this->showTextbox1(); } - protected function showTextbox1( $classes ) { + /** + * Method to output wpTextbox1 + * The $textoverride method can be used by subclasses overriding showContentForm + * to pass back to this method. + * + * @param array $customAttribs An array of html attributes to use in the textarea + * @param string $textoverride Optional text to override $this->textarea1 with + */ + protected function showTextbox1($customAttribs = null, $textoverride = null) { + $classes = array(); // Textarea CSS + if ( $this->mTitle->getNamespace() != NS_MEDIAWIKI && $this->mTitle->isProtected( 'edit' ) ) { + # Is the title semi-protected? + if ( $this->mTitle->isSemiProtected() ) { + $classes[] = 'mw-textarea-sprotected'; + } else { + # Then it must be protected based on static groups (regular) + $classes[] = 'mw-textarea-protected'; + } + } $attribs = array( 'tabindex' => 1 ); + if ( is_array($customAttribs) ) + $attribs += $customAttribs; if ( $this->wasDeletedSinceLastEdit() ) $attribs['type'] = 'hidden'; - if ( !empty( $classes ) ) + if ( !empty( $classes ) ) { + if ( isset($attribs['class']) ) + $classes[] = $attribs['class']; $attribs['class'] = implode( ' ', $classes ); + } - $this->showTextbox( $this->textbox1, 'wpTextbox1', $attribs ); + # Set focus to the edit box on load, except on preview or diff, where it would interfere with the display + if ( !$this->preview && !$this->diff ) { + global $wgOut; + $wgOut->setOnloadHandler( 'document.editform.wpTextbox1.focus();' ); + } + + $this->showTextbox( isset($textoverride) ? $textoverride : $this->textbox1, 'wpTextbox1', $attribs ); } protected function showTextbox2() { $this->showTextbox( $this->textbox2, 'wpTextbox2', array( 'tabindex' => 6 ) ); } - protected function showTextbox( $content, $name, $attribs = array() ) { + protected function showTextbox( $content, $name, $customAttribs = array() ) { global $wgOut, $wgUser; $wikitext = $this->safeUnicodeOutput( $content ); @@ -1658,17 +1705,27 @@ END $wikitext .= "\n"; } - $attribs['accesskey'] = ','; - $attribs['id'] = $name; + $attribs = $customAttribs + array( + 'accesskey' => ',', + 'id' => $name, + 'cols' => $wgUser->getIntOption( 'cols' ), + 'rows' => $wgUser->getIntOption( 'rows' ), + 'onfocus' => "currentFocused = this;", + ); if ( $wgUser->getOption( 'editwidth' ) ) - $attribs['style'] = 'width: 100%'; + $attribs['style'] .= 'width: 100%'; - $wgOut->addHTML( Xml::textarea( - $name, - $wikitext, - $wgUser->getIntOption( 'cols' ), $wgUser->getIntOption( 'rows' ), - $attribs ) ); + $wgOut->addHTML( Html::textarea( $name, $wikitext, $attribs ) ); + } + + protected function showMetaData() { + global $wgOut, $wgContLang, $wgUser; + $metadata = htmlspecialchars( $wgContLang->recodeForEdit( $this->mMetaData ) ); + $ew = $wgUser->getOption( 'editwidth' ) ? ' style="width:100%"' : ''; + $cols = $wgUser->getIntOption( 'cols' ); + $metadata = wfMsgWikiHtml( 'metadata_help' ) . "" ; + $wgOut->addHTML( $metadata ); } protected function displayPreviewArea( $previewOutput, $isOnTop = false ) { @@ -1739,6 +1796,63 @@ END $wgOut->addWikiMsgArray( 'edittools', array(), array( 'content' ) ); $wgOut->addHTML( '' ); } + + protected function getCopywarn() { + global $wgRightsText; + if ( $wgRightsText ) { + $copywarnMsg = array( 'copyrightwarning', + '[[' . wfMsgForContent( 'copyrightpage' ) . ']]', + $wgRightsText ); + } else { + $copywarnMsg = array( 'copyrightwarning2', + '[[' . wfMsgForContent( 'copyrightpage' ) . ']]' ); + } + // Allow for site and per-namespace customization of contribution/copyright notice. + wfRunHooks( 'EditPageCopyrightWarning', array( $this->mTitle, &$copywarnMsg ) ); + + return "
\n" . call_user_func_array("wfMsgNoTrans", $copywarnMsg) . "\n
"; + } + + protected function showStandardInputs( &$tabindex = 2 ) { + global $wgOut, $wgUser; + $wgOut->addHTML( "
\n" ); + + if ( $this->section != 'new' ) { + $this->showSummaryInput( false, $this->summary ); + $wgOut->addHTML( $this->getSummaryPreview( false, $this->summary ) ); + } + + $checkboxes = $this->getCheckboxes( $tabindex, $wgUser->getSkin(), + array( 'minor' => $this->minoredit, 'watch' => $this->watchthis ) ); + $wgOut->addHTML( "
" . implode( $checkboxes, "\n" ) . "
\n" ); + $wgOut->addHTML( "
\n" ); + $wgOut->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" ); + + $cancel = $this->getCancelLink(); + $separator = wfMsgExt( 'pipe-separator' , 'escapenoentities' ); + $edithelpurl = Skin::makeInternalOrExternalUrl( wfMsgForContent( 'edithelppage' ) ); + $edithelp = ''. + htmlspecialchars( wfMsg( 'edithelp' ) ).' '. + htmlspecialchars( wfMsg( 'newwindow' ) ); + $wgOut->addHTML( " {$cancel}{$separator}{$edithelp}\n" ); + $wgOut->addHTML( "
\n
\n" ); + } + + protected function showConflict() { + global $wgOut; + $this->textbox2 = $this->textbox1; + $this->textbox1 = $this->getContent(); + if ( wfRunHooks( 'EditPageBeforeConflictDiff', array( &$this, &$wgOut ) ) ) { + $wgOut->wrapWikiMsg( '

$1

', "yourdiff" ); + + $de = new DifferenceEngine( $this->mTitle ); + $de->setText( $this->textbox2, $this->textbox1 ); + $de->showDiff( wfMsg( "yourtext" ), wfMsg( "storedversion" ) ); + + $wgOut->wrapWikiMsg( '

$1

', "yourtext" ); + $this->showTextbox2(); + } + } protected function getLastDelete() { $dbr = wfGetDB( DB_SLAVE ); @@ -2057,6 +2171,7 @@ END global $wgStylePath, $wgContLang, $wgLang; /** + * toolarray an array of arrays which each include the filename of * the button image (without path), the opening tag, the closing tag, * and optionally a sample text that is inserted between the two when no @@ -2324,6 +2439,22 @@ END } + public function getCancelLink() { + global $wgUser, $wgTitle; + $cancelParams = array(); + if ( !$this->isConflict && isset( $this->mArticle ) && + isset( $this->mArticle->mRevision ) && + !$this->mArticle->mRevision->isCurrent() ) + $cancelParams['oldid'] = $this->mArticle->mRevision->getId(); + return $wgUser->getSkin()->link( + $wgTitle, + wfMsgExt( 'cancel', array( 'parseinline' ) ), + array( 'id' => 'mw-editform-cancel' ), + $cancelParams, + array( 'known', 'noclasses' ) + ); + } + /** * Get a diff between the current contents of the edit box and the * version of the page we're editing from. @@ -2367,6 +2498,13 @@ END : $text; } + function safeUnicodeText( $request, $text ) { + $text = rtrim( $text ); + return $request->getBool( 'safemode' ) + ? $this->unmakesafe( $text ) + : $text; + } + /** * Filter an output field through a Unicode armoring process if it is * going to an old browser with known broken Unicode editing issues. diff --git a/includes/Html.php b/includes/Html.php index 4ecfcba9c6..7357eb93fb 100644 --- a/includes/Html.php +++ b/includes/Html.php @@ -486,4 +486,29 @@ class Html { public static function hidden( $name, $value, $attribs = array() ) { return self::input( $name, $value, 'hidden', $attribs ); } + + /** + * Convenience function to produce an element. This supports leaving + * out the cols= and rows= which Xml requires and are required by HTML4/XHTML + * but not required by HTML5 and will silently set cols="" and rows="" if + * $wgHtml5 is false and cols and rows are omitted (HTML4 validates present + * but empty cols="" and rows="" as valid). + * + * @param $name string name attribute + * @param $value string value attribute + * @param $attribs array Associative array of miscellaneous extra + * attributes, passed to Html::element() + * @return string Raw HTML + */ + public static function textarea( $name, $value = '', $attribs = array() ) { + global $wgHtml5; + $attribs['name'] = $name; + if ( !$wgHtml5 ) { + if ( !array_key_exists('cols', $attribs) ) + $attribs['cols'] = ""; + if ( !array_key_exists('rows', $attribs) ) + $attribs['rows'] = ""; + } + return self::element( 'textarea', $attribs, $value ); + } } diff --git a/languages/messages/MessagesEn.php b/languages/messages/MessagesEn.php index 5dd57f051b..295f723c82 100644 --- a/languages/messages/MessagesEn.php +++ b/languages/messages/MessagesEn.php @@ -1338,6 +1338,8 @@ The administrator who locked it offered this explanation: $1", 'nocreatetext' => '{{SITENAME}} has restricted the ability to create new pages. You can go back and edit an existing page, or [[Special:UserLogin|log in or create an account]].', 'nocreate-loggedin' => 'You do not have permission to create new pages.', +'sectioneditnotsupported-title' => 'Section editing not supported', +'sectioneditnotsupported-text' => 'Section editing is not supported in this edit page.', 'permissionserrors' => 'Permissions Errors', 'permissionserrorstext' => 'You do not have permission to do that, for the following {{PLURAL:$1|reason|reasons}}:', 'permissionserrorstext-withaction' => 'You do not have permission to $2, for the following {{PLURAL:$1|reason|reasons}}:', diff --git a/skins/common/edit.js b/skins/common/edit.js index a36e5db440..17bf113785 100644 --- a/skins/common/edit.js +++ b/skins/common/edit.js @@ -5,13 +5,13 @@ var currentFocused; function addButton(imageFile, speedTip, tagOpen, tagClose, sampleText, imageId) { // Don't generate buttons for browsers which don't fully // support it. - mwEditButtons[mwEditButtons.length] = + mwEditButtons.push( {"imageId": imageId, "imageFile": imageFile, "speedTip": speedTip, "tagOpen": tagOpen, "tagClose": tagClose, - "sampleText": sampleText}; + "sampleText": sampleText}); } // this function generates the actual toolbar buttons with localized text @@ -44,11 +44,9 @@ function mwSetupToolbar() { var toolbar = document.getElementById('toolbar'); if (!toolbar) { return false; } - var textbox = document.getElementById('wpTextbox1'); - if (!textbox) { return false; } - // Don't generate buttons for browsers which don't fully // support it. + var textbox = document.createElement('textarea'); // abstract, don't assume wpTextbox1 is always there if (!(document.selection && document.selection.createRange) && textbox.selectionStart === null) { return false; @@ -154,17 +152,13 @@ function scrollEditBox() { if( scrollTop.value ) editBox.scrollTop = scrollTop.value; addHandler( editForm, 'submit', function() { - document.getElementById( 'wpScrolltop' ).value = document.getElementById( 'wpTextbox1' ).scrollTop; + scrollTop.value = editBox.scrollTop; } ); } } hookEvent( 'load', scrollEditBox ); hookEvent( 'load', mwSetupToolbar ); hookEvent( 'load', function() { - if ( document.editform ) { - currentFocused = document.editform.wpTextbox1; - document.editform.wpTextbox1.onfocus = function() { currentFocused = document.editform.wpTextbox1; }; - document.editform.wpSummary.onfocus = function() { currentFocused = document.editform.wpSummary; }; - } + currentFocused = document.getElementById( 'wpTextbox1' ); } ); -- 2.20.1