* @file
*/
+use MediaWiki\Logger\LoggerFactory;
+
/**
* The edit page/HTML interface (split from Article)
* The actual database and text munging is still in Article,
/** @var bool */
public $tooBig = false;
- /** @var bool */
- public $kblength = false;
-
/** @var bool */
public $missingComment = false;
/** @var string */
public $edittime = '';
+ /** @var integer */
+ private $editRevId = null;
+
/** @var string */
public $section = '';
/** @var bool */
protected $edit;
+ /** @var bool|int */
+ protected $contentLength = false;
+
/**
* @var bool Set in ApiEditPage, based on ContentHandler::allowsDirectApiEditing
*/
$this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
$this->edittime = $request->getVal( 'wpEdittime' );
+ $this->editRevId = $request->getIntOrNull( 'editRevId' );
$this->starttime = $request->getVal( 'wpStarttime' );
$undidRev = $request->getInt( 'wpUndidRevision' );
$this->summary = '';
$this->sectiontitle = '';
$this->edittime = '';
+ $this->editRevId = null;
$this->starttime = wfTimestampNow();
$this->edit = false;
$this->preview = false;
function initialiseForm() {
global $wgUser;
$this->edittime = $this->page->getTimestamp();
+ $this->editRevId = $this->page->getLatest();
$content = $this->getContentObject( false ); # TODO: track content object?!
if ( $content === false ) {
return $handler->makeEmptyContent();
} else {
- # nasty side-effect, but needed for consistency
- $this->contentModel = $rev->getContentModel();
- $this->contentFormat = $rev->getContentFormat();
+ // Content models should always be the same since we error
+ // out if they are different before this point.
+ $logger = LoggerFactory::getInstance( 'editpage' );
+ if ( $this->contentModel !== $rev->getContentModel() ) {
+ $logger->warning( "Overriding content model from current edit {prev} to {new}", [
+ 'prev' => $this->contentModel,
+ 'new' => $rev->getContentModel(),
+ 'title' => $this->getTitle()->getPrefixedDBkey(),
+ 'method' => __METHOD__
+ ] );
+ $this->contentModel = $rev->getContentModel();
+ }
+
+ // Given that the content models should match, the current selected
+ // format should be supported.
+ if ( !$content->isSupportedFormat( $this->contentFormat ) ) {
+ $logger->warning( "Current revision content format unsupported. Overriding {prev} to {new}", [
+
+ 'prev' => $this->contentFormat,
+ 'new' => $rev->getContentFormat(),
+ 'title' => $this->getTitle()->getPrefixedDBkey(),
+ 'method' => __METHOD__
+ ] );
+ $this->contentFormat = $rev->getContentFormat();
+ }
return $content;
}
return $this->mPreloadContent;
}
- $handler = ContentHandler::getForTitle( $this->getTitle() );
+ $handler = ContentHandler::getForModelID( $this->contentModel );
if ( $preload === '' ) {
return $handler->makeEmptyContent();
return $status;
}
- $this->kblength = (int)( strlen( $this->textbox1 ) / 1024 );
- if ( $this->kblength > $wgMaxArticleSize ) {
+ $this->contentLength = strlen( $this->textbox1 );
+ if ( $this->contentLength > $wgMaxArticleSize * 1024 ) {
// Error will be displayed by showEditForm()
$this->tooBig = true;
$status->setResult( false, self::AS_CONTENT_TOO_BIG );
$this->page->clear(); # Force reload of dates, etc.
$timestamp = $this->page->getTimestamp();
+ $latest = $this->page->getLatest();
wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
- if ( $timestamp != $this->edittime ) {
+ // Check editRevId if set, which handles same-second timestamp collisions
+ if ( $timestamp != $this->edittime
+ || ( $this->editRevId !== null && $this->editRevId != $latest )
+ ) {
$this->isConflict = true;
if ( $this->section == 'new' ) {
if ( $this->page->getUserText() == $wgUser->getName() &&
if ( $this->isConflict ) {
wfDebug( __METHOD__
. ": conflict! getting section '{$this->section}' for time '{$this->edittime}'"
- . " (article time '{$timestamp}')\n" );
-
- $content = $this->page->replaceSectionContent(
- $this->section,
- $textbox_content,
- $sectionTitle,
- $this->edittime
- );
+ . " (id '{$this->editRevId}') (article time '{$timestamp}')\n" );
+ // @TODO: replaceSectionAtRev() with base ID (not prior current) for ?oldid=X case
+ // ...or disable section editing for non-current revisions (not exposed anyway).
+ if ( $this->editRevId !== null ) {
+ $content = $this->page->replaceSectionAtRev(
+ $this->section,
+ $textbox_content,
+ $sectionTitle,
+ $this->editRevId
+ );
+ } else {
+ $content = $this->page->replaceSectionContent(
+ $this->section,
+ $textbox_content,
+ $sectionTitle,
+ $this->edittime
+ );
+ }
} else {
wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" );
$content = $this->page->replaceSectionContent(
}
// Check for length errors again now that the section is merged in
- $this->kblength = (int)( strlen( $this->toEditText( $content ) ) / 1024 );
- if ( $this->kblength > $wgMaxArticleSize ) {
+ $this->contentLength = strlen( $this->toEditText( $content ) );
+ if ( $this->contentLength > $wgMaxArticleSize * 1024 ) {
$this->tooBig = true;
$status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
return $status;
function getBaseRevision() {
if ( !$this->mBaseRevision ) {
$db = wfGetDB( DB_MASTER );
- $this->mBaseRevision = Revision::loadFromTimestamp(
- $db, $this->mTitle, $this->edittime );
+ $this->mBaseRevision = $this->editRevId
+ ? Revision::newFromId( $this->editRevId, Revision::READ_LATEST )
+ : Revision::loadFromTimestamp( $db, $this->mTitle, $this->edittime );
}
return $this->mBaseRevision;
}
if ( $this->isConflict ) {
$wgOut->wrapWikiMsg( "<div class='mw-explainconflict'>\n$1\n</div>", 'explainconflict' );
- $this->edittime = $this->page->getTimestamp();
+ $this->editRevId = $this->page->getLatest();
} else {
if ( $this->section != '' && !$this->isSectionEditSupported() ) {
// We use $this->section to much before this and getVal('wgSection') directly in other places
'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ] );
}
- if ( $this->kblength === false ) {
- $this->kblength = (int)( strlen( $this->textbox1 ) / 1024 );
+ if ( $this->contentLength === false ) {
+ $this->contentLength = strlen( $this->textbox1 );
}
- if ( $this->tooBig || $this->kblength > $wgMaxArticleSize ) {
+ if ( $this->tooBig || $this->contentLength > $wgMaxArticleSize * 1024 ) {
$wgOut->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
[
'longpageerror',
- $wgLang->formatNum( $this->kblength ),
+ $wgLang->formatNum( round( $this->contentLength / 1024, 3 ) ),
$wgLang->formatNum( $wgMaxArticleSize )
]
);
* @param string $summary The text of the summary to display
*/
protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
- global $wgOut, $wgContLang;
+ global $wgOut;
# Add a class if 'missingsummary' is triggered to allow styling of the summary line
$summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
if ( $isSubjectPreview ) {
return;
}
}
- $summary = $wgContLang->recodeForEdit( $summary );
$labelText = wfMessage( $isSubjectPreview ? 'subject' : 'summary' )->parse();
list( $label, $input ) = $this->getSummaryInput(
$summary,
<input type='hidden' value="{$section}" name="wpSection"/>
<input type='hidden' value="{$this->starttime}" name="wpStarttime" />
<input type='hidden' value="{$this->edittime}" name="wpEdittime" />
+<input type='hidden' value="{$this->editRevId}" name="editRevId" />
<input type='hidden' value="{$this->scrolltop}" name="wpScrolltop" id="wpScrolltop" />
HTML
}
$textboxContent = $this->toEditContent( $this->textbox1 );
-
- $newContent = $this->page->replaceSectionContent(
- $this->section, $textboxContent,
- $this->summary, $this->edittime );
+ if ( $this->editRevId !== null ) {
+ $newContent = $this->page->replaceSectionAtRev(
+ $this->section, $textboxContent, $this->summary, $this->editRevId
+ );
+ } else {
+ $newContent = $this->page->replaceSectionContent(
+ $this->section, $textboxContent, $this->summary, $this->edittime
+ );
+ }
if ( $newContent ) {
ContentHandler::runLegacyHooks( 'EditPageGetDiffText', [ $this, &$newContent ] );
if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$this, &$wgOut ] ) ) {
$stats = $wgOut->getContext()->getStats();
$stats->increment( 'edit.failures.conflict' );
+ // Only include 'standard' namespaces to avoid creating unknown numbers of statsd metrics
+ if (
+ $this->mTitle->getNamespace() >= NS_MAIN &&
+ $this->mTitle->getNamespace() <= NS_CATEGORY_TALK
+ ) {
+ $stats->increment( 'edit.failures.conflict.byNamespaceId.' . $this->mTitle->getNamespace() );
+ }
$wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
* @return string
*/
function getPreviewText() {
- global $wgOut, $wgUser, $wgRawHtml, $wgLang;
+ global $wgOut, $wgRawHtml, $wgLang;
global $wgAllowUserCss, $wgAllowUserJs;
$stats = $wgOut->getContext()->getStats();
$note = wfMessage( 'previewnote' )->plain() . ' ' . $continueEditing;
}
- $parserOptions = $this->page->makeParserOptions( $this->mArticle->getContext() );
- $parserOptions->setIsPreview( true );
- $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
-
# don't parse non-wikitext pages, show message about preview
if ( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
if ( $this->mTitle->isCssJsSubpage() ) {
ContentHandler::runLegacyHooks( 'EditPageGetPreviewText', $hook_args );
Hooks::run( 'EditPageGetPreviewContent', $hook_args );
- $parserOptions->enableLimitReport();
-
- # For CSS/JS pages, we should have called the ShowRawCssJs hook here.
- # But it's now deprecated, so never mind
-
- $pstContent = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
- $scopedCallback = $parserOptions->setupFakeRevision(
- $this->mTitle, $pstContent, $wgUser );
- $parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
-
- $parserOutput->setEditSectionTokens( false ); // no section edit links
- $previewHTML = $parserOutput->getText();
+ $parserResult = $this->doPreviewParse( $content );
+ $parserOutput = $parserResult['parserOutput'];
+ $previewHTML = $parserResult['html'];
$this->mParserOutput = $parserOutput;
$wgOut->addParserOutputMetadata( $parserOutput );
$note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
}
- ScopedCallback::consume( $scopedCallback );
} catch ( MWContentSerializationException $ex ) {
$m = wfMessage(
'content-failed-to-parse',
return $previewhead . $previewHTML . $this->previewTextAfterContent;
}
+ /**
+ * Get parser options for a preview
+ * @return ParserOptions
+ */
+ protected function getPreviewParserOptions() {
+ $parserOptions = $this->page->makeParserOptions( $this->mArticle->getContext() );
+ $parserOptions->setIsPreview( true );
+ $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
+ $parserOptions->enableLimitReport();
+ return $parserOptions;
+ }
+
+ /**
+ * Parse the page for a preview. Subclasses may override this class, in order
+ * to parse with different options, or to otherwise modify the preview HTML.
+ *
+ * @param Content @content The page content
+ * @return Associative array with keys:
+ * - parserOutput: The ParserOutput object
+ * - html: The HTML to be displayed
+ */
+ protected function doPreviewParse( Content $content ) {
+ global $wgUser;
+ $parserOptions = $this->getPreviewParserOptions();
+ $pstContent = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
+ $scopedCallback = $parserOptions->setupFakeRevision(
+ $this->mTitle, $pstContent, $wgUser );
+ $parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
+ ScopedCallback::consume( $scopedCallback );
+ $parserOutput->setEditSectionTokens( false ); // no section edit links
+ return [
+ 'parserOutput' => $parserOutput,
+ 'html' => $parserOutput->getText() ];
+ }
+
/**
* @return array
*/
* @return string
*/
protected function safeUnicodeOutput( $text ) {
- global $wgContLang;
- $codedText = $wgContLang->recodeForEdit( $text );
return $this->checkUnicodeCompliantBrowser()
- ? $codedText
- : $this->makeSafe( $codedText );
+ ? $text
+ : $this->makesafe( $text );
}
/**