Pingback   A
last analyzed

Complexity

Total Complexity 26

Size/Duplication

Total Lines 231
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Importance

Changes 0
Metric Value
dl 0
loc 231
rs 10
c 0
b 0
f 0
wmc 26
lcom 1
cbo 10

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 3
A shouldSend() 0 3 2
A checkIfSent() 0 6 1
A markSent() 0 5 1
A acquireLock() 0 13 3
B getSystemInfo() 0 21 5
A getData() 0 8 1
B getOrCreatePingbackId() 0 26 4
A postPingback() 0 6 1
A sendPingback() 0 16 3
A schedulePingback() 0 8 2
1
<?php
2
/**
3
 * Send information about this MediaWiki instance to MediaWiki.org.
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 Psr\Log\LoggerInterface;
24
use MediaWiki\Logger\LoggerFactory;
25
26
/**
27
 * Send information about this MediaWiki instance to MediaWiki.org.
28
 *
29
 * @since 1.28
30
 */
31
class Pingback {
32
33
	/**
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>.
37
	 */
38
	const SCHEMA_REV = 15781718;
39
40
	/** @var LoggerInterface */
41
	protected $logger;
42
43
	/** @var Config */
44
	protected $config;
45
46
	/** @var string updatelog key (also used as cache/db lock key) */
47
	protected $key;
48
49
	/** @var string Randomly-generated identifier for this wiki */
50
	protected $id;
51
52
	/**
53
	 * @param Config $config
54
	 * @param LoggerInterface $logger
55
	 */
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' );
60
	}
61
62
	/**
63
	 * Should a pingback be sent?
64
	 * @return bool
65
	 */
66
	private function shouldSend() {
67
		return $this->config->get( 'Pingback' ) && !$this->checkIfSent();
68
	}
69
70
	/**
71
	 * Has a pingback already been sent for this MediaWiki version?
72
	 * @return bool
73
	 */
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;
79
	}
80
81
	/**
82
	 * Record the fact that we have sent a pingback for this MediaWiki version,
83
	 * to ensure we don't submit data multiple times.
84
	 */
85
	private function markSent() {
86
		$dbw = wfGetDB( DB_MASTER );
87
		return $dbw->insert(
88
			'updatelog', [ 'ul_key' => $this->key ], __METHOD__, 'IGNORE' );
89
	}
90
91
	/**
92
	 * Acquire lock for sending a pingback
93
	 *
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.
96
	 *
97
	 * @return bool Whether lock was acquired
98
	 */
99
	private function acquireLock() {
100
		$cache = ObjectCache::getLocalClusterInstance();
101
		if ( !$cache->add( $this->key, 1, 60 * 60 ) ) {
102
			return false;  // throttled
103
		}
104
105
		$dbw = wfGetDB( DB_MASTER );
106
		if ( !$dbw->lock( $this->key, __METHOD__, 0 ) ) {
107
			return false;  // already in progress
108
		}
109
110
		return true;
111
	}
112
113
	/**
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>).
117
	 *
118
	 * This is public so we can display it in the installer
119
	 *
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
122
	 *
123
	 * @return array
124
	 */
125
	public function getSystemInfo() {
0 ignored issues
show
Coding Style introduced by
getSystemInfo uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
126
		$event = [
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' ),
133
		];
134
135
		if ( isset( $_SERVER['SERVER_SOFTWARE'] ) ) {
136
			$event['serverSoftware'] = $_SERVER['SERVER_SOFTWARE'];
137
		}
138
139
		$limit = ini_get( 'memory_limit' );
140
		if ( $limit && $limit != -1 ) {
141
			$event['memoryLimit'] = $limit;
142
		}
143
144
		return $event;
145
	}
146
147
	/**
148
	 * Get the EventLogging packet to be sent to the server
149
	 *
150
	 * @return array
151
	 */
152
	private function getData() {
153
		return [
154
			'schema'           => 'MediaWikiPingback',
155
			'revision'         => self::SCHEMA_REV,
156
			'wiki'             => $this->getOrCreatePingbackId(),
157
			'event'            => $this->getSystemInfo(),
158
		];
159
	}
160
161
	/**
162
	 * Get a unique, stable identifier for this wiki
163
	 *
164
	 * If the identifier does not already exist, create it and save it in the
165
	 * database. The identifier is randomly-generated.
166
	 *
167
	 * @return string 32-character hex string
168
	 */
169
	private function getOrCreatePingbackId() {
170
		if ( !$this->id ) {
171
			$id = wfGetDB( DB_REPLICA )->selectField(
172
				'updatelog', 'ul_value', [ 'ul_key' => 'PingBack' ] );
173
174
			if ( $id == false ) {
175
				$id = MWCryptRand::generateHex( 32 );
176
				$dbw = wfGetDB( DB_MASTER );
177
				$dbw->insert(
178
					'updatelog',
179
					[ 'ul_key' => 'PingBack', 'ul_value' => $id ],
180
					__METHOD__,
181
					'IGNORE'
182
				);
183
184
				if ( !$dbw->affectedRows() ) {
185
					$id = $dbw->selectField(
186
						'updatelog', 'ul_value', [ 'ul_key' => 'PingBack' ] );
187
				}
188
			}
189
190
			$this->id = $id;
191
		}
192
193
		return $this->id;
194
	}
195
196
	/**
197
	 * Serialize pingback data and send it to MediaWiki.org via a POST
198
	 * to its event beacon endpoint.
199
	 *
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.
203
	 *
204
	 * Compare:
205
	 * <https://github.com/wikimedia/mediawiki-extensions-EventLogging/
206
	 *   blob/7e5fe4f1ef/includes/EventLogging.php#L32-L74>
207
	 *
208
	 * @param array $data Pingback data as an associative array
209
	 * @return bool true on success, false on failure
210
	 */
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;
216
	}
217
218
	/**
219
	 * Send information about this MediaWiki instance to MediaWiki.org.
220
	 *
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.
224
	 *
225
	 * Compare:
226
	 * <https://github.com/wikimedia/mediawiki-extensions-EventLogging/
227
	 *   blob/7e5fe4f1ef/includes/EventLogging.php#L32-L74>
228
	 *
229
	 * The schema for the data is located at:
230
	 * <https://meta.wikimedia.org/wiki/Schema:MediaWikiPingback>
231
	 */
232
	public function sendPingback() {
233
		if ( !$this->acquireLock() ) {
234
			$this->logger->debug( __METHOD__ . ": couldn't acquire lock" );
235
			return false;
236
		}
237
238
		$data = $this->getData();
239
		if ( !$this->postPingback( $data ) ) {
240
			$this->logger->warning( __METHOD__ . ": failed to send pingback; check 'http' log" );
241
			return false;
242
		}
243
244
		$this->markSent();
245
		$this->logger->debug( __METHOD__ . ": pingback sent OK ({$this->key})" );
246
		return true;
247
	}
248
249
	/**
250
	 * Schedule a deferred callable that will check if a pingback should be
251
	 * sent and (if so) proceed to send it.
252
	 */
253
	public static function schedulePingback() {
254
		DeferredUpdates::addCallableUpdate( function () {
255
			$instance = new Pingback;
256
			if ( $instance->shouldSend() ) {
257
				$instance->sendPingback();
258
			}
259
		} );
260
	}
261
}
262