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