*/
const AS_PARSE_ERROR = 240;
+ /**
+ * Status: when changing the content model is disallowed due to
+ * $wgContentHandlerUseDB being false
+ */
+ const AS_CANNOT_USE_CUSTOM_MODEL = 241;
+
/**
* HTML id and name for the beginning of the edit form.
*/
/** @var Article */
public $mArticle;
+ /** @var WikiPage */
+ private $page;
/** @var Title */
public $mTitle;
public $suppressIntro = false;
- /** @var bool Set to true to allow editing of non-text content types. */
- public $allowNonTextContent = false;
-
/** @var bool */
protected $edit;
+ /**
+ * @var bool Set in ApiEditPage, based on ContentHandler::allowsDirectApiEditing
+ */
+ private $enableApiEditOverride = false;
+
/**
* @param Article $article
*/
public function __construct( Article $article ) {
$this->mArticle = $article;
+ $this->page = $article->getPage(); // model object
$this->mTitle = $article->getTitle();
$this->contentModel = $this->mTitle->getContentModel();
* @throws MWException If $modelId has no known handler
*/
public function isSupportedContentModel( $modelId ) {
- return $this->allowNonTextContent ||
- ContentHandler::getForModelID( $modelId ) instanceof TextContentHandler;
+ return $this->enableApiEditOverride === true ||
+ ContentHandler::getForModelID( $modelId )->supportsDirectEditing();
+ }
+
+ /**
+ * Allow editing of content that supports API direct editing, but not general
+ * direct editing. Set to false by default.
+ *
+ * @param bool $enableOverride
+ */
+ public function setApiEditOverride( $enableOverride ) {
+ $this->enableApiEditOverride = $enableOverride;
}
function submit() {
if ( $permErrors ) {
wfDebug( __METHOD__ . ": User can't edit\n" );
// Auto-block user's IP if the account was "hard" blocked
- $wgUser->spreadAnyEditBlock();
+ $user = $wgUser;
+ DeferredUpdates::addCallableUpdate( function() use ( $user ) {
+ $user->spreadAnyEditBlock();
+ } );
$this->displayPermissionsError( $permErrors );
return;
}
+ $revision = $this->mArticle->getRevisionFetched();
+ // Disallow editing revisions with content models different from the current one
+ if ( $revision && $revision->getContentModel() !== $this->contentModel ) {
+ $this->displayViewSourcePage(
+ $this->getContentObject(),
+ wfMessage(
+ 'contentmodelediterror',
+ $revision->getContentModel(),
+ $this->contentModel
+ )->plain()
+ );
+ return;
+ }
+
$this->isConflict = false;
// css / js subpages of user pages get a special treatment
$this->isCssJsSubpage = $this->mTitle->isCssJsSubpage();
throw new PermissionsError( $action, $permErrors );
}
+ $this->displayViewSourcePage(
+ $content,
+ $wgOut->formatPermissionsErrorMessage( $permErrors, 'edit' )
+ );
+ }
+
+ /**
+ * Display a read-only View Source page
+ * @param Content $content content object
+ * @param string $errorMessage additional wikitext error message to display
+ */
+ protected function displayViewSourcePage( Content $content, $errorMessage = '' ) {
+ global $wgOut;
+
Hooks::run( 'EditPage::showReadOnlyForm:initial', array( $this, &$wgOut ) );
$wgOut->setRobotPolicy( 'noindex,nofollow' );
$this->getContextTitle()->getPrefixedText()
) );
$wgOut->addBacklinkSubtitle( $this->getContextTitle() );
- $wgOut->addWikiText( $wgOut->formatPermissionsErrorMessage( $permErrors, 'edit' ) );
- $wgOut->addHTML( "<hr />\n" );
+ $wgOut->addHTML( $this->editFormPageTop );
+ $wgOut->addHTML( $this->editFormTextTop );
+
+ if ( $errorMessage !== '' ) {
+ $wgOut->addWikiText( $errorMessage );
+ $wgOut->addHTML( "<hr />\n" );
+ }
# If the user made changes, preserve them when showing the markup
# (This happens when a user is blocked during edit, for instance)
$text = $this->textbox1;
$wgOut->addWikiMsg( 'viewyourtext' );
} else {
- $text = $this->toEditText( $content );
+ try {
+ $text = $this->toEditText( $content );
+ } catch ( MWException $e ) {
+ # Serialize using the default format if the content model is not supported
+ # (e.g. for an old revision with a different model)
+ $text = $content->serialize();
+ }
$wgOut->addWikiMsg( 'viewsourcetext' );
}
+ $wgOut->addHTML( $this->editFormTextBeforeContent );
$this->showTextbox( $text, 'wpTextbox1', array( 'readonly' ) );
+ $wgOut->addHTML( $this->editFormTextAfterContent );
$wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'templatesUsed' ),
Linker::formatTemplates( $this->getTemplates() ) ) );
$wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
+ $wgOut->addHTML( $this->editFormTextBottom );
if ( $this->mTitle->exists() ) {
$wgOut->returnToMain( null, $this->mTitle );
}
* for saving, preview parsing and so on...
*
* @param WebRequest $request
+ * @return string|null
*/
protected function importContentFormData( &$request ) {
return; // Don't do anything, EditPage already extracted wpTextbox1
*/
function initialiseForm() {
global $wgUser;
- $this->edittime = $this->mArticle->getTimestamp();
+ $this->edittime = $this->page->getTimestamp();
- $content = $this->getContentObject( false ); #TODO: track content object?!
+ $content = $this->getContentObject( false ); # TODO: track content object?!
if ( $content === false ) {
return false;
}
$undo = $wgRequest->getInt( 'undo' );
if ( $undo > 0 && $undoafter > 0 ) {
-
$undorev = Revision::newFromId( $undo );
$oldrev = Revision::newFromId( $undoafter );
# Otherwise, $content will be left as-is.
if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
!$undorev->isDeleted( Revision::DELETED_TEXT ) &&
- !$oldrev->isDeleted( Revision::DELETED_TEXT ) ) {
-
- $content = $this->mArticle->getUndoContent( $undorev, $oldrev );
+ !$oldrev->isDeleted( Revision::DELETED_TEXT )
+ ) {
+ $content = $this->page->getUndoContent( $undorev, $oldrev );
if ( $content === false ) {
# Warn the user that something went wrong
$undoMsg = 'failure';
} else {
- $oldContent = $this->mArticle->getPage()->getContent( Revision::RAW );
+ $oldContent = $this->page->getContent( Revision::RAW );
$popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
$newContent = $content->preSaveTransform( $this->mTitle, $wgUser, $popts );
* @return Content
*/
protected function getCurrentContent() {
- $rev = $this->mArticle->getRevision();
+ $rev = $this->page->getRevision();
$content = $rev ? $rev->getContent( Revision::RAW ) : null;
if ( $content === false || $content === null ) {
$title = Title::newFromText( $preload );
# Check for existence to avoid getting MediaWiki:Noarticletext
if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
- //TODO: somehow show a warning to the user!
+ // TODO: somehow show a warning to the user!
return $handler->makeEmptyContent();
}
$title = $page->getRedirectTarget();
# Same as before
if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
- //TODO: somehow show a warning to the user!
+ // TODO: somehow show a warning to the user!
return $handler->makeEmptyContent();
}
$page = WikiPage::factory( $title );
$content = $page->getContent( Revision::RAW );
if ( !$content ) {
- //TODO: somehow show a warning to the user!
+ // TODO: somehow show a warning to the user!
return $handler->makeEmptyContent();
}
$converted = $content->convert( $handler->getModelID() );
if ( !$converted ) {
- //TODO: somehow show a warning to the user!
- wfDebug( "Attempt to preload incompatible content: "
- . "can't convert " . $content->getModel()
- . " to " . $handler->getModelID() );
+ // TODO: somehow show a warning to the user!
+ wfDebug( "Attempt to preload incompatible content: " .
+ "can't convert " . $content->getModel() .
+ " to " . $handler->getModelID() );
return $handler->makeEmptyContent();
}
* @param int $statusValue The status value (to check for new article status)
*/
protected function setPostEditCookie( $statusValue ) {
- $revisionId = $this->mArticle->getLatest();
+ $revisionId = $this->page->getLatest();
$postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
$val = 'saved';
}
$response = RequestContext::getMain()->getRequest()->response();
- $response->setcookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION, array(
+ $response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION, array(
'httpOnly' => false,
) );
}
case self::AS_HOOK_ERROR:
return false;
+ case self::AS_CANNOT_USE_CUSTOM_MODEL:
case self::AS_PARSE_ERROR:
$wgOut->addWikiText( '<div class="error">' . $status->getWikiText() . '</div>' );
return true;
*/
function internalAttemptSave( &$result, $bot = false ) {
global $wgUser, $wgRequest, $wgParser, $wgMaxArticleSize;
+ global $wgContentHandlerUseDB;
$status = Status::newGood();
}
}
- if ( $this->contentModel !== $this->mTitle->getContentModel()
- && !$wgUser->isAllowed( 'editcontentmodel' )
- ) {
- $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
- return $status;
+ $changingContentModel = false;
+ if ( $this->contentModel !== $this->mTitle->getContentModel() ) {
+ if ( !$wgContentHandlerUseDB ) {
+ $status->fatal( 'editpage-cannot-use-custom-model' );
+ $status->value = self::AS_CANNOT_USE_CUSTOM_MODEL;
+ return $status;
+ } elseif ( !$wgUser->isAllowed( 'editcontentmodel' ) ) {
+ $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
+ return $status;
+
+ }
+ $changingContentModel = true;
+ $oldContentModel = $this->mTitle->getContentModel();
}
if ( $this->changeTags ) {
# Load the page data from the master. If anything changes in the meantime,
# we detect it by using page_latest like a token in a 1 try compare-and-swap.
- $this->mArticle->loadPageData( 'fromdbmaster' );
- $new = !$this->mArticle->exists();
+ $this->page->loadPageData( 'fromdbmaster' );
+ $new = !$this->page->exists();
if ( $new ) {
// Late check for create permission, just in case *PARANOIA*
# Article exists. Check for edit conflict.
- $this->mArticle->clear(); # Force reload of dates, etc.
- $timestamp = $this->mArticle->getTimestamp();
+ $this->page->clear(); # Force reload of dates, etc.
+ $timestamp = $this->page->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->newSectionSummary()
+ if ( $this->page->getUserText() == $wgUser->getName() &&
+ $this->page->getComment() == $this->newSectionSummary()
) {
// Probably a duplicate submission of a new comment.
// This can happen when squid resends a request after
. ": conflict! getting section '{$this->section}' for time '{$this->edittime}'"
. " (article time '{$timestamp}')\n" );
- $content = $this->mArticle->replaceSectionContent(
+ $content = $this->page->replaceSectionContent(
$this->section,
$textbox_content,
$sectionTitle,
);
} else {
wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" );
- $content = $this->mArticle->replaceSectionContent(
+ $content = $this->page->replaceSectionContent(
$this->section,
$textbox_content,
$sectionTitle
return $status;
}
- $flags = EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY |
+ $flags = EDIT_AUTOSUMMARY |
( $new ? EDIT_NEW : EDIT_UPDATE ) |
( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
( $bot ? EDIT_FORCE_BOT : 0 );
- $doEditStatus = $this->mArticle->doEditContent(
+ $doEditStatus = $this->page->doEditContent(
$content,
$this->summary,
$flags,
false,
- null,
+ $wgUser,
$content->getDefaultFormat()
);
if ( $this->changeTags && isset( $doEditStatus->value['revision'] ) ) {
// If a revision was created, apply any change tags that were requested
- ChangeTags::addTags(
- $this->changeTags,
- isset( $doEditStatus->value['rc'] ) ? $doEditStatus->value['rc']->mAttribs['rc_id'] : null,
- $doEditStatus->value['revision']->getId()
+ $addTags = $this->changeTags;
+ $revId = $doEditStatus->value['revision']->getId();
+ // Defer this both for performance and so that addTags() sees the rc_id
+ // since the recentchange entry addition is deferred first (bug T100248)
+ DeferredUpdates::addCallableUpdate( function() use ( $addTags, $revId ) {
+ ChangeTags::addTags( $addTags, null, $revId );
+ } );
+ }
+
+ // If the content model changed, add a log entry
+ if ( $changingContentModel ) {
+ $this->addContentModelChangeLogEntry(
+ $wgUser,
+ $oldContentModel,
+ $this->contentModel,
+ $this->summary
);
}
return $status;
}
+ /**
+ * @param User $user
+ * @param string $oldModel
+ * @param string $newModel
+ * @param string $reason
+ */
+ protected function addContentModelChangeLogEntry( User $user, $oldModel, $newModel, $reason ) {
+ $log = new ManualLogEntry( 'contentmodel', 'change' );
+ $log->setPerformer( $user );
+ $log->setTarget( $this->mTitle );
+ $log->setComment( $reason );
+ $log->setParameters( array(
+ '4::oldmodel' => $oldModel,
+ '5::newmodel' => $newModel
+ ) );
+ $logid = $log->insert();
+ $log->publish( $logid );
+ }
+
/**
* Register the change of watch status
*/
protected function updateWatchlist() {
global $wgUser;
- if ( $wgUser->isLoggedIn()
- && $this->watchthis != $wgUser->isWatched( $this->mTitle, WatchedItem::IGNORE_USER_RIGHTS )
- ) {
- $fname = __METHOD__;
- $title = $this->mTitle;
- $watch = $this->watchthis;
-
- // Do this in its own transaction to reduce contention...
- $dbw = wfGetDB( DB_MASTER );
- $dbw->onTransactionIdle( function () use ( $dbw, $title, $watch, $wgUser, $fname ) {
- WatchAction::doWatchOrUnwatch( $watch, $title, $wgUser );
- } );
+ if ( !$wgUser->isLoggedIn() ) {
+ return;
}
+
+ $user = $wgUser;
+ $title = $this->mTitle;
+ $watch = $this->watchthis;
+ // Do this in its own transaction to reduce contention...
+ DeferredUpdates::addCallableUpdate( function () use ( $user, $title, $watch ) {
+ if ( $watch == $user->isWatched( $title, WatchedItem::IGNORE_USER_RIGHTS ) ) {
+ return; // nothing to change
+ }
+ WatchAction::doWatchOrUnwatch( $watch, $title, $user );
+ } );
}
/**
}
/**
- * @return Revision
+ * @note: this method is very poorly named. If the user opened the form with ?oldid=X,
+ * one might think of X as the "base revision", which is NOT what this returns.
+ * @return Revision Current version when the edit was started
*/
function getBaseRevision() {
if ( !$this->mBaseRevision ) {
}
# Use the title defined by DISPLAYTITLE magic word when present
+ # NOTE: getDisplayTitle() returns HTML while getPrefixedText() returns plain text.
+ # setPageTitle() treats the input as wikitext, which should be safe in either case.
$displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false;
if ( $displayTitle === false ) {
$displayTitle = $contextTitle->getPrefixedText();
# 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' && $this->nosummary ) ) {
$wgOut->addHTML( $this->editFormTextBeforeContent );
if ( !$this->isCssJsSubpage && $showToolbar && $wgUser->getOption( 'showtoolbar' ) ) {
- $wgOut->addHTML( EditPage::getEditToolbar() );
+ $wgOut->addHTML( EditPage::getEditToolbar( $this->mTitle ) );
}
if ( $this->blankArticle ) {
Linker::formatTemplates( $this->getTemplates(), $this->preview, $this->section != '' ) ) );
$wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'hiddencats' ),
- Linker::formatHiddenCategories( $this->mArticle->getHiddenCategories() ) ) );
+ Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
$wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'limitreport' ),
self::getPreviewLimitReport( $this->mParserOutput ) ) );
if ( $this->isConflict ) {
$wgOut->wrapWikiMsg( "<div class='mw-explainconflict'>\n$1\n</div>", 'explainconflict' );
- $this->edittime = $this->mArticle->getTimestamp();
+ $this->edittime = $this->page->getTimestamp();
} else {
if ( $this->section != '' && !$this->isSectionEditSupported() ) {
// We use $this->section to much before this and getVal('wgSection') directly in other places
if ( $this->section != '' && $this->section != 'new' ) {
if ( !$this->summary && !$this->preview && !$this->diff ) {
- $sectionTitle = self::extractSectionTitle( $this->textbox1 ); //FIXME: use Content object
+ $sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object
if ( $sectionTitle !== false ) {
$this->summary = "/* $sectionTitle */ ";
}
}
if ( $this->mTitle->isCascadeProtected() ) {
# Is this page under cascading protection from some source pages?
+ /** @var Title[] $cascadeSources */
list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
$notice = "<div class='mw-cascadeprotectedwarning'>\n$1\n";
$cascadeSourcesCount = count( $cascadeSources );
$textboxContent = $this->toEditContent( $this->textbox1 );
- $newContent = $this->mArticle->replaceSectionContent(
+ $newContent = $this->page->replaceSectionContent(
$this->section, $textboxContent,
$this->summary, $this->edittime );
$this->deletedSinceEdit = false;
- if ( $this->mTitle->isDeletedQuick() ) {
+ if ( !$this->mTitle->exists() && $this->mTitle->isDeletedQuick() ) {
$this->lastDelete = $this->getLastDelete();
if ( $this->lastDelete ) {
$deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp );
global $wgOut, $wgUser, $wgRawHtml, $wgLang;
global $wgAllowUserCss, $wgAllowUserJs;
+ $stats = $wgOut->getContext()->getStats();
+
if ( $wgRawHtml && !$this->mTokenOk ) {
// Could be an offsite preview attempt. This is very unsafe if
// HTML is enabled, as it could be an attack.
$parsedNote = $wgOut->parse( "<div class='previewnote'>" .
wfMessage( 'session_fail_preview_html' )->text() . "</div>", true, /* interface */true );
}
+ $stats->increment( 'edit.failures.session_loss' );
return $parsedNote;
}
if ( $this->mTriedSave && !$this->mTokenOk ) {
if ( $this->mTokenOkExceptSuffix ) {
$note = wfMessage( 'token_suffix_mismatch' )->plain();
+ $stats->increment( 'edit.failures.bad_token' );
} else {
$note = wfMessage( 'session_fail_preview' )->plain();
+ $stats->increment( 'edit.failures.session_loss' );
}
} elseif ( $this->incompleteForm ) {
$note = wfMessage( 'edit_form_incomplete' )->plain();
+ if ( $this->mTriedSave ) {
+ $stats->increment( 'edit.failures.incomplete_form' );
+ }
} else {
$note = wfMessage( 'previewnote' )->plain() . ' ' . $continueEditing;
}
- $parserOptions = $this->mArticle->makeParserOptions( $this->mArticle->getContext() );
+ $parserOptions = $this->page->makeParserOptions( $this->mArticle->getContext() );
$parserOptions->setIsPreview( true );
$parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
if ( count( $parserOutput->getWarnings() ) ) {
$note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
}
+
+ ScopedCallback::consume( $scopedCallback );
} catch ( MWContentSerializationException $ex ) {
$m = wfMessage(
'content-failed-to-parse',
* Shows a bulletin board style toolbar for common editing functions.
* It can be disabled in the user preferences.
*
+ * @param $title Title object for the page being edited (optional)
* @return string
*/
- static function getEditToolbar() {
+ static function getEditToolbar( $title = null ) {
global $wgContLang, $wgOut;
global $wgEnableUploads, $wgForeignFileRepos;
$imagesAvailable = $wgEnableUploads || count( $wgForeignFileRepos );
+ $showSignature = true;
+ if ( $title ) {
+ $showSignature = MWNamespace::wantSignatures( $title->getNamespace() );
+ }
/**
* $toolarray is an array of arrays each of which includes the
'sample' => wfMessage( 'nowiki_sample' )->text(),
'tip' => wfMessage( 'nowiki_tip' )->text(),
),
- array(
+ $showSignature ? array(
'id' => 'mw-editbutton-signature',
'open' => '--~~~~',
'close' => '',
'sample' => '',
'tip' => wfMessage( 'sig_tip' )->text(),
- ),
+ ) : false,
array(
'id' => 'mw-editbutton-hr',
'open' => "\n----\n",
}
$script .= '});';
- $wgOut->addScript( Html::inlineScript( ResourceLoader::makeLoaderConditionalScript( $script ) ) );
+ $wgOut->addScript( ResourceLoader::makeInlineScript( $script ) );
$toolbar = '<div id="toolbar"></div>';