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