* @file
*/
-/**
- * Abstract class for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
- */
-interface Page {
-}
+use \MediaWiki\Logger\LoggerFactory;
+use \MediaWiki\MediaWikiServices;
/**
* Class representing a MediaWiki article and history.
*/
protected $mLinksUpdated = '19700101000000';
+ const PURGE_CDN_CACHE = 1; // purge CDN cache for page variant URLs
+ const PURGE_CLUSTER_PCACHE = 2; // purge parser cache in the local datacenter
+ const PURGE_GLOBAL_PCACHE = 4; // set page_touched to clear parser cache in all datacenters
+ const PURGE_ALL = 7;
+
/**
* Constructor and clear the article
* @param Title $title Reference to a Title object.
$this->mTitle = $title;
}
+ /**
+ * Makes sure that the mTitle object is cloned
+ * to the newly cloned WikiPage.
+ */
+ public function __clone() {
+ $this->mTitle = clone $this->mTitle;
+ }
+
/**
* Create a WikiPage object of the appropriate class for the given title.
*
* @param Title $title
*
* @throws MWException
- * @return WikiPage Object of the appropriate type
+ * @return WikiPage|WikiCategoryPage|WikiFilePage
*/
public static function factory( Title $title ) {
$ns = $title->getNamespace();
throw new MWException( "Invalid or virtual namespace $ns given." );
}
+ $page = null;
+ if ( !Hooks::run( 'WikiPageFactory', [ $title, &$page ] ) ) {
+ return $page;
+ }
+
switch ( $ns ) {
case NS_FILE:
$page = new WikiFilePage( $title );
*
* @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
+ * - "fromdb" or WikiPage::READ_NORMAL to select from a replica DB
* - "fromdbmaster" or WikiPage::READ_LATEST to select from the master database
*
* @return WikiPage|null
}
$from = self::convertSelectType( $from );
- $db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_SLAVE );
+ $db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_REPLICA );
$row = $db->selectRow(
- 'page', self::selectFields(), array( 'page_id' => $id ), __METHOD__ );
+ 'page', self::selectFields(), [ 'page_id' => $id ], __METHOD__ );
if ( !$row ) {
return null;
}
* @since 1.20
* @param object $row Database row containing at least fields returned by selectFields().
* @param string|int $from Source of $data:
- * - "fromdb" or WikiPage::READ_NORMAL: from a slave DB
+ * - "fromdb" or WikiPage::READ_NORMAL: from a replica DB
* - "fromdbmaster" or WikiPage::READ_LATEST: from the master DB
* - "forupdate" or WikiPage::READ_LOCKING: from the master DB using SELECT FOR UPDATE
* @return WikiPage
}
/**
- * Returns overrides for action handlers.
- * Classes listed here will be used instead of the default one when
- * (and only when) $wgActions[$action] === true. This allows subclasses
- * to override the default behavior.
- *
* @todo Move this UI stuff somewhere else
*
- * @return array
+ * @see ContentHandler::getActionOverrides
*/
public function getActionOverrides() {
- $content_handler = $this->getContentHandler();
- return $content_handler->getActionOverrides();
+ return $this->getContentHandler()->getActionOverrides();
}
/**
public static function selectFields() {
global $wgContentHandlerUseDB, $wgPageLanguageUseDB;
- $fields = array(
+ $fields = [
'page_id',
'page_namespace',
'page_title',
'page_links_updated',
'page_latest',
'page_len',
- );
+ ];
if ( $wgContentHandlerUseDB ) {
$fields[] = 'page_content_model';
* @param array $options
* @return object|bool Database result resource, or false on failure
*/
- protected function pageData( $dbr, $conditions, $options = array() ) {
+ protected function pageData( $dbr, $conditions, $options = [] ) {
$fields = self::selectFields();
- Hooks::run( 'ArticlePageDataBefore', array( &$this, &$fields ) );
+ Hooks::run( 'ArticlePageDataBefore', [ &$this, &$fields ] );
$row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__, $options );
- Hooks::run( 'ArticlePageDataAfter', array( &$this, &$row ) );
+ Hooks::run( 'ArticlePageDataAfter', [ &$this, &$row ] );
return $row;
}
* @param array $options
* @return object|bool Database result resource, or false on failure
*/
- public function pageDataFromTitle( $dbr, $title, $options = array() ) {
- return $this->pageData( $dbr, array(
+ public function pageDataFromTitle( $dbr, $title, $options = [] ) {
+ return $this->pageData( $dbr, [
'page_namespace' => $title->getNamespace(),
- 'page_title' => $title->getDBkey() ), $options );
+ 'page_title' => $title->getDBkey() ], $options );
}
/**
* @param array $options
* @return object|bool Database result resource, or false on failure
*/
- public function pageDataFromId( $dbr, $id, $options = array() ) {
- return $this->pageData( $dbr, array( 'page_id' => $id ), $options );
+ public function pageDataFromId( $dbr, $id, $options = [] ) {
+ return $this->pageData( $dbr, [ 'page_id' => $id ], $options );
}
/**
*
* @param object|string|int $from One of the following:
* - A DB query result object.
- * - "fromdb" or WikiPage::READ_NORMAL to get from a slave DB.
+ * - "fromdb" or WikiPage::READ_NORMAL to get from a replica DB.
* - "fromdbmaster" or WikiPage::READ_LATEST to get from the master DB.
* - "forupdate" or WikiPage::READ_LOCKING to get from the master DB
* using SELECT FOR UPDATE.
$data = $this->pageDataFromTitle( wfGetDB( $index ), $this->mTitle, $opts );
if ( !$data
- && $index == DB_SLAVE
+ && $index == DB_REPLICA
&& wfGetLB()->getServerCount() > 1
&& wfGetLB()->hasOrMadeRecentMasterChanges()
) {
$data = $this->pageDataFromTitle( wfGetDB( $index ), $this->mTitle, $opts );
}
} else {
- // No idea from where the caller got this data, assume slave database.
+ // No idea from where the caller got this data, assume replica DB.
$data = $from;
$from = self::READ_NORMAL;
}
* @since 1.20
* @param object|bool $data DB row containing fields returned by selectFields() or false
* @param string|int $from One of the following:
- * - "fromdb" or WikiPage::READ_NORMAL if the data comes from a slave DB
+ * - "fromdb" or WikiPage::READ_NORMAL if the data comes from a replica DB
* - "fromdbmaster" or WikiPage::READ_LATEST if the data comes from the master DB
* - "forupdate" or WikiPage::READ_LOCKING if the data comes from
* the master DB using SELECT FOR UPDATE
* @return bool
*/
public function hasViewableContent() {
- return $this->exists() || $this->mTitle->isAlwaysKnown();
+ return $this->mTitle->isKnown();
}
/**
*/
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!" );
- }
+ $cache = ObjectCache::getMainWANInstance();
+
+ return $cache->getWithSetCallback(
+ $cache->makeKey( 'page', 'content-model', $this->getLatest() ),
+ $cache::TTL_MONTH,
+ function () {
+ $rev = $this->getRevision();
+ if ( $rev ) {
+ // Look at the revision's actual content model
+ return $rev->getContentModel();
+ } else {
+ $title = $this->mTitle->getPrefixedDBkey();
+ wfWarn( "Page $title exists but has no (visible) revisions!" );
+ return $this->mTitle->getContentModel();
+ }
+ }
+ );
}
// use the default model for this page
/**
* Loads page_touched and returns a value indicating if it should be used
- * @return bool True if not a redirect
+ * @return bool True if this page exists and is not a redirect
*/
public function checkTouched() {
if ( !$this->mDataLoaded ) {
$this->loadPageData();
}
- return !$this->mIsRedirect;
+ return ( $this->mId && !$this->mIsRedirect );
}
/**
*/
public function getOldestRevision() {
- // Try using the slave database first, then try the master
+ // Try using the replica DB first, then try the master
$continue = 2;
- $db = wfGetDB( DB_SLAVE );
+ $db = wfGetDB( DB_REPLICA );
$revSelectFields = Revision::selectFields();
$row = null;
while ( $continue ) {
$row = $db->selectRow(
- array( 'page', 'revision' ),
+ [ 'page', 'revision' ],
$revSelectFields,
- array(
+ [
'page_namespace' => $this->mTitle->getNamespace(),
'page_title' => $this->mTitle->getDBkey(),
'rev_page = page_id'
- ),
+ ],
__METHOD__,
- array(
+ [
'ORDER BY' => 'rev_timestamp ASC'
- )
+ ]
);
if ( $row ) {
// happened after the first S1 SELECT.
// http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read
$flags = Revision::READ_LOCKING;
+ $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
} elseif ( $this->mDataLoadedFrom == self::READ_LATEST ) {
// Bug T93976: if page_latest was loaded from the master, fetch the
- // revision from there as well, as it may not exist yet on a slave DB.
+ // revision from there as well, as it may not exist yet on a replica DB.
// Also, this keeps the queries in the same REPEATABLE-READ snapshot.
$flags = Revision::READ_LATEST;
+ $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
} else {
- $flags = 0;
+ $dbr = wfGetDB( DB_REPLICA );
+ $revision = Revision::newKnownCurrent( $dbr, $this->getId(), $latest );
}
- $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
+
if ( $revision ) { // sanity
$this->setLastEdit( $revision );
}
* @deprecated since 1.21, getContent() should be used instead.
*/
public function getText( $audience = Revision::FOR_PUBLIC, User $user = null ) {
- ContentHandler::deprecated( __METHOD__, '1.21' );
+ wfDeprecated( __METHOD__, '1.21' );
$this->loadLastEdit();
if ( $this->mLastRevision ) {
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 since 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
*/
// links.
$hasLinks = (bool)count( $editInfo->output->getLinks() );
} else {
- $hasLinks = (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1,
- array( 'pl_from' => $this->getId() ), __METHOD__ );
+ $hasLinks = (bool)wfGetDB( DB_REPLICA )->selectField( 'pagelinks', 1,
+ [ 'pl_from' => $this->getId() ], __METHOD__ );
}
}
}
// Query the redirect table
- $dbr = wfGetDB( DB_SLAVE );
+ $dbr = wfGetDB( DB_REPLICA );
$row = $dbr->selectRow( 'redirect',
- array( 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ),
- array( 'rd_from' => $this->getId() ),
+ [ 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
+ [ 'rd_from' => $this->getId() ],
__METHOD__
);
// Update the DB post-send if the page has not cached since now
$that = $this;
$latest = $this->getLatest();
- DeferredUpdates::addCallableUpdate( function() use ( $that, $retval, $latest ) {
- $that->insertRedirectEntry( $retval, $latest );
- } );
+ DeferredUpdates::addCallableUpdate(
+ function () use ( $that, $retval, $latest ) {
+ $that->insertRedirectEntry( $retval, $latest );
+ },
+ DeferredUpdates::POSTSEND,
+ wfGetDB( DB_MASTER )
+ );
return $retval;
}
if ( !$oldLatest || $oldLatest == $this->lockAndGetLatest() ) {
$dbw->replace( 'redirect',
- array( 'rd_from' ),
- array(
+ [ 'rd_from' ],
+ [
'rd_from' => $this->getId(),
'rd_namespace' => $rt->getNamespace(),
'rd_title' => $rt->getDBkey(),
'rd_fragment' => $rt->getFragment(),
'rd_interwiki' => $rt->getInterwiki(),
- ),
+ ],
__METHOD__
);
}
// This can be hard to reverse and may produce loops,
// so they may be disabled in the site configuration.
$source = $this->mTitle->getFullURL( 'redirect=no' );
- return $rt->getFullURL( array( 'rdfrom' => $source ) );
+ return $rt->getFullURL( [ 'rdfrom' => $source ] );
} else {
// External pages without "local" bit set are not valid
// redirect targets
public function getContributors() {
// @todo FIXME: This is expensive; cache this info somewhere.
- $dbr = wfGetDB( DB_SLAVE );
+ $dbr = wfGetDB( DB_REPLICA );
if ( $dbr->implicitGroupby() ) {
$realNameField = 'user_real_name';
$realNameField = 'MIN(user_real_name) AS user_real_name';
}
- $tables = array( 'revision', 'user' );
+ $tables = [ 'revision', 'user' ];
- $fields = array(
+ $fields = [
'user_id' => 'rev_user',
'user_name' => 'rev_user_text',
$realNameField,
'timestamp' => 'MAX(rev_timestamp)',
- );
+ ];
- $conds = array( 'rev_page' => $this->getId() );
+ $conds = [ 'rev_page' => $this->getId() ];
// The user who made the top revision gets credited as "this page was last edited by
// John, based on contributions by Tom, Dick and Harry", so don't include them twice.
// Username hidden?
$conds[] = "{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0";
- $jconds = array(
- 'user' => array( 'LEFT JOIN', 'rev_user = user_id' ),
- );
+ $jconds = [
+ 'user' => [ 'LEFT JOIN', 'rev_user = user_id' ],
+ ];
- $options = array(
- 'GROUP BY' => array( 'rev_user', 'rev_user_text' ),
+ $options = [
+ 'GROUP BY' => [ 'rev_user', 'rev_user_text' ],
'ORDER BY' => 'timestamp DESC',
- );
+ ];
$res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds );
return new UserArrayFromResult( $res );
*
* @since 1.19
* @param ParserOptions $parserOptions ParserOptions to use for the parse operation
- * @param null|int $oldid Revision ID to get the text from, passing null or 0 will
- * get the current revision (default value)
- *
- * @return ParserOutput|bool ParserOutput or false if the revision was not found
+ * @param null|int $oldid Revision ID to get the text from, passing null or 0 will
+ * get the current revision (default value)
+ * @param bool $forceParse Force reindexing, regardless of cache settings
+ * @return bool|ParserOutput ParserOutput or false if the revision was not found
*/
- public function getParserOutput( ParserOptions $parserOptions, $oldid = null ) {
-
- $useParserCache = $this->shouldCheckParserCache( $parserOptions, $oldid );
+ public function getParserOutput(
+ ParserOptions $parserOptions, $oldid = null, $forceParse = false
+ ) {
+ $useParserCache =
+ ( !$forceParse ) && $this->shouldCheckParserCache( $parserOptions, $oldid );
wfDebug( __METHOD__ .
': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
if ( $parserOptions->getStubThreshold() ) {
return;
}
- Hooks::run( 'PageViewUpdates', array( $this, $user ) );
+ Hooks::run( 'PageViewUpdates', [ $this, $user ] );
// Update newtalk / watchlist notification status
try {
$user->clearNotification( $this->mTitle, $oldid );
/**
* Perform the actions of a page purging
+ * @param integer $flags Bitfield of WikiPage::PURGE_* constants
* @return bool
*/
- public function doPurge() {
- if ( !Hooks::run( 'ArticlePurge', array( &$this ) ) ) {
+ public function doPurge( $flags = self::PURGE_ALL ) {
+ if ( !Hooks::run( 'ArticlePurge', [ &$this ] ) ) {
return false;
}
- $title = $this->mTitle;
- wfGetDB( DB_MASTER )->onTransactionIdle( function() use ( $title ) {
- // Invalidate the cache in auto-commit mode
- $title->invalidateCache();
- } );
+ if ( ( $flags & self::PURGE_GLOBAL_PCACHE ) == self::PURGE_GLOBAL_PCACHE ) {
+ // Set page_touched in the database to invalidate all DC caches
+ $this->mTitle->invalidateCache();
+ } elseif ( ( $flags & self::PURGE_CLUSTER_PCACHE ) == self::PURGE_CLUSTER_PCACHE ) {
+ // Delete the parser options key in the local cluster to invalidate the DC cache
+ ParserCache::singleton()->deleteOptionsKey( $this );
+ // Avoid sending HTTP 304s in ViewAction to the client who just issued the purge
+ $cache = ObjectCache::getLocalClusterInstance();
+ $cache->set(
+ $cache->makeKey( 'page', 'last-dc-purge', $this->getId() ),
+ wfTimestamp( TS_MW ),
+ $cache::TTL_HOUR
+ );
+ }
- // Send purge after above page_touched update was committed
- DeferredUpdates::addUpdate(
- new SquidUpdate( $title->getSquidURLs() ),
- DeferredUpdates::PRESEND
- );
+ if ( ( $flags & self::PURGE_CDN_CACHE ) == self::PURGE_CDN_CACHE ) {
+ // Clear any HTML file cache
+ HTMLFileCache::clearFileCache( $this->getTitle() );
+ // Send purge after any page_touched above update was committed
+ DeferredUpdates::addUpdate(
+ new CdnCacheUpdate( $this->mTitle->getCdnUrls() ),
+ DeferredUpdates::PRESEND
+ );
+ }
if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
// @todo move this logic to MessageCache
return true;
}
+
+ /**
+ * Get the last time a user explicitly purged the page via action=purge
+ *
+ * @return string|bool TS_MW timestamp or false
+ * @since 1.28
+ */
+ public function getLastPurgeTimestamp() {
+ $cache = ObjectCache::getLocalClusterInstance();
+
+ return $cache->get( $cache->makeKey( 'page', 'last-dc-purge', $this->getId() ) );
+ }
+
/**
* Insert a new empty page record for this article.
* This *must* be followed up by creating a revision
* Best if all done inside a transaction.
*
* @param IDatabase $dbw
- * @return int|bool The newly created page_id key; false if the title already existed
+ * @param int|null $pageId Custom page ID that will be used for the insert statement
+ *
+ * @return bool|int The newly created page_id key; false if the row was not
+ * inserted, e.g. because the title already existed or because the specified
+ * page ID is already in use.
*/
- public function insertOn( $dbw ) {
+ public function insertOn( $dbw, $pageId = null ) {
+ $pageIdForInsert = $pageId ?: $dbw->nextSequenceValue( 'page_page_id_seq' );
$dbw->insert(
'page',
- array(
- 'page_id' => $dbw->nextSequenceValue( 'page_page_id_seq' ),
+ [
+ 'page_id' => $pageIdForInsert,
'page_namespace' => $this->mTitle->getNamespace(),
'page_title' => $this->mTitle->getDBkey(),
'page_restrictions' => '',
'page_touched' => $dbw->timestamp(),
'page_latest' => 0, // Fill this in shortly...
'page_len' => 0, // Fill this in shortly...
- ),
+ ],
__METHOD__,
'IGNORE'
);
if ( $dbw->affectedRows() > 0 ) {
- $newid = $dbw->insertId();
+ $newid = $pageId ?: $dbw->insertId();
$this->mId = $newid;
$this->mTitle->resetArticleID( $newid );
$len = $content ? $content->getSize() : 0;
$rt = $content ? $content->getUltimateRedirectTarget() : null;
- $conditions = array( 'page_id' => $this->getId() );
+ $conditions = [ 'page_id' => $this->getId() ];
if ( !is_null( $lastRevision ) ) {
// An extra check against threads stepping on each other
$conditions['page_latest'] = $lastRevision;
}
- $row = array( /* SET */
+ $row = [ /* SET */
'page_latest' => $revision->getId(),
'page_touched' => $dbw->timestamp( $revision->getTimestamp() ),
'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();
$this->insertRedirectEntry( $redirectTitle );
} else {
// This is not a redirect, remove row from redirect table
- $where = array( 'rd_from' => $this->getId() );
+ $where = [ 'rd_from' => $this->getId() ];
$dbw->delete( 'redirect', $where, __METHOD__ );
}
public function updateIfNewerOn( $dbw, $revision ) {
$row = $dbw->selectRow(
- array( 'revision', 'page' ),
- array( 'rev_id', 'rev_timestamp', 'page_is_redirect' ),
- array(
+ [ 'revision', 'page' ],
+ [ 'rev_id', 'rev_timestamp', 'page_is_redirect' ],
+ [
'page_id' => $this->getId(),
- 'page_latest=rev_id' ),
+ 'page_latest=rev_id' ],
__METHOD__ );
if ( $row ) {
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,
- * must exist and must not be deleted
- * @param Revision $undo
- * @param Revision $undoafter Must be an earlier revision than $undo
- * @return string|bool String on success, false on failure
- * @deprecated since 1.21: use ContentHandler::getUndoContent() instead.
- */
- public function getUndoText( Revision $undo, Revision $undoafter = null ) {
- ContentHandler::deprecated( __METHOD__, '1.21' );
-
- $this->loadLastEdit();
-
- if ( $this->mLastRevision ) {
- if ( is_null( $undoafter ) ) {
- $undoafter = $undo->getPrevious();
- }
-
- $handler = $this->getContentHandler();
- $undone = $handler->getUndoContent( $this->mLastRevision, $undo, $undoafter );
-
- if ( !$undone ) {
- return false;
- } else {
- return ContentHandler::getContentText( $undone );
- }
- }
-
- return false;
- }
-
- /**
- * @param string|number|null|bool $sectionId Section identifier as a number or string
- * (e.g. 0, 1 or 'T-1'), null/false or an empty string for the whole page
- * or 'new' for a new section.
- * @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|null New complete article text, or null if error.
- *
- * @deprecated since 1.21, use replaceSectionAtRev() instead
- */
- public function replaceSection( $sectionId, $text, $sectionTitle = '',
- $edittime = null
- ) {
- ContentHandler::deprecated( __METHOD__, '1.21' );
-
- // NOTE: keep condition in sync with condition in replaceSectionContent!
- if ( strval( $sectionId ) === '' ) {
- // 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( $sectionId, $sectionContent, $sectionTitle,
- $edittime );
-
- return ContentHandler::getContentText( $newContent );
- }
-
/**
* Returns true if this page's content model supports sections.
*
$baseRevId = null;
if ( $edittime && $sectionId !== 'new' ) {
- $dbr = wfGetDB( DB_SLAVE );
+ $dbr = wfGetDB( DB_REPLICA );
$rev = Revision::loadFromTimestamp( $dbr, $this->mTitle, $edittime );
// Try the master if this thread may have just added it.
// This could be abstracted into a Revision method, but we don't want
* Mark the edit a "bot" edit regardless of user rights
* EDIT_AUTOSUMMARY
* Fill in blank summaries with generated text where possible
+ * EDIT_INTERNAL
+ * Signal that the page retrieve/save cycle happened entirely in this request.
*
* 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
* @deprecated since 1.21: use doEditContent() instead.
*/
public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) {
- ContentHandler::deprecated( __METHOD__, '1.21' );
+ wfDeprecated( __METHOD__, '1.21' );
$content = ContentHandler::makeContent( $text, $this->getTitle() );
* Mark the edit a "bot" edit regardless of user rights
* EDIT_AUTOSUMMARY
* Fill in blank summaries with generated text where possible
+ * EDIT_INTERNAL
+ * Signal that the page retrieve/save cycle happened entirely in this request.
*
* 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
* @param User $user The user doing the edit
* @param string $serialFormat Format for storing the content in the
* database.
+ * @param array|null $tags Change tags to apply to this edit
+ * Callers are responsible for permission checks
+ * (with ChangeTags::canAddTagsAccompanyingChange)
*
* @throws MWException
* @return Status Possible errors:
*/
public function doEditContent(
Content $content, $summary, $flags = 0, $baseRevId = false,
- User $user = null, $serialFormat = null
+ User $user = null, $serialFormat = null, $tags = []
) {
global $wgUser, $wgUseAutomaticEditSummaries;
+ // Old default parameter for $tags was null
+ if ( $tags === null ) {
+ $tags = [];
+ }
+
// Low-level sanity check
if ( $this->mTitle->getText() === '' ) {
throw new MWException( 'Something is trying to edit an article with an empty title' );
$flags = $this->checkFlags( $flags );
// Trigger pre-save hook (using provided edit summary)
- $hookStatus = Status::newGood( array() );
- $hook_args = array( &$this, &$user, &$content, &$summary,
- $flags & EDIT_MINOR, null, null, &$flags, &$hookStatus );
+ $hookStatus = Status::newGood( [] );
+ $hook_args = [ &$this, &$user, &$content, &$summary,
+ $flags & EDIT_MINOR, null, null, &$flags, &$hookStatus ];
// Check if the hook rejected the attempted save
if ( !Hooks::run( 'PageContentSave', $hook_args )
- || !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args )
+ || !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args, '1.21' )
) {
if ( $hookStatus->isOK() ) {
// Hook returned false but didn't call fatal(); use generic message
$old_revision = $this->getRevision(); // current revision
$old_content = $this->getContent( Revision::RAW ); // current revision's content
+ if ( $old_content && $old_content->getModel() !== $content->getModel() ) {
+ $tags[] = 'mw-contentmodelchange';
+ }
+
// Provide autosummaries if one is not provided and autosummaries are enabled
if ( $wgUseAutomaticEditSummaries && ( $flags & EDIT_AUTOSUMMARY ) && $summary == '' ) {
$handler = $content->getContentHandler();
$summary = $handler->getAutosummary( $old_content, $content, $flags );
}
+ // Avoid statsd noise and wasted cycles check the edit stash (T136678)
+ if ( ( $flags & EDIT_INTERNAL ) || ( $flags & EDIT_FORCE_BOT ) ) {
+ $useCache = false;
+ } else {
+ $useCache = true;
+ }
+
// Get the pre-save transform content and final parser output
- $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialFormat );
+ $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialFormat, $useCache );
$pstContent = $editInfo->pstContent; // Content object
- $meta = array(
+ $meta = [
'bot' => ( $flags & EDIT_FORCE_BOT ),
'minor' => ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' ),
'serialized' => $editInfo->pst,
'oldContent' => $old_content,
'oldId' => $this->getLatest(),
'oldIsRedirect' => $this->isRedirect(),
- 'oldCountable' => $this->isCountable()
- );
+ 'oldCountable' => $this->isCountable(),
+ 'tags' => ( $tags !== null ) ? (array)$tags : []
+ ];
// Actually create the revision and create/update the page
if ( $flags & EDIT_UPDATE ) {
$status = $this->doCreate( $pstContent, $flags, $user, $summary, $meta );
}
- // Trigger post-save hook
- $revision = $status->value['revision']; // new revision
- $hook_args = array( &$this, &$user, $pstContent, $summary,
- $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId );
- ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $hook_args );
- Hooks::run( 'PageContentSaveComplete', $hook_args );
-
// Promote user to any groups they meet the criteria for
DeferredUpdates::addCallableUpdate( function () use ( $user ) {
$user->addAutopromoteOnceGroups( 'onEdit' );
global $wgUseRCPatrol;
// Update article, but only if changed.
- $status = Status::newGood( array( 'new' => false, 'revision' => null ) );
+ $status = Status::newGood( [ 'new' => false, 'revision' => null ] );
// Convenience variables
$now = wfTimestampNow();
}
// @TODO: pass content object?!
- $revision = new Revision( array(
+ $revision = new Revision( [
'page' => $this->getId(),
'title' => $this->mTitle, // for determining the default content model
'comment' => $summary,
'timestamp' => $now,
'content_model' => $content->getModel(),
'content_format' => $meta['serialFormat'],
- ) );
+ ] );
$changed = !$content->equals( $oldContent );
+ $dbw = wfGetDB( DB_MASTER );
+
if ( $changed ) {
$prepStatus = $content->prepareSave( $this, $flags, $oldid, $user );
$status->merge( $prepStatus );
return $status;
}
- $dbw = wfGetDB( DB_MASTER );
- $dbw->begin( __METHOD__ );
+ $dbw->startAtomic( __METHOD__ );
// Get the latest page_latest value while locking it.
// Do a CAS style check to see if it's the same as when this method
// started. If it changed then bail out before touching the DB.
$latestNow = $this->lockAndGetLatest();
if ( $latestNow != $oldid ) {
- $dbw->commit( __METHOD__ );
+ $dbw->endAtomic( __METHOD__ );
// Page updated or deleted in the mean time
$status->fatal( 'edit-conflict' );
$revisionId = $revision->insertOn( $dbw );
// Update page_latest and friends to reflect the new revision
if ( !$this->updateRevisionOn( $dbw, $revision, null, $meta['oldIsRedirect'] ) ) {
- $dbw->rollback( __METHOD__ );
throw new MWException( "Failed to update page row to use new revision." );
}
Hooks::run( 'NewRevisionFromEditComplete',
- array( $this, $revision, $meta['baseRevId'], $user ) );
+ [ $this, $revision, $meta['baseRevId'], $user ] );
// Update recentchanges
if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
$oldContent ? $oldContent->getSize() : 0,
$newsize,
$revisionId,
- $patrolled
+ $patrolled,
+ $meta['tags']
);
}
$user->incEditCount();
- $dbw->commit( __METHOD__ );
+ $dbw->endAtomic( __METHOD__ );
$this->mTimestamp = $now;
} else {
// Bug 32948: revision ID must be set to page {{REVISIONID}} and
- // related variables correctly
+ // related variables correctly. Likewise for {{REVISIONUSER}} (T135261).
$revision->setId( $this->getLatest() );
+ $revision->setUserIdAndName(
+ $this->getUser( Revision::RAW ),
+ $this->getUserText( Revision::RAW )
+ );
}
- // Update links tables, site stats, etc.
- $this->doEditUpdates(
- $revision,
- $user,
- array(
- 'changed' => $changed,
- 'oldcountable' => $meta['oldCountable'],
- 'oldrevision' => $meta['oldRevision']
- )
- );
-
if ( $changed ) {
// Return the new revision to the caller
$status->value['revision'] = $revision;
$this->mTitle->invalidateCache( $now );
}
+ // Do secondary updates once the main changes have been committed...
+ DeferredUpdates::addUpdate(
+ new AtomicSectionUpdate(
+ $dbw,
+ __METHOD__,
+ function () use (
+ $revision, &$user, $content, $summary, &$flags,
+ $changed, $meta, &$status
+ ) {
+ // Update links tables, site stats, etc.
+ $this->doEditUpdates(
+ $revision,
+ $user,
+ [
+ 'changed' => $changed,
+ 'oldcountable' => $meta['oldCountable'],
+ 'oldrevision' => $meta['oldRevision']
+ ]
+ );
+ // Trigger post-save hook
+ $params = [ &$this, &$user, $content, $summary, $flags & EDIT_MINOR,
+ null, null, &$flags, $revision, &$status, $meta['baseRevId'] ];
+ ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $params );
+ Hooks::run( 'PageContentSaveComplete', $params );
+ }
+ ),
+ DeferredUpdates::PRESEND
+ );
+
return $status;
}
) {
global $wgUseRCPatrol, $wgUseNPPatrol;
- $status = Status::newGood( array( 'new' => true, 'revision' => null ) );
+ $status = Status::newGood( [ 'new' => true, 'revision' => null ] );
$now = wfTimestampNow();
$newsize = $content->getSize();
}
$dbw = wfGetDB( DB_MASTER );
- $dbw->begin( __METHOD__ );
+ $dbw->startAtomic( __METHOD__ );
// Add the page record unless one already exists for the title
$newid = $this->insertOn( $dbw );
if ( $newid === false ) {
- $dbw->commit( __METHOD__ ); // nothing inserted
+ $dbw->endAtomic( __METHOD__ ); // nothing inserted
$status->fatal( 'edit-already-exists' );
return $status; // nothing done
// unless they actually try to catch exceptions (which is rare).
// @TODO: pass content object?!
- $revision = new Revision( array(
+ $revision = new Revision( [
'page' => $newid,
'title' => $this->mTitle, // for determining the default content model
'comment' => $summary,
'timestamp' => $now,
'content_model' => $content->getModel(),
'content_format' => $meta['serialFormat'],
- ) );
+ ] );
// Save the revision text...
$revisionId = $revision->insertOn( $dbw );
// Update the page record with revision data
if ( !$this->updateRevisionOn( $dbw, $revision, 0 ) ) {
- $dbw->rollback( __METHOD__ );
throw new MWException( "Failed to update page row to use new revision." );
}
- Hooks::run( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) );
+ Hooks::run( 'NewRevisionFromEditComplete', [ $this, $revision, false, $user ] );
// Update recentchanges
if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
'',
$newsize,
$revisionId,
- $patrolled
+ $patrolled,
+ $meta['tags']
);
}
$user->incEditCount();
- $dbw->commit( __METHOD__ );
+ $dbw->endAtomic( __METHOD__ );
$this->mTimestamp = $now;
- // Update links, etc.
- $this->doEditUpdates( $revision, $user, array( 'created' => true ) );
-
- $hook_args = array( &$this, &$user, $content, $summary,
- $flags & EDIT_MINOR, null, null, &$flags, $revision );
- ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $hook_args );
- Hooks::run( 'PageContentInsertComplete', $hook_args );
-
// Return the new revision to the caller
$status->value['revision'] = $revision;
+ // Do secondary updates once the main changes have been committed...
+ DeferredUpdates::addUpdate(
+ new AtomicSectionUpdate(
+ $dbw,
+ __METHOD__,
+ function () use (
+ $revision, &$user, $content, $summary, &$flags, $meta, &$status
+ ) {
+ // Update links, etc.
+ $this->doEditUpdates( $revision, $user, [ 'created' => true ] );
+ // Trigger post-create hook
+ $params = [ &$this, &$user, $content, $summary,
+ $flags & EDIT_MINOR, null, null, &$flags, $revision ];
+ ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $params, '1.21' );
+ Hooks::run( 'PageContentInsertComplete', $params );
+ // Trigger post-save hook
+ $params = array_merge( $params, [ &$status, $meta['baseRevId'] ] );
+ ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $params, '1.21' );
+ Hooks::run( 'PageContentSaveComplete', $params );
+
+ }
+ ),
+ DeferredUpdates::PRESEND
+ );
+
return $status;
}
* @return object
*/
public function prepareTextForEdit( $text, $revid = null, User $user = null ) {
- ContentHandler::deprecated( __METHOD__, '1.21' );
+ wfDeprecated( __METHOD__, '1.21' );
$content = ContentHandler::makeContent( $text, $this->getTitle() );
return $this->prepareContentForEdit( $content, $revid, $user );
}
}
if ( $this->mPreparedEdit
- && $this->mPreparedEdit->newContent
+ && isset( $this->mPreparedEdit->newContent )
&& $this->mPreparedEdit->newContent->equals( $content )
&& $this->mPreparedEdit->revid == $revid
&& $this->mPreparedEdit->format == $serialFormat
: false;
$popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
- Hooks::run( 'ArticlePrepareTextForEdit', array( $this, $popts ) );
+ Hooks::run( 'ArticlePrepareTextForEdit', [ $this, $popts ] );
- $edit = (object)array();
+ $edit = (object)[];
if ( $cachedEdit ) {
$edit->timestamp = $cachedEdit->timestamp;
} else {
$edit->timestamp = wfTimestampNow();
}
- // @note: $cachedEdit is not used if the rev ID was referenced in the text
+ // @note: $cachedEdit is safely not used if the rev ID was referenced in the text
$edit->revid = $revid;
if ( $cachedEdit ) {
// We get here if vary-revision is set. This means that this page references
// itself (such as via self-transclusion). In this case, we need to make sure
// that any such self-references refer to the newly-saved revision, and not
- // to the previous one, which could otherwise happen due to slave lag.
+ // to the previous one, which could otherwise happen due to replica DB lag.
$oldCallback = $edit->popts->getCurrentRevisionCallback();
$edit->popts->setCurrentRevisionCallback(
function ( Title $title, $parser = false ) use ( $revision, &$oldCallback ) {
}
}
);
+ } else {
+ // Try to avoid a second parse if {{REVISIONID}} is used
+ $edit->popts->setSpeculativeRevIdCallback( function () {
+ return 1 + (int)wfGetDB( DB_MASTER )->selectField(
+ 'revision',
+ 'MAX(rev_id)',
+ [],
+ __METHOD__
+ );
+ } );
}
$edit->output = $edit->pstContent
? $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts )
: '';
$edit->pst = $edit->pstContent ? $edit->pstContent->serialize( $serialFormat ) : '';
+ if ( $edit->output ) {
+ $edit->output->setCacheTime( wfTimestampNow() );
+ }
+
+ // Process cache the result
$this->mPreparedEdit = $edit;
+
return $edit;
}
* is true, do update the article count
* - 'no-change': don't update the article count, ever
*/
- public function doEditUpdates( Revision $revision, User $user, array $options = array() ) {
- global $wgRCWatchCategoryMembership;
+ public function doEditUpdates( Revision $revision, User $user, array $options = [] ) {
+ global $wgRCWatchCategoryMembership, $wgContLang;
- $options += array(
+ $options += [
'changed' => true,
'created' => false,
'moved' => false,
'restored' => false,
'oldrevision' => null,
'oldcountable' => null
- );
+ ];
$content = $revision->getContent();
- // Parse the text
- // Be careful not to do pre-save transform twice: $text is usually
- // already pre-save transformed once.
- if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
- wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" );
- $editInfo = $this->prepareContentForEdit( $content, $revision, $user );
+ $logger = LoggerFactory::getInstance( 'SaveParse' );
+
+ // See if the parser output before $revision was inserted is still valid
+ $editInfo = false;
+ if ( !$this->mPreparedEdit ) {
+ $logger->debug( __METHOD__ . ": No prepared edit...\n" );
+ } elseif ( $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
+ $logger->info( __METHOD__ . ": Prepared edit has vary-revision...\n" );
+ } elseif ( $this->mPreparedEdit->output->getFlag( 'vary-revision-id' )
+ && $this->mPreparedEdit->output->getSpeculativeRevIdUsed() !== $revision->getId()
+ ) {
+ $logger->info( __METHOD__ . ": Prepared edit has vary-revision-id with wrong ID...\n" );
+ } elseif ( $this->mPreparedEdit->output->getFlag( 'vary-user' ) && !$options['changed'] ) {
+ $logger->info( __METHOD__ . ": Prepared edit has vary-user and is null...\n" );
} else {
- wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" );
+ wfDebug( __METHOD__ . ": Using prepared edit...\n" );
$editInfo = $this->mPreparedEdit;
}
+ if ( !$editInfo ) {
+ // Parse the text again if needed. Be careful not to do pre-save transform twice:
+ // $text is usually already pre-save transformed once. Avoid using the edit stash
+ // as any prepared content from there or in doEditContent() was already rejected.
+ $editInfo = $this->prepareContentForEdit( $content, $revision, $user, null, false );
+ }
+
// Save it to the parser cache.
// Make sure the cache time matches page_touched to avoid double parsing.
ParserCache::singleton()->save(
DeferredUpdates::addUpdate( $update );
}
if ( $wgRCWatchCategoryMembership
+ && $this->getContentHandler()->supportsCategories() === true
&& ( $options['changed'] || $options['created'] )
&& !$options['restored']
) {
// bot/deletion/IP flags, ect.
JobQueueGroup::singleton()->lazyPush( new CategoryMembershipChangeJob(
$this->getTitle(),
- array(
+ [
'pageId' => $this->getId(),
'revTimestamp' => $revision->getTimestamp()
- )
+ ]
) );
}
}
- Hooks::run( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) );
+ Hooks::run( 'ArticleEditUpdates', [ &$this, &$editInfo, $options['changed'] ] );
- if ( Hooks::run( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) {
+ if ( Hooks::run( 'ArticleEditUpdatesDeleteFromRecentchanges', [ &$this ] ) ) {
// Flush old entries from the `recentchanges` table
if ( mt_rand( 0, 9 ) == 0 ) {
JobQueueGroup::singleton()->lazyPush( RecentChangesUpdateJob::newPurgeJob() );
} else {
// Allow extensions to prevent user notification
// when a new message is added to their talk page
- if ( Hooks::run( 'ArticleEditUpdateNewTalk', array( &$this, $recipient ) ) ) {
+ if ( Hooks::run( 'ArticleEditUpdateNewTalk', [ &$this, $recipient ] ) ) {
if ( User::isIP( $shortTitle ) ) {
// An anonymous user
$recipient->setNewtalk( true, $revision );
}
MessageCache::singleton()->replace( $shortTitle, $msgtext );
+
+ if ( $wgContLang->hasVariants() ) {
+ $wgContLang->updateConversionTable( $this->mTitle );
+ }
}
if ( $options['created'] ) {
} elseif ( $options['changed'] ) { // bug 50785
self::onArticleEdit( $this->mTitle, $revision );
}
- }
-
- /**
- * 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 bool $minor Whereas it's a minor modification
- * @param string $serialFormat Format for storing the content in the database
- */
- public function doQuickEditContent(
- Content $content, User $user, $comment = '', $minor = false, $serialFormat = null
- ) {
-
- $serialized = $content->serialize( $serialFormat );
-
- $dbw = wfGetDB( DB_MASTER );
- $revision = new Revision( array(
- 'title' => $this->getTitle(), // for determining the default content model
- 'page' => $this->getId(),
- 'user_text' => $user->getName(),
- 'user' => $user->getId(),
- 'text' => $serialized,
- 'length' => $content->getSize(),
- 'comment' => $comment,
- 'minor_edit' => $minor ? 1 : 0,
- ) ); // XXX: set the content object?
- $revision->insertOn( $dbw );
- $this->updateRevisionOn( $dbw, $revision );
-
- Hooks::run( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) );
+ ResourceLoaderWikiModule::invalidateModuleCache(
+ $this->mTitle, $options['oldrevision'], $revision, wfWikiID()
+ );
}
/**
* @param int &$cascade Set to false if cascading protection isn't allowed.
* @param string $reason
* @param User $user The user updating the restrictions
- * @return Status
+ * @param string|string[] $tags Change tags to add to the pages and protection log entries
+ * ($user should be able to add the specified tags before this is called)
+ * @return Status Status object; if action is taken, $status->value is the log_id of the
+ * protection log entry.
*/
public function doUpdateRestrictions( array $limit, array $expiry,
- &$cascade, $reason, User $user
+ &$cascade, $reason, User $user, $tags = null
) {
global $wgCascadingRestrictionLevels, $wgContLang;
// Truncate for whole multibyte characters
$reason = $wgContLang->truncate( $reason, 255 );
- $logRelationsValues = array();
+ $logRelationsValues = [];
$logRelationsField = null;
- $logParamsDetails = array();
+ $logParamsDetails = [];
+
+ // Null revision (used for change tag insertion)
+ $nullRevision = null;
if ( $id ) { // Protection of existing page
- if ( !Hooks::run( 'ArticleProtect', array( &$this, &$user, $limit, $reason ) ) ) {
+ if ( !Hooks::run( 'ArticleProtect', [ &$this, &$user, $limit, $reason ] ) ) {
return Status::newGood();
}
// Only certain restrictions can cascade...
$editrestriction = isset( $limit['edit'] )
- ? array( $limit['edit'] )
+ ? [ $limit['edit'] ]
: $this->mTitle->getRestrictions( 'edit' );
foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) {
$editrestriction[$key] = 'editprotected'; // backwards compatibility
foreach ( $limit as $action => $restrictions ) {
$dbw->delete(
'page_restrictions',
- array(
+ [
'pr_page' => $id,
'pr_type' => $action
- ),
+ ],
__METHOD__
);
if ( $restrictions != '' ) {
$cascadeValue = ( $cascade && $action == 'edit' ) ? 1 : 0;
$dbw->insert(
'page_restrictions',
- array(
+ [
'pr_id' => $dbw->nextSequenceValue( 'page_restrictions_pr_id_seq' ),
'pr_page' => $id,
'pr_type' => $action,
'pr_level' => $restrictions,
'pr_cascade' => $cascadeValue,
'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
- ),
+ ],
__METHOD__
);
$logRelationsValues[] = $dbw->insertId();
- $logParamsDetails[] = array(
+ $logParamsDetails[] = [
'type' => $action,
'level' => $restrictions,
'expiry' => $expiry[$action],
'cascade' => (bool)$cascadeValue,
- );
+ ];
}
}
// Clear out legacy restriction fields
$dbw->update(
'page',
- array( 'page_restrictions' => '' ),
- array( 'page_id' => $id ),
+ [ 'page_restrictions' => '' ],
+ [ 'page_id' => $id ],
__METHOD__
);
Hooks::run( 'NewRevisionFromEditComplete',
- array( $this, $nullRevision, $latest, $user ) );
- Hooks::run( 'ArticleProtectComplete', array( &$this, &$user, $limit, $reason ) );
+ [ $this, $nullRevision, $latest, $user ] );
+ Hooks::run( 'ArticleProtectComplete', [ &$this, &$user, $limit, $reason ] );
} else { // Protection of non-existing page (also known as "title protection")
// Cascade protection is meaningless in this case
$cascade = false;
if ( $limit['create'] != '' ) {
$dbw->replace( 'protected_titles',
- array( array( 'pt_namespace', 'pt_title' ) ),
- array(
+ [ [ 'pt_namespace', 'pt_title' ] ],
+ [
'pt_namespace' => $this->mTitle->getNamespace(),
'pt_title' => $this->mTitle->getDBkey(),
'pt_create_perm' => $limit['create'],
'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
'pt_user' => $user->getId(),
'pt_reason' => $reason,
- ), __METHOD__
+ ], __METHOD__
);
- $logParamsDetails[] = array(
+ $logParamsDetails[] = [
'type' => 'create',
'level' => $limit['create'],
'expiry' => $expiry['create'],
- );
+ ];
} else {
$dbw->delete( 'protected_titles',
- array(
+ [
'pt_namespace' => $this->mTitle->getNamespace(),
'pt_title' => $this->mTitle->getDBkey()
- ), __METHOD__
+ ], __METHOD__
);
}
}
InfoAction::invalidateCache( $this->mTitle );
if ( $logAction == 'unprotect' ) {
- $params = array();
+ $params = [];
} else {
$protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
- $params = array(
+ $params = [
'4::description' => $protectDescriptionLog, // parameter for IRC
'5:bool:cascade' => $cascade,
'details' => $logParamsDetails, // parameter for localize and api
- );
+ ];
}
// Update the protection log
$logEntry->setComment( $reason );
$logEntry->setPerformer( $user );
$logEntry->setParameters( $params );
+ if ( !is_null( $nullRevision ) ) {
+ $logEntry->setAssociatedRevId( $nullRevision->getId() );
+ }
+ $logEntry->setTags( $tags );
if ( $logRelationsField !== null && count( $logRelationsValues ) ) {
- $logEntry->setRelations( array( $logRelationsField => $logRelationsValues ) );
+ $logEntry->setRelations( [ $logRelationsField => $logRelationsValues ] );
}
$logId = $logEntry->insert();
$logEntry->publish( $logId );
- return Status::newGood();
+ return Status::newGood( $logId );
}
/**
$protectDescription = '';
foreach ( array_filter( $limit ) as $action => $restrictions ) {
- # $action is one of $wgRestrictionTypes = array( 'create', 'edit', 'move', 'upload' ).
+ # $action is one of $wgRestrictionTypes = [ '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' ),
+ # $restrictions is one of $wgRestrictionLevels = [ '', 'autoconfirmed', 'sysop' ],
# with '' filtered out. All possible message keys are listed below:
# * protect-level-autoconfirmed
# * protect-level-sysop
throw new MWException( __METHOD__ . ' given non-array restriction set' );
}
- $bits = array();
+ $bits = [];
ksort( $limit );
foreach ( array_filter( $limit ) as $action => $restrictions ) {
* @param bool $u2 Unused
* @param array|string &$error Array of errors to append to
* @param User $user The deleting user
+ * @param array $tags Tags to apply to the deletion action
* @return Status Status object; if successful, $status->value is the log_id of the
* deletion log entry. If the page couldn't be deleted because it wasn't
* found, $status is a non-fatal 'cannotdelete' error
*/
public function doDeleteArticleReal(
- $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null
+ $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
+ $tags = []
) {
global $wgUser, $wgContentHandlerUseDB;
$user = is_null( $user ) ? $wgUser : $user;
if ( !Hooks::run( 'ArticleDelete',
- array( &$this, &$user, &$reason, &$error, &$status, $suppress )
+ [ &$this, &$user, &$reason, &$error, &$status, $suppress ]
) ) {
if ( $status->isOK() ) {
// Hook aborted but didn't set a fatal status
$dbw->startAtomic( __METHOD__ );
$this->loadPageData( self::READ_LATEST );
- $id = $this->getID();
+ $id = $this->getId();
// T98706: lock the page from various other updates but avoid using
// WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to
// the revisions queries (which also JOIN on user). Only lock the page
return $status;
}
+ // Given the lock above, we can be confident in the title and page ID values
+ $namespace = $this->getTitle()->getNamespace();
+ $dbKey = $this->getTitle()->getDBkey();
+
// At this point we are now comitted to returning an OK
// status unless some DB query error or other exception comes up.
// This way callers don't have to call rollback() if $status is bad
// unless they actually try to catch exceptions (which is rare).
// we need to remember the old content so we can use it to generate all deletion updates.
- $content = $this->getContent( Revision::RAW );
+ $revision = $this->getRevision();
+ try {
+ $content = $this->getContent( Revision::RAW );
+ } catch ( Exception $ex ) {
+ wfLogWarning( __METHOD__ . ': failed to load content during deletion! '
+ . $ex->getMessage() );
+
+ $content = null;
+ }
+
+ $fields = Revision::selectFields();
+ $bitfield = false;
// Bitfields to further suppress the content
if ( $suppress ) {
- $bitfield = 0;
- // This should be 15...
- $bitfield |= Revision::DELETED_TEXT;
- $bitfield |= Revision::DELETED_COMMENT;
- $bitfield |= Revision::DELETED_USER;
- $bitfield |= Revision::DELETED_RESTRICTED;
- } else {
- $bitfield = 'rev_deleted';
- }
-
- /**
- * For now, shunt the revision data into the archive table.
- * Text is *not* removed from the text table; bulk storage
- * is left intact to avoid breaking block-compression or
- * immutable storage schemes.
- *
- * For backwards compatibility, note that some older archive
- * table entries will have ar_text and ar_flags fields still.
- *
- * 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',
+ $bitfield = Revision::SUPPRESSED_ALL;
+ $fields = array_diff( $fields, [ 'rev_deleted' ] );
+ }
+
+ // For now, shunt the revision data into the archive table.
+ // Text is *not* removed from the text table; bulk storage
+ // is left intact to avoid breaking block-compression or
+ // immutable storage schemes.
+ // In the future, we may keep revisions and mark them with
+ // the rev_deleted field, which is reserved for this purpose.
+
+ // Get all of the page revisions
+ $res = $dbw->select(
+ 'revision',
+ $fields,
+ [ 'rev_page' => $id ],
+ __METHOD__,
+ 'FOR UPDATE'
);
-
- if ( $wgContentHandlerUseDB ) {
- $row['ar_content_model'] = 'rev_content_model';
- $row['ar_content_format'] = 'rev_content_format';
+ // Build their equivalent archive rows
+ $rowsInsert = [];
+ foreach ( $res as $row ) {
+ $rowInsert = [
+ 'ar_namespace' => $namespace,
+ 'ar_title' => $dbKey,
+ 'ar_comment' => $row->rev_comment,
+ 'ar_user' => $row->rev_user,
+ 'ar_user_text' => $row->rev_user_text,
+ 'ar_timestamp' => $row->rev_timestamp,
+ 'ar_minor_edit' => $row->rev_minor_edit,
+ 'ar_rev_id' => $row->rev_id,
+ 'ar_parent_id' => $row->rev_parent_id,
+ 'ar_text_id' => $row->rev_text_id,
+ 'ar_text' => '',
+ 'ar_flags' => '',
+ 'ar_len' => $row->rev_len,
+ 'ar_page_id' => $id,
+ 'ar_deleted' => $suppress ? $bitfield : $row->rev_deleted,
+ 'ar_sha1' => $row->rev_sha1,
+ ];
+ if ( $wgContentHandlerUseDB ) {
+ $rowInsert['ar_content_model'] = $row->rev_content_model;
+ $rowInsert['ar_content_format'] = $row->rev_content_format;
+ }
+ $rowsInsert[] = $rowInsert;
}
+ // Copy them into the archive table
+ $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
+ // Save this so we can pass it to the ArticleDeleteComplete hook.
+ $archivedRevisionCount = $dbw->affectedRows();
- // Copy all the page revisions into the archive table
- $dbw->insertSelect(
- 'archive',
- array( 'page', 'revision' ),
- $row,
- array(
- 'page_id' => $id,
- 'page_id = rev_page'
- ),
- __METHOD__
- );
+ // Clone the title and wikiPage, so we have the information we need when
+ // we log and run the ArticleDeleteComplete hook.
+ $logTitle = clone $this->mTitle;
+ $wikiPageBeforeDelete = clone $this;
// Now that it's safely backed up, delete it
- $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__ );
-
- if ( !$dbw->cascadingDeletes() ) {
- $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ );
- }
-
- // Clone the title, so we have the information we need when we log
- $logTitle = clone $this->mTitle;
+ $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
+ $dbw->delete( 'revision', [ 'rev_page' => $id ], __METHOD__ );
// Log the deletion, if the page was suppressed, put it in the suppression log instead
$logtype = $suppress ? 'suppress' : 'delete';
$logEntry->setPerformer( $user );
$logEntry->setTarget( $logTitle );
$logEntry->setComment( $reason );
+ $logEntry->setTags( $tags );
$logid = $logEntry->insert();
- $dbw->onTransactionPreCommitOrIdle( function () use ( $dbw, $logEntry, $logid ) {
- // Bug 56776: avoid deadlocks (especially from FileDeleteForm)
- $logEntry->publish( $logid );
- } );
+ $dbw->onTransactionPreCommitOrIdle(
+ function () use ( $dbw, $logEntry, $logid ) {
+ // Bug 56776: avoid deadlocks (especially from FileDeleteForm)
+ $logEntry->publish( $logid );
+ },
+ __METHOD__
+ );
$dbw->endAtomic( __METHOD__ );
- $this->doDeleteUpdates( $id, $content );
-
- Hooks::run( 'ArticleDeleteComplete',
- array( &$this, &$user, $reason, $id, $content, $logEntry ) );
+ $this->doDeleteUpdates( $id, $content, $revision );
+
+ Hooks::run( 'ArticleDeleteComplete', [
+ &$wikiPageBeforeDelete,
+ &$user,
+ $reason,
+ $id,
+ $content,
+ $logEntry,
+ $archivedRevisionCount
+ ] );
$status->value = $logid;
// Show log excerpt on 404 pages rather than just a link
return (int)wfGetDB( DB_MASTER )->selectField(
'page',
'page_latest',
- array(
+ [
'page_id' => $this->getId(),
// Typically page_id is enough, but some code might try to do
// updates assuming the title is the same, so verify that
'page_namespace' => $this->getTitle()->getNamespace(),
'page_title' => $this->getTitle()->getDBkey()
- ),
+ ],
__METHOD__,
- array( 'FOR UPDATE' )
+ [ 'FOR UPDATE' ]
);
}
* Do some database updates after deletion
*
* @param int $id The page_id value of the page being deleted
- * @param Content $content Optional page content to be used when determining
+ * @param Content|null $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.
+ * @param Revision|null $revision The latest page revision
*/
- public function doDeleteUpdates( $id, Content $content = null ) {
+ public function doDeleteUpdates( $id, Content $content = null, Revision $revision = null ) {
+ try {
+ $countable = $this->isCountable();
+ } catch ( Exception $ex ) {
+ // fallback for deleting broken pages for which we cannot load the content for
+ // some reason. Note that doDeleteArticleReal() already logged this problem.
+ $countable = false;
+ }
+
// Update site status
- DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) );
+ DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$countable, -1 ) );
// Delete pagelinks, update secondary indexes, etc
$updates = $this->getDeletionUpdates( $content );
// Clear caches
WikiPage::onArticleDelete( $this->mTitle );
+ ResourceLoaderWikiModule::invalidateModuleCache(
+ $this->mTitle, $revision, null, wfWikiID()
+ );
// Reset this object and the Title object
$this->loadFromRow( false, self::READ_LATEST );
* to do the dirty work
*
* @todo Separate the business/permission stuff out from backend code
+ * @todo Remove $token parameter. Already verified by RollbackAction and ApiRollback.
*
* @param string $fromP Name of the user whose edits to rollback.
* @param string $summary Custom summary. Set to default summary if empty.
* success : 'summary' (str), 'current' (rev), 'target' (rev)
*
* @param User $user The user performing the rollback
+ * @param array|null $tags Change tags to apply to the rollback
+ * Callers are responsible for permission checks
+ * (with ChangeTags::canAddTagsAccompanyingChange)
+ *
* @return array Array of errors, each error formatted as
* array(messagekey, param1, param2, ...).
* On success, the array is empty. This array can also be passed to
* OutputPage::showPermissionsErrorPage().
*/
public function doRollback(
- $fromP, $summary, $token, $bot, &$resultDetails, User $user
+ $fromP, $summary, $token, $bot, &$resultDetails, User $user, $tags = null
) {
$resultDetails = null;
$rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user );
$errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
- if ( !$user->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) ) {
- $errors[] = array( 'sessionfailure' );
+ if ( !$user->matchEditToken( $token, 'rollback' ) ) {
+ $errors[] = [ 'sessionfailure' ];
}
if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) {
- $errors[] = array( 'actionthrottledtext' );
+ $errors[] = [ 'actionthrottledtext' ];
}
// If there were errors, bail out now
return $errors;
}
- return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user );
+ return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user, $tags );
}
/**
*
* @param array $resultDetails Contains result-specific array of additional values
* @param User $guser The user performing the rollback
+ * @param array|null $tags Change tags to apply to the rollback
+ * Callers are responsible for permission checks
+ * (with ChangeTags::canAddTagsAccompanyingChange)
+ *
* @return array
*/
- public function commitRollback( $fromP, $summary, $bot, &$resultDetails, User $guser ) {
+ public function commitRollback( $fromP, $summary, $bot,
+ &$resultDetails, User $guser, $tags = null
+ ) {
global $wgUseRCPatrol, $wgContLang;
$dbw = wfGetDB( DB_MASTER );
if ( wfReadOnly() ) {
- return array( array( 'readonlytext' ) );
+ return [ [ 'readonlytext' ] ];
}
// Get the last editor
$current = $this->getRevision();
if ( is_null( $current ) ) {
// Something wrong... no page?
- return array( array( 'notanarticle' ) );
+ return [ [ '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.
if ( $from != $current->getUserText() ) {
- $resultDetails = array( 'current' => $current );
- return array( array( 'alreadyrolled',
+ $resultDetails = [ 'current' => $current ];
+ return [ [ 'alreadyrolled',
htmlspecialchars( $this->mTitle->getPrefixedText() ),
htmlspecialchars( $fromP ),
htmlspecialchars( $current->getUserText() )
- ) );
+ ] ];
}
// Get the last edit not by this person...
$user = intval( $current->getUser( Revision::RAW ) );
$user_text = $dbw->addQuotes( $current->getUserText( Revision::RAW ) );
$s = $dbw->selectRow( 'revision',
- array( 'rev_id', 'rev_timestamp', 'rev_deleted' ),
- array( 'rev_page' => $current->getPage(),
+ [ 'rev_id', 'rev_timestamp', 'rev_deleted' ],
+ [ 'rev_page' => $current->getPage(),
"rev_user != {$user} OR rev_user_text != {$user_text}"
- ), __METHOD__,
- array( 'USE INDEX' => 'page_timestamp',
- 'ORDER BY' => 'rev_timestamp DESC' )
+ ], __METHOD__,
+ [ 'USE INDEX' => 'page_timestamp',
+ 'ORDER BY' => 'rev_timestamp DESC' ]
);
if ( $s === false ) {
// No one else ever edited this page
- return array( array( 'cantrollback' ) );
+ return [ [ 'cantrollback' ] ];
} elseif ( $s->rev_deleted & Revision::DELETED_TEXT
|| $s->rev_deleted & Revision::DELETED_USER
) {
// Only admins can see this text
- return array( array( 'notvisiblerev' ) );
+ return [ [ 'notvisiblerev' ] ];
}
// Generate the edit summary if necessary
}
// Allow the custom summary to use the same args as the default message
- $args = array(
+ $args = [
$target->getUserText(), $from, $s->rev_id,
$wgContLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ),
$current->getId(), $wgContLang->timeanddate( $current->getTimestamp() )
- );
+ ];
if ( $summary instanceof Message ) {
$summary = $summary->params( $args )->inContentLanguage()->text();
} else {
$summary = $wgContLang->truncate( $summary, 255 );
// Save
- $flags = EDIT_UPDATE;
+ $flags = EDIT_UPDATE | EDIT_INTERNAL;
if ( $guser->isAllowed( 'minoredit' ) ) {
$flags |= EDIT_MINOR;
$flags |= EDIT_FORCE_BOT;
}
+ $targetContent = $target->getContent();
+ $changingContentModel = $targetContent->getModel() !== $current->getContentModel();
+
// Actually store the edit
$status = $this->doEditContent(
- $target->getContent(),
+ $targetContent,
$summary,
$flags,
$target->getId(),
- $guser
+ $guser,
+ null,
+ $tags
);
// Set patrolling and bot flag on the edits, which gets rollbacked.
// This is done even on edit failure to have patrolling in that case (bug 62157).
- $set = array();
+ $set = [];
if ( $bot && $guser->isAllowed( 'markbotedits' ) ) {
// Mark all reverted edits as bot
$set['rc_bot'] = 1;
if ( count( $set ) ) {
$dbw->update( 'recentchanges', $set,
- array( /* WHERE */
+ [ /* WHERE */
'rc_cur_id' => $current->getPage(),
'rc_user_text' => $current->getUserText(),
'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ),
- ),
+ ],
__METHOD__
);
}
? $status->value['revision']
: null;
if ( !( $statusRev instanceof Revision ) ) {
- $resultDetails = array( 'current' => $current );
- return array( array( 'alreadyrolled',
+ $resultDetails = [ 'current' => $current ];
+ return [ [ 'alreadyrolled',
htmlspecialchars( $this->mTitle->getPrefixedText() ),
htmlspecialchars( $fromP ),
htmlspecialchars( $current->getUserText() )
- ) );
+ ] ];
+ }
+
+ if ( $changingContentModel ) {
+ // If the content model changed during the rollback,
+ // make sure it gets logged to Special:Log/contentmodel
+ $log = new ManualLogEntry( 'contentmodel', 'change' );
+ $log->setPerformer( $guser );
+ $log->setTarget( $this->mTitle );
+ $log->setComment( $summary );
+ $log->setParameters( [
+ '4::oldmodel' => $current->getContentModel(),
+ '5::newmodel' => $targetContent->getModel(),
+ ] );
+
+ $logId = $log->insert( $dbw );
+ $log->publish( $logId );
}
$revId = $statusRev->getId();
- Hooks::run( 'ArticleRollbackComplete', array( $this, $guser, $target, $current ) );
+ Hooks::run( 'ArticleRollbackComplete', [ $this, $guser, $target, $current ] );
- $resultDetails = array(
+ $resultDetails = [
'summary' => $summary,
'current' => $current,
'target' => $target,
'newid' => $revId
- );
+ ];
- return array();
+ return [];
}
/**
$title->touchLinks();
$title->purgeSquid();
$title->deleteTitleProtection();
+
+ MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
+
+ if ( $title->getNamespace() == NS_CATEGORY ) {
+ // Load the Category object, which will schedule a job to create
+ // the category table row if necessary. Checking a replica DB is ok
+ // here, in the worst case it'll run an unnecessary recount job on
+ // a category that probably doesn't have many members.
+ Category::newFromTitle( $title )->getID();
+ }
}
/**
* @param Title $title
*/
public static function onArticleDelete( Title $title ) {
+ global $wgContLang;
+
// Update existence markers on article/talk tabs...
$other = $title->getOtherPage();
$title->touchLinks();
$title->purgeSquid();
+ MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
+
// File cache
HTMLFileCache::clearFileCache( $title );
InfoAction::invalidateCache( $title );
// Messages
if ( $title->getNamespace() == NS_MEDIAWIKI ) {
MessageCache::singleton()->replace( $title->getDBkey(), false );
+
+ if ( $wgContLang->hasVariants() ) {
+ $wgContLang->updateConversionTable( $title );
+ }
}
// Images
// Invalidate the caches of all pages which redirect here
DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'redirect' ) );
- // Purge squid for this page only
+ MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
+
+ // Purge CDN for this page only
$title->purgeSquid();
// Clear file cache for this page only
HTMLFileCache::clearFileCache( $title );
public function getCategories() {
$id = $this->getId();
if ( $id == 0 ) {
- return TitleArray::newFromResult( new FakeResultWrapper( array() ) );
+ return TitleArray::newFromResult( new FakeResultWrapper( [] ) );
}
- $dbr = wfGetDB( DB_SLAVE );
+ $dbr = wfGetDB( DB_REPLICA );
$res = $dbr->select( 'categorylinks',
- array( 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ),
- // Have to do that since DatabaseBase::fieldNamesWithAlias treats numeric indexes
+ [ 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ],
+ // Have to do that since Database::fieldNamesWithAlias treats numeric indexes
// as not being aliases, and NS_CATEGORY is numeric
- array( 'cl_from' => $id ),
+ [ 'cl_from' => $id ],
__METHOD__ );
return TitleArray::newFromResult( $res );
* @return array Array of Title objects
*/
public function getHiddenCategories() {
- $result = array();
+ $result = [];
$id = $this->getId();
if ( $id == 0 ) {
- return array();
+ return [];
}
- $dbr = wfGetDB( DB_SLAVE );
- $res = $dbr->select( array( 'categorylinks', 'page_props', 'page' ),
- array( 'cl_to' ),
- array( 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
- 'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ),
+ $dbr = wfGetDB( DB_REPLICA );
+ $res = $dbr->select( [ 'categorylinks', 'page_props', 'page' ],
+ [ 'cl_to' ],
+ [ 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
+ 'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ],
__METHOD__ );
if ( $res !== false ) {
// NOTE: stub for backwards-compatibility. assumes the given text is
// wikitext. will break horribly if it isn't.
- ContentHandler::deprecated( __METHOD__, '1.21' );
+ wfDeprecated( __METHOD__, '1.21' );
$handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT );
$oldContent = is_null( $oldtext ) ? null : $handler->unserializeContent( $oldtext );
* Update all the appropriate counts in the category table, given that
* we've added the categories $added and deleted the categories $deleted.
*
+ * This should only be called from deferred updates or jobs to avoid contention.
+ *
* @param array $added The names of categories that were added
* @param array $deleted The names of categories that were deleted
+ * @param integer $id Page ID (this should be the original deleted page ID)
*/
- public function updateCategoryCounts( array $added, array $deleted ) {
- $that = $this;
- $method = __METHOD__;
- $dbw = wfGetDB( DB_MASTER );
+ public function updateCategoryCounts( array $added, array $deleted, $id = 0 ) {
+ $id = $id ?: $this->getId();
+ $ns = $this->getTitle()->getNamespace();
- // Do this at the end of the commit to reduce lock wait timeouts
- $dbw->onTransactionPreCommitOrIdle(
- function () use ( $dbw, $that, $method, $added, $deleted ) {
- $ns = $that->getTitle()->getNamespace();
-
- $addFields = array( 'cat_pages = cat_pages + 1' );
- $removeFields = array( 'cat_pages = cat_pages - 1' );
- if ( $ns == NS_CATEGORY ) {
- $addFields[] = 'cat_subcats = cat_subcats + 1';
- $removeFields[] = 'cat_subcats = cat_subcats - 1';
- } elseif ( $ns == NS_FILE ) {
- $addFields[] = 'cat_files = cat_files + 1';
- $removeFields[] = 'cat_files = cat_files - 1';
- }
+ $addFields = [ 'cat_pages = cat_pages + 1' ];
+ $removeFields = [ 'cat_pages = cat_pages - 1' ];
+ if ( $ns == NS_CATEGORY ) {
+ $addFields[] = 'cat_subcats = cat_subcats + 1';
+ $removeFields[] = 'cat_subcats = cat_subcats - 1';
+ } elseif ( $ns == NS_FILE ) {
+ $addFields[] = 'cat_files = cat_files + 1';
+ $removeFields[] = 'cat_files = cat_files - 1';
+ }
- if ( count( $added ) ) {
- $existingAdded = $dbw->selectFieldValues(
- 'category',
- 'cat_title',
- array( 'cat_title' => $added ),
- __METHOD__
- );
+ $dbw = wfGetDB( DB_MASTER );
- // For category rows that already exist, do a plain
- // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
- // to avoid creating gaps in the cat_id sequence.
- if ( count( $existingAdded ) ) {
- $dbw->update(
- 'category',
- $addFields,
- array( 'cat_title' => $existingAdded ),
- __METHOD__
- );
- }
+ if ( count( $added ) ) {
+ $existingAdded = $dbw->selectFieldValues(
+ 'category',
+ 'cat_title',
+ [ 'cat_title' => $added ],
+ __METHOD__
+ );
- $missingAdded = array_diff( $added, $existingAdded );
- if ( count( $missingAdded ) ) {
- $insertRows = array();
- foreach ( $missingAdded as $cat ) {
- $insertRows[] = array(
- 'cat_title' => $cat,
- 'cat_pages' => 1,
- 'cat_subcats' => ( $ns == NS_CATEGORY ) ? 1 : 0,
- 'cat_files' => ( $ns == NS_FILE ) ? 1 : 0,
- );
- }
- $dbw->upsert(
- 'category',
- $insertRows,
- array( 'cat_title' ),
- $addFields,
- $method
- );
- }
- }
+ // For category rows that already exist, do a plain
+ // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
+ // to avoid creating gaps in the cat_id sequence.
+ if ( count( $existingAdded ) ) {
+ $dbw->update(
+ 'category',
+ $addFields,
+ [ 'cat_title' => $existingAdded ],
+ __METHOD__
+ );
+ }
- if ( count( $deleted ) ) {
- $dbw->update(
- 'category',
- $removeFields,
- array( 'cat_title' => $deleted ),
- $method
- );
+ $missingAdded = array_diff( $added, $existingAdded );
+ if ( count( $missingAdded ) ) {
+ $insertRows = [];
+ foreach ( $missingAdded as $cat ) {
+ $insertRows[] = [
+ 'cat_title' => $cat,
+ 'cat_pages' => 1,
+ 'cat_subcats' => ( $ns == NS_CATEGORY ) ? 1 : 0,
+ 'cat_files' => ( $ns == NS_FILE ) ? 1 : 0,
+ ];
}
+ $dbw->upsert(
+ 'category',
+ $insertRows,
+ [ 'cat_title' ],
+ $addFields,
+ __METHOD__
+ );
+ }
+ }
- foreach ( $added as $catName ) {
- $cat = Category::newFromName( $catName );
- Hooks::run( 'CategoryAfterPageAdded', array( $cat, $that ) );
- }
+ if ( count( $deleted ) ) {
+ $dbw->update(
+ 'category',
+ $removeFields,
+ [ 'cat_title' => $deleted ],
+ __METHOD__
+ );
+ }
- foreach ( $deleted as $catName ) {
- $cat = Category::newFromName( $catName );
- Hooks::run( 'CategoryAfterPageRemoved', array( $cat, $that ) );
- }
+ foreach ( $added as $catName ) {
+ $cat = Category::newFromName( $catName );
+ Hooks::run( 'CategoryAfterPageAdded', [ $cat, $this ] );
+ }
+
+ foreach ( $deleted as $catName ) {
+ $cat = Category::newFromName( $catName );
+ Hooks::run( 'CategoryAfterPageRemoved', [ $cat, $this, $id ] );
+ }
+
+ // Refresh counts on categories that should be empty now, to
+ // trigger possible deletion. Check master for the most
+ // up-to-date cat_pages.
+ if ( count( $deleted ) ) {
+ $rows = $dbw->select(
+ 'category',
+ [ 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ],
+ [ 'cat_title' => $deleted, 'cat_pages <= 0' ],
+ __METHOD__
+ );
+ foreach ( $rows as $row ) {
+ $cat = Category::newFromRow( $row );
+ $cat->refreshCounts();
}
- );
+ }
}
/**
}
if ( !Hooks::run( 'OpportunisticLinksUpdate',
- array( $this, $this->mTitle, $parserOutput )
+ [ $this, $this->mTitle, $parserOutput ]
) ) {
return;
}
- $params = array(
+ $config = RequestContext::getMain()->getConfig();
+
+ $params = [
'isOpportunistic' => true,
'rootJobTimestamp' => $parserOutput->getCacheTime()
- );
+ ];
if ( $this->mTitle->areRestrictionsCascading() ) {
// If the page is cascade protecting, the links should really be up-to-date
JobQueueGroup::singleton()->lazyPush(
RefreshLinksJob::newPrioritized( $this->mTitle, $params )
);
- } elseif ( $parserOutput->hasDynamicContent() ) {
+ } elseif ( !$config->get( 'MiserMode' ) && $parserOutput->hasDynamicContent() ) {
// Assume the output contains "dynamic" time/random based magic words.
// Only update pages that expired due to dynamic content and NOT due to edits
// to referenced templates/files. When the cache expires due to dynamic content,
// Although it would be de-duplicated, it would still waste I/O.
$cache = ObjectCache::getLocalClusterInstance();
$key = $cache->makeKey( 'dynamic-linksupdate', 'last', $this->getId() );
- if ( $cache->add( $key, time(), 60 ) ) {
+ $ttl = max( $parserOutput->getCacheExpiry(), 3600 );
+ if ( $cache->add( $key, time(), $ttl ) ) {
JobQueueGroup::singleton()->lazyPush(
RefreshLinksJob::newDynamic( $this->mTitle, $params )
);
}
}
- /**
- * Return a list of templates used by this article.
- * Uses the templatelinks table
- *
- * @deprecated since 1.19; use Title::getTemplateLinksFrom()
- * @return array Array of Title objects
- */
- public function getUsedTemplates() {
- return $this->mTitle->getTemplateLinksFrom();
- }
-
- /**
- * This function is called right before saving the wikitext,
- * so we can do things like signatures and links-in-context.
- *
- * @deprecated since 1.19; use Parser::preSaveTransform() instead
- * @param string $text Article contents
- * @param User $user User doing the edit
- * @param ParserOptions $popts Parser options, default options for
- * the user loaded if null given
- * @return string Article contents with altered wikitext markup (signatures
- * converted, {{subst:}}, templates, etc.)
- */
- public function preSaveTransform( $text, User $user = null, ParserOptions $popts = null ) {
- global $wgParser, $wgUser;
-
- wfDeprecated( __METHOD__, '1.19' );
-
- $user = is_null( $user ) ? $wgUser : $user;
-
- if ( $popts === null ) {
- $popts = ParserOptions::newFromUser( $user );
- }
-
- return $wgParser->preSaveTransform( $text, $this->mTitle, $user, $popts );
- }
-
- /**
- * Update the article's restriction field, and leave a log entry.
- *
- * @deprecated since 1.19
- * @param array $limit Set of restriction keys
- * @param string $reason
- * @param int &$cascade Set to false if cascading protection isn't allowed.
- * @param array $expiry Per restriction type expiration
- * @param User $user The user updating the restrictions
- * @return bool True on success
- */
- public function updateRestrictions(
- $limit = array(), $reason = '', &$cascade = 0, $expiry = array(), User $user = null
- ) {
- global $wgUser;
-
- $user = is_null( $user ) ? $wgUser : $user;
-
- return $this->doUpdateRestrictions( $limit, $expiry, $cascade, $reason, $user )->isOK();
- }
-
/**
* 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
*
* @param Content|null $content Optional Content object for determining the
* necessary updates.
- * @return DataUpdate[]
+ * @return DeferrableUpdate[]
*/
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.
- $content = $this->getContent( Revision::RAW );
+ try {
+ $content = $this->getContent( Revision::RAW );
+ } catch ( Exception $ex ) {
+ // If we can't load the content, something is wrong. Perhaps that's why
+ // the user is trying to delete the page, so let's not fail in that case.
+ // Note that doDeleteArticleReal() will already have logged an issue with
+ // loading the content.
+ }
}
if ( !$content ) {
- $updates = array();
+ $updates = [];
} else {
$updates = $content->getDeletionUpdates( $this );
}
- Hooks::run( 'WikiPageDeletionUpdates', array( $this, $content, &$updates ) );
+ Hooks::run( 'WikiPageDeletionUpdates', [ $this, $content, &$updates ] );
return $updates;
}
+
+ /**
+ * Whether this content displayed on this page
+ * comes from the local database
+ *
+ * @since 1.28
+ * @return bool
+ */
+ public function isLocal() {
+ return true;
+ }
}