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