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