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