3 * Send information about this MediaWiki instance to MediaWiki.org.
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.
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.
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
23 use Psr\Log\LoggerInterface
;
24 use MediaWiki\Logger\LoggerFactory
;
27 * Send information about this MediaWiki instance to MediaWiki.org.
34 * @var int Revision ID of the JSON schema that describes the pingback
35 * payload. The schema lives on MetaWiki, at
36 * <https://meta.wikimedia.org/wiki/Schema:MediaWikiPingback>.
38 const SCHEMA_REV
= 15781718;
40 /** @var LoggerInterface */
46 /** @var string updatelog key (also used as cache/db lock key) */
49 /** @var string Randomly-generated identifier for this wiki */
53 * @param Config $config
54 * @param LoggerInterface $logger
56 public function __construct( Config
$config = null, LoggerInterface
$logger = null ) {
57 $this->config
= $config ?
: RequestContext
::getMain()->getConfig();
58 $this->logger
= $logger ?
: LoggerFactory
::getInstance( __CLASS__
);
59 $this->key
= 'Pingback-' . $this->config
->get( 'Version' );
63 * Should a pingback be sent?
66 private function shouldSend() {
67 return $this->config
->get( 'Pingback' ) && !$this->checkIfSent();
71 * Has a pingback already been sent for this MediaWiki version?
74 private function checkIfSent() {
75 $dbr = wfGetDB( DB_REPLICA
);
76 $sent = $dbr->selectField(
77 'updatelog', '1', [ 'ul_key' => $this->key
], __METHOD__
);
78 return $sent !== false;
82 * Record the fact that we have sent a pingback for this MediaWiki version,
83 * to ensure we don't submit data multiple times.
85 private function markSent() {
86 $dbw = wfGetDB( DB_MASTER
);
88 'updatelog', [ 'ul_key' => $this->key
], __METHOD__
, 'IGNORE' );
92 * Acquire lock for sending a pingback
94 * This ensures only one thread can attempt to send a pingback at any given
95 * time and that we wait an hour before retrying failed attempts.
97 * @return bool Whether lock was acquired
99 private function acquireLock() {
100 $cache = ObjectCache
::getLocalClusterInstance();
101 if ( !$cache->add( $this->key
, 1, 60 * 60 ) ) {
102 return false; // throttled
105 $dbw = wfGetDB( DB_MASTER
);
106 if ( !$dbw->lock( $this->key
, __METHOD__
, 0 ) ) {
107 return false; // already in progress
114 * Collect basic data about this MediaWiki installation and return it
115 * as an associative array conforming to the Pingback schema on MetaWiki
116 * (<https://meta.wikimedia.org/wiki/Schema:MediaWikiPingback>).
118 * This is public so we can display it in the installer
120 * Developers: If you're adding a new piece of data to this, please ensure
121 * that you update https://www.mediawiki.org/wiki/Manual:$wgPingback
125 public function getSystemInfo() {
127 'database' => $this->config
->get( 'DBtype' ),
128 'MediaWiki' => $this->config
->get( 'Version' ),
129 'PHP' => PHP_VERSION
,
130 'OS' => PHP_OS
. ' ' . php_uname( 'r' ),
131 'arch' => PHP_INT_SIZE
=== 8 ?
64 : 32,
132 'machine' => php_uname( 'm' ),
135 if ( isset( $_SERVER['SERVER_SOFTWARE'] ) ) {
136 $event['serverSoftware'] = $_SERVER['SERVER_SOFTWARE'];
139 $limit = ini_get( 'memory_limit' );
140 if ( $limit && $limit != -1 ) {
141 $event['memoryLimit'] = $limit;
148 * Get the EventLogging packet to be sent to the server
152 private function getData() {
154 'schema' => 'MediaWikiPingback',
155 'revision' => self
::SCHEMA_REV
,
156 'wiki' => $this->getOrCreatePingbackId(),
157 'event' => $this->getSystemInfo(),
162 * Get a unique, stable identifier for this wiki
164 * If the identifier does not already exist, create it and save it in the
165 * database. The identifier is randomly-generated.
167 * @return string 32-character hex string
169 private function getOrCreatePingbackId() {
171 $id = wfGetDB( DB_REPLICA
)->selectField(
172 'updatelog', 'ul_value', [ 'ul_key' => 'PingBack' ] );
174 if ( $id == false ) {
175 $id = MWCryptRand
::generateHex( 32 );
176 $dbw = wfGetDB( DB_MASTER
);
179 [ 'ul_key' => 'PingBack', 'ul_value' => $id ],
184 if ( !$dbw->affectedRows() ) {
185 $id = $dbw->selectField(
186 'updatelog', 'ul_value', [ 'ul_key' => 'PingBack' ] );
197 * Serialize pingback data and send it to MediaWiki.org via a POST
198 * to its event beacon endpoint.
200 * The data encoding conforms to the expectations of EventLogging,
201 * a software suite used by the Wikimedia Foundation for logging and
202 * processing analytic data.
205 * <https://github.com/wikimedia/mediawiki-extensions-EventLogging/
206 * blob/7e5fe4f1ef/includes/EventLogging.php#L32-L74>
208 * @param array $data Pingback data as an associative array
209 * @return bool true on success, false on failure
211 private function postPingback( array $data ) {
212 $json = FormatJson
::encode( $data );
213 $queryString = rawurlencode( str_replace( ' ', '\u0020', $json ) ) . ';';
214 $url = 'https://www.mediawiki.org/beacon/event?' . $queryString;
215 return Http
::post( $url ) !== false;
219 * Send information about this MediaWiki instance to MediaWiki.org.
221 * The data is structured and serialized to match the expectations of
222 * EventLogging, a software suite used by the Wikimedia Foundation for
223 * logging and processing analytic data.
226 * <https://github.com/wikimedia/mediawiki-extensions-EventLogging/
227 * blob/7e5fe4f1ef/includes/EventLogging.php#L32-L74>
229 * The schema for the data is located at:
230 * <https://meta.wikimedia.org/wiki/Schema:MediaWikiPingback>
232 public function sendPingback() {
233 if ( !$this->acquireLock() ) {
234 $this->logger
->debug( __METHOD__
. ": couldn't acquire lock" );
238 $data = $this->getData();
239 if ( !$this->postPingback( $data ) ) {
240 $this->logger
->warning( __METHOD__
. ": failed to send pingback; check 'http' log" );
245 $this->logger
->debug( __METHOD__
. ": pingback sent OK ({$this->key})" );
250 * Schedule a deferred callable that will check if a pingback should be
251 * sent and (if so) proceed to send it.
253 public static function schedulePingback() {
254 DeferredUpdates
::addCallableUpdate( function () {
255 $instance = new Pingback
;
256 if ( $instance->shouldSend() ) {
257 $instance->sendPingback();