SECURITY: rate-limit and prevent blocked users from changing email
[lhc/web/wiklou.git] / includes / SiteStats.php
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
23 use Wikimedia\Rdbms\Database;
24 use Wikimedia\Rdbms\IDatabase;
25 use MediaWiki\MediaWikiServices;
26 use Wikimedia\Rdbms\LoadBalancer;
27
28 /**
29 * Static accessor class for site_stats and related things
30 */
31 class SiteStats {
32 /** @var stdClass */
33 private static $row;
34
35 /**
36 * Trigger a reload next time a field is accessed
37 */
38 public static function unload() {
39 self::$row = null;
40 }
41
42 protected static function load() {
43 if ( self::$row === null ) {
44 self::$row = self::loadAndLazyInit();
45 }
46 }
47
48 /**
49 * @return stdClass
50 */
51 protected static function loadAndLazyInit() {
52 $config = MediaWikiServices::getInstance()->getMainConfig();
53
54 $lb = self::getLB();
55 $dbr = $lb->getConnection( DB_REPLICA );
56 wfDebug( __METHOD__ . ": reading site_stats from replica DB\n" );
57 $row = self::doLoadFromDB( $dbr );
58
59 if ( !self::isRowSane( $row ) && $lb->hasOrMadeRecentMasterChanges() ) {
60 // Might have just been initialized during this request? Underflow?
61 wfDebug( __METHOD__ . ": site_stats damaged or missing on replica DB\n" );
62 $row = self::doLoadFromDB( $lb->getConnection( DB_MASTER ) );
63 }
64
65 if ( !self::isRowSane( $row ) ) {
66 if ( $config->get( 'MiserMode' ) ) {
67 // Start off with all zeroes, assuming that this is a new wiki or any
68 // repopulations where done manually via script.
69 SiteStatsInit::doPlaceholderInit();
70 } else {
71 // Normally the site_stats table is initialized at install time.
72 // Some manual construction scenarios may leave the table empty or
73 // broken, however, for instance when importing from a dump into a
74 // clean schema with mwdumper.
75 wfDebug( __METHOD__ . ": initializing damaged or missing site_stats\n" );
76 SiteStatsInit::doAllAndCommit( $dbr );
77 }
78
79 $row = self::doLoadFromDB( $lb->getConnection( DB_MASTER ) );
80 }
81
82 if ( !self::isRowSane( $row ) ) {
83 wfDebug( __METHOD__ . ": site_stats persistently nonsensical o_O\n" );
84 // Always return a row-like object
85 $row = self::salvageInsaneRow( $row );
86 }
87
88 return $row;
89 }
90
91 /**
92 * @return int
93 */
94 public static function edits() {
95 self::load();
96
97 return (int)self::$row->ss_total_edits;
98 }
99
100 /**
101 * @return int
102 */
103 public static function articles() {
104 self::load();
105
106 return (int)self::$row->ss_good_articles;
107 }
108
109 /**
110 * @return int
111 */
112 public static function pages() {
113 self::load();
114
115 return (int)self::$row->ss_total_pages;
116 }
117
118 /**
119 * @return int
120 */
121 public static function users() {
122 self::load();
123
124 return (int)self::$row->ss_users;
125 }
126
127 /**
128 * @return int
129 */
130 public static function activeUsers() {
131 self::load();
132
133 return (int)self::$row->ss_active_users;
134 }
135
136 /**
137 * @return int
138 */
139 public static function images() {
140 self::load();
141
142 return (int)self::$row->ss_images;
143 }
144
145 /**
146 * Find the number of users in a given user group.
147 * @param string $group Name of group
148 * @return int
149 */
150 public static function numberingroup( $group ) {
151 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
152 $fname = __METHOD__;
153
154 return $cache->getWithSetCallback(
155 $cache->makeKey( 'SiteStats', 'groupcounts', $group ),
156 $cache::TTL_HOUR,
157 function ( $oldValue, &$ttl, array &$setOpts ) use ( $group, $fname ) {
158 $dbr = self::getLB()->getConnection( DB_REPLICA );
159 $setOpts += Database::getCacheSetOptions( $dbr );
160
161 return (int)$dbr->selectField(
162 'user_groups',
163 'COUNT(*)',
164 [
165 'ug_group' => $group,
166 'ug_expiry IS NULL OR ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() )
167 ],
168 $fname
169 );
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 function ( $oldValue, &$ttl, array &$setOpts ) {
186 try{
187 $jobs = array_sum( JobQueueGroup::singleton()->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 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->selectField(
213 'page',
214 'COUNT(*)',
215 [ 'page_namespace' => $ns ],
216 $fname
217 );
218 },
219 [ 'pcTTL' => $cache::TTL_PROC_LONG ]
220 );
221 }
222
223 /**
224 * @return array
225 */
226 public static function selectFields() {
227 return [
228 'ss_total_edits',
229 'ss_good_articles',
230 'ss_total_pages',
231 'ss_users',
232 'ss_active_users',
233 'ss_images',
234 ];
235 }
236
237 /**
238 * @param IDatabase $db
239 * @return stdClass|bool
240 */
241 private static function doLoadFromDB( IDatabase $db ) {
242 return $db->selectRow(
243 'site_stats',
244 self::selectFields(),
245 [ 'ss_row_id' => 1 ],
246 __METHOD__
247 );
248 }
249
250 /**
251 * Is the provided row of site stats sane, or should it be regenerated?
252 *
253 * Checks only fields which are filled by SiteStatsInit::refresh.
254 *
255 * @param bool|object $row
256 * @return bool
257 */
258 private static function isRowSane( $row ) {
259 if ( $row === false
260 || $row->ss_total_pages < $row->ss_good_articles
261 || $row->ss_total_edits < $row->ss_total_pages
262 ) {
263 return false;
264 }
265 // Now check for underflow/overflow
266 foreach ( [
267 'ss_total_edits',
268 'ss_good_articles',
269 'ss_total_pages',
270 'ss_users',
271 'ss_images',
272 ] as $member ) {
273 if ( $row->$member < 0 ) {
274 return false;
275 }
276 }
277
278 return true;
279 }
280
281 /**
282 * @param stdClass|bool $row
283 * @return stdClass
284 */
285 private static function salvageInsaneRow( $row ) {
286 $map = $row ? (array)$row : [];
287 // Fill in any missing values with zero
288 $map += array_fill_keys( self::selectFields(), 0 );
289 // Convert negative values to zero
290 foreach ( $map as $field => $value ) {
291 $map[$field] = max( 0, $value );
292 }
293
294 return (object)$row;
295 }
296
297 /**
298 * @return LoadBalancer
299 */
300 private static function getLB() {
301 return MediaWikiServices::getInstance()->getDBLoadBalancer();
302 }
303 }