Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
62.18% covered (warning)
62.18%
74 / 119
62.50% covered (warning)
62.50%
10 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
SiteStats
62.71% covered (warning)
62.71%
74 / 118
62.50% covered (warning)
62.50%
10 / 16
80.82
0.00% covered (danger)
0.00%
0 / 1
 unload
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 load
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 loadAndLazyInit
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
5.01
 edits
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 articles
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 pages
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 users
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 activeUsers
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 images
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 numberingroup
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
2
 jobs
83.33% covered (warning)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
2.02
 pagesInNs
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
2
 selectFields
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 doLoadFromDB
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
5
 isRowSensible
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
6.01
 getLB
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Accessors and mutators for the site-wide statistics.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 */
22
23namespace MediaWiki\SiteStats;
24
25use JobQueueError;
26use MediaWiki\MainConfigNames;
27use MediaWiki\MediaWikiServices;
28use stdClass;
29use Wikimedia\Rdbms\Database;
30use Wikimedia\Rdbms\ILoadBalancer;
31use Wikimedia\Rdbms\IReadableDatabase;
32
33/**
34 * Static accessor class for site_stats and related things
35 */
36class SiteStats {
37    /** @var stdClass|null */
38    private static $row;
39
40    /**
41     * Trigger a reload next time a field is accessed
42     */
43    public static function unload() {
44        self::$row = null;
45    }
46
47    protected static function load() {
48        if ( self::$row === null ) {
49            self::$row = self::loadAndLazyInit();
50        }
51    }
52
53    /**
54     * @return stdClass
55     */
56    protected static function loadAndLazyInit() {
57        $config = MediaWikiServices::getInstance()->getMainConfig();
58
59        $lb = self::getLB();
60        $dbr = $lb->getConnection( DB_REPLICA );
61        wfDebug( __METHOD__ . ": reading site_stats from replica DB" );
62        $row = self::doLoadFromDB( $dbr );
63
64        if ( !self::isRowSensible( $row ) && $lb->hasOrMadeRecentPrimaryChanges() ) {
65            // Might have just been initialized during this request? Underflow?
66            wfDebug( __METHOD__ . ": site_stats damaged or missing on replica DB" );
67            $row = self::doLoadFromDB( $lb->getConnection( DB_PRIMARY ) );
68        }
69
70        if ( !self::isRowSensible( $row ) ) {
71            if ( $config->get( MainConfigNames::MiserMode ) ) {
72                // Start off with all zeroes, assuming that this is a new wiki or any
73                // repopulations where done manually via script.
74                SiteStatsInit::doPlaceholderInit();
75            } else {
76                // Normally the site_stats table is initialized at install time.
77                // Some manual construction scenarios may leave the table empty or
78                // broken, however, for instance when importing from a dump into a
79                // clean schema with mwdumper.
80                wfDebug( __METHOD__ . ": initializing damaged or missing site_stats" );
81                SiteStatsInit::doAllAndCommit( $dbr );
82            }
83
84            $row = self::doLoadFromDB( $lb->getConnection( DB_PRIMARY ) );
85        }
86
87        return $row;
88    }
89
90    /**
91     * @return int
92     */
93    public static function edits() {
94        self::load();
95
96        return (int)self::$row->ss_total_edits;
97    }
98
99    /**
100     * @return int
101     */
102    public static function articles() {
103        self::load();
104
105        return (int)self::$row->ss_good_articles;
106    }
107
108    /**
109     * @return int
110     */
111    public static function pages() {
112        self::load();
113
114        return (int)self::$row->ss_total_pages;
115    }
116
117    /**
118     * @return int
119     */
120    public static function users() {
121        self::load();
122
123        return (int)self::$row->ss_users;
124    }
125
126    /**
127     * @return int
128     */
129    public static function activeUsers() {
130        self::load();
131
132        return (int)self::$row->ss_active_users;
133    }
134
135    /**
136     * @return int
137     */
138    public static function images() {
139        self::load();
140
141        return (int)self::$row->ss_images;
142    }
143
144    /**
145     * Find the number of users in a given user group.
146     * @param string $group Name of group
147     * @return int
148     */
149    public static function numberingroup( $group ) {
150        $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
151        $fname = __METHOD__;
152
153        return $cache->getWithSetCallback(
154            $cache->makeKey( 'SiteStats', 'groupcounts', $group ),
155            $cache::TTL_HOUR,
156            static function ( $oldValue, &$ttl, array &$setOpts ) use ( $group, $fname ) {
157                $dbr = self::getLB()->getConnection( DB_REPLICA );
158                $setOpts += Database::getCacheSetOptions( $dbr );
159                return (int)$dbr->newSelectQueryBuilder()
160                    ->select( 'COUNT(*)' )
161                    ->from( 'user_groups' )
162                    ->where(
163                        [
164                            'ug_group' => $group,
165                            $dbr->expr( 'ug_expiry', '=', null )->or( 'ug_expiry', '>=', $dbr->timestamp() )
166                        ]
167                    )
168                    ->caller( $fname )
169                    ->fetchField();
170            },
171            [ 'pcTTL' => $cache::TTL_PROC_LONG ]
172        );
173    }
174
175    /**
176     * Total number of jobs in the job queue.
177     * @return int
178     */
179    public static function jobs() {
180        $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
181
182        return $cache->getWithSetCallback(
183            $cache->makeKey( 'SiteStats', 'jobscount' ),
184            $cache::TTL_MINUTE,
185            static function ( $oldValue, &$ttl, array &$setOpts ) {
186                try {
187                    $jobs = array_sum( MediaWikiServices::getInstance()->getJobQueueGroup()->getQueueSizes() );
188                } catch ( JobQueueError $e ) {
189                    $jobs = 0;
190                }
191                return $jobs;
192            },
193            [ 'pcTTL' => $cache::TTL_PROC_LONG ]
194        );
195    }
196
197    /**
198     * @param int $ns
199     * @return int
200     */
201    public static function pagesInNs( $ns ) {
202        $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
203        $fname = __METHOD__;
204
205        return $cache->getWithSetCallback(
206            $cache->makeKey( 'SiteStats', 'page-in-namespace', $ns ),
207            $cache::TTL_HOUR,
208            static function ( $oldValue, &$ttl, array &$setOpts ) use ( $ns, $fname ) {
209                $dbr = self::getLB()->getConnection( DB_REPLICA );
210                $setOpts += Database::getCacheSetOptions( $dbr );
211
212                return (int)$dbr->newSelectQueryBuilder()
213                    ->select( 'COUNT(*)' )
214                    ->from( 'page' )
215                    ->where( [ 'page_namespace' => $ns ] )
216                    ->caller( $fname )->fetchField();
217            },
218            [ 'pcTTL' => $cache::TTL_PROC_LONG ]
219        );
220    }
221
222    /**
223     * @return array
224     */
225    public static function selectFields() {
226        return [
227            'ss_total_edits',
228            'ss_good_articles',
229            'ss_total_pages',
230            'ss_users',
231            'ss_active_users',
232            'ss_images',
233        ];
234    }
235
236    /**
237     * @param IReadableDatabase $db
238     * @return stdClass|false
239     */
240    private static function doLoadFromDB( IReadableDatabase $db ) {
241        $fields = self::selectFields();
242        $rows = $db->newSelectQueryBuilder()
243            ->select( $fields )
244            ->from( 'site_stats' )
245            ->caller( __METHOD__ )
246            ->fetchResultSet();
247        if ( !$rows->numRows() ) {
248            return false;
249        }
250        $finalRow = new stdClass();
251        foreach ( $rows as $row ) {
252            foreach ( $fields as $field ) {
253                $finalRow->$field ??= 0;
254                if ( $row->$field ) {
255                    $finalRow->$field += $row->$field;
256                }
257            }
258
259        }
260        return $finalRow;
261    }
262
263    /**
264     * Is the provided row of site stats sensible, or should it be regenerated?
265     *
266     * Checks only fields which are filled by SiteStatsInit::refresh.
267     *
268     * @param stdClass|false $row
269     * @return bool
270     */
271    private static function isRowSensible( $row ) {
272        if ( $row === false
273            || $row->ss_total_pages < $row->ss_good_articles
274            || $row->ss_total_edits < $row->ss_total_pages
275        ) {
276            return false;
277        }
278        // Now check for underflow/overflow
279        foreach ( [
280            'ss_total_edits',
281            'ss_good_articles',
282            'ss_total_pages',
283            'ss_users',
284            'ss_images',
285        ] as $member ) {
286            if ( $row->$member < 0 ) {
287                return false;
288            }
289        }
290
291        return true;
292    }
293
294    /**
295     * @return ILoadBalancer
296     */
297    private static function getLB() {
298        return MediaWikiServices::getInstance()->getDBLoadBalancer();
299    }
300}
301
302/** @deprecated class alias since 1.41 */
303class_alias( SiteStats::class, 'SiteStats' );