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