Response.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. <?php
  2. /**
  3. * @package Grav\Common\GPM
  4. *
  5. * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Common\GPM;
  9. use Grav\Common\Utils;
  10. use Grav\Common\Grav;
  11. class Response
  12. {
  13. /**
  14. * The callback for the progress
  15. *
  16. * @var callable Either a function or callback in array notation
  17. */
  18. public static $callback = null;
  19. /**
  20. * Which method to use for HTTP calls, can be 'curl', 'fopen' or 'auto'. Auto is default and fopen is the preferred method
  21. *
  22. * @var string
  23. */
  24. private static $method = 'auto';
  25. /**
  26. * Default parameters for `curl` and `fopen`
  27. *
  28. * @var array
  29. */
  30. private static $defaults = [
  31. 'curl' => [
  32. CURLOPT_USERAGENT => 'Grav GPM',
  33. CURLOPT_RETURNTRANSFER => true,
  34. CURLOPT_FOLLOWLOCATION => true,
  35. CURLOPT_FAILONERROR => true,
  36. CURLOPT_TIMEOUT => 15,
  37. CURLOPT_HEADER => false,
  38. //CURLOPT_SSL_VERIFYPEER => true, // this is set in the constructor since it's a setting
  39. /**
  40. * Example of callback parameters from within your own class
  41. */
  42. //CURLOPT_NOPROGRESS => false,
  43. //CURLOPT_PROGRESSFUNCTION => [$this, 'progress']
  44. ],
  45. 'fopen' => [
  46. 'method' => 'GET',
  47. 'user_agent' => 'Grav GPM',
  48. 'max_redirects' => 5,
  49. 'follow_location' => 1,
  50. 'timeout' => 15,
  51. /* // this is set in the constructor since it's a setting
  52. 'ssl' => [
  53. 'verify_peer' => true,
  54. 'verify_peer_name' => true,
  55. ],
  56. */
  57. /**
  58. * Example of callback parameters from within your own class
  59. */
  60. //'notification' => [$this, 'progress']
  61. ]
  62. ];
  63. /**
  64. * Sets the preferred method to use for making HTTP calls.
  65. *
  66. * @param string $method Default is `auto`
  67. *
  68. * @return Response
  69. */
  70. public static function setMethod($method = 'auto')
  71. {
  72. if (!\in_array($method, ['auto', 'curl', 'fopen'], true)) {
  73. $method = 'auto';
  74. }
  75. self::$method = $method;
  76. return new self();
  77. }
  78. /**
  79. * Makes a request to the URL by using the preferred method
  80. *
  81. * @param string $uri URL to call
  82. * @param array $options An array of parameters for both `curl` and `fopen`
  83. * @param callable $callback Either a function or callback in array notation
  84. *
  85. * @return string The response of the request
  86. */
  87. public static function get($uri = '', $options = [], $callback = null)
  88. {
  89. if (!self::isCurlAvailable() && !self::isFopenAvailable()) {
  90. throw new \RuntimeException('Could not start an HTTP request. `allow_url_open` is disabled and `cURL` is not available');
  91. }
  92. // check if this function is available, if so use it to stop any timeouts
  93. try {
  94. if (function_exists('set_time_limit') && !Utils::isFunctionDisabled('set_time_limit')) {
  95. set_time_limit(0);
  96. }
  97. } catch (\Exception $e) {
  98. }
  99. $config = Grav::instance()['config'];
  100. $overrides = [];
  101. // Override CA Bundle
  102. $caPathOrFile = \Composer\CaBundle\CaBundle::getSystemCaRootBundlePath();
  103. if (is_dir($caPathOrFile) || (is_link($caPathOrFile) && is_dir(readlink($caPathOrFile)))) {
  104. $overrides['curl'][CURLOPT_CAPATH] = $caPathOrFile;
  105. $overrides['fopen']['ssl']['capath'] = $caPathOrFile;
  106. } else {
  107. $overrides['curl'][CURLOPT_CAINFO] = $caPathOrFile;
  108. $overrides['fopen']['ssl']['cafile'] = $caPathOrFile;
  109. }
  110. // SSL Verify Peer and Proxy Setting
  111. $settings = [
  112. 'method' => $config->get('system.gpm.method', self::$method),
  113. 'verify_peer' => $config->get('system.gpm.verify_peer', true),
  114. // `system.proxy_url` is for fallback
  115. // introduced with 1.1.0-beta.1 probably safe to remove at some point
  116. 'proxy_url' => $config->get('system.gpm.proxy_url', $config->get('system.proxy_url', false)),
  117. ];
  118. if (!$settings['verify_peer']) {
  119. $overrides = array_replace_recursive([], $overrides, [
  120. 'curl' => [
  121. CURLOPT_SSL_VERIFYPEER => $settings['verify_peer']
  122. ],
  123. 'fopen' => [
  124. 'ssl' => [
  125. 'verify_peer' => $settings['verify_peer'],
  126. 'verify_peer_name' => $settings['verify_peer'],
  127. ]
  128. ]
  129. ]);
  130. }
  131. // Proxy Setting
  132. if ($settings['proxy_url']) {
  133. $proxy = parse_url($settings['proxy_url']);
  134. $fopen_proxy = ($proxy['scheme'] ?: 'http') . '://' . $proxy['host'] . (isset($proxy['port']) ? ':' . $proxy['port'] : '');
  135. $overrides = array_replace_recursive([], $overrides, [
  136. 'curl' => [
  137. CURLOPT_PROXY => $proxy['host'],
  138. CURLOPT_PROXYTYPE => 'HTTP'
  139. ],
  140. 'fopen' => [
  141. 'proxy' => $fopen_proxy,
  142. 'request_fulluri' => true
  143. ]
  144. ]);
  145. if (isset($proxy['port'])) {
  146. $overrides['curl'][CURLOPT_PROXYPORT] = $proxy['port'];
  147. }
  148. if (isset($proxy['user'], $proxy['pass'])) {
  149. $fopen_auth = $auth = base64_encode($proxy['user'] . ':' . $proxy['pass']);
  150. $overrides['curl'][CURLOPT_PROXYUSERPWD] = $proxy['user'] . ':' . $proxy['pass'];
  151. $overrides['fopen']['header'] = "Proxy-Authorization: Basic $fopen_auth";
  152. }
  153. }
  154. $options = array_replace_recursive(self::$defaults, $options, $overrides);
  155. $method = 'get' . ucfirst(strtolower($settings['method']));
  156. self::$callback = $callback;
  157. return static::$method($uri, $options, $callback);
  158. }
  159. /**
  160. * Checks if cURL is available
  161. *
  162. * @return bool
  163. */
  164. public static function isCurlAvailable()
  165. {
  166. return function_exists('curl_version');
  167. }
  168. /**
  169. * Checks if the remote fopen request is enabled in PHP
  170. *
  171. * @return bool
  172. */
  173. public static function isFopenAvailable()
  174. {
  175. return preg_match('/1|yes|on|true/i', ini_get('allow_url_fopen'));
  176. }
  177. /**
  178. * Is this a remote file or not
  179. *
  180. * @param string $file
  181. * @return bool
  182. */
  183. public static function isRemote($file)
  184. {
  185. return (bool) filter_var($file, FILTER_VALIDATE_URL);
  186. }
  187. /**
  188. * Progress normalized for cURL and Fopen
  189. * Accepts a variable length of arguments passed in by stream method
  190. */
  191. public static function progress()
  192. {
  193. static $filesize = null;
  194. $args = func_get_args();
  195. $isCurlResource = is_resource($args[0]) && get_resource_type($args[0]) === 'curl';
  196. $notification_code = !$isCurlResource ? $args[0] : false;
  197. $bytes_transferred = $isCurlResource ? $args[2] : $args[4];
  198. if ($isCurlResource) {
  199. $filesize = $args[1];
  200. } elseif ($notification_code == STREAM_NOTIFY_FILE_SIZE_IS) {
  201. $filesize = $args[5];
  202. }
  203. if ($bytes_transferred > 0) {
  204. if ($notification_code == STREAM_NOTIFY_PROGRESS | STREAM_NOTIFY_COMPLETED || $isCurlResource) {
  205. $progress = [
  206. 'code' => $notification_code,
  207. 'filesize' => $filesize,
  208. 'transferred' => $bytes_transferred,
  209. 'percent' => $filesize <= 0 ? '-' : round(($bytes_transferred * 100) / $filesize, 1)
  210. ];
  211. if (self::$callback !== null) {
  212. call_user_func(self::$callback, $progress);
  213. }
  214. }
  215. }
  216. }
  217. /**
  218. * Automatically picks the preferred method
  219. *
  220. * @return string The response of the request
  221. */
  222. private static function getAuto()
  223. {
  224. if (!ini_get('open_basedir') && self::isFopenAvailable()) {
  225. return self::getFopen(func_get_args());
  226. }
  227. if (self::isCurlAvailable()) {
  228. return self::getCurl(func_get_args());
  229. }
  230. return null;
  231. }
  232. /**
  233. * Starts a HTTP request via fopen
  234. *
  235. * @return string The response of the request
  236. */
  237. private static function getFopen()
  238. {
  239. if (\count($args = func_get_args()) === 1) {
  240. $args = $args[0];
  241. }
  242. $uri = $args[0];
  243. $options = $args[1] ?? [];
  244. $callback = $args[2] ?? null;
  245. if ($callback) {
  246. $options['fopen']['notification'] = ['self', 'progress'];
  247. }
  248. $options['fopen']['header'] = 'Referer: ' . Grav::instance()['uri']->rootUrl(true);
  249. if (isset($options['fopen']['ssl'])) {
  250. $ssl = $options['fopen']['ssl'];
  251. unset($options['fopen']['ssl']);
  252. $stream = stream_context_create([
  253. 'http' => $options['fopen'],
  254. 'ssl' => $ssl
  255. ], $options['fopen']);
  256. } else {
  257. $stream = stream_context_create(['http' => $options['fopen']], $options['fopen']);
  258. }
  259. $content = @file_get_contents($uri, false, $stream);
  260. if ($content === false) {
  261. $code = null;
  262. // Function file_get_contents() creates local variable $http_response_header, check it
  263. if (isset($http_response_header)) {
  264. $code = explode(' ', $http_response_header[0] ?? '')[1] ?? null;
  265. }
  266. switch ($code) {
  267. case '404':
  268. throw new \RuntimeException('Page not found');
  269. case '401':
  270. throw new \RuntimeException('Invalid LICENSE');
  271. default:
  272. throw new \RuntimeException("Error while trying to download (code: {$code}): {$uri}\n");
  273. }
  274. }
  275. return $content;
  276. }
  277. /**
  278. * Starts a HTTP request via cURL
  279. *
  280. * @return string The response of the request
  281. */
  282. private static function getCurl()
  283. {
  284. $args = func_get_args();
  285. $args = count($args) > 1 ? $args : array_shift($args);
  286. $uri = $args[0];
  287. $options = $args[1] ?? [];
  288. $callback = $args[2] ?? null;
  289. $ch = curl_init($uri);
  290. $response = static::curlExecFollow($ch, $options, $callback);
  291. $errno = curl_errno($ch);
  292. if ($errno) {
  293. $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  294. $error_message = curl_strerror($errno) . "\n" . curl_error($ch);
  295. switch ($code) {
  296. case '404':
  297. throw new \RuntimeException('Page not found');
  298. case '401':
  299. throw new \RuntimeException('Invalid LICENSE');
  300. default:
  301. throw new \RuntimeException("Error while trying to download (code: $code): $uri \nMessage: $error_message");
  302. }
  303. }
  304. curl_close($ch);
  305. return $response;
  306. }
  307. /**
  308. * @param resource $ch
  309. * @param array $options
  310. * @param bool $callback
  311. *
  312. * @return bool|mixed
  313. */
  314. private static function curlExecFollow($ch, $options, $callback)
  315. {
  316. curl_setopt_array($ch, [ CURLOPT_REFERER => Grav::instance()['uri']->rootUrl(true) ]);
  317. if ($callback) {
  318. curl_setopt_array(
  319. $ch,
  320. [
  321. CURLOPT_NOPROGRESS => false,
  322. CURLOPT_PROGRESSFUNCTION => ['self', 'progress']
  323. ]
  324. );
  325. }
  326. // no open_basedir set, we can proceed normally
  327. if (!ini_get('open_basedir')) {
  328. curl_setopt_array($ch, $options['curl']);
  329. return curl_exec($ch);
  330. }
  331. $max_redirects = $options['curl'][CURLOPT_MAXREDIRS] ?? 5;
  332. $options['curl'][CURLOPT_FOLLOWLOCATION] = false;
  333. // open_basedir set but no redirects to follow, we can disable followlocation and proceed normally
  334. curl_setopt_array($ch, $options['curl']);
  335. if ($max_redirects <= 0) {
  336. return curl_exec($ch);
  337. }
  338. $uri = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
  339. $rch = curl_copy_handle($ch);
  340. curl_setopt($rch, CURLOPT_HEADER, true);
  341. curl_setopt($rch, CURLOPT_NOBODY, true);
  342. curl_setopt($rch, CURLOPT_FORBID_REUSE, false);
  343. curl_setopt($rch, CURLOPT_RETURNTRANSFER, true);
  344. do {
  345. curl_setopt($rch, CURLOPT_URL, $uri);
  346. $header = curl_exec($rch);
  347. if (curl_errno($rch)) {
  348. $code = 0;
  349. } else {
  350. $code = (int)curl_getinfo($rch, CURLINFO_HTTP_CODE);
  351. if ($code === 301 || $code === 302 || $code === 303) {
  352. preg_match('/(?:^|\n)Location:(.*?)\n/i', $header, $matches);
  353. $uri = trim(array_pop($matches));
  354. } else {
  355. $code = 0;
  356. }
  357. }
  358. } while ($code && --$max_redirects);
  359. curl_close($rch);
  360. if (!$max_redirects) {
  361. if ($max_redirects === null) {
  362. trigger_error('Too many redirects. When following redirects, libcurl hit the maximum amount.', E_USER_WARNING);
  363. }
  364. return false;
  365. }
  366. curl_setopt($ch, CURLOPT_URL, $uri);
  367. return curl_exec($ch);
  368. }
  369. }