279 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			279 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| /**
 | |
|  * @package   FOF
 | |
|  * @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
 | |
|  * @license   GNU General Public License version 3, or later
 | |
|  */
 | |
| 
 | |
| namespace FOF40\Download\Adapter;
 | |
| 
 | |
| defined('_JEXEC') || die;
 | |
| 
 | |
| use FOF40\Download\DownloadInterface;
 | |
| use FOF40\Download\Exception\DownloadError;
 | |
| use Joomla\CMS\Language\Text;
 | |
| 
 | |
| /**
 | |
|  * A download adapter using the cURL PHP integration
 | |
|  */
 | |
| class Curl extends AbstractAdapter implements DownloadInterface
 | |
| {
 | |
| 	protected $headers = [];
 | |
| 
 | |
| 	public function __construct()
 | |
| 	{
 | |
| 		$this->priority              = 110;
 | |
| 		$this->supportsFileSize      = true;
 | |
| 		$this->supportsChunkDownload = true;
 | |
| 		$this->name                  = 'curl';
 | |
| 		$this->isSupported           = function_exists('curl_init') && function_exists('curl_exec') && function_exists('curl_close');
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Download a part (or the whole) of a remote URL and return the downloaded
 | |
| 	 * data. You are supposed to check the size of the returned data. If it's
 | |
| 	 * smaller than what you expected you've reached end of file. If it's empty
 | |
| 	 * you have tried reading past EOF. If it's larger than what you expected
 | |
| 	 * the server doesn't support chunk downloads.
 | |
| 	 *
 | |
| 	 * If this class' supportsChunkDownload returns false you should assume
 | |
| 	 * that the $from and $to parameters will be ignored.
 | |
| 	 *
 | |
| 	 * @param   string   $url     The remote file's URL
 | |
| 	 * @param   integer  $from    Byte range to start downloading from. Use null for start of file.
 | |
| 	 * @param   integer  $to      Byte range to stop downloading. Use null to download the entire file ($from is
 | |
| 	 *                            ignored)
 | |
| 	 * @param   array    $params  Additional params that will be added before performing the download
 | |
| 	 *
 | |
| 	 * @return  string  The raw file data retrieved from the remote URL.
 | |
| 	 *
 | |
| 	 * @throws  DownloadError  A generic exception is thrown on error
 | |
| 	 */
 | |
| 	public function downloadAndReturn(string $url, ?int $from = null, ?int $to = null, array $params = []): string
 | |
| 	{
 | |
| 		$ch = curl_init();
 | |
| 
 | |
| 		if (empty($from))
 | |
| 		{
 | |
| 			$from = 0;
 | |
| 		}
 | |
| 
 | |
| 		if (empty($to))
 | |
| 		{
 | |
| 			$to = 0;
 | |
| 		}
 | |
| 
 | |
| 		if ($to < $from)
 | |
| 		{
 | |
| 			$temp = $to;
 | |
| 			$to   = $from;
 | |
| 			$from = $temp;
 | |
| 			unset($temp);
 | |
| 		}
 | |
| 
 | |
| 		$caCertPath = class_exists('\\Composer\\CaBundle\\CaBundle')
 | |
| 			? \Composer\CaBundle\CaBundle::getBundledCaBundlePath()
 | |
| 			: JPATH_LIBRARIES . '/src/Http/Transport/cacert.pem';
 | |
| 
 | |
| 		curl_setopt($ch, CURLOPT_URL, $url);
 | |
| 		curl_setopt($ch, CURLOPT_AUTOREFERER, 1);
 | |
| 		curl_setopt($ch, CURLOPT_BINARYTRANSFER, 1);
 | |
| 		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
 | |
| 		@curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
 | |
| 		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
 | |
| 		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
 | |
| 		curl_setopt($ch, CURLOPT_SSLVERSION, 0);
 | |
| 		curl_setopt($ch, CURLOPT_CAINFO, $caCertPath);
 | |
| 		curl_setopt($ch, CURLOPT_HEADERFUNCTION, [$this, 'reponseHeaderCallback']);
 | |
| 
 | |
| 		if (!(empty($from) && empty($to)))
 | |
| 		{
 | |
| 			curl_setopt($ch, CURLOPT_RANGE, "$from-$to");
 | |
| 		}
 | |
| 
 | |
| 		if (!is_array($params))
 | |
| 		{
 | |
| 			$params = [];
 | |
| 		}
 | |
| 
 | |
| 		$patched_accept_encoding = false;
 | |
| 
 | |
| 		// Work around LiteSpeed sending compressed output under HTTP/2 when no encoding was requested
 | |
| 		// See https://github.com/joomla/joomla-cms/issues/21423#issuecomment-410941000
 | |
| 		if (defined('CURLOPT_ACCEPT_ENCODING'))
 | |
| 		{
 | |
| 			if (!array_key_exists(CURLOPT_ACCEPT_ENCODING, $params))
 | |
| 			{
 | |
| 				$params[CURLOPT_ACCEPT_ENCODING] = 'identity';
 | |
| 			}
 | |
| 
 | |
| 			$patched_accept_encoding = true;
 | |
| 		}
 | |
| 
 | |
| 		foreach ($params as $k => $v)
 | |
| 		{
 | |
| 			// I couldn't patch the accept encoding header (missing constant), so I'll check if we manually set it
 | |
| 			if (!$patched_accept_encoding && $k == CURLOPT_HTTPHEADER)
 | |
| 			{
 | |
| 				foreach ($v as $custom_header)
 | |
| 				{
 | |
| 					// Ok, we explicitly set the Accept-Encoding header, so we consider it patched
 | |
| 					if (stripos($custom_header, 'Accept-Encoding') !== false)
 | |
| 					{
 | |
| 						$patched_accept_encoding = true;
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			@curl_setopt($ch, $k, $v);
 | |
| 		}
 | |
| 
 | |
| 		// Accept encoding wasn't patched, let's manually do that
 | |
| 		if (!$patched_accept_encoding)
 | |
| 		{
 | |
| 			@curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept-Encoding: identity']);
 | |
| 
 | |
| 			$patched_accept_encoding = true;
 | |
| 		}
 | |
| 
 | |
| 		$result = curl_exec($ch);
 | |
| 
 | |
| 		$errno       = curl_errno($ch);
 | |
| 		$errmsg      = curl_error($ch);
 | |
| 		$error       = '';
 | |
| 		$http_status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
 | |
| 
 | |
| 		if ($result === false)
 | |
| 		{
 | |
| 			$error = Text::sprintf('LIB_FOF40_DOWNLOAD_ERR_CURL_ERROR', $errno, $errmsg);
 | |
| 		}
 | |
| 		elseif (($http_status >= 300) && ($http_status <= 399) && isset($this->headers['location']) && !empty($this->headers['location']))
 | |
| 		{
 | |
| 			return $this->downloadAndReturn($this->headers['location'], $from, $to, $params);
 | |
| 		}
 | |
| 		elseif ($http_status > 399)
 | |
| 		{
 | |
| 			$result = false;
 | |
| 			$errno  = $http_status;
 | |
| 			$error  = Text::sprintf('LIB_FOF40_DOWNLOAD_ERR_HTTPERROR', $http_status);
 | |
| 		}
 | |
| 
 | |
| 		curl_close($ch);
 | |
| 
 | |
| 		if ($result === false)
 | |
| 		{
 | |
| 			throw new DownloadError($error, $errno);
 | |
| 		}
 | |
| 		else
 | |
| 		{
 | |
| 			return $result;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Get the size of a remote file in bytes
 | |
| 	 *
 | |
| 	 * @param   string  $url  The remote file's URL
 | |
| 	 *
 | |
| 	 * @return  integer  The file size, or -1 if the remote server doesn't support this feature
 | |
| 	 */
 | |
| 	public function getFileSize(string $url): int
 | |
| 	{
 | |
| 		$result = -1;
 | |
| 
 | |
| 		$ch = curl_init();
 | |
| 
 | |
| 		curl_setopt($ch, CURLOPT_AUTOREFERER, 1);
 | |
| 		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
 | |
| 		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
 | |
| 		curl_setopt($ch, CURLOPT_SSLVERSION, 0);
 | |
| 
 | |
| 		$caCertPath = class_exists('\\Composer\\CaBundle\\CaBundle')
 | |
| 			? \Composer\CaBundle\CaBundle::getBundledCaBundlePath()
 | |
| 			: JPATH_LIBRARIES . '/src/Http/Transport/cacert.pem';;
 | |
| 
 | |
| 		curl_setopt($ch, CURLOPT_URL, $url);
 | |
| 		curl_setopt($ch, CURLOPT_NOBODY, true);
 | |
| 		curl_setopt($ch, CURLOPT_HEADER, true);
 | |
| 		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
 | |
| 		@curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
 | |
| 		curl_setopt($ch, CURLOPT_CAINFO, $caCertPath);
 | |
| 
 | |
| 		$data = curl_exec($ch);
 | |
| 		curl_close($ch);
 | |
| 
 | |
| 		if ($data)
 | |
| 		{
 | |
| 			$content_length = "unknown";
 | |
| 			$status         = "unknown";
 | |
| 			$redirection    = null;
 | |
| 
 | |
| 			if (preg_match("/^HTTP\/1\.[01] (\d\d\d)/i", $data, $matches))
 | |
| 			{
 | |
| 				$status = (int) $matches[1];
 | |
| 			}
 | |
| 
 | |
| 			if (preg_match("/Content-Length: (\d+)/i", $data, $matches))
 | |
| 			{
 | |
| 				$content_length = (int) $matches[1];
 | |
| 			}
 | |
| 
 | |
| 			if (preg_match("/Location: (.*)/i", $data, $matches))
 | |
| 			{
 | |
| 				$redirection = (int) $matches[1];
 | |
| 			}
 | |
| 
 | |
| 			if ($status == 200 || ($status > 300 && $status <= 308))
 | |
| 			{
 | |
| 				$result = $content_length;
 | |
| 			}
 | |
| 
 | |
| 			if (($status > 300) && ($status <= 308))
 | |
| 			{
 | |
| 				if (!empty($redirection))
 | |
| 				{
 | |
| 					return $this->getFileSize($redirection);
 | |
| 				}
 | |
| 
 | |
| 				return -1;
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		return (int) $result;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Handles the HTTP headers returned by cURL
 | |
| 	 *
 | |
| 	 * @param   resource  $ch    cURL resource handle (unused)
 | |
| 	 * @param   string    $data  Each header line, as returned by the server
 | |
| 	 *
 | |
| 	 * @return  int  The length of the $data string
 | |
| 	 */
 | |
| 	protected function reponseHeaderCallback($ch, string $data): int
 | |
| 	{
 | |
| 		$strlen = strlen($data);
 | |
| 
 | |
| 		if (($strlen) <= 2)
 | |
| 		{
 | |
| 			return $strlen;
 | |
| 		}
 | |
| 
 | |
| 		if (substr($data, 0, 4) == 'HTTP')
 | |
| 		{
 | |
| 			return $strlen;
 | |
| 		}
 | |
| 
 | |
| 		if (strpos($data, ':') === false)
 | |
| 		{
 | |
| 			return $strlen;
 | |
| 		}
 | |
| 
 | |
| 		[$header, $value] = explode(': ', trim($data), 2);
 | |
| 
 | |
| 		$this->headers[strtolower($header)] = $value;
 | |
| 
 | |
| 		return $strlen;
 | |
| 	}
 | |
| }
 |