X-Git-Url: https://git.cyclocoop.org/%242?a=blobdiff_plain;ds=sidebyside;f=includes%2FEditPage.php;h=48da565e1d2f9565d59918fa250a063a75d03f14;hb=b801fa8b3b3b42fb6db9129d4642f6e583b56879;hp=6e6f825f817e837f340f762b613ae27b0dd9a3a4;hpb=50ee9585c0513b3319d70cdeb1b92161cbcfcf32;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/EditPage.php b/includes/EditPage.php index 6e6f825f81..48da565e1d 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -155,6 +155,11 @@ class EditPage { */ const AS_IMAGE_REDIRECT_LOGGED = 234; + /** + * Status: can't parse content + */ + const AS_PARSE_ERROR = 240; + /** * HTML id and name for the beginning of the edit form. */ @@ -214,6 +219,7 @@ class EditPage { var $textbox1 = '', $textbox2 = '', $summary = '', $nosummary = false; var $edittime = '', $section = '', $sectiontitle = '', $starttime = ''; var $oldid = 0, $editintro = '', $scrolltop = null, $bot = true; + var $content_model = null, $content_format = null; # Placeholders for text injection by hooks (must be HTML) # extensions should take care to _append_ to the present value @@ -225,7 +231,7 @@ class EditPage { public $editFormTextBottom = ''; public $editFormTextAfterContent = ''; public $previewTextAfterContent = ''; - public $mPreloadText = ''; + public $mPreloadContent = null; /* $didSave should be set to true whenever an article was succesfully altered. */ public $didSave = false; @@ -239,6 +245,11 @@ class EditPage { public function __construct( Article $article ) { $this->mArticle = $article; $this->mTitle = $article->getTitle(); + + $this->content_model = $this->mTitle->getContentModel(); + + $handler = ContentHandler::getForModelID( $this->content_model ); + $this->content_format = $handler->getDefaultFormat(); #NOTE: should be overridden by format of actual revision } /** @@ -450,10 +461,10 @@ class EditPage { return; } - $content = $this->getContent(); + $content = $this->getContentObject(); # Use the normal message if there's nothing to display - if ( $this->firsttime && $content === '' ) { + if ( $this->firsttime && $content->isEmpty() ) { $action = $this->mTitle->exists() ? 'edit' : ( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' ); throw new PermissionsError( $action, $permErrors ); @@ -467,13 +478,14 @@ class EditPage { # If the user made changes, preserve them when showing the markup # (This happens when a user is blocked during edit, for instance) if ( !$this->firsttime ) { - $content = $this->textbox1; + $text = $this->textbox1; $wgOut->addWikiMsg( 'viewyourtext' ); } else { + $text = $content->serialize( $this->content_format ); $wgOut->addWikiMsg( 'viewsourcetext' ); } - $this->showTextbox( $content, 'wpTextbox1', array( 'readonly' ) ); + $this->showTextbox( $text, 'wpTextbox1', array( 'readonly' ) ); $wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'templatesUsed' ), Linker::formatTemplates( $this->getTemplates() ) ) ); @@ -679,7 +691,7 @@ class EditPage { } else { # Not a posted form? Start with nothing. wfDebug( __METHOD__ . ": Not a posted form.\n" ); - $this->textbox1 = ''; + $this->textbox1 = ''; #FIXME: track content object $this->summary = ''; $this->sectiontitle = ''; $this->edittime = ''; @@ -711,10 +723,17 @@ class EditPage { } } + $this->oldid = $request->getInt( 'oldid' ); + $this->bot = $request->getBool( 'bot', true ); $this->nosummary = $request->getBool( 'nosummary' ); - $this->oldid = $request->getInt( 'oldid' ); + $content_handler = ContentHandler::getForTitle( $this->mTitle ); + $this->content_model = $request->getText( 'model', $content_handler->getModelID() ); #may be overridden by revision + $this->content_format = $request->getText( 'format', $content_handler->getDefaultFormat() ); #may be overridden by revision + + #TODO: check if the desired model is allowed in this namespace, and if a transition from the page's current model to the new model is allowed + #TODO: check if the desired content model supports the given content format! $this->live = $request->getCheck( 'live' ); $this->editintro = $request->getText( 'editintro', @@ -747,7 +766,10 @@ class EditPage { function initialiseForm() { global $wgUser; $this->edittime = $this->mArticle->getTimestamp(); - $this->textbox1 = $this->getContent( false ); + + $content = $this->getContentObject( false ); #TODO: track content object?! + $this->textbox1 = $content->serialize( $this->content_format ); + // activate checkboxes if user wants them to be always active # Sort out the "watch" checkbox if ( $wgUser->getOption( 'watchdefault' ) ) { @@ -776,33 +798,54 @@ class EditPage { * @param $def_text string * @return mixed string on success, $def_text for invalid sections * @private + * @deprecated since 1.WD */ - function getContent( $def_text = '' ) { - global $wgOut, $wgRequest, $wgParser; + function getContent( $def_text = false ) { #FIXME: deprecated, replace usage! + wfDeprecated( __METHOD__, '1.WD' ); + + if ( $def_text !== null && $def_text !== false && $def_text !== '' ) { + $def_content = ContentHandler::makeContent( $def_text, $this->getTitle() ); + } else { + $def_content = false; + } + + $content = $this->getContentObject( $def_content ); + + return $content->serialize( $this->content_format ); #XXX: really use serialized form? use ContentHandler::getContentText() instead? + } + + private function getContentObject( $def_content = null ) { #FIXME: use this! + global $wgOut, $wgRequest; wfProfileIn( __METHOD__ ); - $text = false; + $content = false; // For message page not locally set, use the i18n message. // For other non-existent articles, use preload text if any. if ( !$this->mTitle->exists() || $this->section == 'new' ) { if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) { # If this is a system message, get the default text. - $text = $this->mTitle->getDefaultMessageText(); + $msg = $this->mTitle->getDefaultMessageText(); + + $content = ContentHandler::makeContent( $msg, $this->mTitle ); } - if ( $text === false ) { + if ( $content === false ) { # If requested, preload some text. $preload = $wgRequest->getVal( 'preload', // Custom preload text for new sections $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' ); - $text = $this->getPreloadedText( $preload ); + + $content = $this->getPreloadedContent( $preload ); } // For existing pages, get text based on "undo" or section parameters. } else { if ( $this->section != '' ) { // Get section edit text (returns $def_text for invalid sections) - $text = $wgParser->getSection( $this->getOriginalContent(), $this->section, $def_text ); + $orig = $this->getOriginalContent(); + $content = $orig ? $orig->getSection( $this->section ) : null; + + if ( !$content ) $content = $def_content; } else { $undoafter = $wgRequest->getInt( 'undoafter' ); $undo = $wgRequest->getInt( 'undo' ); @@ -818,15 +861,16 @@ class EditPage { # Sanity check, make sure it's the right page, # the revisions exist and they were not deleted. - # Otherwise, $text will be left as-is. + # Otherwise, $content will be left as-is. if ( !is_null( $undorev ) && !is_null( $oldrev ) && $undorev->getPage() == $oldrev->getPage() && $undorev->getPage() == $this->mTitle->getArticleID() && !$undorev->isDeleted( Revision::DELETED_TEXT ) && !$oldrev->isDeleted( Revision::DELETED_TEXT ) ) { - $text = $this->mArticle->getUndoText( $undorev, $oldrev ); - if ( $text === false ) { + $content = $this->mArticle->getUndoContent( $undorev, $oldrev ); + + if ( $content === false ) { # Warn the user that something went wrong $undoMsg = 'failure'; } else { @@ -858,14 +902,14 @@ class EditPage { wfMsgNoTrans( 'undo-' . $undoMsg ) . '', true, /* interface */true ); } - if ( $text === false ) { - $text = $this->getOriginalContent(); + if ( $content === false ) { + $content = $this->getOriginalContent(); } } } wfProfileOut( __METHOD__ ); - return $text; + return $content; } /** @@ -884,39 +928,69 @@ class EditPage { */ private function getOriginalContent() { if ( $this->section == 'new' ) { - return $this->getCurrentText(); + return $this->getCurrentContent(); } $revision = $this->mArticle->getRevisionFetched(); if ( $revision === null ) { - return ''; + if ( !$this->content_model ) $this->content_model = $this->getTitle()->getContentModel(); + $handler = ContentHandler::getForModelID( $this->content_model ); + + return $handler->makeEmptyContent(); } - return $this->mArticle->getContent(); + $content = $revision->getContent(); + return $content; } /** - * Get the actual text of the page. This is basically similar to - * WikiPage::getRawText() except that when the page doesn't exist an empty - * string is returned instead of false. + * Get the current content of the page. This is basically similar to + * WikiPage::getContent( Revision::RAW ) except that when the page doesn't exist an empty + * content object is returned instead of null. * - * @since 1.19 + * @since 1.WD * @return string */ - private function getCurrentText() { - $text = $this->mArticle->getRawText(); - if ( $text === false ) { - return ''; + private function getCurrentContent() { + $rev = $this->mArticle->getRevision(); + $content = $rev ? $rev->getContent( Revision::RAW ) : null; + + if ( $content === false || $content === null ) { + if ( !$this->content_model ) $this->content_model = $this->getTitle()->getContentModel(); + $handler = ContentHandler::getForModelID( $this->content_model ); + + return $handler->makeEmptyContent(); } else { - return $text; + #FIXME: nasty side-effect! + $this->content_model = $rev->getContentModel(); + $this->content_format = $rev->getContentFormat(); + + return $content; } } + /** * Use this method before edit() to preload some text into the edit box * * @param $text string + * @deprecated since 1.WD */ public function setPreloadedText( $text ) { - $this->mPreloadText = $text; + wfDeprecated( __METHOD__, "1.WD" ); + + $content = ContentHandler::makeContent( $text, $this->getTitle() ); + + $this->setPreloadedContent( $content ); + } + + /** + * Use this method before edit() to preload some content into the edit box + * + * @param $content Content + * + * @since 1.WD + */ + public function setPreloadedContent( Content $content ) { + $this->mPreloadedContent = $content; } /** @@ -924,23 +998,47 @@ class EditPage { * an earlier setPreloadText() or by loading the given page. * * @param $preload String: representing the title to preload from. + * * @return String + * + * @deprecated since 1.WD, use getPreloadedContent() instead */ - protected function getPreloadedText( $preload ) { - global $wgUser, $wgParser; + protected function getPreloadedText( $preload ) { #NOTE: B/C only, replace usage! + wfDeprecated( __METHOD__, "1.WD" ); + + $content = $this->getPreloadedContent( $preload ); + $text = $content->serialize( $this->content_format ); #XXX: really use serialized form? use ContentHandler::getContentText() instead?! - if ( !empty( $this->mPreloadText ) ) { - return $this->mPreloadText; + return $text; + } + + /** + * Get the contents to be preloaded into the box, either set by + * an earlier setPreloadText() or by loading the given page. + * + * @param $preload String: representing the title to preload from. + * + * @return Content + * + * @since 1.WD + */ + protected function getPreloadedContent( $preload ) { #@todo: use this! + global $wgUser; + + if ( !empty( $this->mPreloadContent ) ) { + return $this->mPreloadContent; } + $handler = ContentHandler::getForTitle( $this->getTitle() ); + if ( $preload === '' ) { - return ''; + return $handler->makeEmptyContent(); } $title = Title::newFromText( $preload ); # Check for existence to avoid getting MediaWiki:Noarticletext if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) { - return ''; + return $handler->makeEmptyContent(); } $page = WikiPage::factory( $title ); @@ -948,13 +1046,15 @@ class EditPage { $title = $page->getRedirectTarget(); # Same as before if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) { - return ''; + return $handler->makeEmptyContent(); } $page = WikiPage::factory( $title ); } $parserOptions = ParserOptions::newFromUser( $wgUser ); - return $wgParser->getPreloadText( $page->getRawText(), $title, $parserOptions ); + $content = $page->getContent( Revision::RAW ); + + return $content->preloadTransform( $title, $parserOptions ); } /** @@ -1002,6 +1102,11 @@ class EditPage { case self::AS_HOOK_ERROR: return false; + case self::AS_PARSE_ERROR: + $wgOut->addWikiText( '
' . $status->getWikiText() . '
'); + #FIXME: cause editform to be shown again, not just an error! + return false; + case self::AS_SUCCESS_NEW_ARTICLE: $query = $resultDetails['redirect'] ? 'redirect=no' : ''; $anchor = isset ( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : ''; @@ -1096,7 +1201,7 @@ class EditPage { # Check image redirect if ( $this->mTitle->getNamespace() == NS_FILE && - Title::newFromRedirect( $this->textbox1 ) instanceof Title && + Title::newFromRedirect( $this->textbox1 ) instanceof Title && #FIXME: use content handler to check for redirect !$wgUser->isAllowed( 'upload' ) ) { $code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED; $status->setResult( false, $code ); @@ -1206,262 +1311,281 @@ class EditPage { $this->mArticle->loadPageData( 'forupdate' ); $new = !$this->mArticle->exists(); - if ( $new ) { - // Late check for create permission, just in case *PARANOIA* - if ( !$this->mTitle->userCan( 'create' ) ) { - $status->fatal( 'nocreatetext' ); - $status->value = self::AS_NO_CREATE_PERMISSION; - wfDebug( __METHOD__ . ": no create permission\n" ); - wfProfileOut( __METHOD__ ); - return $status; - } + try { + if ( $new ) { + // Late check for create permission, just in case *PARANOIA* + if ( !$this->mTitle->userCan( 'create' ) ) { + $status->fatal( 'nocreatetext' ); + $status->value = self::AS_NO_CREATE_PERMISSION; + wfDebug( __METHOD__ . ": no create permission\n" ); + wfProfileOut( __METHOD__ ); + return $status; + } - # Don't save a new article if it's blank. - if ( $this->textbox1 == '' ) { - $status->setResult( false, self::AS_BLANK_ARTICLE ); - wfProfileOut( __METHOD__ ); - return $status; - } + # Don't save a new article if it's blank. + if ( $this->textbox1 == '' ) { + $status->setResult( false, self::AS_BLANK_ARTICLE ); + wfProfileOut( __METHOD__ ); + return $status; + } - // Run post-section-merge edit filter - if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError, $this->summary ) ) ) { - # Error messages etc. could be handled within the hook... - $status->fatal( 'hookaborted' ); - $status->value = self::AS_HOOK_ERROR; - wfProfileOut( __METHOD__ ); - return $status; - } elseif ( $this->hookError != '' ) { - # ...or the hook could be expecting us to produce an error - $status->fatal( 'hookaborted' ); - $status->value = self::AS_HOOK_ERROR_EXPECTED; - wfProfileOut( __METHOD__ ); - return $status; - } + // Run post-section-merge edit filter + if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError, $this->summary ) ) ) { + # Error messages etc. could be handled within the hook... + $status->fatal( 'hookaborted' ); + $status->value = self::AS_HOOK_ERROR; + wfProfileOut( __METHOD__ ); + return $status; + } elseif ( $this->hookError != '' ) { + # ...or the hook could be expecting us to produce an error + $status->fatal( 'hookaborted' ); + $status->value = self::AS_HOOK_ERROR_EXPECTED; + wfProfileOut( __METHOD__ ); + return $status; + } - $text = $this->textbox1; - $result['sectionanchor'] = ''; - if ( $this->section == 'new' ) { - if ( $this->sectiontitle !== '' ) { - // Insert the section title above the content. - $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->sectiontitle ) . "\n\n" . $text; - - // Jump to the new section - $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle ); - - // If no edit summary was specified, create one automatically from the section - // title and have it link to the new section. Otherwise, respect the summary as - // passed. - if ( $this->summary === '' ) { - $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle ); - $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle ); - } - } elseif ( $this->summary !== '' ) { - // Insert the section title above the content. - $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->summary ) . "\n\n" . $text; + $content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format ); - // Jump to the new section - $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->summary ); + $result['sectionanchor'] = ''; + if ( $this->section == 'new' ) { + if ( $this->sectiontitle !== '' ) { + // Insert the section title above the content. + $content = $content->addSectionHeader( $this->sectiontitle ); + + // Jump to the new section + $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle ); + + // If no edit summary was specified, create one automatically from the section + // title and have it link to the new section. Otherwise, respect the summary as + // passed. + if ( $this->summary === '' ) { + $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle ); + $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle ); + } + } elseif ( $this->summary !== '' ) { + // Insert the section title above the content. + $content = $content->addSectionHeader( $this->sectiontitle ); - // Create a link to the new section from the edit summary. - $cleanSummary = $wgParser->stripSectionName( $this->summary ); - $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary ); + // Jump to the new section + $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->summary ); + + // Create a link to the new section from the edit summary. + $cleanSummary = $wgParser->stripSectionName( $this->summary ); + $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary ); + } } - } - $status->value = self::AS_SUCCESS_NEW_ARTICLE; + $status->value = self::AS_SUCCESS_NEW_ARTICLE; - } else { + } else { # not $new - # Article exists. Check for edit conflict. - $timestamp = $this->mArticle->getTimestamp(); - wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" ); + # Article exists. Check for edit conflict. - if ( $timestamp != $this->edittime ) { - $this->isConflict = true; - if ( $this->section == 'new' ) { - if ( $this->mArticle->getUserText() == $wgUser->getName() && - $this->mArticle->getComment() == $this->summary ) { - // Probably a duplicate submission of a new comment. - // This can happen when squid resends a request after - // a timeout but the first one actually went through. - wfDebug( __METHOD__ . ": duplicate new section submission; trigger edit conflict!\n" ); - } else { - // New comment; suppress conflict. + $this->mArticle->clear(); # Force reload of dates, etc. + $timestamp = $this->mArticle->getTimestamp(); + + wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" ); + + if ( $timestamp != $this->edittime ) { + $this->isConflict = true; + if ( $this->section == 'new' ) { + if ( $this->mArticle->getUserText() == $wgUser->getName() && + $this->mArticle->getComment() == $this->summary ) { + // Probably a duplicate submission of a new comment. + // This can happen when squid resends a request after + // a timeout but the first one actually went through. + wfDebug( __METHOD__ . ": duplicate new section submission; trigger edit conflict!\n" ); + } else { + // New comment; suppress conflict. + $this->isConflict = false; + wfDebug( __METHOD__ . ": conflict suppressed; new section\n" ); + } + } elseif ( $this->section == '' && $this->userWasLastToEdit( $wgUser->getId(), $this->edittime ) ) { + # Suppress edit conflict with self, except for section edits where merging is required. + wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" ); $this->isConflict = false; - wfDebug( __METHOD__ . ": conflict suppressed; new section\n" ); } - } elseif ( $this->section == '' && $this->userWasLastToEdit( $wgUser->getId(), $this->edittime ) ) { - # Suppress edit conflict with self, except for section edits where merging is required. - wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" ); - $this->isConflict = false; } - } - - // If sectiontitle is set, use it, otherwise use the summary as the section title (for - // backwards compatibility with old forms/bots). - if ( $this->sectiontitle !== '' ) { - $sectionTitle = $this->sectiontitle; - } else { - $sectionTitle = $this->summary; - } - if ( $this->isConflict ) { - wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '{$timestamp}')\n" ); - $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $sectionTitle, $this->edittime ); - } else { - wfDebug( __METHOD__ . ": getting section '$this->section'\n" ); - $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $sectionTitle ); - } - if ( is_null( $text ) ) { - wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" ); - $this->isConflict = true; - $text = $this->textbox1; // do not try to merge here! - } elseif ( $this->isConflict ) { - # Attempt merge - if ( $this->mergeChangesInto( $text ) ) { - // Successful merge! Maybe we should tell the user the good news? - $this->isConflict = false; - wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" ); + // If sectiontitle is set, use it, otherwise use the summary as the section title (for + // backwards compatibility with old forms/bots). + if ( $this->sectiontitle !== '' ) { + $sectionTitle = $this->sectiontitle; } else { - $this->section = ''; - $this->textbox1 = $text; - wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" ); + $sectionTitle = $this->summary; } - } - if ( $this->isConflict ) { - $status->setResult( false, self::AS_CONFLICT_DETECTED ); - wfProfileOut( __METHOD__ ); - return $status; - } + $textbox_content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format ); + $content = null; - // Run post-section-merge edit filter - if ( !wfRunHooks( 'EditFilterMerged', array( $this, $text, &$this->hookError, $this->summary ) ) ) { - # Error messages etc. could be handled within the hook... - $status->fatal( 'hookaborted' ); - $status->value = self::AS_HOOK_ERROR; - wfProfileOut( __METHOD__ ); - return $status; - } elseif ( $this->hookError != '' ) { - # ...or the hook could be expecting us to produce an error - $status->fatal( 'hookaborted' ); - $status->value = self::AS_HOOK_ERROR_EXPECTED; - wfProfileOut( __METHOD__ ); - return $status; - } + if ( $this->isConflict ) { + wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '{$timestamp}')\n" ); + $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle, $this->edittime ); + } else { + wfDebug( __METHOD__ . ": getting section '$this->section'\n" ); + $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle ); + } - # Handle the user preference to force summaries here, but not for null edits - if ( $this->section != 'new' && !$this->allowBlankSummary - && $this->getOriginalContent() != $text - && !Title::newFromRedirect( $text ) ) # check if it's not a redirect - { - if ( md5( $this->summary ) == $this->autoSumm ) { - $this->missingSummary = true; - $status->fatal( 'missingsummary' ); - $status->value = self::AS_SUMMARY_NEEDED; - wfProfileOut( __METHOD__ ); - return $status; + if ( is_null( $content ) ) { + wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" ); + $this->isConflict = true; + $content = $textbox_content; // do not try to merge here! + } elseif ( $this->isConflict ) { + # Attempt merge + if ( $this->mergeChangesIntoContent( $textbox_content ) ) { + // Successful merge! Maybe we should tell the user the good news? + $this->isConflict = false; + $content = $textbox_content; + wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" ); + } else { + $this->section = ''; + #$this->textbox1 = $text; #redundant, nothing to do here? + wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" ); + } } - } - # And a similar thing for new sections - if ( $this->section == 'new' && !$this->allowBlankSummary ) { - if ( trim( $this->summary ) == '' ) { - $this->missingSummary = true; - $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh - $status->value = self::AS_SUMMARY_NEEDED; + if ( $this->isConflict ) { + $status->setResult( false, self::AS_CONFLICT_DETECTED ); wfProfileOut( __METHOD__ ); return $status; } - } - # All's well - wfProfileIn( __METHOD__ . '-sectionanchor' ); - $sectionanchor = ''; - if ( $this->section == 'new' ) { - if ( $this->textbox1 == '' ) { - $this->missingComment = true; - $status->fatal( 'missingcommenttext' ); - $status->value = self::AS_TEXTBOX_EMPTY; - wfProfileOut( __METHOD__ . '-sectionanchor' ); + // Run post-section-merge edit filter + if ( !wfRunHooks( 'EditFilterMerged', array( $this, $content->serialize( $this->content_format ), &$this->hookError, $this->summary ) ) + || !wfRunHooks( 'EditFilterMergedContent', array( $this, $content, &$this->hookError, $this->summary ) ) ) { + # Error messages etc. could be handled within the hook... + $status->fatal( 'hookaborted' ); + $status->value = self::AS_HOOK_ERROR; + wfProfileOut( __METHOD__ ); + return $status; + } elseif ( $this->hookError != '' ) { + # ...or the hook could be expecting us to produce an error + $status->fatal( 'hookaborted' ); + $status->value = self::AS_HOOK_ERROR_EXPECTED; wfProfileOut( __METHOD__ ); return $status; } - if ( $this->sectiontitle !== '' ) { - $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle ); - // If no edit summary was specified, create one automatically from the section - // title and have it link to the new section. Otherwise, respect the summary as - // passed. - if ( $this->summary === '' ) { - $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle ); - $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle ); + + $content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format ); + + # Handle the user preference to force summaries here, but not for null edits + if ( $this->section != 'new' && !$this->allowBlankSummary + && !$content->equals( $this->getOriginalContent() ) + && !$content->isRedirect() ) # check if it's not a redirect + { + if ( md5( $this->summary ) == $this->autoSumm ) { + $this->missingSummary = true; + $status->fatal( 'missingsummary' ); + $status->value = self::AS_SUMMARY_NEEDED; + wfProfileOut( __METHOD__ ); + return $status; } - } elseif ( $this->summary !== '' ) { - $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary ); - # This is a new section, so create a link to the new section - # in the revision summary. - $cleanSummary = $wgParser->stripSectionName( $this->summary ); - $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary ); } - } elseif ( $this->section != '' ) { - # Try to get a section anchor from the section source, redirect to edited section if header found - # XXX: might be better to integrate this into Article::replaceSection - # for duplicate heading checking and maybe parsing - $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches ); - # we can't deal with anchors, includes, html etc in the header for now, - # headline would need to be parsed to improve this - if ( $hasmatch && strlen( $matches[2] ) > 0 ) { - $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] ); + + # And a similar thing for new sections + if ( $this->section == 'new' && !$this->allowBlankSummary ) { + if ( trim( $this->summary ) == '' ) { + $this->missingSummary = true; + $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh + $status->value = self::AS_SUMMARY_NEEDED; + wfProfileOut( __METHOD__ ); + return $status; + } } - } - $result['sectionanchor'] = $sectionanchor; - wfProfileOut( __METHOD__ . '-sectionanchor' ); - // Save errors may fall down to the edit form, but we've now - // merged the section into full text. Clear the section field - // so that later submission of conflict forms won't try to - // replace that into a duplicated mess. - $this->textbox1 = $text; - $this->section = ''; + # All's well + wfProfileIn( __METHOD__ . '-sectionanchor' ); + $sectionanchor = ''; + if ( $this->section == 'new' ) { + if ( $this->textbox1 == '' ) { + $this->missingComment = true; + $status->fatal( 'missingcommenttext' ); + $status->value = self::AS_TEXTBOX_EMPTY; + wfProfileOut( __METHOD__ . '-sectionanchor' ); + wfProfileOut( __METHOD__ ); + return $status; + } + if ( $this->sectiontitle !== '' ) { + $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle ); + // If no edit summary was specified, create one automatically from the section + // title and have it link to the new section. Otherwise, respect the summary as + // passed. + if ( $this->summary === '' ) { + $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle ); + $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle ); + } + } elseif ( $this->summary !== '' ) { + $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary ); + # This is a new section, so create a link to the new section + # in the revision summary. + $cleanSummary = $wgParser->stripSectionName( $this->summary ); + $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary ); + } + } elseif ( $this->section != '' ) { + # Try to get a section anchor from the section source, redirect to edited section if header found + # XXX: might be better to integrate this into Article::replaceSection + # for duplicate heading checking and maybe parsing + $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches ); + # we can't deal with anchors, includes, html etc in the header for now, + # headline would need to be parsed to improve this + if ( $hasmatch && strlen( $matches[2] ) > 0 ) { + $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] ); + } + } + $result['sectionanchor'] = $sectionanchor; + wfProfileOut( __METHOD__ . '-sectionanchor' ); - $status->value = self::AS_SUCCESS_UPDATE; - } + // Save errors may fall down to the edit form, but we've now + // merged the section into full text. Clear the section field + // so that later submission of conflict forms won't try to + // replace that into a duplicated mess. + $this->textbox1 = $content->serialize( $this->content_format ); + $this->section = ''; - // Check for length errors again now that the section is merged in - $this->kblength = (int)( strlen( $text ) / 1024 ); - if ( $this->kblength > $wgMaxArticleSize ) { - $this->tooBig = true; - $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED ); - wfProfileOut( __METHOD__ ); - return $status; - } + $status->value = self::AS_SUCCESS_UPDATE; + } + + // Check for length errors again now that the section is merged in + $this->kblength = (int)( strlen( $content->serialize( $this->content_format ) ) / 1024 ); + if ( $this->kblength > $wgMaxArticleSize ) { + $this->tooBig = true; + $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED ); + wfProfileOut( __METHOD__ ); + return $status; + } - $flags = EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY | - ( $new ? EDIT_NEW : EDIT_UPDATE ) | - ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) | - ( $bot ? EDIT_FORCE_BOT : 0 ); + $flags = EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY | + ( $new ? EDIT_NEW : EDIT_UPDATE ) | + ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) | + ( $bot ? EDIT_FORCE_BOT : 0 ); - $doEditStatus = $this->mArticle->doEdit( $text, $this->summary, $flags ); + $doEditStatus = $this->mArticle->doEditContent( $content, $this->summary, $flags, false, null, $this->content_format ); - if ( $doEditStatus->isOK() ) { - $result['redirect'] = Title::newFromRedirect( $text ) !== null; - $this->commitWatch(); - wfProfileOut( __METHOD__ ); - return $status; - } else { - // Failure from doEdit() - // Show the edit conflict page for certain recognized errors from doEdit(), - // but don't show it for errors from extension hooks - $errors = $doEditStatus->getErrorsArray(); - if ( in_array( $errors[0][0], array( 'edit-gone-missing', 'edit-conflict', - 'edit-already-exists' ) ) ) - { - $this->isConflict = true; - // Destroys data doEdit() put in $status->value but who cares - $doEditStatus->value = self::AS_END; + if ( $doEditStatus->isOK() ) { + $result['redirect'] = $content->isRedirect(); + $this->commitWatch(); + wfProfileOut( __METHOD__ ); + return $status; + } else { + // Failure from doEdit() + // Show the edit conflict page for certain recognized errors from doEdit(), + // but don't show it for errors from extension hooks + $errors = $doEditStatus->getErrorsArray(); + if ( in_array( $errors[0][0], array( 'edit-gone-missing', 'edit-conflict', + 'edit-already-exists' ) ) ) + { + $this->isConflict = true; + // Destroys data doEdit() put in $status->value but who cares + $doEditStatus->value = self::AS_END; + } + wfProfileOut( __METHOD__ ); + return $doEditStatus; } + } catch (MWContentSerializationException $ex) { + $status->fatal( 'content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() ); + $status->value = self::AS_PARSE_ERROR; wfProfileOut( __METHOD__ ); - return $doEditStatus; + return $status; } } @@ -1518,8 +1642,33 @@ class EditPage { * @parma $editText string * * @return bool + * @deprecated since 1.WD, use mergeChangesIntoContent() instead */ - function mergeChangesInto( &$editText ) { + function mergeChangesInto( &$editText ){ + wfDebug( __METHOD__, "1.WD" ); + + $editContent = ContentHandler::makeContent( $editText, $this->getTitle(), $this->content_model, $this->content_format ); + + $ok = $this->mergeChangesIntoContent( $editContent ); + + if ( $ok ) { + $editText = $editContent->serialize( $this->content_format ); #XXX: really serialize?! + return true; + } else { + return false; + } + } + + /** + * @private + * @todo document + * + * @parma $editText string + * + * @return bool + * @since since 1.WD + */ + private function mergeChangesIntoContent( &$editContent ){ wfProfileIn( __METHOD__ ); $db = wfGetDB( DB_MASTER ); @@ -1530,7 +1679,7 @@ class EditPage { wfProfileOut( __METHOD__ ); return false; } - $baseText = $baseRevision->getText(); + $baseContent = $baseRevision->getContent(); // The current state, we want to merge updates into it $currentRevision = Revision::loadFromTitle( $db, $this->mTitle ); @@ -1538,11 +1687,14 @@ class EditPage { wfProfileOut( __METHOD__ ); return false; } - $currentText = $currentRevision->getText(); + $currentContent = $currentRevision->getContent(); + + $handler = ContentHandler::getForModelID( $baseContent->getModel() ); - $result = ''; - if ( wfMerge( $baseText, $editText, $currentText, $result ) ) { - $editText = $result; + $result = $handler->merge3( $baseContent, $editContent, $currentContent ); + + if ( $result ) { + $editContent = $result; wfProfileOut( __METHOD__ ); return true; } else { @@ -1789,6 +1941,7 @@ class EditPage { } } + #FIXME: add EditForm plugin interface and use it here! #FIXME: search for textarea1 and textares2, and allow EditForm to override all uses. $wgOut->addHTML( Html::openElement( 'form', array( 'id' => self::EDITFORM_ID, 'name' => self::EDITFORM_ID, 'method' => 'post', 'action' => $this->getActionURL( $this->getContextTitle() ), 'enctype' => 'multipart/form-data' ) ) ); @@ -1849,6 +2002,9 @@ class EditPage { $wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) ); + $wgOut->addHTML( Html::hidden( 'format', $this->content_format ) ); + $wgOut->addHTML( Html::hidden( 'model', $this->content_model ) ); + if ( $this->section == 'new' ) { $this->showSummaryInput( true, $this->summary ); $wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) ); @@ -1866,7 +2022,9 @@ class EditPage { // resolved between page source edits and custom ui edits using the // custom edit ui. $this->textbox2 = $this->textbox1; - $this->textbox1 = $this->getCurrentText(); + + $content = $this->getCurrentContent(); + $this->textbox1 = $content->serialize( $this->content_format ); $this->showTextbox1(); } else { @@ -2274,10 +2432,10 @@ HTML $this->showTextbox( $this->textbox2, 'wpTextbox2', array( 'tabindex' => 6, 'readonly' ) ); } - protected function showTextbox( $content, $name, $customAttribs = array() ) { + protected function showTextbox( $text, $name, $customAttribs = array() ) { global $wgOut, $wgUser; - $wikitext = $this->safeUnicodeOutput( $content ); + $wikitext = $this->safeUnicodeOutput( $text ); if ( strval( $wikitext ) !== '' ) { // Ensure there's a newline at the end, otherwise adding lines // is awkward. @@ -2361,24 +2519,43 @@ HTML $oldtext = $this->mTitle->getDefaultMessageText(); if( $oldtext !== false ) { $oldtitlemsg = 'defaultmessagetext'; + $oldContent = ContentHandler::makeContent( $oldtext, $this->mTitle ); + } else { + $oldContent = null; } } else { - $oldtext = $this->mArticle->getRawText(); + $oldContent = $this->getOriginalContent(); } - $newtext = $this->mArticle->replaceSection( - $this->section, $this->textbox1, $this->summary, $this->edittime ); + $textboxContent = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), + $this->content_model, $this->content_format ); #XXX: handle parse errors ? + + $newContent = $this->mArticle->replaceSectionContent( + $this->section, $textboxContent, + $this->summary, $this->edittime ); + + # hanlde legacy text-based hook + $newtext_orig = $newContent->serialize( $this->content_format ); + $newtext = $newtext_orig; #clone wfRunHooks( 'EditPageGetDiffText', array( $this, &$newtext ) ); + if ( $newtext != $newtext_orig ) { + #if the hook changed the text, create a new Content object accordingly. + $newContent = ContentHandler::makeContent( $newtext, $this->getTitle(), $newContent->getModel() ); #XXX: handle parse errors ? + } + + wfRunHooks( 'EditPageGetDiffContent', array( $this, &$newContent ) ); + $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang ); - $newtext = $wgParser->preSaveTransform( $newtext, $this->mTitle, $wgUser, $popts ); + $newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts ); - if ( $oldtext !== false || $newtext != '' ) { + if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) { $oldtitle = wfMsgExt( $oldtitlemsg, array( 'parseinline' ) ); $newtitle = wfMsgExt( 'yourtext', array( 'parseinline' ) ); - $de = new DifferenceEngine( $this->mArticle->getContext() ); - $de->setText( $oldtext, $newtext ); + $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->mArticle->getContext() ); + $de->setContent( $oldContent, $newContent ); + $difftext = $de->getDiff( $oldtitle, $newtitle ); $de->showDiffStyle(); } else { @@ -2468,8 +2645,12 @@ HTML if ( wfRunHooks( 'EditPageBeforeConflictDiff', array( &$this, &$wgOut ) ) ) { $wgOut->wrapWikiMsg( '

$1

', "yourdiff" ); - $de = new DifferenceEngine( $this->mArticle->getContext() ); - $de->setText( $this->textbox2, $this->textbox1 ); + $content1 = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format ); #XXX: handle parse errors? + $content2 = ContentHandler::makeContent( $this->textbox2, $this->getTitle(), $this->content_model, $this->content_format ); #XXX: handle parse errors? + + $handler = ContentHandler::getForModelID( $this->content_model ); + $de = $handler->createDifferenceEngine( $this->mArticle->getContext() ); + $de->setContent( $content2, $content1 ); $de->showDiff( wfMsgExt( 'yourtext', 'parseinline' ), wfMsg( 'storedversion' ) ); $wgOut->wrapWikiMsg( '

$1

', "yourtext" ); @@ -2589,84 +2770,101 @@ HTML return $parsedNote; } - if ( $this->mTriedSave && !$this->mTokenOk ) { - if ( $this->mTokenOkExceptSuffix ) { - $note = wfMsg( 'token_suffix_mismatch' ); - } else { - $note = wfMsg( 'session_fail_preview' ); - } - } elseif ( $this->incompleteForm ) { - $note = wfMsg( 'edit_form_incomplete' ); - } else { - $note = wfMsg( 'previewnote' ) . - ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMsg( 'continue-editing' ) . ']]'; - } + $note = ''; - $parserOptions = ParserOptions::newFromUser( $wgUser ); - $parserOptions->setEditSection( false ); - $parserOptions->setTidy( true ); - $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->isWikitextPage() ) { - if ( $this->mTitle->isCssJsSubpage() ) { - $level = 'user'; - } elseif ( $this->mTitle->isCssOrJsPage() ) { - $level = 'site'; - } else { - $level = false; - } + try { + $content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format ); - # Used messages to make sure grep find them: - # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview - $class = 'mw-code'; - if ( $level ) { - if ( preg_match( "/\\.css$/", $this->mTitle->getText() ) ) { - $previewtext = "
\n" . wfMsg( "{$level}csspreview" ) . "\n
"; - $class .= " mw-css"; - } elseif ( preg_match( "/\\.js$/", $this->mTitle->getText() ) ) { - $previewtext = "
\n" . wfMsg( "{$level}jspreview" ) . "\n
"; - $class .= " mw-js"; + if ( $this->mTriedSave && !$this->mTokenOk ) { + if ( $this->mTokenOkExceptSuffix ) { + $note = wfMsg( 'token_suffix_mismatch' ); } else { - throw new MWException( 'A CSS/JS (sub)page but which is not css nor js!' ); + $note = wfMsg( 'session_fail_preview' ); } - $parserOutput = $wgParser->parse( $previewtext, $this->mTitle, $parserOptions ); - $previewHTML = $parserOutput->getText(); + } elseif ( $this->incompleteForm ) { + $note = wfMsg( 'edit_form_incomplete' ); } else { - $previewHTML = ''; - } + $note = wfMsg( 'previewnote' ) . + ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMsg( 'continue-editing' ) . ']]'; + } + + $parserOptions = ParserOptions::newFromUser( $wgUser ); + $parserOptions->setEditSection( false ); + $parserOptions->setTidy( true ); + $parserOptions->setIsPreview( true ); + $parserOptions->setIsSectionPreview( !is_null($this->section) && $this->section !== '' ); + + if ( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) { + # don't parse non-wikitext pages, show message about preview + if( $this->mTitle->isCssJsSubpage() ) { + $level = 'user'; + } elseif( $this->mTitle->isCssOrJsPage() ) { + $level = 'site'; + } else { + $level = false; + } - $previewHTML .= "
\n" . htmlspecialchars( $this->textbox1 ) . "\n
\n"; - } else { - $toparse = $this->textbox1; + if ( $content->getModel() == CONTENT_MODEL_CSS ) { + $format = 'css'; + } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) { + $format = 'js'; + } else { + $format = false; + } - # If we're adding a comment, we need to show the - # summary as the headline - if ( $this->section == "new" && $this->summary != "" ) { - $toparse = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->summary ) . "\n\n" . $toparse; + # Used messages to make sure grep find them: + # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview + if( $level && $format ) { + $note = "
" . wfMsg( "{$level}{$format}preview" ) . "
"; + } else { + $note = wfMsg( 'previewnote' ); + } + } else { + $note = wfMsg( 'previewnote' ); } - wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) ); - - $parserOptions->enableLimitReport(); - - $toparse = $wgParser->preSaveTransform( $toparse, $this->mTitle, $wgUser, $parserOptions ); - $parserOutput = $wgParser->parse( $toparse, $this->mTitle, $parserOptions ); + $rt = $content->getRedirectChain(); - $rt = Title::newFromRedirectArray( $this->textbox1 ); if ( $rt ) { $previewHTML = $this->mArticle->viewRedirect( $rt, false ); } else { - $previewHTML = $parserOutput->getText(); - } - $this->mParserOutput = $parserOutput; - $wgOut->addParserOutputNoText( $parserOutput ); + # If we're adding a comment, we need to show the + # summary as the headline + if ( $this->section == "new" && $this->summary != "" ) { + $content = $content->addSectionHeader( $this->summary ); + } + + $toparse_orig = $content->serialize( $this->content_format ); + $toparse = $toparse_orig; + wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) ); + + if ( $toparse !== $toparse_orig ) { + #hook changed the text, create new Content object + $content = ContentHandler::makeContent( $toparse, $this->getTitle(), $this->content_model, $this->content_format ); + } + + wfRunHooks( 'EditPageGetPreviewContent', array( $this, &$content ) ); - if ( count( $parserOutput->getWarnings() ) ) { - $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() ); + $parserOptions->enableLimitReport(); + + #XXX: For CSS/JS pages, we should have called the ShowRawCssJs hook here. But it's now deprecated, so never mind + $content = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions ); + + // TODO: might be a saner way to get a meaningfull context here? + $parserOutput = $content->getParserOutput( $this->getArticle()->getTitle(), null, $parserOptions ); + + $previewHTML = $parserOutput->getText(); + $this->mParserOutput = $parserOutput; + $wgOut->addParserOutputNoText( $parserOutput ); + + if ( count( $parserOutput->getWarnings() ) ) { + $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() ); + } } + } catch (MWContentSerializationException $ex) { + $note .= "\n\n" . wfMsg('content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() ); + $previewHTML = ''; } if ( $this->isConflict ) {