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