'SiteLookup' => __DIR__ . '/includes/site/SiteLookup.php',
'SiteSQLStore' => __DIR__ . '/includes/site/SiteSQLStore.php',
'SiteStats' => __DIR__ . '/includes/SiteStats.php',
- 'SiteStatsInit' => __DIR__ . '/includes/SiteStats.php',
+ 'SiteStatsInit' => __DIR__ . '/includes/SiteStatsInit.php',
'SiteStatsUpdate' => __DIR__ . '/includes/deferred/SiteStatsUpdate.php',
'SiteStore' => __DIR__ . '/includes/site/SiteStore.php',
'SitesCacheFileBuilder' => __DIR__ . '/includes/site/SitesCacheFileBuilder.php',
use Wikimedia\Rdbms\Database;
use Wikimedia\Rdbms\IDatabase;
use MediaWiki\MediaWikiServices;
+use Wikimedia\Rdbms\LoadBalancer;
/**
* Static accessor class for site_stats and related things
*/
class SiteStats {
- /** @var bool|stdClass */
+ /** @var stdClass */
private static $row;
- /** @var bool */
- private static $loaded = false;
- /** @var int[] */
- private static $pageCount = [];
-
- static function unload() {
- self::$loaded = false;
- }
-
- static function recache() {
- self::load( true );
- }
-
/**
- * @param bool $recache
+ * Trigger a reload next time a field is accessed
*/
- static function load( $recache = false ) {
- if ( self::$loaded && !$recache ) {
- return;
- }
-
- self::$row = self::loadAndLazyInit();
+ public static function unload() {
+ self::$row = null;
+ }
- self::$loaded = true;
+ protected static function load() {
+ if ( self::$row === null ) {
+ self::$row = self::loadAndLazyInit();
+ }
}
/**
- * @return bool|stdClass
+ * @return stdClass
*/
- static function loadAndLazyInit() {
- global $wgMiserMode;
+ protected static function loadAndLazyInit() {
+ $config = MediaWikiServices::getInstance()->getMainConfig();
+ $lb = self::getLB();
+ $dbr = $lb->getConnection( DB_REPLICA );
wfDebug( __METHOD__ . ": reading site_stats from replica DB\n" );
- $row = self::doLoad( wfGetDB( DB_REPLICA ) );
+ $row = self::doLoadFromDB( $dbr );
- if ( !self::isSane( $row ) ) {
- $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
- if ( $lb->hasOrMadeRecentMasterChanges() ) {
- // Might have just been initialized during this request? Underflow?
- wfDebug( __METHOD__ . ": site_stats damaged or missing on replica DB\n" );
- $row = self::doLoad( wfGetDB( DB_MASTER ) );
- }
+ if ( !self::isSane( $row ) && $lb->hasOrMadeRecentMasterChanges() ) {
+ // Might have just been initialized during this request? Underflow?
+ wfDebug( __METHOD__ . ": site_stats damaged or missing on replica DB\n" );
+ $row = self::doLoadFromDB( $lb->getConnection( DB_MASTER ) );
}
if ( !self::isSane( $row ) ) {
- if ( $wgMiserMode ) {
+ if ( $config->get( 'MiserMode' ) ) {
// Start off with all zeroes, assuming that this is a new wiki or any
// repopulations where done manually via script.
SiteStatsInit::doPlaceholderInit();
// broken, however, for instance when importing from a dump into a
// clean schema with mwdumper.
wfDebug( __METHOD__ . ": initializing damaged or missing site_stats\n" );
- SiteStatsInit::doAllAndCommit( wfGetDB( DB_REPLICA ) );
+ SiteStatsInit::doAllAndCommit( $dbr );
}
- $row = self::doLoad( wfGetDB( DB_MASTER ) );
+ $row = self::doLoadFromDB( $lb->getConnection( DB_MASTER ) );
}
if ( !self::isSane( $row ) ) {
wfDebug( __METHOD__ . ": site_stats persistently nonsensical o_O\n" );
-
+ // Always return a row-like object
$row = (object)array_fill_keys( self::selectFields(), 0 );
}
/**
* @param IDatabase $db
- * @return bool|stdClass
- */
- static function doLoad( $db ) {
- return $db->selectRow( 'site_stats', self::selectFields(), [], __METHOD__ );
- }
-
- /**
- * Return the total number of page views. Except we don't track those anymore.
- * Stop calling this function, it will be removed some time in the future. It's
- * kept here simply to prevent fatal errors.
- *
- * @deprecated since 1.25
- * @return int
+ * @return stdClass|bool
*/
- static function views() {
- wfDeprecated( __METHOD__, '1.25' );
- return 0;
+ private static function doLoadFromDB( IDatabase $db ) {
+ return $db->selectRow(
+ 'site_stats',
+ self::selectFields(),
+ [ 'ss_row_id' => 1 ],
+ __METHOD__
+ );
}
/**
* @return int
*/
- static function edits() {
+ public static function edits() {
self::load();
+
return self::$row->ss_total_edits;
}
/**
* @return int
*/
- static function articles() {
+ public static function articles() {
self::load();
+
return self::$row->ss_good_articles;
}
/**
* @return int
*/
- static function pages() {
+ public static function pages() {
self::load();
+
return self::$row->ss_total_pages;
}
/**
* @return int
*/
- static function users() {
+ public static function users() {
self::load();
+
return self::$row->ss_users;
}
/**
* @return int
*/
- static function activeUsers() {
+ public static function activeUsers() {
self::load();
+
return self::$row->ss_active_users;
}
/**
* @return int
*/
- static function images() {
+ public static function images() {
self::load();
+
return self::$row->ss_images;
}
* @param string $group Name of group
* @return int
*/
- static function numberingroup( $group ) {
+ public static function numberingroup( $group ) {
$cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+
return $cache->getWithSetCallback(
$cache->makeKey( 'SiteStats', 'groupcounts', $group ),
$cache::TTL_HOUR,
function ( $oldValue, &$ttl, array &$setOpts ) use ( $group ) {
- $dbr = wfGetDB( DB_REPLICA );
-
+ $dbr = self::getLB()->getConnection( DB_REPLICA );
$setOpts += Database::getCacheSetOptions( $dbr );
- return $dbr->selectField(
+ return (int)$dbr->selectField(
'user_groups',
'COUNT(*)',
[
* Total number of jobs in the job queue.
* @return int
*/
- static function jobs() {
+ public static function jobs() {
$cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+
return $cache->getWithSetCallback(
$cache->makeKey( 'SiteStats', 'jobscount' ),
$cache::TTL_MINUTE,
/**
* @param int $ns
- *
* @return int
*/
- static function pagesInNs( $ns ) {
- if ( !isset( self::$pageCount[$ns] ) ) {
- $dbr = wfGetDB( DB_REPLICA );
- self::$pageCount[$ns] = (int)$dbr->selectField(
- 'page',
- 'COUNT(*)',
- [ 'page_namespace' => $ns ],
- __METHOD__
- );
- }
- return self::$pageCount[$ns];
+ public static function pagesInNs( $ns ) {
+ $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+
+ return $cache->getWithSetCallback(
+ $cache->makeKey( 'SiteStats', 'page-in-namespace', $ns ),
+ $cache::TTL_HOUR,
+ function ( $oldValue, &$ttl, array &$setOpts ) use ( $ns ) {
+ $dbr = self::getLB()->getConnection( DB_REPLICA );
+ $setOpts += Database::getCacheSetOptions( $dbr );
+
+ return (int)$dbr->selectField(
+ 'page',
+ 'COUNT(*)',
+ [ 'page_namespace' => $ns ],
+ __METHOD__
+ );
+ },
+ [ 'pcTTL' => $cache::TTL_PROC_LONG ]
+ );
}
/**
*/
public static function selectFields() {
return [
- 'ss_row_id',
'ss_total_edits',
'ss_good_articles',
'ss_total_pages',
* Checks only fields which are filled by SiteStatsInit::refresh.
*
* @param bool|object $row
- *
* @return bool
*/
private static function isSane( $row ) {
return false;
}
}
- return true;
- }
-}
-
-/**
- * Class designed for counting of stats.
- */
-class SiteStatsInit {
-
- // Database connection
- private $db;
-
- // Various stats
- private $mEdits = null, $mArticles = null, $mPages = null;
- private $mUsers = null, $mFiles = null;
-
- /**
- * @param bool|IDatabase $database
- * - bool: Whether to use the master DB
- * - IDatabase: Database connection to use
- */
- public function __construct( $database = false ) {
- if ( $database instanceof IDatabase ) {
- $this->db = $database;
- } elseif ( $database ) {
- $this->db = wfGetDB( DB_MASTER );
- } else {
- $this->db = wfGetDB( DB_REPLICA, 'vslow' );
- }
- }
-
- /**
- * Count the total number of edits
- * @return int
- */
- public function edits() {
- $this->mEdits = $this->db->selectField( 'revision', 'COUNT(*)', '', __METHOD__ );
- $this->mEdits += $this->db->selectField( 'archive', 'COUNT(*)', '', __METHOD__ );
- return $this->mEdits;
- }
-
- /**
- * Count pages in article space(s)
- * @return int
- */
- public function articles() {
- global $wgArticleCountMethod;
-
- $tables = [ 'page' ];
- $conds = [
- 'page_namespace' => MWNamespace::getContentNamespaces(),
- 'page_is_redirect' => 0,
- ];
-
- if ( $wgArticleCountMethod == 'link' ) {
- $tables[] = 'pagelinks';
- $conds[] = 'pl_from=page_id';
- } elseif ( $wgArticleCountMethod == 'comma' ) {
- // To make a correct check for this, we would need, for each page,
- // to load the text, maybe uncompress it, maybe decode it and then
- // check if there's one comma.
- // But one thing we are sure is that if the page is empty, it can't
- // contain a comma :)
- $conds[] = 'page_len > 0';
- }
-
- $this->mArticles = $this->db->selectField( $tables, 'COUNT(DISTINCT page_id)',
- $conds, __METHOD__ );
- return $this->mArticles;
- }
- /**
- * Count total pages
- * @return int
- */
- public function pages() {
- $this->mPages = $this->db->selectField( 'page', 'COUNT(*)', '', __METHOD__ );
- return $this->mPages;
- }
-
- /**
- * Count total users
- * @return int
- */
- public function users() {
- $this->mUsers = $this->db->selectField( 'user', 'COUNT(*)', '', __METHOD__ );
- return $this->mUsers;
- }
-
- /**
- * Count total files
- * @return int
- */
- public function files() {
- $this->mFiles = $this->db->selectField( 'image', 'COUNT(*)', '', __METHOD__ );
- return $this->mFiles;
- }
-
- /**
- * Do all updates and commit them. More or less a replacement
- * for the original initStats, but without output.
- *
- * @param IDatabase|bool $database
- * - bool: Whether to use the master DB
- * - IDatabase: Database connection to use
- * @param array $options Array of options, may contain the following values
- * - activeUsers bool: Whether to update the number of active users (default: false)
- */
- public static function doAllAndCommit( $database, array $options = [] ) {
- $options += [ 'update' => false, 'activeUsers' => false ];
-
- // Grab the object and count everything
- $counter = new SiteStatsInit( $database );
-
- $counter->edits();
- $counter->articles();
- $counter->pages();
- $counter->users();
- $counter->files();
-
- $counter->refresh();
-
- // Count active users if need be
- if ( $options['activeUsers'] ) {
- SiteStatsUpdate::cacheUpdate( wfGetDB( DB_MASTER ) );
- }
- }
-
- /**
- * Insert a dummy row with all zeroes if no row is present
- */
- public static function doPlaceholderInit() {
- $dbw = wfGetDB( DB_MASTER );
- if ( $dbw->selectRow( 'site_stats', '1', [], __METHOD__ ) === false ) {
- $dbw->insert(
- 'site_stats',
- array_fill_keys( SiteStats::selectFields(), 0 ),
- __METHOD__,
- [ 'IGNORE' ]
- );
- }
+ return true;
}
/**
- * Refresh site_stats
+ * @return LoadBalancer
*/
- public function refresh() {
- $values = [
- 'ss_row_id' => 1,
- 'ss_total_edits' => ( $this->mEdits === null ? $this->edits() : $this->mEdits ),
- 'ss_good_articles' => ( $this->mArticles === null ? $this->articles() : $this->mArticles ),
- 'ss_total_pages' => ( $this->mPages === null ? $this->pages() : $this->mPages ),
- 'ss_users' => ( $this->mUsers === null ? $this->users() : $this->mUsers ),
- 'ss_images' => ( $this->mFiles === null ? $this->files() : $this->mFiles ),
- ];
-
- $dbw = wfGetDB( DB_MASTER );
- $dbw->upsert( 'site_stats', $values, [ 'ss_row_id' ], $values, __METHOD__ );
+ private static function getLB() {
+ return MediaWikiServices::getInstance()->getDBLoadBalancer();
}
}
--- /dev/null
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+use Wikimedia\Rdbms\IDatabase;
+use MediaWiki\MediaWikiServices;
+
+/**
+ * Class designed for counting of stats.
+ */
+class SiteStatsInit {
+ /* @var IDatabase */
+ private $dbr;
+ /** @var int */
+ private $edits;
+ /** @var int */
+ private $articles;
+ /** @var int */
+ private $pages;
+ /** @var int */
+ private $users;
+ /** @var int */
+ private $files;
+
+ /**
+ * @param bool|IDatabase $database
+ * - bool: Whether to use the master DB
+ * - IDatabase: Database connection to use
+ */
+ public function __construct( $database = false ) {
+ if ( $database instanceof IDatabase ) {
+ $this->dbr = $database;
+ } elseif ( $database ) {
+ $this->dbr = self::getDB( DB_MASTER );
+ } else {
+ $this->dbr = self::getDB( DB_REPLICA, 'vslow' );
+ }
+ }
+
+ /**
+ * Count the total number of edits
+ * @return int
+ */
+ public function edits() {
+ $this->edits = $this->dbr->selectField( 'revision', 'COUNT(*)', '', __METHOD__ );
+ $this->edits += $this->dbr->selectField( 'archive', 'COUNT(*)', '', __METHOD__ );
+
+ return $this->edits;
+ }
+
+ /**
+ * Count pages in article space(s)
+ * @return int
+ */
+ public function articles() {
+ $config = MediaWikiServices::getInstance()->getMainConfig();
+
+ $tables = [ 'page' ];
+ $conds = [
+ 'page_namespace' => MWNamespace::getContentNamespaces(),
+ 'page_is_redirect' => 0,
+ ];
+
+ if ( $config->get( 'ArticleCountMethod' ) == 'link' ) {
+ $tables[] = 'pagelinks';
+ $conds[] = 'pl_from=page_id';
+ } elseif ( $config->get( 'ArticleCountMethod' ) == 'comma' ) {
+ // To make a correct check for this, we would need, for each page,
+ // to load the text, maybe uncompress it, maybe decode it and then
+ // check if there's one comma.
+ // But one thing we are sure is that if the page is empty, it can't
+ // contain a comma :)
+ $conds[] = 'page_len > 0';
+ }
+
+ $this->articles = $this->dbr->selectField(
+ $tables,
+ 'COUNT(DISTINCT page_id)',
+ $conds,
+ __METHOD__
+ );
+
+ return $this->articles;
+ }
+
+ /**
+ * Count total pages
+ * @return int
+ */
+ public function pages() {
+ $this->pages = $this->dbr->selectField( 'page', 'COUNT(*)', '', __METHOD__ );
+
+ return $this->pages;
+ }
+
+ /**
+ * Count total users
+ * @return int
+ */
+ public function users() {
+ $this->users = $this->dbr->selectField( 'user', 'COUNT(*)', '', __METHOD__ );
+
+ return $this->users;
+ }
+
+ /**
+ * Count total files
+ * @return int
+ */
+ public function files() {
+ $this->files = $this->dbr->selectField( 'image', 'COUNT(*)', '', __METHOD__ );
+
+ return $this->files;
+ }
+
+ /**
+ * Do all updates and commit them. More or less a replacement
+ * for the original initStats, but without output.
+ *
+ * @param IDatabase|bool $database
+ * - bool: Whether to use the master DB
+ * - IDatabase: Database connection to use
+ * @param array $options Array of options, may contain the following values
+ * - activeUsers bool: Whether to update the number of active users (default: false)
+ */
+ public static function doAllAndCommit( $database, array $options = [] ) {
+ $options += [ 'update' => false, 'activeUsers' => false ];
+
+ // Grab the object and count everything
+ $counter = new self( $database );
+
+ $counter->edits();
+ $counter->articles();
+ $counter->pages();
+ $counter->users();
+ $counter->files();
+
+ $counter->refresh();
+
+ // Count active users if need be
+ if ( $options['activeUsers'] ) {
+ SiteStatsUpdate::cacheUpdate( self::getDB( DB_MASTER ) );
+ }
+ }
+
+ /**
+ * Insert a dummy row with all zeroes if no row is present
+ */
+ public static function doPlaceholderInit() {
+ $dbw = self::getDB( DB_MASTER );
+ $exists = $dbw->selectField( 'site_stats', '1', [ 'ss_row_id' => 1 ], __METHOD__ );
+ if ( $exists === false ) {
+ $dbw->insert(
+ 'site_stats',
+ [ 'ss_row_id' => 1 ] + array_fill_keys( SiteStats::selectFields(), 0 ),
+ __METHOD__,
+ [ 'IGNORE' ]
+ );
+ }
+ }
+
+ /**
+ * Refresh site_stats
+ */
+ public function refresh() {
+ $values = [
+ 'ss_row_id' => 1,
+ 'ss_total_edits' => $this->edits === null ? $this->edits() : $this->edits,
+ 'ss_good_articles' => $this->articles === null ? $this->articles() : $this->articles,
+ 'ss_total_pages' => $this->pages === null ? $this->pages() : $this->pages,
+ 'ss_users' => $this->users === null ? $this->users() : $this->users,
+ 'ss_images' => $this->files === null ? $this->files() : $this->files,
+ ];
+
+ self::getDB( DB_MASTER )->upsert(
+ 'site_stats',
+ $values,
+ [ 'ss_row_id' ],
+ $values,
+ __METHOD__
+ );
+ }
+
+ /**
+ * @param int $index
+ * @return IDatabase
+ */
+ private static function getDB( $index ) {
+ return MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( $index );
+ }
+}
* Class for handling updates to the site_stats table
*/
class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate {
+ /** @var BagOStuff */
+ protected $stash;
/** @var int */
protected $edits = 0;
/** @var int */
$this->articles = $good;
$this->pages = $pages;
$this->users = $users;
+
+ $this->stash = MediaWikiServices::getInstance()->getMainObjectStash();
}
public function merge( MergeableUpdate $update ) {
}
public function doUpdate() {
- global $wgSiteStatsAsyncFactor;
-
$this->doUpdateContextStats();
- $rate = $wgSiteStatsAsyncFactor; // convenience
+ $rate = MediaWikiServices::getInstance()->getMainConfig()->get( 'SiteStatsAsyncFactor' );
// If set to do so, only do actual DB updates 1 every $rate times.
// The other times, just update "pending delta" values in memcached.
if ( $rate && ( $rate < 0 || mt_rand( 0, $rate - 1 ) != 0 ) ) {
* Do not call this outside of SiteStatsUpdate
*/
public function tryDBUpdateInternal() {
- global $wgSiteStatsAsyncFactor;
+ $services = MediaWikiServices::getInstance();
+ $config = $services->getMainConfig();
- $dbw = wfGetDB( DB_MASTER );
- $lockKey = wfWikiID() . ':site_stats'; // prepend wiki ID
+ $dbw = $services->getDBLoadBalancer()->getConnection( DB_MASTER );
+ $lockKey = $dbw->getDomainID() . ':site_stats'; // prepend wiki ID
$pd = [];
- if ( $wgSiteStatsAsyncFactor ) {
+ if ( $config->get( 'SiteStatsAsyncFactor' ) ) {
// Lock the table so we don't have double DB/memcached updates
if ( !$dbw->lockIsFree( $lockKey, __METHOD__ )
|| !$dbw->lock( $lockKey, __METHOD__, 1 ) // 1 sec timeout
$dbw->update( 'site_stats', [ $updates ], [], __METHOD__ );
}
- if ( $wgSiteStatsAsyncFactor ) {
+ if ( $config->get( 'SiteStatsAsyncFactor' ) ) {
// Decrement the async deltas now that we applied them
$this->removePendingDeltas( $pd );
// Commit the updates and unlock the table
* @param IDatabase $dbw
* @return bool|mixed
*/
- public static function cacheUpdate( $dbw ) {
- global $wgActiveUserDays;
- $dbr = wfGetDB( DB_REPLICA, 'vslow' );
+ public static function cacheUpdate( IDatabase $dbw ) {
+ $services = MediaWikiServices::getInstance();
+ $config = $services->getMainConfig();
+
+ $dbr = $services->getDBLoadBalancer()->getConnection( DB_REPLICA, 'vslow' );
# Get non-bot users than did some recent action other than making accounts.
# If account creation is included, the number gets inflated ~20+ fold on enwiki.
$activeUsers = $dbr->selectField(
'rc_user != 0',
'rc_bot' => 0,
'rc_log_type != ' . $dbr->addQuotes( 'newusers' ) . ' OR rc_log_type IS NULL',
- 'rc_timestamp >= ' . $dbr->addQuotes( $dbr->timestamp( wfTimestamp( TS_UNIX )
- - $wgActiveUserDays * 24 * 3600 ) ),
+ 'rc_timestamp >= ' . $dbr->addQuotes(
+ $dbr->timestamp( time() - $config->get( 'ActiveUserDays' ) * 24 * 3600 ) ),
],
__METHOD__
);
}
/**
- * @param BagOStuff $cache
+ * @param BagOStuff $stash
* @param string $type
* @param string $sign ('+' or '-')
* @return string
*/
- private function getTypeCacheKey( BagOStuff $cache, $type, $sign ) {
- return $cache->makeKey( 'sitestatsupdate', 'pendingdelta', $type, $sign );
+ private function getTypeCacheKey( BagOStuff $stash, $type, $sign ) {
+ return $stash->makeKey( 'sitestatsupdate', 'pendingdelta', $type, $sign );
}
/**
* @param int $delta Delta (positive or negative)
*/
protected function adjustPending( $type, $delta ) {
- $cache = MediaWikiServices::getInstance()->getMainObjectStash();
if ( $delta < 0 ) { // decrement
- $key = $this->getTypeCacheKey( $cache, $type, '-' );
+ $key = $this->getTypeCacheKey( $this->stash, $type, '-' );
} else { // increment
- $key = $this->getTypeCacheKey( $cache, $type, '+' );
+ $key = $this->getTypeCacheKey( $this->stash, $type, '+' );
}
$magnitude = abs( $delta );
- $cache->incrWithInit( $key, 0, $magnitude, $magnitude );
+ $this->stash->incrWithInit( $key, 0, $magnitude, $magnitude );
}
/**
* @return array Positive and negative deltas for each type
*/
protected function getPendingDeltas() {
- $cache = MediaWikiServices::getInstance()->getMainObjectStash();
-
$pending = [];
foreach ( [ 'ss_total_edits',
'ss_good_articles', 'ss_total_pages', 'ss_users', 'ss_images' ] as $type
) {
// Get pending increments and pending decrements
$flg = BagOStuff::READ_LATEST;
- $pending[$type]['+'] = (int)$cache->get( $this->getTypeCacheKey( $cache, $type, '+' ), $flg );
- $pending[$type]['-'] = (int)$cache->get( $this->getTypeCacheKey( $cache, $type, '-' ), $flg );
+ $pending[$type]['+'] = (int)$this->stash->get(
+ $this->getTypeCacheKey( $this->stash, $type, '+' ),
+ $flg
+ );
+ $pending[$type]['-'] = (int)$this->stash->get(
+ $this->getTypeCacheKey( $this->stash, $type, '-' ),
+ $flg
+ );
}
return $pending;
* @param array $pd Result of getPendingDeltas(), used for DB update
*/
protected function removePendingDeltas( array $pd ) {
- $cache = MediaWikiServices::getInstance()->getMainObjectStash();
-
foreach ( $pd as $type => $deltas ) {
foreach ( $deltas as $sign => $magnitude ) {
// Lower the pending counter now that we applied these changes
- $cache->decr( $this->getTypeCacheKey( $cache, $type, $sign ), $magnitude );
+ $key = $this->getTypeCacheKey( $this->stash, $type, $sign );
+ $this->stash->decr( $key, $magnitude );
}
}
}
$user->saveSettings();
// Update user count
- $ssUpdate = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
+ $ssUpdate = SiteStatsUpdate::factory( [ 'users' => 1 ] );
$ssUpdate->doUpdate();
}
$status = Status::newGood();
$edits = $options['changed'] ? 1 : 0;
$total = $options['created'] ? 1 : 0;
- DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, $edits, $good, $total ) );
+ DeferredUpdates::addUpdate( SiteStatsUpdate::factory(
+ [ 'edits' => $edits, 'articles' => $good, 'total' => $total ]
+ ) );
DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content ) );
// If this is another user's talk page, update newtalk.
}
// Update site status
- DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$countable, -1 ) );
+ DeferredUpdates::addUpdate( SiteStatsUpdate::factory(
+ [ 'edits' => 1, 'articles' => -$countable, 'pages' => -1 ]
+ ) );
// Delete pagelinks, update secondary indexes, etc
$updates = $this->getDeletionUpdates( $content );
}
/**
+ * @codeCoverageIgnore
* @return bool
*/
public function checkDependencies() {
private static $instance;
/**
+ * @codeCoverageIgnore
* @return ExtensionRegistry
*/
public static function getInstance() {
} else {
throw new Exception( "$path does not exist!" );
}
+ // @codeCoverageIgnoreStart
if ( !$mtime ) {
$err = error_get_last();
throw new Exception( "Couldn't stat $path: {$err['message']}" );
+ // @codeCoverageIgnoreEnd
}
}
$this->queued[$path] = $mtime;
return $this->loaded;
}
- /**
- * Mark a thing as loaded
- *
- * @param string $name
- * @param array $credits
- */
- protected function markLoaded( $name, array $credits ) {
- $this->loaded[$name] = $credits;
- }
-
/**
* Fully expand autoloader paths
*
if ( !isset( $this->loaded[$dependencyName]['version'] ) ) {
// If we depend upon any version, and none is set, that's fine.
if ( $constraint === '*' ) {
- wfDebug( "{$dependencyName} does not expose it's version, but {$checkedExt}
- mentions it with constraint '*'. Assume it's ok so." );
+ wfDebug( "{$dependencyName} does not expose its version, but {$checkedExt}"
+ . " mentions it with constraint '*'. Assume it's ok so." );
return false;
} else {
// Otherwise, mark it as incompatible.
- return "{$dependencyName} does not expose it's version, but {$checkedExt}
- requires: {$constraint}.";
+ return "{$dependencyName} does not expose its version, but {$checkedExt}"
+ . " requires: {$constraint}.";
}
} else {
// Try to get a constraint for the dependency version
if ( $offender ) {
if ( $offender->getId() > 0 ) {
$qc = [ 'ls_field' => 'target_author_id', 'ls_value' => $offender->getId() ];
- } elseif ( empty( $opts->getValue( 'offender' ) ) === false ) {
+ } elseif ( !empty( $opts->getValue( 'offender' ) ) ) {
$qc = [ 'ls_field' => 'target_author_ip', 'ls_value' => $offender->getName() ];
}
}
* @singleton
*/
-/* eslint-disable no-use-before-define */
/* global Uint8Array */
( function ( mw, $ ) {
return mw.msg( sizeMsgs[ 0 ], Math.round( s ) );
}
+ /**
+ * Start loading a file into memory; when complete, pass it as a
+ * data URL to the callback function. If the callbackBinary is set it will
+ * first be read as binary and afterwards as data URL. Useful if you want
+ * to do preprocessing on the binary data first.
+ *
+ * @param {File} file
+ * @param {Function} callback
+ * @param {Function} callbackBinary
+ */
+ function fetchPreview( file, callback, callbackBinary ) {
+ var reader = new FileReader();
+ if ( callbackBinary && 'readAsBinaryString' in reader ) {
+ // To fetch JPEG metadata we need a binary string; start there.
+ // TODO
+ reader.onload = function () {
+ callbackBinary( reader.result );
+
+ // Now run back through the regular code path.
+ fetchPreview( file, callback );
+ };
+ reader.readAsBinaryString( file );
+ } else if ( callbackBinary && 'readAsArrayBuffer' in reader ) {
+ // readAsArrayBuffer replaces readAsBinaryString
+ // However, our JPEG metadata library wants a string.
+ // So, this is going to be an ugly conversion.
+ reader.onload = function () {
+ var i,
+ buffer = new Uint8Array( reader.result ),
+ string = '';
+ for ( i = 0; i < buffer.byteLength; i++ ) {
+ string += String.fromCharCode( buffer[ i ] );
+ }
+ callbackBinary( string );
+
+ // Now run back through the regular code path.
+ fetchPreview( file, callback );
+ };
+ reader.readAsArrayBuffer( file );
+ } else if ( 'URL' in window && 'createObjectURL' in window.URL ) {
+ // Supported in Firefox 4.0 and above <https://developer.mozilla.org/en/DOM/window.URL.createObjectURL>
+ // WebKit has it in a namespace for now but that's ok. ;)
+ //
+ // Lifetime of this URL is until document close, which is fine
+ // for Special:Upload -- if this code gets used on longer-running
+ // pages, add a revokeObjectURL() when it's no longer needed.
+ //
+ // Prefer this over readAsDataURL for Firefox 7 due to bug reading
+ // some SVG files from data URIs <https://bugzilla.mozilla.org/show_bug.cgi?id=694165>
+ callback( window.URL.createObjectURL( file ) );
+ } else {
+ // This ends up decoding the file to base-64 and back again, which
+ // feels horribly inefficient.
+ reader.onload = function () {
+ callback( reader.result );
+ };
+ reader.readAsDataURL( file );
+ }
+ }
+
+ /**
+ * Clear the file upload preview area.
+ */
+ function clearPreview() {
+ $( '#mw-upload-thumbnail' ).remove();
+ }
+
/**
* Show a thumbnail preview of PNG, JPEG, GIF, and SVG files prior to upload
* in browsers supporting HTML5 FileAPI.
} : null );
}
- /**
- * Start loading a file into memory; when complete, pass it as a
- * data URL to the callback function. If the callbackBinary is set it will
- * first be read as binary and afterwards as data URL. Useful if you want
- * to do preprocessing on the binary data first.
- *
- * @param {File} file
- * @param {Function} callback
- * @param {Function} callbackBinary
- */
- function fetchPreview( file, callback, callbackBinary ) {
- var reader = new FileReader();
- if ( callbackBinary && 'readAsBinaryString' in reader ) {
- // To fetch JPEG metadata we need a binary string; start there.
- // TODO
- reader.onload = function () {
- callbackBinary( reader.result );
-
- // Now run back through the regular code path.
- fetchPreview( file, callback );
- };
- reader.readAsBinaryString( file );
- } else if ( callbackBinary && 'readAsArrayBuffer' in reader ) {
- // readAsArrayBuffer replaces readAsBinaryString
- // However, our JPEG metadata library wants a string.
- // So, this is going to be an ugly conversion.
- reader.onload = function () {
- var i,
- buffer = new Uint8Array( reader.result ),
- string = '';
- for ( i = 0; i < buffer.byteLength; i++ ) {
- string += String.fromCharCode( buffer[ i ] );
- }
- callbackBinary( string );
-
- // Now run back through the regular code path.
- fetchPreview( file, callback );
- };
- reader.readAsArrayBuffer( file );
- } else if ( 'URL' in window && 'createObjectURL' in window.URL ) {
- // Supported in Firefox 4.0 and above <https://developer.mozilla.org/en/DOM/window.URL.createObjectURL>
- // WebKit has it in a namespace for now but that's ok. ;)
- //
- // Lifetime of this URL is until document close, which is fine
- // for Special:Upload -- if this code gets used on longer-running
- // pages, add a revokeObjectURL() when it's no longer needed.
- //
- // Prefer this over readAsDataURL for Firefox 7 due to bug reading
- // some SVG files from data URIs <https://bugzilla.mozilla.org/show_bug.cgi?id=694165>
- callback( window.URL.createObjectURL( file ) );
- } else {
- // This ends up decoding the file to base-64 and back again, which
- // feels horribly inefficient.
- reader.onload = function () {
- callback( reader.result );
- };
- reader.readAsDataURL( file );
- }
- }
-
- /**
- * Clear the file upload preview area.
- */
- function clearPreview() {
- $( '#mw-upload-thumbnail' ).remove();
- }
-
/**
* Check if the file does not exceed the maximum size
*
protected $dependencies = [];
protected $group = null;
protected $source = 'local';
- protected $position = 'bottom';
protected $script = '';
protected $styles = '';
protected $skipFunction = null;
public function getSource() {
return $this->source;
}
- public function getPosition() {
- return $this->position;
- }
public function getType() {
return $this->type;
--- /dev/null
+{
+ "name": "FooBar",
+ "license-name": "not a license identifier",
+ "manifest_version": 1
+}
+
--- /dev/null
+{
+ "name": "FooBar",
+ "manifest_version": 1
+}
--- /dev/null
+{
+ "name": "FooBar",
+ "license-name": [ "array" ],
+ "manifest_version": 1
+}
--- /dev/null
+{
+ "name": "FooBar",
+ "manifest_version": 999999
+}
--- /dev/null
+{
+ "name": "FooBar"
+}
--- /dev/null
+This is definitely not JSON.
--- /dev/null
+{
+ "name": "FooBar",
+ "manifest_version": -2
+}
--- /dev/null
+<?php
+/**
+ * Copyright (C) 2018 Kunal Mehta <legoktm@member.fsf.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+
+/**
+ * @covers ExtensionJsonValidator
+ */
+class ExtensionJsonValidatorTest extends MediaWikiTestCase {
+
+ /**
+ * @dataProvider provideValidate
+ */
+ public function testValidate( $file, $expected ) {
+ // If a dependency is missing, skip this test.
+ $validator = new ExtensionJsonValidator( function ( $msg ) {
+ $this->markTestSkipped( $msg );
+ } );
+
+ if ( is_string( $expected ) ) {
+ $this->setExpectedException(
+ ExtensionJsonValidationError::class,
+ $expected
+ );
+ }
+
+ $dir = __DIR__ . '/../../data/registration/';
+ $this->assertSame(
+ $expected,
+ $validator->validate( $dir . $file )
+ );
+ }
+
+ public function provideValidate() {
+ return [
+ [
+ 'notjson.txt',
+ 'notjson.txt is not valid JSON'
+ ],
+ [
+ 'no_manifest_version.json',
+ 'no_manifest_version.json does not have manifest_version set.'
+ ],
+ [
+ 'old_manifest_version.json',
+ 'old_manifest_version.json is using a non-supported schema version'
+ ],
+ [
+ 'newer_manifest_version.json',
+ 'newer_manifest_version.json is using a non-supported schema version'
+ ],
+ [
+ 'bad_spdx.json',
+ "bad_spdx.json did not pass validation.
+[license-name] Invalid SPDX license identifier, see <https://spdx.org/licenses/>"
+ ],
+ [
+ 'invalid.json',
+ "invalid.json did not pass validation.
+[license-name] Array value found, but a string is required"
+ ],
+ [
+ 'good.json',
+ true
+ ],
+ ];
+ }
+
+}
<?php
+/**
+ * @covers ExtensionRegistry
+ */
class ExtensionRegistryTest extends MediaWikiTestCase {
+ private $dataDir;
+
+ public function setUp() {
+ parent::setUp();
+ $this->dataDir = __DIR__ . '/../../data/registration';
+ }
+
+ public function testQueue_invalid() {
+ $registry = new ExtensionRegistry();
+ $path = __DIR__ . '/doesnotexist.json';
+ $this->setExpectedException(
+ Exception::class,
+ "$path does not exist!"
+ );
+ $registry->queue( $path );
+ }
+
+ public function testQueue() {
+ $registry = new ExtensionRegistry();
+ $path = "{$this->dataDir}/good.json";
+ $registry->queue( $path );
+ $this->assertArrayHasKey(
+ $path,
+ $registry->getQueue()
+ );
+ $registry->clearQueue();
+ $this->assertEmpty( $registry->getQueue() );
+ }
+
+ public function testLoadFromQueue_empty() {
+ $registry = new ExtensionRegistry();
+ $registry->loadFromQueue();
+ $this->assertEmpty( $registry->getAllThings() );
+ }
+
+ public function testLoadFromQueue_late() {
+ $registry = new ExtensionRegistry();
+ $registry->finish();
+ $registry->queue( "{$this->dataDir}/good.json" );
+ $this->setExpectedException(
+ MWException::class,
+ "The following paths tried to load late: {$this->dataDir}/good.json"
+ );
+ $registry->loadFromQueue();
+ }
+
/**
- * @covers ExtensionRegistry::exportExtractedData
* @dataProvider provideExportExtractedDataGlobals
*/
public function testExportExtractedDataGlobals( $desc, $before, $globals, $expected ) {
'FakeDependency' => [
'version' => '1.0.0',
],
+ 'NoVersionGiven' => [],
] );
$this->assertEquals( $expected, $checker->checkArray( [
'FakeExtension' => $given,
],
[]
],
+ [
+ [
+ 'extensions' => [
+ 'NoVersionGiven' => '*'
+ ]
+ ],
+ [],
+ ],
+ [
+ [
+ 'extensions' => [
+ 'NoVersionGiven' => '1.0',
+ ]
+ ],
+ [ 'NoVersionGiven does not expose its version, but FakeExtension requires: 1.0.' ],
+ ],
+ [
+ [
+ 'extensions' => [
+ 'Missing' => '*',
+ ]
+ ],
+ [ 'FakeExtension requires Missing to be installed.' ],
+ ],
+ [
+ [
+ 'extensions' => [
+ 'FakeDependency' => '2.0.0',
+ ]
+ ],
+ // phpcs:ignore Generic.Files.LineLength.TooLong
+ [ 'FakeExtension is not compatible with the current installed version of FakeDependency (1.0.0), it requires: 2.0.0.' ],
+ ]
];
}
protected static function makeSampleModules() {
$modules = [
'test' => [],
- 'test.top' => [ 'position' => 'top' ],
- 'test.private.top' => [ 'group' => 'private', 'position' => 'top' ],
- 'test.private.bottom' => [ 'group' => 'private', 'position' => 'bottom' ],
+ 'test.private' => [ 'group' => 'private' ],
'test.shouldembed.empty' => [ 'shouldEmbed' => true, 'isKnownEmpty' => true ],
'test.shouldembed' => [ 'shouldEmbed' => true ],
],
'test.scripts' => [],
- 'test.scripts.top' => [ 'position' => 'top' ],
'test.scripts.user' => [ 'group' => 'user' ],
'test.scripts.user.empty' => [ 'group' => 'user', 'isKnownEmpty' => true ],
'test.scripts.raw' => [ 'isRaw' => true ],
$client = new ResourceLoaderClientHtml( $context );
$client->setModules( [
'test',
- 'test.private.bottom',
- 'test.private.top',
- 'test.top',
+ 'test.private',
'test.shouldembed.empty',
'test.shouldembed',
'test.unregistered',
$client->setModuleScripts( [
'test.scripts',
'test.scripts.user.empty',
- 'test.scripts.top',
'test.scripts.shouldembed',
'test.unregistered.scripts',
] );
$expected = [
'states' => [
- 'test.private.top' => 'loading',
- 'test.private.bottom' => 'loading',
+ 'test.private' => 'loading',
'test.shouldembed.empty' => 'ready',
'test.shouldembed' => 'loading',
'test.styles.pure' => 'ready',
'test.styles.private' => 'ready',
'test.styles.shouldembed' => 'ready',
'test.scripts' => 'loading',
- 'test.scripts.top' => 'loading',
'test.scripts.user.empty' => 'ready',
'test.scripts.shouldembed' => 'loading',
],
'general' => [
'test',
- 'test.top',
],
'styles' => [
'test.styles.pure',
],
'scripts' => [
'test.scripts',
- 'test.scripts.top',
'test.scripts.shouldembed',
],
'embed' => [
'styles' => [ 'test.styles.private', 'test.styles.shouldembed' ],
'general' => [
- 'test.private.bottom',
- 'test.private.top',
+ 'test.private',
'test.shouldembed',
],
],
$client = new ResourceLoaderClientHtml( $context );
$client->setConfig( [ 'key' => 'value' ] );
$client->setModules( [
- 'test.top',
- 'test.private.top',
+ 'test',
+ 'test.private',
] );
$client->setModuleStyles( [
'test.styles.pure',
'test.styles.private',
] );
$client->setModuleScripts( [
- 'test.scripts.top',
+ 'test.scripts',
] );
$client->setExemptStates( [
'test.exempt' => 'ready',
$expected = '<script>document.documentElement.className = document.documentElement.className.replace( /(^|\s)client-nojs(\s|$)/, "$1client-js$2" );</script>' . "\n"
. '<script>(window.RLQ=window.RLQ||[]).push(function(){'
. 'mw.config.set({"key":"value"});'
- . 'mw.loader.state({"test.exempt":"ready","test.private.top":"loading","test.styles.pure":"ready","test.styles.private":"ready","test.scripts.top":"loading"});'
- . 'mw.loader.implement("test.private.top@{blankVer}",function($,jQuery,require,module){},{"css":[]});'
- . 'mw.loader.load(["test.top"]);'
- . 'mw.loader.load("/w/load.php?debug=false\u0026lang=nl\u0026modules=test.scripts.top\u0026only=scripts\u0026skin=fallback");'
+ . 'mw.loader.state({"test.exempt":"ready","test.private":"loading","test.styles.pure":"ready","test.styles.private":"ready","test.scripts":"loading"});'
+ . 'mw.loader.implement("test.private@{blankVer}",function($,jQuery,require,module){},{"css":[]});'
+ . 'mw.loader.load(["test"]);'
+ . 'mw.loader.load("/w/load.php?debug=false\u0026lang=nl\u0026modules=test.scripts\u0026only=scripts\u0026skin=fallback");'
. '});</script>' . "\n"
. '<link rel="stylesheet" href="/w/load.php?debug=false&lang=nl&modules=test.styles.pure&only=styles&skin=fallback"/>' . "\n"
. '<style>.private{}</style>' . "\n"
],
[
'context' => [],
- 'modules' => [ 'test.private.top' ],
+ 'modules' => [ 'test.private' ],
'only' => ResourceLoaderModule::TYPE_COMBINED,
- 'output' => '<script>(window.RLQ=window.RLQ||[]).push(function(){mw.loader.implement("test.private.top@{blankVer}",function($,jQuery,require,module){},{"css":[]});});</script>',
+ 'output' => '<script>(window.RLQ=window.RLQ||[]).push(function(){mw.loader.implement("test.private@{blankVer}",function($,jQuery,require,module){},{"css":[]});});</script>',
],
[
'context' => [],
'mediawiki.page.startup',
'test.sinonjs',
],
- 'position' => 'top',
'targets' => [ 'desktop', 'mobile' ],
],