PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 4.0.2
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v4.0.2
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / Http.php
matomo / app / core Last commit date
API 5 years ago Access 5 years ago Application 5 years ago Archive 5 years ago ArchiveProcessor 5 years ago Archiver 5 years ago AssetManager 5 years ago Auth 5 years ago Category 5 years ago CliMulti 5 years ago Columns 5 years ago Composer 5 years ago Concurrency 5 years ago Config 5 years ago Container 5 years ago CronArchive 5 years ago DataAccess 5 years ago DataFiles 5 years ago DataTable 5 years ago Db 5 years ago DeviceDetector 5 years ago Email 5 years ago Exception 5 years ago Http 5 years ago Intl 5 years ago Mail 5 years ago Measurable 5 years ago Menu 5 years ago Metrics 5 years ago Notification 5 years ago Period 5 years ago Plugin 5 years ago ProfessionalServices 5 years ago Report 5 years ago ReportRenderer 5 years ago Scheduler 5 years ago Segment 5 years ago Session 5 years ago Settings 5 years ago Tracker 5 years ago Translation 5 years ago UpdateCheck 5 years ago Updater 5 years ago Updates 5 years ago Validators 5 years ago View 5 years ago ViewDataTable 5 years ago Visualization 5 years ago Widget 5 years ago .htaccess 6 years ago Access.php 5 years ago Archive.php 5 years ago ArchiveProcessor.php 5 years ago AssetManager.php 5 years ago Auth.php 5 years ago AuthResult.php 5 years ago BaseFactory.php 5 years ago Cache.php 5 years ago CacheId.php 5 years ago CliMulti.php 5 years ago Common.php 5 years ago Config.php 5 years ago Console.php 5 years ago Context.php 5 years ago Cookie.php 5 years ago CronArchive.php 5 years ago DataArray.php 5 years ago DataTable.php 5 years ago Date.php 5 years ago Db.php 5 years ago DbHelper.php 5 years ago Development.php 5 years ago ErrorHandler.php 5 years ago EventDispatcher.php 5 years ago ExceptionHandler.php 5 years ago FileIntegrity.php 5 years ago Filechecks.php 5 years ago Filesystem.php 5 years ago FrontController.php 5 years ago Http.php 5 years ago IP.php 5 years ago Log.php 5 years ago LogDeleter.php 5 years ago Mail.php 5 years ago Metrics.php 5 years ago NoAccessException.php 5 years ago Nonce.php 5 years ago Notification.php 5 years ago NumberFormatter.php 5 years ago Option.php 5 years ago Period.php 5 years ago Piwik.php 5 years ago Plugin.php 5 years ago Profiler.php 5 years ago ProxyHeaders.php 5 years ago ProxyHttp.php 5 years ago QuickForm2.php 5 years ago RankingQuery.php 5 years ago ReportRenderer.php 5 years ago Segment.php 5 years ago Sequence.php 5 years ago Session.php 5 years ago SettingsPiwik.php 5 years ago SettingsServer.php 5 years ago Singleton.php 5 years ago Site.php 5 years ago TCPDF.php 5 years ago Theme.php 5 years ago Timer.php 5 years ago Tracker.php 5 years ago Twig.php 5 years ago Unzip.php 5 years ago UpdateCheck.php 5 years ago Updater.php 5 years ago UpdaterErrorException.php 5 years ago Updates.php 5 years ago Url.php 5 years ago UrlHelper.php 5 years ago Version.php 5 years ago View.php 5 years ago bootstrap.php 5 years ago dispatch.php 5 years ago testMinimumPhpVersion.php 5 years ago
Http.php
1012 lines
1 <?php
2 /**
3 * Matomo - free/libre analytics platform
4 *
5 * @link https://matomo.org
6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7 *
8 */
9 namespace Piwik;
10
11 use Exception;
12
13 /**
14 * Contains HTTP client related helper methods that can retrieve content from remote servers
15 * and optionally save to a local file.
16 *
17 * Used to check for the latest Piwik version and download updates.
18 *
19 */
20 class Http
21 {
22 /**
23 * Returns the "best" available transport method for {@link sendHttpRequest()} calls.
24 *
25 * @return string|null Either curl, fopen, socket or null if no method is supported.
26 * @api
27 */
28 public static function getTransportMethod()
29 {
30 $method = 'curl';
31 if (!self::isCurlEnabled()) {
32 $method = 'fopen';
33 if (@ini_get('allow_url_fopen') != '1') {
34 $method = 'socket';
35 if (!self::isSocketEnabled()) {
36 return null;
37 }
38 }
39 }
40 return $method;
41 }
42
43 protected static function isSocketEnabled()
44 {
45 return function_exists('fsockopen');
46 }
47
48 protected static function isCurlEnabled()
49 {
50 return function_exists('curl_init') && function_exists('curl_exec');
51 }
52
53 /**
54 * Sends an HTTP request using best available transport method.
55 *
56 * @param string $aUrl The target URL.
57 * @param int $timeout The number of seconds to wait before aborting the HTTP request.
58 * @param string|null $userAgent The user agent to use.
59 * @param string|null $destinationPath If supplied, the HTTP response will be saved to the file specified by
60 * this path.
61 * @param int|null $followDepth Internal redirect count. Should always pass `null` for this parameter.
62 * @param bool $acceptLanguage The value to use for the `'Accept-Language'` HTTP request header.
63 * @param array|bool $byteRange For `Range:` header. Should be two element array of bytes, eg, `array(0, 1024)`
64 * Doesn't work w/ `fopen` transport method.
65 * @param bool $getExtendedInfo If true returns the status code, headers & response, if false just the response.
66 * @param string $httpMethod The HTTP method to use. Defaults to `'GET'`.
67 * @param string $httpUsername HTTP Auth username
68 * @param string $httpPassword HTTP Auth password
69 *
70 * @throws Exception if the response cannot be saved to `$destinationPath`, if the HTTP response cannot be sent,
71 * if there are more than 5 redirects or if the request times out.
72 * @return bool|string If `$destinationPath` is not specified the HTTP response is returned on success. `false`
73 * is returned on failure.
74 * If `$getExtendedInfo` is `true` and `$destinationPath` is not specified an array with
75 * the following information is returned on success:
76 *
77 * - **status**: the HTTP status code
78 * - **headers**: the HTTP headers
79 * - **data**: the HTTP response data
80 *
81 * `false` is still returned on failure.
82 * @api
83 */
84 public static function sendHttpRequest($aUrl,
85 $timeout,
86 $userAgent = null,
87 $destinationPath = null,
88 $followDepth = 0,
89 $acceptLanguage = false,
90 $byteRange = false,
91 $getExtendedInfo = false,
92 $httpMethod = 'GET',
93 $httpUsername = null,
94 $httpPassword = null)
95 {
96 // create output file
97 $file = self::ensureDestinationDirectoryExists($destinationPath);
98
99 $acceptLanguage = $acceptLanguage ? 'Accept-Language: ' . $acceptLanguage : '';
100 return self::sendHttpRequestBy(self::getTransportMethod(), $aUrl, $timeout, $userAgent, $destinationPath, $file, $followDepth, $acceptLanguage, $acceptInvalidSslCertificate = false, $byteRange, $getExtendedInfo, $httpMethod, $httpUsername, $httpPassword);
101 }
102
103 public static function ensureDestinationDirectoryExists($destinationPath)
104 {
105 if ($destinationPath) {
106 Filesystem::mkdir(dirname($destinationPath));
107 if (($file = @fopen($destinationPath, 'wb')) === false || !is_resource($file)) {
108 throw new Exception('Error while creating the file: ' . $destinationPath);
109 }
110
111 return $file;
112 }
113
114 return null;
115 }
116
117 /**
118 * Sends an HTTP request using the specified transport method.
119 *
120 * @param string $method
121 * @param string $aUrl
122 * @param int $timeout in seconds
123 * @param string $userAgent
124 * @param string $destinationPath
125 * @param resource $file
126 * @param int $followDepth
127 * @param bool|string $acceptLanguage Accept-language header
128 * @param bool $acceptInvalidSslCertificate Only used with $method == 'curl'. If set to true (NOT recommended!) the SSL certificate will not be checked
129 * @param array|bool $byteRange For Range: header. Should be two element array of bytes, eg, array(0, 1024)
130 * Doesn't work w/ fopen method.
131 * @param bool $getExtendedInfo True to return status code, headers & response, false if just response.
132 * @param string $httpMethod The HTTP method to use. Defaults to `'GET'`.
133 * @param string $httpUsername HTTP Auth username
134 * @param string $httpPassword HTTP Auth password
135 * @param array|string $requestBody If $httpMethod is 'POST' this may accept an array of variables or a string that needs to be posted
136 * @param array $additionalHeaders List of additional headers to set for the request
137 *
138 * @return string|array true (or string/array) on success; false on HTTP response error code (1xx or 4xx)
139 *@throws Exception
140 */
141 public static function sendHttpRequestBy(
142 $method,
143 $aUrl,
144 $timeout,
145 $userAgent = null,
146 $destinationPath = null,
147 $file = null,
148 $followDepth = 0,
149 $acceptLanguage = false,
150 $acceptInvalidSslCertificate = false,
151 $byteRange = false,
152 $getExtendedInfo = false,
153 $httpMethod = 'GET',
154 $httpUsername = null,
155 $httpPassword = null,
156 $requestBody = null,
157 $additionalHeaders = array(),
158 $forcePost = null
159 ) {
160 if ($followDepth > 5) {
161 throw new Exception('Too many redirects (' . $followDepth . ')');
162 }
163
164 $contentLength = 0;
165 $fileLength = 0;
166
167 if ( !empty($requestBody ) && is_array($requestBody )) {
168 $requestBodyQuery = self::buildQuery($requestBody );
169 } else {
170 $requestBodyQuery = $requestBody;
171 }
172
173 // Piwik services behave like a proxy, so we should act like one.
174 $xff = 'X-Forwarded-For: '
175 . (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && !empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] . ',' : '')
176 . IP::getIpFromHeader();
177
178 if (empty($userAgent)) {
179 $userAgent = self::getUserAgent();
180 }
181
182 $via = 'Via: '
183 . (isset($_SERVER['HTTP_VIA']) && !empty($_SERVER['HTTP_VIA']) ? $_SERVER['HTTP_VIA'] . ', ' : '')
184 . Version::VERSION . ' '
185 . ($userAgent ? " ($userAgent)" : '');
186
187 // range header
188 $rangeBytes = '';
189 $rangeHeader = '';
190 if (!empty($byteRange)) {
191 $rangeBytes = $byteRange[0] . '-' . $byteRange[1];
192 $rangeHeader = 'Range: bytes=' . $rangeBytes . "\r\n";
193 }
194
195 list($proxyHost, $proxyPort, $proxyUser, $proxyPassword) = self::getProxyConfiguration($aUrl);
196
197 $aUrl = trim($aUrl);
198
199 // other result data
200 $status = null;
201 $headers = array();
202 $response = null;
203
204 $httpAuthIsUsed = !empty($httpUsername) || !empty($httpPassword);
205
206 $httpAuth = '';
207 if ($httpAuthIsUsed) {
208 $httpAuth = 'Authorization: Basic ' . base64_encode($httpUsername.':'.$httpPassword) . "\r\n";
209 }
210
211 $httpEventParams = array(
212 'httpMethod' => $httpMethod,
213 'body' => $requestBody,
214 'userAgent' => $userAgent,
215 'timeout' => $timeout,
216 'headers' => array_map('trim', array_filter(array_merge(array(
217 $rangeHeader, $via, $xff, $httpAuth, $acceptLanguage
218 ), $additionalHeaders))),
219 'verifySsl' => !$acceptInvalidSslCertificate,
220 'destinationPath' => $destinationPath
221 );
222
223 /**
224 * Triggered to send an HTTP request. Allows plugins to resolve the HTTP request themselves or to find out
225 * when an HTTP request is triggered to log this information for example to a monitoring tool.
226 *
227 * @param string $url The URL that needs to be requested
228 * @param array $params HTTP params like
229 * - 'httpMethod' (eg GET, POST, ...),
230 * - 'body' the request body if the HTTP method needs to be posted
231 * - 'userAgent'
232 * - 'timeout' After how many seconds a request should time out
233 * - 'headers' An array of header strings like array('Accept-Language: en', '...')
234 * - 'verifySsl' A boolean whether SSL certificate should be verified
235 * - 'destinationPath' If set, the response of the HTTP request should be saved to this file
236 * @param string &$response A plugin listening to this event should assign the HTTP response it received to this variable, for example "{value: true}"
237 * @param string &$status A plugin listening to this event should assign the HTTP status code it received to this variable, for example "200"
238 * @param array &$headers A plugin listening to this event should assign the HTTP headers it received to this variable, eg array('Content-Length' => '5')
239 */
240 Piwik::postEvent('Http.sendHttpRequest', array($aUrl, $httpEventParams, &$response, &$status, &$headers));
241
242 if ($response !== null || $status !== null || !empty($headers)) {
243 // was handled by event above...
244 /**
245 * described below
246 * @ignore
247 */
248 Piwik::postEvent('Http.sendHttpRequest.end', array($aUrl, $httpEventParams, &$response, &$status, &$headers));
249
250 if ($destinationPath && file_exists($destinationPath)) {
251 return true;
252 }
253 if ($getExtendedInfo) {
254 return array(
255 'status' => $status,
256 'headers' => $headers,
257 'data' => $response
258 );
259 } else {
260 return trim($response);
261 }
262 }
263
264 if ($method == 'socket') {
265 if (!self::isSocketEnabled()) {
266 // can be triggered in tests
267 throw new Exception("HTTP socket support is not enabled (php function fsockopen is not available) ");
268 }
269 // initialization
270 $url = @parse_url($aUrl);
271 if ($url === false || !isset($url['scheme'])) {
272 throw new Exception('Malformed URL: ' . $aUrl);
273 }
274
275 if ($url['scheme'] != 'http' && $url['scheme'] != 'https') {
276 throw new Exception('Invalid protocol/scheme: ' . $url['scheme']);
277 }
278 $host = $url['host'];
279 $port = isset($url['port']) ? $url['port'] : ('https' == $url['scheme'] ? 443 : 80);
280 $path = isset($url['path']) ? $url['path'] : '/';
281 if (isset($url['query'])) {
282 $path .= '?' . $url['query'];
283 }
284 $errno = null;
285 $errstr = null;
286
287 if ((!empty($proxyHost) && !empty($proxyPort))
288 || !empty($byteRange)
289 ) {
290 $httpVer = '1.1';
291 } else {
292 $httpVer = '1.0';
293 }
294
295 $proxyAuth = null;
296 if (!empty($proxyHost) && !empty($proxyPort)) {
297 $connectHost = $proxyHost;
298 $connectPort = $proxyPort;
299 if (!empty($proxyUser) && !empty($proxyPassword)) {
300 $proxyAuth = 'Proxy-Authorization: Basic ' . base64_encode("$proxyUser:$proxyPassword") . "\r\n";
301 }
302 $requestHeader = "$httpMethod $aUrl HTTP/$httpVer\r\n";
303 } else {
304 $connectHost = $host;
305 $connectPort = $port;
306 $requestHeader = "$httpMethod $path HTTP/$httpVer\r\n";
307
308 if ('https' == $url['scheme']) {
309 $connectHost = 'ssl://' . $connectHost;
310 }
311 }
312
313 // connection attempt
314 if (($fsock = @fsockopen($connectHost, $connectPort, $errno, $errstr, $timeout)) === false || !is_resource($fsock)) {
315 if (is_resource($file)) {
316 @fclose($file);
317 }
318 throw new Exception("Error while connecting to: $host. Please try again later. $errstr");
319 }
320
321 // send HTTP request header
322 $requestHeader .=
323 "Host: $host" . ($port != 80 && ('https' == $url['scheme'] && $port != 443) ? ':' . $port : '') . "\r\n"
324 . ($httpAuth ? $httpAuth : '')
325 . ($proxyAuth ? $proxyAuth : '')
326 . 'User-Agent: ' . $userAgent . "\r\n"
327 . ($acceptLanguage ? $acceptLanguage . "\r\n" : '')
328 . $xff . "\r\n"
329 . $via . "\r\n"
330 . $rangeHeader
331 . (!empty($additionalHeaders) ? implode("\r\n", $additionalHeaders) . "\r\n" : '')
332 . "Connection: close\r\n";
333 fwrite($fsock, $requestHeader);
334
335 if (strtolower($httpMethod) === 'post' && !empty($requestBodyQuery )) {
336 fwrite($fsock, self::buildHeadersForPost($requestBodyQuery ));
337 fwrite($fsock, "\r\n");
338 fwrite($fsock, $requestBodyQuery );
339 } else {
340 fwrite($fsock, "\r\n");
341 }
342
343 $streamMetaData = array('timed_out' => false);
344 @stream_set_blocking($fsock, true);
345
346 if (function_exists('stream_set_timeout')) {
347 @stream_set_timeout($fsock, $timeout);
348 } elseif (function_exists('socket_set_timeout')) {
349 @socket_set_timeout($fsock, $timeout);
350 }
351
352 // process header
353 $status = null;
354
355 while (!feof($fsock)) {
356 $line = fgets($fsock, 4096);
357
358 $streamMetaData = @stream_get_meta_data($fsock);
359 if ($streamMetaData['timed_out']) {
360 if (is_resource($file)) {
361 @fclose($file);
362 }
363 @fclose($fsock);
364 throw new Exception('Timed out waiting for server response');
365 }
366
367 // a blank line marks the end of the server response header
368 if (rtrim($line, "\r\n") == '') {
369 break;
370 }
371
372 // parse first line of server response header
373 if (!$status) {
374 // expect first line to be HTTP response status line, e.g., HTTP/1.1 200 OK
375 if (!preg_match('~^HTTP/(\d\.\d)\s+(\d+)(\s*.*)?~', $line, $m)) {
376 if (is_resource($file)) {
377 @fclose($file);
378 }
379 @fclose($fsock);
380 throw new Exception('Expected server response code. Got ' . rtrim($line, "\r\n"));
381 }
382
383 $status = (integer)$m[2];
384
385 // Informational 1xx or Client Error 4xx
386 if ($status < 200 || $status >= 400) {
387 if (is_resource($file)) {
388 @fclose($file);
389 }
390 @fclose($fsock);
391
392 if (!$getExtendedInfo) {
393 return false;
394 } else {
395 return array('status' => $status);
396 }
397 }
398
399 continue;
400 }
401
402 // handle redirect
403 if (preg_match('/^Location:\s*(.+)/', rtrim($line, "\r\n"), $m)) {
404 if (is_resource($file)) {
405 @fclose($file);
406 }
407 @fclose($fsock);
408 // Successful 2xx vs Redirect 3xx
409 if ($status < 300) {
410 throw new Exception('Unexpected redirect to Location: ' . rtrim($line) . ' for status code ' . $status);
411 }
412 return self::sendHttpRequestBy(
413 $method,
414 trim($m[1]),
415 $timeout,
416 $userAgent,
417 $destinationPath,
418 $file,
419 $followDepth + 1,
420 $acceptLanguage,
421 $acceptInvalidSslCertificate = false,
422 $byteRange,
423 $getExtendedInfo,
424 $httpMethod,
425 $httpUsername,
426 $httpPassword,
427 $requestBodyQuery,
428 $additionalHeaders
429 );
430 }
431
432 // save expected content length for later verification
433 if (preg_match('/^Content-Length:\s*(\d+)/', $line, $m)) {
434 $contentLength = (integer)$m[1];
435 }
436
437 self::parseHeaderLine($headers, $line);
438 }
439
440 if (feof($fsock)
441 && $httpMethod != 'HEAD'
442 ) {
443 throw new Exception('Unexpected end of transmission');
444 }
445
446 // process content/body
447 $response = '';
448
449 while (!feof($fsock)) {
450 $line = fread($fsock, 8192);
451
452 $streamMetaData = @stream_get_meta_data($fsock);
453 if ($streamMetaData['timed_out']) {
454 if (is_resource($file)) {
455 @fclose($file);
456 }
457 @fclose($fsock);
458 throw new Exception('Timed out waiting for server response');
459 }
460
461 $fileLength += strlen($line);
462
463 if (is_resource($file)) {
464 // save to file
465 fwrite($file, $line);
466 } else {
467 // concatenate to response string
468 $response .= $line;
469 }
470 }
471
472 // determine success or failure
473 @fclose(@$fsock);
474 } elseif ($method == 'fopen') {
475 $response = false;
476
477 // we make sure the request takes less than a few seconds to fail
478 // we create a stream_context (works in php >= 5.2.1)
479 // we also set the socket_timeout (for php < 5.2.1)
480 $default_socket_timeout = @ini_get('default_socket_timeout');
481 @ini_set('default_socket_timeout', $timeout);
482
483 $ctx = null;
484 if (function_exists('stream_context_create')) {
485 $stream_options = array(
486 'http' => array(
487 'header' => 'User-Agent: ' . $userAgent . "\r\n"
488 . ($httpAuth ? $httpAuth : '')
489 . ($acceptLanguage ? $acceptLanguage . "\r\n" : '')
490 . $xff . "\r\n"
491 . $via . "\r\n"
492 . (!empty($additionalHeaders) ? implode("\r\n", $additionalHeaders) . "\r\n" : '')
493 . $rangeHeader,
494 'max_redirects' => 5, // PHP 5.1.0
495 'timeout' => $timeout, // PHP 5.2.1
496 )
497 );
498
499 if (!empty($proxyHost) && !empty($proxyPort)) {
500 $stream_options['http']['proxy'] = 'tcp://' . $proxyHost . ':' . $proxyPort;
501 $stream_options['http']['request_fulluri'] = true; // required by squid proxy
502 if (!empty($proxyUser) && !empty($proxyPassword)) {
503 $stream_options['http']['header'] .= 'Proxy-Authorization: Basic ' . base64_encode("$proxyUser:$proxyPassword") . "\r\n";
504 }
505 }
506
507 if (strtolower($httpMethod) === 'post' && !empty($requestBodyQuery )) {
508 $postHeader = self::buildHeadersForPost($requestBodyQuery );
509 $postHeader .= "\r\n";
510 $stream_options['http']['method'] = 'POST';
511 $stream_options['http']['header'] .= $postHeader;
512 $stream_options['http']['content'] = $requestBodyQuery;
513 }
514
515 $ctx = stream_context_create($stream_options);
516 }
517
518 // save to file
519 if (is_resource($file)) {
520 if (!($handle = fopen($aUrl, 'rb', false, $ctx))) {
521 throw new Exception("Unable to open $aUrl");
522 }
523 while (!feof($handle)) {
524 $response = fread($handle, 8192);
525 $fileLength += strlen($response);
526 fwrite($file, $response);
527 }
528 fclose($handle);
529 } else {
530 $response = @file_get_contents($aUrl, 0, $ctx);
531
532 // try to get http status code from response headers
533 if (isset($http_response_header) && preg_match('~^HTTP/(\d\.\d)\s+(\d+)(\s*.*)?~', implode("\n", $http_response_header), $m)) {
534 $status = (int)$m[2];
535 }
536
537 if (!$status && $response === false) {
538 $error = error_get_last();
539 throw new \Exception($error['message']);
540 }
541 $fileLength = strlen($response);
542 }
543
544 foreach ($http_response_header as $line) {
545 self::parseHeaderLine($headers, $line);
546 }
547
548 // restore the socket_timeout value
549 if (!empty($default_socket_timeout)) {
550 @ini_set('default_socket_timeout', $default_socket_timeout);
551 }
552 } elseif ($method == 'curl') {
553 if (!self::isCurlEnabled()) {
554 // can be triggered in tests
555 throw new Exception("CURL is not enabled in php.ini, but is being used.");
556 }
557 $ch = @curl_init();
558
559 if (!empty($proxyHost) && !empty($proxyPort)) {
560 @curl_setopt($ch, CURLOPT_PROXY, $proxyHost . ':' . $proxyPort);
561 if (!empty($proxyUser) && !empty($proxyPassword)) {
562 // PROXYAUTH defaults to BASIC
563 @curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxyUser . ':' . $proxyPassword);
564 }
565 }
566
567 $curl_options = array(
568 // internal to ext/curl
569 CURLOPT_BINARYTRANSFER => is_resource($file),
570
571 // curl options (sorted oldest to newest)
572 CURLOPT_URL => $aUrl,
573 CURLOPT_USERAGENT => $userAgent,
574 CURLOPT_HTTPHEADER => array_merge(array(
575 $xff,
576 $via,
577 $acceptLanguage
578 ), $additionalHeaders),
579 // only get header info if not saving directly to file
580 CURLOPT_HEADER => is_resource($file) ? false : true,
581 CURLOPT_CONNECTTIMEOUT => $timeout,
582 CURLOPT_TIMEOUT => $timeout,
583 );
584
585 if ($rangeBytes) {
586 curl_setopt($ch, CURLOPT_RANGE, $rangeBytes);
587 }
588
589 // Case core:archive command is triggering archiving on https:// and the certificate is not valid
590 if ($acceptInvalidSslCertificate) {
591 $curl_options += array(
592 CURLOPT_SSL_VERIFYHOST => false,
593 CURLOPT_SSL_VERIFYPEER => false,
594 );
595 }
596 @curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $httpMethod);
597 if ($httpMethod == 'HEAD') {
598 @curl_setopt($ch, CURLOPT_NOBODY, true);
599 }
600
601 if (strtolower($httpMethod) === 'post' && !empty($requestBodyQuery )) {
602 curl_setopt($ch, CURLOPT_POST, 1);
603 curl_setopt($ch, CURLOPT_POSTFIELDS, $requestBodyQuery );
604 }
605
606 if (!empty($httpUsername) && !empty($httpPassword)) {
607 $curl_options += array(
608 CURLOPT_USERPWD => $httpUsername . ':' . $httpPassword,
609 );
610 }
611
612 @curl_setopt_array($ch, $curl_options);
613 self::configCurlCertificate($ch);
614
615 /*
616 * as of php 5.2.0, CURLOPT_FOLLOWLOCATION can't be set if
617 * in safe_mode or open_basedir is set
618 */
619 if ((string)ini_get('safe_mode') == '' && ini_get('open_basedir') == '') {
620 $curl_options = array(
621 // curl options (sorted oldest to newest)
622 CURLOPT_FOLLOWLOCATION => true,
623 CURLOPT_MAXREDIRS => 5,
624 );
625 if ($forcePost) {
626 $curl_options[CURLOPT_POSTREDIR] = CURL_REDIR_POST_ALL;
627 }
628 @curl_setopt_array($ch, $curl_options);
629 }
630
631 if (is_resource($file)) {
632 // write output directly to file
633 @curl_setopt($ch, CURLOPT_FILE, $file);
634 } else {
635 // internal to ext/curl
636 @curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
637 }
638
639 ob_start();
640 $response = @curl_exec($ch);
641 ob_end_clean();
642
643 if ($response === true) {
644 $response = '';
645 } elseif ($response === false) {
646 $errstr = curl_error($ch);
647 if ($errstr != '') {
648 throw new Exception('curl_exec: ' . $errstr
649 . '. Hostname requested was: ' . UrlHelper::getHostFromUrl($aUrl));
650 }
651 $response = '';
652 } else {
653 $header = '';
654 // redirects are included in the output html, so we look for the last line that starts w/ HTTP/...
655 // to split the response
656 while (substr($response, 0, 5) == "HTTP/") {
657 $split = explode("\r\n\r\n", $response, 2);
658
659 if(count($split) == 2) {
660 list($header, $response) = $split;
661 } else {
662 $response = '';
663 $header = $split;
664 }
665 }
666
667 foreach (explode("\r\n", $header) as $line) {
668 self::parseHeaderLine($headers, $line);
669 }
670 }
671
672 $contentLength = @curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD);
673 $fileLength = is_resource($file) ? @curl_getinfo($ch, CURLINFO_SIZE_DOWNLOAD) : strlen($response);
674 $status = @curl_getinfo($ch, CURLINFO_HTTP_CODE);
675
676 @curl_close($ch);
677 unset($ch);
678 } else {
679 throw new Exception('Invalid request method: ' . $method);
680 }
681
682 if (is_resource($file)) {
683 fflush($file);
684 @fclose($file);
685
686 $fileSize = filesize($destinationPath);
687 if ((($contentLength > 0) && ($fileLength != $contentLength))
688 || ($fileSize != $fileLength)
689 ) {
690 throw new Exception('File size error: ' . $destinationPath . '; expected ' . $contentLength . ' bytes; received ' . $fileLength . ' bytes; saved ' . $fileSize . ' bytes to file');
691 }
692 return true;
693 }
694
695 /**
696 * Triggered when an HTTP request finished. A plugin can for example listen to this and alter the response,
697 * status code, or finish a timer in case the plugin is measuring how long it took to execute the request
698 *
699 * @param string $url The URL that needs to be requested
700 * @param array $params HTTP params like
701 * - 'httpMethod' (eg GET, POST, ...),
702 * - 'body' the request body if the HTTP method needs to be posted
703 * - 'userAgent'
704 * - 'timeout' After how many seconds a request should time out
705 * - 'headers' An array of header strings like array('Accept-Language: en', '...')
706 * - 'verifySsl' A boolean whether SSL certificate should be verified
707 * - 'destinationPath' If set, the response of the HTTP request should be saved to this file
708 * @param string &$response The response of the HTTP request, for example "{value: true}"
709 * @param string &$status The returned HTTP status code, for example "200"
710 * @param array &$headers The returned headers, eg array('Content-Length' => '5')
711 */
712 Piwik::postEvent('Http.sendHttpRequest.end', array($aUrl, $httpEventParams, &$response, &$status, &$headers));
713
714 if (!$getExtendedInfo) {
715 return trim($response);
716 } else {
717 return array(
718 'status' => $status,
719 'headers' => $headers,
720 'data' => $response
721 );
722 }
723 }
724
725 public static function buildQuery($params)
726 {
727 return http_build_query($params, '', '&');
728 }
729
730 private static function buildHeadersForPost($requestBody)
731 {
732 $postHeader = "Content-Type: application/x-www-form-urlencoded\r\n";
733 $postHeader .= "Content-Length: " . strlen($requestBody) . "\r\n";
734
735 return $postHeader;
736 }
737
738 /**
739 * Downloads the next chunk of a specific file. The next chunk's byte range
740 * is determined by the existing file's size and the expected file size, which
741 * is stored in the option table before starting a download. The expected
742 * file size is obtained through a `HEAD` HTTP request.
743 *
744 * _Note: this function uses the **Range** HTTP header to accomplish downloading in
745 * parts. Not every server supports this header._
746 *
747 * The proper use of this function is to call it once per request. The browser
748 * should continue to send requests to Piwik which will in turn call this method
749 * until the file has completely downloaded. In this way, the user can be informed
750 * of a download's progress.
751 *
752 * **Example Usage**
753 *
754 * ```
755 * // browser JavaScript
756 * var downloadFile = function (isStart) {
757 * var ajax = new ajaxHelper();
758 * ajax.addParams({
759 * module: 'MyPlugin',
760 * action: 'myAction',
761 * isStart: isStart ? 1 : 0
762 * }, 'post');
763 * ajax.setCallback(function (response) {
764 * var progress = response.progress
765 * // ...update progress...
766 *
767 * downloadFile(false);
768 * });
769 * ajax.send();
770 * }
771 *
772 * downloadFile(true);
773 * ```
774 *
775 * ```
776 * // PHP controller action
777 * public function myAction()
778 * {
779 * $outputPath = PIWIK_INCLUDE_PATH . '/tmp/averybigfile.zip';
780 * $isStart = Common::getRequestVar('isStart', 1, 'int');
781 * Http::downloadChunk("http://bigfiles.com/averybigfile.zip", $outputPath, $isStart == 1);
782 * }
783 * ```
784 *
785 * @param string $url The url to download from.
786 * @param string $outputPath The path to the file to save/append to.
787 * @param bool $isContinuation `true` if this is the continuation of a download,
788 * or if we're starting a fresh one.
789 * @throws Exception if the file already exists and we're starting a new download,
790 * if we're trying to continue a download that never started
791 * @return array
792 * @api
793 */
794 public static function downloadChunk($url, $outputPath, $isContinuation)
795 {
796 // make sure file doesn't already exist if we're starting a new download
797 if (!$isContinuation
798 && file_exists($outputPath)
799 ) {
800 throw new Exception(
801 Piwik::translate('General_DownloadFail_FileExists', "'" . $outputPath . "'")
802 . ' ' . Piwik::translate('General_DownloadPleaseRemoveExisting'));
803 }
804
805 // if we're starting a download, get the expected file size & save as an option
806 $downloadOption = $outputPath . '_expectedDownloadSize';
807 if (!$isContinuation) {
808 $expectedFileSizeResult = Http::sendHttpRequest(
809 $url,
810 $timeout = 300,
811 $userAgent = null,
812 $destinationPath = null,
813 $followDepth = 0,
814 $acceptLanguage = false,
815 $byteRange = false,
816 $getExtendedInfo = true,
817 $httpMethod = 'HEAD'
818 );
819
820 $expectedFileSize = 0;
821 if (isset($expectedFileSizeResult['headers']['Content-Length'])) {
822 $expectedFileSize = (int)$expectedFileSizeResult['headers']['Content-Length'];
823 }
824
825 if ($expectedFileSize == 0) {
826 Log::info("HEAD request for '%s' failed, got following: %s", $url, print_r($expectedFileSizeResult, true));
827 throw new Exception(Piwik::translate('General_DownloadFail_HttpRequestFail'));
828 }
829
830 Option::set($downloadOption, $expectedFileSize);
831 } else {
832 $expectedFileSize = (int)Option::get($downloadOption);
833 if ($expectedFileSize === false) { // sanity check
834 throw new Exception("Trying to continue a download that never started?! That's not supposed to happen...");
835 }
836 }
837
838 // if existing file is already big enough, then fail so we don't accidentally overwrite
839 // existing DB
840 $existingSize = file_exists($outputPath) ? filesize($outputPath) : 0;
841 if ($existingSize >= $expectedFileSize) {
842 throw new Exception(
843 Piwik::translate('General_DownloadFail_FileExistsContinue', "'" . $outputPath . "'")
844 . ' ' . Piwik::translate('General_DownloadPleaseRemoveExisting'));
845 }
846
847 // download a chunk of the file
848 $result = Http::sendHttpRequest(
849 $url,
850 $timeout = 300,
851 $userAgent = null,
852 $destinationPath = null,
853 $followDepth = 0,
854 $acceptLanguage = false,
855 $byteRange = array($existingSize, min($existingSize + 1024 * 1024 - 1, $expectedFileSize)),
856 $getExtendedInfo = true
857 );
858
859 if ($result === false
860 || $result['status'] < 200
861 || $result['status'] > 299
862 ) {
863 $result['data'] = self::truncateStr($result['data'], 1024);
864 Log::info("Failed to download range '%s-%s' of file from url '%s'. Got result: %s",
865 $byteRange[0], $byteRange[1], $url, print_r($result, true));
866
867 throw new Exception(Piwik::translate('General_DownloadFail_HttpRequestFail'));
868 }
869
870 // write chunk to file
871 $f = fopen($outputPath, 'ab');
872 fwrite($f, $result['data']);
873 fclose($f);
874
875 clearstatcache($clear_realpath_cache = true, $outputPath);
876 return array(
877 'current_size' => filesize($outputPath),
878 'expected_file_size' => $expectedFileSize,
879 );
880 }
881
882 /**
883 * Will configure CURL handle $ch
884 * to use local list of Certificate Authorities,
885 */
886 public static function configCurlCertificate(&$ch)
887 {
888 $general = Config::getInstance()->General;
889 if (!empty($general['custom_cacert_pem'])) {
890 $cacertPath = $general['custom_cacert_pem'];
891 } else {
892 $cacertPath = PIWIK_INCLUDE_PATH . '/core/DataFiles/cacert.pem';
893 }
894 @curl_setopt($ch, CURLOPT_CAINFO, $cacertPath);
895 }
896
897 public static function getUserAgent()
898 {
899 return !empty($_SERVER['HTTP_USER_AGENT'])
900 ? $_SERVER['HTTP_USER_AGENT']
901 : 'Matomo/' . Version::VERSION;
902 }
903
904 /**
905 * Fetches a file located at `$url` and saves it to `$destinationPath`.
906 *
907 * @param string $url The URL of the file to download.
908 * @param string $destinationPath The path to download the file to.
909 * @param int $tries (deprecated)
910 * @param int $timeout The amount of seconds to wait before aborting the HTTP request.
911 * @throws Exception if the response cannot be saved to `$destinationPath`, if the HTTP response cannot be sent,
912 * if there are more than 5 redirects or if the request times out.
913 * @return bool `true` on success, throws Exception on failure
914 * @api
915 */
916 public static function fetchRemoteFile($url, $destinationPath = null, $tries = 0, $timeout = 10)
917 {
918 @ignore_user_abort(true);
919 SettingsServer::setMaxExecutionTime(0);
920 return self::sendHttpRequest($url, $timeout, 'Update', $destinationPath);
921 }
922
923 /**
924 * Utility function, parses an HTTP header line into key/value & sets header
925 * array with them.
926 *
927 * @param array $headers
928 * @param string $line
929 */
930 private static function parseHeaderLine(&$headers, $line)
931 {
932 $parts = explode(':', $line, 2);
933 if (count($parts) == 1) {
934 return;
935 }
936
937 list($name, $value) = $parts;
938 $name = trim($name);
939 $headers[$name] = trim($value);
940
941 /**
942 * With HTTP/2 Cloudflare is passing headers in lowercase (e.g. 'content-type' instead of 'Content-Type')
943 * which breaks any code which uses the header data.
944 */
945 if (version_compare(PHP_VERSION, '5.5.16', '>=')) {
946 // Passing a second arg to ucwords is not supported by older versions of PHP
947 $camelName = ucwords($name, '-');
948 if ($camelName !== $name) {
949 $headers[$camelName] = trim($value);
950 }
951 }
952 }
953
954 /**
955 * Utility function that truncates a string to an arbitrary limit.
956 *
957 * @param string $str The string to truncate.
958 * @param int $limit The maximum length of the truncated string.
959 * @return string
960 */
961 private static function truncateStr($str, $limit)
962 {
963 if (strlen($str) > $limit) {
964 return substr($str, 0, $limit) . '...';
965 }
966 return $str;
967 }
968
969 /**
970 * Returns the If-Modified-Since HTTP header if it can be found. If it cannot be
971 * found, an empty string is returned.
972 *
973 * @return string
974 */
975 public static function getModifiedSinceHeader()
976 {
977 $modifiedSince = '';
978 if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
979 $modifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
980
981 // strip any trailing data appended to header
982 if (false !== ($semicolonPos = strpos($modifiedSince, ';'))) {
983 $modifiedSince = substr($modifiedSince, 0, $semicolonPos);
984 }
985 }
986 return $modifiedSince;
987 }
988
989 /**
990 * Returns Proxy to use for connecting via HTTP to given URL
991 *
992 * @param string $url
993 * @return array
994 */
995 private static function getProxyConfiguration($url)
996 {
997 $hostname = UrlHelper::getHostFromUrl($url);
998
999 if (Url::isLocalHost($hostname)) {
1000 return array(null, null, null, null);
1001 }
1002
1003 // proxy configuration
1004 $proxyHost = Config::getInstance()->proxy['host'];
1005 $proxyPort = Config::getInstance()->proxy['port'];
1006 $proxyUser = Config::getInstance()->proxy['username'];
1007 $proxyPassword = Config::getInstance()->proxy['password'];
1008
1009 return array($proxyHost, $proxyPort, $proxyUser, $proxyPassword);
1010 }
1011 }
1012