Teknik is a suite of services with attractive and functional interfaces. https://www.teknik.io/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Http.php 30KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777
  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. $aUrl = trim($aUrl);
  152. // other result data
  153. $status = null;
  154. $headers = array();
  155. if ($method == 'socket') {
  156. if (!self::isSocketEnabled()) {
  157. // can be triggered in tests
  158. throw new Exception("HTTP socket support is not enabled (php function fsockopen is not available) ");
  159. }
  160. // initialization
  161. $url = @parse_url($aUrl);
  162. if ($url === false || !isset($url['scheme'])) {
  163. throw new Exception('Malformed URL: ' . $aUrl);
  164. }
  165. if ($url['scheme'] != 'http') {
  166. throw new Exception('Invalid protocol/scheme: ' . $url['scheme']);
  167. }
  168. $host = $url['host'];
  169. $port = isset($url['port)']) ? $url['port'] : 80;
  170. $path = isset($url['path']) ? $url['path'] : '/';
  171. if (isset($url['query'])) {
  172. $path .= '?' . $url['query'];
  173. }
  174. $errno = null;
  175. $errstr = null;
  176. if ((!empty($proxyHost) && !empty($proxyPort))
  177. || !empty($byteRange)
  178. ) {
  179. $httpVer = '1.1';
  180. } else {
  181. $httpVer = '1.0';
  182. }
  183. $proxyAuth = null;
  184. if (!empty($proxyHost) && !empty($proxyPort)) {
  185. $connectHost = $proxyHost;
  186. $connectPort = $proxyPort;
  187. if (!empty($proxyUser) && !empty($proxyPassword)) {
  188. $proxyAuth = 'Proxy-Authorization: Basic ' . base64_encode("$proxyUser:$proxyPassword") . "\r\n";
  189. }
  190. $requestHeader = "$httpMethod $aUrl HTTP/$httpVer\r\n";
  191. } else {
  192. $connectHost = $host;
  193. $connectPort = $port;
  194. $requestHeader = "$httpMethod $path HTTP/$httpVer\r\n";
  195. }
  196. // connection attempt
  197. if (($fsock = @fsockopen($connectHost, $connectPort, $errno, $errstr, $timeout)) === false || !is_resource($fsock)) {
  198. if (is_resource($file)) {
  199. @fclose($file);
  200. }
  201. throw new Exception("Error while connecting to: $host. Please try again later. $errstr");
  202. }
  203. // send HTTP request header
  204. $requestHeader .=
  205. "Host: $host" . ($port != 80 ? ':' . $port : '') . "\r\n"
  206. . ($proxyAuth ? $proxyAuth : '')
  207. . 'User-Agent: ' . $userAgent . "\r\n"
  208. . ($acceptLanguage ? $acceptLanguage . "\r\n" : '')
  209. . $xff . "\r\n"
  210. . $via . "\r\n"
  211. . $rangeHeader
  212. . "Connection: close\r\n"
  213. . "\r\n";
  214. fwrite($fsock, $requestHeader);
  215. $streamMetaData = array('timed_out' => false);
  216. @stream_set_blocking($fsock, true);
  217. if (function_exists('stream_set_timeout')) {
  218. @stream_set_timeout($fsock, $timeout);
  219. } elseif (function_exists('socket_set_timeout')) {
  220. @socket_set_timeout($fsock, $timeout);
  221. }
  222. // process header
  223. $status = null;
  224. while (!feof($fsock)) {
  225. $line = fgets($fsock, 4096);
  226. $streamMetaData = @stream_get_meta_data($fsock);
  227. if ($streamMetaData['timed_out']) {
  228. if (is_resource($file)) {
  229. @fclose($file);
  230. }
  231. @fclose($fsock);
  232. throw new Exception('Timed out waiting for server response');
  233. }
  234. // a blank line marks the end of the server response header
  235. if (rtrim($line, "\r\n") == '') {
  236. break;
  237. }
  238. // parse first line of server response header
  239. if (!$status) {
  240. // expect first line to be HTTP response status line, e.g., HTTP/1.1 200 OK
  241. if (!preg_match('~^HTTP/(\d\.\d)\s+(\d+)(\s*.*)?~', $line, $m)) {
  242. if (is_resource($file)) {
  243. @fclose($file);
  244. }
  245. @fclose($fsock);
  246. throw new Exception('Expected server response code. Got ' . rtrim($line, "\r\n"));
  247. }
  248. $status = (integer)$m[2];
  249. // Informational 1xx or Client Error 4xx
  250. if ($status < 200 || $status >= 400) {
  251. if (is_resource($file)) {
  252. @fclose($file);
  253. }
  254. @fclose($fsock);
  255. if (!$getExtendedInfo) {
  256. return false;
  257. } else {
  258. return array('status' => $status);
  259. }
  260. }
  261. continue;
  262. }
  263. // handle redirect
  264. if (preg_match('/^Location:\s*(.+)/', rtrim($line, "\r\n"), $m)) {
  265. if (is_resource($file)) {
  266. @fclose($file);
  267. }
  268. @fclose($fsock);
  269. // Successful 2xx vs Redirect 3xx
  270. if ($status < 300) {
  271. throw new Exception('Unexpected redirect to Location: ' . rtrim($line) . ' for status code ' . $status);
  272. }
  273. return self::sendHttpRequestBy(
  274. $method,
  275. trim($m[1]),
  276. $timeout,
  277. $userAgent,
  278. $destinationPath,
  279. $file,
  280. $followDepth + 1,
  281. $acceptLanguage,
  282. $acceptInvalidSslCertificate = false,
  283. $byteRange,
  284. $getExtendedInfo,
  285. $httpMethod
  286. );
  287. }
  288. // save expected content length for later verification
  289. if (preg_match('/^Content-Length:\s*(\d+)/', $line, $m)) {
  290. $contentLength = (integer)$m[1];
  291. }
  292. self::parseHeaderLine($headers, $line);
  293. }
  294. if (feof($fsock)
  295. && $httpMethod != 'HEAD'
  296. ) {
  297. throw new Exception('Unexpected end of transmission');
  298. }
  299. // process content/body
  300. $response = '';
  301. while (!feof($fsock)) {
  302. $line = fread($fsock, 8192);
  303. $streamMetaData = @stream_get_meta_data($fsock);
  304. if ($streamMetaData['timed_out']) {
  305. if (is_resource($file)) {
  306. @fclose($file);
  307. }
  308. @fclose($fsock);
  309. throw new Exception('Timed out waiting for server response');
  310. }
  311. $fileLength += strlen($line);
  312. if (is_resource($file)) {
  313. // save to file
  314. fwrite($file, $line);
  315. } else {
  316. // concatenate to response string
  317. $response .= $line;
  318. }
  319. }
  320. // determine success or failure
  321. @fclose(@$fsock);
  322. } else if ($method == 'fopen') {
  323. $response = false;
  324. // we make sure the request takes less than a few seconds to fail
  325. // we create a stream_context (works in php >= 5.2.1)
  326. // we also set the socket_timeout (for php < 5.2.1)
  327. $default_socket_timeout = @ini_get('default_socket_timeout');
  328. @ini_set('default_socket_timeout', $timeout);
  329. $ctx = null;
  330. if (function_exists('stream_context_create')) {
  331. $stream_options = array(
  332. 'http' => array(
  333. 'header' => 'User-Agent: ' . $userAgent . "\r\n"
  334. . ($acceptLanguage ? $acceptLanguage . "\r\n" : '')
  335. . $xff . "\r\n"
  336. . $via . "\r\n"
  337. . $rangeHeader,
  338. 'max_redirects' => 5, // PHP 5.1.0
  339. 'timeout' => $timeout, // PHP 5.2.1
  340. )
  341. );
  342. if (!empty($proxyHost) && !empty($proxyPort)) {
  343. $stream_options['http']['proxy'] = 'tcp://' . $proxyHost . ':' . $proxyPort;
  344. $stream_options['http']['request_fulluri'] = true; // required by squid proxy
  345. if (!empty($proxyUser) && !empty($proxyPassword)) {
  346. $stream_options['http']['header'] .= 'Proxy-Authorization: Basic ' . base64_encode("$proxyUser:$proxyPassword") . "\r\n";
  347. }
  348. }
  349. $ctx = stream_context_create($stream_options);
  350. }
  351. // save to file
  352. if (is_resource($file)) {
  353. $handle = fopen($aUrl, 'rb', false, $ctx);
  354. while (!feof($handle)) {
  355. $response = fread($handle, 8192);
  356. $fileLength += strlen($response);
  357. fwrite($file, $response);
  358. }
  359. fclose($handle);
  360. } else {
  361. $response = file_get_contents($aUrl, 0, $ctx);
  362. $fileLength = strlen($response);
  363. }
  364. // restore the socket_timeout value
  365. if (!empty($default_socket_timeout)) {
  366. @ini_set('default_socket_timeout', $default_socket_timeout);
  367. }
  368. } else if ($method == 'curl') {
  369. if (!self::isCurlEnabled()) {
  370. // can be triggered in tests
  371. throw new Exception("CURL is not enabled in php.ini, but is being used.");
  372. }
  373. $ch = @curl_init();
  374. if (!empty($proxyHost) && !empty($proxyPort)) {
  375. @curl_setopt($ch, CURLOPT_PROXY, $proxyHost . ':' . $proxyPort);
  376. if (!empty($proxyUser) && !empty($proxyPassword)) {
  377. // PROXYAUTH defaults to BASIC
  378. @curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxyUser . ':' . $proxyPassword);
  379. }
  380. }
  381. $curl_options = array(
  382. // internal to ext/curl
  383. CURLOPT_BINARYTRANSFER => is_resource($file),
  384. // curl options (sorted oldest to newest)
  385. CURLOPT_URL => $aUrl,
  386. CURLOPT_USERAGENT => $userAgent,
  387. CURLOPT_HTTPHEADER => array(
  388. $xff,
  389. $via,
  390. $rangeHeader,
  391. $acceptLanguage
  392. ),
  393. // only get header info if not saving directly to file
  394. CURLOPT_HEADER => is_resource($file) ? false : true,
  395. CURLOPT_CONNECTTIMEOUT => $timeout,
  396. );
  397. // Case core:archive command is triggering archiving on https:// and the certificate is not valid
  398. if ($acceptInvalidSslCertificate) {
  399. $curl_options += array(
  400. CURLOPT_SSL_VERIFYHOST => false,
  401. CURLOPT_SSL_VERIFYPEER => false,
  402. );
  403. }
  404. @curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $httpMethod);
  405. if ($httpMethod == 'HEAD') {
  406. @curl_setopt($ch, CURLOPT_NOBODY, true);
  407. }
  408. @curl_setopt_array($ch, $curl_options);
  409. self::configCurlCertificate($ch);
  410. /*
  411. * as of php 5.2.0, CURLOPT_FOLLOWLOCATION can't be set if
  412. * in safe_mode or open_basedir is set
  413. */
  414. if ((string)ini_get('safe_mode') == '' && ini_get('open_basedir') == '') {
  415. $curl_options = array(
  416. // curl options (sorted oldest to newest)
  417. CURLOPT_FOLLOWLOCATION => true,
  418. CURLOPT_MAXREDIRS => 5,
  419. );
  420. @curl_setopt_array($ch, $curl_options);
  421. }
  422. if (is_resource($file)) {
  423. // write output directly to file
  424. @curl_setopt($ch, CURLOPT_FILE, $file);
  425. } else {
  426. // internal to ext/curl
  427. @curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  428. }
  429. ob_start();
  430. $response = @curl_exec($ch);
  431. ob_end_clean();
  432. if ($response === true) {
  433. $response = '';
  434. } else if ($response === false) {
  435. $errstr = curl_error($ch);
  436. if ($errstr != '') {
  437. throw new Exception('curl_exec: ' . $errstr
  438. . '. Hostname requested was: ' . UrlHelper::getHostFromUrl($aUrl));
  439. }
  440. $response = '';
  441. } else {
  442. $header = '';
  443. // redirects are included in the output html, so we look for the last line that starts w/ HTTP/...
  444. // to split the response
  445. while (substr($response, 0, 5) == "HTTP/") {
  446. list($header, $response) = explode("\r\n\r\n", $response, 2);
  447. }
  448. foreach (explode("\r\n", $header) as $line) {
  449. self::parseHeaderLine($headers, $line);
  450. }
  451. }
  452. $contentLength = @curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD);
  453. $fileLength = is_resource($file) ? @curl_getinfo($ch, CURLINFO_SIZE_DOWNLOAD) : strlen($response);
  454. $status = @curl_getinfo($ch, CURLINFO_HTTP_CODE);
  455. @curl_close($ch);
  456. unset($ch);
  457. } else {
  458. throw new Exception('Invalid request method: ' . $method);
  459. }
  460. if (is_resource($file)) {
  461. fflush($file);
  462. @fclose($file);
  463. $fileSize = filesize($destinationPath);
  464. if ((($contentLength > 0) && ($fileLength != $contentLength))
  465. || ($fileSize != $fileLength)
  466. ) {
  467. throw new Exception('File size error: ' . $destinationPath . '; expected ' . $contentLength . ' bytes; received ' . $fileLength . ' bytes; saved ' . $fileSize . ' bytes to file');
  468. }
  469. return true;
  470. }
  471. if (!$getExtendedInfo) {
  472. return trim($response);
  473. } else {
  474. return array(
  475. 'status' => $status,
  476. 'headers' => $headers,
  477. 'data' => $response
  478. );
  479. }
  480. }
  481. /**
  482. * Downloads the next chunk of a specific file. The next chunk's byte range
  483. * is determined by the existing file's size and the expected file size, which
  484. * is stored in the piwik_option table before starting a download. The expected
  485. * file size is obtained through a `HEAD` HTTP request.
  486. *
  487. * _Note: this function uses the **Range** HTTP header to accomplish downloading in
  488. * parts. Not every server supports this header._
  489. *
  490. * The proper use of this function is to call it once per request. The browser
  491. * should continue to send requests to Piwik which will in turn call this method
  492. * until the file has completely downloaded. In this way, the user can be informed
  493. * of a download's progress.
  494. *
  495. * **Example Usage**
  496. *
  497. * ```
  498. * // browser JavaScript
  499. * var downloadFile = function (isStart) {
  500. * var ajax = new ajaxHelper();
  501. * ajax.addParams({
  502. * module: 'MyPlugin',
  503. * action: 'myAction',
  504. * isStart: isStart ? 1 : 0
  505. * }, 'post');
  506. * ajax.setCallback(function (response) {
  507. * var progress = response.progress
  508. * // ...update progress...
  509. *
  510. * downloadFile(false);
  511. * });
  512. * ajax.send();
  513. * }
  514. *
  515. * downloadFile(true);
  516. * ```
  517. *
  518. * ```
  519. * // PHP controller action
  520. * public function myAction()
  521. * {
  522. * $outputPath = PIWIK_INCLUDE_PATH . '/tmp/averybigfile.zip';
  523. * $isStart = Common::getRequestVar('isStart', 1, 'int');
  524. * Http::downloadChunk("http://bigfiles.com/averybigfile.zip", $outputPath, $isStart == 1);
  525. * }
  526. * ```
  527. *
  528. * @param string $url The url to download from.
  529. * @param string $outputPath The path to the file to save/append to.
  530. * @param bool $isContinuation `true` if this is the continuation of a download,
  531. * or if we're starting a fresh one.
  532. * @throws Exception if the file already exists and we're starting a new download,
  533. * if we're trying to continue a download that never started
  534. * @return array
  535. * @api
  536. */
  537. public static function downloadChunk($url, $outputPath, $isContinuation)
  538. {
  539. // make sure file doesn't already exist if we're starting a new download
  540. if (!$isContinuation
  541. && file_exists($outputPath)
  542. ) {
  543. throw new Exception(
  544. Piwik::translate('General_DownloadFail_FileExists', "'" . $outputPath . "'")
  545. . ' ' . Piwik::translate('General_DownloadPleaseRemoveExisting'));
  546. }
  547. // if we're starting a download, get the expected file size & save as an option
  548. $downloadOption = $outputPath . '_expectedDownloadSize';
  549. if (!$isContinuation) {
  550. $expectedFileSizeResult = Http::sendHttpRequest(
  551. $url,
  552. $timeout = 300,
  553. $userAgent = null,
  554. $destinationPath = null,
  555. $followDepth = 0,
  556. $acceptLanguage = false,
  557. $byteRange = false,
  558. $getExtendedInfo = true,
  559. $httpMethod = 'HEAD'
  560. );
  561. $expectedFileSize = 0;
  562. if (isset($expectedFileSizeResult['headers']['Content-Length'])) {
  563. $expectedFileSize = (int)$expectedFileSizeResult['headers']['Content-Length'];
  564. }
  565. if ($expectedFileSize == 0) {
  566. Log::info("HEAD request for '%s' failed, got following: %s", $url, print_r($expectedFileSizeResult, true));
  567. throw new Exception(Piwik::translate('General_DownloadFail_HttpRequestFail'));
  568. }
  569. Option::set($downloadOption, $expectedFileSize);
  570. } else {
  571. $expectedFileSize = (int)Option::get($downloadOption);
  572. if ($expectedFileSize === false) { // sanity check
  573. throw new Exception("Trying to continue a download that never started?! That's not supposed to happen...");
  574. }
  575. }
  576. // if existing file is already big enough, then fail so we don't accidentally overwrite
  577. // existing DB
  578. $existingSize = file_exists($outputPath) ? filesize($outputPath) : 0;
  579. if ($existingSize >= $expectedFileSize) {
  580. throw new Exception(
  581. Piwik::translate('General_DownloadFail_FileExistsContinue', "'" . $outputPath . "'")
  582. . ' ' . Piwik::translate('General_DownloadPleaseRemoveExisting'));
  583. }
  584. // download a chunk of the file
  585. $result = Http::sendHttpRequest(
  586. $url,
  587. $timeout = 300,
  588. $userAgent = null,
  589. $destinationPath = null,
  590. $followDepth = 0,
  591. $acceptLanguage = false,
  592. $byteRange = array($existingSize, min($existingSize + 1024 * 1024 - 1, $expectedFileSize)),
  593. $getExtendedInfo = true
  594. );
  595. if ($result === false
  596. || $result['status'] < 200
  597. || $result['status'] > 299
  598. ) {
  599. $result['data'] = self::truncateStr($result['data'], 1024);
  600. Log::info("Failed to download range '%s-%s' of file from url '%s'. Got result: %s",
  601. $byteRange[0], $byteRange[1], $url, print_r($result, true));
  602. throw new Exception(Piwik::translate('General_DownloadFail_HttpRequestFail'));
  603. }
  604. // write chunk to file
  605. $f = fopen($outputPath, 'ab');
  606. fwrite($f, $result['data']);
  607. fclose($f);
  608. clearstatcache($clear_realpath_cache = true, $outputPath);
  609. return array(
  610. 'current_size' => filesize($outputPath),
  611. 'expected_file_size' => $expectedFileSize,
  612. );
  613. }
  614. /**
  615. * Will configure CURL handle $ch
  616. * to use local list of Certificate Authorities,
  617. */
  618. public static function configCurlCertificate(&$ch)
  619. {
  620. if (file_exists(PIWIK_INCLUDE_PATH . '/core/DataFiles/cacert.pem')) {
  621. @curl_setopt($ch, CURLOPT_CAINFO, PIWIK_INCLUDE_PATH . '/core/DataFiles/cacert.pem');
  622. }
  623. }
  624. public static function getUserAgent()
  625. {
  626. return !empty($_SERVER['HTTP_USER_AGENT'])
  627. ? $_SERVER['HTTP_USER_AGENT']
  628. : 'Piwik/' . Version::VERSION;
  629. }
  630. /**
  631. * Fetches a file located at `$url` and saves it to `$destinationPath`.
  632. *
  633. * @param string $url The URL of the file to download.
  634. * @param string $destinationPath The path to download the file to.
  635. * @param int $tries (deprecated)
  636. * @param int $timeout The amount of seconds to wait before aborting the HTTP request.
  637. * @throws Exception if the response cannot be saved to `$destinationPath`, if the HTTP response cannot be sent,
  638. * if there are more than 5 redirects or if the request times out.
  639. * @return bool `true` on success, throws Exception on failure
  640. * @api
  641. */
  642. public static function fetchRemoteFile($url, $destinationPath = null, $tries = 0, $timeout = 10)
  643. {
  644. @ignore_user_abort(true);
  645. SettingsServer::setMaxExecutionTime(0);
  646. return self::sendHttpRequest($url, $timeout, 'Update', $destinationPath);
  647. }
  648. /**
  649. * Utility function, parses an HTTP header line into key/value & sets header
  650. * array with them.
  651. *
  652. * @param array $headers
  653. * @param string $line
  654. */
  655. private static function parseHeaderLine(&$headers, $line)
  656. {
  657. $parts = explode(':', $line, 2);
  658. if (count($parts) == 1) {
  659. return;
  660. }
  661. list($name, $value) = $parts;
  662. $headers[trim($name)] = trim($value);
  663. }
  664. /**
  665. * Utility function that truncates a string to an arbitrary limit.
  666. *
  667. * @param string $str The string to truncate.
  668. * @param int $limit The maximum length of the truncated string.
  669. * @return string
  670. */
  671. private static function truncateStr($str, $limit)
  672. {
  673. if (strlen($str) > $limit) {
  674. return substr($str, 0, $limit) . '...';
  675. }
  676. return $str;
  677. }
  678. /**
  679. * Returns the If-Modified-Since HTTP header if it can be found. If it cannot be
  680. * found, an empty string is returned.
  681. *
  682. * @return string
  683. */
  684. public static function getModifiedSinceHeader()
  685. {
  686. $modifiedSince = '';
  687. if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
  688. $modifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
  689. // strip any trailing data appended to header
  690. if (false !== ($semicolonPos = strpos($modifiedSince, ';'))) {
  691. $modifiedSince = substr($modifiedSince, 0, $semicolonPos);
  692. }
  693. }
  694. return $modifiedSince;
  695. }
  696. }