Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
51.11% |
46 / 90 |
|
50.00% |
4 / 8 |
CRAP | |
0.00% |
0 / 1 |
Pingback | |
51.69% |
46 / 89 |
|
50.00% |
4 / 8 |
76.59 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
run | |
90.48% |
19 / 21 |
|
0.00% |
0 / 1 |
5.02 | |||
wasRecentlySent | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
acquireLock | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
getData | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
getSystemInfo | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
30 | |||
fetchOrInsertId | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
12 | |||
postPingback | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | namespace MediaWiki\Installer; |
22 | |
23 | use BagOStuff; |
24 | use FormatJson; |
25 | use MediaWiki\Config\Config; |
26 | use MediaWiki\Http\HttpRequestFactory; |
27 | use MediaWiki\MainConfigNames; |
28 | use MWCryptRand; |
29 | use Psr\Log\LoggerInterface; |
30 | use Wikimedia\Rdbms\DBError; |
31 | use Wikimedia\Rdbms\IConnectionProvider; |
32 | use Wikimedia\Timestamp\ConvertibleTimestamp; |
33 | |
34 | /** |
35 | * Send information about this MediaWiki instance to mediawiki.org. |
36 | * |
37 | * This service uses two kinds of rows in the `update_log` database table: |
38 | * |
39 | * - ul_key `PingBack`, this holds a random identifier for this wiki, |
40 | * created only once, when the first ping after wiki creation is sent. |
41 | * - ul_key `Pingback-<MW_VERSION>`, this holds a timestamp and is created |
42 | * once after each MediaWiki upgrade, and then updated up to once a month. |
43 | * |
44 | * @internal For use by Setup.php only |
45 | * @since 1.28 |
46 | */ |
47 | class Pingback { |
48 | /** |
49 | * @var int Revision ID of the JSON schema that describes the pingback payload. |
50 | * The schema lives on Meta-Wiki, at <https://meta.wikimedia.org/wiki/Schema:MediaWikiPingback>. |
51 | */ |
52 | private const SCHEMA_REV = 20104427; |
53 | |
54 | /** @var LoggerInterface */ |
55 | protected $logger; |
56 | /** @var Config */ |
57 | protected $config; |
58 | /** @var IConnectionProvider */ |
59 | protected $dbProvider; |
60 | /** @var BagOStuff */ |
61 | protected $cache; |
62 | /** @var HttpRequestFactory */ |
63 | protected $http; |
64 | /** @var string updatelog key (also used as cache/db lock key) */ |
65 | protected $key; |
66 | |
67 | /** |
68 | * @param Config $config |
69 | * @param IConnectionProvider $dbProvider |
70 | * @param BagOStuff $cache |
71 | * @param HttpRequestFactory $http |
72 | * @param LoggerInterface $logger |
73 | */ |
74 | public function __construct( |
75 | Config $config, |
76 | IConnectionProvider $dbProvider, |
77 | BagOStuff $cache, |
78 | HttpRequestFactory $http, |
79 | LoggerInterface $logger |
80 | ) { |
81 | $this->config = $config; |
82 | $this->dbProvider = $dbProvider; |
83 | $this->cache = $cache; |
84 | $this->http = $http; |
85 | $this->logger = $logger; |
86 | $this->key = 'Pingback-' . MW_VERSION; |
87 | } |
88 | |
89 | /** |
90 | * Maybe send a ping. |
91 | * |
92 | * @throws DBError If identifier insert fails |
93 | * @throws DBError If timestamp upsert fails |
94 | */ |
95 | public function run(): void { |
96 | if ( !$this->config->get( MainConfigNames::Pingback ) ) { |
97 | // disabled |
98 | return; |
99 | } |
100 | if ( $this->wasRecentlySent() ) { |
101 | // already sent recently |
102 | return; |
103 | } |
104 | if ( !$this->acquireLock() ) { |
105 | $this->logger->debug( __METHOD__ . ": couldn't acquire lock" ); |
106 | return; |
107 | } |
108 | |
109 | $data = $this->getData(); |
110 | if ( !$this->postPingback( $data ) ) { |
111 | $this->logger->warning( __METHOD__ . ": failed to send; check 'http' log channel" ); |
112 | return; |
113 | } |
114 | |
115 | // Record the fact that we have sent a pingback for this MediaWiki version, |
116 | // so we don't submit data multiple times. |
117 | $dbw = $this->dbProvider->getPrimaryDatabase(); |
118 | $timestamp = ConvertibleTimestamp::time(); |
119 | $dbw->newInsertQueryBuilder() |
120 | ->insertInto( 'updatelog' ) |
121 | ->row( [ 'ul_key' => $this->key, 'ul_value' => $timestamp ] ) |
122 | ->onDuplicateKeyUpdate() |
123 | ->uniqueIndexFields( [ 'ul_key' ] ) |
124 | ->set( [ 'ul_value' => $timestamp ] ) |
125 | ->caller( __METHOD__ )->execute(); |
126 | $this->logger->debug( __METHOD__ . ": pingback sent OK ({$this->key})" ); |
127 | } |
128 | |
129 | /** |
130 | * Was a pingback sent in the last month for this MediaWiki version? |
131 | * |
132 | * @return bool |
133 | */ |
134 | private function wasRecentlySent(): bool { |
135 | $timestamp = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder() |
136 | ->select( 'ul_value' ) |
137 | ->from( 'updatelog' ) |
138 | ->where( [ 'ul_key' => $this->key ] ) |
139 | ->caller( __METHOD__ )->fetchField(); |
140 | if ( $timestamp === false ) { |
141 | return false; |
142 | } |
143 | // send heartbeat ping if the last ping was over a month ago |
144 | if ( ConvertibleTimestamp::time() - (int)$timestamp > 60 * 60 * 24 * 30 ) { |
145 | return false; |
146 | } |
147 | return true; |
148 | } |
149 | |
150 | /** |
151 | * Acquire lock for sending a pingback |
152 | * |
153 | * This ensures only one thread can attempt to send a pingback at any given |
154 | * time and that we wait an hour before retrying failed attempts. |
155 | * |
156 | * @return bool Whether lock was acquired |
157 | */ |
158 | private function acquireLock(): bool { |
159 | $cacheKey = $this->cache->makeKey( 'pingback', $this->key ); |
160 | if ( !$this->cache->add( $cacheKey, 1, $this->cache::TTL_HOUR ) ) { |
161 | // throttled |
162 | return false; |
163 | } |
164 | |
165 | $dbw = $this->dbProvider->getPrimaryDatabase(); |
166 | if ( !$dbw->lock( $this->key, __METHOD__, 0 ) ) { |
167 | // already in progress |
168 | return false; |
169 | } |
170 | |
171 | return true; |
172 | } |
173 | |
174 | /** |
175 | * Get the EventLogging packet to be sent to the server |
176 | * |
177 | * @throws DBError If identifier insert fails |
178 | * @return array |
179 | */ |
180 | protected function getData(): array { |
181 | return [ |
182 | 'schema' => 'MediaWikiPingback', |
183 | 'revision' => self::SCHEMA_REV, |
184 | 'wiki' => $this->fetchOrInsertId(), |
185 | 'event' => self::getSystemInfo( $this->config ), |
186 | ]; |
187 | } |
188 | |
189 | /** |
190 | * Collect basic data about this MediaWiki installation and return it |
191 | * as an associative array conforming to the Pingback schema on Meta-Wiki |
192 | * (<https://meta.wikimedia.org/wiki/Schema:MediaWikiPingback>). |
193 | * |
194 | * Developers: If you're adding a new piece of data to this, please document |
195 | * this data at <https://www.mediawiki.org/wiki/Manual:$wgPingback>. |
196 | * |
197 | * @internal For use by Installer only to display which data we send. |
198 | * @param Config $config With `DBtype` set. |
199 | * @return array |
200 | */ |
201 | public static function getSystemInfo( Config $config ): array { |
202 | $event = [ |
203 | 'database' => $config->get( MainConfigNames::DBtype ), |
204 | 'MediaWiki' => MW_VERSION, |
205 | 'PHP' => PHP_VERSION, |
206 | 'OS' => PHP_OS . ' ' . php_uname( 'r' ), |
207 | 'arch' => PHP_INT_SIZE === 8 ? 64 : 32, |
208 | 'machine' => php_uname( 'm' ), |
209 | ]; |
210 | |
211 | if ( isset( $_SERVER['SERVER_SOFTWARE'] ) ) { |
212 | $event['serverSoftware'] = $_SERVER['SERVER_SOFTWARE']; |
213 | } |
214 | |
215 | $limit = ini_get( 'memory_limit' ); |
216 | if ( $limit && $limit !== "-1" ) { |
217 | $event['memoryLimit'] = $limit; |
218 | } |
219 | |
220 | return $event; |
221 | } |
222 | |
223 | /** |
224 | * Get a unique, stable identifier for this wiki |
225 | * |
226 | * If the identifier does not already exist, create it and save it in the |
227 | * database. The identifier is randomly-generated. |
228 | * |
229 | * @throws DBError If identifier insert fails |
230 | * @return string 32-character hex string |
231 | */ |
232 | private function fetchOrInsertId(): string { |
233 | // We've already obtained a primary connection for the lock, and plan to do a write. |
234 | // But, still prefer reading this immutable value from a replica to reduce load. |
235 | $id = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder() |
236 | ->select( 'ul_value' ) |
237 | ->from( 'updatelog' ) |
238 | ->where( [ 'ul_key' => 'PingBack' ] ) |
239 | ->caller( __METHOD__ )->fetchField(); |
240 | if ( $id !== false ) { |
241 | return $id; |
242 | } |
243 | |
244 | $dbw = $this->dbProvider->getPrimaryDatabase(); |
245 | $id = $dbw->newSelectQueryBuilder() |
246 | ->select( 'ul_value' ) |
247 | ->from( 'updatelog' ) |
248 | ->where( [ 'ul_key' => 'PingBack' ] ) |
249 | ->caller( __METHOD__ )->fetchField(); |
250 | if ( $id !== false ) { |
251 | return $id; |
252 | } |
253 | |
254 | $id = MWCryptRand::generateHex( 32 ); |
255 | $dbw->newInsertQueryBuilder() |
256 | ->insertInto( 'updatelog' ) |
257 | ->row( [ 'ul_key' => 'PingBack', 'ul_value' => $id ] ) |
258 | ->caller( __METHOD__ )->execute(); |
259 | return $id; |
260 | } |
261 | |
262 | /** |
263 | * Serialize pingback data and send it to mediawiki.org via a POST request |
264 | * to its EventLogging beacon endpoint. |
265 | * |
266 | * The data encoding conforms to the expectations of EventLogging as used by |
267 | * Wikimedia Foundation for logging and processing analytic data. |
268 | * |
269 | * Compare: |
270 | * <https://gerrit.wikimedia.org/g/mediawiki/extensions/EventLogging/+/7e5fe4f1ef/includes/EventLogging.php#L32> |
271 | * |
272 | * The schema for the data is located at: |
273 | * <https://meta.wikimedia.org/wiki/Schema:MediaWikiPingback> |
274 | * |
275 | * @param array $data Pingback data as an associative array |
276 | * @return bool |
277 | */ |
278 | private function postPingback( array $data ): bool { |
279 | $json = FormatJson::encode( $data ); |
280 | $queryString = rawurlencode( str_replace( ' ', '\u0020', $json ) ) . ';'; |
281 | $url = 'https://www.mediawiki.org/beacon/event?' . $queryString; |
282 | return $this->http->post( $url, [], __METHOD__ ) !== null; |
283 | } |
284 | } |
285 | |
286 | /** @deprecated class alias since 1.41 */ |
287 | class_alias( Pingback::class, 'Pingback' ); |