__METHOD__
);
- // clean up the old title before reset article id - bug 45348
if ( !$redirectContent ) {
+ // Clean up the old title *before* reset article id - bug 45348
WikiPage::onArticleDelete( $this->oldTitle );
}
/**
* Output a standard permission error page
*
- * @param array $errors Error message keys
+ * @param array $errors Error message keys or [key, param...] arrays
* @param string $action Action that was denied or null if unknown
*/
public function showPermissionsErrorPage( array $errors, $action = null ) {
+ foreach ( $errors as $key => $error ) {
+ $errors[$key] = (array)$error;
+ }
+
// For some action (read, edit, create and upload), display a "login to do this action"
// error if all of the following conditions are met:
// 1. the user is not logged in
$exemptStates = [];
$moduleStyles = $this->getModuleStyles( /*filter*/ true );
- // Batch preload getTitleInfo for isKnownEmpty() calls below
- $exemptModules = array_filter( $moduleStyles,
- function ( $name ) use ( $rl, &$exemptGroups ) {
- $module = $rl->getModule( $name );
- return $module && isset( $exemptGroups[ $module->getGroup() ] );
- }
- );
- ResourceLoaderWikiModule::preloadTitleInfo(
- $context, wfGetDB( DB_REPLICA ), $exemptModules );
+ // Preload getTitleInfo for isKnownEmpty calls below and in ResourceLoaderClientHtml
+ // Separate user-specific batch for improved cache-hit ratio.
+ $userBatch = [ 'user.styles', 'user' ];
+ $siteBatch = array_diff( $moduleStyles, $userBatch );
+ $dbr = wfGetDB( DB_REPLICA );
+ ResourceLoaderWikiModule::preloadTitleInfo( $context, $dbr, $siteBatch );
+ ResourceLoaderWikiModule::preloadTitleInfo( $context, $dbr, $userBatch );
// Filter out modules handled by buildExemptModules()
$moduleStyles = array_filter( $moduleStyles,
public function invalidateCache( $purgeTime = null ) {
if ( wfReadOnly() ) {
return false;
- }
-
- if ( $this->mArticleID === 0 ) {
+ } elseif ( $this->mArticleID === 0 ) {
return true; // avoid gap locking if we know it's not there
}
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->onTransactionPreCommitOrIdle( function () {
+ ResourceLoaderWikiModule::invalidateModuleCache( $this, null, null, wfWikiID() );
+ } );
+
$conds = $this->pageCond();
DeferredUpdates::addUpdate(
new AutoCommitUpdate(
- wfGetDB( DB_MASTER ),
+ $dbw,
__METHOD__,
function ( IDatabase $dbw, $fname ) use ( $conds, $purgeTime ) {
$dbTimestamp = $dbw->timestamp( $purgeTime ?: time() );
public $permission, $errors;
/**
- * @param string $permission A permission name.
- * @param string[] $errors Error message keys
+ * @param string|null $permission A permission name or null if unknown
+ * @param array $errors Error message keys or [key, param...] arrays; must not be empty if
+ * $permission is null
+ * @throws \InvalidArgumentException
*/
public function __construct( $permission, $errors = [] ) {
global $wgLang;
+ if ( $permission === null && !$errors ) {
+ throw new \InvalidArgumentException( __METHOD__ .
+ ': $permission and $errors cannot both be empty' );
+ }
+
$this->permission = $permission;
if ( !count( $errors ) ) {
*
* @since 1.17
*
- * @param array $update The update to run. Format is the following:
- * first item is the callback function, it also can be a
- * simple string with the name of a function in this class,
- * following elements are parameters to the function.
- * Note that callback functions will receive this object as
- * first parameter.
+ * @param array $update The update to run. Format is [ $callback, $params... ]
+ * $callback is the method to call; either a DatabaseUpdater method name or a callable.
+ * Must be serializable (ie. no anonymous functions allowed). The rest of the parameters
+ * (if any) will be passed to the callback. The first parameter passed to the callback
+ * is always this object.
*/
public function addExtensionUpdate( array $update ) {
$this->extensionUpdates[] = $update;
} elseif ( $options['changed'] ) { // bug 50785
self::onArticleEdit( $this->mTitle, $revision );
}
+
+ ResourceLoaderWikiModule::invalidateModuleCache(
+ $this->mTitle, $options['oldrevision'], $revision, wfWikiID()
+ );
}
/**
// 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.
+ $revision = $this->getRevision();
try {
$content = $this->getContent( Revision::RAW );
} catch ( Exception $ex ) {
$dbw->endAtomic( __METHOD__ );
- $this->doDeleteUpdates( $id, $content );
+ $this->doDeleteUpdates( $id, $content, $revision );
Hooks::run( 'ArticleDeleteComplete', [
&$wikiPageBeforeDelete,
* 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 ) {
// 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 );
* Abstraction for ResourceLoader modules which pull from wiki pages
*
* This can only be used for wiki pages in the MediaWiki and User namespaces,
- * because of its dependence on the functionality of Title::isCssJsSubpage.
+ * because of its dependence on the functionality of Title::isCssJsSubpage
+ * and Title::isCssOrJsPage().
*
* This module supports being used as a placeholder for a module on a remote wiki.
* To do so, getDB() must be overloaded to return a foreign database object that
}
/**
- * @param string $title
+ * @param string $titleText
* @return null|string
*/
protected function getContent( $titleText ) {
* @since 1.28
* @param ResourceLoaderContext $context
* @param IDatabase $db
- * @param string[] $modules
+ * @param string[] $moduleNames
*/
public static function preloadTitleInfo(
ResourceLoaderContext $context, IDatabase $db, array $moduleNames
// getDB() can be overridden to point to a foreign database.
// For now, only preload local. In the future, we could preload by wikiID.
$allPages = [];
+ /** @var ResourceLoaderWikiModule[] $wikiModules */
$wikiModules = [];
foreach ( $moduleNames as $name ) {
$module = $rl->getModule( $name );
}
}
}
- $allInfo = static::fetchTitleInfo( $db, array_keys( $allPages ), __METHOD__ );
- foreach ( $wikiModules as $module ) {
- $pages = $module->getPages( $context );
+
+ $pageNames = array_keys( $allPages );
+ sort( $pageNames );
+ $hash = sha1( implode( '|', $pageNames ) );
+
+ // Avoid Zend bug where "static::" does not apply LSB in the closure
+ $func = [ static::class, 'fetchTitleInfo' ];
+ $fname = __METHOD__;
+
+ $cache = ObjectCache::getMainWANInstance();
+ $allInfo = $cache->getWithSetCallback(
+ $cache->makeGlobalKey( 'resourceloader', 'titleinfo', $db->getWikiID(), $hash ),
+ $cache::TTL_HOUR,
+ function ( $curVal, &$ttl, array &$setOpts ) use ( $func, $pageNames, $db, $fname ) {
+ $setOpts += Database::getCacheSetOptions( $db );
+
+ return call_user_func( $func, $db, $pageNames, $fname );
+ },
+ [ 'checkKeys' => [ $cache->makeGlobalKey( 'resourceloader', 'titleinfo', $db->getWikiID() ) ] ]
+ );
+
+ foreach ( $wikiModules as $wikiModule ) {
+ $pages = $wikiModule->getPages( $context );
// Before we intersect, map the names to canonical form (T145673).
$intersect = [];
foreach ( $pages as $page => $unused ) {
}
}
$info = array_intersect_key( $allInfo, $intersect );
-
$pageNames = array_keys( $pages );
sort( $pageNames );
$key = implode( '|', $pageNames );
- $module->setTitleInfo( $key, $info );
+ $wikiModule->setTitleInfo( $key, $info );
+ }
+ }
+
+ /**
+ * Clear the preloadTitleInfo() cache for all wiki modules on this wiki on
+ * page change if it was a JS or CSS page
+ *
+ * @param Title $title
+ * @param Revision|null $old Prior page revision
+ * @param Revision|null $new New page revision
+ * @param string $wikiId
+ * @since 1.28
+ */
+ public static function invalidateModuleCache(
+ Title $title, Revision $old = null, Revision $new = null, $wikiId
+ ) {
+ static $formats = [ CONTENT_FORMAT_CSS, CONTENT_FORMAT_JAVASCRIPT ];
+
+ if ( $old && in_array( $old->getContentFormat(), $formats ) ) {
+ $purge = true;
+ } elseif ( $new && in_array( $new->getContentFormat(), $formats ) ) {
+ $purge = true;
+ } else {
+ $purge = ( $title->isCssOrJsPage() || $title->isCssJsSubpage() );
+ }
+
+ if ( $purge ) {
+ $cache = ObjectCache::getMainWANInstance();
+ $key = $cache->makeGlobalKey( 'resourceloader', 'titleinfo', $wikiId );
+ $cache->touchCheckKey( $key );
}
- return $allInfo;
}
/**
$form->addHeaderText( $headerMsg->parseAsBlock() );
}
- // Retain query parameters (uselang etc)
- $params = array_diff_key(
- $this->getRequest()->getQueryValues(), [ 'title' => null ] );
- $form->addHiddenField( 'redirectparams', wfArrayToCgi( $params ) );
-
$form->addPreText( $this->preText() );
$form->addPostText( $this->postText() );
$this->alterForm( $form );
+ if ( $form->getMethod() == 'post' ) {
+ // Retain query parameters (uselang etc) on POST requests
+ $params = array_diff_key(
+ $this->getRequest()->getQueryValues(), [ 'title' => null ] );
+ $form->addHiddenField( 'redirectparams', wfArrayToCgi( $params ) );
+ }
// Give hooks a chance to alter the form, adding extra fields or text etc
Hooks::run( 'SpecialPageBeforeFormDisplay', [ $this->getName(), &$form ] );
$rl->register( 'testmodule', $module );
$context = new ResourceLoaderContext( $rl, new FauxRequest() );
+ TestResourceLoaderWikiModule::invalidateModuleCache(
+ Title::newFromText( 'MediaWiki:Common.css' ),
+ null,
+ null,
+ wfWikiID()
+ );
TestResourceLoaderWikiModule::preloadTitleInfo(
$context,
wfGetDB( DB_REPLICA ),