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