MediaWiki master
Pingback.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\Installer;
8
13use MWCryptRand;
14use Psr\Log\LoggerInterface;
18use Wikimedia\Timestamp\ConvertibleTimestamp;
19use Wikimedia\Timestamp\TimestampFormat as TS;
20
34class Pingback {
35
39 private const LEGACY_EVENTLOGGING_SCHEMA = 'MediaWikiPingback';
40
47 private const EVENT_PLATFORM_SCHEMA_ID = '/analytics/legacy/mediawikipingback/1.0.0';
48
59 private const EVENT_PLATFORM_STREAM = 'eventlogging_MediaWikiPingback';
60
62 private const EVENT_PLATFORM_EVENT_INTAKE_SERVICE_URI =
63 'https://intake-analytics.wikimedia.org/v1/events?hasty=true';
64
66 protected $logger;
68 protected $config;
70 protected $dbProvider;
72 protected $cache;
74 protected $http;
76 protected $key;
78 protected $eventIntakeUri;
79
87 public function __construct(
92 LoggerInterface $logger,
93 string $eventIntakeUrl = self::EVENT_PLATFORM_EVENT_INTAKE_SERVICE_URI
94 ) {
95 $this->config = $config;
96 $this->dbProvider = $dbProvider;
97 $this->cache = $cache;
98 $this->http = $http;
99 $this->logger = $logger;
100 $this->key = 'Pingback-' . MW_VERSION;
101 $this->eventIntakeUri = $eventIntakeUrl;
102 }
103
110 public function run(): void {
111 if ( !$this->config->get( MainConfigNames::Pingback ) ) {
112 // disabled
113 return;
114 }
115 if ( $this->wasRecentlySent() ) {
116 // already sent recently
117 return;
118 }
119 if ( !$this->acquireLock() ) {
120 $this->logger->debug( __METHOD__ . ": couldn't acquire lock" );
121 return;
122 }
123
124 $data = $this->getData();
125 if ( !$this->postPingback( $data ) ) {
126 $this->logger->warning( __METHOD__ . ": failed to send; check 'http' log channel" );
127 return;
128 }
129
130 // Record the fact that we have sent a pingback for this MediaWiki version,
131 // so we don't submit data multiple times.
132 $dbw = $this->dbProvider->getPrimaryDatabase();
133 $timestamp = ConvertibleTimestamp::time();
134 $dbw->newInsertQueryBuilder()
135 ->insertInto( 'updatelog' )
136 ->row( [ 'ul_key' => $this->key, 'ul_value' => $timestamp ] )
137 ->onDuplicateKeyUpdate()
138 ->uniqueIndexFields( [ 'ul_key' ] )
139 ->set( [ 'ul_value' => $timestamp ] )
140 ->caller( __METHOD__ )->execute();
141 $this->logger->debug( __METHOD__ . ": pingback sent OK ({$this->key})" );
142 }
143
147 private function wasRecentlySent(): bool {
148 $timestamp = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder()
149 ->select( 'ul_value' )
150 ->from( 'updatelog' )
151 ->where( [ 'ul_key' => $this->key ] )
152 ->caller( __METHOD__ )->fetchField();
153 if ( $timestamp === false ) {
154 return false;
155 }
156 // send heartbeat ping if the last ping was over a month ago
157 if ( ConvertibleTimestamp::time() - (int)$timestamp > 60 * 60 * 24 * 30 ) {
158 return false;
159 }
160 return true;
161 }
162
171 private function acquireLock(): bool {
172 $cacheKey = $this->cache->makeKey( 'pingback', $this->key );
173 if ( !$this->cache->add( $cacheKey, 1, $this->cache::TTL_HOUR ) ) {
174 // throttled
175 return false;
176 }
177
178 $dbw = $this->dbProvider->getPrimaryDatabase();
179 if ( !$dbw->lock( $this->key, __METHOD__, 0 ) ) {
180 // already in progress
181 return false;
182 }
183
184 return true;
185 }
186
196 protected function getData(): array {
197 $wiki = $this->fetchOrInsertId();
198
199 return [
200 'event' => self::getSystemInfo( $this->config ),
201 'schema' => self::LEGACY_EVENTLOGGING_SCHEMA,
202 'wiki' => $wiki,
203
204 // This would be added by
205 // https://gerrit.wikimedia.org/g/mediawiki/extensions/EventLogging/+/d47dbc10455bcb6dbc98a49fa169f75d6131c3da/includes/EventLogging.php#274
206 // onwards.
207 '$schema' => self::EVENT_PLATFORM_SCHEMA_ID,
208 'client_dt' => ConvertibleTimestamp::now( TS::ISO_8601 ),
209
210 // This would be added by
211 // https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/extensions/EventLogging/+/d47dbc10455bcb6dbc98a49fa169f75d6131c3da/includes/EventSubmitter/EventBusEventSubmitter.php#81
212 // onwards.
213 'meta' => [
214 'stream' => self::EVENT_PLATFORM_STREAM,
215 ],
216 ];
217 }
218
231 public static function getSystemInfo( Config $config ): array {
232 $event = [
233 'database' => $config->get( MainConfigNames::DBtype ),
234 'MediaWiki' => MW_VERSION,
235 'PHP' => PHP_VERSION,
236 'OS' => PHP_OS . ' ' . php_uname( 'r' ),
237 'arch' => PHP_INT_SIZE === 8 ? 64 : 32,
238 'machine' => php_uname( 'm' ),
239 ];
240
241 if ( isset( $_SERVER['SERVER_SOFTWARE'] ) ) {
242 $event['serverSoftware'] = $_SERVER['SERVER_SOFTWARE'];
243 }
244
245 $limit = ini_get( 'memory_limit' );
246 if ( $limit && $limit !== "-1" ) {
247 $event['memoryLimit'] = $limit;
248 }
249
250 return $event;
251 }
252
262 private function fetchOrInsertId(): string {
263 // We've already obtained a primary connection for the lock, and plan to do a write.
264 // But, still prefer reading this immutable value from a replica to reduce load.
265 $id = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder()
266 ->select( 'ul_value' )
267 ->from( 'updatelog' )
268 ->where( [ 'ul_key' => 'PingBack' ] )
269 ->caller( __METHOD__ )->fetchField();
270 if ( $id !== false ) {
271 return $id;
272 }
273
274 $dbw = $this->dbProvider->getPrimaryDatabase();
275 $id = $dbw->newSelectQueryBuilder()
276 ->select( 'ul_value' )
277 ->from( 'updatelog' )
278 ->where( [ 'ul_key' => 'PingBack' ] )
279 ->caller( __METHOD__ )->fetchField();
280 if ( $id !== false ) {
281 return $id;
282 }
283
284 $id = MWCryptRand::generateHex( 32 );
285 $dbw->newInsertQueryBuilder()
286 ->insertInto( 'updatelog' )
287 ->row( [ 'ul_key' => 'PingBack', 'ul_value' => $id ] )
288 ->caller( __METHOD__ )->execute();
289 return $id;
290 }
291
305 private function postPingback( array $data ): bool {
306 $request = $this->http->create( $this->eventIntakeUri, [
307 'method' => 'POST',
308 'postData' => FormatJson::encode( $data ),
309 ], __METHOD__ );
310 $request->setHeader( 'Content-Type', 'application/json' );
311
312 $result = $request->execute();
313
314 return $result->isGood();
315 }
316}
317
319class_alias( Pingback::class, 'Pingback' );
const MW_VERSION
The running version of MediaWiki.
Definition Defines.php:23
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:68
A cryptographic random generator class used for generating secret keys.
static generateHex( $chars)
Generate a run of cryptographically random data and return it in hexadecimal string format.
Factory creating MWHttpRequest objects.
Send information about this MediaWiki instance to mediawiki.org.
Definition Pingback.php:34
run()
Maybe send a ping.
Definition Pingback.php:110
__construct(Config $config, IConnectionProvider $dbProvider, BagOStuff $cache, HttpRequestFactory $http, LoggerInterface $logger, string $eventIntakeUrl=self::EVENT_PLATFORM_EVENT_INTAKE_SERVICE_URI)
Definition Pingback.php:87
LoggerInterface $logger
Definition Pingback.php:66
getData()
Get the event to be sent to the server.
Definition Pingback.php:196
IConnectionProvider $dbProvider
Definition Pingback.php:70
HttpRequestFactory $http
Definition Pingback.php:74
string $key
updatelog key (also used as cache/db lock key)
Definition Pingback.php:76
static getSystemInfo(Config $config)
Collect basic data about this MediaWiki installation and return it as an associative array conforming...
Definition Pingback.php:231
JSON formatter wrapper class.
A class containing constants representing the names of configuration variables.
Abstract class for any ephemeral data store.
Definition BagOStuff.php:73
Database error base class.
Definition DBError.php:22
Interface for configuration instances.
Definition Config.php:18
Provide primary and replica IDatabase connections.