GitInfo: Don't try shelling out if it's disabled
[lhc/web/wiklou.git] / includes / Pingback.php
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 been sent in the last month for this MediaWiki version?
72 * @return bool
73 */
74 private function checkIfSent() {
75 $dbr = wfGetDB( DB_REPLICA );
76 $timestamp = $dbr->selectField(
77 'updatelog',
78 'ul_value',
79 [ 'ul_key' => $this->key ],
80 __METHOD__
81 );
82 if ( $timestamp === false ) {
83 return false;
84 }
85 // send heartbeat ping if last ping was over a month ago
86 if ( time() - (int)$timestamp > 60 * 60 * 24 * 30 ) {
87 return false;
88 }
89 return true;
90 }
91
92 /**
93 * Record the fact that we have sent a pingback for this MediaWiki version,
94 * to ensure we don't submit data multiple times.
95 */
96 private function markSent() {
97 $dbw = wfGetDB( DB_MASTER );
98 $timestamp = time();
99 return $dbw->upsert(
100 'updatelog',
101 [ 'ul_key' => $this->key, 'ul_value' => $timestamp ],
102 [ 'ul_key' => $this->key ],
103 [ 'ul_value' => $timestamp ],
104 __METHOD__
105 );
106 }
107
108 /**
109 * Acquire lock for sending a pingback
110 *
111 * This ensures only one thread can attempt to send a pingback at any given
112 * time and that we wait an hour before retrying failed attempts.
113 *
114 * @return bool Whether lock was acquired
115 */
116 private function acquireLock() {
117 $cache = ObjectCache::getLocalClusterInstance();
118 if ( !$cache->add( $this->key, 1, 60 * 60 ) ) {
119 return false; // throttled
120 }
121
122 $dbw = wfGetDB( DB_MASTER );
123 if ( !$dbw->lock( $this->key, __METHOD__, 0 ) ) {
124 return false; // already in progress
125 }
126
127 return true;
128 }
129
130 /**
131 * Collect basic data about this MediaWiki installation and return it
132 * as an associative array conforming to the Pingback schema on MetaWiki
133 * (<https://meta.wikimedia.org/wiki/Schema:MediaWikiPingback>).
134 *
135 * This is public so we can display it in the installer
136 *
137 * Developers: If you're adding a new piece of data to this, please ensure
138 * that you update https://www.mediawiki.org/wiki/Manual:$wgPingback
139 *
140 * @return array
141 */
142 public function getSystemInfo() {
143 $event = [
144 'database' => $this->config->get( 'DBtype' ),
145 'MediaWiki' => $this->config->get( 'Version' ),
146 'PHP' => PHP_VERSION,
147 'OS' => PHP_OS . ' ' . php_uname( 'r' ),
148 'arch' => PHP_INT_SIZE === 8 ? 64 : 32,
149 'machine' => php_uname( 'm' ),
150 ];
151
152 if ( isset( $_SERVER['SERVER_SOFTWARE'] ) ) {
153 $event['serverSoftware'] = $_SERVER['SERVER_SOFTWARE'];
154 }
155
156 $limit = ini_get( 'memory_limit' );
157 if ( $limit && $limit != -1 ) {
158 $event['memoryLimit'] = $limit;
159 }
160
161 return $event;
162 }
163
164 /**
165 * Get the EventLogging packet to be sent to the server
166 *
167 * @return array
168 */
169 private function getData() {
170 return [
171 'schema' => 'MediaWikiPingback',
172 'revision' => self::SCHEMA_REV,
173 'wiki' => $this->getOrCreatePingbackId(),
174 'event' => $this->getSystemInfo(),
175 ];
176 }
177
178 /**
179 * Get a unique, stable identifier for this wiki
180 *
181 * If the identifier does not already exist, create it and save it in the
182 * database. The identifier is randomly-generated.
183 *
184 * @return string 32-character hex string
185 */
186 private function getOrCreatePingbackId() {
187 if ( !$this->id ) {
188 $id = wfGetDB( DB_REPLICA )->selectField(
189 'updatelog', 'ul_value', [ 'ul_key' => 'PingBack' ] );
190
191 if ( $id == false ) {
192 $id = MWCryptRand::generateHex( 32 );
193 $dbw = wfGetDB( DB_MASTER );
194 $dbw->insert(
195 'updatelog',
196 [ 'ul_key' => 'PingBack', 'ul_value' => $id ],
197 __METHOD__,
198 'IGNORE'
199 );
200
201 if ( !$dbw->affectedRows() ) {
202 $id = $dbw->selectField(
203 'updatelog', 'ul_value', [ 'ul_key' => 'PingBack' ] );
204 }
205 }
206
207 $this->id = $id;
208 }
209
210 return $this->id;
211 }
212
213 /**
214 * Serialize pingback data and send it to MediaWiki.org via a POST
215 * to its event beacon endpoint.
216 *
217 * The data encoding conforms to the expectations of EventLogging,
218 * a software suite used by the Wikimedia Foundation for logging and
219 * processing analytic data.
220 *
221 * Compare:
222 * <https://github.com/wikimedia/mediawiki-extensions-EventLogging/
223 * blob/7e5fe4f1ef/includes/EventLogging.php#L32-L74>
224 *
225 * @param array $data Pingback data as an associative array
226 * @return bool true on success, false on failure
227 */
228 private function postPingback( array $data ) {
229 $json = FormatJson::encode( $data );
230 $queryString = rawurlencode( str_replace( ' ', '\u0020', $json ) ) . ';';
231 $url = 'https://www.mediawiki.org/beacon/event?' . $queryString;
232 return Http::post( $url ) !== false;
233 }
234
235 /**
236 * Send information about this MediaWiki instance to MediaWiki.org.
237 *
238 * The data is structured and serialized to match the expectations of
239 * EventLogging, a software suite used by the Wikimedia Foundation for
240 * logging and processing analytic data.
241 *
242 * Compare:
243 * <https://github.com/wikimedia/mediawiki-extensions-EventLogging/
244 * blob/7e5fe4f1ef/includes/EventLogging.php#L32-L74>
245 *
246 * The schema for the data is located at:
247 * <https://meta.wikimedia.org/wiki/Schema:MediaWikiPingback>
248 * @return bool
249 */
250 public function sendPingback() {
251 if ( !$this->acquireLock() ) {
252 $this->logger->debug( __METHOD__ . ": couldn't acquire lock" );
253 return false;
254 }
255
256 $data = $this->getData();
257 if ( !$this->postPingback( $data ) ) {
258 $this->logger->warning( __METHOD__ . ": failed to send pingback; check 'http' log" );
259 return false;
260 }
261
262 $this->markSent();
263 $this->logger->debug( __METHOD__ . ": pingback sent OK ({$this->key})" );
264 return true;
265 }
266
267 /**
268 * Schedule a deferred callable that will check if a pingback should be
269 * sent and (if so) proceed to send it.
270 */
271 public static function schedulePingback() {
272 DeferredUpdates::addCallableUpdate( function () {
273 $instance = new Pingback;
274 if ( $instance->shouldSend() ) {
275 $instance->sendPingback();
276 }
277 } );
278 }
279 }