[ CURLOPT_USERAGENT => 'Grav GPM', CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_FAILONERROR => true, CURLOPT_TIMEOUT => 15, CURLOPT_HEADER => false, //CURLOPT_SSL_VERIFYPEER => true, // this is set in the constructor since it's a setting /** * Example of callback parameters from within your own class */ //CURLOPT_NOPROGRESS => false, //CURLOPT_PROGRESSFUNCTION => [$this, 'progress'] ], 'fopen' => [ 'method' => 'GET', 'user_agent' => 'Grav GPM', 'max_redirects' => 5, 'follow_location' => 1, 'timeout' => 15, /* // this is set in the constructor since it's a setting 'ssl' => [ 'verify_peer' => true, 'verify_peer_name' => true, ], */ /** * Example of callback parameters from within your own class */ //'notification' => [$this, 'progress'] ] ]; /** * Sets the preferred method to use for making HTTP calls. * * @param string $method Default is `auto` * * @return Response */ public static function setMethod($method = 'auto') { if (!\in_array($method, ['auto', 'curl', 'fopen'], true)) { $method = 'auto'; } self::$method = $method; return new self(); } /** * Makes a request to the URL by using the preferred method * * @param string $uri URL to call * @param array $options An array of parameters for both `curl` and `fopen` * @param callable $callback Either a function or callback in array notation * * @return string The response of the request */ public static function get($uri = '', $options = [], $callback = null) { if (!self::isCurlAvailable() && !self::isFopenAvailable()) { throw new \RuntimeException('Could not start an HTTP request. `allow_url_open` is disabled and `cURL` is not available'); } // check if this function is available, if so use it to stop any timeouts try { if (function_exists('set_time_limit') && !Utils::isFunctionDisabled('set_time_limit')) { set_time_limit(0); } } catch (\Exception $e) { } $config = Grav::instance()['config']; $overrides = []; // Override CA Bundle $caPathOrFile = \Composer\CaBundle\CaBundle::getSystemCaRootBundlePath(); if (is_dir($caPathOrFile) || (is_link($caPathOrFile) && is_dir(readlink($caPathOrFile)))) { $overrides['curl'][CURLOPT_CAPATH] = $caPathOrFile; $overrides['fopen']['ssl']['capath'] = $caPathOrFile; } else { $overrides['curl'][CURLOPT_CAINFO] = $caPathOrFile; $overrides['fopen']['ssl']['cafile'] = $caPathOrFile; } // SSL Verify Peer and Proxy Setting $settings = [ 'method' => $config->get('system.gpm.method', self::$method), 'verify_peer' => $config->get('system.gpm.verify_peer', true), // `system.proxy_url` is for fallback // introduced with 1.1.0-beta.1 probably safe to remove at some point 'proxy_url' => $config->get('system.gpm.proxy_url', $config->get('system.proxy_url', false)), ]; if (!$settings['verify_peer']) { $overrides = array_replace_recursive([], $overrides, [ 'curl' => [ CURLOPT_SSL_VERIFYPEER => $settings['verify_peer'] ], 'fopen' => [ 'ssl' => [ 'verify_peer' => $settings['verify_peer'], 'verify_peer_name' => $settings['verify_peer'], ] ] ]); } // Proxy Setting if ($settings['proxy_url']) { $proxy = parse_url($settings['proxy_url']); $fopen_proxy = ($proxy['scheme'] ?: 'http') . '://' . $proxy['host'] . (isset($proxy['port']) ? ':' . $proxy['port'] : ''); $overrides = array_replace_recursive([], $overrides, [ 'curl' => [ CURLOPT_PROXY => $proxy['host'], CURLOPT_PROXYTYPE => 'HTTP' ], 'fopen' => [ 'proxy' => $fopen_proxy, 'request_fulluri' => true ] ]); if (isset($proxy['port'])) { $overrides['curl'][CURLOPT_PROXYPORT] = $proxy['port']; } if (isset($proxy['user'], $proxy['pass'])) { $fopen_auth = $auth = base64_encode($proxy['user'] . ':' . $proxy['pass']); $overrides['curl'][CURLOPT_PROXYUSERPWD] = $proxy['user'] . ':' . $proxy['pass']; $overrides['fopen']['header'] = "Proxy-Authorization: Basic $fopen_auth"; } } $options = array_replace_recursive(self::$defaults, $options, $overrides); $method = 'get' . ucfirst(strtolower($settings['method'])); self::$callback = $callback; return static::$method($uri, $options, $callback); } /** * Checks if cURL is available * * @return bool */ public static function isCurlAvailable() { return function_exists('curl_version'); } /** * Checks if the remote fopen request is enabled in PHP * * @return bool */ public static function isFopenAvailable() { return preg_match('/1|yes|on|true/i', ini_get('allow_url_fopen')); } /** * Is this a remote file or not * * @param string $file * @return bool */ public static function isRemote($file) { return (bool) filter_var($file, FILTER_VALIDATE_URL); } /** * Progress normalized for cURL and Fopen * Accepts a variable length of arguments passed in by stream method */ public static function progress() { static $filesize = null; $args = func_get_args(); $isCurlResource = is_resource($args[0]) && get_resource_type($args[0]) === 'curl'; $notification_code = !$isCurlResource ? $args[0] : false; $bytes_transferred = $isCurlResource ? $args[2] : $args[4]; if ($isCurlResource) { $filesize = $args[1]; } elseif ($notification_code == STREAM_NOTIFY_FILE_SIZE_IS) { $filesize = $args[5]; } if ($bytes_transferred > 0) { if ($notification_code == STREAM_NOTIFY_PROGRESS | STREAM_NOTIFY_COMPLETED || $isCurlResource) { $progress = [ 'code' => $notification_code, 'filesize' => $filesize, 'transferred' => $bytes_transferred, 'percent' => $filesize <= 0 ? '-' : round(($bytes_transferred * 100) / $filesize, 1) ]; if (self::$callback !== null) { call_user_func(self::$callback, $progress); } } } } /** * Automatically picks the preferred method * * @return string The response of the request */ private static function getAuto() { if (!ini_get('open_basedir') && self::isFopenAvailable()) { return self::getFopen(func_get_args()); } if (self::isCurlAvailable()) { return self::getCurl(func_get_args()); } return null; } /** * Starts a HTTP request via fopen * * @return string The response of the request */ private static function getFopen() { if (\count($args = func_get_args()) === 1) { $args = $args[0]; } $uri = $args[0]; $options = $args[1] ?? []; $callback = $args[2] ?? null; if ($callback) { $options['fopen']['notification'] = ['self', 'progress']; } $options['fopen']['header'] = 'Referer: ' . Grav::instance()['uri']->rootUrl(true); if (isset($options['fopen']['ssl'])) { $ssl = $options['fopen']['ssl']; unset($options['fopen']['ssl']); $stream = stream_context_create([ 'http' => $options['fopen'], 'ssl' => $ssl ], $options['fopen']); } else { $stream = stream_context_create(['http' => $options['fopen']], $options['fopen']); } $content = @file_get_contents($uri, false, $stream); if ($content === false) { $code = null; // Function file_get_contents() creates local variable $http_response_header, check it if (isset($http_response_header)) { $code = explode(' ', $http_response_header[0] ?? '')[1] ?? null; } switch ($code) { case '404': throw new \RuntimeException('Page not found'); case '401': throw new \RuntimeException('Invalid LICENSE'); default: throw new \RuntimeException("Error while trying to download (code: {$code}): {$uri}\n"); } } return $content; } /** * Starts a HTTP request via cURL * * @return string The response of the request */ private static function getCurl() { $args = func_get_args(); $args = count($args) > 1 ? $args : array_shift($args); $uri = $args[0]; $options = $args[1] ?? []; $callback = $args[2] ?? null; $ch = curl_init($uri); $response = static::curlExecFollow($ch, $options, $callback); $errno = curl_errno($ch); if ($errno) { $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error_message = curl_strerror($errno) . "\n" . curl_error($ch); switch ($code) { case '404': throw new \RuntimeException('Page not found'); case '401': throw new \RuntimeException('Invalid LICENSE'); default: throw new \RuntimeException("Error while trying to download (code: $code): $uri \nMessage: $error_message"); } } curl_close($ch); return $response; } /** * @param resource $ch * @param array $options * @param bool $callback * * @return bool|mixed */ private static function curlExecFollow($ch, $options, $callback) { curl_setopt_array($ch, [ CURLOPT_REFERER => Grav::instance()['uri']->rootUrl(true) ]); if ($callback) { curl_setopt_array( $ch, [ CURLOPT_NOPROGRESS => false, CURLOPT_PROGRESSFUNCTION => ['self', 'progress'] ] ); } // no open_basedir set, we can proceed normally if (!ini_get('open_basedir')) { curl_setopt_array($ch, $options['curl']); return curl_exec($ch); } $max_redirects = $options['curl'][CURLOPT_MAXREDIRS] ?? 5; $options['curl'][CURLOPT_FOLLOWLOCATION] = false; // open_basedir set but no redirects to follow, we can disable followlocation and proceed normally curl_setopt_array($ch, $options['curl']); if ($max_redirects <= 0) { return curl_exec($ch); } $uri = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL); $rch = curl_copy_handle($ch); curl_setopt($rch, CURLOPT_HEADER, true); curl_setopt($rch, CURLOPT_NOBODY, true); curl_setopt($rch, CURLOPT_FORBID_REUSE, false); curl_setopt($rch, CURLOPT_RETURNTRANSFER, true); do { curl_setopt($rch, CURLOPT_URL, $uri); $header = curl_exec($rch); if (curl_errno($rch)) { $code = 0; } else { $code = (int)curl_getinfo($rch, CURLINFO_HTTP_CODE); if ($code === 301 || $code === 302 || $code === 303) { preg_match('/(?:^|\n)Location:(.*?)\n/i', $header, $matches); $uri = trim(array_pop($matches)); } else { $code = 0; } } } while ($code && --$max_redirects); curl_close($rch); if (!$max_redirects) { if ($max_redirects === null) { trigger_error('Too many redirects. When following redirects, libcurl hit the maximum amount.', E_USER_WARNING); } return false; } curl_setopt($ch, CURLOPT_URL, $uri); return curl_exec($ch); } }