From 08aa4418c30cfc18ccc69a0f0f9cb9e17be6c196 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Mon, 12 Aug 2013 09:28:15 +0200 Subject: Update to MediaWiki 1.21.1 --- includes/WikiPage.php | 1320 ++++++++++++++++++++++++++++++------------------- 1 file changed, 806 insertions(+), 514 deletions(-) (limited to 'includes/WikiPage.php') diff --git a/includes/WikiPage.php b/includes/WikiPage.php index bc8766de..de881ef6 100644 --- a/includes/WikiPage.php +++ b/includes/WikiPage.php @@ -23,7 +23,7 @@ /** * Abstract class for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage) */ -abstract class Page {} +interface Page {} /** * Class representing a MediaWiki article and history. @@ -33,7 +33,7 @@ abstract class Page {} * * @internal documentation reviewed 15 Mar 2010 */ -class WikiPage extends Page implements IDBAccessObject { +class WikiPage implements Page, IDBAccessObject { // Constants for $mDataLoadedFrom and related /** @@ -121,8 +121,8 @@ class WikiPage extends Page implements IDBAccessObject { /** * Constructor from a page id * - * @param $id Int article ID to load - * @param $from string|int one of the following values: + * @param int $id article ID to load + * @param string|int $from one of the following values: * - "fromdb" or WikiPage::READ_NORMAL to select from a slave database * - "fromdbmaster" or WikiPage::READ_LATEST to select from the master database * @@ -144,7 +144,7 @@ class WikiPage extends Page implements IDBAccessObject { * @since 1.20 * @param $row object: database row containing at least fields returned * by selectFields(). - * @param $from string|int: source of $data: + * @param string|int $from source of $data: * - "fromdb" or WikiPage::READ_NORMAL: from a slave DB * - "fromdbmaster" or WikiPage::READ_LATEST: from the master DB * - "forupdate" or WikiPage::READ_LOCKING: from the master DB using SELECT FOR UPDATE @@ -187,7 +187,21 @@ class WikiPage extends Page implements IDBAccessObject { * @return Array */ public function getActionOverrides() { - return array(); + $content_handler = $this->getContentHandler(); + return $content_handler->getActionOverrides(); + } + + /** + * Returns the ContentHandler instance to be used to deal with the content of this WikiPage. + * + * Shorthand for ContentHandler::getForModelID( $this->getContentModel() ); + * + * @return ContentHandler + * + * @since 1.21 + */ + public function getContentHandler() { + return ContentHandler::getForModelID( $this->getContentModel() ); } /** @@ -215,8 +229,8 @@ class WikiPage extends Page implements IDBAccessObject { */ protected function clearCacheFields() { $this->mCounter = null; - $this->mRedirectTarget = null; # Title object if set - $this->mLastRevision = null; # Latest revision + $this->mRedirectTarget = null; // Title object if set + $this->mLastRevision = null; // Latest revision $this->mTouched = '19700101000000'; $this->mTimestamp = ''; $this->mIsRedirect = false; @@ -231,7 +245,9 @@ class WikiPage extends Page implements IDBAccessObject { * @return array */ public static function selectFields() { - return array( + global $wgContentHandlerUseDB; + + $fields = array( 'page_id', 'page_namespace', 'page_title', @@ -244,6 +260,12 @@ class WikiPage extends Page implements IDBAccessObject { 'page_latest', 'page_len', ); + + if ( $wgContentHandlerUseDB ) { + $fields[] = 'page_content_model'; + } + + return $fields; } /** @@ -317,8 +339,8 @@ class WikiPage extends Page implements IDBAccessObject { $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle ); } elseif ( $from === self::READ_NORMAL ) { $data = $this->pageDataFromTitle( wfGetDB( DB_SLAVE ), $this->mTitle ); - # Use a "last rev inserted" timestamp key to dimish the issue of slave lag. - # Note that DB also stores the master position in the session and checks it. + // Use a "last rev inserted" timestamp key to diminish the issue of slave lag. + // Note that DB also stores the master position in the session and checks it. $touched = $this->getCachedLastEditTime(); if ( $touched ) { // key set if ( !$data || $touched > wfTimestamp( TS_MW, $data->page_touched ) ) { @@ -341,7 +363,7 @@ class WikiPage extends Page implements IDBAccessObject { * @since 1.20 * @param $data object: database row containing at least fields returned * by selectFields() - * @param $from string|int One of the following: + * @param string|int $from One of the following: * - "fromdb" or WikiPage::READ_NORMAL if the data comes from a slave DB * - "fromdbmaster" or WikiPage::READ_LATEST if the data comes from the master DB * - "forupdate" or WikiPage::READ_LOCKING if the data comes from from @@ -349,13 +371,14 @@ class WikiPage extends Page implements IDBAccessObject { */ public function loadFromRow( $data, $from ) { $lc = LinkCache::singleton(); + $lc->clearLink( $this->mTitle ); if ( $data ) { $lc->addGoodLinkObjFromRow( $this->mTitle, $data ); $this->mTitle->loadFromRow( $data ); - # Old-fashioned restrictions + // Old-fashioned restrictions $this->mTitle->loadRestrictions( $data->page_restrictions ); $this->mCounter = intval( $data->page_counter ); @@ -418,21 +441,42 @@ class WikiPage extends Page implements IDBAccessObject { } /** - * Tests if the article text represents a redirect + * Tests if the article content represents a redirect * - * @param $text mixed string containing article contents, or boolean * @return bool */ - public function isRedirect( $text = false ) { - if ( $text === false ) { - if ( !$this->mDataLoaded ) { - $this->loadPageData(); - } + public function isRedirect() { + $content = $this->getContent(); + if ( !$content ) return false; - return (bool)$this->mIsRedirect; - } else { - return Title::newFromRedirect( $text ) !== null; + return $content->isRedirect(); + } + + /** + * Returns the page's content model id (see the CONTENT_MODEL_XXX constants). + * + * Will use the revisions actual content model if the page exists, + * and the page's default if the page doesn't exist yet. + * + * @return String + * + * @since 1.21 + */ + public function getContentModel() { + if ( $this->exists() ) { + // look at the revision's actual content model + $rev = $this->getRevision(); + + if ( $rev !== null ) { + return $rev->getContentModel(); + } else { + $title = $this->mTitle->getPrefixedDBkey(); + wfWarn( "Page $title exists but has no (visible) revisions!" ); + } } + + // use the default model for this page + return $this->mTitle->getContentModel(); } /** @@ -555,35 +599,60 @@ class WikiPage extends Page implements IDBAccessObject { } /** - * Get the text of the current revision. No side-effects... + * Get the content of the current revision. No side-effects... * * @param $audience Integer: one of: * Revision::FOR_PUBLIC to be displayed to all users * Revision::FOR_THIS_USER to be displayed to $wgUser * Revision::RAW get the text regardless of permissions - * @return String|bool The text of the current revision. False on failure + * @param $user User object to check for, only if FOR_THIS_USER is passed + * to the $audience parameter + * @return Content|null The content of the current revision + * + * @since 1.21 */ - public function getText( $audience = Revision::FOR_PUBLIC ) { + public function getContent( $audience = Revision::FOR_PUBLIC, User $user = null ) { $this->loadLastEdit(); if ( $this->mLastRevision ) { - return $this->mLastRevision->getText( $audience ); + return $this->mLastRevision->getContent( $audience, $user ); } - return false; + return null; } /** * Get the text of the current revision. No side-effects... * - * @return String|bool The text of the current revision. False on failure + * @param $audience Integer: one of: + * Revision::FOR_PUBLIC to be displayed to all users + * Revision::FOR_THIS_USER to be displayed to the given user + * Revision::RAW get the text regardless of permissions + * @param $user User object to check for, only if FOR_THIS_USER is passed + * to the $audience parameter + * @return String|false The text of the current revision + * @deprecated as of 1.21, getContent() should be used instead. */ - public function getRawText() { + public function getText( $audience = Revision::FOR_PUBLIC, User $user = null ) { // @todo: deprecated, replace usage! + ContentHandler::deprecated( __METHOD__, '1.21' ); + $this->loadLastEdit(); if ( $this->mLastRevision ) { - return $this->mLastRevision->getRawText(); + return $this->mLastRevision->getText( $audience, $user ); } return false; } + /** + * Get the text of the current revision. No side-effects... + * + * @return String|bool The text of the current revision. False on failure + * @deprecated as of 1.21, getContent() should be used instead. + */ + public function getRawText() { + ContentHandler::deprecated( __METHOD__, '1.21' ); + + return $this->getText( Revision::RAW ); + } + /** * @return string MW timestamp of last article revision */ @@ -598,7 +667,7 @@ class WikiPage extends Page implements IDBAccessObject { /** * Set the page timestamp (use only to avoid DB queries) - * @param $ts string MW timestamp of last article revision + * @param string $ts MW timestamp of last article revision * @return void */ public function setTimestamp( $ts ) { @@ -608,14 +677,16 @@ class WikiPage extends Page implements IDBAccessObject { /** * @param $audience Integer: one of: * Revision::FOR_PUBLIC to be displayed to all users - * Revision::FOR_THIS_USER to be displayed to $wgUser + * Revision::FOR_THIS_USER to be displayed to the given user * Revision::RAW get the text regardless of permissions + * @param $user User object to check for, only if FOR_THIS_USER is passed + * to the $audience parameter * @return int user ID for the user that made the last article revision */ - public function getUser( $audience = Revision::FOR_PUBLIC ) { + public function getUser( $audience = Revision::FOR_PUBLIC, User $user = null ) { $this->loadLastEdit(); if ( $this->mLastRevision ) { - return $this->mLastRevision->getUser( $audience ); + return $this->mLastRevision->getUser( $audience, $user ); } else { return -1; } @@ -625,14 +696,16 @@ class WikiPage extends Page implements IDBAccessObject { * Get the User object of the user who created the page * @param $audience Integer: one of: * Revision::FOR_PUBLIC to be displayed to all users - * Revision::FOR_THIS_USER to be displayed to $wgUser + * Revision::FOR_THIS_USER to be displayed to the given user * Revision::RAW get the text regardless of permissions + * @param $user User object to check for, only if FOR_THIS_USER is passed + * to the $audience parameter * @return User|null */ - public function getCreator( $audience = Revision::FOR_PUBLIC ) { + public function getCreator( $audience = Revision::FOR_PUBLIC, User $user = null ) { $revision = $this->getOldestRevision(); if ( $revision ) { - $userName = $revision->getUserText( $audience ); + $userName = $revision->getUserText( $audience, $user ); return User::newFromName( $userName, false ); } else { return null; @@ -642,14 +715,16 @@ class WikiPage extends Page implements IDBAccessObject { /** * @param $audience Integer: one of: * Revision::FOR_PUBLIC to be displayed to all users - * Revision::FOR_THIS_USER to be displayed to $wgUser + * Revision::FOR_THIS_USER to be displayed to the given user * Revision::RAW get the text regardless of permissions + * @param $user User object to check for, only if FOR_THIS_USER is passed + * to the $audience parameter * @return string username of the user that made the last article revision */ - public function getUserText( $audience = Revision::FOR_PUBLIC ) { + public function getUserText( $audience = Revision::FOR_PUBLIC, User $user = null ) { $this->loadLastEdit(); if ( $this->mLastRevision ) { - return $this->mLastRevision->getUserText( $audience ); + return $this->mLastRevision->getUserText( $audience, $user ); } else { return ''; } @@ -658,14 +733,16 @@ class WikiPage extends Page implements IDBAccessObject { /** * @param $audience Integer: one of: * Revision::FOR_PUBLIC to be displayed to all users - * Revision::FOR_THIS_USER to be displayed to $wgUser + * Revision::FOR_THIS_USER to be displayed to the given user * Revision::RAW get the text regardless of permissions + * @param $user User object to check for, only if FOR_THIS_USER is passed + * to the $audience parameter * @return string Comment stored for the last article revision */ - public function getComment( $audience = Revision::FOR_PUBLIC ) { + public function getComment( $audience = Revision::FOR_PUBLIC, User $user = null ) { $this->loadLastEdit(); if ( $this->mLastRevision ) { - return $this->mLastRevision->getComment( $audience ); + return $this->mLastRevision->getComment( $audience, $user ); } else { return ''; } @@ -723,32 +800,34 @@ class WikiPage extends Page implements IDBAccessObject { return false; } - $text = $editInfo ? $editInfo->pst : false; + if ( $editInfo ) { + $content = $editInfo->pstContent; + } else { + $content = $this->getContent(); + } - if ( $this->isRedirect( $text ) ) { + if ( !$content || $content->isRedirect() ) { return false; } - switch ( $wgArticleCountMethod ) { - case 'any': - return true; - case 'comma': - if ( $text === false ) { - $text = $this->getRawText(); - } - return strpos( $text, ',' ) !== false; - case 'link': + $hasLinks = null; + + if ( $wgArticleCountMethod === 'link' ) { + // nasty special case to avoid re-parsing to detect links + if ( $editInfo ) { // ParserOutput::getLinks() is a 2D array of page links, so // to be really correct we would need to recurse in the array // but the main array should only have items in it if there are // links. - return (bool)count( $editInfo->output->getLinks() ); + $hasLinks = (bool)count( $editInfo->output->getLinks() ); } else { - return (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1, + $hasLinks = (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1, array( 'pl_from' => $this->getId() ), __METHOD__ ); } } + + return $content->isCountable( $hasLinks ); } /** @@ -767,7 +846,7 @@ class WikiPage extends Page implements IDBAccessObject { return $this->mRedirectTarget; } - # Query the redirect table + // Query the redirect table $dbr = wfGetDB( DB_SLAVE ); $row = $dbr->selectRow( 'redirect', array( 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ), @@ -782,7 +861,7 @@ class WikiPage extends Page implements IDBAccessObject { $row->rd_fragment, $row->rd_interwiki ); } - # This page doesn't have an entry in the redirect table + // This page doesn't have an entry in the redirect table return $this->mRedirectTarget = $this->insertRedirect(); } @@ -794,7 +873,8 @@ class WikiPage extends Page implements IDBAccessObject { */ public function insertRedirect() { // recurse through to only get the final target - $retval = Title::newFromRedirectRecurse( $this->getRawText() ); + $content = $this->getContent(); + $retval = $content ? $content->getUltimateRedirectTarget() : null; if ( !$retval ) { return null; } @@ -879,7 +959,7 @@ class WikiPage extends Page implements IDBAccessObject { * @return UserArrayFromResult */ public function getContributors() { - # @todo FIXME: This is expensive; cache this info somewhere. + // @todo FIXME: This is expensive; cache this info somewhere. $dbr = wfGetDB( DB_SLAVE ); @@ -927,7 +1007,7 @@ class WikiPage extends Page implements IDBAccessObject { /** * Get the last N authors * @param $num Integer: number of revisions to get - * @param $revLatest String: the latest rev_id, selected from the master (optional) + * @param string $revLatest the latest rev_id, selected from the master (optional) * @return array Array of authors, duplicates not removed */ public function getLastNAuthors( $num, $revLatest = 0 ) { @@ -990,7 +1070,7 @@ class WikiPage extends Page implements IDBAccessObject { && $parserOptions->getStubThreshold() == 0 && $this->mTitle->exists() && ( $oldid === null || $oldid === 0 || $oldid === $this->getLatest() ) - && $this->mTitle->isWikitextPage(); + && $this->getContentHandler()->isParserCacheSupported(); } /** @@ -1001,6 +1081,7 @@ class WikiPage extends Page implements IDBAccessObject { * @param $parserOptions ParserOptions to use for the parse operation * @param $oldid Revision ID to get the text from, passing null or 0 will * get the current revision (default value) + * * @return ParserOutput or false if the revision was not found */ public function getParserOutput( ParserOptions $parserOptions, $oldid = null ) { @@ -1042,13 +1123,13 @@ class WikiPage extends Page implements IDBAccessObject { return; } - # Don't update page view counters on views from bot users (bug 14044) + // Don't update page view counters on views from bot users (bug 14044) if ( !$wgDisableCounters && !$user->isAllowed( 'bot' ) && $this->mTitle->exists() ) { DeferredUpdates::addUpdate( new ViewCountUpdate( $this->getId() ) ); DeferredUpdates::addUpdate( new SiteStatsUpdate( 1, 0, 0 ) ); } - # Update newtalk / watchlist notification status + // Update newtalk / watchlist notification status $user->clearNotification( $this->mTitle ); } @@ -1059,7 +1140,7 @@ class WikiPage extends Page implements IDBAccessObject { public function doPurge() { global $wgUseSquid; - if( !wfRunHooks( 'ArticlePurge', array( &$this ) ) ){ + if( !wfRunHooks( 'ArticlePurge', array( &$this ) ) ) { return false; } @@ -1078,8 +1159,16 @@ class WikiPage extends Page implements IDBAccessObject { } if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + // @todo: move this logic to MessageCache + if ( $this->mTitle->exists() ) { - $text = $this->getRawText(); + // NOTE: use transclusion text for messages. + // This is consistent with MessageCache::getMsgFromNamespace() + + $content = $this->getContent(); + $text = $content === null ? null : $content->getWikitextForTransclusion(); + + if ( $text === null ) $text = false; } else { $text = false; } @@ -1109,12 +1198,12 @@ class WikiPage extends Page implements IDBAccessObject { 'page_title' => $this->mTitle->getDBkey(), 'page_counter' => 0, 'page_restrictions' => '', - 'page_is_redirect' => 0, # Will set this shortly... + 'page_is_redirect' => 0, // Will set this shortly... 'page_is_new' => 1, 'page_random' => wfRandom(), 'page_touched' => $dbw->timestamp(), - 'page_latest' => 0, # Fill this in shortly... - 'page_len' => 0, # Fill this in shortly... + 'page_latest' => 0, // Fill this in shortly... + 'page_len' => 0, // Fill this in shortly... ), __METHOD__, 'IGNORE' ); $affected = $dbw->affectedRows(); @@ -1144,28 +1233,36 @@ class WikiPage extends Page implements IDBAccessObject { * @private */ public function updateRevisionOn( $dbw, $revision, $lastRevision = null, $lastRevIsRedirect = null ) { + global $wgContentHandlerUseDB; + wfProfileIn( __METHOD__ ); - $text = $revision->getText(); - $len = strlen( $text ); - $rt = Title::newFromRedirectRecurse( $text ); + $content = $revision->getContent(); + $len = $content ? $content->getSize() : 0; + $rt = $content ? $content->getUltimateRedirectTarget() : null; $conditions = array( 'page_id' => $this->getId() ); if ( !is_null( $lastRevision ) ) { - # An extra check against threads stepping on each other + // An extra check against threads stepping on each other $conditions['page_latest'] = $lastRevision; } $now = wfTimestampNow(); + $row = array( /* SET */ + 'page_latest' => $revision->getId(), + 'page_touched' => $dbw->timestamp( $now ), + 'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0, + 'page_is_redirect' => $rt !== null ? 1 : 0, + 'page_len' => $len, + ); + + if ( $wgContentHandlerUseDB ) { + $row[ 'page_content_model' ] = $revision->getContentModel(); + } + $dbw->update( 'page', - array( /* SET */ - 'page_latest' => $revision->getId(), - 'page_touched' => $dbw->timestamp( $now ), - 'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0, - 'page_is_redirect' => $rt !== null ? 1 : 0, - 'page_len' => $len, - ), + $row, $conditions, __METHOD__ ); @@ -1176,8 +1273,9 @@ class WikiPage extends Page implements IDBAccessObject { $this->setCachedLastEditTime( $now ); $this->mLatest = $revision->getId(); $this->mIsRedirect = (bool)$rt; - # Update the LinkCache. - LinkCache::singleton()->addGoodLinkObj( $this->getId(), $this->mTitle, $len, $this->mIsRedirect, $this->mLatest ); + // Update the LinkCache. + LinkCache::singleton()->addGoodLinkObj( $this->getId(), $this->mTitle, $len, $this->mIsRedirect, + $this->mLatest, $revision->getContentModel() ); } wfProfileOut( __METHOD__ ); @@ -1249,7 +1347,7 @@ class WikiPage extends Page implements IDBAccessObject { $prev = $row->rev_id; $lastRevIsRedirect = (bool)$row->page_is_redirect; } else { - # No or missing previous revision; mark the page as new + // No or missing previous revision; mark the page as new $prev = 0; $lastRevIsRedirect = null; } @@ -1260,6 +1358,21 @@ class WikiPage extends Page implements IDBAccessObject { return $ret; } + /** + * Get the content that needs to be saved in order to undo all revisions + * between $undo and $undoafter. Revisions must belong to the same page, + * must exist and must not be deleted + * @param $undo Revision + * @param $undoafter Revision Must be an earlier revision than $undo + * @return mixed string on success, false on failure + * @since 1.21 + * Before we had the Content object, this was done in getUndoText + */ + public function getUndoContent( Revision $undo, Revision $undoafter = null ) { + $handler = $undo->getContentHandler(); + return $handler->getUndoContent( $this->getRevision(), $undo, $undoafter ); + } + /** * Get the text that needs to be saved in order to undo all revisions * between $undo and $undoafter. Revisions must belong to the same page, @@ -1267,50 +1380,98 @@ class WikiPage extends Page implements IDBAccessObject { * @param $undo Revision * @param $undoafter Revision Must be an earlier revision than $undo * @return mixed string on success, false on failure + * @deprecated since 1.21: use ContentHandler::getUndoContent() instead. */ public function getUndoText( Revision $undo, Revision $undoafter = null ) { - $cur_text = $this->getRawText(); - if ( $cur_text === false ) { - return false; // no page - } - $undo_text = $undo->getText(); - $undoafter_text = $undoafter->getText(); + ContentHandler::deprecated( __METHOD__, '1.21' ); - if ( $cur_text == $undo_text ) { - # No use doing a merge if it's just a straight revert. - return $undoafter_text; - } + $this->loadLastEdit(); - $undone_text = ''; + if ( $this->mLastRevision ) { + if ( is_null( $undoafter ) ) { + $undoafter = $undo->getPrevious(); + } - if ( !wfMerge( $undo_text, $undoafter_text, $cur_text, $undone_text ) ) { - return false; + $handler = $this->getContentHandler(); + $undone = $handler->getUndoContent( $this->mLastRevision, $undo, $undoafter ); + + if ( !$undone ) { + return false; + } else { + return ContentHandler::getContentText( $undone ); + } } - return $undone_text; + return false; } /** * @param $section null|bool|int or a section number (0, 1, 2, T1, T2...) - * @param $text String: new text of the section - * @param $sectionTitle String: new section's subject, only if $section is 'new' - * @param $edittime String: revision timestamp or null to use the current revision - * @return string Complete article text, or null if error + * @param string $text new text of the section + * @param string $sectionTitle new section's subject, only if $section is 'new' + * @param string $edittime revision timestamp or null to use the current revision + * @throws MWException + * @return String new complete article text, or null if error + * + * @deprecated since 1.21, use replaceSectionContent() instead */ public function replaceSection( $section, $text, $sectionTitle = '', $edittime = null ) { + ContentHandler::deprecated( __METHOD__, '1.21' ); + + if ( strval( $section ) == '' ) { //NOTE: keep condition in sync with condition in replaceSectionContent! + // Whole-page edit; let the whole text through + return $text; + } + + if ( !$this->supportsSections() ) { + throw new MWException( "sections not supported for content model " . $this->getContentHandler()->getModelID() ); + } + + // could even make section title, but that's not required. + $sectionContent = ContentHandler::makeContent( $text, $this->getTitle() ); + + $newContent = $this->replaceSectionContent( $section, $sectionContent, $sectionTitle, $edittime ); + + return ContentHandler::getContentText( $newContent ); + } + + /** + * Returns true iff this page's content model supports sections. + * + * @return boolean whether sections are supported. + * + * @todo: the skin should check this and not offer section functionality if sections are not supported. + * @todo: the EditPage should check this and not offer section functionality if sections are not supported. + */ + public function supportsSections() { + return $this->getContentHandler()->supportsSections(); + } + + /** + * @param $section null|bool|int or a section number (0, 1, 2, T1, T2...) + * @param $sectionContent Content: new content of the section + * @param string $sectionTitle new section's subject, only if $section is 'new' + * @param string $edittime revision timestamp or null to use the current revision + * + * @throws MWException + * @return Content new complete article content, or null if error + * + * @since 1.21 + */ + public function replaceSectionContent( $section, Content $sectionContent, $sectionTitle = '', $edittime = null ) { wfProfileIn( __METHOD__ ); if ( strval( $section ) == '' ) { // Whole-page edit; let the whole text through + $newContent = $sectionContent; } else { + if ( !$this->supportsSections() ) { + throw new MWException( "sections not supported for content model " . $this->getContentHandler()->getModelID() ); + } + // Bug 30711: always use current version when adding a new section if ( is_null( $edittime ) || $section == 'new' ) { - $oldtext = $this->getRawText(); - if ( $oldtext === false ) { - wfDebug( __METHOD__ . ": no page text\n" ); - wfProfileOut( __METHOD__ ); - return null; - } + $oldContent = $this->getContent(); } else { $dbw = wfGetDB( DB_MASTER ); $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime ); @@ -1322,28 +1483,21 @@ class WikiPage extends Page implements IDBAccessObject { return null; } - $oldtext = $rev->getText(); + $oldContent = $rev->getContent(); } - if ( $section == 'new' ) { - # Inserting a new section - $subject = $sectionTitle ? wfMessage( 'newsectionheaderdefaultlevel' ) - ->rawParams( $sectionTitle )->inContentLanguage()->text() . "\n\n" : ''; - if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) { - $text = strlen( trim( $oldtext ) ) > 0 - ? "{$oldtext}\n\n{$subject}{$text}" - : "{$subject}{$text}"; - } - } else { - # Replacing an existing section; roll out the big guns - global $wgParser; - - $text = $wgParser->replaceSection( $oldtext, $section, $text ); + if ( ! $oldContent ) { + wfDebug( __METHOD__ . ": no page text\n" ); + wfProfileOut( __METHOD__ ); + return null; } + + // FIXME: $oldContent might be null? + $newContent = $oldContent->replaceSection( $section, $sectionContent, $sectionTitle ); } wfProfileOut( __METHOD__ ); - return $text; + return $newContent; } /** @@ -1367,8 +1521,8 @@ class WikiPage extends Page implements IDBAccessObject { * Change an existing article or create a new article. Updates RC and all necessary caches, * optionally via the deferred update array. * - * @param $text String: new text - * @param $summary String: edit summary + * @param string $text new text + * @param string $summary edit summary * @param $flags Integer bitfield: * EDIT_NEW * Article is known or assumed to be non-existent, create a new one @@ -1409,17 +1563,83 @@ class WikiPage extends Page implements IDBAccessObject { * revision: The revision object for the inserted revision, or null * * Compatibility note: this function previously returned a boolean value indicating success/failure + * + * @deprecated since 1.21: use doEditContent() instead. */ public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) { + ContentHandler::deprecated( __METHOD__, '1.21' ); + + $content = ContentHandler::makeContent( $text, $this->getTitle() ); + + return $this->doEditContent( $content, $summary, $flags, $baseRevId, $user ); + } + + /** + * Change an existing article or create a new article. Updates RC and all necessary caches, + * optionally via the deferred update array. + * + * @param $content Content: new content + * @param string $summary edit summary + * @param $flags Integer bitfield: + * EDIT_NEW + * Article is known or assumed to be non-existent, create a new one + * EDIT_UPDATE + * Article is known or assumed to be pre-existing, update it + * EDIT_MINOR + * Mark this edit minor, if the user is allowed to do so + * EDIT_SUPPRESS_RC + * Do not log the change in recentchanges + * EDIT_FORCE_BOT + * Mark the edit a "bot" edit regardless of user rights + * EDIT_DEFER_UPDATES + * Defer some of the updates until the end of index.php + * EDIT_AUTOSUMMARY + * Fill in blank summaries with generated text where possible + * + * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the article will be detected. + * If EDIT_UPDATE is specified and the article doesn't exist, the function will return an + * edit-gone-missing error. If EDIT_NEW is specified and the article does exist, an + * edit-already-exists error will be returned. These two conditions are also possible with + * auto-detection due to MediaWiki's performance-optimised locking strategy. + * + * @param bool|\the $baseRevId the revision ID this edit was based off, if any + * @param $user User the user doing the edit + * @param $serialisation_format String: format for storing the content in the database + * + * @throws MWException + * @return Status object. Possible errors: + * edit-hook-aborted: The ArticleSave hook aborted the edit but didn't set the fatal flag of $status + * edit-gone-missing: In update mode, but the article didn't exist + * edit-conflict: In update mode, the article changed unexpectedly + * edit-no-change: Warning that the text was the same as before + * edit-already-exists: In creation mode, but the article already exists + * + * Extensions may define additional errors. + * + * $return->value will contain an associative array with members as follows: + * new: Boolean indicating if the function attempted to create a new article + * revision: The revision object for the inserted revision, or null + * + * @since 1.21 + */ + public function doEditContent( Content $content, $summary, $flags = 0, $baseRevId = false, + User $user = null, $serialisation_format = null ) { global $wgUser, $wgUseAutomaticEditSummaries, $wgUseRCPatrol, $wgUseNPPatrol; - # Low-level sanity check + // Low-level sanity check if ( $this->mTitle->getText() === '' ) { throw new MWException( 'Something is trying to edit an article with an empty title' ); } wfProfileIn( __METHOD__ ); + if ( !$content->getContentHandler()->canBeUsedOn( $this->getTitle() ) ) { + wfProfileOut( __METHOD__ ); + return Status::newFatal( 'content-not-allowed-here', + ContentHandler::getLocalizedName( $content->getModel() ), + $this->getTitle()->getPrefixedText() ); + } + $user = is_null( $user ) ? $wgUser : $user; $status = Status::newGood( array() ); @@ -1430,10 +1650,14 @@ class WikiPage extends Page implements IDBAccessObject { $flags = $this->checkFlags( $flags ); - if ( !wfRunHooks( 'ArticleSave', array( &$this, &$user, &$text, &$summary, - $flags & EDIT_MINOR, null, null, &$flags, &$status ) ) ) - { - wfDebug( __METHOD__ . ": ArticleSave hook aborted save!\n" ); + // handle hook + $hook_args = array( &$this, &$user, &$content, &$summary, + $flags & EDIT_MINOR, null, null, &$flags, &$status ); + + if ( !wfRunHooks( 'PageContentSave', $hook_args ) + || !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args ) ) { + + wfDebug( __METHOD__ . ": ArticleSave or ArticleSaveContent hook aborted save!\n" ); if ( $status->isOK() ) { $status->fatal( 'edit-hook-aborted' ); @@ -1443,77 +1667,98 @@ class WikiPage extends Page implements IDBAccessObject { return $status; } - # Silently ignore EDIT_MINOR if not allowed + // Silently ignore EDIT_MINOR if not allowed $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' ); $bot = $flags & EDIT_FORCE_BOT; - $oldtext = $this->getRawText(); // current revision - $oldsize = strlen( $oldtext ); + $old_content = $this->getContent( Revision::RAW ); // current revision's content + + $oldsize = $old_content ? $old_content->getSize() : 0; $oldid = $this->getLatest(); $oldIsRedirect = $this->isRedirect(); $oldcountable = $this->isCountable(); - # Provide autosummaries if one is not provided and autosummaries are enabled. + $handler = $content->getContentHandler(); + + // Provide autosummaries if one is not provided and autosummaries are enabled. if ( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) { - $summary = self::getAutosummary( $oldtext, $text, $flags ); + if ( !$old_content ) $old_content = null; + $summary = $handler->getAutosummary( $old_content, $content, $flags ); } - $editInfo = $this->prepareTextForEdit( $text, null, $user ); - $text = $editInfo->pst; - $newsize = strlen( $text ); + $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialisation_format ); + $serialized = $editInfo->pst; + $content = $editInfo->pstContent; + $newsize = $content->getSize(); $dbw = wfGetDB( DB_MASTER ); $now = wfTimestampNow(); $this->mTimestamp = $now; if ( $flags & EDIT_UPDATE ) { - # Update article, but only if changed. + // Update article, but only if changed. $status->value['new'] = false; if ( !$oldid ) { - # Article gone missing + // Article gone missing wfDebug( __METHOD__ . ": EDIT_UPDATE specified but article doesn't exist\n" ); $status->fatal( 'edit-gone-missing' ); wfProfileOut( __METHOD__ ); return $status; - } elseif ( $oldtext === false ) { - # Sanity check for bug 37225 + } elseif ( !$old_content ) { + // Sanity check for bug 37225 wfProfileOut( __METHOD__ ); throw new MWException( "Could not find text for current revision {$oldid}." ); } $revision = new Revision( array( 'page' => $this->getId(), + 'title' => $this->getTitle(), // for determining the default content model 'comment' => $summary, 'minor_edit' => $isminor, - 'text' => $text, + 'text' => $serialized, + 'len' => $newsize, 'parent_id' => $oldid, 'user' => $user->getId(), 'user_text' => $user->getName(), - 'timestamp' => $now - ) ); - # Bug 37225: use accessor to get the text as Revision may trim it. - # After trimming, the text may be a duplicate of the current text. - $text = $revision->getText(); // sanity; EditPage should trim already + 'timestamp' => $now, + 'content_model' => $content->getModel(), + 'content_format' => $serialisation_format, + ) ); // XXX: pass content object?! - $changed = ( strcmp( $text, $oldtext ) != 0 ); + $changed = !$content->equals( $old_content ); if ( $changed ) { + if ( !$content->isValid() ) { + throw new MWException( "New content failed validity check!" ); + } + $dbw->begin( __METHOD__ ); + + $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user ); + $status->merge( $prepStatus ); + + if ( !$status->isOK() ) { + $dbw->rollback( __METHOD__ ); + + wfProfileOut( __METHOD__ ); + return $status; + } + $revisionId = $revision->insertOn( $dbw ); - # Update page - # - # Note that we use $this->mLatest instead of fetching a value from the master DB - # during the course of this function. This makes sure that EditPage can detect - # edit conflicts reliably, either by $ok here, or by $article->getTimestamp() - # before this function is called. A previous function used a separate query, this - # creates a window where concurrent edits can cause an ignored edit conflict. + // Update page + // + // Note that we use $this->mLatest instead of fetching a value from the master DB + // during the course of this function. This makes sure that EditPage can detect + // edit conflicts reliably, either by $ok here, or by $article->getTimestamp() + // before this function is called. A previous function used a separate query, this + // creates a window where concurrent edits can cause an ignored edit conflict. $ok = $this->updateRevisionOn( $dbw, $revision, $oldid, $oldIsRedirect ); if ( !$ok ) { - # Belated edit conflict! Run away!! + // Belated edit conflict! Run away!! $status->fatal( 'edit-conflict' ); $dbw->rollback( __METHOD__ ); @@ -1523,18 +1768,18 @@ class WikiPage extends Page implements IDBAccessObject { } wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId, $user ) ); - # Update recentchanges + // Update recentchanges if ( !( $flags & EDIT_SUPPRESS_RC ) ) { - # Mark as patrolled if the user can do so + // Mark as patrolled if the user can do so $patrolled = $wgUseRCPatrol && !count( $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) ); - # Add RC row to the DB + // Add RC row to the DB $rc = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $user, $summary, $oldid, $this->getTimestamp(), $bot, '', $oldsize, $newsize, $revisionId, $patrolled ); - # Log auto-patrolled edits + // Log auto-patrolled edits if ( $patrolled ) { PatrolLog::record( $rc, true, $user ); } @@ -1547,9 +1792,15 @@ class WikiPage extends Page implements IDBAccessObject { $revision->setId( $this->getLatest() ); } - # Update links tables, site stats, etc. - $this->doEditUpdates( $revision, $user, array( 'changed' => $changed, - 'oldcountable' => $oldcountable ) ); + // Update links tables, site stats, etc. + $this->doEditUpdates( + $revision, + $user, + array( + 'changed' => $changed, + 'oldcountable' => $oldcountable + ) + ); if ( !$changed ) { $status->warning( 'edit-no-change' ); @@ -1559,13 +1810,25 @@ class WikiPage extends Page implements IDBAccessObject { $this->mTitle->invalidateCache(); } } else { - # Create new article + // Create new article $status->value['new'] = true; $dbw->begin( __METHOD__ ); - # Add the page record; stake our claim on this title! - # This will return false if the article already exists + $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user ); + $status->merge( $prepStatus ); + + if ( !$status->isOK() ) { + $dbw->rollback( __METHOD__ ); + + wfProfileOut( __METHOD__ ); + return $status; + } + + $status->merge( $prepStatus ); + + // Add the page record; stake our claim on this title! + // This will return false if the article already exists $newid = $this->insertOn( $dbw ); if ( $newid === false ) { @@ -1576,36 +1839,44 @@ class WikiPage extends Page implements IDBAccessObject { return $status; } - # Save the revision text... + // Save the revision text... $revision = new Revision( array( 'page' => $newid, + 'title' => $this->getTitle(), // for determining the default content model 'comment' => $summary, 'minor_edit' => $isminor, - 'text' => $text, + 'text' => $serialized, + 'len' => $newsize, 'user' => $user->getId(), 'user_text' => $user->getName(), - 'timestamp' => $now + 'timestamp' => $now, + 'content_model' => $content->getModel(), + 'content_format' => $serialisation_format, ) ); $revisionId = $revision->insertOn( $dbw ); - # Bug 37225: use accessor to get the text as Revision may trim it - $text = $revision->getText(); // sanity; EditPage should trim already + // Bug 37225: use accessor to get the text as Revision may trim it + $content = $revision->getContent(); // sanity; get normalized version - # Update the page record with revision data + if ( $content ) { + $newsize = $content->getSize(); + } + + // Update the page record with revision data $this->updateRevisionOn( $dbw, $revision, 0 ); wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) ); - # Update recentchanges + // Update recentchanges if ( !( $flags & EDIT_SUPPRESS_RC ) ) { - # Mark as patrolled if the user can do so + // Mark as patrolled if the user can do so $patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) && !count( $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) ); - # Add RC row to the DB + // Add RC row to the DB $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot, - '', strlen( $text ), $revisionId, $patrolled ); + '', $newsize, $revisionId, $patrolled ); - # Log auto-patrolled edits + // Log auto-patrolled edits if ( $patrolled ) { PatrolLog::record( $rc, true, $user ); } @@ -1613,14 +1884,17 @@ class WikiPage extends Page implements IDBAccessObject { $user->incEditCount(); $dbw->commit( __METHOD__ ); - # Update links, etc. + // Update links, etc. $this->doEditUpdates( $revision, $user, array( 'created' => true ) ); - wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $text, $summary, - $flags & EDIT_MINOR, null, null, &$flags, $revision ) ); + $hook_args = array( &$this, &$user, $content, $summary, + $flags & EDIT_MINOR, null, null, &$flags, $revision ); + + ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $hook_args ); + wfRunHooks( 'PageContentInsertComplete', $hook_args ); } - # Do updates right now unless deferral was requested + // Do updates right now unless deferral was requested if ( !( $flags & EDIT_DEFER_UPDATES ) ) { DeferredUpdates::doUpdates(); } @@ -1628,10 +1902,13 @@ class WikiPage extends Page implements IDBAccessObject { // Return the new revision (or null) to the caller $status->value['revision'] = $revision; - wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $text, $summary, - $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) ); + $hook_args = array( &$this, &$user, $content, $summary, + $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ); - # Promote user to any groups they meet the criteria for + ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $hook_args ); + wfRunHooks( 'PageContentSaveComplete', $hook_args ); + + // Promote user to any groups they meet the criteria for $user->addAutopromoteOnceGroups( 'onEdit' ); wfProfileOut( __METHOD__ ); @@ -1641,6 +1918,8 @@ class WikiPage extends Page implements IDBAccessObject { /** * Get parser options suitable for rendering the primary article wikitext * + * @see ContentHandler::makeParserOptions + * * @param IContextSource|User|string $context One of the following: * - IContextSource: Use the User and the Language of the provided * context @@ -1651,38 +1930,52 @@ class WikiPage extends Page implements IDBAccessObject { * @return ParserOptions */ public function makeParserOptions( $context ) { - global $wgContLang; - - if ( $context instanceof IContextSource ) { - $options = ParserOptions::newFromContext( $context ); - } elseif ( $context instanceof User ) { // settings per user (even anons) - $options = ParserOptions::newFromUser( $context ); - } else { // canonical settings - $options = ParserOptions::newFromUserAndLang( new User, $wgContLang ); - } + $options = $this->getContentHandler()->makeParserOptions( $context ); if ( $this->getTitle()->isConversionTable() ) { + //@todo: ConversionTable should become a separate content model, so we don't need special cases like this one. $options->disableContentConversion(); } - $options->enableLimitReport(); // show inclusion/loop reports - $options->setTidy( true ); // fix bad HTML - return $options; } /** * Prepare text which is about to be saved. * Returns a stdclass with source, pst and output members - * @return bool|object + * + * @deprecated in 1.21: use prepareContentForEdit instead. */ public function prepareTextForEdit( $text, $revid = null, User $user = null ) { - global $wgParser, $wgContLang, $wgUser; + ContentHandler::deprecated( __METHOD__, '1.21' ); + $content = ContentHandler::makeContent( $text, $this->getTitle() ); + return $this->prepareContentForEdit( $content, $revid, $user ); + } + + /** + * Prepare content which is about to be saved. + * Returns a stdclass with source, pst and output members + * + * @param \Content $content + * @param null $revid + * @param null|\User $user + * @param null $serialization_format + * + * @return bool|object + * + * @since 1.21 + */ + public function prepareContentForEdit( Content $content, $revid = null, User $user = null, $serialization_format = null ) { + global $wgContLang, $wgUser; $user = is_null( $user ) ? $wgUser : $user; - // @TODO fixme: check $user->getId() here??? + //XXX: check $user->getId() here??? + if ( $this->mPreparedEdit - && $this->mPreparedEdit->newText == $text + && $this->mPreparedEdit->newContent + && $this->mPreparedEdit->newContent->equals( $content ) && $this->mPreparedEdit->revid == $revid + && $this->mPreparedEdit->format == $serialization_format + // XXX: also check $user here? ) { // Already prepared return $this->mPreparedEdit; @@ -1693,14 +1986,22 @@ class WikiPage extends Page implements IDBAccessObject { $edit = (object)array(); $edit->revid = $revid; - $edit->newText = $text; - $edit->pst = $wgParser->preSaveTransform( $text, $this->mTitle, $user, $popts ); + + $edit->pstContent = $content ? $content->preSaveTransform( $this->mTitle, $user, $popts ) : null; + + $edit->format = $serialization_format; $edit->popts = $this->makeParserOptions( 'canonical' ); - $edit->output = $wgParser->parse( $edit->pst, $this->mTitle, $edit->popts, true, true, $revid ); - $edit->oldText = $this->getRawText(); + $edit->output = $edit->pstContent ? $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts ) : null; - $this->mPreparedEdit = $edit; + $edit->newContent = $content; + $edit->oldContent = $this->getContent( Revision::RAW ); + // NOTE: B/C for hooks! don't use these fields! + $edit->newText = $edit->newContent ? ContentHandler::getContentText( $edit->newContent ) : ''; + $edit->oldText = $edit->oldContent ? ContentHandler::getContentText( $edit->oldContent ) : ''; + $edit->pst = $edit->pstContent ? $edit->pstContent->serialize( $serialization_format ) : ''; + + $this->mPreparedEdit = $edit; return $edit; } @@ -1710,10 +2011,9 @@ class WikiPage extends Page implements IDBAccessObject { * Purges pages that include this page if the text was changed here. * Every 100th edit, prune the recent changes table. * - * @private * @param $revision Revision object * @param $user User object that did the revision - * @param $options Array of options, following indexes are used: + * @param array $options of options, following indexes are used: * - changed: boolean, whether the revision changed the content (default true) * - created: boolean, whether the revision created the page (default false) * - oldcountable: boolean or null (default null): @@ -1727,27 +2027,29 @@ class WikiPage extends Page implements IDBAccessObject { wfProfileIn( __METHOD__ ); $options += array( 'changed' => true, 'created' => false, 'oldcountable' => null ); - $text = $revision->getText(); + $content = $revision->getContent(); - # Parse the text - # Be careful not to double-PST: $text is usually already PST-ed once + // Parse the text + // Be careful not to double-PST: $text is usually already PST-ed once if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) { wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" ); - $editInfo = $this->prepareTextForEdit( $text, $revision->getId(), $user ); + $editInfo = $this->prepareContentForEdit( $content, $revision->getId(), $user ); } else { wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" ); $editInfo = $this->mPreparedEdit; } - # Save it to the parser cache + // Save it to the parser cache if ( $wgEnableParserCache ) { $parserCache = ParserCache::singleton(); $parserCache->save( $editInfo->output, $this, $editInfo->popts ); } - # Update the links tables and other secondary data - $updates = $editInfo->output->getSecondaryDataUpdates( $this->mTitle ); - DataUpdate::runUpdates( $updates ); + // Update the links tables and other secondary data + if ( $content ) { + $updates = $content->getSecondaryDataUpdates( $this->getTitle(), null, true, $editInfo->output ); + DataUpdate::runUpdates( $updates ); + } wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) ); @@ -1761,7 +2063,7 @@ class WikiPage extends Page implements IDBAccessObject { $cutoff = $dbw->timestamp( time() - $wgRCMaxAge ); $dbw->delete( 'recentchanges', - array( "rc_timestamp < '$cutoff'" ), + array( 'rc_timestamp < ' . $dbw->addQuotes( $cutoff ) ), __METHOD__ ); } @@ -1791,11 +2093,12 @@ class WikiPage extends Page implements IDBAccessObject { } DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, $good, $total ) ); - DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $text ) ); + DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content->getTextForSearchIndex() ) ); + // @TODO: let the search engine decide what to do with the content object - # If this is another user's talk page, update newtalk. - # Don't do this if $options['changed'] = false (null-edits) nor if - # it's a minor edit and the user doesn't want notifications for those. + // If this is another user's talk page, update newtalk. + // Don't do this if $options['changed'] = false (null-edits) nor if + // it's a minor edit and the user doesn't want notifications for those. if ( $options['changed'] && $this->mTitle->getNamespace() == NS_USER_TALK && $shortTitle != $user->getTitleKey() @@ -1817,7 +2120,11 @@ class WikiPage extends Page implements IDBAccessObject { } if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { - MessageCache::singleton()->replace( $shortTitle, $text ); + // XXX: could skip pseudo-messages like js/css here, based on content model. + $msgtext = $content ? $content->getWikitextForTransclusion() : null; + if ( $msgtext === false || $msgtext === null ) $msgtext = ''; + + MessageCache::singleton()->replace( $shortTitle, $msgtext ); } if( $options['created'] ) { @@ -1834,21 +2141,45 @@ class WikiPage extends Page implements IDBAccessObject { * The article must already exist; link tables etc * are not updated, caches are not flushed. * - * @param $text String: text submitted + * @param string $text text submitted * @param $user User The relevant user - * @param $comment String: comment submitted + * @param string $comment comment submitted * @param $minor Boolean: whereas it's a minor modification + * + * @deprecated since 1.21, use doEditContent() instead. */ public function doQuickEdit( $text, User $user, $comment = '', $minor = 0 ) { + ContentHandler::deprecated( __METHOD__, "1.21" ); + + $content = ContentHandler::makeContent( $text, $this->getTitle() ); + return $this->doQuickEditContent( $content, $user, $comment, $minor ); + } + + /** + * Edit an article without doing all that other stuff + * The article must already exist; link tables etc + * are not updated, caches are not flushed. + * + * @param $content Content: content submitted + * @param $user User The relevant user + * @param string $comment comment submitted + * @param $serialisation_format String: format for storing the content in the database + * @param $minor Boolean: whereas it's a minor modification + */ + public function doQuickEditContent( Content $content, User $user, $comment = '', $minor = 0, $serialisation_format = null ) { wfProfileIn( __METHOD__ ); + $serialized = $content->serialize( $serialisation_format ); + $dbw = wfGetDB( DB_MASTER ); $revision = new Revision( array( + 'title' => $this->getTitle(), // for determining the default content model 'page' => $this->getId(), - 'text' => $text, + 'text' => $serialized, + 'length' => $content->getSize(), 'comment' => $comment, 'minor_edit' => $minor ? 1 : 0, - ) ); + ) ); // XXX: set the content object? $revision->insertOn( $dbw ); $this->updateRevisionOn( $dbw, $revision ); @@ -1861,10 +2192,10 @@ class WikiPage extends Page implements IDBAccessObject { * Update the article's restriction field, and leave a log entry. * This works for protection both existing and non-existing pages. * - * @param $limit Array: set of restriction keys + * @param array $limit set of restriction keys * @param $reason String * @param &$cascade Integer. Set to false if cascading protection isn't allowed. - * @param $expiry Array: per restriction type expiration + * @param array $expiry per restriction type expiration * @param $user User The user updating the restrictions * @return Status */ @@ -1886,8 +2217,8 @@ class WikiPage extends Page implements IDBAccessObject { // Take this opportunity to purge out expired restrictions Title::purgeExpiredRestrictions(); - # @todo FIXME: Same limitations as described in ProtectionForm.php (line 37); - # we expect a single selection, but the schema allows otherwise. + // @todo FIXME: Same limitations as described in ProtectionForm.php (line 37); + // we expect a single selection, but the schema allows otherwise. $isProtected = false; $protect = false; $changed = false; @@ -1904,7 +2235,7 @@ class WikiPage extends Page implements IDBAccessObject { $protect = true; } - # Get current restrictions on $action + // Get current restrictions on $action $current = implode( '', $this->mTitle->getRestrictions( $action ) ); if ( $current != '' ) { $isProtected = true; @@ -1913,9 +2244,9 @@ class WikiPage extends Page implements IDBAccessObject { if ( $limit[$action] != $current ) { $changed = true; } elseif ( $limit[$action] != '' ) { - # Only check expiry change if the action is actually being - # protected, since expiry does nothing on an not-protected - # action. + // Only check expiry change if the action is actually being + // protected, since expiry does nothing on an not-protected + // action. if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) { $changed = true; } @@ -1926,12 +2257,12 @@ class WikiPage extends Page implements IDBAccessObject { $changed = true; } - # If nothing's changed, do nothing + // If nothing has changed, do nothing if ( !$changed ) { return Status::newGood(); } - if ( !$protect ) { # No protection at all means unprotection + if ( !$protect ) { // No protection at all means unprotection $revCommentMsg = 'unprotectedarticle'; $logAction = 'unprotect'; } elseif ( $isProtected ) { @@ -1944,42 +2275,64 @@ class WikiPage extends Page implements IDBAccessObject { $encodedExpiry = array(); $protectDescription = ''; + # Some bots may parse IRC lines, which are generated from log entries which contain plain + # protect description text. Keep them in old format to avoid breaking compatibility. + # TODO: Fix protection log to store structured description and format it on-the-fly. + $protectDescriptionLog = ''; foreach ( $limit as $action => $restrictions ) { $encodedExpiry[$action] = $dbw->encodeExpiry( $expiry[$action] ); if ( $restrictions != '' ) { - $protectDescription .= $wgContLang->getDirMark() . "[$action=$restrictions] ("; + $protectDescriptionLog .= $wgContLang->getDirMark() . "[$action=$restrictions] ("; + # $action is one of $wgRestrictionTypes = array( 'create', 'edit', 'move', 'upload' ). + # All possible message keys are listed here for easier grepping: + # * restriction-create + # * restriction-edit + # * restriction-move + # * restriction-upload + $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text(); + # $restrictions is one of $wgRestrictionLevels = array( '', 'autoconfirmed', 'sysop' ), + # with '' filtered out. All possible message keys are listed below: + # * protect-level-autoconfirmed + # * protect-level-sysop + $restrictionsText = wfMessage( 'protect-level-' . $restrictions )->inContentLanguage()->text(); if ( $encodedExpiry[$action] != 'infinity' ) { - $protectDescription .= wfMessage( + $expiryText = wfMessage( 'protect-expiring', - $wgContLang->timeanddate( $expiry[$action], false, false ) , - $wgContLang->date( $expiry[$action], false, false ) , + $wgContLang->timeanddate( $expiry[$action], false, false ), + $wgContLang->date( $expiry[$action], false, false ), $wgContLang->time( $expiry[$action], false, false ) )->inContentLanguage()->text(); } else { - $protectDescription .= wfMessage( 'protect-expiry-indefinite' ) + $expiryText = wfMessage( 'protect-expiry-indefinite' ) ->inContentLanguage()->text(); } - $protectDescription .= ') '; + if ( $protectDescription !== '' ) { + $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text(); + } + $protectDescription .= wfMessage( 'protect-summary-desc' ) + ->params( $actionText, $restrictionsText, $expiryText ) + ->inContentLanguage()->text(); + $protectDescriptionLog .= $expiryText . ') '; } } - $protectDescription = trim( $protectDescription ); + $protectDescriptionLog = trim( $protectDescriptionLog ); - if ( $id ) { # Protection of existing page + if ( $id ) { // Protection of existing page if ( !wfRunHooks( 'ArticleProtect', array( &$this, &$user, $limit, $reason ) ) ) { return Status::newGood(); } - # Only restrictions with the 'protect' right can cascade... - # Otherwise, people who cannot normally protect can "protect" pages via transclusion + // Only restrictions with the 'protect' right can cascade... + // Otherwise, people who cannot normally protect can "protect" pages via transclusion $editrestriction = isset( $limit['edit'] ) ? array( $limit['edit'] ) : $this->mTitle->getRestrictions( 'edit' ); - # The schema allows multiple restrictions + // The schema allows multiple restrictions if ( !in_array( 'protect', $editrestriction ) && !in_array( 'sysop', $editrestriction ) ) { $cascade = false; } - # Update restrictions table + // Update restrictions table foreach ( $limit as $action => $restrictions ) { if ( $restrictions != '' ) { $dbw->replace( 'page_restrictions', array( array( 'pr_page', 'pr_type' ) ), @@ -1997,7 +2350,7 @@ class WikiPage extends Page implements IDBAccessObject { } } - # Prepare a null revision to be added to the history + // Prepare a null revision to be added to the history $editComment = $wgContLang->ucfirst( wfMessage( $revCommentMsg, @@ -2005,23 +2358,25 @@ class WikiPage extends Page implements IDBAccessObject { )->inContentLanguage()->text() ); if ( $reason ) { - $editComment .= ": $reason"; + $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason; } if ( $protectDescription ) { - $editComment .= " ($protectDescription)"; + $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text(); + $editComment .= wfMessage( 'parentheses' )->params( $protectDescription )->inContentLanguage()->text(); } if ( $cascade ) { - // FIXME: Should use 'brackets' message. - $editComment .= ' [' . wfMessage( 'protect-summary-cascade' ) - ->inContentLanguage()->text() . ']'; + $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text(); + $editComment .= wfMessage( 'brackets' )->params( + wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text() + )->inContentLanguage()->text(); } - # Insert a null revision + // Insert a null revision $nullRevision = Revision::newNullRevision( $dbw, $id, $editComment, true ); $nullRevId = $nullRevision->insertOn( $dbw ); $latest = $this->getLatest(); - # Update page record + // Update page record $dbw->update( 'page', array( /* SET */ 'page_touched' => $dbw->timestamp(), @@ -2034,8 +2389,8 @@ class WikiPage extends Page implements IDBAccessObject { wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $nullRevision, $latest, $user ) ); wfRunHooks( 'ArticleProtectComplete', array( &$this, &$user, $limit, $reason ) ); - } else { # Protection of non-existing page (also known as "title protection") - # Cascade protection is meaningless in this case + } else { // Protection of non-existing page (also known as "title protection") + // Cascade protection is meaningless in this case $cascade = false; if ( $limit['create'] != '' ) { @@ -2066,10 +2421,10 @@ class WikiPage extends Page implements IDBAccessObject { if ( $logAction == 'unprotect' ) { $logParams = array(); } else { - $logParams = array( $protectDescription, $cascade ? 'cascade' : '' ); + $logParams = array( $protectDescriptionLog, $cascade ? 'cascade' : '' ); } - # Update the protection log + // Update the protection log $log = new LogPage( 'protect' ); $log->addEntry( $logAction, $this->mTitle, trim( $reason ), $logParams, $user ); @@ -2107,10 +2462,10 @@ class WikiPage extends Page implements IDBAccessObject { * * Deletes the article with database consistency, writes logs, purges caches * - * @param $reason string delete reason for deletion log + * @param string $reason delete reason for deletion log * @param $suppress boolean suppress all revisions and log the deletion in * the suppression log instead of the deletion log - * @param $id int article ID + * @param int $id article ID * @param $commit boolean defaults to true, triggers transaction end * @param &$error Array of errors to append to * @param $user User The deleting user @@ -2129,9 +2484,10 @@ class WikiPage extends Page implements IDBAccessObject { * * @since 1.19 * - * @param $reason string delete reason for deletion log + * @param string $reason delete reason for deletion log * @param $suppress boolean suppress all revisions and log the deletion in * the suppression log instead of the deletion log + * @param int $id article ID * @param $commit boolean defaults to true, triggers transaction end * @param &$error Array of errors to append to * @param $user User The deleting user @@ -2142,7 +2498,7 @@ class WikiPage extends Page implements IDBAccessObject { public function doDeleteArticleReal( $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null ) { - global $wgUser; + global $wgUser, $wgContentHandlerUseDB; wfDebug( __METHOD__ . "\n" ); @@ -2183,6 +2539,9 @@ class WikiPage extends Page implements IDBAccessObject { $bitfield = 'rev_deleted'; } + // we need to remember the old content so we can use it to generate all deletion updates. + $content = $this->getContent( Revision::RAW ); + $dbw = wfGetDB( DB_MASTER ); $dbw->begin( __METHOD__ ); // For now, shunt the revision data into the archive table. @@ -2195,31 +2554,40 @@ class WikiPage extends Page implements IDBAccessObject { // // In the future, we may keep revisions and mark them with // the rev_deleted field, which is reserved for this purpose. + + $row = array( + 'ar_namespace' => 'page_namespace', + 'ar_title' => 'page_title', + 'ar_comment' => 'rev_comment', + 'ar_user' => 'rev_user', + 'ar_user_text' => 'rev_user_text', + 'ar_timestamp' => 'rev_timestamp', + 'ar_minor_edit' => 'rev_minor_edit', + 'ar_rev_id' => 'rev_id', + 'ar_parent_id' => 'rev_parent_id', + 'ar_text_id' => 'rev_text_id', + 'ar_text' => '\'\'', // Be explicit to appease + 'ar_flags' => '\'\'', // MySQL's "strict mode"... + 'ar_len' => 'rev_len', + 'ar_page_id' => 'page_id', + 'ar_deleted' => $bitfield, + 'ar_sha1' => 'rev_sha1', + ); + + if ( $wgContentHandlerUseDB ) { + $row[ 'ar_content_model' ] = 'rev_content_model'; + $row[ 'ar_content_format' ] = 'rev_content_format'; + } + $dbw->insertSelect( 'archive', array( 'page', 'revision' ), + $row, array( - 'ar_namespace' => 'page_namespace', - 'ar_title' => 'page_title', - 'ar_comment' => 'rev_comment', - 'ar_user' => 'rev_user', - 'ar_user_text' => 'rev_user_text', - 'ar_timestamp' => 'rev_timestamp', - 'ar_minor_edit' => 'rev_minor_edit', - 'ar_rev_id' => 'rev_id', - 'ar_parent_id' => 'rev_parent_id', - 'ar_text_id' => 'rev_text_id', - 'ar_text' => '\'\'', // Be explicit to appease - 'ar_flags' => '\'\'', // MySQL's "strict mode"... - 'ar_len' => 'rev_len', - 'ar_page_id' => 'page_id', - 'ar_deleted' => $bitfield, - 'ar_sha1' => 'rev_sha1' - ), array( 'page_id' => $id, 'page_id = rev_page' ), __METHOD__ ); - # Now that it's safely backed up, delete it + // Now that it's safely backed up, delete it $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__ ); $ok = ( $dbw->affectedRows() > 0 ); // getArticleID() uses slave, could be laggy @@ -2229,9 +2597,9 @@ class WikiPage extends Page implements IDBAccessObject { return $status; } - $this->doDeleteUpdates( $id ); + $this->doDeleteUpdates( $id, $content ); - # Log the deletion, if the page was suppressed, log it at Oversight instead + // Log the deletion, if the page was suppressed, log it at Oversight instead $logtype = $suppress ? 'suppress' : 'delete'; $logEntry = new ManualLogEntry( $logtype, 'delete' ); @@ -2245,7 +2613,7 @@ class WikiPage extends Page implements IDBAccessObject { $dbw->commit( __METHOD__ ); } - wfRunHooks( 'ArticleDeleteComplete', array( &$this, &$user, $reason, $id ) ); + wfRunHooks( 'ArticleDeleteComplete', array( &$this, &$user, $reason, $id, $content, $logEntry ) ); $status->value = $logid; return $status; } @@ -2253,36 +2621,28 @@ class WikiPage extends Page implements IDBAccessObject { /** * Do some database updates after deletion * - * @param $id Int: page_id value of the page being deleted (B/C, currently unused) + * @param int $id page_id value of the page being deleted (B/C, currently unused) + * @param $content Content: optional page content to be used when determining the required updates. + * This may be needed because $this->getContent() may already return null when the page proper was deleted. */ - public function doDeleteUpdates( $id ) { - # update site status + public function doDeleteUpdates( $id, Content $content = null ) { + // update site status DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) ); - # remove secondary indexes, etc - $updates = $this->getDeletionUpdates( ); + // remove secondary indexes, etc + $updates = $this->getDeletionUpdates( $content ); DataUpdate::runUpdates( $updates ); - # Clear caches + // Clear caches WikiPage::onArticleDelete( $this->mTitle ); - # Reset this object + // Reset this object $this->clear(); - # Clear the cached article id so the interface doesn't act like we exist + // Clear the cached article id so the interface doesn't act like we exist $this->mTitle->resetArticleID( 0 ); } - public function getDeletionUpdates() { - $updates = array( - new LinksDeletionUpdate( $this ), - ); - - //@todo: make a hook to add update objects - //NOTE: deletion updates will be determined by the ContentHandler in the future - return $updates; - } - /** * Roll back the most recent consecutive set of edits to a page * from the same user; fails if there are no eligible edits to @@ -2290,14 +2650,14 @@ class WikiPage extends Page implements IDBAccessObject { * performs permissions checks on $user, then calls commitRollback() * to do the dirty work * - * @todo: seperate the business/permission stuff out from backend code + * @todo: separate the business/permission stuff out from backend code * - * @param $fromP String: Name of the user whose edits to rollback. - * @param $summary String: Custom summary. Set to default summary if empty. - * @param $token String: Rollback token. + * @param string $fromP Name of the user whose edits to rollback. + * @param string $summary Custom summary. Set to default summary if empty. + * @param string $token Rollback token. * @param $bot Boolean: If true, mark all reverted edits as bot. * - * @param $resultDetails Array: contains result-specific array of additional values + * @param array $resultDetails contains result-specific array of additional values * 'alreadyrolled' : 'current' (rev) * success : 'summary' (str), 'current' (rev), 'target' (rev) * @@ -2312,7 +2672,7 @@ class WikiPage extends Page implements IDBAccessObject { ) { $resultDetails = null; - # Check permissions + // Check permissions $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user ); $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user ); $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) ); @@ -2325,7 +2685,7 @@ class WikiPage extends Page implements IDBAccessObject { $errors[] = array( 'actionthrottledtext' ); } - # If there were errors, bail out now + // If there were errors, bail out now if ( !empty( $errors ) ) { return $errors; } @@ -2341,11 +2701,11 @@ class WikiPage extends Page implements IDBAccessObject { * rollback to the DB. Therefore, you should only call this function direct- * ly if you want to use custom permissions checks. If you don't, use * doRollback() instead. - * @param $fromP String: Name of the user whose edits to rollback. - * @param $summary String: Custom summary. Set to default summary if empty. + * @param string $fromP Name of the user whose edits to rollback. + * @param string $summary Custom summary. Set to default summary if empty. * @param $bot Boolean: If true, mark all reverted edits as bot. * - * @param $resultDetails Array: contains result-specific array of additional values + * @param array $resultDetails contains result-specific array of additional values * @param $guser User The user performing the rollback * @return array */ @@ -2358,16 +2718,16 @@ class WikiPage extends Page implements IDBAccessObject { return array( array( 'readonlytext' ) ); } - # Get the last editor + // Get the last editor $current = $this->getRevision(); if ( is_null( $current ) ) { - # Something wrong... no page? + // Something wrong... no page? return array( array( 'notanarticle' ) ); } $from = str_replace( '_', ' ', $fromP ); - # User name given should match up with the top revision. - # If the user was deleted then $from should be empty. + // User name given should match up with the top revision. + // If the user was deleted then $from should be empty. if ( $from != $current->getUserText() ) { $resultDetails = array( 'current' => $current ); return array( array( 'alreadyrolled', @@ -2377,8 +2737,8 @@ class WikiPage extends Page implements IDBAccessObject { ) ); } - # Get the last edit not by this guy... - # Note: these may not be public values + // Get the last edit not by this guy... + // Note: these may not be public values $user = intval( $current->getRawUser() ); $user_text = $dbw->addQuotes( $current->getRawUserText() ); $s = $dbw->selectRow( 'revision', @@ -2390,21 +2750,21 @@ class WikiPage extends Page implements IDBAccessObject { 'ORDER BY' => 'rev_timestamp DESC' ) ); if ( $s === false ) { - # No one else ever edited this page + // No one else ever edited this page return array( array( 'cantrollback' ) ); } elseif ( $s->rev_deleted & Revision::DELETED_TEXT || $s->rev_deleted & Revision::DELETED_USER ) { - # Only admins can see this text + // Only admins can see this text return array( array( 'notvisiblerev' ) ); } $set = array(); if ( $bot && $guser->isAllowed( 'markbotedits' ) ) { - # Mark all reverted edits as bot + // Mark all reverted edits as bot $set['rc_bot'] = 1; } if ( $wgUseRCPatrol ) { - # Mark all reverted edits as patrolled + // Mark all reverted edits as patrolled $set['rc_patrolled'] = 1; } @@ -2418,7 +2778,7 @@ class WikiPage extends Page implements IDBAccessObject { ); } - # Generate the edit summary if necessary + // Generate the edit summary if necessary $target = Revision::newFromId( $s->rev_id ); if ( empty( $summary ) ) { if ( $from == '' ) { // no public user name @@ -2428,7 +2788,7 @@ class WikiPage extends Page implements IDBAccessObject { } } - # Allow the custom summary to use the same args as the default message + // Allow the custom summary to use the same args as the default message $args = array( $target->getUserText(), $from, $s->rev_id, $wgContLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ), @@ -2440,10 +2800,13 @@ class WikiPage extends Page implements IDBAccessObject { $summary = wfMsgReplaceArgs( $summary, $args ); } - # Truncate for whole multibyte characters. + // Trim spaces on user supplied text + $summary = trim( $summary ); + + // Truncate for whole multibyte characters. $summary = $wgContLang->truncate( $summary, 255 ); - # Save + // Save $flags = EDIT_UPDATE; if ( $guser->isAllowed( 'minoredit' ) ) { @@ -2454,8 +2817,13 @@ class WikiPage extends Page implements IDBAccessObject { $flags |= EDIT_FORCE_BOT; } - # Actually store the edit - $status = $this->doEdit( $target->getText(), $summary, $flags, $target->getId(), $guser ); + // Actually store the edit + $status = $this->doEditContent( $target->getContent(), $summary, $flags, $target->getId(), $guser ); + + if ( !$status->isOK() ) { + return $status->getErrorsArray(); + } + if ( !empty( $status->value['revision'] ) ) { $revId = $status->value['revision']->getId(); } else { @@ -2486,7 +2854,7 @@ class WikiPage extends Page implements IDBAccessObject { * @param $title Title object */ public static function onArticleCreate( $title ) { - # Update existence markers on article/talk tabs... + // Update existence markers on article/talk tabs... if ( $title->isTalkPage() ) { $other = $title->getSubjectPage(); } else { @@ -2507,7 +2875,7 @@ class WikiPage extends Page implements IDBAccessObject { * @param $title Title */ public static function onArticleDelete( $title ) { - # Update existence markers on article/talk tabs... + // Update existence markers on article/talk tabs... if ( $title->isTalkPage() ) { $other = $title->getSubjectPage(); } else { @@ -2520,21 +2888,21 @@ class WikiPage extends Page implements IDBAccessObject { $title->touchLinks(); $title->purgeSquid(); - # File cache + // File cache HTMLFileCache::clearFileCache( $title ); - # Messages + // Messages if ( $title->getNamespace() == NS_MEDIAWIKI ) { MessageCache::singleton()->replace( $title->getDBkey(), false ); } - # Images + // Images if ( $title->getNamespace() == NS_FILE ) { $update = new HTMLCacheUpdate( $title, 'imagelinks' ); $update->doUpdate(); } - # User talk pages + // User talk pages if ( $title->getNamespace() == NS_USER_TALK ) { $user = User::newFromName( $title->getText(), false ); if ( $user ) { @@ -2542,7 +2910,7 @@ class WikiPage extends Page implements IDBAccessObject { } } - # Image redirects + // Image redirects RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title ); } @@ -2556,14 +2924,13 @@ class WikiPage extends Page implements IDBAccessObject { // Invalidate caches of articles which include this page DeferredUpdates::addHTMLCacheUpdate( $title, 'templatelinks' ); - // Invalidate the caches of all pages which redirect here DeferredUpdates::addHTMLCacheUpdate( $title, 'redirect' ); - # Purge squid for this page only + // Purge squid for this page only $title->purgeSquid(); - # Clear file cache for this page only + // Clear file cache for this page only HTMLFileCache::clearFileCache( $title ); } @@ -2600,61 +2967,24 @@ class WikiPage extends Page implements IDBAccessObject { } /** - * Return an applicable autosummary if one exists for the given edit. - * @param $oldtext String: the previous text of the page. - * @param $newtext String: The submitted text of the page. - * @param $flags Int bitmask: a bitmask of flags submitted for the edit. - * @return string An appropriate autosummary, or an empty string. - */ + * Return an applicable autosummary if one exists for the given edit. + * @param string|null $oldtext the previous text of the page. + * @param string|null $newtext The submitted text of the page. + * @param int $flags bitmask: a bitmask of flags submitted for the edit. + * @return string An appropriate autosummary, or an empty string. + * + * @deprecated since 1.21, use ContentHandler::getAutosummary() instead + */ public static function getAutosummary( $oldtext, $newtext, $flags ) { - global $wgContLang; - - # Decide what kind of autosummary is needed. - - # Redirect autosummaries - $ot = Title::newFromRedirect( $oldtext ); - $rt = Title::newFromRedirect( $newtext ); - - if ( is_object( $rt ) && ( !is_object( $ot ) || !$rt->equals( $ot ) || $ot->getFragment() != $rt->getFragment() ) ) { - $truncatedtext = $wgContLang->truncate( - str_replace( "\n", ' ', $newtext ), - max( 0, 255 - - strlen( wfMessage( 'autoredircomment' )->inContentLanguage()->text() ) - - strlen( $rt->getFullText() ) - ) ); - return wfMessage( 'autoredircomment', $rt->getFullText() ) - ->rawParams( $truncatedtext )->inContentLanguage()->text(); - } - - # New page autosummaries - if ( $flags & EDIT_NEW && strlen( $newtext ) ) { - # If they're making a new article, give its text, truncated, in the summary. - - $truncatedtext = $wgContLang->truncate( - str_replace( "\n", ' ', $newtext ), - max( 0, 200 - strlen( wfMessage( 'autosumm-new' )->inContentLanguage()->text() ) ) ); - - return wfMessage( 'autosumm-new' )->rawParams( $truncatedtext ) - ->inContentLanguage()->text(); - } + // NOTE: stub for backwards-compatibility. assumes the given text is wikitext. will break horribly if it isn't. - # Blanking autosummaries - if ( $oldtext != '' && $newtext == '' ) { - return wfMessage( 'autosumm-blank' )->inContentLanguage()->text(); - } elseif ( strlen( $oldtext ) > 10 * strlen( $newtext ) && strlen( $newtext ) < 500 ) { - # Removing more than 90% of the article + ContentHandler::deprecated( __METHOD__, '1.21' ); - $truncatedtext = $wgContLang->truncate( - $newtext, - max( 0, 200 - strlen( wfMessage( 'autosumm-replace' )->inContentLanguage()->text() ) ) ); + $handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT ); + $oldContent = is_null( $oldtext ) ? null : $handler->unserializeContent( $oldtext ); + $newContent = is_null( $newtext ) ? null : $handler->unserializeContent( $newtext ); - return wfMessage( 'autosumm-replace' )->rawParams( $truncatedtext ) - ->inContentLanguage()->text(); - } - - # If we reach this point, there's no applicable autosummary for our case, so our - # autosummary is empty. - return ''; + return $handler->getAutosummary( $oldContent, $newContent, $flags ); } /** @@ -2665,117 +2995,29 @@ class WikiPage extends Page implements IDBAccessObject { * if no revision occurred */ public function getAutoDeleteReason( &$hasHistory ) { - global $wgContLang; - - // Get the last revision - $rev = $this->getRevision(); - - if ( is_null( $rev ) ) { - return false; - } - - // Get the article's contents - $contents = $rev->getText(); - $blank = false; - - // If the page is blank, use the text from the previous revision, - // which can only be blank if there's a move/import/protect dummy revision involved - if ( $contents == '' ) { - $prev = $rev->getPrevious(); - - if ( $prev ) { - $contents = $prev->getText(); - $blank = true; - } - } - - $dbw = wfGetDB( DB_MASTER ); - - // Find out if there was only one contributor - // Only scan the last 20 revisions - $res = $dbw->select( 'revision', 'rev_user_text', - array( 'rev_page' => $this->getID(), $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ), - __METHOD__, - array( 'LIMIT' => 20 ) - ); - - if ( $res === false ) { - // This page has no revisions, which is very weird - return false; - } - - $hasHistory = ( $res->numRows() > 1 ); - $row = $dbw->fetchObject( $res ); - - if ( $row ) { // $row is false if the only contributor is hidden - $onlyAuthor = $row->rev_user_text; - // Try to find a second contributor - foreach ( $res as $row ) { - if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999 - $onlyAuthor = false; - break; - } - } - } else { - $onlyAuthor = false; - } - - // Generate the summary with a '$1' placeholder - if ( $blank ) { - // The current revision is blank and the one before is also - // blank. It's just not our lucky day - $reason = wfMessage( 'exbeforeblank', '$1' )->inContentLanguage()->text(); - } else { - if ( $onlyAuthor ) { - $reason = wfMessage( - 'excontentauthor', - '$1', - $onlyAuthor - )->inContentLanguage()->text(); - } else { - $reason = wfMessage( 'excontent', '$1' )->inContentLanguage()->text(); - } - } - - if ( $reason == '-' ) { - // Allow these UI messages to be blanked out cleanly - return ''; - } - - // Replace newlines with spaces to prevent uglyness - $contents = preg_replace( "/[\n\r]/", ' ', $contents ); - // Calculate the maximum amount of chars to get - // Max content length = max comment length - length of the comment (excl. $1) - $maxLength = 255 - ( strlen( $reason ) - 2 ); - $contents = $wgContLang->truncate( $contents, $maxLength ); - // Remove possible unfinished links - $contents = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $contents ); - // Now replace the '$1' placeholder - $reason = str_replace( '$1', $contents, $reason ); - - return $reason; + return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory ); } /** * Update all the appropriate counts in the category table, given that * we've added the categories $added and deleted the categories $deleted. * - * @param $added array The names of categories that were added - * @param $deleted array The names of categories that were deleted + * @param array $added The names of categories that were added + * @param array $deleted The names of categories that were deleted */ public function updateCategoryCounts( $added, $deleted ) { $ns = $this->mTitle->getNamespace(); $dbw = wfGetDB( DB_MASTER ); - # First make sure the rows exist. If one of the "deleted" ones didn't - # exist, we might legitimately not create it, but it's simpler to just - # create it and then give it a negative value, since the value is bogus - # anyway. - # - # Sometimes I wish we had INSERT ... ON DUPLICATE KEY UPDATE. + // First make sure the rows exist. If one of the "deleted" ones didn't + // exist, we might legitimately not create it, but it's simpler to just + // create it and then give it a negative value, since the value is bogus + // anyway. + // + // Sometimes I wish we had INSERT ... ON DUPLICATE KEY UPDATE. $insertCats = array_merge( $added, $deleted ); if ( !$insertCats ) { - # Okay, nothing to do + // Okay, nothing to do return; } @@ -2789,14 +3031,14 @@ class WikiPage extends Page implements IDBAccessObject { } $dbw->insert( 'category', $insertRows, __METHOD__, 'IGNORE' ); - $addFields = array( 'cat_pages = cat_pages + 1' ); + $addFields = array( 'cat_pages = cat_pages + 1' ); $removeFields = array( 'cat_pages = cat_pages - 1' ); if ( $ns == NS_CATEGORY ) { - $addFields[] = 'cat_subcats = cat_subcats + 1'; + $addFields[] = 'cat_subcats = cat_subcats + 1'; $removeFields[] = 'cat_subcats = cat_subcats - 1'; } elseif ( $ns == NS_FILE ) { - $addFields[] = 'cat_files = cat_files + 1'; + $addFields[] = 'cat_files = cat_files + 1'; $removeFields[] = 'cat_files = cat_files - 1'; } @@ -2817,6 +3059,15 @@ class WikiPage extends Page implements IDBAccessObject { __METHOD__ ); } + + foreach( $added as $catName ) { + $cat = Category::newFromName( $catName ); + wfRunHooks( 'CategoryAfterPageAdded', array( $cat, $this ) ); + } + foreach( $deleted as $catName ) { + $cat = Category::newFromName( $catName ); + wfRunHooks( 'CategoryAfterPageRemoved', array( $cat, $this ) ); + } } /** @@ -2836,7 +3087,7 @@ class WikiPage extends Page implements IDBAccessObject { // that cascaded protections apply as soon as the changes // are visible. - # Get templates from templatelinks + // Get templates from templatelinks $id = $this->mTitle->getArticleID(); $tlTemplates = array(); @@ -2852,7 +3103,7 @@ class WikiPage extends Page implements IDBAccessObject { $tlTemplates["{$row->tl_namespace}:{$row->tl_title}"] = true; } - # Get templates from parser output. + // Get templates from parser output. $poTemplates = array(); foreach ( $parserOutput->getTemplates() as $ns => $templates ) { foreach ( $templates as $dbk => $id ) { @@ -2860,12 +3111,12 @@ class WikiPage extends Page implements IDBAccessObject { } } - # Get the diff + // Get the diff $templates_diff = array_diff_key( $poTemplates, $tlTemplates ); if ( count( $templates_diff ) > 0 ) { - # Whee, link updates time. - # Note: we are only interested in links here. We don't need to get other DataUpdate items from the parser output. + // Whee, link updates time. + // Note: we are only interested in links here. We don't need to get other DataUpdate items from the parser output. $u = new LinksUpdate( $this->mTitle, $parserOutput, false ); $u->doUpdate(); } @@ -2903,7 +3154,7 @@ class WikiPage extends Page implements IDBAccessObject { * so we can do things like signatures and links-in-context. * * @deprecated in 1.19; use Parser::preSaveTransform() instead - * @param $text String article contents + * @param string $text article contents * @param $user User object: user doing the edit * @param $popts ParserOptions object: parser options, default options for * the user loaded if null given @@ -2950,10 +3201,10 @@ class WikiPage extends Page implements IDBAccessObject { * Update the article's restriction field, and leave a log entry. * * @deprecated since 1.19 - * @param $limit Array: set of restriction keys + * @param array $limit set of restriction keys * @param $reason String * @param &$cascade Integer. Set to false if cascading protection isn't allowed. - * @param $expiry Array: per restriction type expiration + * @param array $expiry per restriction type expiration * @param $user User The user updating the restrictions * @return bool true on success */ @@ -2995,6 +3246,31 @@ class WikiPage extends Page implements IDBAccessObject { global $wgUser; return $this->isParserCacheUsed( ParserOptions::newFromUser( $wgUser ), $oldid ); } + + /** + * Returns a list of updates to be performed when this page is deleted. The updates should remove any information + * about this page from secondary data stores such as links tables. + * + * @param Content|null $content optional Content object for determining the necessary updates + * @return Array an array of DataUpdates objects + */ + public function getDeletionUpdates( Content $content = null ) { + if ( !$content ) { + // load content object, which may be used to determine the necessary updates + // XXX: the content may not be needed to determine the updates, then this would be overhead. + $content = $this->getContent( Revision::RAW ); + } + + if ( !$content ) { + $updates = array(); + } else { + $updates = $content->getDeletionUpdates( $this ); + } + + wfRunHooks( 'WikiPageDeletionUpdates', array( $this, $content, &$updates ) ); + return $updates; + } + } class PoolWorkArticleView extends PoolCounterWork { @@ -3020,9 +3296,9 @@ class PoolWorkArticleView extends PoolCounterWork { private $parserOptions; /** - * @var string|null + * @var Content|null */ - private $text; + private $content = null; /** * @var ParserOutput|bool @@ -3046,14 +3322,20 @@ class PoolWorkArticleView extends PoolCounterWork { * @param $revid Integer: ID of the revision being parsed * @param $useParserCache Boolean: whether to use the parser cache * @param $parserOptions parserOptions to use for the parse operation - * @param $text String: text to parse or null to load it + * @param $content Content|String: content to parse or null to load it; may also be given as a wikitext string, for BC */ - function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $text = null ) { + function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $content = null ) { + if ( is_string( $content ) ) { // BC: old style call + $modelId = $page->getRevision()->getContentModel(); + $format = $page->getRevision()->getContentFormat(); + $content = ContentHandler::makeContent( $content, $page->getTitle(), $modelId, $format ); + } + $this->page = $page; $this->revid = $revid; $this->cacheable = $useParserCache; $this->parserOptions = $parserOptions; - $this->text = $text; + $this->content = $content; $this->cacheKey = ParserCache::singleton()->getKey( $page, $parserOptions ); parent::__construct( 'ArticleView', $this->cacheKey . ':revid:' . $revid ); } @@ -3089,28 +3371,38 @@ class PoolWorkArticleView extends PoolCounterWork { * @return bool */ function doWork() { - global $wgParser, $wgUseFileCache; + global $wgUseFileCache; + + // @todo: several of the methods called on $this->page are not declared in Page, but present + // in WikiPage and delegated by Article. $isCurrent = $this->revid === $this->page->getLatest(); - if ( $this->text !== null ) { - $text = $this->text; + if ( $this->content !== null ) { + $content = $this->content; } elseif ( $isCurrent ) { - $text = $this->page->getRawText(); + // XXX: why use RAW audience here, and PUBLIC (default) below? + $content = $this->page->getContent( Revision::RAW ); } else { $rev = Revision::newFromTitle( $this->page->getTitle(), $this->revid ); + if ( $rev === null ) { - return false; + $content = null; + } else { + // XXX: why use PUBLIC audience here (default), and RAW above? + $content = $rev->getContent(); } - $text = $rev->getText(); + } + + if ( $content === null ) { + return false; } $time = - microtime( true ); - $this->parserOutput = $wgParser->parse( $text, $this->page->getTitle(), - $this->parserOptions, true, true, $this->revid ); + $this->parserOutput = $content->getParserOutput( $this->page->getTitle(), $this->revid, $this->parserOptions ); $time += microtime( true ); - # Timing hack + // Timing hack if ( $time > 3 ) { wfDebugLog( 'slow-parse', sprintf( "%-5.2f %s", $time, $this->page->getTitle()->getPrefixedDBkey() ) ); -- cgit v1.2.2