PageRenderTime 41ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/core/Http.php

https://github.com/CodeYellowBV/piwik
PHP | 755 lines | 477 code | 77 blank | 201 comment | 123 complexity | b36f6213b0787997ebe409bf79bff5a1 MD5 | raw file
Possible License(s): LGPL-3.0, JSON, MIT, GPL-3.0, LGPL-2.1, GPL-2.0, AGPL-1.0, BSD-2-Clause, BSD-3-Clause
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. *
  8. */
  9. namespace Piwik;
  10. use Exception;
  11. /**
  12. * Contains HTTP client related helper methods that can retrieve content from remote servers
  13. * and optionally save to a local file.
  14. *
  15. * Used to check for the latest Piwik version and download updates.
  16. *
  17. */
  18. class Http
  19. {
  20. /**
  21. * Returns the "best" available transport method for {@link sendHttpRequest()} calls.
  22. *
  23. * @return string Either `'curl'`, `'fopen'` or `'socket'`.
  24. * @api
  25. */
  26. public static function getTransportMethod()
  27. {
  28. $method = 'curl';
  29. if (!self::isCurlEnabled()) {
  30. $method = 'fopen';
  31. if (@ini_get('allow_url_fopen') != '1') {
  32. $method = 'socket';
  33. if (!self::isSocketEnabled()) {
  34. return null;
  35. }
  36. }
  37. }
  38. return $method;
  39. }
  40. protected static function isSocketEnabled()
  41. {
  42. return function_exists('fsockopen');
  43. }
  44. protected static function isCurlEnabled()
  45. {
  46. return function_exists('curl_init');
  47. }
  48. /**
  49. * Sends an HTTP request using best available transport method.
  50. *
  51. * @param string $aUrl The target URL.
  52. * @param int $timeout The number of seconds to wait before aborting the HTTP request.
  53. * @param string|null $userAgent The user agent to use.
  54. * @param string|null $destinationPath If supplied, the HTTP response will be saved to the file specified by
  55. * this path.
  56. * @param int|null $followDepth Internal redirect count. Should always pass `null` for this parameter.
  57. * @param bool $acceptLanguage The value to use for the `'Accept-Language'` HTTP request header.
  58. * @param array|bool $byteRange For `Range:` header. Should be two element array of bytes, eg, `array(0, 1024)`
  59. * Doesn't work w/ `fopen` transport method.
  60. * @param bool $getExtendedInfo If true returns the status code, headers & response, if false just the response.
  61. * @param string $httpMethod The HTTP method to use. Defaults to `'GET'`.
  62. * @throws Exception if the response cannot be saved to `$destinationPath`, if the HTTP response cannot be sent,
  63. * if there are more than 5 redirects or if the request times out.
  64. * @return bool|string If `$destinationPath` is not specified the HTTP response is returned on success. `false`
  65. * is returned on failure.
  66. * If `$getExtendedInfo` is `true` and `$destinationPath` is not specified an array with
  67. * the following information is returned on success:
  68. *
  69. * - **status**: the HTTP status code
  70. * - **headers**: the HTTP headers
  71. * - **data**: the HTTP response data
  72. *
  73. * `false` is still returned on failure.
  74. * @api
  75. */
  76. public static function sendHttpRequest($aUrl, $timeout, $userAgent = null, $destinationPath = null, $followDepth = 0, $acceptLanguage = false, $byteRange = false, $getExtendedInfo = false, $httpMethod = 'GET')
  77. {
  78. // create output file
  79. $file = null;
  80. if ($destinationPath) {
  81. // Ensure destination directory exists
  82. Filesystem::mkdir(dirname($destinationPath));
  83. if (($file = @fopen($destinationPath, 'wb')) === false || !is_resource($file)) {
  84. throw new Exception('Error while creating the file: ' . $destinationPath);
  85. }
  86. }
  87. $acceptLanguage = $acceptLanguage ? 'Accept-Language: ' . $acceptLanguage : '';
  88. return self::sendHttpRequestBy(self::getTransportMethod(), $aUrl, $timeout, $userAgent, $destinationPath, $file, $followDepth, $acceptLanguage, $acceptInvalidSslCertificate = false, $byteRange, $getExtendedInfo, $httpMethod);
  89. }
  90. /**
  91. * Sends an HTTP request using the specified transport method.
  92. *
  93. * @param string $method
  94. * @param string $aUrl
  95. * @param int $timeout
  96. * @param string $userAgent
  97. * @param string $destinationPath
  98. * @param resource $file
  99. * @param int $followDepth
  100. * @param bool|string $acceptLanguage Accept-language header
  101. * @param bool $acceptInvalidSslCertificate Only used with $method == 'curl'. If set to true (NOT recommended!) the SSL certificate will not be checked
  102. * @param array|bool $byteRange For Range: header. Should be two element array of bytes, eg, array(0, 1024)
  103. * Doesn't work w/ fopen method.
  104. * @param bool $getExtendedInfo True to return status code, headers & response, false if just response.
  105. * @param string $httpMethod The HTTP method to use. Defaults to `'GET'`.
  106. *
  107. * @throws Exception
  108. * @return bool true (or string/array) on success; false on HTTP response error code (1xx or 4xx)
  109. */
  110. public static function sendHttpRequestBy(
  111. $method = 'socket',
  112. $aUrl,
  113. $timeout,
  114. $userAgent = null,
  115. $destinationPath = null,
  116. $file = null,
  117. $followDepth = 0,
  118. $acceptLanguage = false,
  119. $acceptInvalidSslCertificate = false,
  120. $byteRange = false,
  121. $getExtendedInfo = false,
  122. $httpMethod = 'GET'
  123. )
  124. {
  125. if ($followDepth > 5) {
  126. throw new Exception('Too many redirects (' . $followDepth . ')');
  127. }
  128. $contentLength = 0;
  129. $fileLength = 0;
  130. // Piwik services behave like a proxy, so we should act like one.
  131. $xff = 'X-Forwarded-For: '
  132. . (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && !empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] . ',' : '')
  133. . IP::getIpFromHeader();
  134. if (empty($userAgent)) {
  135. $userAgent = self::getUserAgent();
  136. }
  137. $via = 'Via: '
  138. . (isset($_SERVER['HTTP_VIA']) && !empty($_SERVER['HTTP_VIA']) ? $_SERVER['HTTP_VIA'] . ', ' : '')
  139. . Version::VERSION . ' '
  140. . ($userAgent ? " ($userAgent)" : '');
  141. // range header
  142. $rangeHeader = '';
  143. if (!empty($byteRange)) {
  144. $rangeHeader = 'Range: bytes=' . $byteRange[0] . '-' . $byteRange[1] . "\r\n";
  145. }
  146. // proxy configuration
  147. $proxyHost = Config::getInstance()->proxy['host'];
  148. $proxyPort = Config::getInstance()->proxy['port'];
  149. $proxyUser = Config::getInstance()->proxy['username'];
  150. $proxyPassword = Config::getInstance()->proxy['password'];
  151. // other result data
  152. $status = null;
  153. $headers = array();
  154. if ($method == 'socket') {
  155. if (!self::isSocketEnabled()) {
  156. // can be triggered in tests
  157. throw new Exception("HTTP socket support is not enabled (php function fsockopen is not available) ");
  158. }
  159. // initialization
  160. $url = @parse_url($aUrl);
  161. if ($url === false || !isset($url['scheme'])) {
  162. throw new Exception('Malformed URL: ' . $aUrl);
  163. }
  164. if ($url['scheme'] != 'http') {
  165. throw new Exception('Invalid protocol/scheme: ' . $url['scheme']);
  166. }
  167. $host = $url['host'];
  168. $port = isset($url['port)']) ? $url['port'] : 80;
  169. $path = isset($url['path']) ? $url['path'] : '/';
  170. if (isset($url['query'])) {
  171. $path .= '?' . $url['query'];
  172. }
  173. $errno = null;
  174. $errstr = null;
  175. if ((!empty($proxyHost) && !empty($proxyPort))
  176. || !empty($byteRange)
  177. ) {
  178. $httpVer = '1.1';
  179. } else {
  180. $httpVer = '1.0';
  181. }
  182. $proxyAuth = null;
  183. if (!empty($proxyHost) && !empty($proxyPort)) {
  184. $connectHost = $proxyHost;
  185. $connectPort = $proxyPort;
  186. if (!empty($proxyUser) && !empty($proxyPassword)) {
  187. $proxyAuth = 'Proxy-Authorization: Basic ' . base64_encode("$proxyUser:$proxyPassword") . "\r\n";
  188. }
  189. $requestHeader = "$httpMethod $aUrl HTTP/$httpVer\r\n";
  190. } else {
  191. $connectHost = $host;
  192. $connectPort = $port;
  193. $requestHeader = "$httpMethod $path HTTP/$httpVer\r\n";
  194. }
  195. // connection attempt
  196. if (($fsock = @fsockopen($connectHost, $connectPort, $errno, $errstr, $timeout)) === false || !is_resource($fsock)) {
  197. if (is_resource($file)) {
  198. @fclose($file);
  199. }
  200. throw new Exception("Error while connecting to: $host. Please try again later. $errstr");
  201. }
  202. // send HTTP request header
  203. $requestHeader .=
  204. "Host: $host" . ($port != 80 ? ':' . $port : '') . "\r\n"
  205. . ($proxyAuth ? $proxyAuth : '')
  206. . 'User-Agent: ' . $userAgent . "\r\n"
  207. . ($acceptLanguage ? $acceptLanguage . "\r\n" : '')
  208. . $xff . "\r\n"
  209. . $via . "\r\n"
  210. . $rangeHeader
  211. . "Connection: close\r\n"
  212. . "\r\n";
  213. fwrite($fsock, $requestHeader);
  214. $streamMetaData = array('timed_out' => false);
  215. @stream_set_blocking($fsock, true);
  216. if (function_exists('stream_set_timeout')) {
  217. @stream_set_timeout($fsock, $timeout);
  218. } elseif (function_exists('socket_set_timeout')) {
  219. @socket_set_timeout($fsock, $timeout);
  220. }
  221. // process header
  222. $status = null;
  223. while (!feof($fsock)) {
  224. $line = fgets($fsock, 4096);
  225. $streamMetaData = @stream_get_meta_data($fsock);
  226. if ($streamMetaData['timed_out']) {
  227. if (is_resource($file)) {
  228. @fclose($file);
  229. }
  230. @fclose($fsock);
  231. throw new Exception('Timed out waiting for server response');
  232. }
  233. // a blank line marks the end of the server response header
  234. if (rtrim($line, "\r\n") == '') {
  235. break;
  236. }
  237. // parse first line of server response header
  238. if (!$status) {
  239. // expect first line to be HTTP response status line, e.g., HTTP/1.1 200 OK
  240. if (!preg_match('~^HTTP/(\d\.\d)\s+(\d+)(\s*.*)?~', $line, $m)) {
  241. if (is_resource($file)) {
  242. @fclose($file);
  243. }
  244. @fclose($fsock);
  245. throw new Exception('Expected server response code. Got ' . rtrim($line, "\r\n"));
  246. }
  247. $status = (integer)$m[2];
  248. // Informational 1xx or Client Error 4xx
  249. if ($status < 200 || $status >= 400) {
  250. if (is_resource($file)) {
  251. @fclose($file);
  252. }
  253. @fclose($fsock);
  254. if (!$getExtendedInfo) {
  255. return false;
  256. } else {
  257. return array('status' => $status);
  258. }
  259. }
  260. continue;
  261. }
  262. // handle redirect
  263. if (preg_match('/^Location:\s*(.+)/', rtrim($line, "\r\n"), $m)) {
  264. if (is_resource($file)) {
  265. @fclose($file);
  266. }
  267. @fclose($fsock);
  268. // Successful 2xx vs Redirect 3xx
  269. if ($status < 300) {
  270. throw new Exception('Unexpected redirect to Location: ' . rtrim($line) . ' for status code ' . $status);
  271. }
  272. return self::sendHttpRequestBy(
  273. $method,
  274. trim($m[1]),
  275. $timeout,
  276. $userAgent,
  277. $destinationPath,
  278. $file,
  279. $followDepth + 1,
  280. $acceptLanguage,
  281. $acceptInvalidSslCertificate = false,
  282. $byteRange,
  283. $getExtendedInfo,
  284. $httpMethod
  285. );
  286. }
  287. // save expected content length for later verification
  288. if (preg_match('/^Content-Length:\s*(\d+)/', $line, $m)) {
  289. $contentLength = (integer)$m[1];
  290. }
  291. self::parseHeaderLine($headers, $line);
  292. }
  293. if (feof($fsock)
  294. && $httpMethod != 'HEAD'
  295. ) {
  296. throw new Exception('Unexpected end of transmission');
  297. }
  298. // process content/body
  299. $response = '';
  300. while (!feof($fsock)) {
  301. $line = fread($fsock, 8192);
  302. $streamMetaData = @stream_get_meta_data($fsock);
  303. if ($streamMetaData['timed_out']) {
  304. if (is_resource($file)) {
  305. @fclose($file);
  306. }
  307. @fclose($fsock);
  308. throw new Exception('Timed out waiting for server response');
  309. }
  310. $fileLength += strlen($line);
  311. if (is_resource($file)) {
  312. // save to file
  313. fwrite($file, $line);
  314. } else {
  315. // concatenate to response string
  316. $response .= $line;
  317. }
  318. }
  319. // determine success or failure
  320. @fclose(@$fsock);
  321. } else if ($method == 'fopen') {
  322. $response = false;
  323. // we make sure the request takes less than a few seconds to fail
  324. // we create a stream_context (works in php >= 5.2.1)
  325. // we also set the socket_timeout (for php < 5.2.1)
  326. $default_socket_timeout = @ini_get('default_socket_timeout');
  327. @ini_set('default_socket_timeout', $timeout);
  328. $ctx = null;
  329. if (function_exists('stream_context_create')) {
  330. $stream_options = array(
  331. 'http' => array(
  332. 'header' => 'User-Agent: ' . $userAgent . "\r\n"
  333. . ($acceptLanguage ? $acceptLanguage . "\r\n" : '')
  334. . $xff . "\r\n"
  335. . $via . "\r\n"
  336. . $rangeHeader,
  337. 'max_redirects' => 5, // PHP 5.1.0
  338. 'timeout' => $timeout, // PHP 5.2.1
  339. )
  340. );
  341. if (!empty($proxyHost) && !empty($proxyPort)) {
  342. $stream_options['http']['proxy'] = 'tcp://' . $proxyHost . ':' . $proxyPort;
  343. $stream_options['http']['request_fulluri'] = true; // required by squid proxy
  344. if (!empty($proxyUser) && !empty($proxyPassword)) {
  345. $stream_options['http']['header'] .= 'Proxy-Authorization: Basic ' . base64_encode("$proxyUser:$proxyPassword") . "\r\n";
  346. }
  347. }
  348. $ctx = stream_context_create($stream_options);
  349. }
  350. // save to file
  351. if (is_resource($file)) {
  352. $handle = fopen($aUrl, 'rb', false, $ctx);
  353. while (!feof($handle)) {
  354. $response = fread($handle, 8192);
  355. $fileLength += strlen($response);
  356. fwrite($file, $response);
  357. }
  358. fclose($handle);
  359. } else {
  360. $response = file_get_contents($aUrl, 0, $ctx);
  361. $fileLength = strlen($response);
  362. }
  363. // restore the socket_timeout value
  364. if (!empty($default_socket_timeout)) {
  365. @ini_set('default_socket_timeout', $default_socket_timeout);
  366. }
  367. } else if ($method == 'curl') {
  368. if (!self::isCurlEnabled()) {
  369. // can be triggered in tests
  370. throw new Exception("CURL is not enabled in php.ini, but is being used.");
  371. }
  372. $ch = @curl_init();
  373. if (!empty($proxyHost) && !empty($proxyPort)) {
  374. @curl_setopt($ch, CURLOPT_PROXY, $proxyHost . ':' . $proxyPort);
  375. if (!empty($proxyUser) && !empty($proxyPassword)) {
  376. // PROXYAUTH defaults to BASIC
  377. @curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxyUser . ':' . $proxyPassword);
  378. }
  379. }
  380. $curl_options = array(
  381. // internal to ext/curl
  382. CURLOPT_BINARYTRANSFER => is_resource($file),
  383. // curl options (sorted oldest to newest)
  384. CURLOPT_URL => $aUrl,
  385. CURLOPT_USERAGENT => $userAgent,
  386. CURLOPT_HTTPHEADER => array(
  387. $xff,
  388. $via,
  389. $rangeHeader,
  390. $acceptLanguage
  391. ),
  392. // only get header info if not saving directly to file
  393. CURLOPT_HEADER => is_resource($file) ? false : true,
  394. CURLOPT_CONNECTTIMEOUT => $timeout,
  395. );
  396. // Case core:archive command is triggering archiving on https:// and the certificate is not valid
  397. if ($acceptInvalidSslCertificate) {
  398. $curl_options += array(
  399. CURLOPT_SSL_VERIFYHOST => false,
  400. CURLOPT_SSL_VERIFYPEER => false,
  401. );
  402. }
  403. @curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $httpMethod);
  404. if ($httpMethod == 'HEAD') {
  405. @curl_setopt($ch, CURLOPT_NOBODY, true);
  406. }
  407. @curl_setopt_array($ch, $curl_options);
  408. self::configCurlCertificate($ch);
  409. /*
  410. * as of php 5.2.0, CURLOPT_FOLLOWLOCATION can't be set if
  411. * in safe_mode or open_basedir is set
  412. */
  413. if ((string)ini_get('safe_mode') == '' && ini_get('open_basedir') == '') {
  414. $curl_options = array(
  415. // curl options (sorted oldest to newest)
  416. CURLOPT_FOLLOWLOCATION => true,
  417. CURLOPT_MAXREDIRS => 5,
  418. );
  419. @curl_setopt_array($ch, $curl_options);
  420. }
  421. if (is_resource($file)) {
  422. // write output directly to file
  423. @curl_setopt($ch, CURLOPT_FILE, $file);
  424. } else {
  425. // internal to ext/curl
  426. @curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  427. }
  428. ob_start();
  429. $response = @curl_exec($ch);
  430. ob_end_clean();
  431. if ($response === true) {
  432. $response = '';
  433. } else if ($response === false) {
  434. $errstr = curl_error($ch);
  435. if ($errstr != '') {
  436. throw new Exception('curl_exec: ' . $errstr
  437. . '. Hostname requested was: ' . UrlHelper::getHostFromUrl($aUrl));
  438. }
  439. $response = '';
  440. } else {
  441. $header = '';
  442. // redirects are included in the output html, so we look for the last line that starts w/ HTTP/...
  443. // to split the response
  444. while (substr($response, 0, 5) == "HTTP/") {
  445. list($header, $response) = explode("\r\n\r\n", $response, 2);
  446. }
  447. foreach (explode("\r\n", $header) as $line) {
  448. self::parseHeaderLine($headers, $line);
  449. }
  450. }
  451. $contentLength = @curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD);
  452. $fileLength = is_resource($file) ? @curl_getinfo($ch, CURLINFO_SIZE_DOWNLOAD) : strlen($response);
  453. $status = @curl_getinfo($ch, CURLINFO_HTTP_CODE);
  454. @curl_close($ch);
  455. unset($ch);
  456. } else {
  457. throw new Exception('Invalid request method: ' . $method);
  458. }
  459. if (is_resource($file)) {
  460. fflush($file);
  461. @fclose($file);
  462. $fileSize = filesize($destinationPath);
  463. if ((($contentLength > 0) && ($fileLength != $contentLength))
  464. || ($fileSize != $fileLength)
  465. ) {
  466. throw new Exception('File size error: ' . $destinationPath . '; expected ' . $contentLength . ' bytes; received ' . $fileLength . ' bytes; saved ' . $fileSize . ' bytes to file');
  467. }
  468. return true;
  469. }
  470. if (!$getExtendedInfo) {
  471. return trim($response);
  472. } else {
  473. return array(
  474. 'status' => $status,
  475. 'headers' => $headers,
  476. 'data' => $response
  477. );
  478. }
  479. }
  480. /**
  481. * Downloads the next chunk of a specific file. The next chunk's byte range
  482. * is determined by the existing file's size and the expected file size, which
  483. * is stored in the piwik_option table before starting a download. The expected
  484. * file size is obtained through a `HEAD` HTTP request.
  485. *
  486. * _Note: this function uses the **Range** HTTP header to accomplish downloading in
  487. * parts. Not every server supports this header._
  488. *
  489. * The proper use of this function is to call it once per request. The browser
  490. * should continue to send requests to Piwik which will in turn call this method
  491. * until the file has completely downloaded. In this way, the user can be informed
  492. * of a download's progress.
  493. *
  494. * **Example Usage**
  495. *
  496. * ```
  497. * // browser JavaScript
  498. * var downloadFile = function (isStart) {
  499. * var ajax = new ajaxHelper();
  500. * ajax.addParams({
  501. * module: 'MyPlugin',
  502. * action: 'myAction',
  503. * isStart: isStart ? 1 : 0
  504. * }, 'post');
  505. * ajax.setCallback(function (response) {
  506. * var progress = response.progress
  507. * // ...update progress...
  508. *
  509. * downloadFile(false);
  510. * });
  511. * ajax.send();
  512. * }
  513. *
  514. * downloadFile(true);
  515. * ```
  516. *
  517. * ```
  518. * // PHP controller action
  519. * public function myAction()
  520. * {
  521. * $outputPath = PIWIK_INCLUDE_PATH . '/tmp/averybigfile.zip';
  522. * $isStart = Common::getRequestVar('isStart', 1, 'int');
  523. * Http::downloadChunk("http://bigfiles.com/averybigfile.zip", $outputPath, $isStart == 1);
  524. * }
  525. * ```
  526. *
  527. * @param string $url The url to download from.
  528. * @param string $outputPath The path to the file to save/append to.
  529. * @param bool $isContinuation `true` if this is the continuation of a download,
  530. * or if we're starting a fresh one.
  531. * @throws Exception if the file already exists and we're starting a new download,
  532. * if we're trying to continue a download that never started
  533. * @return array
  534. * @api
  535. */
  536. public static function downloadChunk($url, $outputPath, $isContinuation)
  537. {
  538. // make sure file doesn't already exist if we're starting a new download
  539. if (!$isContinuation
  540. && file_exists($outputPath)
  541. ) {
  542. throw new Exception(
  543. Piwik::translate('General_DownloadFail_FileExists', "'" . $outputPath . "'")
  544. . ' ' . Piwik::translate('General_DownloadPleaseRemoveExisting'));
  545. }
  546. // if we're starting a download, get the expected file size & save as an option
  547. $downloadOption = $outputPath . '_expectedDownloadSize';
  548. if (!$isContinuation) {
  549. $expectedFileSizeResult = Http::sendHttpRequest(
  550. $url,
  551. $timeout = 300,
  552. $userAgent = null,
  553. $destinationPath = null,
  554. $followDepth = 0,
  555. $acceptLanguage = false,
  556. $byteRange = false,
  557. $getExtendedInfo = true,
  558. $httpMethod = 'HEAD'
  559. );
  560. $expectedFileSize = 0;
  561. if (isset($expectedFileSizeResult['headers']['Content-Length'])) {
  562. $expectedFileSize = (int)$expectedFileSizeResult['headers']['Content-Length'];
  563. }
  564. if ($expectedFileSize == 0) {
  565. Log::info("HEAD request for '%s' failed, got following: %s", $url, print_r($expectedFileSizeResult, true));
  566. throw new Exception(Piwik::translate('General_DownloadFail_HttpRequestFail'));
  567. }
  568. Option::set($downloadOption, $expectedFileSize);
  569. } else {
  570. $expectedFileSize = (int)Option::get($downloadOption);
  571. if ($expectedFileSize === false) { // sanity check
  572. throw new Exception("Trying to continue a download that never started?! That's not supposed to happen...");
  573. }
  574. }
  575. // if existing file is already big enough, then fail so we don't accidentally overwrite
  576. // existing DB
  577. $existingSize = file_exists($outputPath) ? filesize($outputPath) : 0;
  578. if ($existingSize >= $expectedFileSize) {
  579. throw new Exception(
  580. Piwik::translate('General_DownloadFail_FileExistsContinue', "'" . $outputPath . "'")
  581. . ' ' . Piwik::translate('General_DownloadPleaseRemoveExisting'));
  582. }
  583. // download a chunk of the file
  584. $result = Http::sendHttpRequest(
  585. $url,
  586. $timeout = 300,
  587. $userAgent = null,
  588. $destinationPath = null,
  589. $followDepth = 0,
  590. $acceptLanguage = false,
  591. $byteRange = array($existingSize, min($existingSize + 1024 * 1024 - 1, $expectedFileSize)),
  592. $getExtendedInfo = true
  593. );
  594. if ($result === false
  595. || $result['status'] < 200
  596. || $result['status'] > 299
  597. ) {
  598. $result['data'] = self::truncateStr($result['data'], 1024);
  599. Log::info("Failed to download range '%s-%s' of file from url '%s'. Got result: %s",
  600. $byteRange[0], $byteRange[1], $url, print_r($result, true));
  601. throw new Exception(Piwik::translate('General_DownloadFail_HttpRequestFail'));
  602. }
  603. // write chunk to file
  604. $f = fopen($outputPath, 'ab');
  605. fwrite($f, $result['data']);
  606. fclose($f);
  607. clearstatcache($clear_realpath_cache = true, $outputPath);
  608. return array(
  609. 'current_size' => filesize($outputPath),
  610. 'expected_file_size' => $expectedFileSize,
  611. );
  612. }
  613. /**
  614. * Will configure CURL handle $ch
  615. * to use local list of Certificate Authorities,
  616. */
  617. public static function configCurlCertificate(&$ch)
  618. {
  619. if (file_exists(PIWIK_INCLUDE_PATH . '/core/DataFiles/cacert.pem')) {
  620. @curl_setopt($ch, CURLOPT_CAINFO, PIWIK_INCLUDE_PATH . '/core/DataFiles/cacert.pem');
  621. }
  622. }
  623. public static function getUserAgent()
  624. {
  625. return !empty($_SERVER['HTTP_USER_AGENT'])
  626. ? $_SERVER['HTTP_USER_AGENT']
  627. : 'Piwik/' . Version::VERSION;
  628. }
  629. /**
  630. * Fetches a file located at `$url` and saves it to `$destinationPath`.
  631. *
  632. * @param string $url The URL of the file to download.
  633. * @param string $destinationPath The path to download the file to.
  634. * @param int $tries (deprecated)
  635. * @param int $timeout The amount of seconds to wait before aborting the HTTP request.
  636. * @throws Exception if the response cannot be saved to `$destinationPath`, if the HTTP response cannot be sent,
  637. * if there are more than 5 redirects or if the request times out.
  638. * @return bool `true` on success, throws Exception on failure
  639. * @api
  640. */
  641. public static function fetchRemoteFile($url, $destinationPath = null, $tries = 0, $timeout = 10)
  642. {
  643. @ignore_user_abort(true);
  644. SettingsServer::setMaxExecutionTime(0);
  645. return self::sendHttpRequest($url, $timeout, 'Update', $destinationPath);
  646. }
  647. /**
  648. * Utility function, parses an HTTP header line into key/value & sets header
  649. * array with them.
  650. *
  651. * @param array $headers
  652. * @param string $line
  653. */
  654. private static function parseHeaderLine(&$headers, $line)
  655. {
  656. $parts = explode(':', $line, 2);
  657. if (count($parts) == 1) {
  658. return;
  659. }
  660. list($name, $value) = $parts;
  661. $headers[trim($name)] = trim($value);
  662. }
  663. /**
  664. * Utility function that truncates a string to an arbitrary limit.
  665. *
  666. * @param string $str The string to truncate.
  667. * @param int $limit The maximum length of the truncated string.
  668. * @return string
  669. */
  670. private static function truncateStr($str, $limit)
  671. {
  672. if (strlen($str) > $limit) {
  673. return substr($str, 0, $limit) . '...';
  674. }
  675. return $str;
  676. }
  677. }